From 6973bbef7b0f2e015a1f6ab3e4995127fd05a192 Mon Sep 17 00:00:00 2001 From: hejl Date: Tue, 3 Jun 2025 17:45:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=B7=A5=E5=85=B7-xml=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assets/config/uft_macro_list.yaml | 78 ++++++++- .../controllers/tab_items_controller.dart | 1 - 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 + .../controllers/call_function_controller.dart | 88 +++++----- .../call_function/models/call_function.dart | 47 +++--- .../services/call_function_service.dart | 52 +++--- .../widgets/call_function_left_side.dart | 4 +- .../widgets/call_function_right_side.dart | 42 +++++ .../services/base_search_service.dart | 78 +-------- .../services/count_search_service.dart | 3 +- .../services/custom_search_service.dart | 5 +- .../controllers/data_extract_controller.dart | 91 +++++++++++ .../data_extract/models/search_result.dart | 8 + .../modules/data_extract/models/xml_rule.dart | 18 +++ .../data_extract/services/simple_xpath.dart | 100 ++++++++++++ .../services/xml_extract_service.dart | 53 +++++++ .../widgets/condition_setting.dart | 150 ++++++++++++++++++ .../widgets/data_extract_view.dart | 63 ++++++++ .../data_extract/widgets/directory.dart | 86 ++++++++++ .../data_extract/widgets/results_view.dart | 138 ++++++++++++++++ .../lib/modules/module_router.dart | 5 + .../lib/shared/utils/file_utils.dart | 79 ++++++++- win_text_editor/pubspec.yaml | 1 + 25 files changed, 1025 insertions(+), 175 deletions(-) create mode 100644 win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart create mode 100644 win_text_editor/lib/modules/data_extract/models/search_result.dart create mode 100644 win_text_editor/lib/modules/data_extract/models/xml_rule.dart create mode 100644 win_text_editor/lib/modules/data_extract/services/simple_xpath.dart create mode 100644 win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart create mode 100644 win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart create mode 100644 win_text_editor/lib/modules/data_extract/widgets/data_extract_view.dart create mode 100644 win_text_editor/lib/modules/data_extract/widgets/directory.dart create mode 100644 win_text_editor/lib/modules/data_extract/widgets/results_view.dart diff --git a/win_text_editor/assets/config/uft_macro_list.yaml b/win_text_editor/assets/config/uft_macro_list.yaml index 0b5ee74..14409ea 100644 --- a/win_text_editor/assets/config/uft_macro_list.yaml +++ b/win_text_editor/assets/config/uft_macro_list.yaml @@ -164,4 +164,80 @@ templates: {{#fields}} {{name}} = @{{name}} {{^isLast}}, {{/isLast}} {{/fields}} - ] \ No newline at end of file + ] + + 遍历组件所有: + body: | + {{#hasIndex}}[组件排序][{{name}}({{index.name}})]{{/hasIndex}} + [遍历组件开始][{{name}}{{#hasIndex}}({{index.name}}){{/hasIndex}}][ + {{#hasIndex}} + {{#index.fields}} + {{name}} = @{{name}} {{^isLast}}, {{/isLast}} + {{/index.fields}} + {{/hasIndex}} + ][ + {{#fields}} + {{name}} = @{{name}} {{^isLast}}, {{/isLast}} + {{/fields}} + ] + { + + } + "[遍历组件结束]" + + 普通调用: + body: | + [{{chineseName}}][ + {{#input}} + {{name}} = @{{name}} {{^isLast}}, {{/isLast}} + {{/input}} + ][ + {{#output}} + {{name}} = @{{name}} {{^isLast}}, {{/isLast}} + {{/output}} + ] + [处理失败] + { + [获取错误信息][@error_no][@error_info][@error_pathinfo] + //TODO:处理异常 + + [继续执行] + } + else + { + //balabala + } + + 事务调用: + body: | + [事务处理开始] + [{{chineseName}}][ + {{#input}} + {{name}} = @{{name}} {{^isLast}}, {{/isLast}} + {{/input}} + ][ + {{#output}} + {{name}} = @{{name}} {{^isLast}}, {{/isLast}} + {{/output}} + ] + [处理失败] + { + [获取错误信息][@error_no][@error_info][@error_pathinfo] + //TODO:处理异常 + [事务回滚] + [继续执行] + } + else + { + [事务处理结束] + } + + 因子服务: + body: | + LPFN_RISK lpOpenRisk = lpIUFTContext->GetRiskEntry({{functionNo}}); + if(NULL == lpOpenRisk) + { + [记录日志][CNST_DLOG_ERROR][]["获取因子RS_{{functionNo}}失败"][] + } + lpIUFTContext->AsyncExecMsg((void*) lpOpenRisk, lpIUFTContext, (void*)&p_C{{factorParam}}); + \ No newline at end of file diff --git a/win_text_editor/lib/framework/controllers/tab_items_controller.dart b/win_text_editor/lib/framework/controllers/tab_items_controller.dart index 0dddf15..48c7d05 100644 --- a/win_text_editor/lib/framework/controllers/tab_items_controller.dart +++ b/win_text_editor/lib/framework/controllers/tab_items_controller.dart @@ -122,7 +122,6 @@ class TabItemsController with ChangeNotifier { openOrActivateTab("内存表", RouterKey.memoryTable, Icons.list); break; case 'uftfunction': - case 'uftservice': case 'uftatomfunction': case 'uftatomservice': case 'uftfactorfunction': diff --git a/win_text_editor/lib/menus/app_menu.dart b/win_text_editor/lib/menus/app_menu.dart index 68e2fea..9fd6145 100644 --- a/win_text_editor/lib/menus/app_menu.dart +++ b/win_text_editor/lib/menus/app_menu.dart @@ -42,6 +42,10 @@ class AppMenu extends StatelessWidget { value: MenuConstants.dataFormat, child: ListTile(leading: Icon(Icons.date_range), title: Text('数据格式化')), ), + const PopupMenuItem( + value: MenuConstants.dataExtract, + child: ListTile(leading: Icon(Icons.outbox), 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 5746fe1..861c448 100644 --- a/win_text_editor/lib/menus/menu_actions.dart +++ b/win_text_editor/lib/menus/menu_actions.dart @@ -15,6 +15,7 @@ class MenuActions { MenuConstants.templateParser: _openTemplateParser, MenuConstants.dataFormat: _dataFormat, MenuConstants.dataCompare: _dataCompare, + MenuConstants.dataExtract: _dataExtract, MenuConstants.memoryTable: _memoryTable, MenuConstants.uftComponent: _uftComponent, MenuConstants.callFunction: _callFunction, @@ -69,6 +70,10 @@ class MenuActions { await _openOrActivateTab(context, "数据对比", RouterKey.dataCompare, Icons.compare); } + static Future _dataExtract(BuildContext context) async { + await _openOrActivateTab(context, "XML数据提取", RouterKey.dataExtract, Icons.outbox); + } + 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 cfc0520..33dee40 100644 --- a/win_text_editor/lib/menus/menu_constants.dart +++ b/win_text_editor/lib/menus/menu_constants.dart @@ -17,6 +17,7 @@ class MenuConstants { static const String templateParser = 'template_parser'; static const String dataFormat = 'data_format'; static const String dataCompare = 'data_compare'; + static const String dataExtract = 'data_extract'; static const String demo = 'demo'; // AIGC菜单项 diff --git a/win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart b/win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart index 97a5772..9d292b1 100644 --- a/win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart +++ b/win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/framework/services/macro_template_service.dart'; @@ -7,22 +8,20 @@ import 'package:win_text_editor/shared/uft_std_fields/field_data_source.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart'; class CallFunctionController extends BaseContentController { - String? _errorMessage; - String tableName = ""; - String objectId = ""; - String chineseName = ""; - + CallFunction modle = CallFunction( + functionType: '', + functionNo: '', + chineseName: '', + inputParameters: [], + outputParameters: [], + ); late DataGridSource inputSource; late DataGridSource outputSource; + final CallFunctionService _service; final MacroTemplateService templateService = MacroTemplateService(); - // 新增:维护CallFunction对象 - late CallFunction _modle; - CallFunctionController() : _service = CallFunctionService(Logger()) { - // 初始化空数据 - inputSource = FieldsDataSource( [], onSelectionChanged: (index, isSelected) { @@ -36,16 +35,8 @@ class CallFunctionController extends BaseContentController { updateOutputSelection(index, isSelected); }, ); - - // 初始化CallFunction - _modle = CallFunction(functionName: '', inputParameters: [], outputParameters: []); } - String? get errorMessage => _errorMessage; - - // 新增:获取当前CallFunction - CallFunction get memoryTable => _modle; - void initTemplateService() { if (!templateService.inited) { templateService.init(); @@ -54,37 +45,49 @@ class CallFunctionController extends BaseContentController { String? genCodeString(List macroList) { initTemplateService(); - return templateService.renderTemplate(macroList, _modle.toMap()); + StringBuffer sb = StringBuffer(); + + //因子服务单独处理 + if (modle.functionType == 'uftfactorservice') { + sb.write(templateService.renderTemplate(['因子服务'], modle.toMap())); + return sb.toString(); + } + + //入参组件创建 + for (final input in modle.inputParameters) { + if (input.isSelected && input.type == CallFunction.componentType) { + final component = modle.componentList?.firstWhereOrNull((c) => c.name == input.name); + if (component != null) { + sb.write(templateService.renderTemplate(['插入组件'], component.toMap())); + } + } + } + sb.write(templateService.renderTemplate(macroList, modle.toMap())); + //出参组件遍历 + for (final output in modle.outputParameters) { + if (output.isSelected && output.type == CallFunction.componentType) { + final component = modle.componentList?.firstWhereOrNull((c) => c.name == output.name); + if (component != null) { + sb.write(templateService.renderTemplate(['遍历组件所有'], component.toMap())); + } + } + } + return sb.toString(); } @override Future onOpenFile(String filePath) async { Logger().info("Opening file: $filePath"); try { - final FunctionData functionData = await _service.parseXmlFile(filePath); - - // Update controller state - chineseName = functionData.chineseName; - objectId = functionData.objectId; - - // Update data sources - (inputSource as FieldsDataSource).updateData(functionData.inputFields); - (outputSource as FieldsDataSource).updateData(functionData.outputFields); + modle = await _service.parseXmlFile(filePath); - // 更新CallFunction对象 - _modle = CallFunction( - functionName: tableName, - inputParameters: functionData.inputFields, - outputParameters: functionData.outputFields, - ); - - // Clear any previous error - _errorMessage = null; + // datagrid显示的数据源 + (inputSource as FieldsDataSource).updateData(modle.inputParameters); + (outputSource as FieldsDataSource).updateData(modle.outputParameters); // Notify UI to update notifyListeners(); } catch (e) { - _errorMessage = e.toString(); notifyListeners(); Logger().error("Error opening file: $e"); } @@ -98,7 +101,7 @@ class CallFunctionController extends BaseContentController { inputSource.notifyListeners(); // 同步更新CallFunction - _modle.inputParameters[index].isSelected = isSelected; + modle.inputParameters[index].isSelected = isSelected; notifyListeners(); } } @@ -110,7 +113,7 @@ class CallFunctionController extends BaseContentController { outputSource.notifyListeners(); // 同步更新CallFunction - _modle.outputParameters[index].isSelected = isSelected; + modle.outputParameters[index].isSelected = isSelected; notifyListeners(); } } @@ -119,9 +122,4 @@ class CallFunctionController extends BaseContentController { void onOpenFolder(String folderPath) { // 不支持打开文件夹 } - - @override - void dispose() { - super.dispose(); - } } diff --git a/win_text_editor/lib/modules/call_function/models/call_function.dart b/win_text_editor/lib/modules/call_function/models/call_function.dart index db422c1..50eb108 100644 --- a/win_text_editor/lib/modules/call_function/models/call_function.dart +++ b/win_text_editor/lib/modules/call_function/models/call_function.dart @@ -1,24 +1,32 @@ +import 'package:win_text_editor/modules/uft_component/models/uft_component.dart'; import 'package:win_text_editor/shared/models/std_filed.dart'; class CallFunction { - final String functionName; + static const String componentType = 'COMPONENT'; + final String functionType; + final String functionNo; + final String chineseName; final List inputParameters; final List outputParameters; + List? componentList; + String? factorParam; CallFunction({ - required this.functionName, + required this.functionType, + required this.functionNo, + required this.chineseName, required this.inputParameters, required this.outputParameters, + this.componentList, + this.factorParam, }); - List get selectInputFields => inputParameters.where((field) => field.isSelected).toList(); - List get selectOutputFields => - outputParameters.where((field) => field.isSelected).toList(); - Map toMap() { return { - 'tableName': functionName, - 'fields': + 'functionNo': functionNo, + 'chineseName': chineseName, + 'factorParam': factorParam ?? '', + 'input': inputParameters .map( (field) => { @@ -30,27 +38,26 @@ class CallFunction { }, ) .toList(), - 'selectInputFields': - selectInputFields + 'output': + outputParameters .map( (field) => { 'id': field.id, 'name': field.name, 'chineseName': field.chineseName, 'type': field.type, - 'isLast': selectInputFields.indexOf(field) == selectInputFields.length - 1, + 'isLast': outputParameters.indexOf(field) == outputParameters.length - 1, }, ) .toList(), - 'selectOutputFields': - selectOutputFields - .map( - (field) => { - 'id': field.id, - 'name': field.name, - 'chineseName': field.chineseName, - 'type': field.type, - 'isLast': selectInputFields.indexOf(field) == selectInputFields.length - 1, + 'inputComps': + componentList + ?.map( + (componen) => { + 'id': componen.id, + 'name': componen.name, + 'chineseName': componen.chineseName, + 'fields': componen.fields.map((field) => {'name': field.name}), }, ) .toList(), diff --git a/win_text_editor/lib/modules/call_function/services/call_function_service.dart b/win_text_editor/lib/modules/call_function/services/call_function_service.dart index 40742d6..382e35f 100644 --- a/win_text_editor/lib/modules/call_function/services/call_function_service.dart +++ b/win_text_editor/lib/modules/call_function/services/call_function_service.dart @@ -1,6 +1,8 @@ // memory_table_service.dart import 'dart:io'; +import 'package:collection/collection.dart'; +import 'package:win_text_editor/modules/call_function/models/call_function.dart'; import 'package:win_text_editor/modules/uft_component/models/uft_component.dart'; import 'package:win_text_editor/modules/uft_component/services/uft_component_service.dart'; import 'package:win_text_editor/shared/data/std_fields_cache.dart'; @@ -22,7 +24,7 @@ class CallFunctionService { 'uftfactorservice': 'business:FactorService', }; - Future parseXmlFile(String filePath) async { + Future parseXmlFile(String filePath) async { try { // 1. Check file extension final extendFileName = filePath.toLowerCase().split('.').last; @@ -52,19 +54,30 @@ class CallFunctionService { List componentList = []; // 5. Process inputParameters (fields) - final inputParameters = document.findAllElements('inputParameters'); + final inputParameters = document.findAllElements( + extendFileName == "uftfactorservice" ? 'internalParams' : 'inputParameters', + ); final inputFields = await parserFields(filePath, inputParameters, componentList); + final factorParam = + extendFileName == "uftfactorservice" + ? inputFields + .firstWhereOrNull((field) => field.type == CallFunction.componentType) + ?.name + : ''; + // 6. Process outputParameters final outputParameters = document.findAllElements('outputParameters'); final outputFields = await parserFields(filePath, outputParameters, componentList); - return FunctionData( + return CallFunction( + functionType: extendFileName, chineseName: chineseName, - objectId: objectId, - inputFields: inputFields, - outputFields: outputFields, + functionNo: objectId, + inputParameters: inputFields, + outputParameters: outputFields, componentList: componentList, + factorParam: factorParam, ); } on xml.XmlParserException catch (e) { _logger.error("XML解析错误: ${e.message}"); @@ -88,16 +101,17 @@ class CallFunctionService { final id = parameter.getAttribute('id') ?? ''; final paramType = parameter.getAttribute('paramType') ?? 'FIELD'; - if (paramType == 'COMPONENT') { - final component = await UftComponentService.getUftComponent(filePath, id); - // _logger.debug("value.id:${component?.name}, chineseName:${component?.chineseName}"); - if (component != null) componentList.add(component); + if (paramType == CallFunction.componentType) { + var component = await UftComponentService.getUftComponent(filePath, id); + if (component != null) { + componentList.add(component); + } fields.add( Field( index.toString(), id, component?.chineseName ?? '', // 使用组件的中文名 - 'COMPONENT', + paramType, ), ); } else { @@ -111,19 +125,3 @@ class CallFunctionService { return fields; } } - -class FunctionData { - final String chineseName; - final String objectId; - final List inputFields; - final List outputFields; - List? componentList; - - FunctionData({ - required this.chineseName, - required this.objectId, - required this.inputFields, - required this.outputFields, - this.componentList, - }); -} diff --git a/win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart b/win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart index 73185e9..bea089d 100644 --- a/win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart +++ b/win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart @@ -38,8 +38,8 @@ class CallFunctionLeftSide extends StatelessWidget { spacing: 16, runSpacing: 8, children: [ - _buildTextFieldRow('功能号', controller.objectId, 100), - _buildTextFieldRow('中文名', controller.chineseName, 350), + _buildTextFieldRow('功能号', controller.modle.functionNo, 100), + _buildTextFieldRow('中文名', controller.modle.chineseName, 350), ], ), ), diff --git a/win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart b/win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart index 7d7b450..d39a2a9 100644 --- a/win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart +++ b/win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:win_text_editor/modules/call_function/controllers/call_function_controller.dart'; +import 'package:win_text_editor/shared/components/my_checkbox.dart'; class CallFunctionRightSide extends StatefulWidget { final CallFunctionController controller; @@ -36,6 +37,7 @@ class _CallFunctionRightSideState extends State { Widget build(BuildContext context) { return Column( children: [ + _buildCheckboxSection(), Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0), child: Row( @@ -62,6 +64,46 @@ class _CallFunctionRightSideState extends State { ); } + Widget _buildCheckboxSection() { + final operations = ['普通调用', '事务调用']; + + return SizedBox( + width: double.infinity, + child: Card( + child: Padding( + padding: const EdgeInsets.only(left: 8, top: 4, bottom: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: operations.map((op) => _buildCheckbox(op)).toList(), + ), + ], + ), + ), + ), + ); + } + + Widget _buildCheckbox(String label) => MyCheckbox( + title: label, + value: _selectedOperations.contains(label), + onChanged: (bool? value) => _toggleOperation(label, value), + ); + + void _toggleOperation(String operation, bool? value) { + setState(() { + if (value == true) { + _selectedOperations.add(operation); + } else { + _selectedOperations.remove(operation); + } + _updateDisplay(); + }); + } + Widget _buildCodeEditor() { return Card( child: Padding( 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 2e7b257..79c0a5b 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 @@ -1,6 +1,6 @@ // lib/app/modules/content_search/services/base_search_service.dart import 'dart:io'; -import 'package:path/path.dart' as path; +import 'package:win_text_editor/shared/utils/file_utils.dart'; typedef ProgressCallback = void Function(double progress); typedef FileProcessor = Future Function(File file, String content); @@ -25,7 +25,7 @@ abstract class BaseSearchService { await for (final entity in dir.list(recursive: true)) { if (shouldStop?.call() == true) return; - if (entity is! File || !matchesFileType(entity.path, fileType)) continue; + if (entity is! File || !FileUtils.matchesFileType(entity.path, fileType)) continue; processedFiles++; final progress = (processedFiles / totalFiles) * 99 + 1; @@ -72,82 +72,10 @@ abstract class BaseSearchService { int totalFiles = 0; await for (final entity in dir.list(recursive: true)) { if (shouldStop?.call() == true) return totalFiles; - if (entity is File && matchesFileType(entity.path, fileType)) { + if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) { totalFiles++; } } return totalFiles; } - - static bool matchesFileType(String filePath, String fileType) { - // 处理特殊情况 - if (fileType == '*.*') return true; - if (fileType == '*') return true; - - // 分割通配符为文件名和扩展名部分 - final parts = fileType.split('.'); - final patternName = parts.length > 0 ? parts[0] : ''; - final patternExt = parts.length > 1 ? parts.sublist(1).join('.') : ''; - - // 获取文件的实际名称和扩展名 - final fileName = path.basename(filePath); - final fileExt = path.extension(fileName).toLowerCase().replaceFirst('.', ''); - - // 匹配文件名部分(如果有指定) - bool nameMatches = true; - if (patternName.isNotEmpty && patternName != '*') { - nameMatches = matchesWildcard(fileName, patternName); - } - - // 匹配扩展名部分(如果有指定) - bool extMatches = true; - if (patternExt.isNotEmpty) { - if (patternExt == '*') { - extMatches = true; - } else { - extMatches = matchesWildcard(fileExt, patternExt); - } - } - - return nameMatches && extMatches; - } - - // 辅助函数:判断字符串是否匹配通配符模式 - static bool matchesWildcard(String input, String pattern) { - // 处理特殊情况 - if (pattern == '*') return true; - - // 动态规划实现通配符匹配 - final m = input.length; - final n = pattern.length; - final dp = List.generate(m + 1, (_) => List.filled(n + 1, false)); - - // 空字符串匹配空模式 - dp[0][0] = true; - - // 处理模式以 * 开头的情况 - for (int j = 1; j <= n; j++) { - if (pattern[j - 1] == '*') { - dp[0][j] = dp[0][j - 1]; - } - } - - // 填充动态规划表 - for (int i = 1; i <= m; i++) { - for (int j = 1; j <= n; j++) { - if (pattern[j - 1] == '*') { - // * 可以匹配任意字符或空字符 - dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; - } else if (pattern[j - 1] == '?' || input[i - 1] == pattern[j - 1]) { - // ? 匹配单个字符,或者字符直接相等 - dp[i][j] = dp[i - 1][j - 1]; - } else { - // 不匹配 - dp[i][j] = false; - } - } - } - - return dp[m][n]; - } } diff --git a/win_text_editor/lib/modules/content_search/services/count_search_service.dart b/win_text_editor/lib/modules/content_search/services/count_search_service.dart index 4766c1b..3bf4b95 100644 --- a/win_text_editor/lib/modules/content_search/services/count_search_service.dart +++ b/win_text_editor/lib/modules/content_search/services/count_search_service.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:math'; import 'package:win_text_editor/modules/content_search/services/base_search_service.dart'; +import 'package:win_text_editor/shared/utils/file_utils.dart'; class CountSearchService { static const _maxConcurrentIsolates = 8; // 根据CPU核心数调整 @@ -128,7 +129,7 @@ class CountSearchService { final paths = []; await for (final entity in dir.list(recursive: true)) { if (shouldStop?.call() == true) break; - if (entity is File && BaseSearchService.matchesFileType(entity.path, fileType)) { + if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) { paths.add(entity.path); } } diff --git a/win_text_editor/lib/modules/content_search/services/custom_search_service.dart b/win_text_editor/lib/modules/content_search/services/custom_search_service.dart index 5a5d61f..9504738 100644 --- a/win_text_editor/lib/modules/content_search/services/custom_search_service.dart +++ b/win_text_editor/lib/modules/content_search/services/custom_search_service.dart @@ -7,6 +7,7 @@ import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/modules/content_search/models/search_mode.dart'; import 'package:win_text_editor/modules/content_search/models/search_result.dart'; import 'package:win_text_editor/modules/content_search/services/base_search_service.dart'; +import 'package:win_text_editor/shared/utils/file_utils.dart'; typedef ProgressCallback = void Function(double progress); @@ -34,7 +35,7 @@ class CustomSearchService { int totalFiles = 0; await for (final entity in dir.list(recursive: true)) { if (shouldStop?.call() == true) return results; - if (entity is File && BaseSearchService.matchesFileType(entity.path, fileType)) { + if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) { totalFiles++; } } @@ -46,7 +47,7 @@ class CustomSearchService { await for (final entity in dir.list(recursive: true)) { if (shouldStop?.call() == true) return results; - if (entity is File && BaseSearchService.matchesFileType(entity.path, fileType)) { + if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) { try { final lines = await entity.readAsLines(); for (int i = 0; i < lines.length; i++) { diff --git a/win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart b/win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart new file mode 100644 index 0000000..c4044e6 --- /dev/null +++ b/win_text_editor/lib/modules/data_extract/controllers/data_extract_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/data_extract/models/search_result.dart'; +import 'package:win_text_editor/modules/data_extract/models/xml_rule.dart'; +import 'package:win_text_editor/modules/data_extract/services/xml_extract_service.dart'; +import 'package:win_text_editor/shared/base/base_content_controller.dart'; + +class DataExtractController extends BaseContentController { + String _searchDirectory = ''; + String _fileType = '*.*'; + final List _results = []; + final List _rules = []; + final XmlExtractService _extractService = XmlExtractService(); + + List get results => _results; + List get rules => _rules; + + String get searchDirectory => _searchDirectory; + String get fileType => _fileType; + + bool _isExtracting = false; + + bool onlyFileName = false; + bool get isExtracting => _isExtracting; + + Future executeExtraction() async { + Logger().info("开始提取目录:$_searchDirectory, 文件名:$_fileType"); + if (_searchDirectory.isEmpty || _rules.isEmpty) return; + + _isExtracting = true; + notifyListeners(); + + _results.clear(); + notifyListeners(); + + try { + final newResults = await _extractService.extractFromDirectory( + directory: _searchDirectory, + fileType: _fileType, + rule: _rules[0], + ); + _results.addAll(newResults); + } catch (e) { + Logger().error("提取目录出错:$e"); + _results.add(SearchResult(rowNum: 1, filePath: 'Error', content: 'Extraction failed: $e')); + } finally { + _isExtracting = 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 cancelExtraction() {} +} diff --git a/win_text_editor/lib/modules/data_extract/models/search_result.dart b/win_text_editor/lib/modules/data_extract/models/search_result.dart new file mode 100644 index 0000000..b6a6ea9 --- /dev/null +++ b/win_text_editor/lib/modules/data_extract/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/data_extract/models/xml_rule.dart b/win_text_editor/lib/modules/data_extract/models/xml_rule.dart new file mode 100644 index 0000000..106e221 --- /dev/null +++ b/win_text_editor/lib/modules/data_extract/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/data_extract/services/simple_xpath.dart b/win_text_editor/lib/modules/data_extract/services/simple_xpath.dart new file mode 100644 index 0000000..470c7ab --- /dev/null +++ b/win_text_editor/lib/modules/data_extract/services/simple_xpath.dart @@ -0,0 +1,100 @@ +import 'package:xml/xml.dart' as xml; + +class SimpleXPath { + static List query(xml.XmlNode node, String path) { + final segments = path.split('/').where((s) => s.isNotEmpty).toList(); + var current = [node]; + + for (var i = 0; i < segments.length; i++) { + final segment = segments[i]; + final isRecursive = segment.isEmpty; // 修正:空段表示前导// + + current = current.expand((n) => _findNodes(n, segment, isRecursive)).toList(); + if (current.isEmpty) break; + } + + return current; + } + + static Iterable _findNodes(xml.XmlNode node, String segment, bool recursive) sync* { + if (segment == '..') { + if (node.parent != null) yield node.parent!; + } else if (segment == '.') { + yield node; + } else if (segment == 'text()') { + if (node is xml.XmlText || node is xml.XmlAttribute) { + yield node; + } else { + yield* node.children.whereType(); + } + } else if (segment.startsWith('@')) { + final attrName = segment.substring(1); + if (node is xml.XmlElement) { + yield* node.attributes.where((a) => a.name.local == attrName); + } + } else { + if (node is xml.XmlElement) { + if (recursive) { + // 递归查找所有子节点 + yield* _findNodesRecursive(node, segment); + } else { + // 只查找直接子节点 + yield* node.children.whereType().where( + (child) => child.name.local == segment, + ); + } + } + } + } + + // 新增递归查找方法 + static Iterable _findNodesRecursive(xml.XmlNode node, String segment) sync* { + if (node is xml.XmlElement) { + if (node.name.local == segment) { + yield node; + } + for (final child in node.children) { + yield* _findNodesRecursive(child, segment); + } + } + } +} + +void main() { + final doc = xml.XmlDocument.parse(''' + + + Text 1 + + Text 2 + + + + '''); + + void printResults(String query, List results) { + print('\nQuery: "$query"'); + if (results.isEmpty) { + print('No results found'); + } else { + print('Found ${results.length} results:'); + results.forEach((node) { + if (node is xml.XmlAttribute) { + print('Attribute: ${node.name}=${node.value}'); + } else if (node is xml.XmlText) { + print('Text: ${node.text}'); + } else { + print('Element: ${node.toXmlString()}'); + } + }); + } + } + + // 测试查询 + printResults('//items', SimpleXPath.query(doc, '//items')); + printResults('//item', SimpleXPath.query(doc, '//item')); + printResults('//item/text()', SimpleXPath.query(doc, '//item/text()')); + printResults('//item/@id', SimpleXPath.query(doc, '//item/@id')); + printResults('/root/items/item', SimpleXPath.query(doc, '/root/items/item')); + printResults('//nonexistent', SimpleXPath.query(doc, '//nonexistent')); +} diff --git a/win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart b/win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart new file mode 100644 index 0000000..242158e --- /dev/null +++ b/win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart @@ -0,0 +1,53 @@ +// xml_extract_service.dart +import 'dart:io'; +import 'package:win_text_editor/framework/controllers/logger.dart'; +import 'package:win_text_editor/modules/data_extract/services/simple_xpath.dart'; +import 'package:win_text_editor/shared/utils/file_utils.dart'; +import 'package:xml/xml.dart' as xml; +import 'package:win_text_editor/modules/data_extract/models/search_result.dart'; +import 'package:win_text_editor/modules/data_extract/models/xml_rule.dart'; + +class XmlExtractService { + Future> extractFromDirectory({ + 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 = _extractWithRule(document, rule); + for (var value in values) { + results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: value)); + } + } catch (e) { + Logger().error('XmlExtractService.extractFromDirectory方法执行出错: $e'); + results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: 'Error: $e')); + } + } + } + + return results; + } + + List _extractWithRule(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/data_extract/widgets/condition_setting.dart b/win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart new file mode 100644 index 0000000..1649a60 --- /dev/null +++ b/win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart'; +import 'package:win_text_editor/modules/data_extract/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: '节点路径 (XPath)', + hintText: '如: //business:Service 或 /root/items', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _attributeNameController, + decoration: const InputDecoration( + labelText: '属性名称', + hintText: '如: chineseName 或 name', + border: OutlineInputBorder(), + ), + ), + + const SizedBox(height: 12), + const Text('命名空间配置 (可选):'), + TextField( + controller: _namespacePrefixController, + decoration: const InputDecoration( + labelText: '命名空间前缀', + hintText: '如: business', + 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 : () => _startExtraction(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 _startExtraction(DataExtractController controller) async { + if (controller.searchDirectory.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择搜索目录'))); + return; + } + + _setRule(); + + setState(() => _isExtracting = true); + try { + await controller.executeExtraction(); + } finally { + if (mounted) { + setState(() => _isExtracting = false); + } + } + } + + void _stopExtraction() { + // 这里需要确保控制器中有取消提取的逻辑 + final controller = Provider.of(context, listen: false); + // 假设控制器中有cancelExtraction方法 + controller.cancelExtraction(); + setState(() => _isExtracting = false); + } +} diff --git a/win_text_editor/lib/modules/data_extract/widgets/data_extract_view.dart b/win_text_editor/lib/modules/data_extract/widgets/data_extract_view.dart new file mode 100644 index 0000000..f2b6ab2 --- /dev/null +++ b/win_text_editor/lib/modules/data_extract/widgets/data_extract_view.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'results_view.dart'; +import 'package:win_text_editor/modules/data_extract/widgets/condition_setting.dart'; +import 'directory.dart'; +import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; +import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart'; + +class DataExtractView extends StatefulWidget { + final String tabId; + const DataExtractView({super.key, required this.tabId}); + + @override + DataExtractViewState createState() => DataExtractViewState(); +} + +class DataExtractViewState extends State { + late final DataExtractController _controller; + + get tabManager => Provider.of(context, listen: false); + + @override + void initState() { + super.initState(); + _controller = tabManager.getController(widget.tabId) ?? DataExtractController(); + } + + @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/modules/data_extract/widgets/directory.dart b/win_text_editor/lib/modules/data_extract/widgets/directory.dart new file mode 100644 index 0000000..0e9ffd7 --- /dev/null +++ b/win_text_editor/lib/modules/data_extract/widgets/directory.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:win_text_editor/modules/data_extract/controllers/data_extract_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/data_extract/widgets/results_view.dart b/win_text_editor/lib/modules/data_extract/widgets/results_view.dart new file mode 100644 index 0000000..d8f3a01 --- /dev/null +++ b/win_text_editor/lib/modules/data_extract/widgets/results_view.dart @@ -0,0 +1,138 @@ +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/data_extract/controllers/data_extract_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, + DataExtractController 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: 'export', child: Text('导出(csv)'))], + ); + + // 处理菜单选择结果 + if (result == 'export' && context.mounted) { + try { + await _exportToCsv(controller); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); + } + } + } + } + + Future _exportToCsv(DataExtractController controller) async { + String csvData = ''; + csvData = '文件\t行号\t内容\n'; + for (var result in controller.results) { + 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(DataExtractController controller) { + return SfDataGrid( + rowHeight: 32, + headerRowHeight: 32, + source: LocateDataSource(controller), + columns: [ + MyGridColumn(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 DataExtractController 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/module_router.dart b/win_text_editor/lib/modules/module_router.dart index 8a9f0dc..9cddcf4 100644 --- a/win_text_editor/lib/modules/module_router.dart +++ b/win_text_editor/lib/modules/module_router.dart @@ -5,6 +5,8 @@ import 'package:win_text_editor/modules/call_function/controllers/call_function_ import 'package:win_text_editor/modules/call_function/widgets/call_function_view.dart'; import 'package:win_text_editor/modules/data_compare/controllers/data_compare_controller.dart'; import 'package:win_text_editor/modules/data_compare/widgets/data_compare_view.dart'; +import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart'; +import 'package:win_text_editor/modules/data_extract/widgets/data_extract_view.dart'; import 'package:win_text_editor/modules/data_format/controllers/data_format_controller.dart'; import 'package:win_text_editor/modules/data_format/widgets/data_format_view.dart'; import 'package:win_text_editor/modules/demo/controllers/demo_controller.dart'; @@ -25,6 +27,7 @@ class RouterKey { static const String dataFormat = 'data_format'; static const String textEditor = 'text_editor'; static const String dataCompare = 'data_compare'; + static const String dataExtract = 'data_extract'; static const String memoryTable = 'memory_table'; static const String uftComponent = 'uft_component'; static const String callFunction = 'call_function'; @@ -38,6 +41,7 @@ class ModuleRouter { RouterKey.templateParser: (tab) => TemplateParserController(), RouterKey.dataFormat: (tab) => DataFormatController(), RouterKey.dataCompare: (tab) => DataCompareController(), + RouterKey.dataExtract: (tab) => DataExtractController(), RouterKey.memoryTable: (tab) => MemoryTableController(), RouterKey.uftComponent: (tab) => UftComponentController(), RouterKey.callFunction: (tab) => CallFunctionController(), @@ -50,6 +54,7 @@ class ModuleRouter { RouterKey.templateParser: (tab, controller) => TemplateParserView(tabId: tab.id), 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.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/shared/utils/file_utils.dart b/win_text_editor/lib/shared/utils/file_utils.dart index 2fb7ac1..713ab72 100644 --- a/win_text_editor/lib/shared/utils/file_utils.dart +++ b/win_text_editor/lib/shared/utils/file_utils.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:path/path.dart' as path; import 'package:win_text_editor/framework/controllers/logger.dart'; class FileUtils { @@ -55,5 +56,81 @@ class FileUtils { } } - // 移除showLoadingDialog方法,因为现在直接在调用处处理 + static bool matchesFileType(String filePath, String fileType) { + // 处理特殊情况 + if (fileType == '*.*' || fileType == '*') return true; + + // 获取文件的实际名称和扩展名 + final fileName = path.basename(filePath); + final fileExt = path.extension(fileName).toLowerCase().replaceFirst('.', ''); + + // 分割通配符为文件名和扩展名部分 + final parts = fileType.split('.'); + final patternName = parts.length > 0 ? parts[0] : ''; + final patternExt = parts.length > 1 ? parts.sublist(1).join('.') : ''; + + // 匹配文件名部分 + bool nameMatches = true; + if (patternName.isNotEmpty && patternName != '*') { + // 如果模式不含通配符,则执行精确匹配 + if (!patternName.contains('*') && !patternName.contains('?')) { + nameMatches = fileName.startsWith('${patternName.toLowerCase()}.'); + } else { + nameMatches = matchesWildcard(fileName, patternName); + } + } + + // 匹配扩展名部分 + bool extMatches = true; + if (patternExt.isNotEmpty) { + if (patternExt == '*') { + extMatches = true; + } else if (!patternExt.contains('*') && !patternExt.contains('?')) { + // 如果扩展名模式不含通配符,执行精确匹配 + extMatches = fileExt == patternExt.toLowerCase(); + } else { + extMatches = matchesWildcard(fileExt, patternExt.toLowerCase()); + } + } + + return nameMatches && extMatches; + } + + static bool matchesWildcard(String input, String pattern) { + // 处理特殊情况 + if (pattern == '*') return true; + + // 动态规划实现通配符匹配 + final m = input.length; + final n = pattern.length; + final dp = List.generate(m + 1, (_) => List.filled(n + 1, false)); + + // 空字符串匹配空模式 + dp[0][0] = true; + + // 处理模式以 * 开头的情况 + for (int j = 1; j <= n; j++) { + if (pattern[j - 1] == '*') { + dp[0][j] = dp[0][j - 1]; + } + } + + // 填充动态规划表 + for (int i = 1; i <= m; i++) { + for (int j = 1; j <= n; j++) { + if (pattern[j - 1] == '*') { + // * 可以匹配任意字符或空字符 + dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; + } else if (pattern[j - 1] == '?' || input[i - 1] == pattern[j - 1]) { + // ? 匹配单个字符,或者字符直接相等 + dp[i][j] = dp[i - 1][j - 1]; + } else { + // 不匹配 + dp[i][j] = false; + } + } + } + + return dp[m][n]; + } } diff --git a/win_text_editor/pubspec.yaml b/win_text_editor/pubspec.yaml index 5ad7e00..1907ff6 100644 --- a/win_text_editor/pubspec.yaml +++ b/win_text_editor/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: hive: ^2.2.3 hive_flutter: ^1.1.0 yaml: ^3.1.1 + xpath: ^1.0.0 dev_dependencies: flutter_test: