From 282911ee0f79cf09fdbd052440fc0c9b34effc42 Mon Sep 17 00:00:00 2001 From: hejl Date: Fri, 6 Jun 2025 10:30:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=8F=9C=E5=8D=95XML?= =?UTF-8?q?=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- win_text_editor/lib/menus/app_menu.dart | 4 + win_text_editor/lib/menus/menu_actions.dart | 5 + win_text_editor/lib/menus/menu_constants.dart | 1 + .../services/base_search_service.dart | 6 +- .../content_search/widgets/results_view.dart | 54 +++++- .../widgets/search_settings.dart | 2 +- .../controllers/data_compare_controller.dart | 6 +- .../widgets/data_compare_data_source.dart | 58 +++---- .../widgets/condition_setting.dart | 14 +- .../data_extract/widgets/results_view.dart | 4 +- .../data_format/widgets/grid_view.dart | 2 +- .../lib/modules/module_router.dart | 5 + .../template_parser/widgets/grid_view.dart | 6 +- .../controllers/xml_search_controller.dart | 91 +++++++++++ .../xml_search/models/search_result.dart | 8 + .../modules/xml_search/models/xml_rule.dart | 18 ++ .../services/xml_search_service.dart | 52 ++++++ .../xml_search/widgets/condition_setting.dart | 140 ++++++++++++++++ .../modules/xml_search/widgets/directory.dart | 86 ++++++++++ .../xml_search/widgets/results_view.dart | 154 ++++++++++++++++++ .../xml_search/widgets/xml_search_view.dart | 63 +++++++ .../lib/shared/components/my_grid_column.dart | 27 +-- 22 files changed, 732 insertions(+), 74 deletions(-) create mode 100644 win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart create mode 100644 win_text_editor/lib/modules/xml_search/models/search_result.dart create mode 100644 win_text_editor/lib/modules/xml_search/models/xml_rule.dart create mode 100644 win_text_editor/lib/modules/xml_search/services/xml_search_service.dart create mode 100644 win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart create mode 100644 win_text_editor/lib/modules/xml_search/widgets/directory.dart create mode 100644 win_text_editor/lib/modules/xml_search/widgets/results_view.dart create mode 100644 win_text_editor/lib/modules/xml_search/widgets/xml_search_view.dart diff --git a/win_text_editor/lib/menus/app_menu.dart b/win_text_editor/lib/menus/app_menu.dart index 9fd6145..9417afd 100644 --- a/win_text_editor/lib/menus/app_menu.dart +++ b/win_text_editor/lib/menus/app_menu.dart @@ -46,6 +46,10 @@ class AppMenu extends StatelessWidget { value: MenuConstants.dataExtract, child: ListTile(leading: Icon(Icons.outbox), title: Text('XML数据提取')), ), + const PopupMenuItem( + value: MenuConstants.xmlSearch, + child: ListTile(leading: Icon(Icons.find_in_page), title: Text('XML搜索')), + ), const PopupMenuDivider(), const PopupMenuItem( value: MenuConstants.demo, diff --git a/win_text_editor/lib/menus/menu_actions.dart b/win_text_editor/lib/menus/menu_actions.dart index 861c448..eb68df3 100644 --- a/win_text_editor/lib/menus/menu_actions.dart +++ b/win_text_editor/lib/menus/menu_actions.dart @@ -16,6 +16,7 @@ class MenuActions { MenuConstants.dataFormat: _dataFormat, MenuConstants.dataCompare: _dataCompare, MenuConstants.dataExtract: _dataExtract, + MenuConstants.xmlSearch: _xmlSearch, MenuConstants.memoryTable: _memoryTable, MenuConstants.uftComponent: _uftComponent, MenuConstants.callFunction: _callFunction, @@ -74,6 +75,10 @@ class MenuActions { await _openOrActivateTab(context, "XML数据提取", RouterKey.dataExtract, Icons.outbox); } + static Future _xmlSearch(BuildContext context) async { + await _openOrActivateTab(context, "XML搜索", RouterKey.xmlSearch, Icons.find_in_page); + } + static Future _demo(BuildContext context) async { await _openOrActivateTab(context, "Demo", RouterKey.demo, Icons.code); } diff --git a/win_text_editor/lib/menus/menu_constants.dart b/win_text_editor/lib/menus/menu_constants.dart index 33dee40..7745db9 100644 --- a/win_text_editor/lib/menus/menu_constants.dart +++ b/win_text_editor/lib/menus/menu_constants.dart @@ -18,6 +18,7 @@ class MenuConstants { static const String dataFormat = 'data_format'; static const String dataCompare = 'data_compare'; static const String dataExtract = 'data_extract'; + static const String xmlSearch = 'xml_search'; static const String demo = 'demo'; // AIGC菜单项 diff --git a/win_text_editor/lib/modules/content_search/services/base_search_service.dart b/win_text_editor/lib/modules/content_search/services/base_search_service.dart index 79c0a5b..bcfb806 100644 --- a/win_text_editor/lib/modules/content_search/services/base_search_service.dart +++ b/win_text_editor/lib/modules/content_search/services/base_search_service.dart @@ -43,7 +43,11 @@ abstract class BaseSearchService { /// 以下为静态工具方法(被所有服务共享) static List splitQuery(String query) { - return query.split(',').map((q) => q.trim()).where((q) => q.isNotEmpty).toList(); + return query + .split('\n') // 先按换行符拆分 + .expand((line) => line.split(',').map((q) => q.trim())) // 再按逗号拆分并去空格 + .where((q) => q.isNotEmpty) // 过滤空字符串 + .toList(); } static RegExp buildSearchPattern({ diff --git a/win_text_editor/lib/modules/content_search/widgets/results_view.dart b/win_text_editor/lib/modules/content_search/widgets/results_view.dart index 06c88ab..d31224a 100644 --- a/win_text_editor/lib/modules/content_search/widgets/results_view.dart +++ b/win_text_editor/lib/modules/content_search/widgets/results_view.dart @@ -76,14 +76,15 @@ class ResultsView extends StatelessWidget { Future _exportToCsv(ContentSearchController controller) async { String csvData = ''; if (controller.searchMode == SearchMode.locate) { - csvData = '文件,行号,内容\n'; + csvData = '文件\t行号\t内容\n'; for (var result in controller.results) { - csvData += '${path.basename(result.filePath)},${result.lineNumber},${result.lineContent}\n'; + csvData += + '${path.basename(result.filePath)}\t${result.lineNumber}\t${result.lineContent}\n'; } } else { - csvData = '关键词,匹配数量\n'; + csvData = '关键词\t匹配数量\n'; for (var result in controller.results) { - csvData += '${result.filePath},${result.lineNumber}\n'; + csvData += '${result.filePath}\t${result.lineNumber}\n'; } } @@ -113,9 +114,9 @@ class ResultsView extends StatelessWidget { source: LocateDataSource(controller, context), columns: [ ShortGridColumn(columnName: 'index', label: '序号'), - MyGridColumn(columnName: 'file', label: '文件(行号)', minimumWidth: 300), + MyGridColumn(columnName: 'file', label: '文件(行号)', maximumWidth: 400), MyGridColumn(columnName: 'content', label: '内容'), - ShortGridColumn(columnName: 'action', label: '操作'), + ShortGridColumn(columnName: 'action', label: '操作', width: 90), ], selectionMode: SelectionMode.multiple, navigationMode: GridNavigationMode.cell, @@ -209,15 +210,50 @@ class LocateDataSource extends DataGridSource { ), Container( alignment: Alignment.center, - child: IconButton( - icon: const Icon(Icons.delete_forever, size: 18, color: Colors.red), - onPressed: () => _showDeleteConfirmation(result.filePath), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.open_in_new, size: 18, color: Colors.blue), + onPressed: () => _openFile(result.filePath), + ), + IconButton( + icon: const Icon(Icons.delete_forever, size: 18, color: Colors.red), + onPressed: () => _showDeleteConfirmation(result.filePath), + ), + ], ), ), ], ); } + Future _openFile(String filePath) async { + try { + final file = File(filePath); + if (await file.exists()) { + // Use Process.run to open the file with the default system handler + if (Platform.isWindows) { + await Process.run('start', ['""', filePath], runInShell: true); + } else if (Platform.isMacOS) { + await Process.run('open', [filePath], runInShell: true); + } else if (Platform.isLinux) { + await Process.run('xdg-open', [filePath], runInShell: true); + } + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件不存在: $filePath'))); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('打开文件失败: ${e.toString()}'))); + } + } + } + Future _showDeleteConfirmation(String filePath) async { bool confirmed = false; diff --git a/win_text_editor/lib/modules/content_search/widgets/search_settings.dart b/win_text_editor/lib/modules/content_search/widgets/search_settings.dart index e78e990..23a6891 100644 --- a/win_text_editor/lib/modules/content_search/widgets/search_settings.dart +++ b/win_text_editor/lib/modules/content_search/widgets/search_settings.dart @@ -80,7 +80,7 @@ class _SearchSettingsState extends State { height: 360, child: TextEditor( tabId: 'search_content_${controller.hashCode}', - title: '搜索内容[列表以半角逗号分隔]', + title: '搜索内容[列表以换行或半角逗号分隔]', initialContent: controller.searchQuery, // 绑定到控制器的状态 onContentChanged: (content) { controller.searchQuery = content; // 实时同步内容到控制器 diff --git a/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart b/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart index fdec3ef..f6766d8 100644 --- a/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart +++ b/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart @@ -19,7 +19,7 @@ class DataCompareController extends BaseContentController { final lines = csvContent.split('\n'); if (lines.isEmpty) return; - String delimiter = lines[0].contains(',') ? ',' : '\t'; + String delimiter = lines[0].contains('\t') ? '\t' : ','; // 2. 解析列标题(从第一行第二列开始) final headers = lines[0].split(delimiter).skip(1).toList(); @@ -195,7 +195,7 @@ class DataCompareController extends BaseContentController { // 写入标题行 (只保留主键和各列名) buffer.write('主键'); // 不再包含"序号" for (var column in columns) { - buffer.write(',$column'); + buffer.write('\t$column'); } buffer.writeln(); @@ -203,7 +203,7 @@ class DataCompareController extends BaseContentController { for (var row in data) { buffer.write('${row['key']}'); // 不再包含serial for (var column in columns) { - buffer.write(',${row[column] ?? ''}'); + buffer.write('\t${row[column] ?? ''}'); } buffer.writeln(); } diff --git a/win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart b/win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart index 0131032..1fbf26f 100644 --- a/win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart +++ b/win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart @@ -26,47 +26,44 @@ class DataCompareDataSource extends DataGridSource { final rightData = row['right_data'] as Map?; final status = row['match_status'] as String; - return DataGridRow(cells: [ - // 左表列 - 只显示左表数据或匹配数据的左表部分 - DataGridCell( - columnName: 'left_serial', - value: (isLeft || status != 'no_match') ? row['serial'] : '' + return DataGridRow( + cells: [ + // 左表列 - 只显示左表数据或匹配数据的左表部分 + DataGridCell( + columnName: 'left_serial', + value: (isLeft || status != 'no_match') ? row['serial'] : '', + ), + DataGridCell( + columnName: 'left_key', + value: (isLeft || status != 'no_match') ? row['key'] : '', + ), + ...controller.leftColumns.map( + (col) => DataGridCell( + columnName: 'left_$col', + value: (isLeft || status != 'no_match') ? row[col] : '', + ), ), - DataGridCell( - columnName: 'left_key', - value: (isLeft || status != 'no_match') ? row['key'] : '' - ), - ...controller.leftColumns.map((col) => DataGridCell( - columnName: 'left_$col', - value: (isLeft || status != 'no_match') ? row[col] : '' - )), // 对比状态 - DataGridCell( - columnName: 'comparison', - value: _getStatusIcon(status) - ), + DataGridCell(columnName: 'comparison', value: _getStatusIcon(status)), - // 右表列 - 只显示右表数据或匹配数据的右表部分 + // 右表列 - 只显示右表数据或匹配数据的右表部分 DataGridCell( columnName: 'right_serial', - value: (!isLeft || status != 'no_match') - ? (rightData?['serial'] ?? row['serial']) - : '' + value: (!isLeft || status != 'no_match') ? (rightData?['serial'] ?? row['serial']) : '', ), DataGridCell( columnName: 'right_key', - value: (!isLeft || status != 'no_match') - ? (rightData?['key'] ?? row['key']) - : '' + value: (!isLeft || status != 'no_match') ? (rightData?['key'] ?? row['key']) : '', ), - ...controller.rightColumns.map((col) => DataGridCell( + ...controller.rightColumns.map( + (col) => DataGridCell( columnName: 'right_$col', - value: (!isLeft || status != 'no_match') - ? (rightData?[col] ?? row[col]) - : '' - )), - ]); + value: (!isLeft || status != 'no_match') ? (rightData?[col] ?? row[col]) : '', + ), + ), + ], + ); }).toList(); } @@ -88,7 +85,6 @@ class DataCompareDataSource extends DataGridSource { row.getCells().map((cell) { return Container( alignment: Alignment.center, - padding: const EdgeInsets.all(8), child: cell.value.runtimeType == Icon ? cell.value : Text(cell.value.toString()), ); }).toList(), diff --git a/win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart b/win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart index bda2d87..ed29241 100644 --- a/win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart +++ b/win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart @@ -43,8 +43,8 @@ class _ConditionSettingState extends State { TextField( controller: _nodePathController, decoration: const InputDecoration( - labelText: '节点路径 (XPath)', - hintText: '如: //business:Service 或 /root/items', + labelText: '节点名称', + hintText: '如: business:Service', border: OutlineInputBorder(), ), ), @@ -58,16 +58,6 @@ class _ConditionSettingState extends State { ), ), - const SizedBox(height: 12), - const Text('命名空间配置 (可选):'), - TextField( - controller: _namespacePrefixController, - decoration: const InputDecoration( - labelText: '命名空间前缀', - hintText: '如: business', - border: OutlineInputBorder(), - ), - ), const SizedBox(height: 12), MyCheckbox( diff --git a/win_text_editor/lib/modules/data_extract/widgets/results_view.dart b/win_text_editor/lib/modules/data_extract/widgets/results_view.dart index 268a1c6..f7e63a5 100644 --- a/win_text_editor/lib/modules/data_extract/widgets/results_view.dart +++ b/win_text_editor/lib/modules/data_extract/widgets/results_view.dart @@ -70,7 +70,7 @@ class ResultsView extends StatelessWidget { csvData = exportType == 'exportFileName' ? '文件名称\n' - : (exportType == 'exportContent' ? '内容\n' : '文件名称,内容\n'); + : (exportType == 'exportContent' ? '内容\n' : '文件名称\t内容\n'); for (var result in controller.results) { switch (exportType) { case 'exportFileName': @@ -80,7 +80,7 @@ class ResultsView extends StatelessWidget { csvData += '${result.content}\n'; break; default: - csvData += '${path.basename(result.filePath)},${result.content}\n'; + csvData += '${path.basename(result.filePath)}\t${result.content}\n'; } } diff --git a/win_text_editor/lib/modules/data_format/widgets/grid_view.dart b/win_text_editor/lib/modules/data_format/widgets/grid_view.dart index 4d46d0e..580d72e 100644 --- a/win_text_editor/lib/modules/data_format/widgets/grid_view.dart +++ b/win_text_editor/lib/modules/data_format/widgets/grid_view.dart @@ -204,7 +204,7 @@ class _CsvDataSource extends DataGridSource { cells: row.getCells().map((dataGridCell) { return Container( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), alignment: Alignment.centerLeft, child: Text( dataGridCell.value.toString(), diff --git a/win_text_editor/lib/modules/module_router.dart b/win_text_editor/lib/modules/module_router.dart index 9cddcf4..b0c986e 100644 --- a/win_text_editor/lib/modules/module_router.dart +++ b/win_text_editor/lib/modules/module_router.dart @@ -15,6 +15,8 @@ import 'package:win_text_editor/modules/memory_table/controllers/memory_table_co import 'package:win_text_editor/modules/memory_table/widgets/memory_table_view.dart'; import 'package:win_text_editor/modules/uft_component/controllers/uft_component_controller.dart'; import 'package:win_text_editor/modules/uft_component/widgets/uft_component_view.dart'; +import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; +import 'package:win_text_editor/modules/xml_search/widgets/xml_search_view.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart'; import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart'; import 'package:win_text_editor/modules/template_parser/controllers/template_parser_controller.dart'; @@ -28,6 +30,7 @@ class RouterKey { static const String textEditor = 'text_editor'; static const String dataCompare = 'data_compare'; static const String dataExtract = 'data_extract'; + static const String xmlSearch = 'xml_search'; static const String memoryTable = 'memory_table'; static const String uftComponent = 'uft_component'; static const String callFunction = 'call_function'; @@ -42,6 +45,7 @@ class ModuleRouter { RouterKey.dataFormat: (tab) => DataFormatController(), RouterKey.dataCompare: (tab) => DataCompareController(), RouterKey.dataExtract: (tab) => DataExtractController(), + RouterKey.xmlSearch: (tab) => XmlSearchController(), RouterKey.memoryTable: (tab) => MemoryTableController(), RouterKey.uftComponent: (tab) => UftComponentController(), RouterKey.callFunction: (tab) => CallFunctionController(), @@ -55,6 +59,7 @@ class ModuleRouter { RouterKey.dataFormat: (tab, controller) => DataFormatView(tabId: tab.id), RouterKey.dataCompare: (tab, controller) => DataCompareView(tabId: tab.id), RouterKey.dataExtract: (tab, controller) => DataExtractView(tabId: tab.id), + RouterKey.xmlSearch: (tab, controller) => XmlSearchView(tabId: tab.id), RouterKey.memoryTable: (tab, controller) => MemoryTableView(tabId: tab.id), RouterKey.uftComponent: (tab, controller) => UftComponentView(tabId: tab.id), RouterKey.callFunction: (tab, controller) => CallFunctionView(tabId: tab.id), 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 4c3a8c9..7fecd97 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 @@ -111,8 +111,8 @@ class TemplateGridView extends StatelessWidget { final dataSource = _TemplateItemDataSource(rows: rows, selectedNodes: selectedNodes); // 构建列 - final columns = [ - MyGridColumn(columnName: 'index', minimumWidth: 60, label: '序号'), + final columns = [ + ShortGridColumn(columnName: 'index', label: '序号'), ...selectedNodes.map((node) { return MyGridColumn( columnName: node.path, @@ -193,7 +193,7 @@ class _TemplateItemDataSource extends DataGridSource { cells: row.getCells().map((dataGridCell) { return Container( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), alignment: dataGridCell.columnName == 'index' ? Alignment.center : Alignment.centerLeft, child: Text(dataGridCell.value.toString()), 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 new file mode 100644 index 0000000..0a11b3a --- /dev/null +++ b/win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart @@ -0,0 +1,91 @@ +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 = '*.*'; + 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; + + bool _isSearching = false; + + bool onlyFileName = false; + bool get isSearching => _isSearching; + + Future executeSearching() async { + Logger().info("开始提取目录:$_searchDirectory, 文件名:$_fileType"); + if (_searchDirectory.isEmpty || _rules.isEmpty) return; + + _isSearching = true; + notifyListeners(); + + _results.clear(); + notifyListeners(); + + try { + final newResults = await _searchService.searchFromDirectory( + directory: _searchDirectory, + fileType: _fileType, + rule: _rules[0], + ); + _results.addAll(newResults); + } catch (e) { + Logger().error("提取目录出错:$e"); + _results.add(SearchResult(rowNum: 1, filePath: 'Error', content: 'Searchion failed: $e')); + } finally { + _isSearching = false; + notifyListeners(); + } + } + + set searchDirectory(String value) { + _searchDirectory = value; + notifyListeners(); + } + + set fileType(String value) { + _fileType = value; + notifyListeners(); + } + + Future pickDirectory() async { + final dir = await FilePicker.platform.getDirectoryPath(); + if (dir != null) { + searchDirectory = dir; + } + } + + 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 + } + + @override + void onOpenFolder(String folderPath) { + searchDirectory = folderPath; + } + + void cancelSearchion() {} +} 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 new file mode 100644 index 0000000..b6a6ea9 --- /dev/null +++ b/win_text_editor/lib/modules/xml_search/models/search_result.dart @@ -0,0 +1,8 @@ +// search_result.dart +class SearchResult { + final int rowNum; + final String filePath; + final String content; + + SearchResult({required this.rowNum, required this.filePath, required this.content}); +} 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 new file mode 100644 index 0000000..106e221 --- /dev/null +++ b/win_text_editor/lib/modules/xml_search/models/xml_rule.dart @@ -0,0 +1,18 @@ +// 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 new file mode 100644 index 0000000..3b7954b --- /dev/null +++ b/win_text_editor/lib/modules/xml_search/services/xml_search_service.dart @@ -0,0 +1,52 @@ +// 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, + }) 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)); + } + } catch (e) { + Logger().error('xmlSearchService.searchFromDirectory方法执行出错: $e'); + results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: 'Error: $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(); + } + } +} 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 new file mode 100644 index 0000000..7f5e079 --- /dev/null +++ b/win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart @@ -0,0 +1,140 @@ +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'; + +class ConditionSetting extends StatefulWidget { + const ConditionSetting({super.key}); + + @override + State createState() => _ConditionSettingState(); +} + +class _ConditionSettingState extends State { + final _nodePathController = TextEditingController(); + final _attributeNameController = TextEditingController(); + final _namespacePrefixController = TextEditingController(); + bool _isFirstOccurrence = false; + bool _isExtracting = false; + + @override + void dispose() { + _nodePathController.dispose(); + _attributeNameController.dispose(); + _namespacePrefixController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final controller = context.watch(); + + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('数据提取规则设置:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + + // 规则输入表单 + TextField( + controller: _nodePathController, + decoration: const InputDecoration( + labelText: '节点名称', + hintText: '如: business:Service', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _attributeNameController, + decoration: const InputDecoration( + labelText: '属性名称', + hintText: '如: chineseName 或 name', + border: OutlineInputBorder(), + ), + ), + + const SizedBox(height: 12), + + MyCheckbox( + title: '仅提取第一个匹配项', + value: _isFirstOccurrence, + onChanged: (value) => setState(() => _isFirstOccurrence = value ?? false), + ), + const SizedBox(height: 16), + // 操作按钮行 + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.play_arrow), + label: const Text('开始'), + onPressed: _isExtracting ? null : () => _startSearching(controller), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.stop, color: Colors.red), + label: const Text('停止', style: TextStyle(color: Colors.red)), + onPressed: _isExtracting ? _stopExtraction : null, + ), + ), + ], + ), + ], + ), + ), + ); + } + + 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('请先选择搜索目录'))); + return; + } + + _setRule(); + + setState(() => _isExtracting = true); + try { + await controller.executeSearching(); + } finally { + if (mounted) { + setState(() => _isExtracting = false); + } + } + } + + void _stopExtraction() { + // 这里需要确保控制器中有取消提取的逻辑 + final controller = Provider.of(context, listen: false); + // 假设控制器中有cancelExtraction方法 + controller.executeSearching(); + setState(() => _isExtracting = 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 new file mode 100644 index 0000000..d279b6d --- /dev/null +++ b/win_text_editor/lib/modules/xml_search/widgets/directory.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; + +class Directory extends StatefulWidget { + const Directory({super.key}); + + @override + State createState() => _DirectoryState(); +} + +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(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, controller, child) { + // 同步 TextEditingController 的值 + if (_searchDirectoryController.text != controller.searchDirectory) { + _searchDirectoryController.text = controller.searchDirectory; + } + if (_fileTypeController.text != controller.fileType) { + _fileTypeController.text = controller.fileType; + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + 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(), + ), + onChanged: (value) => controller.fileType = value, + ), + ), + 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 new file mode 100644 index 0000000..4553c35 --- /dev/null +++ b/win_text_editor/lib/modules/xml_search/widgets/results_view.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.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 'dart:io'; + +import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; +import 'package:win_text_editor/shared/components/my_grid_column.dart'; + +class ResultsView extends StatelessWidget { + const ResultsView({super.key}); + + @override + Widget build(BuildContext context) { + final controller = context.watch(); + + return Card( + child: GestureDetector( + onSecondaryTapDown: (details) { + _showContextMenu(context, details.globalPosition, controller); + }, + child: _buildLocateGrid(controller), + ), + ); + } + + Future _showContextMenu( + BuildContext context, + Offset position, + XmlSearchController controller, + ) async { + // 获取渲染对象以正确定位菜单 + final renderBox = context.findRenderObject() as RenderBox; + final localPosition = renderBox.globalToLocal(position); + + // 显示菜单并等待选择结果 + final result = await showMenu( + context: context, + position: RelativeRect.fromLTRB( + position.dx, + position.dy, + position.dx + renderBox.size.width - localPosition.dx, + position.dy + renderBox.size.height - localPosition.dy, + ), + items: [ + const PopupMenuItem(value: 'exportFileName', child: Text('导出文件名(csv)')), + const PopupMenuItem(value: 'exportContent', child: Text('导出内容(csv)')), + const PopupMenuItem(value: 'exportAll', child: Text('导出全部(csv)')), + ], + ); + + // 处理菜单选择结果 + if (result != null && result.startsWith('export') && context.mounted) { + try { + await _exportToCsv(controller, result); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); + } + } + } + } + + Future _exportToCsv(XmlSearchController controller, String? exportType) async { + String csvData = ''; + csvData = + exportType == 'exportFileName' + ? '文件名称\n' + : (exportType == 'exportContent' ? '内容\n' : '文件名称\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'; + } + } + + final filePath = await FilePicker.platform.saveFile( + dialogTitle: '保存导出结果', + fileName: 'search_results.csv', + type: FileType.custom, + allowedExtensions: ['csv'], + ); + + if (filePath != null) { + final file = File(filePath); + await file.writeAsString(csvData); + } + } + + Widget _buildLocateGrid(XmlSearchController controller) { + return SfDataGrid( + rowHeight: 32, + headerRowHeight: 32, + source: LocateDataSource(controller), + columns: [ + ShortGridColumn(columnName: 'rowNum', label: '序号'), + MyGridColumn(columnName: 'file', label: '文件名称', minimumWidth: 300), + MyGridColumn(columnName: 'content', label: '内容'), + ], + selectionMode: SelectionMode.multiple, + navigationMode: GridNavigationMode.cell, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + allowSorting: false, + allowFiltering: false, + columnWidthMode: ColumnWidthMode.fill, + isScrollbarAlwaysShown: true, + allowColumnsResizing: true, // 关键开关 + columnResizeMode: ColumnResizeMode.onResizeEnd, + ); + } +} + +class LocateDataSource extends DataGridSource { + final XmlSearchController controller; + + LocateDataSource(this.controller); + + @override + List get rows => + controller.results.map((result) { + return DataGridRow( + cells: [ + DataGridCell(columnName: 'rowNum', value: result.rowNum), + DataGridCell(columnName: 'file', value: path.basename(result.filePath)), + DataGridCell(columnName: 'content', value: result.content), + ], + ); + }).toList(); + + @override + DataGridRowAdapter? buildRow(DataGridRow row) { + 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(), + ); + } +} diff --git a/win_text_editor/lib/modules/xml_search/widgets/xml_search_view.dart b/win_text_editor/lib/modules/xml_search/widgets/xml_search_view.dart new file mode 100644 index 0000000..eb9bc62 --- /dev/null +++ b/win_text_editor/lib/modules/xml_search/widgets/xml_search_view.dart @@ -0,0 +1,63 @@ +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/widgets/condition_setting.dart'; +import 'results_view.dart'; +import 'directory.dart'; +import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; + +class XmlSearchView extends StatefulWidget { + final String tabId; + const XmlSearchView({super.key, required this.tabId}); + + @override + XmlSearchViewState createState() => XmlSearchViewState(); +} + +class XmlSearchViewState extends State { + late final XmlSearchController _controller; + + get tabManager => Provider.of(context, listen: false); + + @override + void initState() { + super.initState(); + _controller = tabManager.getController(widget.tabId) ?? XmlSearchController(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: _controller, + child: Consumer( + builder: (context, controller, child) { + return const Padding( + padding: EdgeInsets.all(4.0), + child: Column( + children: [ + Directory(), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 左侧部分 (50%) + Expanded(flex: 3, child: ConditionSetting()), + SizedBox(width: 8), + // 右侧部分 (50%) + Expanded(flex: 7, child: ResultsView()), + ], + ), + ), + ], + ), + ); + }, + ), + ); + } +} 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 31a24e4..76de635 100644 --- a/win_text_editor/lib/shared/components/my_grid_column.dart +++ b/win_text_editor/lib/shared/components/my_grid_column.dart @@ -2,17 +2,22 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; class MyGridColumn extends GridColumn { - MyGridColumn({required String columnName, double minimumWidth = 100, required String label}) - : super( - columnName: columnName, - minimumWidth: minimumWidth, - label: Container( - alignment: Alignment.center, - color: Colors.grey[200], - padding: const EdgeInsets.all(2.0), - child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)), - ), - ); + MyGridColumn({ + required String columnName, + double minimumWidth = 100, + double maximumWidth = double.infinity, + required String label, + }) : super( + columnName: columnName, + minimumWidth: minimumWidth, + maximumWidth: maximumWidth, + label: Container( + alignment: Alignment.center, + color: Colors.grey[200], + padding: const EdgeInsets.all(2.0), + child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)), + ), + ); } class ShortGridColumn extends GridColumn {