From 57a6ef71223c88defe2e25a35c9e195468c03507 Mon Sep 17 00:00:00 2001 From: hejl Date: Fri, 6 Jun 2025 15:10:36 +0800 Subject: [PATCH] =?UTF-8?q?xml=E6=90=9C=E7=B4=A2=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=A4=9A=E9=80=89=E4=B9=8B=E5=89=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/grid_view_controller.dart | 4 + .../template_parser_controller.dart | 1 + .../template_parser/widgets/grid_view.dart | 81 ++++++++- .../controllers/xml_search_controller.dart | 87 +++++---- .../xml_search/models/search_result.dart | 6 +- .../modules/xml_search/models/xml_rule.dart | 18 -- .../services/xml_search_service.dart | 102 +++++++---- .../xml_search/widgets/condition_setting.dart | 94 +++++----- .../modules/xml_search/widgets/directory.dart | 42 ++--- .../xml_search/widgets/results_view.dart | 167 ++++++++++++++---- .../lib/shared/components/my_grid_column.dart | 29 +-- 11 files changed, 416 insertions(+), 215 deletions(-) delete mode 100644 win_text_editor/lib/modules/xml_search/models/xml_rule.dart 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 f25b778..b2ad76f 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 @@ -50,4 +50,8 @@ class GridViewController extends SafeNotifier { _isFilterApplied = false; safeNotify(); } + + void updateItemValue({required String xPath, required newValue}) { + // + } } 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 6dc642d..921498e 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 @@ -58,6 +58,7 @@ class TemplateParserController extends BaseContentController { void setStatisticsMode(String? value) { statisticsMode = value ?? modeByPath; + _loadTemplateData(); notifyListeners(); } 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 7fecd97..3e4a938 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 @@ -102,21 +102,29 @@ class TemplateGridView extends StatelessWidget { return const Center(child: Text('请在左侧树中选择要显示的节点(勾选复选框)')); } - // 获取所有需要显示的数据项 final allItems = controller.displayedItems; - - // 构建数据行 - 每个父节点实例为一行 final rows = _buildDataRows(selectedNodes, allItems); - final dataSource = _TemplateItemDataSource(rows: rows, selectedNodes: selectedNodes); + final dataSource = _TemplateItemDataSource( + rows: rows, + selectedNodes: selectedNodes, + onCellValueChanged: (node, columnName, newValue) { + // 处理数据变更 + controller.updateItemValue(xPath: columnName, newValue: newValue); + }, + ); - // 构建列 final columns = [ - ShortGridColumn(columnName: 'index', label: '序号'), + ShortGridColumn( + columnName: 'index', + label: '序号', + allowEditing: false, // 序号列不可编辑 + ), ...selectedNodes.map((node) { return MyGridColumn( columnName: node.path, label: node.isAttribute ? node.name.substring(1) : node.name, + allowEditing: true, // 允许编辑 ); }).toList(), ]; @@ -130,6 +138,8 @@ class TemplateGridView extends StatelessWidget { headerGridLinesVisibility: GridLinesVisibility.both, columnWidthMode: ColumnWidthMode.fill, allowColumnsResizing: true, + allowEditing: true, // 启用编辑 + editingGestureType: EditingGestureType.tap, // 点击编辑 ); } @@ -162,13 +172,16 @@ class TemplateGridView extends StatelessWidget { class _TemplateItemDataSource extends DataGridSource { final List> _rows; final List selectedNodes; + final Function(TemplateNode, String, dynamic)? onCellValueChanged; - _TemplateItemDataSource({required List> rows, required this.selectedNodes}) - : _rows = rows; + _TemplateItemDataSource({ + required List> rows, + required this.selectedNodes, + this.onCellValueChanged, + }) : _rows = rows; @override List get rows { - // print("[DEBUG] 原始可加载记录数:${_rows.length}"); return _rows.asMap().entries.map((entry) { final index = entry.key; final rowData = entry.value; @@ -201,4 +214,54 @@ class _TemplateItemDataSource extends DataGridSource { }).toList(), ); } + + // 关键1: 启用单元格编辑 + @override + Future onCellSubmit( + DataGridRow dataGridRow, + RowColumnIndex rowColumnIndex, + GridColumn column, + ) async { + final dynamic newValue = dataGridRow.getCells()[rowColumnIndex.columnIndex].value; + final int dataRowIndex = _rows.indexWhere( + (row) => row['_index'] == dataGridRow.getCells()[0].value, + ); + + if (dataRowIndex >= 0) { + final node = selectedNodes[rowColumnIndex.columnIndex - 1]; // 减去序号列 + _rows[dataRowIndex][node.path] = newValue; + + if (onCellValueChanged != null) { + onCellValueChanged!(node, column.columnName, newValue); + } + + notifyListeners(); + } + } + + // 关键2: 自定义编辑控件 + @override + Widget? buildEditWidget( + DataGridRow dataGridRow, + RowColumnIndex rowColumnIndex, + GridColumn column, + CellSubmit submitCell, + ) { + // 序号列不可编辑 + if (column.columnName == 'index') return null; + + final String value = dataGridRow.getCells()[rowColumnIndex.columnIndex].value.toString(); + + return Container( + padding: const EdgeInsets.all(8.0), + child: TextField( + autofocus: true, + controller: TextEditingController(text: value), + onSubmitted: (String newValue) { + // 修正后的submitCell调用方式 + submitCell(); + }, + ), + ); + } } diff --git a/win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart b/win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart index 0a11b3a..8f5dedb 100644 --- a/win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart +++ b/win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart @@ -1,31 +1,60 @@ +import 'dart:async'; + import 'package:file_picker/file_picker.dart'; import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/modules/xml_search/models/search_result.dart'; -import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart'; import 'package:win_text_editor/modules/xml_search/services/xml_search_service.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart'; class XmlSearchController extends BaseContentController { String _searchDirectory = ''; - String _fileType = '*.*'; + String _searchQuery = ''; + String nodePath = ''; + String attributeName = ''; + bool _isSearching = false; + final List _results = []; - final List _rules = []; final XmlSearchService _searchService = XmlSearchService(); List get results => _results; - List get rules => _rules; - String get searchDirectory => _searchDirectory; - String get fileType => _fileType; + String get searchQuery => _searchQuery; + bool get isSearching => _isSearching; - bool _isSearching = false; + Timer? _searchDebounce; - bool onlyFileName = false; - bool get isSearching => _isSearching; + set errorMessage(String value) { + Logger().error('打开文件出错:$value'); + } + + set searchQuery(String value) { + _searchDebounce?.cancel(); + _searchDebounce = Timer(const Duration(milliseconds: 500), () { + _searchQuery = value; + notifyListeners(); + }); + } + + Future pickFile() async { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['xml', '*'], + ); + if (result != null) { + _searchDirectory = result.files.single.path!; + notifyListeners(); // 通知 Consumer 刷新 + } + } Future executeSearching() async { - Logger().info("开始提取目录:$_searchDirectory, 文件名:$_fileType"); - if (_searchDirectory.isEmpty || _rules.isEmpty) return; + Logger().info("开始搜索文件:$_searchDirectory"); + if (_searchDirectory.isEmpty || + _searchQuery.isEmpty || + nodePath.isEmpty || + attributeName.isEmpty) { + Logger().error("所有条件都不能为空。"); + return; + } _isSearching = true; notifyListeners(); @@ -36,13 +65,13 @@ class XmlSearchController extends BaseContentController { try { final newResults = await _searchService.searchFromDirectory( directory: _searchDirectory, - fileType: _fileType, - rule: _rules[0], + nodeName: nodePath, + attributeName: attributeName, + queryContent: searchQuery, ); _results.addAll(newResults); } catch (e) { - Logger().error("提取目录出错:$e"); - _results.add(SearchResult(rowNum: 1, filePath: 'Error', content: 'Searchion failed: $e')); + Logger().error("搜索文件出错:$e"); } finally { _isSearching = false; notifyListeners(); @@ -54,11 +83,6 @@ class XmlSearchController extends BaseContentController { notifyListeners(); } - set fileType(String value) { - _fileType = value; - notifyListeners(); - } - Future pickDirectory() async { final dir = await FilePicker.platform.getDirectoryPath(); if (dir != null) { @@ -66,26 +90,21 @@ class XmlSearchController extends BaseContentController { } } - void setRule(XmlRule rule) { - _rules.clear(); - rules.add(rule); - notifyListeners(); - } - - void removeRule(int index) { - _rules.removeAt(index); - notifyListeners(); - } - @override void onOpenFile(String filePath) { - // TODO: implement onOpenFile + searchDirectory = filePath; } @override void onOpenFolder(String folderPath) { - searchDirectory = folderPath; + //不支持 } - void cancelSearchion() {} + void cancelSearching() {} + + void removeResult(SearchResult result) async { + await _searchService.removeNode(searchDirectory, nodePath, attributeName, result); + results.remove(result); + notifyListeners(); + } } diff --git a/win_text_editor/lib/modules/xml_search/models/search_result.dart b/win_text_editor/lib/modules/xml_search/models/search_result.dart index b6a6ea9..47740d7 100644 --- a/win_text_editor/lib/modules/xml_search/models/search_result.dart +++ b/win_text_editor/lib/modules/xml_search/models/search_result.dart @@ -1,8 +1,8 @@ // search_result.dart class SearchResult { final int rowNum; - final String filePath; - final String content; + final String attributeValue; + int index = 0; - SearchResult({required this.rowNum, required this.filePath, required this.content}); + SearchResult({required this.rowNum, required this.attributeValue, this.index = 0}); } diff --git a/win_text_editor/lib/modules/xml_search/models/xml_rule.dart b/win_text_editor/lib/modules/xml_search/models/xml_rule.dart deleted file mode 100644 index 106e221..0000000 --- a/win_text_editor/lib/modules/xml_search/models/xml_rule.dart +++ /dev/null @@ -1,18 +0,0 @@ -// xml_rule.dart -class XmlRule { - final String nodePath; - final String attributeName; - final bool isFirstOccurrence; - final String? namespacePrefix; - - XmlRule({ - required this.nodePath, - required this.attributeName, - this.isFirstOccurrence = false, - this.namespacePrefix, - }); - - String toxPath() { - return '${namespacePrefix != null ? '$namespacePrefix:' : ''}$nodePath${isFirstOccurrence ? '[0]' : ''}/${attributeName.isNotEmpty ? '@$attributeName' : 'text()'}'; - } -} diff --git a/win_text_editor/lib/modules/xml_search/services/xml_search_service.dart b/win_text_editor/lib/modules/xml_search/services/xml_search_service.dart index 3b7954b..2f0b82d 100644 --- a/win_text_editor/lib/modules/xml_search/services/xml_search_service.dart +++ b/win_text_editor/lib/modules/xml_search/services/xml_search_service.dart @@ -1,52 +1,94 @@ // xml_search_service.dart import 'dart:io'; import 'package:win_text_editor/framework/controllers/logger.dart'; -import 'package:win_text_editor/shared/utils/file_utils.dart'; import 'package:xml/xml.dart' as xml; import 'package:win_text_editor/modules/xml_search/models/search_result.dart'; -import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart'; class XmlSearchService { Future> searchFromDirectory({ required String directory, - required String fileType, - required XmlRule rule, + required String nodeName, + required String attributeName, + required String queryContent, }) async { final results = []; - final dir = Directory(directory); - int rowNum = 1; - - await for (var entity in dir.list(recursive: true)) { - if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) { - try { - final fileContent = await entity.readAsString(); - final document = xml.XmlDocument.parse(fileContent); - final values = _searchWithRule(document, rule); - for (var value in values) { - results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: value)); + try { + final searchValues = + queryContent + .split(RegExp(r'[\n,]')) + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toList(); + if (searchValues.isEmpty) { + searchValues.add(queryContent); + } + + final file = File(directory); + final content = await file.readAsString(); + final document = xml.XmlDocument.parse(content); + + int rowNum = 1; + + // 2. Search for nodes with specified name and attribute + for (int i = 0; i < searchValues.length; i++) { + final nodes = document.findAllElements(nodeName); + int index = 0; + for (final node in nodes) { + final attributeValue = node.getAttribute(attributeName); + if (attributeValue != null && attributeValue == searchValues[i]) { + results.add( + SearchResult(rowNum: rowNum++, attributeValue: attributeValue, index: index++), + ); } - } catch (e) { - Logger().error('xmlSearchService.searchFromDirectory方法执行出错: $e'); - results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: 'Error: $e')); } } + Logger().info("共发现记录 ${rowNum - 1} 条"); + } catch (e) { + Logger().error('xmlSearchService.searchFromDirectory方法执行出错: $e'); } return results; } - List _searchWithRule(xml.XmlDocument document, XmlRule rule) { - final nodes = document.findAllElements(rule.nodePath); - //final nodes = SimpleXPath.query(document, rule.toxPath()); - if (rule.isFirstOccurrence && nodes.isNotEmpty) { - final attr = nodes.first.getAttribute(rule.attributeName); - return attr != null ? [attr] : []; - } else { - return nodes - .map((node) => node.getAttribute(rule.attributeName)) - .where((attr) => attr != null) - .cast() - .toList(); + Future removeNode( + String directory, + String nodeName, + String attributeName, + SearchResult result, + ) async { + try { + // 1. 打开并解析 XML 文件 + final file = File(directory); + final content = await file.readAsString(); + final document = xml.XmlDocument.parse(content); + + // 2. 查找所有匹配的节点 + final nodes = + document.findAllElements(nodeName).where((node) { + final attributeValue = node.getAttribute(attributeName); + return attributeValue == result.attributeValue; + }).toList(); + + // 3. 检查是否存在指定序号的节点 + if (result.index >= 0 && result.index < nodes.length) { + final nodeToRemove = nodes[result.index]; + + // 4. 删除节点 + nodeToRemove.parent?.children.remove(nodeToRemove); + + // 5. 保存修改后的 XML 文件 + final newContent = document.toXmlString(pretty: true); + await file.writeAsString(newContent); + + Logger().info( + '成功删除节点: $nodeName[$attributeName="${result.attributeValue}"][${result.index}]', + ); + } else { + Logger().warning('未找到序号为 ${result.index} 的匹配节点'); + } + } catch (e) { + Logger().error('删除节点时出错: $e'); + rethrow; // 重新抛出异常以便上层处理 } } } diff --git a/win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart b/win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart index 7f5e079..98d461c 100644 --- a/win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart +++ b/win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; -import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart'; -import 'package:win_text_editor/shared/components/my_checkbox.dart'; +import 'package:win_text_editor/shared/components/text_editor.dart'; class ConditionSetting extends StatefulWidget { const ConditionSetting({super.key}); @@ -12,17 +11,22 @@ class ConditionSetting extends StatefulWidget { } class _ConditionSettingState extends State { - final _nodePathController = TextEditingController(); - final _attributeNameController = TextEditingController(); - final _namespacePrefixController = TextEditingController(); - bool _isFirstOccurrence = false; - bool _isExtracting = false; + bool _isSearching = false; + late TextEditingController _nodePathController; + late TextEditingController _attributeNameController; + + @override + void initState() { + super.initState(); + final controller = context.read(); + _nodePathController = TextEditingController(text: controller.nodePath); + _attributeNameController = TextEditingController(text: controller.attributeName); + } @override void dispose() { _nodePathController.dispose(); _attributeNameController.dispose(); - _namespacePrefixController.dispose(); super.dispose(); } @@ -36,7 +40,7 @@ class _ConditionSettingState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text('数据提取规则设置:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + const Text('搜索规则设置:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), const SizedBox(height: 16), // 规则输入表单 @@ -47,6 +51,9 @@ class _ConditionSettingState extends State { hintText: '如: business:Service', border: OutlineInputBorder(), ), + onChanged: (value) { + controller.nodePath = value; + }, ), const SizedBox(height: 12), TextField( @@ -56,14 +63,24 @@ class _ConditionSettingState extends State { hintText: '如: chineseName 或 name', border: OutlineInputBorder(), ), + onChanged: (value) { + controller.attributeName = value; + }, ), const SizedBox(height: 12), - MyCheckbox( - title: '仅提取第一个匹配项', - value: _isFirstOccurrence, - onChanged: (value) => setState(() => _isFirstOccurrence = value ?? false), + SizedBox( + width: MediaQuery.of(context).size.width * 0.5, + height: 360, + child: TextEditor( + tabId: 'search_content_${controller.hashCode}', + title: '[列表以\\n或,分隔]', + initialContent: controller.searchQuery, // 绑定到控制器的状态 + onContentChanged: (content) { + controller.searchQuery = content; // 实时同步内容到控制器 + }, + ), ), const SizedBox(height: 16), // 操作按钮行 @@ -73,7 +90,7 @@ class _ConditionSettingState extends State { child: ElevatedButton.icon( icon: const Icon(Icons.play_arrow), label: const Text('开始'), - onPressed: _isExtracting ? null : () => _startSearching(controller), + onPressed: _isSearching ? null : () => _startSearching(controller), ), ), const SizedBox(width: 12), @@ -81,7 +98,7 @@ class _ConditionSettingState extends State { child: ElevatedButton.icon( icon: const Icon(Icons.stop, color: Colors.red), label: const Text('停止', style: TextStyle(color: Colors.red)), - onPressed: _isExtracting ? _stopExtraction : null, + onPressed: _isSearching ? _stopSearch : null, ), ), ], @@ -92,49 +109,42 @@ class _ConditionSettingState extends State { ); } - void _setRule() { - final nodePath = _nodePathController.text.trim(); - final attributeName = _attributeNameController.text.trim(); - - if (nodePath.isNotEmpty && attributeName.isNotEmpty) { - final controller = Provider.of(context, listen: false); - controller.setRule( - XmlRule( - nodePath: nodePath, - attributeName: attributeName, - isFirstOccurrence: _isFirstOccurrence, - namespacePrefix: - _namespacePrefixController.text.trim().isNotEmpty - ? _namespacePrefixController.text.trim() - : null, - ), - ); - } - } - Future _startSearching(XmlSearchController controller) async { if (controller.searchDirectory.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择搜索目录'))); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择搜索文件'))); return; } - _setRule(); + if (controller.nodePath.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先设置节点名称'))); + return; + } + + if (controller.attributeName.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先设置属性名称'))); + return; + } + + if (controller.searchQuery.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先设置搜索内容'))); + return; + } - setState(() => _isExtracting = true); + setState(() => _isSearching = true); try { await controller.executeSearching(); } finally { if (mounted) { - setState(() => _isExtracting = false); + setState(() => _isSearching = false); } } } - void _stopExtraction() { + void _stopSearch() { // 这里需要确保控制器中有取消提取的逻辑 final controller = Provider.of(context, listen: false); // 假设控制器中有cancelExtraction方法 - controller.executeSearching(); - setState(() => _isExtracting = false); + controller.cancelSearching(); + setState(() => _isSearching = false); } } diff --git a/win_text_editor/lib/modules/xml_search/widgets/directory.dart b/win_text_editor/lib/modules/xml_search/widgets/directory.dart index d279b6d..9c2309e 100644 --- a/win_text_editor/lib/modules/xml_search/widgets/directory.dart +++ b/win_text_editor/lib/modules/xml_search/widgets/directory.dart @@ -11,20 +11,17 @@ class Directory extends StatefulWidget { class _DirectoryState extends State { late TextEditingController _searchDirectoryController; - late TextEditingController _fileTypeController; @override void initState() { super.initState(); final controller = context.read(); _searchDirectoryController = TextEditingController(text: controller.searchDirectory); - _fileTypeController = TextEditingController(text: controller.fileType); } @override void dispose() { _searchDirectoryController.dispose(); - _fileTypeController.dispose(); super.dispose(); } @@ -36,9 +33,6 @@ class _DirectoryState extends State { if (_searchDirectoryController.text != controller.searchDirectory) { _searchDirectoryController.text = controller.searchDirectory; } - if (_fileTypeController.text != controller.fileType) { - _fileTypeController.text = controller.fileType; - } return Card( child: Padding( @@ -47,35 +41,19 @@ class _DirectoryState extends State { children: [ Expanded( child: TextField( - controller: _searchDirectoryController, - decoration: const InputDecoration( - labelText: '搜索目录', - border: OutlineInputBorder(), - ), - onChanged: (value) => controller.searchDirectory = value, - ), - ), - const SizedBox(width: 8), - SizedBox( - width: 100, - child: TextField( - controller: _fileTypeController, - decoration: const InputDecoration( - labelText: '文件类型', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: 'XML File', + hintText: 'Select an XML file', + suffixIcon: IconButton( + icon: const Icon(Icons.folder_open), + onPressed: controller.pickFile, + ), + border: const OutlineInputBorder(), ), - onChanged: (value) => controller.fileType = value, + controller: TextEditingController(text: controller.searchDirectory), + readOnly: true, ), ), - const SizedBox(width: 8), - IconButton( - icon: const Icon(Icons.folder_open), - onPressed: () async { - await controller.pickDirectory(); - // 不需要手动更新 _searchDirectoryController.text, - // 因为 Consumer 会触发重建并自动同步 - }, - ), ], ), ), diff --git a/win_text_editor/lib/modules/xml_search/widgets/results_view.dart b/win_text_editor/lib/modules/xml_search/widgets/results_view.dart index 4553c35..9697880 100644 --- a/win_text_editor/lib/modules/xml_search/widgets/results_view.dart +++ b/win_text_editor/lib/modules/xml_search/widgets/results_view.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:path/path.dart' as path; import 'package:file_picker/file_picker.dart'; +import 'package:win_text_editor/framework/controllers/logger.dart'; +import 'package:win_text_editor/modules/xml_search/models/search_result.dart'; import 'dart:io'; import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; @@ -21,7 +24,7 @@ class ResultsView extends StatelessWidget { onSecondaryTapDown: (details) { _showContextMenu(context, details.globalPosition, controller); }, - child: _buildLocateGrid(controller), + child: _buildLocateGrid(controller, context), ), ); } @@ -44,11 +47,7 @@ class ResultsView extends StatelessWidget { position.dx + renderBox.size.width - localPosition.dx, position.dy + renderBox.size.height - localPosition.dy, ), - items: [ - const PopupMenuItem(value: 'exportFileName', child: Text('导出文件名(csv)')), - const PopupMenuItem(value: 'exportContent', child: Text('导出内容(csv)')), - const PopupMenuItem(value: 'exportAll', child: Text('导出全部(csv)')), - ], + items: [const PopupMenuItem(value: 'exportAll', child: Text('导出(csv)'))], ); // 处理菜单选择结果 @@ -66,22 +65,9 @@ class ResultsView extends StatelessWidget { } Future _exportToCsv(XmlSearchController controller, String? exportType) async { - String csvData = ''; - csvData = - exportType == 'exportFileName' - ? '文件名称\n' - : (exportType == 'exportContent' ? '内容\n' : '文件名称\t内容\n'); + String csvData = '搜索值\t节点\n'; for (var result in controller.results) { - switch (exportType) { - case 'exportFileName': - csvData += '${path.basename(result.filePath)}\n'; - break; - case 'exportContent': - csvData += '${result.content}\n'; - break; - default: - csvData += '${path.basename(result.filePath)}\t${result.content}\n'; - } + csvData += '${path.basename(result.attributeValue)}\t${result.index}\n'; } final filePath = await FilePicker.platform.saveFile( @@ -97,15 +83,16 @@ class ResultsView extends StatelessWidget { } } - Widget _buildLocateGrid(XmlSearchController controller) { + Widget _buildLocateGrid(XmlSearchController controller, BuildContext context) { return SfDataGrid( rowHeight: 32, headerRowHeight: 32, - source: LocateDataSource(controller), + source: LocateDataSource(controller, context), columns: [ ShortGridColumn(columnName: 'rowNum', label: '序号'), - MyGridColumn(columnName: 'file', label: '文件名称', minimumWidth: 300), - MyGridColumn(columnName: 'content', label: '内容'), + MyGridColumn(columnName: 'content', label: '搜索内容', minimumWidth: 300), + ShortGridColumn(columnName: 'index', label: '节点序号', width: 80), + ShortGridColumn(columnName: 'action', label: '操作', width: 90), ], selectionMode: SelectionMode.multiple, navigationMode: GridNavigationMode.cell, @@ -123,8 +110,9 @@ class ResultsView extends StatelessWidget { class LocateDataSource extends DataGridSource { final XmlSearchController controller; + final BuildContext context; - LocateDataSource(this.controller); + LocateDataSource(this.controller, this.context); @override List get rows => @@ -132,23 +120,130 @@ class LocateDataSource extends DataGridSource { return DataGridRow( cells: [ DataGridCell(columnName: 'rowNum', value: result.rowNum), - DataGridCell(columnName: 'file', value: path.basename(result.filePath)), - DataGridCell(columnName: 'content', value: result.content), + DataGridCell(columnName: 'content', value: path.basename(result.attributeValue)), + DataGridCell(columnName: 'index', value: result.index), + DataGridCell( + columnName: 'action', + value: result, // Store file path for delete action + ), ], ); }).toList(); @override DataGridRowAdapter? buildRow(DataGridRow row) { + final cells = row.getCells(); + final result = cells[3].value as SearchResult; return DataGridRowAdapter( - cells: - row.getCells().map((cell) { - return Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text(cell.value.toString(), overflow: TextOverflow.ellipsis), - ); - }).toList(), + cells: [ + Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text(cells[0].value.toString()), + ), + // 文件名单元格 + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text(cells[1].value.toString(), overflow: TextOverflow.ellipsis, maxLines: 1), + ), + // 内容单元格(带高亮) + Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text(cells[2].value.toString()), + ), + Container( + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.delete_forever, size: 18, color: Colors.red), + onPressed: () => _showDeleteConfirmation(result), + ), + ], + ), + ), + ], ); } + + Future _showDeleteConfirmation(SearchResult result) async { + bool confirmed = false; + + await showDialog( + context: context, + builder: (context) { + // 捕获键盘事件 + return Shortcuts( + shortcuts: const { + // 绑定回车键 + SingleActivator(LogicalKeyboardKey.enter): _ConfirmAction(), + }, + child: Actions( + actions: { + _ConfirmAction: CallbackAction<_ConfirmAction>( + onInvoke: (_) { + confirmed = true; + Navigator.pop(context, true); + return null; + }, + ), + }, + child: Focus( + autofocus: true, // 自动获取焦点 + child: AlertDialog( + title: const Text('确认删除'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('将所选xml节点删除吗?'), + const SizedBox(height: 8), + Text( + '${result.attributeValue}[${result.index}]', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + confirmed = true; + Navigator.pop(context, true); + }, + child: const Text('确认'), + ), + ], + ), + ), + ), + ); + }, + ); + + if (confirmed && context.mounted) { + try { + controller.removeResult(result); + notifyListeners(); + + if (context.mounted) { + Logger().info('已删除节点文件: ${result.attributeValue}[${result.index}]'); + } + } catch (e) { + if (context.mounted) { + Logger().error('删除失败: ${e.toString()}'); + } + } + } + } +} + +class _ConfirmAction extends Intent { + const _ConfirmAction(); } diff --git a/win_text_editor/lib/shared/components/my_grid_column.dart b/win_text_editor/lib/shared/components/my_grid_column.dart index 76de635..f7f5281 100644 --- a/win_text_editor/lib/shared/components/my_grid_column.dart +++ b/win_text_editor/lib/shared/components/my_grid_column.dart @@ -7,6 +7,7 @@ class MyGridColumn extends GridColumn { double minimumWidth = 100, double maximumWidth = double.infinity, required String label, + bool allowEditing = false, }) : super( columnName: columnName, minimumWidth: minimumWidth, @@ -17,19 +18,25 @@ class MyGridColumn extends GridColumn { padding: const EdgeInsets.all(2.0), child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)), ), + allowEditing: allowEditing, ); } class ShortGridColumn extends GridColumn { - ShortGridColumn({required String columnName, double width = 60, required String label}) - : super( - columnName: columnName, - width: width, - label: Container( - alignment: Alignment.center, - color: Colors.grey[200], - padding: const EdgeInsets.all(2.0), - child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)), - ), - ); + ShortGridColumn({ + required String columnName, + double width = 60, + required String label, + bool allowEditing = false, + }) : super( + columnName: columnName, + width: width, + label: Container( + alignment: Alignment.center, + color: Colors.grey[200], + padding: const EdgeInsets.all(2.0), + child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)), + ), + allowEditing: allowEditing, + ); }