From ea72f1dc0f46a4f798fbd115c2711c79e70c9d0d Mon Sep 17 00:00:00 2001 From: hejl Date: Sun, 8 Jun 2025 13:47:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=95=E5=85=A5pluto=5Fgrid=E6=88=90?= =?UTF-8?q?=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/custom_search_service.dart | 1 - .../controllers/xml_search_controller.dart | 52 +++-- .../xml_search/models/search_result.dart | 18 +- .../services/xml_search_service.dart | 5 +- .../xml_search/widgets/results_view.dart | 178 +++++++----------- .../components/my_pluto_configuration.dart | 25 +++ 6 files changed, 147 insertions(+), 132 deletions(-) create mode 100644 win_text_editor/lib/shared/components/my_pluto_configuration.dart diff --git a/win_text_editor/lib/modules/content_search/services/custom_search_service.dart b/win_text_editor/lib/modules/content_search/services/custom_search_service.dart index 9504738..07daeac 100644 --- a/win_text_editor/lib/modules/content_search/services/custom_search_service.dart +++ b/win_text_editor/lib/modules/content_search/services/custom_search_service.dart @@ -6,7 +6,6 @@ import 'package:flutter_js/flutter_js.dart'; import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/modules/content_search/models/search_mode.dart'; import 'package:win_text_editor/modules/content_search/models/search_result.dart'; -import 'package:win_text_editor/modules/content_search/services/base_search_service.dart'; import 'package:win_text_editor/shared/utils/file_utils.dart'; typedef ProgressCallback = void Function(double progress); diff --git a/win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart b/win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart index a5a9be5..822cca2 100644 --- a/win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart +++ b/win_text_editor/lib/modules/xml_search/controllers/xml_search_controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.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/services/xml_search_service.dart'; @@ -13,16 +14,18 @@ class XmlSearchController extends BaseContentController { String attributeName = ''; bool _isSearching = false; - final List _results = [SearchResult(rowNum: 1, attributeValue: '模拟数据')]; + final List _results = []; final XmlSearchService _searchService = XmlSearchService(); - List get results => _results; + List get results => List.unmodifiable(_results); // 返回不可修改的副本 String get searchDirectory => _searchDirectory; String get searchQuery => _searchQuery; bool get isSearching => _isSearching; Timer? _searchDebounce; + final ValueNotifier refreshNotifier = ValueNotifier(false); + set errorMessage(String value) { Logger().error('打开文件出错:$value'); } @@ -35,6 +38,20 @@ class XmlSearchController extends BaseContentController { }); } + void removeResult(SearchResult result) async { + try { + Logger().debug('删除前结果数: ${_results.length}'); + await _searchService.removeNodes(searchDirectory, nodePath, attributeName, [result]); + _results.removeWhere((r) => r.hashCode == result.hashCode); // 使用hashCode确保正确删除 + Logger().debug('删除后结果数: ${_results.length}'); + notifyListeners(); + Future.delayed(Duration.zero, notifyListeners); + } catch (e) { + Logger().error('删除失败: $e'); + rethrow; + } + } + Future pickFile() async { final result = await FilePicker.platform.pickFiles( type: FileType.custom, @@ -59,17 +76,19 @@ class XmlSearchController extends BaseContentController { _isSearching = true; notifyListeners(); - _results.clear(); - notifyListeners(); - try { - final newResults = await _searchService.searchFromDirectory( - directory: _searchDirectory, - nodeName: nodePath, - attributeName: attributeName, - queryContent: searchQuery, + _results.clear(); + _results.addAll( + await _searchService.searchFromDirectory( + directory: _searchDirectory, + nodeName: nodePath, + attributeName: attributeName, + queryContent: searchQuery, + ), ); - _results.addAll(newResults); + notifyListeners(); + // 添加延迟二次通知 + Future.delayed(Duration.zero, notifyListeners); } catch (e) { Logger().error("搜索文件出错:$e"); } finally { @@ -78,6 +97,10 @@ class XmlSearchController extends BaseContentController { } } + void refresh() { + notifyListeners(); + } + set searchDirectory(String value) { _searchDirectory = value; notifyListeners(); @@ -102,17 +125,10 @@ class XmlSearchController extends BaseContentController { void cancelSearching() {} - void removeResult(SearchResult result) async { - await _searchService.removeNodes(searchDirectory, nodePath, attributeName, [result]); - results.remove(result); - notifyListeners(); - } - void toggleSelectAll(bool select) { for (var result in results) { result.isSelected = select; } - notifyListeners(); } List getSelectedResults() { diff --git a/win_text_editor/lib/modules/xml_search/models/search_result.dart b/win_text_editor/lib/modules/xml_search/models/search_result.dart index 0f2957d..1d4a6a2 100644 --- a/win_text_editor/lib/modules/xml_search/models/search_result.dart +++ b/win_text_editor/lib/modules/xml_search/models/search_result.dart @@ -1,9 +1,8 @@ -// search_result.dart class SearchResult { final int rowNum; final String attributeValue; int index = 0; - bool isSelected = false; // 新增选择状态字段 + bool isSelected; SearchResult({ required this.rowNum, @@ -11,4 +10,19 @@ class SearchResult { this.index = 0, this.isSelected = false, }); + + // 必须正确实现equals和hashCode + @override + bool operator ==(Object other) => + identical(this, other) || + other is SearchResult && + runtimeType == other.runtimeType && + rowNum == other.rowNum && + attributeValue == other.attributeValue && + index == other.index && + isSelected == other.isSelected; + + @override + int get hashCode => + rowNum.hashCode ^ attributeValue.hashCode ^ index.hashCode ^ isSelected.hashCode; } diff --git a/win_text_editor/lib/modules/xml_search/services/xml_search_service.dart b/win_text_editor/lib/modules/xml_search/services/xml_search_service.dart index c105844..5c9047d 100644 --- a/win_text_editor/lib/modules/xml_search/services/xml_search_service.dart +++ b/win_text_editor/lib/modules/xml_search/services/xml_search_service.dart @@ -63,8 +63,9 @@ class XmlSearchService { final document = xml.XmlDocument.parse(content); String newContent = content; - // 2. 查找所有匹配的节点 - for (SearchResult result in resultList) { + // 2. 查找所有匹配的节点,反向删除(避免删除后序号为空) + for (int i = resultList.length - 1; i >= 0; i--) { + SearchResult result = resultList[i]; final nodes = document.findAllElements(nodeName).where((node) { final attributeValue = node.getAttribute(attributeName); diff --git a/win_text_editor/lib/modules/xml_search/widgets/results_view.dart b/win_text_editor/lib/modules/xml_search/widgets/results_view.dart index 8c3551e..c89c06d 100644 --- a/win_text_editor/lib/modules/xml_search/widgets/results_view.dart +++ b/win_text_editor/lib/modules/xml_search/widgets/results_view.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:pluto_grid/pluto_grid.dart'; import 'package:path/path.dart' as path; @@ -11,6 +10,7 @@ import 'dart:io'; import 'package:win_text_editor/modules/xml_search/controllers/xml_search_controller.dart'; import 'package:win_text_editor/shared/components/my_pluto_column.dart'; +import 'package:win_text_editor/shared/components/my_pluto_configuration.dart'; class ResultsView extends StatefulWidget { const ResultsView({super.key}); @@ -20,19 +20,29 @@ class ResultsView extends StatefulWidget { } class _ResultsViewState extends State { - late final PlutoGridStateManager stateManager; + late final XmlSearchController controller; + PlutoGridStateManager? stateManager; @override - Widget build(BuildContext context) { - final controller = context.watch(); + void initState() { + super.initState(); + controller = context.read(); + } - return Card( - child: GestureDetector( - onSecondaryTapDown: (details) { - _showContextMenu(context, details.globalPosition, controller); - }, - child: _buildLocateGrid(controller, context), - ), + @override + Widget build(BuildContext context) { + // 直接使用Consumer而不是ValueListenableBuilder + return Consumer( + builder: (context, controller, child) { + return Card( + child: GestureDetector( + onSecondaryTapDown: (details) { + _showContextMenu(context, details.globalPosition, controller); + }, + child: _buildResultGrid(controller), + ), + ); + }, ); } @@ -87,68 +97,40 @@ class _ResultsViewState extends State { } } - Widget _buildLocateGrid(XmlSearchController controller, BuildContext context) { + Widget _buildResultGrid(XmlSearchController controller) { return PlutoGrid( - configuration: const PlutoGridConfiguration( - style: PlutoGridStyleConfig( - rowHeight: 32, - columnHeight: 32, - iconColor: Colors.transparent, - gridBorderRadius: BorderRadius.zero, - columnTextStyle: TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.normal, - ), - ), - columnSize: PlutoGridColumnSizeConfig( - autoSizeMode: PlutoAutoSizeMode.scale, - resizeMode: PlutoResizeMode.pushAndPull, - ), - ), - + key: ValueKey(controller.results.hashCode), + configuration: MyPlutoGridConfiguration(), columns: _buildColumns(), mode: PlutoGridMode.normal, - rows: _buildRows(controller), - onLoaded: (PlutoGridOnLoadedEvent event) { - stateManager = event.stateManager; - stateManager.setSelectingMode(PlutoGridSelectingMode.row); - - // 添加行选中状态监听器 - stateManager.addListener(() { - if (stateManager.hasFocus) { - final checkedRows = stateManager.checkedRows; - for (var row in checkedRows) { - final result = row.cells['action']?.value as SearchResult?; - if (result != null) { - result.isSelected = true; - } - } + rows: controller.results.isEmpty ? [] : _buildRows(controller), + noRowsWidget: const Center(child: Text('没有搜索结果')), - // 更新未选中的行 - for (var row in stateManager.refRows.where((r) => !checkedRows.contains(r))) { - final result = row.cells['action']?.value as SearchResult?; - if (result != null) { - result.isSelected = false; - } - } + onLoaded: (PlutoGridOnLoadedEvent event) { + stateManager ??= event.stateManager; + }, - controller.notifyListeners(); - } - }); + onRowChecked: (PlutoGridOnRowCheckedEvent e) { + if (e.isAll) { + controller.toggleSelectAll(e.isChecked!); + } else { + final result = e.row?.cells['action']!.value as SearchResult; + result.isSelected = e.isChecked!; + } }, - noRowsWidget: const Center(child: Text('没有搜索结果')), ); } List _buildRows(XmlSearchController controller) { - return controller.results.map((result) { + return controller.results.asMap().entries.map((entry) { + final index = entry.key; + final result = entry.value; return PlutoRow( - checked: result.isSelected, // 绑定选中状态到内置复选框 + key: ValueKey('${result.hashCode}_$index'), // 复合key确保唯一性 cells: { - 'rowNum': PlutoCell(value: result.rowNum), + 'rowNum': PlutoCell(value: index + 1), 'content': PlutoCell(value: result.attributeValue), - 'index': PlutoCell(value: result.index ?? 0), + 'index': PlutoCell(value: result.index), 'action': PlutoCell(value: result), }, ); @@ -157,7 +139,7 @@ class _ResultsViewState extends State { List _buildColumns() { return [ - MyPlutoColumn(title: '序号', field: 'rowNum', width: 90, checkable: true), + MyPlutoColumn(title: '#', field: 'rowNum', width: 60, checkable: true), MyPlutoColumn( title: '搜索内容', field: 'content', @@ -190,69 +172,47 @@ class _ResultsViewState extends State { PlutoRow row, SearchResult result, ) async { - bool confirmed = + final confirmed = await showDialog( context: context, - builder: (context) { - return Shortcuts( - shortcuts: const {SingleActivator(LogicalKeyboardKey.enter): _ConfirmAction()}, - child: Actions( - actions: { - _ConfirmAction: CallbackAction<_ConfirmAction>( - onInvoke: (_) { - Navigator.pop(context, true); - return null; - }, + builder: + (context) => AlertDialog( + title: const Text('确认删除'), + content: Text('确定要删除 ${result.attributeValue} 吗?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('取消'), ), - }, - 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: () => Navigator.pop(context, true), - child: const Text('确认'), - ), - ], + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('删除'), ), - ), + ], ), - ); - }, ) ?? false; if (confirmed && context.mounted) { try { final controller = context.read(); + + // 1. 从数据源删除 controller.removeResult(result); - stateManager.removeRows([row]); - Logger().info('已删除节点文件: ${result.attributeValue}[${result.index}]'); + Logger().info('已删除节点: ${result.attributeValue}'); + + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('已删除: ${result.attributeValue}'))); } catch (e) { - Logger().error('删除失败: ${e.toString()}'); + Logger().error('删除失败: $e'); + if (context.mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('删除失败: ${e.toString()}'))); + } } } } } - -class _ConfirmAction extends Intent { - const _ConfirmAction(); -} diff --git a/win_text_editor/lib/shared/components/my_pluto_configuration.dart b/win_text_editor/lib/shared/components/my_pluto_configuration.dart new file mode 100644 index 0000000..1de717b --- /dev/null +++ b/win_text_editor/lib/shared/components/my_pluto_configuration.dart @@ -0,0 +1,25 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:pluto_grid/pluto_grid.dart'; + +class MyPlutoGridConfiguration extends PlutoGridConfiguration { + MyPlutoGridConfiguration() + : super( + style: const PlutoGridStyleConfig( + rowHeight: 32, + columnHeight: 32, + iconColor: Colors.transparent, + gridBorderRadius: BorderRadius.zero, + columnTextStyle: TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.normal, + ), + ), + columnSize: const PlutoGridColumnSizeConfig( + autoSizeMode: PlutoAutoSizeMode.scale, + resizeMode: PlutoResizeMode.pushAndPull, + ), + ); +}