diff --git a/win_text_editor/lib/framework/controllers/tab_items_controller.dart b/win_text_editor/lib/framework/controllers/tab_items_controller.dart index a02f493..909c139 100644 --- a/win_text_editor/lib/framework/controllers/tab_items_controller.dart +++ b/win_text_editor/lib/framework/controllers/tab_items_controller.dart @@ -116,4 +116,8 @@ class TabItemsController with ChangeNotifier { } activeContentController?.onOpenFile(filePath); } + + bool hasController(String tabId) { + return _contentControllers.containsKey(tabId); + } } diff --git a/win_text_editor/lib/framework/widgets/tab_view.dart b/win_text_editor/lib/framework/widgets/tab_view.dart index f004cd7..044c4ef 100644 --- a/win_text_editor/lib/framework/widgets/tab_view.dart +++ b/win_text_editor/lib/framework/widgets/tab_view.dart @@ -55,7 +55,12 @@ class _TabViewState extends State { } Widget _buildTabItem(AppTab tab, ChangeNotifier? controller) { - return ModuleRouter.buildWidgetForTab(tab, controller); + return Container( + decoration: const BoxDecoration( + border: Border(top: BorderSide(color: Colors.lightBlue, width: 1)), + ), + child: ModuleRouter.buildWidgetForTab(tab, controller), + ); } } @@ -78,12 +83,7 @@ class _TabItem extends StatelessWidget { onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: isActive ? Colors.blue[100] : Colors.grey[200], - border: Border( - bottom: BorderSide(color: isActive ? Colors.blue : Colors.transparent, width: 2), - ), - ), + decoration: BoxDecoration(color: isActive ? Colors.blue[100] : Colors.grey[200]), child: Row( children: [ if (tab.icon != null) Icon(tab.icon, size: 16), diff --git a/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart b/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart index b313900..7bda91c 100644 --- a/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart +++ b/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart @@ -1,6 +1,80 @@ +import 'package:flutter/material.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; class DataCompareController extends BaseContentController { + List leftColumns = []; + List rightColumns = []; + List> leftData = []; + List> rightData = []; + late DataCompareDataSource dataSource; + + DataCompareController() { + dataSource = DataCompareDataSource(this); + } + + void importLeftTable(String csvContent) { + final lines = csvContent.split('\n'); + if (lines.isEmpty) return; + + // Parse headers (skip first column which is for serial number) + leftColumns = lines[0].split(',').skip(1).toList(); + + // Parse data + leftData = []; + for (int i = 1; i < lines.length; i++) { + if (lines[i].trim().isEmpty) continue; + final values = lines[i].split(','); + final row = {'serial': i.toString(), 'key': values[0]}; + for (int j = 0; j < leftColumns.length; j++) { + row[leftColumns[j]] = j + 1 < values.length ? values[j + 1] : ''; + } + leftData.add(row); + } + + _compareData(); + notifyListeners(); + } + + void importRightTable(String csvContent) { + final lines = csvContent.split('\n'); + if (lines.isEmpty) return; + + // Parse headers (skip first column which is for serial number) + rightColumns = lines[0].split(',').skip(1).toList(); + + // Parse data + rightData = []; + for (int i = 1; i < lines.length; i++) { + if (lines[i].trim().isEmpty) continue; + final values = lines[i].split(','); + final row = {'serial': i.toString(), 'key': values[0]}; + for (int j = 0; j < rightColumns.length; j++) { + row[rightColumns[j]] = j + 1 < values.length ? values[j + 1] : ''; + } + rightData.add(row); + } + + _compareData(); + notifyListeners(); + } + + void _compareData() { + // Implement comparison logic here + // This would update the comparison status for each row + dataSource.updateData(leftData, rightData); + } + + void exportLeftTable(String matchType) { + // Implement export logic based on matchType + // 'full_match', 'key_match', or 'no_match' + } + + void exportRightTable(String matchType) { + // Implement export logic based on matchType + // 'full_match', 'key_match', or 'no_match' + } + @override void onOpenFile(String filePath) { // TODO: implement onOpenFile @@ -11,3 +85,39 @@ class DataCompareController extends BaseContentController { // TODO: implement onOpenFolder } } + +class DataCompareDataSource extends DataGridSource { + final DataCompareController controller; + List _rows = []; + + DataCompareDataSource(this.controller) { + _rows = []; + } + + void updateData(List> leftData, List> rightData) { + _rows = []; + // Implement logic to combine left and right data into rows + // and determine comparison status + notifyListeners(); + } + + @override + List get rows => _rows; + + @override + DataGridRowAdapter? buildRow(DataGridRow row) { + return DataGridRowAdapter( + cells: + row.getCells().map((dataGridCell) { + return Container( + alignment: Alignment.center, + padding: EdgeInsets.all(8), + child: + dataGridCell.value.runtimeType == Icon + ? dataGridCell.value + : Text(dataGridCell.value.toString()), + ); + }).toList(), + ); + } +} diff --git a/win_text_editor/lib/modules/data_compare/widgets/data_compare_grid.dart b/win_text_editor/lib/modules/data_compare/widgets/data_compare_grid.dart new file mode 100644 index 0000000..6b4d00c --- /dev/null +++ b/win_text_editor/lib/modules/data_compare/widgets/data_compare_grid.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:win_text_editor/modules/data_compare/controllers/data_compare_controller.dart'; + +class DataCompareGrid extends StatelessWidget { + final DataCompareController controller; + + const DataCompareGrid({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.0), // 这里可以根据需求调整圆角的大小 + ), + child: SfDataGrid( + source: controller.dataSource, + columns: _buildColumns(), + stackedHeaderRows: _buildStackedHeaders(), + columnWidthMode: ColumnWidthMode.fill, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + ), + ); + } + + List _buildColumns() { + return [ + // 左表列 + GridColumn(columnName: 'left_serial', width: 60, label: _buildHeaderCell('序号')), + GridColumn(columnName: 'left_key', label: _buildHeaderCell('主键')), + ...controller.leftColumns.map( + (col) => GridColumn(columnName: 'left_$col', label: _buildHeaderCell(col)), + ), + // 对比列 + GridColumn( + columnName: 'comparison', + label: _buildHeaderCell('对比', color: Colors.purple[50]), + width: 80, + ), + // 右表列 + GridColumn(columnName: 'right_serial', width: 60, label: _buildHeaderCell('序号')), + GridColumn(columnName: 'right_key', label: _buildHeaderCell('主键')), + ...controller.rightColumns.map( + (col) => GridColumn(columnName: 'right_$col', label: _buildHeaderCell(col)), + ), + ]; + } + + List _buildStackedHeaders() { + return [ + // 第一行表头(左表和右表分组) + StackedHeaderRow( + cells: [ + StackedHeaderCell( + columnNames: [ + 'left_serial', + 'left_key', + ...controller.leftColumns.map((col) => 'left_$col'), + ], + child: _buildGroupHeader('左表', color: Colors.green[50]), + ), + // 对比列留空,将在第二行合并 + // StackedHeaderCell(columnNames: ["comparison"], child: Container()), + StackedHeaderCell( + columnNames: [ + 'right_serial', + 'right_key', + ...controller.rightColumns.map((col) => 'right_$col'), + ], + child: _buildGroupHeader('右表', color: Colors.blue[50]), + ), + ], + ), + ]; + } + + Container _buildHeaderCell(String text, {Color? color}) { + return Container( + alignment: Alignment.center, + color: color ?? Colors.grey[200], + child: Text(text, style: const TextStyle(fontWeight: FontWeight.bold)), + ); + } + + Container _buildGroupHeader(String text, {Color? color}) { + return Container( + alignment: Alignment.center, + color: color ?? Colors.purple[50], + child: Text(text, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + ); + } +} diff --git a/win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart b/win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart index b5fa5c0..dd6b454 100644 --- a/win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart +++ b/win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart @@ -1,7 +1,11 @@ +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; import 'package:win_text_editor/modules/data_compare/controllers/data_compare_controller.dart'; +import 'data_compare_grid.dart'; // 新增导入 class DataCompareView extends StatefulWidget { final String tabId; @@ -13,28 +17,203 @@ class DataCompareView extends StatefulWidget { class _DataCompareViewState extends State { late final DataCompareController _controller; + late final DataGridController _dataGridController; + bool _isControllerFromTabManager = false; get tabManager => Provider.of(context, listen: false); @override void initState() { super.initState(); - _controller = tabManager.getController(widget.tabId) ?? DataCompareController(); + _dataGridController = DataGridController(); + + final controllerFromManager = tabManager.getController(widget.tabId); + if (controllerFromManager != null) { + _controller = controllerFromManager; + _isControllerFromTabManager = true; + } else { + _controller = DataCompareController(); + _isControllerFromTabManager = false; + tabManager.registerController(widget.tabId, _controller); + } } @override void dispose() { - _controller.dispose(); + _dataGridController.dispose(); + + if (!_isControllerFromTabManager) { + _controller.dispose(); + } + super.dispose(); } + Future _importLeftTable() async { + try { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['csv'], + ); + + if (result != null) { + final file = result.files.single; + final content = await rootBundle.loadString(file.path!); + _controller.importLeftTable(content); + } + } catch (e) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to import left table: $e'))); + } + } + + Future _importRightTable() async { + try { + FilePickerResult? result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['csv'], + ); + + if (result != null) { + final file = result.files.single; + final content = await rootBundle.loadString(file.path!); + _controller.importRightTable(content); + } + } catch (e) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Failed to import right table: $e'))); + } + } + + // 修改后的导出按钮方法 + Widget _buildExportButton(bool isLeftTable) { + return ElevatedButton( + onPressed: () { + _showExportMenu(isLeftTable); + }, + style: ElevatedButton.styleFrom( + backgroundColor: isLeftTable ? Colors.green[50] : Colors.blue[50], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Row( + children: [ + const Icon(Icons.output, size: 18), + const SizedBox(width: 4), + Text('导出${isLeftTable ? '左表' : '右表'}'), + ], + ), + ); + } + + // 显示导出菜单 + void _showExportMenu(bool isLeftTable) { + final RenderBox button = context.findRenderObject() as RenderBox; + final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; + + final RelativeRect position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(Offset.zero, ancestor: overlay), // 按钮左上角 + button.localToGlobal(button.size.bottomRight(Offset.zero), ancestor: overlay), + ), + Offset.zero & overlay.size, + ); + + showMenu( + context: context, + position: position, + items: [ + const PopupMenuItem( + value: 'full_match', + child: Row( + children: [ + Icon(Icons.double_arrow, color: Colors.green), + SizedBox(width: 8), + Text('整行匹配'), + ], + ), + ), + const PopupMenuItem( + value: 'key_match', + child: Row( + children: [ + Icon(Icons.arrow_forward_ios, color: Colors.blue), + SizedBox(width: 8), + Text('主键匹配'), + ], + ), + ), + const PopupMenuItem( + value: 'no_match', + child: Row( + children: [Icon(Icons.close, color: Colors.red), SizedBox(width: 8), Text('不匹配')], + ), + ), + ], + ).then((value) { + if (value != null) { + if (isLeftTable) { + _controller.exportLeftTable(value); + } else { + _controller.exportRightTable(value); + } + } + }); + } + @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _controller, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Column(children: [Expanded(flex: 1, child: Text("空白"))]), + child: Consumer( + builder: (context, controller, child) { + return Padding( + padding: const EdgeInsets.only(left: 4.0, right: 4.0), + child: Column( + children: [ + Container( + height: 50, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + ElevatedButton( + onPressed: _importLeftTable, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green[50], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: const Text('导入左表(CSV)'), + ), + const SizedBox(width: 8), + _buildExportButton(true), // 使用新的导出按钮 + ], + ), + Row( + children: [ + ElevatedButton( + onPressed: _importRightTable, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue[50], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: const Text('导入右表(CSV)'), + ), + const SizedBox(width: 8), + _buildExportButton(false), // 使用新的导出按钮 + ], + ), + ], + ), + ), + Expanded(child: DataCompareGrid(controller: controller)), + ], + ), + ); + }, ), ); } diff --git a/win_text_editor/lib/modules/data_format/widgets/data_format_view.dart b/win_text_editor/lib/modules/data_format/widgets/data_format_view.dart index ec97bcf..6a116ca 100644 --- a/win_text_editor/lib/modules/data_format/widgets/data_format_view.dart +++ b/win_text_editor/lib/modules/data_format/widgets/data_format_view.dart @@ -16,17 +16,29 @@ class DataFormatView extends StatefulWidget { class _DataFormatViewState extends State { late final DataFormatController _controller; + bool _isControllerFromTabManager = false; + get tabManager => Provider.of(context, listen: false); @override void initState() { super.initState(); - _controller = tabManager.getController(widget.tabId) ?? DataFormatController(); + final controllerFromManager = tabManager.getController(widget.tabId); + if (controllerFromManager != null) { + _controller = controllerFromManager; + _isControllerFromTabManager = true; + } else { + _controller = DataFormatController(); + _isControllerFromTabManager = false; + tabManager.registerController(widget.tabId, _controller); + } } @override void dispose() { - _controller.dispose(); + if (!_isControllerFromTabManager) { + _controller.dispose(); + } super.dispose(); } diff --git a/win_text_editor/lib/modules/demo/widgets/demo_view.dart b/win_text_editor/lib/modules/demo/widgets/demo_view.dart index dc706cc..87582d7 100644 --- a/win_text_editor/lib/modules/demo/widgets/demo_view.dart +++ b/win_text_editor/lib/modules/demo/widgets/demo_view.dart @@ -13,18 +13,30 @@ class DemoView extends StatefulWidget { class _DemoViewState extends State { late final DemoController _controller; + bool _isControllerFromTabManager = false; get tabManager => Provider.of(context, listen: false); @override void initState() { super.initState(); - _controller = tabManager.getController(widget.tabId) ?? DemoController(); + + final controllerFromManager = tabManager.getController(widget.tabId); + if (controllerFromManager != null) { + _controller = controllerFromManager; + _isControllerFromTabManager = true; + } else { + _controller = DemoController(); + _isControllerFromTabManager = false; + tabManager.registerController(widget.tabId, _controller); + } } @override void dispose() { - _controller.dispose(); + if (!_isControllerFromTabManager) { + _controller.dispose(); + } super.dispose(); } diff --git a/win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart b/win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart index 3daf767..45a526a 100644 --- a/win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart +++ b/win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart @@ -17,17 +17,30 @@ class TemplateParserView extends StatefulWidget { class _TemplateParserViewState extends State { late final TemplateParserController _controller; + bool _isControllerFromTabManager = false; + get tabManager => Provider.of(context, listen: false); @override void initState() { super.initState(); - _controller = tabManager.getController(widget.tabId) ?? TemplateParserController(); + + final controllerFromManager = tabManager.getController(widget.tabId); + if (controllerFromManager != null) { + _controller = controllerFromManager; + _isControllerFromTabManager = true; + } else { + _controller = TemplateParserController(); + _isControllerFromTabManager = false; + tabManager.registerController(widget.tabId, _controller); + } } @override void dispose() { - _controller.dispose(); + if (!_isControllerFromTabManager) { + _controller.dispose(); + } super.dispose(); }