Browse Source

新工具-xml内容提取

master
hejl 1 month ago
parent
commit
6973bbef7b
  1. 76
      win_text_editor/assets/config/uft_macro_list.yaml
  2. 1
      win_text_editor/lib/framework/controllers/tab_items_controller.dart
  3. 4
      win_text_editor/lib/menus/app_menu.dart
  4. 5
      win_text_editor/lib/menus/menu_actions.dart
  5. 1
      win_text_editor/lib/menus/menu_constants.dart
  6. 88
      win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart
  7. 47
      win_text_editor/lib/modules/call_function/models/call_function.dart
  8. 52
      win_text_editor/lib/modules/call_function/services/call_function_service.dart
  9. 4
      win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart
  10. 42
      win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart
  11. 78
      win_text_editor/lib/modules/content_search/services/base_search_service.dart
  12. 3
      win_text_editor/lib/modules/content_search/services/count_search_service.dart
  13. 5
      win_text_editor/lib/modules/content_search/services/custom_search_service.dart
  14. 91
      win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart
  15. 8
      win_text_editor/lib/modules/data_extract/models/search_result.dart
  16. 18
      win_text_editor/lib/modules/data_extract/models/xml_rule.dart
  17. 100
      win_text_editor/lib/modules/data_extract/services/simple_xpath.dart
  18. 53
      win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart
  19. 150
      win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart
  20. 63
      win_text_editor/lib/modules/data_extract/widgets/data_extract_view.dart
  21. 86
      win_text_editor/lib/modules/data_extract/widgets/directory.dart
  22. 138
      win_text_editor/lib/modules/data_extract/widgets/results_view.dart
  23. 5
      win_text_editor/lib/modules/module_router.dart
  24. 79
      win_text_editor/lib/shared/utils/file_utils.dart
  25. 1
      win_text_editor/pubspec.yaml

76
win_text_editor/assets/config/uft_macro_list.yaml

@ -165,3 +165,79 @@ templates: @@ -165,3 +165,79 @@ templates:
{{name}} = @{{name}} {{^isLast}}, {{/isLast}}
{{/fields}}
]
遍历组件所有:
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: |
<M>[{{chineseName}}][
{{#input}}
{{name}} = @{{name}} {{^isLast}}, {{/isLast}}
{{/input}}
][
{{#output}}
{{name}} = @{{name}} {{^isLast}}, {{/isLast}}
{{/output}}
]
[处理失败]
{
[获取错误信息][@error_no][@error_info][@error_pathinfo]
//TODO:处理异常
[继续执行]
}
else
{
//balabala
}
事务调用:
body: |
[事务处理开始]
<M>[{{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}});

1
win_text_editor/lib/framework/controllers/tab_items_controller.dart

@ -122,7 +122,6 @@ class TabItemsController with ChangeNotifier { @@ -122,7 +122,6 @@ class TabItemsController with ChangeNotifier {
openOrActivateTab("内存表", RouterKey.memoryTable, Icons.list);
break;
case 'uftfunction':
case 'uftservice':
case 'uftatomfunction':
case 'uftatomservice':
case 'uftfactorfunction':

4
win_text_editor/lib/menus/app_menu.dart

@ -42,6 +42,10 @@ class AppMenu extends StatelessWidget { @@ -42,6 +42,10 @@ class AppMenu extends StatelessWidget {
value: MenuConstants.dataFormat,
child: ListTile(leading: Icon(Icons.date_range), title: Text('数据格式化')),
),
const PopupMenuItem<String>(
value: MenuConstants.dataExtract,
child: ListTile(leading: Icon(Icons.outbox), title: Text('XML数据提取')),
),
const PopupMenuDivider(),
const PopupMenuItem<String>(
value: MenuConstants.demo,

5
win_text_editor/lib/menus/menu_actions.dart

@ -15,6 +15,7 @@ class MenuActions { @@ -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 { @@ -69,6 +70,10 @@ class MenuActions {
await _openOrActivateTab(context, "数据对比", RouterKey.dataCompare, Icons.compare);
}
static Future<void> _dataExtract(BuildContext context) async {
await _openOrActivateTab(context, "XML数据提取", RouterKey.dataExtract, Icons.outbox);
}
static Future<void> _demo(BuildContext context) async {
await _openOrActivateTab(context, "Demo", RouterKey.demo, Icons.code);
}

1
win_text_editor/lib/menus/menu_constants.dart

@ -17,6 +17,7 @@ class MenuConstants { @@ -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菜单项

88
win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart

@ -1,3 +1,4 @@ @@ -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'; @@ -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 { @@ -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 { @@ -54,37 +45,49 @@ class CallFunctionController extends BaseContentController {
String? genCodeString(List<String> 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<void> 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 { @@ -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 { @@ -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 { @@ -119,9 +122,4 @@ class CallFunctionController extends BaseContentController {
void onOpenFolder(String folderPath) {
//
}
@override
void dispose() {
super.dispose();
}
}

47
win_text_editor/lib/modules/call_function/models/call_function.dart

@ -1,24 +1,32 @@ @@ -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<Field> inputParameters;
final List<Field> outputParameters;
List<UftComponent>? 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<Field> get selectInputFields => inputParameters.where((field) => field.isSelected).toList();
List<Field> get selectOutputFields =>
outputParameters.where((field) => field.isSelected).toList();
Map<String, dynamic> toMap() {
return {
'tableName': functionName,
'fields':
'functionNo': functionNo,
'chineseName': chineseName,
'factorParam': factorParam ?? '',
'input':
inputParameters
.map(
(field) => {
@ -30,27 +38,26 @@ class CallFunction { @@ -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(),

52
win_text_editor/lib/modules/call_function/services/call_function_service.dart

@ -1,6 +1,8 @@ @@ -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 { @@ -22,7 +24,7 @@ class CallFunctionService {
'uftfactorservice': 'business:FactorService',
};
Future<FunctionData> parseXmlFile(String filePath) async {
Future<CallFunction> parseXmlFile(String filePath) async {
try {
// 1. Check file extension
final extendFileName = filePath.toLowerCase().split('.').last;
@ -52,19 +54,30 @@ class CallFunctionService { @@ -52,19 +54,30 @@ class CallFunctionService {
List<UftComponent> 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 { @@ -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 { @@ -111,19 +125,3 @@ class CallFunctionService {
return fields;
}
}
class FunctionData {
final String chineseName;
final String objectId;
final List<Field> inputFields;
final List<Field> outputFields;
List<UftComponent>? componentList;
FunctionData({
required this.chineseName,
required this.objectId,
required this.inputFields,
required this.outputFields,
this.componentList,
});
}

4
win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart

@ -38,8 +38,8 @@ class CallFunctionLeftSide extends StatelessWidget { @@ -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),
],
),
),

42
win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart

@ -1,6 +1,7 @@ @@ -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<CallFunctionRightSide> { @@ -36,6 +37,7 @@ class _CallFunctionRightSideState extends State<CallFunctionRightSide> {
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<CallFunctionRightSide> { @@ -62,6 +64,46 @@ class _CallFunctionRightSideState extends State<CallFunctionRightSide> {
);
}
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(

78
win_text_editor/lib/modules/content_search/services/base_search_service.dart

@ -1,6 +1,6 @@ @@ -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<void> Function(File file, String content);
@ -25,7 +25,7 @@ abstract class BaseSearchService { @@ -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 { @@ -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];
}
}

3
win_text_editor/lib/modules/content_search/services/count_search_service.dart

@ -3,6 +3,7 @@ import 'dart:io'; @@ -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 { @@ -128,7 +129,7 @@ class CountSearchService {
final paths = <String>[];
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);
}
}

5
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'; @@ -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 { @@ -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 { @@ -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++) {

91
win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart

@ -0,0 +1,91 @@ @@ -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<SearchResult> _results = [];
final List<XmlRule> _rules = [];
final XmlExtractService _extractService = XmlExtractService();
List<SearchResult> get results => _results;
List<XmlRule> get rules => _rules;
String get searchDirectory => _searchDirectory;
String get fileType => _fileType;
bool _isExtracting = false;
bool onlyFileName = false;
bool get isExtracting => _isExtracting;
Future<void> 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<void> 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() {}
}

8
win_text_editor/lib/modules/data_extract/models/search_result.dart

@ -0,0 +1,8 @@ @@ -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});
}

18
win_text_editor/lib/modules/data_extract/models/xml_rule.dart

@ -0,0 +1,18 @@ @@ -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()'}';
}
}

100
win_text_editor/lib/modules/data_extract/services/simple_xpath.dart

@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
import 'package:xml/xml.dart' as xml;
class SimpleXPath {
static List<xml.XmlNode> 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<xml.XmlNode> _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<xml.XmlText>();
}
} 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<xml.XmlElement>().where(
(child) => child.name.local == segment,
);
}
}
}
}
//
static Iterable<xml.XmlNode> _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('''
<root>
<items>
<item id="1">Text 1</item>
<items>
<item id="2">Text 2</item>
</items>
</items>
</root>
''');
void printResults(String query, List<xml.XmlNode> 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'));
}

53
win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart

@ -0,0 +1,53 @@ @@ -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<List<SearchResult>> extractFromDirectory({
required String directory,
required String fileType,
required XmlRule rule,
}) async {
final results = <SearchResult>[];
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<String> _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<String>()
.toList();
}
}
}

150
win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart

@ -0,0 +1,150 @@ @@ -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<ConditionSetting> createState() => _ConditionSettingState();
}
class _ConditionSettingState extends State<ConditionSetting> {
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<DataExtractController>();
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<DataExtractController>(context, listen: false);
controller.setRule(
XmlRule(
nodePath: nodePath,
attributeName: attributeName,
isFirstOccurrence: _isFirstOccurrence,
namespacePrefix:
_namespacePrefixController.text.trim().isNotEmpty
? _namespacePrefixController.text.trim()
: null,
),
);
}
}
Future<void> _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<DataExtractController>(context, listen: false);
// cancelExtraction方法
controller.cancelExtraction();
setState(() => _isExtracting = false);
}
}

63
win_text_editor/lib/modules/data_extract/widgets/data_extract_view.dart

@ -0,0 +1,63 @@ @@ -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<DataExtractView> {
late final DataExtractController _controller;
get tabManager => Provider.of<TabItemsController>(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<DataExtractController>(
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()),
],
),
),
],
),
);
},
),
);
}
}

86
win_text_editor/lib/modules/data_extract/widgets/directory.dart

@ -0,0 +1,86 @@ @@ -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<Directory> createState() => _DirectoryState();
}
class _DirectoryState extends State<Directory> {
late TextEditingController _searchDirectoryController;
late TextEditingController _fileTypeController;
@override
void initState() {
super.initState();
final controller = context.read<DataExtractController>();
_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<DataExtractController>(
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
},
),
],
),
),
);
},
);
}
}

138
win_text_editor/lib/modules/data_extract/widgets/results_view.dart

@ -0,0 +1,138 @@ @@ -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<DataExtractController>();
return Card(
child: GestureDetector(
onSecondaryTapDown: (details) {
_showContextMenu(context, details.globalPosition, controller);
},
child: _buildLocateGrid(controller),
),
);
}
Future<void> _showContextMenu(
BuildContext context,
Offset position,
DataExtractController controller,
) async {
//
final renderBox = context.findRenderObject() as RenderBox;
final localPosition = renderBox.globalToLocal(position);
//
final result = await showMenu<String>(
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<String>(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<void> _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<DataGridRow> 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<Widget>((cell) {
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cell.value.toString(), overflow: TextOverflow.ellipsis),
);
}).toList(),
);
}
}

5
win_text_editor/lib/modules/module_router.dart

@ -5,6 +5,8 @@ import 'package:win_text_editor/modules/call_function/controllers/call_function_ @@ -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 { @@ -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 { @@ -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 { @@ -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),

79
win_text_editor/lib/shared/utils/file_utils.dart

@ -3,6 +3,7 @@ import 'dart:async'; @@ -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 { @@ -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];
}
}

1
win_text_editor/pubspec.yaml

@ -27,6 +27,7 @@ dependencies: @@ -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:

Loading…
Cancel
Save