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 new file mode 100644 index 0000000..ddefecd --- /dev/null +++ b/win_text_editor/lib/modules/template_parser/controllers/filter_controller.dart @@ -0,0 +1,181 @@ +// filter_controller.dart +import 'package:flutter/foundation.dart'; +import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; +import 'template_notifier.dart'; + +class FilterController extends TemplateNotifier { + String? _selectedFilterField; + String? _selectedFilterOperator; + String _filterValue = ''; + bool _isFilterValid = false; + bool _isFilterApplied = false; + List _availableFields = []; + + // 仅按钮需要全局通知的Notifier + final ValueNotifier _filterActionNotifier = ValueNotifier(false); + + String? get selectedFilterField => _selectedFilterField; + String? get selectedFilterOperator => _selectedFilterOperator; + String get filterValue => _filterValue; + bool get isFilterValid => _isFilterValid; + bool get isFilterApplied => _isFilterApplied; + List get availableFields => _availableFields; + + ValueNotifier get filterActionNotifier => _filterActionNotifier; + + set selectedFilterOperator(String? value) { + _selectedFilterOperator = value; + _updateFilterValidity(); + // 仅通知本地监听器(下拉框变化) + safeNotify(); + } + + set filterValue(String value) { + _filterValue = value; + _updateFilterValidity(); + // 仅通知本地监听器(输入框变化) + safeNotify(); + } + + set selectedFilterField(String? value) { + // 当设置的值不在可用字段中时自动清空 + if (value != null && !_availableFields.contains(value)) { + _selectedFilterField = null; + } else { + _selectedFilterField = value; + } + _updateFilterValidity(); + safeNotify(); + } + + void updateAvailableFields(List newFields) { + _availableFields = newFields; + // 检查当前选中值是否仍然有效 + if (_selectedFilterField != null && !newFields.contains(_selectedFilterField)) { + _selectedFilterField = null; + } + _updateFilterValidity(); + safeNotify(); + } + + void _updateFilterValidity() { + _isFilterValid = _checkFilterValidity(); + } + + bool _checkFilterValidity() { + if (_selectedFilterField == null || _selectedFilterOperator == null || _filterValue.isEmpty) { + return false; + } + + final operator = _selectedFilterOperator!; + final value = _filterValue; + + if (operator == 'between' || operator == 'in') { + final parts = value.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); + if (parts.length < 2) { + return false; + } + + if (operator == 'between') { + for (var part in parts) { + if (double.tryParse(part) == null) { + return false; + } + } + } + } else if (operator == '>' || operator == '<') { + return double.tryParse(value) != null; + } + + return true; + } + + //进行过滤处理,供外部获取调用,提供原始列表,获取过滤结果 + List doFilter(List templateItems) { + if (selectedFilterField == null || selectedFilterOperator == null) { + return templateItems; + } + + List filteredItems = []; + + // 1. 找出所有匹配的主项 + final primaryMatches = templateItems.where((item) { + final nodeName = item.xPath.split('/').last; + return nodeName == selectedFilterField && _matchesCondition(item.value); + }); + + // 2. 收集这些主项的ID(同实例的所有项共享相同ID) + final matchedIds = primaryMatches.map((item) => item.id).toSet(); + + // 3. 过滤出同实例的所有项 + filteredItems.cast(); + + filteredItems.addAll( + filteredItems.where((item) { + return matchedIds.contains(item.id); + }).toList(), + ); + + _isFilterApplied = true; + return filteredItems; + } + + // 检查值是否符合条件 + bool _matchesCondition(String value) { + if (selectedFilterOperator == null || filterValue.isEmpty) { + return false; + } + + switch (selectedFilterOperator!) { + case '==': + return value == filterValue; + + case '>': + final numValue = double.tryParse(value); + final filterNum = double.tryParse(filterValue); + return numValue != null && filterNum != null && numValue > filterNum; + + case '<': + final numValue = double.tryParse(value); + final filterNum = double.tryParse(filterValue); + return numValue != null && filterNum != null && numValue < filterNum; + + case 'between': + final parts = + filterValue.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); + + if (parts.length < 2) return false; + + final lower = double.tryParse(parts[0]); + final upper = double.tryParse(parts[1]); + final numValue = double.tryParse(value); + + return numValue != null && + lower != null && + upper != null && + numValue >= lower && + numValue <= upper; + + case 'in': + final filterValues = + filterValue.split(',').map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); + + return filterValues.contains(value); + + default: + return false; + } + } + + //执行过滤 + void applyFilter() { + _isFilterApplied = true; + _filterActionNotifier.value = !_filterActionNotifier.value; + } + + // 取消过滤 + void clearFilter() { + _isFilterApplied = false; + _filterActionNotifier.value = !_filterActionNotifier.value; + } +} 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 new file mode 100644 index 0000000..ffd1b55 --- /dev/null +++ b/win_text_editor/lib/modules/template_parser/controllers/grid_view_controller.dart @@ -0,0 +1,53 @@ +// grid_view_controller.dart +import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; +import 'template_notifier.dart'; + +class GridViewController extends TemplateNotifier { + 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/template_parser/controllers/template_notifier.dart b/win_text_editor/lib/modules/template_parser/controllers/template_notifier.dart new file mode 100644 index 0000000..9884331 --- /dev/null +++ b/win_text_editor/lib/modules/template_parser/controllers/template_notifier.dart @@ -0,0 +1,9 @@ +// template_notifier.dart +import 'package:flutter/foundation.dart'; + +abstract class TemplateNotifier extends ChangeNotifier { + @protected + void safeNotify() { + if (hasListeners) notifyListeners(); + } +} 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 5a0ab0d..75e43ea 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 @@ -4,26 +4,58 @@ import 'package:win_text_editor/modules/template_parser/models/template_node.dar import 'package:win_text_editor/shared/base/base_content_controller.dart'; import 'package:xml/xml.dart' as xml; import 'dart:io'; +import 'tree_view_controller.dart'; +import 'filter_controller.dart'; +import 'grid_view_controller.dart'; class TemplateParserController extends BaseContentController { + final TreeViewController treeController; + final FilterController filterController; + final GridViewController gridController; + String _filePath = ''; - List _treeNodes = []; - List _templateItems = []; String? _errorMessage; - TemplateNode? _selectedNode; - String? _currentParentPath; // 新增属性,用于记录当前选中节点的父节点路径 - // Getters String get filePath => _filePath; - List get treeNodes => _treeNodes; - List get templateItems => _templateItems; String? get errorMessage => _errorMessage; - TemplateNode? get selectedNode => _selectedNode; + //---------------初始化方法---- + + TemplateParserController() + : treeController = TreeViewController(), + filterController = FilterController(), + gridController = GridViewController() { + _setupCrossControllerCommunication(); + } + + //设置跨控制器状态协同 + void _setupCrossControllerCommunication() { + // 1:树选择变化时更新过滤字段选项 + treeController.addListener(() { + filterController.updateAvailableFields(treeController.selectedNodeNames); + // 更新Grid的节点引用 + gridController.updateTreeNodesRef(treeController.treeNodes); + }); + + // 2:过滤条件变化时更新表格数据 + filterController.filterActionNotifier.addListener(() { + if (filterController.isFilterApplied) { + final List filteredItems = filterController.doFilter( + gridController.templateItems, + ); + gridController.applyFilter(filteredItems); + } else { + gridController.clearFilter(); + } + }); + } + + //----------------业务入口方法----- + //widget调用入口:打开文件 Future pickFile() async { final result = await FilePicker.platform.pickFiles( type: FileType.custom, - allowedExtensions: ['xml'], + allowedExtensions: ['xml', '*'], ); if (result != null) { _filePath = result.files.single.path!; @@ -31,119 +63,35 @@ class TemplateParserController extends BaseContentController { } } + //执行框架回调入口:双击左侧资源管理文件 Future setFilePath(String path) async { _filePath = path; await _loadTemplateData(); } - void selectTreeNode(TemplateNode node) { - _selectedNode = node; - notifyListeners(); - } - - // 新增方法,用于处理节点的选中状态 - void toggleNodeCheck(TemplateNode node) { - final parentPath = node.path.substring(0, node.path.lastIndexOf('/')); - if (_currentParentPath != null && _currentParentPath != parentPath) { - // 如果最新选择的节点与之前选中的节点不在同一个父节点下,清除之前的选中状态 - clearAllChecked(); - } - node.isChecked = !node.isChecked; - _currentParentPath = parentPath; - notifyListeners(); - } - - // 新增方法,用于清除所有节点的选中状态 - void clearAllChecked() { - void traverse(TemplateNode node) { - node.isChecked = false; - for (var child in node.children) { - traverse(child); - } - } - - for (var node in _treeNodes) { - traverse(node); - } - } - + //加载xml文件 Future _loadTemplateData() async { try { _errorMessage = null; - _treeNodes = []; - _templateItems = []; - _selectedNode = null; - _currentParentPath = null; // 加载数据时清空当前父节点路径 - - if (_filePath.isEmpty) return; - final file = File(_filePath); final content = await file.readAsString(); - await _parseXmlContent(content); + final document = xml.XmlDocument.parse(content); + + // 更新各控制器 + //树视图展示文件结构 + treeController.updateTreeNodes( + _buildTreeNodes(document.rootElement, document.rootElement.localName, depth: 0), + ); + //列表展示选中节点的内容 + gridController.updateTemplateItems(_parseAllNodeValues(document)); } catch (e) { _errorMessage = 'Failed to load XML: ${e.toString()}'; Logger().error('XML加载错误$_errorMessage'); - } finally { - notifyListeners(); - } - } - - Future _parseXmlContent(String xmlContent) async { - final document = xml.XmlDocument.parse(xmlContent); - Logger().debug('开始解析XML,根元素: ${document.rootElement.name}'); - _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, String path, { @@ -201,40 +149,77 @@ class TemplateParserController extends BaseContentController { return [node]; } - @override - void onOpenFile(String filePath) { - setFilePath(filePath); - } + //解析全量数据 + List _parseAllNodeValues(xml.XmlDocument document) { + final items = []; + int id = 0; - @override - void dispose() { - _treeNodes.clear(); - _templateItems.clear(); - super.dispose(); - } + void traverse(xml.XmlElement element, String currentPath, int index) { + // 处理属性 + for (final attr in element.attributes) { + items.add( + TemplateItem( + id: id++, + rowId: "$currentPath/@$index", + content: attr.value, + xPath: '$currentPath/@${attr.name.local}', + value: attr.value, + nodeType: NodeType.attribute, + ), + ); + } - @override - void onOpenFolder(String folderPath) { - // TODO: implement onOpenFolder - } + // 处理文本节点 + final textNodes = element.children.whereType(); + if (textNodes.isNotEmpty) { + items.add( + TemplateItem( + id: id++, + rowId: "$currentPath/@$index", + content: textNodes.first.text, + xPath: currentPath, + value: textNodes.first.text, + nodeType: NodeType.text, + ), + ); + } - // 新增方法,用于获取当前选中的节点列表 - List getSelectedNodes() { - List selectedNodes = []; + // 3. 处理子元素(考虑重复元素的情况) + final childElements = element.children.whereType(); + final groupedChildren = >{}; - void traverse(TemplateNode node) { - if (node.isChecked) { - selectedNodes.add(node); + // 按元素名分组 + for (var child in childElements) { + groupedChildren.putIfAbsent(child.name.local, () => []).add(child); } - for (var child in node.children) { - traverse(child); + + // 处理子元素(不再需要特殊处理重复元素) + int i = 0; + for (final child in element.childElements) { + traverse(child, '$currentPath/${child.name.local}', i++); } } - for (var node in _treeNodes) { - traverse(node); - } + traverse(document.rootElement, document.rootElement.localName, 0); + return items; + } + + //-----------框架回调-- + @override + void onOpenFile(String filePath) { + setFilePath(filePath); + } - return selectedNodes; + @override + void onOpenFolder(String folderPath) { + // 不支持打开文件夹 + } + + @override + void dispose() { + treeController.dispose(); + filterController.dispose(); + gridController.dispose(); + super.dispose(); } } 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 new file mode 100644 index 0000000..8ddbf8f --- /dev/null +++ b/win_text_editor/lib/modules/template_parser/controllers/tree_view_controller.dart @@ -0,0 +1,68 @@ +// tree_view_controller.dart + +import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; +import 'template_notifier.dart'; + +class TreeViewController extends TemplateNotifier { + //根节点 + List _treeNodes = []; + TemplateNode? _selectedNode; + String? _currentParentPath; + + List get treeNodes => _treeNodes; + TemplateNode? get selectedNode => _selectedNode; + + // 加载树视图,当文件路径改变时调用 + void updateTreeNodes(List nodes) { + _treeNodes = nodes; + safeNotify(); + } + + void selectTreeNode(TemplateNode node) { + _selectedNode = node; + safeNotify(); + } + + // 选择节点,多选时仅可选中同一层级的节点 + void toggleNodeCheck(TemplateNode node) { + final parentPath = node.path.substring(0, node.path.lastIndexOf('/')); + if (_currentParentPath != null && _currentParentPath != parentPath) { + clearAllChecked(); + } + node.isChecked = !node.isChecked; + _currentParentPath = parentPath; + safeNotify(); + } + + void clearAllChecked() { + void traverse(TemplateNode node) { + node.isChecked = false; + for (var child in node.children) { + traverse(child); + } + } + + for (var node in _treeNodes) { + traverse(node); + } + } + + List get selectedNodeNames { + List selectedNodeNames = []; + + void traverse(TemplateNode node) { + if (node.isChecked) { + selectedNodeNames.add(node.name); + } + for (var child in node.children) { + traverse(child); + } + } + + for (var node in _treeNodes) { + traverse(node); + } + + return selectedNodeNames; + } +} 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 a0be1bb..b10d52d 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 @@ -2,11 +2,16 @@ 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; - final String path; + @override bool isExpanded; + + final String path; bool isRepeated; bool isAttribute; int repreatCount; @@ -15,8 +20,8 @@ class TemplateNode implements TreeNode { TemplateNode({ required this.name, required this.children, - required this.path, required this.depth, + required this.path, this.isExpanded = false, this.isRepeated = false, this.isAttribute = false, @@ -38,6 +43,7 @@ enum NodeType { element, attribute, text } class TemplateItem { final int id; + final String rowId; final String content; final String xPath; final String value; @@ -45,6 +51,7 @@ class TemplateItem { TemplateItem({ required this.id, + required this.rowId, required this.content, required this.xPath, required this.value, diff --git a/win_text_editor/lib/modules/template_parser/widgets/template_filter_panel.dart b/win_text_editor/lib/modules/template_parser/widgets/template_filter_panel.dart new file mode 100644 index 0000000..c7ad587 --- /dev/null +++ b/win_text_editor/lib/modules/template_parser/widgets/template_filter_panel.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:win_text_editor/modules/template_parser/controllers/filter_controller.dart'; + +class TemplateFilterPanel extends StatefulWidget { + final FilterController controller; + + const TemplateFilterPanel({super.key, required this.controller}); + + @override + State createState() => _TemplateFilterPanelState(); +} + +class _TemplateFilterPanelState extends State { + late TextEditingController _textController; + + @override + void initState() { + super.initState(); + _textController = TextEditingController(text: widget.controller.filterValue); + } + + @override + void didUpdateWidget(covariant TemplateFilterPanel oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller.filterValue != _textController.text) { + _textController.text = widget.controller.filterValue; + } + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, controller, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + Expanded( + child: DropdownButtonFormField( + decoration: const InputDecoration( + labelText: '过滤字段', + border: OutlineInputBorder(), + ), + items: + controller.availableFields.map((String value) { + return DropdownMenuItem(value: value, child: Text(value)); + }).toList(), + onChanged: (value) { + controller.selectedFilterField = value; + }, + value: controller.selectedFilterField, + ), + ), + const SizedBox(width: 16), + SizedBox( + width: 120, + child: DropdownButtonFormField( + decoration: const InputDecoration( + labelText: '匹配方式', + border: OutlineInputBorder(), + ), + items: const [ + DropdownMenuItem(value: '==', child: Text('==')), + DropdownMenuItem(value: '>', child: Text('>')), + DropdownMenuItem(value: '<', child: Text('<')), + DropdownMenuItem(value: 'between', child: Text('between')), + DropdownMenuItem(value: 'in', child: Text('in')), + ], + onChanged: (value) { + controller.selectedFilterOperator = value; + }, + value: controller.selectedFilterOperator, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: SizedBox( + height: 190, + child: TextField( + maxLines: null, + expands: true, + decoration: const InputDecoration( + labelText: '过滤条件值', + border: OutlineInputBorder(), + alignLabelWithHint: true, + ), + controller: _textController, + onChanged: (value) { + widget.controller.filterValue = value; + }, + ), + ), + ), + const SizedBox(height: 16), + // 修改按钮部分代码 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: + controller.selectedFilterField != null && + controller.selectedFilterOperator != null && + controller.isFilterValid + ? () => controller.applyFilter() + : null, + child: const Text('过滤'), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: () { + controller.applyFilter(); + }, + child: const Text('刷新'), + ), + ], + ), + ), + ], + ); + }, + ); + } +} diff --git a/win_text_editor/lib/modules/template_parser/widgets/template_grid_view.dart b/win_text_editor/lib/modules/template_parser/widgets/template_grid_view.dart index bc11746..29e984c 100644 --- a/win_text_editor/lib/modules/template_parser/widgets/template_grid_view.dart +++ b/win_text_editor/lib/modules/template_parser/widgets/template_grid_view.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; -import 'package:win_text_editor/framework/controllers/logger.dart'; -import 'package:win_text_editor/modules/template_parser/controllers/template_parser_controller.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 TemplateGridView extends StatelessWidget { - final TemplateParserController controller; + final GridViewController controller; const TemplateGridView({super.key, required this.controller}); @override Widget build(BuildContext context) { - return Consumer( + return Consumer( builder: (context, controller, _) { return GestureDetector( onSecondaryTapDown: (details) { @@ -29,7 +28,7 @@ class TemplateGridView extends StatelessWidget { Future _showContextMenu( BuildContext context, Offset position, - TemplateParserController controller, + GridViewController controller, ) async { final renderBox = context.findRenderObject() as RenderBox; final localPosition = renderBox.globalToLocal(position); @@ -58,7 +57,7 @@ class TemplateGridView extends StatelessWidget { } } - Future _exportToCsv(TemplateParserController controller) async { + Future _exportToCsv(GridViewController controller) async { final selectedNodes = controller.getSelectedNodes(); if (selectedNodes.isEmpty) return; @@ -93,7 +92,7 @@ class TemplateGridView extends StatelessWidget { } } - Widget _buildGridView(TemplateParserController controller) { + Widget _buildGridView(GridViewController controller) { final selectedNodes = controller.getSelectedNodes(); if (selectedNodes.isEmpty) { @@ -101,7 +100,7 @@ class TemplateGridView extends StatelessWidget { } // 获取所有需要显示的数据项 - final allItems = controller.templateItems; + final allItems = controller.displayedItems; // 构建数据行 - 每个父节点实例为一行 final rows = _buildDataRows(selectedNodes, allItems); @@ -146,63 +145,29 @@ class TemplateGridView extends StatelessWidget { List selectedNodes, List allItems, ) { - // 1. 为每个选中的节点路径收集所有匹配项,并记录它们的原始顺序 - final nodeValueGroups = >>{}; - - for (final node in selectedNodes) { - // 获取该节点路径对应的所有值,并保留它们的原始索引 - final valuesWithIndex = - allItems - .asMap() - .entries - .where((entry) => entry.value.xPath == node.path) - .map((entry) => MapEntry(entry.key, entry.value.value)) - .toList(); - - nodeValueGroups[node.path] = valuesWithIndex; + 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; + } } - // 2. 确定最大行数(以最多数据的列为准) - final maxRows = nodeValueGroups.values.fold( - 0, - (max, group) => group.length > max ? group.length : max, - ); - - // 3. 构建结果行列表 - final rows = >[]; - - for (var rowIndex = 0; rowIndex < maxRows; rowIndex++) { - final row = {'_index': rowIndex + 1}; - - // 为每个选中的节点添加当前行的值 + // 3. 确保所有选中列都存在 + return instanceMap.values.map((row) { for (final node in selectedNodes) { - final values = nodeValueGroups[node.path]!; - row[node.path] = rowIndex < values.length ? values[rowIndex].value : ''; + row.putIfAbsent(node.path, () => row[node.path] ?? ''); } - - rows.add(row); - } - - return rows; - } - - List _getInstancesForParent(String parentPath, List items) { - // 获取该父路径下的所有唯一实例路径 - return items - .where((item) => item.xPath.startsWith(parentPath)) - .map((item) { - // 对于重复节点,提取实例标识部分 - if (parentPath.contains('[')) { - return item.xPath.substring(0, item.xPath.indexOf('/', parentPath.length)); - } - return parentPath; - }) - .toSet() - .toList(); + return row; + }).toList(); } // 辅助方法:根据父路径分组数据 - List> _getGroupedData(TemplateParserController controller) { + List> _getGroupedData(GridViewController controller) { final selectedNodes = controller.getSelectedNodes(); if (selectedNodes.isEmpty) return []; 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 1ed5d28..771849c 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 @@ -2,6 +2,7 @@ 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/template_parser/controllers/template_parser_controller.dart'; +import 'package:win_text_editor/modules/template_parser/widgets/template_filter_panel.dart'; import 'package:win_text_editor/modules/template_parser/widgets/template_grid_view.dart'; import 'package:win_text_editor/modules/template_parser/widgets/template_tree_view.dart'; @@ -32,8 +33,13 @@ class _TemplateParserViewState extends State { @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: _controller, + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _controller), + ChangeNotifierProvider.value(value: _controller.treeController), + ChangeNotifierProvider.value(value: _controller.filterController), + ChangeNotifierProvider.value(value: _controller.gridController), + ], child: Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -79,10 +85,21 @@ class _TemplateParserViewState extends State { children: [ SizedBox( width: MediaQuery.of(context).size.width * 0.3, - child: Card(child: TemplateTreeView(controller: controller)), + child: Column( + children: [ + const Expanded(flex: 2, child: Card(child: TemplateTreeView())), + const SizedBox(height: 8), + SizedBox( + height: 340, + child: Card( + child: TemplateFilterPanel(controller: controller.filterController), + ), + ), + ], + ), ), const SizedBox(width: 8), - Expanded(child: Card(child: TemplateGridView(controller: controller))), + Expanded(child: Card(child: TemplateGridView(controller: controller.gridController))), ], ); }, diff --git a/win_text_editor/lib/modules/template_parser/widgets/template_tree_view.dart b/win_text_editor/lib/modules/template_parser/widgets/template_tree_view.dart index 8e47e4d..302f26e 100644 --- a/win_text_editor/lib/modules/template_parser/widgets/template_tree_view.dart +++ b/win_text_editor/lib/modules/template_parser/widgets/template_tree_view.dart @@ -1,24 +1,22 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:win_text_editor/modules/template_parser/controllers/template_parser_controller.dart'; +import 'package:win_text_editor/modules/template_parser/controllers/tree_view_controller.dart'; import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; import 'package:win_text_editor/shared/components/tree_view.dart'; class TemplateTreeView extends StatelessWidget { - final TemplateParserController controller; - - const TemplateTreeView({super.key, required this.controller}); + const TemplateTreeView({super.key}); @override Widget build(BuildContext context) { - return Consumer( + return Consumer( builder: (context, controller, _) { if (controller.treeNodes.isEmpty) { return const Center(child: Text('No XML data available')); } return TreeView( - nodes: _processXmlNodes(controller.treeNodes), + nodes: controller.treeNodes, config: const TreeViewConfig( showIcons: true, singleSelect: true, @@ -26,8 +24,7 @@ class TemplateTreeView extends StatelessWidget { icons: {'element': Icons.label_outline, 'attribute': Icons.code}, ), onNodeTap: (node) { - final templateNode = node as TemplateNode; - controller.selectTreeNode(templateNode); + controller.selectTreeNode; }, nodeBuilder: (context, node, isSelected, onTap) { return _buildTreeNode(node, isSelected, onTap, controller); @@ -37,15 +34,11 @@ class TemplateTreeView extends StatelessWidget { ); } - List _processXmlNodes(List nodes) { - return nodes.cast(); - } - Widget _buildTreeNode( TreeNode node, bool isSelected, VoidCallback onTap, - TemplateParserController controller, + TreeViewController controller, ) { final templateNode = node as TemplateNode; final isAttribute = node.isAttribute;