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. 102
      win_text_editor/lib/modules/xml_search/services/xml_search_service.dart
  8. 94
      win_text_editor/lib/modules/xml_search/widgets/condition_setting.dart
  9. 42
      win_text_editor/lib/modules/xml_search/widgets/directory.dart
  10. 167
      win_text_editor/lib/modules/xml_search/widgets/results_view.dart
  11. 29
      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 { @@ -50,4 +50,8 @@ class GridViewController extends SafeNotifier {
_isFilterApplied = false;
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 { @@ -58,6 +58,7 @@ class TemplateParserController extends BaseContentController {
void setStatisticsMode(String? value) {
statisticsMode = value ?? modeByPath;
_loadTemplateData();
notifyListeners();
}

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

@ -102,21 +102,29 @@ class TemplateGridView extends StatelessWidget { @@ -102,21 +102,29 @@ class TemplateGridView extends StatelessWidget {
return const Center(child: Text('请在左侧树中选择要显示的节点(勾选复选框)'));
}
//
final allItems = controller.displayedItems;
// -
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>[
ShortGridColumn(columnName: 'index', label: '序号'),
ShortGridColumn(
columnName: 'index',
label: '序号',
allowEditing: false, //
),
...selectedNodes.map((node) {
return MyGridColumn(
columnName: node.path,
label: node.isAttribute ? node.name.substring(1) : node.name,
allowEditing: true, //
);
}).toList(),
];
@ -130,6 +138,8 @@ class TemplateGridView extends StatelessWidget { @@ -130,6 +138,8 @@ class TemplateGridView extends StatelessWidget {
headerGridLinesVisibility: GridLinesVisibility.both,
columnWidthMode: ColumnWidthMode.fill,
allowColumnsResizing: true,
allowEditing: true, //
editingGestureType: EditingGestureType.tap, //
);
}
@ -162,13 +172,16 @@ class TemplateGridView extends StatelessWidget { @@ -162,13 +172,16 @@ class TemplateGridView extends StatelessWidget {
class _TemplateItemDataSource extends DataGridSource {
final List<Map<String, dynamic>> _rows;
final List<TemplateNode> selectedNodes;
final Function(TemplateNode, String, dynamic)? onCellValueChanged;
_TemplateItemDataSource({required List<Map<String, dynamic>> rows, required this.selectedNodes})
: _rows = rows;
_TemplateItemDataSource({
required List<Map<String, dynamic>> rows,
required this.selectedNodes,
this.onCellValueChanged,
}) : _rows = rows;
@override
List<DataGridRow> get rows {
// print("[DEBUG] 原始可加载记录数:${_rows.length}");
return _rows.asMap().entries.map((entry) {
final index = entry.key;
final rowData = entry.value;
@ -201,4 +214,54 @@ class _TemplateItemDataSource extends DataGridSource { @@ -201,4 +214,54 @@ class _TemplateItemDataSource extends DataGridSource {
}).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 @@ @@ -1,31 +1,60 @@
import 'dart:async';
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 = '*.*';
String _searchQuery = '';
String nodePath = '';
String attributeName = '';
bool _isSearching = false;
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;
String get searchQuery => _searchQuery;
bool get isSearching => _isSearching;
bool _isSearching = false;
Timer? _searchDebounce;
bool onlyFileName = false;
bool get isSearching => _isSearching;
set errorMessage(String value) {
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 {
Logger().info("开始提取目录:$_searchDirectory, 文件名:$_fileType");
if (_searchDirectory.isEmpty || _rules.isEmpty) return;
Logger().info("开始搜索文件:$_searchDirectory");
if (_searchDirectory.isEmpty ||
_searchQuery.isEmpty ||
nodePath.isEmpty ||
attributeName.isEmpty) {
Logger().error("所有条件都不能为空。");
return;
}
_isSearching = true;
notifyListeners();
@ -36,13 +65,13 @@ class XmlSearchController extends BaseContentController { @@ -36,13 +65,13 @@ class XmlSearchController extends BaseContentController {
try {
final newResults = await _searchService.searchFromDirectory(
directory: _searchDirectory,
fileType: _fileType,
rule: _rules[0],
nodeName: nodePath,
attributeName: attributeName,
queryContent: searchQuery,
);
_results.addAll(newResults);
} catch (e) {
Logger().error("提取目录出错:$e");
_results.add(SearchResult(rowNum: 1, filePath: 'Error', content: 'Searchion failed: $e'));
Logger().error("搜索文件出错:$e");
} finally {
_isSearching = false;
notifyListeners();
@ -54,11 +83,6 @@ class XmlSearchController extends BaseContentController { @@ -54,11 +83,6 @@ class XmlSearchController extends BaseContentController {
notifyListeners();
}
set fileType(String value) {
_fileType = value;
notifyListeners();
}
Future<void> pickDirectory() async {
final dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) {
@ -66,26 +90,21 @@ class XmlSearchController extends BaseContentController { @@ -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
void onOpenFile(String filePath) {
// TODO: implement onOpenFile
searchDirectory = filePath;
}
@override
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 @@ @@ -1,8 +1,8 @@
// search_result.dart
class SearchResult {
final int rowNum;
final String filePath;
final String content;
final String attributeValue;
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 @@ @@ -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()'}';
}
}

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

@ -1,52 +1,94 @@ @@ -1,52 +1,94 @@
// 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,
required String nodeName,
required String attributeName,
required String queryContent,
}) 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));
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;
// 2. Search for nodes with specified name and attribute
for (int i = 0; i < searchValues.length; i++) {
final nodes = document.findAllElements(nodeName);
int index = 0;
for (final node in nodes) {
final attributeValue = node.getAttribute(attributeName);
if (attributeValue != null && attributeValue == searchValues[i]) {
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;
}
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();
Future<void> removeNode(
String directory,
String nodeName,
String attributeName,
SearchResult result,
) 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 {
Logger().warning('未找到序号为 ${result.index} 的匹配节点');
}
} catch (e) {
Logger().error('删除节点时出错: $e');
rethrow; // 便
}
}
}

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

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

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

@ -11,20 +11,17 @@ class Directory extends StatefulWidget { @@ -11,20 +11,17 @@ class Directory extends StatefulWidget {
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();
}
@ -36,9 +33,6 @@ class _DirectoryState extends State<Directory> { @@ -36,9 +33,6 @@ class _DirectoryState extends State<Directory> {
if (_searchDirectoryController.text != controller.searchDirectory) {
_searchDirectoryController.text = controller.searchDirectory;
}
if (_fileTypeController.text != controller.fileType) {
_fileTypeController.text = controller.fileType;
}
return Card(
child: Padding(
@ -47,35 +41,19 @@ class _DirectoryState extends State<Directory> { @@ -47,35 +41,19 @@ class _DirectoryState extends State<Directory> {
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(),
decoration: InputDecoration(
labelText: 'XML File',
hintText: 'Select an XML file',
suffixIcon: IconButton(
icon: const Icon(Icons.folder_open),
onPressed: controller.pickFile,
),
border: const OutlineInputBorder(),
),
onChanged: (value) => controller.fileType = value,
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
},
),
],
),
),

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

@ -1,9 +1,12 @@ @@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.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 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/xml_search/models/search_result.dart';
import 'dart:io';
import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart';
@ -21,7 +24,7 @@ class ResultsView extends StatelessWidget { @@ -21,7 +24,7 @@ class ResultsView extends StatelessWidget {
onSecondaryTapDown: (details) {
_showContextMenu(context, details.globalPosition, controller);
},
child: _buildLocateGrid(controller),
child: _buildLocateGrid(controller, context),
),
);
}
@ -44,11 +47,7 @@ class ResultsView extends StatelessWidget { @@ -44,11 +47,7 @@ class ResultsView extends StatelessWidget {
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)')),
],
items: [const PopupMenuItem<String>(value: 'exportAll', child: Text('导出(csv)'))],
);
//
@ -66,22 +65,9 @@ class ResultsView extends StatelessWidget { @@ -66,22 +65,9 @@ class ResultsView extends StatelessWidget {
}
Future<void> _exportToCsv(XmlSearchController controller, String? exportType) async {
String csvData = '';
csvData =
exportType == 'exportFileName'
? '文件名称\n'
: (exportType == 'exportContent' ? '内容\n' : '文件名称\t内容\n');
String csvData = '搜索值\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';
}
csvData += '${path.basename(result.attributeValue)}\t${result.index}\n';
}
final filePath = await FilePicker.platform.saveFile(
@ -97,15 +83,16 @@ class ResultsView extends StatelessWidget { @@ -97,15 +83,16 @@ class ResultsView extends StatelessWidget {
}
}
Widget _buildLocateGrid(XmlSearchController controller) {
Widget _buildLocateGrid(XmlSearchController controller, BuildContext context) {
return SfDataGrid(
rowHeight: 32,
headerRowHeight: 32,
source: LocateDataSource(controller),
source: LocateDataSource(controller, context),
columns: [
ShortGridColumn(columnName: 'rowNum', label: '序号'),
MyGridColumn(columnName: 'file', label: '文件名称', minimumWidth: 300),
MyGridColumn(columnName: 'content', label: '内容'),
MyGridColumn(columnName: 'content', label: '搜索内容', minimumWidth: 300),
ShortGridColumn(columnName: 'index', label: '节点序号', width: 80),
ShortGridColumn(columnName: 'action', label: '操作', width: 90),
],
selectionMode: SelectionMode.multiple,
navigationMode: GridNavigationMode.cell,
@ -123,8 +110,9 @@ class ResultsView extends StatelessWidget { @@ -123,8 +110,9 @@ class ResultsView extends StatelessWidget {
class LocateDataSource extends DataGridSource {
final XmlSearchController controller;
final BuildContext context;
LocateDataSource(this.controller);
LocateDataSource(this.controller, this.context);
@override
List<DataGridRow> get rows =>
@ -132,23 +120,130 @@ class LocateDataSource extends DataGridSource { @@ -132,23 +120,130 @@ class LocateDataSource extends DataGridSource {
return DataGridRow(
cells: [
DataGridCell(columnName: 'rowNum', value: result.rowNum),
DataGridCell(columnName: 'file', value: path.basename(result.filePath)),
DataGridCell(columnName: 'content', value: result.content),
DataGridCell(columnName: 'content', value: path.basename(result.attributeValue)),
DataGridCell(columnName: 'index', value: result.index),
DataGridCell(
columnName: 'action',
value: result, // Store file path for delete action
),
],
);
}).toList();
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
final cells = row.getCells();
final result = cells[3].value as SearchResult;
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(),
cells: [
Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cells[0].value.toString()),
),
//
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
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('确认'),
),
],
),
),
),
);
},
);
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();
}

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

@ -7,6 +7,7 @@ class MyGridColumn extends GridColumn { @@ -7,6 +7,7 @@ class MyGridColumn extends GridColumn {
double minimumWidth = 100,
double maximumWidth = double.infinity,
required String label,
bool allowEditing = false,
}) : super(
columnName: columnName,
minimumWidth: minimumWidth,
@ -17,19 +18,25 @@ class MyGridColumn extends GridColumn { @@ -17,19 +18,25 @@ class MyGridColumn extends GridColumn {
padding: const EdgeInsets.all(2.0),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)),
),
allowEditing: allowEditing,
);
}
class ShortGridColumn extends GridColumn {
ShortGridColumn({required String columnName, double width = 60, required String label})
: super(
columnName: columnName,
width: width,
label: Container(
alignment: Alignment.center,
color: Colors.grey[200],
padding: const EdgeInsets.all(2.0),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)),
),
);
ShortGridColumn({
required String columnName,
double width = 60,
required String label,
bool allowEditing = false,
}) : super(
columnName: columnName,
width: width,
label: Container(
alignment: Alignment.center,
color: Colors.grey[200],
padding: const EdgeInsets.all(2.0),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.normal)),
),
allowEditing: allowEditing,
);
}

Loading…
Cancel
Save