Browse Source

xml搜索增加多选之前

master
hejl 1 month ago
parent
commit
57a6ef7122
  1. 4
      win_text_editor/lib/modules/template_parser/controllers/grid_view_controller.dart
  2. 1
      win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart
  3. 81
      win_text_editor/lib/modules/template_parser/widgets/grid_view.dart
  4. 87
      win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart
  5. 6
      win_text_editor/lib/modules/xml_search/models/search_result.dart
  6. 18
      win_text_editor/lib/modules/xml_search/models/xml_rule.dart
  7. 96
      win_text_editor/lib/modules/xml_search/services/xml_search_service.dart
  8. 92
      win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart
  9. 40
      win_text_editor/lib/modules/xml_search/widgets/directory.dart
  10. 161
      win_text_editor/lib/modules/xml_search/widgets/results_view.dart
  11. 11
      win_text_editor/lib/shared/components/my_grid_column.dart

4
win_text_editor/lib/modules/template_parser/controllers/grid_view_controller.dart

@ -50,4 +50,8 @@ class GridViewController extends SafeNotifier {
_isFilterApplied = false; _isFilterApplied = false;
safeNotify(); safeNotify();
} }
void updateItemValue({required String xPath, required newValue}) {
//
}
} }

1
win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart

@ -58,6 +58,7 @@ class TemplateParserController extends BaseContentController {
void setStatisticsMode(String? value) { void setStatisticsMode(String? value) {
statisticsMode = value ?? modeByPath; statisticsMode = value ?? modeByPath;
_loadTemplateData();
notifyListeners(); notifyListeners();
} }

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

@ -102,21 +102,29 @@ class TemplateGridView extends StatelessWidget {
return const Center(child: Text('请在左侧树中选择要显示的节点(勾选复选框)')); return const Center(child: Text('请在左侧树中选择要显示的节点(勾选复选框)'));
} }
//
final allItems = controller.displayedItems; final allItems = controller.displayedItems;
// -
final rows = _buildDataRows(selectedNodes, allItems); final rows = _buildDataRows(selectedNodes, allItems);
final dataSource = _TemplateItemDataSource(rows: rows, selectedNodes: selectedNodes); final dataSource = _TemplateItemDataSource(
rows: rows,
selectedNodes: selectedNodes,
onCellValueChanged: (node, columnName, newValue) {
//
controller.updateItemValue(xPath: columnName, newValue: newValue);
},
);
//
final columns = <GridColumn>[ final columns = <GridColumn>[
ShortGridColumn(columnName: 'index', label: '序号'), ShortGridColumn(
columnName: 'index',
label: '序号',
allowEditing: false, //
),
...selectedNodes.map((node) { ...selectedNodes.map((node) {
return MyGridColumn( return MyGridColumn(
columnName: node.path, columnName: node.path,
label: node.isAttribute ? node.name.substring(1) : node.name, label: node.isAttribute ? node.name.substring(1) : node.name,
allowEditing: true, //
); );
}).toList(), }).toList(),
]; ];
@ -130,6 +138,8 @@ class TemplateGridView extends StatelessWidget {
headerGridLinesVisibility: GridLinesVisibility.both, headerGridLinesVisibility: GridLinesVisibility.both,
columnWidthMode: ColumnWidthMode.fill, columnWidthMode: ColumnWidthMode.fill,
allowColumnsResizing: true, allowColumnsResizing: true,
allowEditing: true, //
editingGestureType: EditingGestureType.tap, //
); );
} }
@ -162,13 +172,16 @@ class TemplateGridView extends StatelessWidget {
class _TemplateItemDataSource extends DataGridSource { class _TemplateItemDataSource extends DataGridSource {
final List<Map<String, dynamic>> _rows; final List<Map<String, dynamic>> _rows;
final List<TemplateNode> selectedNodes; final List<TemplateNode> selectedNodes;
final Function(TemplateNode, String, dynamic)? onCellValueChanged;
_TemplateItemDataSource({required List<Map<String, dynamic>> rows, required this.selectedNodes}) _TemplateItemDataSource({
: _rows = rows; required List<Map<String, dynamic>> rows,
required this.selectedNodes,
this.onCellValueChanged,
}) : _rows = rows;
@override @override
List<DataGridRow> get rows { List<DataGridRow> get rows {
// print("[DEBUG] 原始可加载记录数:${_rows.length}");
return _rows.asMap().entries.map((entry) { return _rows.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
final rowData = entry.value; final rowData = entry.value;
@ -201,4 +214,54 @@ class _TemplateItemDataSource extends DataGridSource {
}).toList(), }).toList(),
); );
} }
// 1:
@override
Future<void> onCellSubmit(
DataGridRow dataGridRow,
RowColumnIndex rowColumnIndex,
GridColumn column,
) async {
final dynamic newValue = dataGridRow.getCells()[rowColumnIndex.columnIndex].value;
final int dataRowIndex = _rows.indexWhere(
(row) => row['_index'] == dataGridRow.getCells()[0].value,
);
if (dataRowIndex >= 0) {
final node = selectedNodes[rowColumnIndex.columnIndex - 1]; //
_rows[dataRowIndex][node.path] = newValue;
if (onCellValueChanged != null) {
onCellValueChanged!(node, column.columnName, newValue);
}
notifyListeners();
}
}
// 2:
@override
Widget? buildEditWidget(
DataGridRow dataGridRow,
RowColumnIndex rowColumnIndex,
GridColumn column,
CellSubmit submitCell,
) {
//
if (column.columnName == 'index') return null;
final String value = dataGridRow.getCells()[rowColumnIndex.columnIndex].value.toString();
return Container(
padding: const EdgeInsets.all(8.0),
child: TextField(
autofocus: true,
controller: TextEditingController(text: value),
onSubmitted: (String newValue) {
// submitCell调用方式
submitCell();
},
),
);
}
} }

87
win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart

@ -1,31 +1,60 @@
import 'dart:async';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/framework/controllers/logger.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/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/modules/xml_search/services/xml_search_service.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart';
class XmlSearchController extends BaseContentController { class XmlSearchController extends BaseContentController {
String _searchDirectory = ''; String _searchDirectory = '';
String _fileType = '*.*'; String _searchQuery = '';
String nodePath = '';
String attributeName = '';
bool _isSearching = false;
final List<SearchResult> _results = []; final List<SearchResult> _results = [];
final List<XmlRule> _rules = [];
final XmlSearchService _searchService = XmlSearchService(); final XmlSearchService _searchService = XmlSearchService();
List<SearchResult> get results => _results; List<SearchResult> get results => _results;
List<XmlRule> get rules => _rules;
String get searchDirectory => _searchDirectory; String get searchDirectory => _searchDirectory;
String get fileType => _fileType; String get searchQuery => _searchQuery;
bool get isSearching => _isSearching;
bool _isSearching = false; Timer? _searchDebounce;
bool onlyFileName = false; set errorMessage(String value) {
bool get isSearching => _isSearching; Logger().error('打开文件出错:$value');
}
set searchQuery(String value) {
_searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 500), () {
_searchQuery = value;
notifyListeners();
});
}
Future<void> pickFile() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['xml', '*'],
);
if (result != null) {
_searchDirectory = result.files.single.path!;
notifyListeners(); // Consumer
}
}
Future<void> executeSearching() async { Future<void> executeSearching() async {
Logger().info("开始提取目录:$_searchDirectory, 文件名:$_fileType"); Logger().info("开始搜索文件:$_searchDirectory");
if (_searchDirectory.isEmpty || _rules.isEmpty) return; if (_searchDirectory.isEmpty ||
_searchQuery.isEmpty ||
nodePath.isEmpty ||
attributeName.isEmpty) {
Logger().error("所有条件都不能为空。");
return;
}
_isSearching = true; _isSearching = true;
notifyListeners(); notifyListeners();
@ -36,13 +65,13 @@ class XmlSearchController extends BaseContentController {
try { try {
final newResults = await _searchService.searchFromDirectory( final newResults = await _searchService.searchFromDirectory(
directory: _searchDirectory, directory: _searchDirectory,
fileType: _fileType, nodeName: nodePath,
rule: _rules[0], attributeName: attributeName,
queryContent: searchQuery,
); );
_results.addAll(newResults); _results.addAll(newResults);
} catch (e) { } catch (e) {
Logger().error("提取目录出错:$e"); Logger().error("搜索文件出错:$e");
_results.add(SearchResult(rowNum: 1, filePath: 'Error', content: 'Searchion failed: $e'));
} finally { } finally {
_isSearching = false; _isSearching = false;
notifyListeners(); notifyListeners();
@ -54,11 +83,6 @@ class XmlSearchController extends BaseContentController {
notifyListeners(); notifyListeners();
} }
set fileType(String value) {
_fileType = value;
notifyListeners();
}
Future<void> pickDirectory() async { Future<void> pickDirectory() async {
final dir = await FilePicker.platform.getDirectoryPath(); final dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) { if (dir != null) {
@ -66,26 +90,21 @@ class XmlSearchController extends BaseContentController {
} }
} }
void setRule(XmlRule rule) {
_rules.clear();
rules.add(rule);
notifyListeners();
}
void removeRule(int index) {
_rules.removeAt(index);
notifyListeners();
}
@override @override
void onOpenFile(String filePath) { void onOpenFile(String filePath) {
// TODO: implement onOpenFile searchDirectory = filePath;
} }
@override @override
void onOpenFolder(String folderPath) { void onOpenFolder(String folderPath) {
searchDirectory = folderPath; //
} }
void cancelSearchion() {} void cancelSearching() {}
void removeResult(SearchResult result) async {
await _searchService.removeNode(searchDirectory, nodePath, attributeName, result);
results.remove(result);
notifyListeners();
}
} }

6
win_text_editor/lib/modules/xml_search/models/search_result.dart

@ -1,8 +1,8 @@
// search_result.dart // search_result.dart
class SearchResult { class SearchResult {
final int rowNum; final int rowNum;
final String filePath; final String attributeValue;
final String content; int index = 0;
SearchResult({required this.rowNum, required this.filePath, required this.content}); SearchResult({required this.rowNum, required this.attributeValue, this.index = 0});
} }

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

@ -1,18 +0,0 @@
// xml_rule.dart
class XmlRule {
final String nodePath;
final String attributeName;
final bool isFirstOccurrence;
final String? namespacePrefix;
XmlRule({
required this.nodePath,
required this.attributeName,
this.isFirstOccurrence = false,
this.namespacePrefix,
});
String toxPath() {
return '${namespacePrefix != null ? '$namespacePrefix:' : ''}$nodePath${isFirstOccurrence ? '[0]' : ''}/${attributeName.isNotEmpty ? '@$attributeName' : 'text()'}';
}
}

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

@ -1,52 +1,94 @@
// xml_search_service.dart // xml_search_service.dart
import 'dart:io'; import 'dart:io';
import 'package:win_text_editor/framework/controllers/logger.dart'; 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: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/search_result.dart';
import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart';
class XmlSearchService { class XmlSearchService {
Future<List<SearchResult>> searchFromDirectory({ Future<List<SearchResult>> searchFromDirectory({
required String directory, required String directory,
required String fileType, required String nodeName,
required XmlRule rule, required String attributeName,
required String queryContent,
}) async { }) async {
final results = <SearchResult>[]; final results = <SearchResult>[];
final dir = Directory(directory); try {
final searchValues =
queryContent
.split(RegExp(r'[\n,]'))
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
if (searchValues.isEmpty) {
searchValues.add(queryContent);
}
final file = File(directory);
final content = await file.readAsString();
final document = xml.XmlDocument.parse(content);
int rowNum = 1; int rowNum = 1;
await for (var entity in dir.list(recursive: true)) { // 2. Search for nodes with specified name and attribute
if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) { for (int i = 0; i < searchValues.length; i++) {
try { final nodes = document.findAllElements(nodeName);
final fileContent = await entity.readAsString(); int index = 0;
final document = xml.XmlDocument.parse(fileContent); for (final node in nodes) {
final values = _searchWithRule(document, rule); final attributeValue = node.getAttribute(attributeName);
for (var value in values) { if (attributeValue != null && attributeValue == searchValues[i]) {
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: value)); results.add(
SearchResult(rowNum: rowNum++, attributeValue: attributeValue, index: index++),
);
} }
} catch (e) {
Logger().error('xmlSearchService.searchFromDirectory方法执行出错: $e');
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: 'Error: $e'));
} }
} }
Logger().info("共发现记录 ${rowNum - 1}");
} catch (e) {
Logger().error('xmlSearchService.searchFromDirectory方法执行出错: $e');
} }
return results; return results;
} }
List<String> _searchWithRule(xml.XmlDocument document, XmlRule rule) { Future<void> removeNode(
final nodes = document.findAllElements(rule.nodePath); String directory,
//final nodes = SimpleXPath.query(document, rule.toxPath()); String nodeName,
if (rule.isFirstOccurrence && nodes.isNotEmpty) { String attributeName,
final attr = nodes.first.getAttribute(rule.attributeName); SearchResult result,
return attr != null ? [attr] : []; ) async {
try {
// 1. XML
final file = File(directory);
final content = await file.readAsString();
final document = xml.XmlDocument.parse(content);
// 2.
final nodes =
document.findAllElements(nodeName).where((node) {
final attributeValue = node.getAttribute(attributeName);
return attributeValue == result.attributeValue;
}).toList();
// 3.
if (result.index >= 0 && result.index < nodes.length) {
final nodeToRemove = nodes[result.index];
// 4.
nodeToRemove.parent?.children.remove(nodeToRemove);
// 5. XML
final newContent = document.toXmlString(pretty: true);
await file.writeAsString(newContent);
Logger().info(
'成功删除节点: $nodeName[$attributeName="${result.attributeValue}"][${result.index}]',
);
} else { } else {
return nodes Logger().warning('未找到序号为 ${result.index} 的匹配节点');
.map((node) => node.getAttribute(rule.attributeName)) }
.where((attr) => attr != null) } catch (e) {
.cast<String>() Logger().error('删除节点时出错: $e');
.toList(); rethrow; // 便
} }
} }
} }

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

@ -1,8 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.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/controllers/xml_search_controller.dart';
import 'package:win_text_editor/modules/xml_search/models/xml_rule.dart'; import 'package:win_text_editor/shared/components/text_editor.dart';
import 'package:win_text_editor/shared/components/my_checkbox.dart';
class ConditionSetting extends StatefulWidget { class ConditionSetting extends StatefulWidget {
const ConditionSetting({super.key}); const ConditionSetting({super.key});
@ -12,17 +11,22 @@ class ConditionSetting extends StatefulWidget {
} }
class _ConditionSettingState extends State<ConditionSetting> { class _ConditionSettingState extends State<ConditionSetting> {
final _nodePathController = TextEditingController(); bool _isSearching = false;
final _attributeNameController = TextEditingController(); late TextEditingController _nodePathController;
final _namespacePrefixController = TextEditingController(); late TextEditingController _attributeNameController;
bool _isFirstOccurrence = false;
bool _isExtracting = false; @override
void initState() {
super.initState();
final controller = context.read<XmlSearchController>();
_nodePathController = TextEditingController(text: controller.nodePath);
_attributeNameController = TextEditingController(text: controller.attributeName);
}
@override @override
void dispose() { void dispose() {
_nodePathController.dispose(); _nodePathController.dispose();
_attributeNameController.dispose(); _attributeNameController.dispose();
_namespacePrefixController.dispose();
super.dispose(); super.dispose();
} }
@ -36,7 +40,7 @@ class _ConditionSettingState extends State<ConditionSetting> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
const Text('数据提取规则设置:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), const Text('搜索规则设置:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
@ -47,6 +51,9 @@ class _ConditionSettingState extends State<ConditionSetting> {
hintText: '如: business:Service', hintText: '如: business:Service',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
onChanged: (value) {
controller.nodePath = value;
},
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
TextField( TextField(
@ -56,14 +63,24 @@ class _ConditionSettingState extends State<ConditionSetting> {
hintText: '如: chineseName 或 name', hintText: '如: chineseName 或 name',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
onChanged: (value) {
controller.attributeName = value;
},
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
MyCheckbox( SizedBox(
title: '仅提取第一个匹配项', width: MediaQuery.of(context).size.width * 0.5,
value: _isFirstOccurrence, height: 360,
onChanged: (value) => setState(() => _isFirstOccurrence = value ?? false), child: TextEditor(
tabId: 'search_content_${controller.hashCode}',
title: '[列表以\\n或,分隔]',
initialContent: controller.searchQuery, //
onContentChanged: (content) {
controller.searchQuery = content; //
},
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// //
@ -73,7 +90,7 @@ class _ConditionSettingState extends State<ConditionSetting> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: const Icon(Icons.play_arrow), icon: const Icon(Icons.play_arrow),
label: const Text('开始'), label: const Text('开始'),
onPressed: _isExtracting ? null : () => _startSearching(controller), onPressed: _isSearching ? null : () => _startSearching(controller),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@ -81,7 +98,7 @@ class _ConditionSettingState extends State<ConditionSetting> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: const Icon(Icons.stop, color: Colors.red), icon: const Icon(Icons.stop, color: Colors.red),
label: const Text('停止', style: TextStyle(color: Colors.red)), label: const Text('停止', style: TextStyle(color: Colors.red)),
onPressed: _isExtracting ? _stopExtraction : null, onPressed: _isSearching ? _stopSearch : null,
), ),
), ),
], ],
@ -92,49 +109,42 @@ class _ConditionSettingState extends State<ConditionSetting> {
); );
} }
void _setRule() { Future<void> _startSearching(XmlSearchController controller) async {
final nodePath = _nodePathController.text.trim(); if (controller.searchDirectory.isEmpty) {
final attributeName = _attributeNameController.text.trim(); ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择搜索文件')));
return;
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,
),
);
} }
if (controller.nodePath.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先设置节点名称')));
return;
} }
Future<void> _startSearching(XmlSearchController controller) async { if (controller.attributeName.isEmpty) {
if (controller.searchDirectory.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先设置属性名称')));
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择搜索目录')));
return; return;
} }
_setRule(); if (controller.searchQuery.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先设置搜索内容')));
return;
}
setState(() => _isExtracting = true); setState(() => _isSearching = true);
try { try {
await controller.executeSearching(); await controller.executeSearching();
} finally { } finally {
if (mounted) { if (mounted) {
setState(() => _isExtracting = false); setState(() => _isSearching = false);
} }
} }
} }
void _stopExtraction() { void _stopSearch() {
// //
final controller = Provider.of<XmlSearchController>(context, listen: false); final controller = Provider.of<XmlSearchController>(context, listen: false);
// cancelExtraction方法 // cancelExtraction方法
controller.executeSearching(); controller.cancelSearching();
setState(() => _isExtracting = false); setState(() => _isSearching = false);
} }
} }

40
win_text_editor/lib/modules/xml_search/widgets/directory.dart

@ -11,20 +11,17 @@ class Directory extends StatefulWidget {
class _DirectoryState extends State<Directory> { class _DirectoryState extends State<Directory> {
late TextEditingController _searchDirectoryController; late TextEditingController _searchDirectoryController;
late TextEditingController _fileTypeController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final controller = context.read<XmlSearchController>(); final controller = context.read<XmlSearchController>();
_searchDirectoryController = TextEditingController(text: controller.searchDirectory); _searchDirectoryController = TextEditingController(text: controller.searchDirectory);
_fileTypeController = TextEditingController(text: controller.fileType);
} }
@override @override
void dispose() { void dispose() {
_searchDirectoryController.dispose(); _searchDirectoryController.dispose();
_fileTypeController.dispose();
super.dispose(); super.dispose();
} }
@ -36,9 +33,6 @@ class _DirectoryState extends State<Directory> {
if (_searchDirectoryController.text != controller.searchDirectory) { if (_searchDirectoryController.text != controller.searchDirectory) {
_searchDirectoryController.text = controller.searchDirectory; _searchDirectoryController.text = controller.searchDirectory;
} }
if (_fileTypeController.text != controller.fileType) {
_fileTypeController.text = controller.fileType;
}
return Card( return Card(
child: Padding( child: Padding(
@ -47,34 +41,18 @@ class _DirectoryState extends State<Directory> {
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
controller: _searchDirectoryController, decoration: InputDecoration(
decoration: const InputDecoration( labelText: 'XML File',
labelText: '搜索目录', hintText: 'Select an XML file',
border: OutlineInputBorder(), suffixIcon: IconButton(
), icon: const Icon(Icons.folder_open),
onChanged: (value) => controller.searchDirectory = value, onPressed: controller.pickFile,
),
),
const SizedBox(width: 8),
SizedBox(
width: 100,
child: TextField(
controller: _fileTypeController,
decoration: const InputDecoration(
labelText: '文件类型',
border: OutlineInputBorder(),
), ),
onChanged: (value) => controller.fileType = value, border: const OutlineInputBorder(),
), ),
controller: TextEditingController(text: controller.searchDirectory),
readOnly: true,
), ),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
await controller.pickDirectory();
// _searchDirectoryController.text
// Consumer
},
), ),
], ],
), ),

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

@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/xml_search/models/search_result.dart';
import 'dart:io'; import 'dart:io';
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart';
@ -21,7 +24,7 @@ class ResultsView extends StatelessWidget {
onSecondaryTapDown: (details) { onSecondaryTapDown: (details) {
_showContextMenu(context, details.globalPosition, controller); _showContextMenu(context, details.globalPosition, controller);
}, },
child: _buildLocateGrid(controller), child: _buildLocateGrid(controller, context),
), ),
); );
} }
@ -44,11 +47,7 @@ class ResultsView extends StatelessWidget {
position.dx + renderBox.size.width - localPosition.dx, position.dx + renderBox.size.width - localPosition.dx,
position.dy + renderBox.size.height - localPosition.dy, position.dy + renderBox.size.height - localPosition.dy,
), ),
items: [ items: [const PopupMenuItem<String>(value: 'exportAll', child: Text('导出(csv)'))],
const PopupMenuItem<String>(value: 'exportFileName', child: Text('导出文件名(csv)')),
const PopupMenuItem<String>(value: 'exportContent', child: Text('导出内容(csv)')),
const PopupMenuItem<String>(value: 'exportAll', child: Text('导出全部(csv)')),
],
); );
// //
@ -66,22 +65,9 @@ class ResultsView extends StatelessWidget {
} }
Future<void> _exportToCsv(XmlSearchController controller, String? exportType) async { Future<void> _exportToCsv(XmlSearchController controller, String? exportType) async {
String csvData = ''; String csvData = '搜索值\t节点\n';
csvData =
exportType == 'exportFileName'
? '文件名称\n'
: (exportType == 'exportContent' ? '内容\n' : '文件名称\t内容\n');
for (var result in controller.results) { for (var result in controller.results) {
switch (exportType) { csvData += '${path.basename(result.attributeValue)}\t${result.index}\n';
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( final filePath = await FilePicker.platform.saveFile(
@ -97,15 +83,16 @@ class ResultsView extends StatelessWidget {
} }
} }
Widget _buildLocateGrid(XmlSearchController controller) { Widget _buildLocateGrid(XmlSearchController controller, BuildContext context) {
return SfDataGrid( return SfDataGrid(
rowHeight: 32, rowHeight: 32,
headerRowHeight: 32, headerRowHeight: 32,
source: LocateDataSource(controller), source: LocateDataSource(controller, context),
columns: [ columns: [
ShortGridColumn(columnName: 'rowNum', label: '序号'), ShortGridColumn(columnName: 'rowNum', label: '序号'),
MyGridColumn(columnName: 'file', label: '文件名称', minimumWidth: 300), MyGridColumn(columnName: 'content', label: '搜索内容', minimumWidth: 300),
MyGridColumn(columnName: 'content', label: '内容'), ShortGridColumn(columnName: 'index', label: '节点序号', width: 80),
ShortGridColumn(columnName: 'action', label: '操作', width: 90),
], ],
selectionMode: SelectionMode.multiple, selectionMode: SelectionMode.multiple,
navigationMode: GridNavigationMode.cell, navigationMode: GridNavigationMode.cell,
@ -123,8 +110,9 @@ class ResultsView extends StatelessWidget {
class LocateDataSource extends DataGridSource { class LocateDataSource extends DataGridSource {
final XmlSearchController controller; final XmlSearchController controller;
final BuildContext context;
LocateDataSource(this.controller); LocateDataSource(this.controller, this.context);
@override @override
List<DataGridRow> get rows => List<DataGridRow> get rows =>
@ -132,23 +120,130 @@ class LocateDataSource extends DataGridSource {
return DataGridRow( return DataGridRow(
cells: [ cells: [
DataGridCell(columnName: 'rowNum', value: result.rowNum), DataGridCell(columnName: 'rowNum', value: result.rowNum),
DataGridCell(columnName: 'file', value: path.basename(result.filePath)), DataGridCell(columnName: 'content', value: path.basename(result.attributeValue)),
DataGridCell(columnName: 'content', value: result.content), DataGridCell(columnName: 'index', value: result.index),
DataGridCell(
columnName: 'action',
value: result, // Store file path for delete action
),
], ],
); );
}).toList(); }).toList();
@override @override
DataGridRowAdapter? buildRow(DataGridRow row) { DataGridRowAdapter? buildRow(DataGridRow row) {
final cells = row.getCells();
final result = cells[3].value as SearchResult;
return DataGridRowAdapter( return DataGridRowAdapter(
cells: cells: [
row.getCells().map<Widget>((cell) { Container(
return Container( alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cells[0].value.toString()),
),
//
Container(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cell.value.toString(), overflow: TextOverflow.ellipsis), child: Text(cells[1].value.toString(), overflow: TextOverflow.ellipsis, maxLines: 1),
),
//
Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cells[2].value.toString()),
),
Container(
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.delete_forever, size: 18, color: Colors.red),
onPressed: () => _showDeleteConfirmation(result),
),
],
),
),
],
);
}
Future<void> _showDeleteConfirmation(SearchResult result) async {
bool confirmed = false;
await showDialog<bool>(
context: context,
builder: (context) {
//
return Shortcuts(
shortcuts: const {
//
SingleActivator(LogicalKeyboardKey.enter): _ConfirmAction(),
},
child: Actions(
actions: {
_ConfirmAction: CallbackAction<_ConfirmAction>(
onInvoke: (_) {
confirmed = true;
Navigator.pop(context, true);
return null;
},
),
},
child: Focus(
autofocus: true, //
child: AlertDialog(
title: const Text('确认删除'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('将所选xml节点删除吗?'),
const SizedBox(height: 8),
Text(
'${result.attributeValue}[${result.index}]',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () {
confirmed = true;
Navigator.pop(context, true);
},
child: const Text('确认'),
),
],
),
),
),
); );
}).toList(), },
); );
if (confirmed && context.mounted) {
try {
controller.removeResult(result);
notifyListeners();
if (context.mounted) {
Logger().info('已删除节点文件: ${result.attributeValue}[${result.index}]');
}
} catch (e) {
if (context.mounted) {
Logger().error('删除失败: ${e.toString()}');
} }
}
}
}
}
class _ConfirmAction extends Intent {
const _ConfirmAction();
} }

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

@ -7,6 +7,7 @@ class MyGridColumn extends GridColumn {
double minimumWidth = 100, double minimumWidth = 100,
double maximumWidth = double.infinity, double maximumWidth = double.infinity,
required String label, required String label,
bool allowEditing = false,
}) : super( }) : super(
columnName: columnName, columnName: columnName,
minimumWidth: minimumWidth, minimumWidth: minimumWidth,
@ -17,12 +18,17 @@ class MyGridColumn extends GridColumn {
padding: const EdgeInsets.all(2.0), padding: const EdgeInsets.all(2.0),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)), child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)),
), ),
allowEditing: allowEditing,
); );
} }
class ShortGridColumn extends GridColumn { class ShortGridColumn extends GridColumn {
ShortGridColumn({required String columnName, double width = 60, required String label}) ShortGridColumn({
: super( required String columnName,
double width = 60,
required String label,
bool allowEditing = false,
}) : super(
columnName: columnName, columnName: columnName,
width: width, width: width,
label: Container( label: Container(
@ -31,5 +37,6 @@ class ShortGridColumn extends GridColumn {
padding: const EdgeInsets.all(2.0), padding: const EdgeInsets.all(2.0),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)), child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)),
), ),
allowEditing: allowEditing,
); );
} }

Loading…
Cancel
Save