Browse Source

新增菜单XML搜索

master
hejl 1 month ago
parent
commit
282911ee0f
  1. 4
      win_text_editor/lib/menus/app_menu.dart
  2. 5
      win_text_editor/lib/menus/menu_actions.dart
  3. 1
      win_text_editor/lib/menus/menu_constants.dart
  4. 6
      win_text_editor/lib/modules/content_search/services/base_search_service.dart
  5. 54
      win_text_editor/lib/modules/content_search/widgets/results_view.dart
  6. 2
      win_text_editor/lib/modules/content_search/widgets/search_settings.dart
  7. 6
      win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart
  8. 58
      win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart
  9. 14
      win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart
  10. 4
      win_text_editor/lib/modules/data_extract/widgets/results_view.dart
  11. 2
      win_text_editor/lib/modules/data_format/widgets/grid_view.dart
  12. 5
      win_text_editor/lib/modules/module_router.dart
  13. 6
      win_text_editor/lib/modules/template_parser/widgets/grid_view.dart
  14. 91
      win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart
  15. 8
      win_text_editor/lib/modules/xml_search/models/search_result.dart
  16. 18
      win_text_editor/lib/modules/xml_search/models/xml_rule.dart
  17. 52
      win_text_editor/lib/modules/xml_search/services/xml_search_service.dart
  18. 140
      win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart
  19. 86
      win_text_editor/lib/modules/xml_search/widgets/directory.dart
  20. 154
      win_text_editor/lib/modules/xml_search/widgets/results_view.dart
  21. 63
      win_text_editor/lib/modules/xml_search/widgets/xml_search_view.dart
  22. 27
      win_text_editor/lib/shared/components/my_grid_column.dart

4
win_text_editor/lib/menus/app_menu.dart

@ -46,6 +46,10 @@ class AppMenu extends StatelessWidget { @@ -46,6 +46,10 @@ class AppMenu extends StatelessWidget {
value: MenuConstants.dataExtract,
child: ListTile(leading: Icon(Icons.outbox), title: Text('XML数据提取')),
),
const PopupMenuItem<String>(
value: MenuConstants.xmlSearch,
child: ListTile(leading: Icon(Icons.find_in_page), title: Text('XML搜索')),
),
const PopupMenuDivider(),
const PopupMenuItem<String>(
value: MenuConstants.demo,

5
win_text_editor/lib/menus/menu_actions.dart

@ -16,6 +16,7 @@ class MenuActions { @@ -16,6 +16,7 @@ class MenuActions {
MenuConstants.dataFormat: _dataFormat,
MenuConstants.dataCompare: _dataCompare,
MenuConstants.dataExtract: _dataExtract,
MenuConstants.xmlSearch: _xmlSearch,
MenuConstants.memoryTable: _memoryTable,
MenuConstants.uftComponent: _uftComponent,
MenuConstants.callFunction: _callFunction,
@ -74,6 +75,10 @@ class MenuActions { @@ -74,6 +75,10 @@ class MenuActions {
await _openOrActivateTab(context, "XML数据提取", RouterKey.dataExtract, Icons.outbox);
}
static Future<void> _xmlSearch(BuildContext context) async {
await _openOrActivateTab(context, "XML搜索", RouterKey.xmlSearch, Icons.find_in_page);
}
static Future<void> _demo(BuildContext context) async {
await _openOrActivateTab(context, "Demo", RouterKey.demo, Icons.code);
}

1
win_text_editor/lib/menus/menu_constants.dart

@ -18,6 +18,7 @@ class MenuConstants { @@ -18,6 +18,7 @@ class MenuConstants {
static const String dataFormat = 'data_format';
static const String dataCompare = 'data_compare';
static const String dataExtract = 'data_extract';
static const String xmlSearch = 'xml_search';
static const String demo = 'demo';
// AIGC菜单项

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

@ -43,7 +43,11 @@ abstract class BaseSearchService { @@ -43,7 +43,11 @@ abstract class BaseSearchService {
///
static List<String> splitQuery(String query) {
return query.split(',').map((q) => q.trim()).where((q) => q.isNotEmpty).toList();
return query
.split('\n') //
.expand((line) => line.split(',').map((q) => q.trim())) //
.where((q) => q.isNotEmpty) //
.toList();
}
static RegExp buildSearchPattern({

54
win_text_editor/lib/modules/content_search/widgets/results_view.dart

@ -76,14 +76,15 @@ class ResultsView extends StatelessWidget { @@ -76,14 +76,15 @@ class ResultsView extends StatelessWidget {
Future<void> _exportToCsv(ContentSearchController controller) async {
String csvData = '';
if (controller.searchMode == SearchMode.locate) {
csvData = '文件,行号,内容\n';
csvData = '文件\t行号\t内容\n';
for (var result in controller.results) {
csvData += '${path.basename(result.filePath)},${result.lineNumber},${result.lineContent}\n';
csvData +=
'${path.basename(result.filePath)}\t${result.lineNumber}\t${result.lineContent}\n';
}
} else {
csvData = '关键词,匹配数量\n';
csvData = '关键词\t匹配数量\n';
for (var result in controller.results) {
csvData += '${result.filePath},${result.lineNumber}\n';
csvData += '${result.filePath}\t${result.lineNumber}\n';
}
}
@ -113,9 +114,9 @@ class ResultsView extends StatelessWidget { @@ -113,9 +114,9 @@ class ResultsView extends StatelessWidget {
source: LocateDataSource(controller, context),
columns: [
ShortGridColumn(columnName: 'index', label: '序号'),
MyGridColumn(columnName: 'file', label: '文件(行号)', minimumWidth: 300),
MyGridColumn(columnName: 'file', label: '文件(行号)', maximumWidth: 400),
MyGridColumn(columnName: 'content', label: '内容'),
ShortGridColumn(columnName: 'action', label: '操作'),
ShortGridColumn(columnName: 'action', label: '操作', width: 90),
],
selectionMode: SelectionMode.multiple,
navigationMode: GridNavigationMode.cell,
@ -209,15 +210,50 @@ class LocateDataSource extends DataGridSource { @@ -209,15 +210,50 @@ class LocateDataSource extends DataGridSource {
),
Container(
alignment: Alignment.center,
child: IconButton(
icon: const Icon(Icons.delete_forever, size: 18, color: Colors.red),
onPressed: () => _showDeleteConfirmation(result.filePath),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.open_in_new, size: 18, color: Colors.blue),
onPressed: () => _openFile(result.filePath),
),
IconButton(
icon: const Icon(Icons.delete_forever, size: 18, color: Colors.red),
onPressed: () => _showDeleteConfirmation(result.filePath),
),
],
),
),
],
);
}
Future<void> _openFile(String filePath) async {
try {
final file = File(filePath);
if (await file.exists()) {
// Use Process.run to open the file with the default system handler
if (Platform.isWindows) {
await Process.run('start', ['""', filePath], runInShell: true);
} else if (Platform.isMacOS) {
await Process.run('open', [filePath], runInShell: true);
} else if (Platform.isLinux) {
await Process.run('xdg-open', [filePath], runInShell: true);
}
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件不存在: $filePath')));
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('打开文件失败: ${e.toString()}')));
}
}
}
Future<void> _showDeleteConfirmation(String filePath) async {
bool confirmed = false;

2
win_text_editor/lib/modules/content_search/widgets/search_settings.dart

@ -80,7 +80,7 @@ class _SearchSettingsState extends State<SearchSettings> { @@ -80,7 +80,7 @@ class _SearchSettingsState extends State<SearchSettings> {
height: 360,
child: TextEditor(
tabId: 'search_content_${controller.hashCode}',
title: '搜索内容[列表以半角逗号分隔]',
title: '搜索内容[列表以换行或半角逗号分隔]',
initialContent: controller.searchQuery, //
onContentChanged: (content) {
controller.searchQuery = content; //

6
win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart

@ -19,7 +19,7 @@ class DataCompareController extends BaseContentController { @@ -19,7 +19,7 @@ class DataCompareController extends BaseContentController {
final lines = csvContent.split('\n');
if (lines.isEmpty) return;
String delimiter = lines[0].contains(',') ? ',' : '\t';
String delimiter = lines[0].contains('\t') ? '\t' : ',';
// 2.
final headers = lines[0].split(delimiter).skip(1).toList();
@ -195,7 +195,7 @@ class DataCompareController extends BaseContentController { @@ -195,7 +195,7 @@ class DataCompareController extends BaseContentController {
// ()
buffer.write('主键'); // "序号"
for (var column in columns) {
buffer.write(',$column');
buffer.write('\t$column');
}
buffer.writeln();
@ -203,7 +203,7 @@ class DataCompareController extends BaseContentController { @@ -203,7 +203,7 @@ class DataCompareController extends BaseContentController {
for (var row in data) {
buffer.write('${row['key']}'); // serial
for (var column in columns) {
buffer.write(',${row[column] ?? ''}');
buffer.write('\t${row[column] ?? ''}');
}
buffer.writeln();
}

58
win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart

@ -26,47 +26,44 @@ class DataCompareDataSource extends DataGridSource { @@ -26,47 +26,44 @@ class DataCompareDataSource extends DataGridSource {
final rightData = row['right_data'] as Map<String, dynamic>?;
final status = row['match_status'] as String;
return DataGridRow(cells: [
// -
DataGridCell<String>(
columnName: 'left_serial',
value: (isLeft || status != 'no_match') ? row['serial'] : ''
return DataGridRow(
cells: [
// -
DataGridCell<String>(
columnName: 'left_serial',
value: (isLeft || status != 'no_match') ? row['serial'] : '',
),
DataGridCell<String>(
columnName: 'left_key',
value: (isLeft || status != 'no_match') ? row['key'] : '',
),
...controller.leftColumns.map(
(col) => DataGridCell<String>(
columnName: 'left_$col',
value: (isLeft || status != 'no_match') ? row[col] : '',
),
),
DataGridCell<String>(
columnName: 'left_key',
value: (isLeft || status != 'no_match') ? row['key'] : ''
),
...controller.leftColumns.map((col) => DataGridCell<String>(
columnName: 'left_$col',
value: (isLeft || status != 'no_match') ? row[col] : ''
)),
//
DataGridCell<Icon>(
columnName: 'comparison',
value: _getStatusIcon(status)
),
DataGridCell<Icon>(columnName: 'comparison', value: _getStatusIcon(status)),
// -
// -
DataGridCell<String>(
columnName: 'right_serial',
value: (!isLeft || status != 'no_match')
? (rightData?['serial'] ?? row['serial'])
: ''
value: (!isLeft || status != 'no_match') ? (rightData?['serial'] ?? row['serial']) : '',
),
DataGridCell<String>(
columnName: 'right_key',
value: (!isLeft || status != 'no_match')
? (rightData?['key'] ?? row['key'])
: ''
value: (!isLeft || status != 'no_match') ? (rightData?['key'] ?? row['key']) : '',
),
...controller.rightColumns.map((col) => DataGridCell<String>(
...controller.rightColumns.map(
(col) => DataGridCell<String>(
columnName: 'right_$col',
value: (!isLeft || status != 'no_match')
? (rightData?[col] ?? row[col])
: ''
)),
]);
value: (!isLeft || status != 'no_match') ? (rightData?[col] ?? row[col]) : '',
),
),
],
);
}).toList();
}
@ -88,7 +85,6 @@ class DataCompareDataSource extends DataGridSource { @@ -88,7 +85,6 @@ class DataCompareDataSource extends DataGridSource {
row.getCells().map<Widget>((cell) {
return Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(8),
child: cell.value.runtimeType == Icon ? cell.value : Text(cell.value.toString()),
);
}).toList(),

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

@ -43,8 +43,8 @@ class _ConditionSettingState extends State<ConditionSetting> { @@ -43,8 +43,8 @@ class _ConditionSettingState extends State<ConditionSetting> {
TextField(
controller: _nodePathController,
decoration: const InputDecoration(
labelText: '节点路径 (XPath)',
hintText: '如: //business:Service 或 /root/items',
labelText: '节点名称',
hintText: '如: business:Service',
border: OutlineInputBorder(),
),
),
@ -58,16 +58,6 @@ class _ConditionSettingState extends State<ConditionSetting> { @@ -58,16 +58,6 @@ class _ConditionSettingState extends State<ConditionSetting> {
),
),
const SizedBox(height: 12),
const Text('命名空间配置 (可选):'),
TextField(
controller: _namespacePrefixController,
decoration: const InputDecoration(
labelText: '命名空间前缀',
hintText: '如: business',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
MyCheckbox(

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

@ -70,7 +70,7 @@ class ResultsView extends StatelessWidget { @@ -70,7 +70,7 @@ class ResultsView extends StatelessWidget {
csvData =
exportType == 'exportFileName'
? '文件名称\n'
: (exportType == 'exportContent' ? '内容\n' : '文件名称,内容\n');
: (exportType == 'exportContent' ? '内容\n' : '文件名称\t内容\n');
for (var result in controller.results) {
switch (exportType) {
case 'exportFileName':
@ -80,7 +80,7 @@ class ResultsView extends StatelessWidget { @@ -80,7 +80,7 @@ class ResultsView extends StatelessWidget {
csvData += '${result.content}\n';
break;
default:
csvData += '${path.basename(result.filePath)},${result.content}\n';
csvData += '${path.basename(result.filePath)}\t${result.content}\n';
}
}

2
win_text_editor/lib/modules/data_format/widgets/grid_view.dart

@ -204,7 +204,7 @@ class _CsvDataSource extends DataGridSource { @@ -204,7 +204,7 @@ class _CsvDataSource extends DataGridSource {
cells:
row.getCells().map<Widget>((dataGridCell) {
return Container(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
alignment: Alignment.centerLeft,
child: Text(
dataGridCell.value.toString(),

5
win_text_editor/lib/modules/module_router.dart

@ -15,6 +15,8 @@ import 'package:win_text_editor/modules/memory_table/controllers/memory_table_co @@ -15,6 +15,8 @@ import 'package:win_text_editor/modules/memory_table/controllers/memory_table_co
import 'package:win_text_editor/modules/memory_table/widgets/memory_table_view.dart';
import 'package:win_text_editor/modules/uft_component/controllers/uft_component_controller.dart';
import 'package:win_text_editor/modules/uft_component/widgets/uft_component_view.dart';
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart';
import 'package:win_text_editor/modules/xml_search/widgets/xml_search_view.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart';
import 'package:win_text_editor/modules/template_parser/controllers/template_parser_controller.dart';
@ -28,6 +30,7 @@ class RouterKey { @@ -28,6 +30,7 @@ class RouterKey {
static const String textEditor = 'text_editor';
static const String dataCompare = 'data_compare';
static const String dataExtract = 'data_extract';
static const String xmlSearch = 'xml_search';
static const String memoryTable = 'memory_table';
static const String uftComponent = 'uft_component';
static const String callFunction = 'call_function';
@ -42,6 +45,7 @@ class ModuleRouter { @@ -42,6 +45,7 @@ class ModuleRouter {
RouterKey.dataFormat: (tab) => DataFormatController(),
RouterKey.dataCompare: (tab) => DataCompareController(),
RouterKey.dataExtract: (tab) => DataExtractController(),
RouterKey.xmlSearch: (tab) => XmlSearchController(),
RouterKey.memoryTable: (tab) => MemoryTableController(),
RouterKey.uftComponent: (tab) => UftComponentController(),
RouterKey.callFunction: (tab) => CallFunctionController(),
@ -55,6 +59,7 @@ class ModuleRouter { @@ -55,6 +59,7 @@ class ModuleRouter {
RouterKey.dataFormat: (tab, controller) => DataFormatView(tabId: tab.id),
RouterKey.dataCompare: (tab, controller) => DataCompareView(tabId: tab.id),
RouterKey.dataExtract: (tab, controller) => DataExtractView(tabId: tab.id),
RouterKey.xmlSearch: (tab, controller) => XmlSearchView(tabId: tab.id),
RouterKey.memoryTable: (tab, controller) => MemoryTableView(tabId: tab.id),
RouterKey.uftComponent: (tab, controller) => UftComponentView(tabId: tab.id),
RouterKey.callFunction: (tab, controller) => CallFunctionView(tabId: tab.id),

6
win_text_editor/lib/modules/template_parser/widgets/grid_view.dart

@ -111,8 +111,8 @@ class TemplateGridView extends StatelessWidget { @@ -111,8 +111,8 @@ class TemplateGridView extends StatelessWidget {
final dataSource = _TemplateItemDataSource(rows: rows, selectedNodes: selectedNodes);
//
final columns = <MyGridColumn>[
MyGridColumn(columnName: 'index', minimumWidth: 60, label: '序号'),
final columns = <GridColumn>[
ShortGridColumn(columnName: 'index', label: '序号'),
...selectedNodes.map((node) {
return MyGridColumn(
columnName: node.path,
@ -193,7 +193,7 @@ class _TemplateItemDataSource extends DataGridSource { @@ -193,7 +193,7 @@ class _TemplateItemDataSource extends DataGridSource {
cells:
row.getCells().map<Widget>((dataGridCell) {
return Container(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
alignment:
dataGridCell.columnName == 'index' ? Alignment.center : Alignment.centerLeft,
child: Text(dataGridCell.value.toString()),

91
win_text_editor/lib/modules/xml_search/controllers/xml_search_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/xml_search/models/search_result.dart';
import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart';
import 'package:win_text_editor/modules/xml_search/services/xml_search_service.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
class XmlSearchController extends BaseContentController {
String _searchDirectory = '';
String _fileType = '*.*';
final List<SearchResult> _results = [];
final List<XmlRule> _rules = [];
final XmlSearchService _searchService = XmlSearchService();
List<SearchResult> get results => _results;
List<XmlRule> get rules => _rules;
String get searchDirectory => _searchDirectory;
String get fileType => _fileType;
bool _isSearching = false;
bool onlyFileName = false;
bool get isSearching => _isSearching;
Future<void> executeSearching() async {
Logger().info("开始提取目录:$_searchDirectory, 文件名:$_fileType");
if (_searchDirectory.isEmpty || _rules.isEmpty) return;
_isSearching = true;
notifyListeners();
_results.clear();
notifyListeners();
try {
final newResults = await _searchService.searchFromDirectory(
directory: _searchDirectory,
fileType: _fileType,
rule: _rules[0],
);
_results.addAll(newResults);
} catch (e) {
Logger().error("提取目录出错:$e");
_results.add(SearchResult(rowNum: 1, filePath: 'Error', content: 'Searchion failed: $e'));
} finally {
_isSearching = false;
notifyListeners();
}
}
set searchDirectory(String value) {
_searchDirectory = value;
notifyListeners();
}
set fileType(String value) {
_fileType = value;
notifyListeners();
}
Future<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 cancelSearchion() {}
}

8
win_text_editor/lib/modules/xml_search/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/xml_search/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()'}';
}
}

52
win_text_editor/lib/modules/xml_search/services/xml_search_service.dart

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
// xml_search_service.dart
import 'dart:io';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/shared/utils/file_utils.dart';
import 'package:xml/xml.dart' as xml;
import 'package:win_text_editor/modules/xml_search/models/search_result.dart';
import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart';
class XmlSearchService {
Future<List<SearchResult>> searchFromDirectory({
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 = _searchWithRule(document, rule);
for (var value in values) {
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: value));
}
} catch (e) {
Logger().error('xmlSearchService.searchFromDirectory方法执行出错: $e');
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: 'Error: $e'));
}
}
}
return results;
}
List<String> _searchWithRule(xml.XmlDocument document, XmlRule rule) {
final nodes = document.findAllElements(rule.nodePath);
//final nodes = SimpleXPath.query(document, rule.toxPath());
if (rule.isFirstOccurrence && nodes.isNotEmpty) {
final attr = nodes.first.getAttribute(rule.attributeName);
return attr != null ? [attr] : [];
} else {
return nodes
.map((node) => node.getAttribute(rule.attributeName))
.where((attr) => attr != null)
.cast<String>()
.toList();
}
}
}

140
win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart

@ -0,0 +1,140 @@ @@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart';
import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart';
import 'package:win_text_editor/shared/components/my_checkbox.dart';
class ConditionSetting extends StatefulWidget {
const ConditionSetting({super.key});
@override
State<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<XmlSearchController>();
return Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('数据提取规则设置:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
//
TextField(
controller: _nodePathController,
decoration: const InputDecoration(
labelText: '节点名称',
hintText: '如: business:Service',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _attributeNameController,
decoration: const InputDecoration(
labelText: '属性名称',
hintText: '如: chineseName 或 name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
MyCheckbox(
title: '仅提取第一个匹配项',
value: _isFirstOccurrence,
onChanged: (value) => setState(() => _isFirstOccurrence = value ?? false),
),
const SizedBox(height: 16),
//
Row(
children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.play_arrow),
label: const Text('开始'),
onPressed: _isExtracting ? null : () => _startSearching(controller),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.stop, color: Colors.red),
label: const Text('停止', style: TextStyle(color: Colors.red)),
onPressed: _isExtracting ? _stopExtraction : null,
),
),
],
),
],
),
),
);
}
void _setRule() {
final nodePath = _nodePathController.text.trim();
final attributeName = _attributeNameController.text.trim();
if (nodePath.isNotEmpty && attributeName.isNotEmpty) {
final controller = Provider.of<XmlSearchController>(context, listen: false);
controller.setRule(
XmlRule(
nodePath: nodePath,
attributeName: attributeName,
isFirstOccurrence: _isFirstOccurrence,
namespacePrefix:
_namespacePrefixController.text.trim().isNotEmpty
? _namespacePrefixController.text.trim()
: null,
),
);
}
}
Future<void> _startSearching(XmlSearchController controller) async {
if (controller.searchDirectory.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择搜索目录')));
return;
}
_setRule();
setState(() => _isExtracting = true);
try {
await controller.executeSearching();
} finally {
if (mounted) {
setState(() => _isExtracting = false);
}
}
}
void _stopExtraction() {
//
final controller = Provider.of<XmlSearchController>(context, listen: false);
// cancelExtraction方法
controller.executeSearching();
setState(() => _isExtracting = false);
}
}

86
win_text_editor/lib/modules/xml_search/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/xml_search/controllers/xml_search_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<XmlSearchController>();
_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<XmlSearchController>(
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
},
),
],
),
),
);
},
);
}
}

154
win_text_editor/lib/modules/xml_search/widgets/results_view.dart

@ -0,0 +1,154 @@ @@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:path/path.dart' as path;
import 'package:file_picker/file_picker.dart';
import 'dart:io';
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart';
import 'package:win_text_editor/shared/components/my_grid_column.dart';
class ResultsView extends StatelessWidget {
const ResultsView({super.key});
@override
Widget build(BuildContext context) {
final controller = context.watch<XmlSearchController>();
return Card(
child: GestureDetector(
onSecondaryTapDown: (details) {
_showContextMenu(context, details.globalPosition, controller);
},
child: _buildLocateGrid(controller),
),
);
}
Future<void> _showContextMenu(
BuildContext context,
Offset position,
XmlSearchController 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: 'exportFileName', child: Text('导出文件名(csv)')),
const PopupMenuItem<String>(value: 'exportContent', child: Text('导出内容(csv)')),
const PopupMenuItem<String>(value: 'exportAll', child: Text('导出全部(csv)')),
],
);
//
if (result != null && result.startsWith('export') && context.mounted) {
try {
await _exportToCsv(controller, result);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}')));
}
}
}
}
Future<void> _exportToCsv(XmlSearchController controller, String? exportType) async {
String csvData = '';
csvData =
exportType == 'exportFileName'
? '文件名称\n'
: (exportType == 'exportContent' ? '内容\n' : '文件名称\t内容\n');
for (var result in controller.results) {
switch (exportType) {
case 'exportFileName':
csvData += '${path.basename(result.filePath)}\n';
break;
case 'exportContent':
csvData += '${result.content}\n';
break;
default:
csvData += '${path.basename(result.filePath)}\t${result.content}\n';
}
}
final filePath = await FilePicker.platform.saveFile(
dialogTitle: '保存导出结果',
fileName: 'search_results.csv',
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (filePath != null) {
final file = File(filePath);
await file.writeAsString(csvData);
}
}
Widget _buildLocateGrid(XmlSearchController controller) {
return SfDataGrid(
rowHeight: 32,
headerRowHeight: 32,
source: LocateDataSource(controller),
columns: [
ShortGridColumn(columnName: 'rowNum', label: '序号'),
MyGridColumn(columnName: 'file', label: '文件名称', minimumWidth: 300),
MyGridColumn(columnName: 'content', label: '内容'),
],
selectionMode: SelectionMode.multiple,
navigationMode: GridNavigationMode.cell,
gridLinesVisibility: GridLinesVisibility.both,
headerGridLinesVisibility: GridLinesVisibility.both,
allowSorting: false,
allowFiltering: false,
columnWidthMode: ColumnWidthMode.fill,
isScrollbarAlwaysShown: true,
allowColumnsResizing: true, //
columnResizeMode: ColumnResizeMode.onResizeEnd,
);
}
}
class LocateDataSource extends DataGridSource {
final XmlSearchController controller;
LocateDataSource(this.controller);
@override
List<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(),
);
}
}

63
win_text_editor/lib/modules/xml_search/widgets/xml_search_view.dart

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart';
import 'package:win_text_editor/modules/xml_search/widgets/condition_setting.dart';
import 'results_view.dart';
import 'directory.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
class XmlSearchView extends StatefulWidget {
final String tabId;
const XmlSearchView({super.key, required this.tabId});
@override
XmlSearchViewState createState() => XmlSearchViewState();
}
class XmlSearchViewState extends State<XmlSearchView> {
late final XmlSearchController _controller;
get tabManager => Provider.of<TabItemsController>(context, listen: false);
@override
void initState() {
super.initState();
_controller = tabManager.getController(widget.tabId) ?? XmlSearchController();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: _controller,
child: Consumer<XmlSearchController>(
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()),
],
),
),
],
),
);
},
),
);
}
}

27
win_text_editor/lib/shared/components/my_grid_column.dart

@ -2,17 +2,22 @@ import 'package:flutter/material.dart'; @@ -2,17 +2,22 @@ import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
class MyGridColumn extends GridColumn {
MyGridColumn({required String columnName, double minimumWidth = 100, required String label})
: super(
columnName: columnName,
minimumWidth: minimumWidth,
label: Container(
alignment: Alignment.center,
color: Colors.grey[200],
padding: const EdgeInsets.all(2.0),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)),
),
);
MyGridColumn({
required String columnName,
double minimumWidth = 100,
double maximumWidth = double.infinity,
required String label,
}) : super(
columnName: columnName,
minimumWidth: minimumWidth,
maximumWidth: maximumWidth,
label: Container(
alignment: Alignment.center,
color: Colors.grey[200],
padding: const EdgeInsets.all(2.0),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)),
),
);
}
class ShortGridColumn extends GridColumn {

Loading…
Cancel
Save