From 624622069512eb93692a56db90762ae0bf4cac5b Mon Sep 17 00:00:00 2001 From: hejl Date: Tue, 20 May 2025 16:27:39 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=89=8D=E5=A4=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template_parser_controller.dart | 56 +++- .../template_parser/models/template_node.dart | 30 ++- .../widgets/template_parser_view.dart | 242 +++++++++++++----- .../lib/shared/components/tree_view.dart | 2 +- 4 files changed, 249 insertions(+), 81 deletions(-) diff --git a/win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart b/win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart index 3a9982b..118db68 100644 --- a/win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart +++ b/win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart @@ -63,17 +63,61 @@ class TemplateParserController extends BaseContentController { Future _parseXmlContent(String xmlContent) async { final document = xml.XmlDocument.parse(xmlContent); Logger().debug('开始解析XML,根元素: ${document.rootElement.name}'); - _treeNodes = _buildTreeNodes(document.rootElement, depth: 0); - Logger().debug('解析完成,共生成 ${_treeNodes.length} 个顶级节点'); + _treeNodes = _buildTreeNodes(document.rootElement, document.rootElement.localName, depth: 0); + + // 新增:解析所有节点值到 templateItems + _templateItems = _parseAllNodeValues(document); + notifyListeners(); } + List _parseAllNodeValues(xml.XmlDocument document) { + final items = []; + int id = 0; + + // 递归遍历所有元素 + void traverse(xml.XmlElement element, String currentPath) { + // 1. 添加当前元素的所有属性值 + for (final attr in element.attributes) { + items.add(TemplateItem( + id: id++, + content: attr.value, + xPath: '$currentPath/@${attr.name.local}', + value: attr.value, + nodeType: NodeType.attribute, + )); + } + + // 2. 添加当前元素的文本内容(如果有) + final textNodes = element.children.whereType().where((t) => t.text.trim().isNotEmpty); + if (textNodes.isNotEmpty) { + items.add(TemplateItem( + id: id++, + content: textNodes.first.text, + xPath: currentPath, + value: textNodes.first.text, + nodeType: NodeType.text, + )); + } + + // 3. 递归处理子元素 + for (final child in element.childElements) { + traverse(child, '$currentPath/${child.name.local}'); + } + } + + traverse(document.rootElement, document.rootElement.localName); + return items; +} + List _buildTreeNodes( - xml.XmlElement element, { + xml.XmlElement element, + String path, { required int depth, int repreatCount = 1, }) { final node = TemplateNode( + path: path, name: element.name.local, children: [], depth: depth, @@ -87,6 +131,7 @@ class TemplateParserController extends BaseContentController { node.children.addAll( element.attributes.map( (attr) => TemplateNode( + path: '$path/@${attr.name.local}', name: '@${attr.name.local}', children: [], depth: depth + 1, @@ -107,13 +152,14 @@ class TemplateParserController extends BaseContentController { // 为每个唯一子元素创建节点 groupedChildren.forEach((name, elements) { + String path0 = '$path/${elements.first.name.local}'; if (elements.length == 1) { // 单一节点直接添加(包含其所有属性) - node.children.addAll(_buildTreeNodes(elements.first, depth: depth + 1)); + node.children.addAll(_buildTreeNodes(elements.first, path0, depth: depth + 1)); } else { // 多个相同节点需要合并 node.children.addAll( - _buildTreeNodes(elements.first, depth: depth + 1, repreatCount: elements.length), + _buildTreeNodes(elements.first, path0, depth: depth + 1, repreatCount: elements.length), ); } }); diff --git a/win_text_editor/lib/modules/template_parser/models/template_node.dart b/win_text_editor/lib/modules/template_parser/models/template_node.dart index 836af75..fa26d4d 100644 --- a/win_text_editor/lib/modules/template_parser/models/template_node.dart +++ b/win_text_editor/lib/modules/template_parser/models/template_node.dart @@ -5,6 +5,7 @@ class TemplateNode implements TreeNode { final String name; final List children; final int depth; + final String path; bool isExpanded; bool isRepeated; bool isAttribute; @@ -13,6 +14,7 @@ class TemplateNode implements TreeNode { TemplateNode({ required this.name, required this.children, + required this.path, required this.depth, this.isExpanded = false, this.isRepeated = false, @@ -20,30 +22,34 @@ class TemplateNode implements TreeNode { this.repreatCount = 1, }); - // 实现TreeNode接口 - @override - String get id => '$name-$depth'; - @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 content; final String xPath; final String value; + final NodeType nodeType; + + TemplateItem({ + required this.id, + required this.content, + required this.xPath, + required this.value, + required this.nodeType, + }); - TemplateItem({required this.id, required this.content, required this.xPath, required this.value}); - - bool matches(TemplateNode node) { - if (node.name.startsWith('@')) { - final attrName = node.name.substring(1); - return xPath.contains('@$attrName'); - } - return xPath.contains(node.name); + bool matchesPath(String path) { + return xPath == path; } } 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 7f2374a..a2af407 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 @@ -1,5 +1,9 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.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/template_parser/controllers/template_parser_controller.dart'; import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; @@ -82,102 +86,214 @@ class _TemplateParserViewState extends State { child: Card(child: _buildTreeView(controller.treeNodes)), ), const SizedBox(width: 8), - Expanded(child: Card(child: _buildGridView(controller.templateItems))), + Expanded( + child: Card( + child: GestureDetector( + onSecondaryTapDown: (details) { + _showContextMenu(context, details.globalPosition); + }, + child: _buildGridView(controller.templateItems), + ), + ), + ), ], ); }, ); } - Widget _buildTreeView(List nodes) { - if (nodes.isEmpty) { - return const Center(child: Text('No XML data available')); - } + Future _showContextMenu(BuildContext context, Offset position) async { + // 获取渲染对象以正确定位菜单 + final renderBox = context.findRenderObject() as RenderBox; + final localPosition = renderBox.globalToLocal(position); - return TreeView( - nodes: _processXmlNodes(nodes), - config: const TreeViewConfig( - showIcons: true, - singleSelect: true, - selectedColor: Colors.lightBlueAccent, - icons: {'element': Icons.label_outline, 'attribute': Icons.code}, + // 显示菜单并等待选择结果 + 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, ), - onNodeTap: (node) { - final templateNode = node as TemplateNode; - Provider.of(context, listen: false).selectTreeNode(templateNode); - }, - nodeBuilder: (context, node, isSelected, onTap) => _buildCustomNode(node), + items: [const PopupMenuItem(value: 'export', child: Text('导出(csv)'))], ); + + // 处理菜单选择结果 + if (result == 'export' && context.mounted) { + try { + await _exportToCsv(); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); + } + } + } } - List _processXmlNodes(List nodes) { - return nodes.cast(); + Future _exportToCsv() async { + String csvData = '序号\t内容\n'; + final items = _controller.templateItems; + for (var i = 0; i < items.length; i++) { + final item = items[i]; + csvData += '${i + 1}\t${item.value}\n'; + } + + final filePath = await FilePicker.platform.saveFile( + dialogTitle: '保存导出结果', + fileName: 'template_results.csv', + type: FileType.custom, + allowedExtensions: ['csv'], + ); + + if (filePath != null) { + final file = File(filePath); + await file.writeAsString(csvData); + } } - Widget _buildCustomNode(TreeNode node) { - final templateNode = node as TemplateNode; - final isAttribute = node.isAttribute; + Widget _buildTreeView(List nodes) { + if (nodes.isEmpty) { + return const Center(child: Text('No XML data available')); + } return Consumer( builder: (context, controller, _) { - return Padding( - padding: EdgeInsets.only(left: 12.0 * node.depth), - child: ListTile( - dense: true, - leading: - isAttribute - ? const Icon(Icons.code, size: 16, color: Colors.grey) - : const Icon(Icons.label_outline, size: 18, color: Colors.blue), - title: Text( - isAttribute ? templateNode.name.substring(1) : templateNode.name, - style: TextStyle( - color: isAttribute ? Colors.grey[600] : Colors.black, - fontWeight: isAttribute ? FontWeight.normal : FontWeight.w500, - ), - ), - trailing: - templateNode.isRepeated - ? Text( - "(${templateNode.repreatCount.toString()})", - style: const TextStyle(color: Colors.grey), - ) - : null, - onTap: () => controller.selectTreeNode(templateNode), + return TreeView( + nodes: _processXmlNodes(nodes), + config: const TreeViewConfig( + showIcons: true, + singleSelect: true, + selectedColor: Colors.lightBlueAccent, + icons: {'element': Icons.label_outline, 'attribute': Icons.code}, ), + onNodeTap: (node) { + final templateNode = node as TemplateNode; + controller.selectTreeNode(templateNode); + }, + nodeBuilder: (context, node, isSelected, onTap) { + final templateNode = node as TemplateNode; + final isAttribute = node.isAttribute; + // 使用控制器中的 selectedNode 来判断是否选中当前节点 + final isActuallySelected = controller.selectedNode?.id == templateNode.id; + + return Container( + color: + isActuallySelected ? Colors.lightBlueAccent.withOpacity(0.2) : Colors.transparent, + child: Padding( + padding: EdgeInsets.only(left: 12.0 * node.depth), + child: ListTile( + dense: true, + leading: + isAttribute + ? const Icon(Icons.code, size: 16, color: Colors.grey) + : const Icon(Icons.label_outline, size: 18, color: Colors.blue), + title: Text( + isAttribute ? templateNode.name.substring(1) : templateNode.name, + style: TextStyle( + color: isAttribute ? Colors.grey[600] : Colors.black, + fontWeight: isAttribute ? FontWeight.normal : FontWeight.w500, + ), + ), + trailing: + templateNode.isRepeated + ? Text( + "(${templateNode.repreatCount.toString()})", + style: const TextStyle(color: Colors.grey), + ) + : null, + onTap: onTap, + ), + ), + ); + }, ); }, ); } + List _processXmlNodes(List nodes) { + return nodes.cast(); + } + Widget _buildGridView(List items) { return Consumer( builder: (context, controller, _) { - // 根据当前选中的节点/属性过滤数据 + // 根据当前选中的节点的path过滤数据 final filteredItems = controller.selectedNode != null - ? items.where((item) => item.matches(controller.selectedNode!)) + ? items.where((item) => item.matchesPath(controller.selectedNode!.path)).toList() : items; - return GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 5, - ), - itemCount: filteredItems.length, - itemBuilder: (context, index) { - final item = filteredItems.elementAt(index); - return Card( - child: Padding( + // 创建 DataGridSource + final dataSource = _TemplateItemDataSource( + items: filteredItems, + selectedNode: controller.selectedNode, + ); + + return SfDataGrid( + source: dataSource, + columns: [ + GridColumn( + columnName: 'index', + width: 60, + label: Container( padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text('Path: ${item.xPath}'), Text('Value: ${item.value}')], - ), + color: Colors.grey[200], + alignment: Alignment.center, + child: const Text('序号'), ), - ); - }, + ), + GridColumn( + columnName: 'content', + label: Container( + padding: const EdgeInsets.all(8.0), + alignment: Alignment.center, + color: Colors.grey[200], + child: const Text('内容'), + ), + ), + ], + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + columnWidthMode: ColumnWidthMode.fill, ); }, ); } } + +class _TemplateItemDataSource extends DataGridSource { + final List items; + final TemplateNode? selectedNode; + + _TemplateItemDataSource({required this.items, required this.selectedNode}); + + @override + List get rows => + items.map((item) { + return DataGridRow( + cells: [ + DataGridCell(columnName: 'index', value: items.indexOf(item) + 1), + DataGridCell(columnName: 'content', value: item.value), + ], + ); + }).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/shared/components/tree_view.dart b/win_text_editor/lib/shared/components/tree_view.dart index dac2bc8..e907532 100644 --- a/win_text_editor/lib/shared/components/tree_view.dart +++ b/win_text_editor/lib/shared/components/tree_view.dart @@ -119,7 +119,7 @@ class _TreeViewState extends State { } void _handleNodeTap(TreeNode node) { - if (widget.config.singleSelect) { + if (widget.config.singleSelect && !node.isDirectory) { // 只处理叶子节点的单选逻辑 setState(() { _selectedIds.clear(); _selectedIds.add(node.id);