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 2559a16..49cab26 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 @@ -1,5 +1,6 @@ import 'package:file_picker/file_picker.dart'; import 'package:win_text_editor/framework/controllers/logger.dart'; +import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart'; import 'package:xml/xml.dart' as xml; import 'dart:io'; @@ -9,106 +10,128 @@ class TemplateParserController extends BaseContentController { List _treeNodes = []; List _templateItems = []; String? _errorMessage; + TemplateNode? _selectedNode; // Getters String get filePath => _filePath; List get treeNodes => _treeNodes; List get templateItems => _templateItems; String? get errorMessage => _errorMessage; + TemplateNode? get selectedNode => _selectedNode; Future pickFile() async { - final result = await FilePicker.platform.pickFiles(); + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['xml'], + ); if (result != null) { _filePath = result.files.single.path!; await _loadTemplateData(); - notifyListeners(); } } - void setFilePath(String path) { + Future setFilePath(String path) async { _filePath = path; - _loadTemplateData(); - notifyListeners(); + await _loadTemplateData(); } void selectTreeNode(TemplateNode node) { - _templateItems = List.generate( - 10, - (index) => TemplateItem(id: index + 1, content: 'Content for ${node.name} item ${index + 1}'), - ); + _selectedNode = node; + _templateItems = _generateTemplateItems(node); notifyListeners(); } + List _generateTemplateItems(TemplateNode node) { + // 实际项目中应从XML解析真实数据 + return [ + if (node.attributes != null) + ...node.attributes!.entries.map( + (e) => TemplateItem( + id: e.key.hashCode, + content: '${node.name}.${e.key}', + xPath: '${node.name}@${e.key}', + value: e.value, + ), + ), + if (node.text != null && node.text!.trim().isNotEmpty) + TemplateItem( + id: node.text.hashCode, + content: node.text!, + xPath: '${node.name}/text()', + value: node.text!, + ), + ]; + } + Future _loadTemplateData() async { - _errorMessage = null; - _treeNodes = []; - _templateItems = []; + try { + _errorMessage = null; + _treeNodes = []; + _templateItems = []; + _selectedNode = null; - if (_filePath.isEmpty) return; + if (_filePath.isEmpty) return; - try { final file = File(_filePath); final content = await file.readAsString(); await _parseXmlContent(content); } catch (e) { - _errorMessage = '格式错误: 不是有效的XML文档'; - Logger().error('Failed to parse XML: $e'); + _errorMessage = 'Failed to load XML: ${e.toString()}'; + Logger().error('XML加载错误$_errorMessage'); + } finally { + notifyListeners(); } - - notifyListeners(); } Future _parseXmlContent(String xmlContent) async { - try { - final document = xml.XmlDocument.parse(xmlContent); - _treeNodes = _buildTreeNodes(document.rootElement); - } on xml.XmlParserException catch (e) { - throw Exception('XML解析错误: ${e.message}'); - } + final document = xml.XmlDocument.parse(xmlContent); + _treeNodes = _buildTreeNodes(document.rootElement, depth: 0); } - List _buildTreeNodes(xml.XmlElement element) { - return [ - TemplateNode( - element.name.local, - element.children - .whereType() - .map((e) => _buildTreeNodes(e)) - .expand((nodes) => nodes) - .toList(), - attributes: element.attributes.fold( - {}, - (map, attr) => map!..[attr.name.local] = attr.value, - ), - text: element.text, + List _buildTreeNodes(xml.XmlElement element, {required int depth}) { + final node = TemplateNode( + name: element.name.local, + children: [], + attributes: + element.attributes.isNotEmpty + ? {for (var attr in element.attributes) attr.name.local: attr.value} + : null, + text: element.text.trim().isNotEmpty ? element.text : null, + depth: depth, + isExpanded: depth < 1, // 默认展开第一层 + ); + + // 处理子元素 + node.children.addAll( + element.children.whereType().map( + (e) => _buildTreeNodes(e, depth: depth + 1).first, ), - ]; + ); + + // 添加属性节点 + if (element.attributes.isNotEmpty) { + node.children.addAll( + element.attributes.map((attr) => TemplateNode.attribute(attr.name.local, attr.value)), + ); + } + + return [node]; } @override void onOpenFile(String filePath) { - Logger().info('File selected: $filePath'); setFilePath(filePath); } + @override + void dispose() { + _treeNodes.clear(); + _templateItems.clear(); + super.dispose(); + } + @override void onOpenFolder(String folderPath) { // TODO: implement onOpenFolder } } - -class TemplateNode { - final String name; - final List children; - final Map? attributes; - final String? text; - - TemplateNode(this.name, this.children, {this.attributes, this.text}); -} - -class TemplateItem { - final int id; - final String content; - - TemplateItem({required this.id, required this.content}); -} 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 new file mode 100644 index 0000000..1b84e45 --- /dev/null +++ b/win_text_editor/lib/modules/template_parser/models/template_node.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:win_text_editor/shared/components/tree_view.dart'; + +class TemplateNode implements TreeNode { + @override + final String id; // 使用路径或唯一标识作为ID + @override + final String name; + @override + final List children; + final Map? attributes; + final String? text; + + @override + final int depth; // 节点深度 + @override + bool isExpanded; // 是否展开 + bool isRepeated; // 是否重复节点 + + @override + bool get isDirectory => children.isNotEmpty; // 有子节点即为目录 + + @override + IconData? get iconData { + if (name.startsWith('@')) return Icons.code; // 属性节点 + return isDirectory ? Icons.folder : Icons.insert_drive_file; // 元素节点 + } + + TemplateNode({ + required this.name, + required this.children, + this.attributes, + this.text, + this.depth = 0, + this.isExpanded = false, + this.isRepeated = false, + String? id, + }) : id = id ?? '${depth}_${name}'; // 默认ID生成逻辑 + + // 转换为属性节点 + TemplateNode.attribute(String name, String value) + : this(name: '@$name', children: const [], attributes: {name: value}, depth: 1); + + // 克隆方法用于生成子节点 + TemplateNode copyWith({int? depth, bool? isExpanded}) { + return TemplateNode( + name: name, + children: children, + attributes: attributes, + text: text, + depth: depth ?? this.depth, + isExpanded: isExpanded ?? this.isExpanded, + id: id, + ); + } +} + +class TemplateItem { + final int id; + final String content; + final String xPath; + final String value; + + 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); + } +} 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 82cb65c..5dc4777 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,8 @@ 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/models/template_node.dart'; +import 'package:win_text_editor/shared/components/tree_view.dart'; class TemplateParserView extends StatefulWidget { final String tabId; @@ -92,58 +94,100 @@ class _TemplateParserViewState extends State { return const Center(child: Text('No XML data available')); } - return ListView.builder( - itemCount: nodes.length, - itemBuilder: (context, index) { - return _buildTreeNode(nodes[index]); + 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; + Provider.of(context, listen: false).selectTreeNode(templateNode); }, + nodeBuilder: (context, node, isSelected, onTap) => _buildCustomNode(node), ); } - Widget _buildTreeNode(TemplateNode node) { - return ExpansionTile( - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(node.name), - if (node.attributes != null && node.attributes!.isNotEmpty) - Text( - node.attributes!.entries.map((e) => '${e.key}="${e.value}"').join(' '), - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - if (node.text != null && node.text!.trim().isNotEmpty) - Text( - 'Text: ${node.text!.trim()}', - style: const TextStyle(fontSize: 12, color: Colors.blue), - ), - ], + List _processXmlNodes(List nodes) { + final uniqueNodes = {}; + final result = []; + + for (final node in nodes) { + // 生成结构签名(节点名+属性名组合,忽略值) + final signature = '${node.name}:${node.attributes?.keys.join(',') ?? ''}'; + + if (!uniqueNodes.containsKey(signature)) { + uniqueNodes[signature] = node; + result.add(node); + } else { + // 标记为重复节点(可选) + uniqueNodes[signature]!.isRepeated = true; + } + } + return result.cast(); + } + + Widget _buildCustomNode(TreeNode node) { + final templateNode = node as TemplateNode; + final isAttribute = node.depth > 0 && node.name.startsWith('@'); + + 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 + ? const Text("(repeated)", style: TextStyle(color: Colors.grey)) + : null, + onTap: + () => Provider.of( + context, + listen: false, + ).selectTreeNode(templateNode), ), - children: node.children.map((child) => _buildTreeNode(child)).toList(), - onExpansionChanged: (expanded) { - if (expanded) { - Provider.of(context, listen: false).selectTreeNode(node); - } - }, ); } Widget _buildGridView(List items) { - return GridView.builder( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 5, - ), - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - return Card( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text('ID: ${item.id}'), Text('Content: ${item.content}')], - ), + return Consumer( + builder: (context, controller, _) { + // 根据当前选中的节点/属性过滤数据 + final filteredItems = + controller.selectedNode != null + ? items.where((item) => item.matches(controller.selectedNode!)) + : 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( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text('Path: ${item.xPath}'), Text('Value: ${item.value}')], + ), + ), + ); + }, ); }, ); diff --git a/win_text_editor/lib/shared/components/file_explorer.dart b/win_text_editor/lib/shared/components/file_explorer.dart index 7bf7b53..a0bb878 100644 --- a/win_text_editor/lib/shared/components/file_explorer.dart +++ b/win_text_editor/lib/shared/components/file_explorer.dart @@ -19,15 +19,7 @@ class FileExplorer extends StatefulWidget { } class _FileExplorerState extends State { - final ScrollController _verticalScrollController = ScrollController(); - final ScrollController _horizontalScrollController = ScrollController(); - - @override - void dispose() { - _verticalScrollController.dispose(); - _horizontalScrollController.dispose(); - super.dispose(); - } + // 移除所有 ScrollController Future _promptForDirectory(BuildContext context) async { final fileProvider = Provider.of(context, listen: false); @@ -79,26 +71,13 @@ class _FileExplorerState extends State { ? const Center(child: CircularProgressIndicator()) : fileProvider.fileNodes.isEmpty ? Center(child: _buildEmptyPrompt(context)) - : Scrollbar( - controller: _verticalScrollController, - thumbVisibility: true, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: _horizontalScrollController, - child: Scrollbar( - controller: _horizontalScrollController, - thumbVisibility: true, - scrollbarOrientation: ScrollbarOrientation.bottom, - child: SizedBox( - width: calculateTotalWidth(context, fileProvider), - child: TreeView( - nodes: fileProvider.fileNodes, - config: const TreeViewConfig(showIcons: true, lazyLoad: true), - onNodeTap: (node) => _handleNodeTap(context, node as FileNode), - onNodeDoubleTap: (node) => _handleNodeDoubleTap(node as FileNode), - ), - ), - ), + : SizedBox( + width: calculateTotalWidth(context, fileProvider), + child: TreeView( + nodes: fileProvider.fileNodes, + config: const TreeViewConfig(showIcons: true, lazyLoad: true), + onNodeTap: (node) => _handleNodeTap(context, node as FileNode), + onNodeDoubleTap: (node) => _handleNodeDoubleTap(node as FileNode), ), ), ), diff --git a/win_text_editor/lib/shared/components/tree_view.dart b/win_text_editor/lib/shared/components/tree_view.dart index 0f53565..dac2bc8 100644 --- a/win_text_editor/lib/shared/components/tree_view.dart +++ b/win_text_editor/lib/shared/components/tree_view.dart @@ -13,12 +13,13 @@ abstract class TreeNode { /// 树视图配置 class TreeViewConfig { - final bool lazyLoad; // 是否延时加载 - final bool singleSelect; // 是否单一选择 - final bool showCheckboxes; // 是否显示复选框 - final bool showIcons; // 是否显示图标 - final Color? selectedColor; // 选中项背景色 - final double indentWidth; // 缩进宽度 + final bool lazyLoad; + final bool singleSelect; + final bool showCheckboxes; + final bool showIcons; + final Color? selectedColor; + final double indentWidth; + final Map icons; // 改为final const TreeViewConfig({ this.lazyLoad = false, @@ -27,6 +28,7 @@ class TreeViewConfig { this.showIcons = true, this.selectedColor, this.indentWidth = 24.0, + this.icons = const {}, // 提供空Map作为默认值 }); } @@ -37,6 +39,8 @@ class TreeView extends StatefulWidget { final Function(TreeNode)? onNodeTap; final Function(TreeNode)? onNodeDoubleTap; final Function(TreeNode, bool?)? onNodeCheckChanged; + final Widget Function(BuildContext, TreeNode, bool, VoidCallback)? nodeBuilder; // 新增参数 + final ScrollController? scrollController; const TreeView({ super.key, @@ -45,6 +49,8 @@ class TreeView extends StatefulWidget { this.onNodeTap, this.onNodeDoubleTap, this.onNodeCheckChanged, + this.nodeBuilder, + this.scrollController, }); @override @@ -54,25 +60,61 @@ class TreeView extends StatefulWidget { class _TreeViewState extends State { final Set _selectedIds = {}; final Set _checkedIds = {}; + late ScrollController _effectiveController; + + @override + void initState() { + super.initState(); + _effectiveController = widget.scrollController ?? ScrollController(); + } + + @override + void didUpdateWidget(TreeView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.scrollController != oldWidget.scrollController) { + if (oldWidget.scrollController == null) { + _effectiveController.dispose(); + } + _effectiveController = widget.scrollController ?? ScrollController(); + } + } + + @override + void dispose() { + if (widget.scrollController == null) { + _effectiveController.dispose(); + } + super.dispose(); + } @override Widget build(BuildContext context) { - return ListView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemCount: _countVisibleNodes(widget.nodes), - itemBuilder: (context, index) { - final node = _getVisibleNode(widget.nodes, index); - return _TreeNodeWidget( - node: node, - config: widget.config, - isSelected: _selectedIds.contains(node.id), - isChecked: _checkedIds.contains(node.id), - onTap: () => _handleNodeTap(node), - onDoubleTap: () => widget.onNodeDoubleTap?.call(node), - onCheckChanged: (value) => _handleNodeCheckChanged(node, value), - ); - }, + return Scrollbar( + controller: _effectiveController, + thumbVisibility: true, + notificationPredicate: (_) => true, // 确保接收所有滚动通知 + child: ListView.builder( + controller: _effectiveController, + physics: const ClampingScrollPhysics(), + itemCount: _countVisibleNodes(widget.nodes), + itemBuilder: (context, index) { + final node = _getVisibleNode(widget.nodes, index); + final isSelected = _selectedIds.contains(node.id); + + // 使用自定义构建器或默认构建器 + return widget.nodeBuilder != null + ? widget.nodeBuilder!(context, node, isSelected, () => _handleNodeTap(node)) + : TreeNodeWidget( + node: node, + config: widget.config, + isSelected: isSelected, + isChecked: _checkedIds.contains(node.id), + onTap: () => _handleNodeTap(node), + onDoubleTap: () => widget.onNodeDoubleTap?.call(node), + onCheckChanged: (value) => _handleNodeCheckChanged(node, value), + ); + }, + ), ); } @@ -127,7 +169,7 @@ class _TreeViewState extends State { } } -class _TreeNodeWidget extends StatelessWidget { +class TreeNodeWidget extends StatelessWidget { final TreeNode node; final TreeViewConfig config; final bool isSelected; @@ -136,7 +178,7 @@ class _TreeNodeWidget extends StatelessWidget { final VoidCallback onDoubleTap; final Function(bool?)? onCheckChanged; - const _TreeNodeWidget({ + const TreeNodeWidget({ required this.node, required this.config, required this.isSelected,