From 8ecf715f7914bb2d314eb36296e9a5e5c0df5b74 Mon Sep 17 00:00:00 2001 From: hejl Date: Wed, 21 May 2025 17:56:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=95=B0=E6=8D=AE=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E8=8F=9C=E5=8D=95=E5=8F=8A=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=8C=E9=87=8D=E5=91=BD=E5=90=8Dtemplate?= =?UTF-8?q?=5Fnotifier=E4=B8=BAsafe=5Fnotifier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/framework/widgets/tab_view.dart | 2 +- win_text_editor/lib/menus/app_menu.dart | 3 +- win_text_editor/lib/menus/menu_actions.dart | 28 ++- win_text_editor/lib/menus/menu_constants.dart | 1 + .../controllers/data_format_controller.dart | 34 +++ .../controllers/grid_view_controller.dart | 53 +++++ .../data_format/models/template_node.dart | 64 ++++++ .../data_format/widgets/data_format_view.dart | 55 +++++ .../data_format/widgets/grid_view.dart | 215 ++++++++++++++++++ .../lib/modules/module_router.dart | 1 + .../controllers/filter_controller.dart | 4 +- .../controllers/grid_view_controller.dart | 4 +- .../controllers/tree_view_controller.dart | 4 +- .../template_parser/widgets/grid_view.dart | 4 +- .../widgets/template_parser_view.dart | 2 +- .../base/safe_notifier.dart} | 2 +- 16 files changed, 462 insertions(+), 14 deletions(-) create mode 100644 win_text_editor/lib/modules/data_format/controllers/data_format_controller.dart create mode 100644 win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart create mode 100644 win_text_editor/lib/modules/data_format/models/template_node.dart create mode 100644 win_text_editor/lib/modules/data_format/widgets/data_format_view.dart create mode 100644 win_text_editor/lib/modules/data_format/widgets/grid_view.dart rename win_text_editor/lib/{modules/template_parser/controllers/template_notifier.dart => shared/base/safe_notifier.dart} (72%) diff --git a/win_text_editor/lib/framework/widgets/tab_view.dart b/win_text_editor/lib/framework/widgets/tab_view.dart index 7f9f95f..f004cd7 100644 --- a/win_text_editor/lib/framework/widgets/tab_view.dart +++ b/win_text_editor/lib/framework/widgets/tab_view.dart @@ -42,7 +42,7 @@ class _TabViewState extends State { Widget _buildTabContent() { final tabManager = Provider.of(context, listen: false); final activeIndex = widget.tabs.indexWhere((t) => t.id == widget.currentTabId); - if (activeIndex == -1) return const Center(child: Text('无活动标签页')); + if (activeIndex == -1) return const Center(child: Text('欢迎光临,敬请指导!')); return IndexedStack( index: activeIndex, diff --git a/win_text_editor/lib/menus/app_menu.dart b/win_text_editor/lib/menus/app_menu.dart index 024776c..09480bb 100644 --- a/win_text_editor/lib/menus/app_menu.dart +++ b/win_text_editor/lib/menus/app_menu.dart @@ -25,8 +25,9 @@ class AppMenu extends StatelessWidget { List> _buildToolsMenuItems() { return [ + const PopupMenuItem(value: MenuConstants.templateParser, child: Text('XML解析')), const PopupMenuItem(value: MenuConstants.contentSearch, child: Text('内容搜索')), - const PopupMenuItem(value: MenuConstants.templateParser, child: Text('模板解析')), + const PopupMenuItem(value: MenuConstants.dataFormat, child: Text('格式化')), ]; } diff --git a/win_text_editor/lib/menus/menu_actions.dart b/win_text_editor/lib/menus/menu_actions.dart index f862473..e887bbd 100644 --- a/win_text_editor/lib/menus/menu_actions.dart +++ b/win_text_editor/lib/menus/menu_actions.dart @@ -21,6 +21,9 @@ class MenuActions { case MenuConstants.templateParser: await _openTemplateParser(context); break; + case MenuConstants.dataFormat: + await _dataFormat(context); + break; case MenuConstants.exit: _exitApplication(); break; @@ -67,7 +70,7 @@ class MenuActions { final tabId = DateTime.now().millisecondsSinceEpoch.toString(); await tabManager.addTab( tabId, - title: "模板解析", + title: "XML解析", type: RouterKey.templateParser, icon: Icons.auto_awesome_mosaic, content: "", @@ -75,6 +78,29 @@ class MenuActions { } } + static Future _dataFormat(BuildContext context) async { + final tabManager = Provider.of(context, listen: false); + + // 使用 firstWhereOrNull 查找选项卡 + final existingTab = tabManager.tabs.firstWhereOrNull( + (tab) => tab.type == RouterKey.templateParser, + ); + + if (existingTab != null) { + // 如果存在,激活该选项卡 + tabManager.setActiveTab(existingTab.id); + } else { + final tabId = DateTime.now().millisecondsSinceEpoch.toString(); + await tabManager.addTab( + tabId, + title: "数据格式化", + type: RouterKey.dataFormat, + icon: Icons.auto_awesome_mosaic, + content: "", + ); + } + } + static void _exitApplication() { exit(0); } diff --git a/win_text_editor/lib/menus/menu_constants.dart b/win_text_editor/lib/menus/menu_constants.dart index f937408..a2ef800 100644 --- a/win_text_editor/lib/menus/menu_constants.dart +++ b/win_text_editor/lib/menus/menu_constants.dart @@ -15,6 +15,7 @@ class MenuConstants { // 工具菜单项 static const String contentSearch = "content_search"; static const String templateParser = 'template_parser'; + static const String dataFormat = 'data_format'; // 编辑菜单项 static const String undo = 'undo'; diff --git a/win_text_editor/lib/modules/data_format/controllers/data_format_controller.dart b/win_text_editor/lib/modules/data_format/controllers/data_format_controller.dart new file mode 100644 index 0000000..b4073fa --- /dev/null +++ b/win_text_editor/lib/modules/data_format/controllers/data_format_controller.dart @@ -0,0 +1,34 @@ +import 'package:win_text_editor/shared/base/base_content_controller.dart'; +import 'grid_view_controller.dart'; + +class DataFormatController extends BaseContentController { + final GridViewController gridController; + + //---------------初始化方法---- + + DataFormatController() : gridController = GridViewController() { + _setupCrossControllerCommunication(); + } + + //设置跨控制器状态协同 + void _setupCrossControllerCommunication() {} + + //----------------业务入口方法----- + + //--------------------私有方法--------- + + //-----------框架回调-- + @override + void onOpenFile(String filePath) {} + + @override + void onOpenFolder(String folderPath) { + // 不支持打开文件夹 + } + + @override + void dispose() { + gridController.dispose(); + super.dispose(); + } +} diff --git a/win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart b/win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart new file mode 100644 index 0000000..982e5c5 --- /dev/null +++ b/win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart @@ -0,0 +1,53 @@ +// grid_view_controller.dart +import 'package:win_text_editor/shared/base/safe_notifier.dart'; +import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; + +class GridViewController extends SafeNotifier { + List _templateItems = []; + List _filteredItems = []; + bool _isFilterApplied = false; + + List get displayedItems => _isFilterApplied ? _filteredItems : _templateItems; + bool get isFilterApplied => _isFilterApplied; + List get templateItems => _templateItems; + + // 新增方法:更新节点引用 + List? _currentTreeNodes; + void updateTreeNodesRef(List nodes) { + _currentTreeNodes = nodes; + safeNotify(); + } + + List getSelectedNodes() { + if (_currentTreeNodes == null) return []; + + List selectedNodes = []; + void traverse(TemplateNode node) { + if (node.isChecked) selectedNodes.add(node); + for (var child in node.children) { + traverse(child); + } + } + + for (var node in _currentTreeNodes!) { + traverse(node); + } + return selectedNodes; + } + + void updateTemplateItems(List items) { + _templateItems = items; + safeNotify(); + } + + void applyFilter(List filteredItems) { + _filteredItems = filteredItems; + _isFilterApplied = true; + safeNotify(); + } + + void clearFilter() { + _isFilterApplied = false; + safeNotify(); + } +} diff --git a/win_text_editor/lib/modules/data_format/models/template_node.dart b/win_text_editor/lib/modules/data_format/models/template_node.dart new file mode 100644 index 0000000..b10d52d --- /dev/null +++ b/win_text_editor/lib/modules/data_format/models/template_node.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:win_text_editor/shared/components/tree_view.dart'; + +class TemplateNode implements TreeNode { + @override + final String name; + @override + final List children; + @override + final int depth; + @override + bool isExpanded; + + final String path; + bool isRepeated; + bool isAttribute; + int repreatCount; + bool isChecked; // 新增属性,用于记录节点是否被选中 + + TemplateNode({ + required this.name, + required this.children, + required this.depth, + required this.path, + this.isExpanded = false, + this.isRepeated = false, + this.isAttribute = false, + this.repreatCount = 1, + this.isChecked = false, // 初始化默认未选中 + }); + + @override + bool get isDirectory => children.isNotEmpty; + + @override + IconData? get iconData => isAttribute ? Icons.code : Icons.label_outline; + + @override + String get id => path; +} + +enum NodeType { element, attribute, text } + +class TemplateItem { + final int id; + final String rowId; + final String content; + final String xPath; + final String value; + final NodeType nodeType; + + TemplateItem({ + required this.id, + required this.rowId, + required this.content, + required this.xPath, + required this.value, + required this.nodeType, + }); + + bool matchesPath(String path) { + return xPath == path; + } +} 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 new file mode 100644 index 0000000..78b350a --- /dev/null +++ b/win_text_editor/lib/modules/data_format/widgets/data_format_view.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; +import 'package:win_text_editor/modules/data_format/controllers/data_format_controller.dart'; +import 'package:win_text_editor/modules/data_format/widgets/grid_view.dart'; + +class DataFormatView extends StatefulWidget { + final String tabId; + const DataFormatView({super.key, required this.tabId}); + + @override + State createState() => _DataFormatViewState(); +} + +class _DataFormatViewState extends State { + late final DataFormatController _controller; + + get tabManager => Provider.of(context, listen: false); + + @override + void initState() { + super.initState(); + _controller = tabManager.getController(widget.tabId) ?? DataFormatController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _controller), + ChangeNotifierProvider.value(value: _controller.gridController), + ], + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column(children: [const SizedBox(height: 8), Expanded(child: _buildMainContent())]), + ), + ); + } + + Widget _buildMainContent() { + return Consumer( + builder: (context, controller, _) { + return const Row( + children: [SizedBox(width: 8), Expanded(child: Card(child: DataGridView()))], + ); + }, + ); + } +} diff --git a/win_text_editor/lib/modules/data_format/widgets/grid_view.dart b/win_text_editor/lib/modules/data_format/widgets/grid_view.dart new file mode 100644 index 0000000..e79c575 --- /dev/null +++ b/win_text_editor/lib/modules/data_format/widgets/grid_view.dart @@ -0,0 +1,215 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:win_text_editor/modules/template_parser/controllers/grid_view_controller.dart'; +import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; +import 'package:file_picker/file_picker.dart'; +import 'dart:io'; + +class DataGridView extends StatelessWidget { + const DataGridView({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, controller, _) { + return GestureDetector( + onSecondaryTapDown: (details) { + _showContextMenu(context, details.globalPosition, controller); + }, + child: _buildGridView(controller), + ); + }, + ); + } + + Future _showContextMenu( + BuildContext context, + Offset position, + GridViewController controller, + ) async { + final renderBox = context.findRenderObject() as RenderBox; + final localPosition = renderBox.globalToLocal(position); + + final result = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy, + position.dx + renderBox.size.width - localPosition.dx, + position.dy + renderBox.size.height - localPosition.dy, + ), + items: [const PopupMenuItem(value: 'export', child: Text('导出(csv)'))], + ); + + if (result == 'export' && context.mounted) { + try { + await _exportToCsv(controller); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); + } + } + } + } + + Future _exportToCsv(GridViewController controller) async { + final selectedNodes = controller.getSelectedNodes(); + if (selectedNodes.isEmpty) return; + + // 直接从数据源获取数据 + final dataSource = _TemplateItemDataSource( + rows: _buildDataRows(selectedNodes, controller.displayedItems), + selectedNodes: selectedNodes, + ); + + // 构建表头 + String csvData = '序号\t'; + csvData += selectedNodes + .map((node) => node.isAttribute ? node.name.substring(1) : node.name) + .join('\t'); + csvData += '\n'; + + // 填充数据 + for (final row in dataSource.rows) { + csvData += '${row.getCells()[0].value}\t'; // 序号 + for (int i = 1; i < row.getCells().length; i++) { + csvData += row.getCells()[i].value.toString(); + if (i < row.getCells().length - 1) csvData += '\t'; + } + csvData += '\n'; + } + + // 保存文件 + final filePath = await FilePicker.platform.saveFile( + dialogTitle: '保存导出结果', + fileName: 'template_results.csv', + type: FileType.custom, + allowedExtensions: ['csv'], + ); + + if (filePath != null) { + await File(filePath).writeAsString(csvData); + } + } + + Widget _buildGridView(GridViewController controller) { + final selectedNodes = controller.getSelectedNodes(); + + if (selectedNodes.isEmpty) { + return const Center(child: Text('请在左侧树中选择要显示的节点(勾选复选框)')); + } + + // 获取所有需要显示的数据项 + final allItems = controller.displayedItems; + + // 构建数据行 - 每个父节点实例为一行 + final rows = _buildDataRows(selectedNodes, allItems); + + final dataSource = _TemplateItemDataSource(rows: rows, selectedNodes: selectedNodes); + + // 构建列 + final columns = [ + GridColumn( + columnName: 'index', + width: 60, + label: Container( + padding: const EdgeInsets.all(8.0), + color: Colors.grey[200], + alignment: Alignment.center, + child: const Text('序号'), + ), + ), + ...selectedNodes.map((node) { + return GridColumn( + columnName: node.path, + label: Container( + padding: const EdgeInsets.all(8.0), + alignment: Alignment.center, + color: Colors.grey[200], + child: Text(node.isAttribute ? node.name.substring(1) : node.name), + ), + ); + }).toList(), + ]; + + return SfDataGrid( + source: dataSource, + columns: columns, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + columnWidthMode: ColumnWidthMode.fill, + ); + } + + List> _buildDataRows( + List selectedNodes, + List allItems, + ) { + final instanceMap = >{}; + + // 1. 先按实例分组 + for (final item in allItems) { + // 2. 只填充选中的列 + if (selectedNodes.any((n) => n.path == item.xPath)) { + final instanceId = item.rowId; // 或使用其他分组逻辑 + instanceMap.putIfAbsent(instanceId, () => {'_index': instanceMap.length + 1}); + instanceMap[instanceId]![item.xPath] = item.value; + } + } + + // 3. 确保所有选中列都存在 + return instanceMap.values.map((row) { + for (final node in selectedNodes) { + row.putIfAbsent(node.path, () => row[node.path] ?? ''); + } + return row; + }).toList(); + } +} + +class _TemplateItemDataSource extends DataGridSource { + final List> _rows; + final List selectedNodes; + + _TemplateItemDataSource({required List> rows, required this.selectedNodes}) + : _rows = rows; + + @override + List get rows { + // print("[DEBUG] 原始可加载记录数:${_rows.length}"); + return _rows.asMap().entries.map((entry) { + final index = entry.key; + final rowData = entry.value; + + return DataGridRow( + cells: [ + DataGridCell(columnName: 'index', value: index + 1), + ...selectedNodes.map((node) { + return DataGridCell( + columnName: node.path, + value: rowData[node.path]?.toString() ?? '', + ); + }).toList(), + ], + ); + }).toList(); + } + + @override + DataGridRowAdapter? buildRow(DataGridRow row) { + return DataGridRowAdapter( + cells: + row.getCells().map((dataGridCell) { + return Container( + padding: const EdgeInsets.all(8.0), + alignment: + dataGridCell.columnName == 'index' ? Alignment.center : Alignment.centerLeft, + child: Text(dataGridCell.value.toString()), + ); + }).toList(), + ); + } +} diff --git a/win_text_editor/lib/modules/module_router.dart b/win_text_editor/lib/modules/module_router.dart index 004deec..078bd83 100644 --- a/win_text_editor/lib/modules/module_router.dart +++ b/win_text_editor/lib/modules/module_router.dart @@ -10,6 +10,7 @@ import 'package:win_text_editor/modules/template_parser/widgets/template_parser_ class RouterKey { static const String contentSearch = 'content_search'; static const String templateParser = 'template_parser'; + static const String dataFormat = 'data_format'; static const String textEditor = 'text_editor'; } diff --git a/win_text_editor/lib/modules/template_parser/controllers/filter_controller.dart b/win_text_editor/lib/modules/template_parser/controllers/filter_controller.dart index d47209c..f4dc860 100644 --- a/win_text_editor/lib/modules/template_parser/controllers/filter_controller.dart +++ b/win_text_editor/lib/modules/template_parser/controllers/filter_controller.dart @@ -2,9 +2,9 @@ import 'package:flutter/foundation.dart'; import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; -import 'template_notifier.dart'; +import '../../../shared/base/safe_notifier.dart'; -class FilterController extends TemplateNotifier { +class FilterController extends SafeNotifier { String? _selectedFilterField; String? _selectedFilterOperator; String _filterValue = ''; diff --git a/win_text_editor/lib/modules/template_parser/controllers/grid_view_controller.dart b/win_text_editor/lib/modules/template_parser/controllers/grid_view_controller.dart index ffd1b55..2c1beef 100644 --- a/win_text_editor/lib/modules/template_parser/controllers/grid_view_controller.dart +++ b/win_text_editor/lib/modules/template_parser/controllers/grid_view_controller.dart @@ -1,8 +1,8 @@ // grid_view_controller.dart import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; -import 'template_notifier.dart'; +import '../../../shared/base/safe_notifier.dart'; -class GridViewController extends TemplateNotifier { +class GridViewController extends SafeNotifier { List _templateItems = []; List _filteredItems = []; bool _isFilterApplied = false; diff --git a/win_text_editor/lib/modules/template_parser/controllers/tree_view_controller.dart b/win_text_editor/lib/modules/template_parser/controllers/tree_view_controller.dart index 8ddbf8f..e18ca83 100644 --- a/win_text_editor/lib/modules/template_parser/controllers/tree_view_controller.dart +++ b/win_text_editor/lib/modules/template_parser/controllers/tree_view_controller.dart @@ -1,9 +1,9 @@ // tree_view_controller.dart import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; -import 'template_notifier.dart'; +import '../../../shared/base/safe_notifier.dart'; -class TreeViewController extends TemplateNotifier { +class TreeViewController extends SafeNotifier { //根节点 List _treeNodes = []; TemplateNode? _selectedNode; diff --git a/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart b/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart index 701bbf3..ab34705 100644 --- a/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart +++ b/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart @@ -7,9 +7,7 @@ import 'package:file_picker/file_picker.dart'; import 'dart:io'; class TemplateGridView extends StatelessWidget { - final GridViewController controller; - - const TemplateGridView({super.key, required this.controller}); + const TemplateGridView({super.key}); @override Widget build(BuildContext context) { 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 6dbb8e2..3daf767 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 @@ -99,7 +99,7 @@ class _TemplateParserViewState extends State { ), ), const SizedBox(width: 8), - Expanded(child: Card(child: TemplateGridView(controller: controller.gridController))), + const Expanded(child: Card(child: TemplateGridView())), ], ); }, diff --git a/win_text_editor/lib/modules/template_parser/controllers/template_notifier.dart b/win_text_editor/lib/shared/base/safe_notifier.dart similarity index 72% rename from win_text_editor/lib/modules/template_parser/controllers/template_notifier.dart rename to win_text_editor/lib/shared/base/safe_notifier.dart index 9884331..b3544fc 100644 --- a/win_text_editor/lib/modules/template_parser/controllers/template_notifier.dart +++ b/win_text_editor/lib/shared/base/safe_notifier.dart @@ -1,7 +1,7 @@ // template_notifier.dart import 'package:flutter/foundation.dart'; -abstract class TemplateNotifier extends ChangeNotifier { +abstract class SafeNotifier extends ChangeNotifier { @protected void safeNotify() { if (hasListeners) notifyListeners();