diff --git a/win_text_editor/lib/modules/content_search/models/count_result.dart b/win_text_editor/lib/modules/content_search/models/count_result.dart new file mode 100644 index 0000000..53cc863 --- /dev/null +++ b/win_text_editor/lib/modules/content_search/models/count_result.dart @@ -0,0 +1,10 @@ +import 'package:win_text_editor/shared/base/selectable_item.dart'; + +class CountResult implements SelectableItem { + final String keyword; + final int matchCount; + @override + bool isSelected; + + CountResult({required this.keyword, required this.matchCount, this.isSelected = false}); +} diff --git a/win_text_editor/lib/modules/content_search/models/search_result.dart b/win_text_editor/lib/modules/content_search/models/search_result.dart index 09223bc..ac8a7fd 100644 --- a/win_text_editor/lib/modules/content_search/models/search_result.dart +++ b/win_text_editor/lib/modules/content_search/models/search_result.dart @@ -1,11 +1,14 @@ import 'package:win_text_editor/modules/content_search/models/match_result.dart'; +import 'package:win_text_editor/shared/base/selectable_item.dart'; -class SearchResult { +class SearchResult implements SelectableItem { final String filePath; final int lineNumber; final String lineContent; final List matches; final String queryTerm; // 记录匹配的查询项 + @override + bool isSelected; SearchResult({ required this.filePath, @@ -13,5 +16,6 @@ class SearchResult { required this.lineContent, required this.matches, required this.queryTerm, + this.isSelected = false, }); } diff --git a/win_text_editor/lib/modules/content_search/widgets/results_view.dart b/win_text_editor/lib/modules/content_search/widgets/results_view.dart index d31224a..d1b6141 100644 --- a/win_text_editor/lib/modules/content_search/widgets/results_view.dart +++ b/win_text_editor/lib/modules/content_search/widgets/results_view.dart @@ -6,10 +6,13 @@ import 'package:path/path.dart' as path; import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:win_text_editor/modules/content_search/models/count_result.dart'; import 'dart:io'; 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/shared/base/my_sf_data_grid.dart'; +import 'package:win_text_editor/shared/base/my_sf_data_source.dart'; import 'package:win_text_editor/shared/components/my_grid_column.dart'; // import 'package:recycle_bin/recycle_bin.dart'; @@ -29,88 +32,15 @@ class ResultsView extends StatelessWidget { } return Card( - child: GestureDetector( - onSecondaryTapDown: (details) { - _showContextMenu(context, details.globalPosition, controller); - }, - child: _buildResultsGrid(controller, context), - ), + child: + controller.searchMode == SearchMode.locate + ? _buildLocateGrid(controller, context) + : _buildCountGrid(controller, context), ); } - Future _showContextMenu( - BuildContext context, - Offset position, - ContentSearchController controller, - ) async { - // 获取渲染对象以正确定位菜单 - final renderBox = context.findRenderObject() as RenderBox; - final localPosition = renderBox.globalToLocal(position); - - // 显示菜单并等待选择结果 - final result = await showMenu( - 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(value: 'export', child: Text('导出(csv)'))], - ); - - // 处理菜单选择结果 - if (result == 'export' && context.mounted) { - try { - await _exportToCsv(controller); - } catch (e) { - if (context.mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); - } - } - } - } - - Future _exportToCsv(ContentSearchController controller) async { - String csvData = ''; - if (controller.searchMode == SearchMode.locate) { - csvData = '文件\t行号\t内容\n'; - for (var result in controller.results) { - csvData += - '${path.basename(result.filePath)}\t${result.lineNumber}\t${result.lineContent}\n'; - } - } else { - csvData = '关键词\t匹配数量\n'; - for (var result in controller.results) { - csvData += '${result.filePath}\t${result.lineNumber}\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 _buildResultsGrid(ContentSearchController controller, BuildContext context) { - return controller.searchMode == SearchMode.locate - ? _buildLocateGrid(controller, context) - : _buildCountGrid(controller); - } - Widget _buildLocateGrid(ContentSearchController controller, BuildContext context) { - return SfDataGrid( - rowHeight: 32, - headerRowHeight: 32, + return MySfDataGrid( source: LocateDataSource(controller, context), columns: [ ShortGridColumn(columnName: 'index', label: '序号'), @@ -118,45 +48,29 @@ class ResultsView extends StatelessWidget { MyGridColumn(columnName: 'content', label: '内容'), ShortGridColumn(columnName: 'action', label: '操作', width: 90), ], - 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, + selectable: false, ); } - Widget _buildCountGrid(ContentSearchController controller) { - return SfDataGrid( - rowHeight: 32, - headerRowHeight: 32, - source: CountDataSource(controller), + Widget _buildCountGrid(ContentSearchController controller, BuildContext context) { + return MySfDataGrid( + source: CountDataSource(controller, context), columns: [ ShortGridColumn(columnName: 'index', label: '序号'), MyGridColumn(columnName: 'keyword', label: '关键词', minimumWidth: 300), - MyGridColumn(columnName: 'count', label: '匹配数量'), + MyGridColumn(columnName: 'matchCount', label: '匹配数量'), ], - selectionMode: SelectionMode.multiple, - navigationMode: GridNavigationMode.cell, - allowColumnsResizing: true, - gridLinesVisibility: GridLinesVisibility.both, - headerGridLinesVisibility: GridLinesVisibility.both, - columnWidthMode: ColumnWidthMode.fill, - isScrollbarAlwaysShown: true, + selectable: false, ); } } -class LocateDataSource extends DataGridSource { +class LocateDataSource extends MySfDataSource { final ContentSearchController controller; final BuildContext context; - LocateDataSource(this.controller, this.context); + LocateDataSource(this.controller, this.context) + : super(controller.results, onSelectionChanged: (index, isSelected) {}); @override List get rows => @@ -182,7 +96,7 @@ class LocateDataSource extends DataGridSource { }).toList(); @override - DataGridRowAdapter? buildRow(DataGridRow row) { + DataGridRowAdapter buildRow(DataGridRow row) { final cells = row.getCells(); final result = cells[2].value as SearchResult; @@ -357,11 +271,13 @@ class LocateDataSource extends DataGridSource { } } -class CountDataSource extends DataGridSource { +class CountDataSource extends MySfDataSource { final ContentSearchController controller; + final BuildContext context; late final List> _counts; - CountDataSource(this.controller) { + CountDataSource(this.controller, this.context) + : super([], onSelectionChanged: (index, isSelected) {}) { final counts = {}; for (var result in controller.results) { counts[result.filePath] = (counts[result.filePath] ?? 0) + result.lineNumber; @@ -383,7 +299,7 @@ class CountDataSource extends DataGridSource { }).toList(); @override - DataGridRowAdapter? buildRow(DataGridRow row) { + DataGridRowAdapter buildRow(DataGridRow row) { return DataGridRowAdapter( cells: row.getCells().map((cell) { @@ -397,7 +313,6 @@ class CountDataSource extends DataGridSource { } } -// 自定义Action标识类 class _ConfirmAction extends Intent { const _ConfirmAction(); } diff --git a/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart b/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart index 3e4a938..36f31e6 100644 --- a/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart +++ b/win_text_editor/lib/modules/template_parser/widgets/grid_view.dart @@ -214,54 +214,4 @@ class _TemplateItemDataSource extends DataGridSource { }).toList(), ); } - - // 关键1: 启用单元格编辑 - @override - Future 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(); - }, - ), - ); - } } diff --git a/win_text_editor/lib/modules/uft_component/controllers/component_source.dart b/win_text_editor/lib/modules/uft_component/controllers/component_source.dart index 2e6a9f2..a5016e2 100644 --- a/win_text_editor/lib/modules/uft_component/controllers/component_source.dart +++ b/win_text_editor/lib/modules/uft_component/controllers/component_source.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:win_text_editor/modules/uft_component/models/uft_component.dart'; -import 'package:win_text_editor/shared/base/selectable_data_source.dart'; +import 'package:win_text_editor/shared/base/my_sf_data_source.dart'; -class ComponentSource extends SelectableDataSource { +class ComponentSource extends MySfDataSource { ComponentSource( List uftComponents, { required Null Function(dynamic index, dynamic isSelected) onSelectionChanged, diff --git a/win_text_editor/lib/shared/base/my_sf_data_grid.dart b/win_text_editor/lib/shared/base/my_sf_data_grid.dart index ad31054..3e703c9 100644 --- a/win_text_editor/lib/shared/base/my_sf_data_grid.dart +++ b/win_text_editor/lib/shared/base/my_sf_data_grid.dart @@ -2,18 +2,23 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:win_text_editor/shared/base/selectable_data_source.dart'; import 'package:win_text_editor/shared/base/selectable_item.dart'; -import 'package:win_text_editor/shared/components/my_sf_data_source.dart'; +import 'package:win_text_editor/shared/base/my_sf_data_source.dart'; +// ignore: must_be_immutable class MySfDataGrid extends StatelessWidget { - final MySfDataSource dataSource; + final MySfDataSource source; final Function(int index, bool isSelected)? onSelectionChanged; final List columns; + final DataGridController? controller; + bool selectable = false; - const MySfDataGrid({ + MySfDataGrid({ super.key, - required this.dataSource, + required this.source, required this.columns, this.onSelectionChanged, + this.controller, + this.selectable = false, }); Widget _buildCheckboxHeader(BuildContext context, SelectableDataSource dataSource) { @@ -52,32 +57,30 @@ class MySfDataGrid extends StatelessWidget { child: SfDataGrid( rowHeight: 32, headerRowHeight: 32, - source: dataSource, + source: source, gridLinesVisibility: GridLinesVisibility.both, headerGridLinesVisibility: GridLinesVisibility.both, columnWidthMode: ColumnWidthMode.fitByCellValue, selectionMode: SelectionMode.none, - allowEditing: true, + controller: controller, columns: [ - GridColumn( - columnName: 'select', - label: ValueListenableBuilder( - valueListenable: dataSource.selectionNotifier, - builder: (context, _, __) => _buildCheckboxHeader(context, dataSource), + if (selectable) + GridColumn( + columnName: 'select', + label: ValueListenableBuilder( + valueListenable: source.selectionNotifier, + builder: (context, _, __) => _buildCheckboxHeader(context, source), + ), + width: 60, ), - width: 60, - ), ...columns, ], onCellTap: (details) { if (details.column.columnName == 'select') { final rowIndex = details.rowColumnIndex.rowIndex - 1; - if (rowIndex >= 0 && rowIndex < dataSource.items.length) { - dataSource.toggleRowSelection( - rowIndex, - !dataSource.items[rowIndex].isSelected, - ); - onSelectionChanged?.call(rowIndex, dataSource.items[rowIndex].isSelected); + if (rowIndex >= 0 && rowIndex < source.items.length) { + source.toggleRowSelection(rowIndex, !source.items[rowIndex].isSelected); + onSelectionChanged?.call(rowIndex, source.items[rowIndex].isSelected); } } }, @@ -98,33 +101,43 @@ class MySfDataGrid extends StatelessWidget { position.dy + renderBox.size.height - position.dy, ), items: [ + const PopupMenuItem(value: 'export', child: Text('导出表格(csv)')), if (!isHeader) - PopupMenuItem(value: 'copyCell', child: Text('复制单元格 ($columnName)')), + PopupMenuItem( + value: 'copyCell', + child: Text('复制单元格 ($columnName)'), + ), if (!isHeader) - const PopupMenuItem(value: 'copyRow', child: Text('复制当前行')), - PopupMenuItem(value: 'copyColumn', child: Text('复制整列 ($columnName)')), + const PopupMenuItem(value: 'copyRow', child: Text('复制当前行')), + PopupMenuItem( + value: 'copyColumn', + child: Text('复制整列 ($columnName)'), + ), ], ).then((value) async { if (value != null) { switch (value) { case 'copyCell': - final row = dataSource.effectiveRows[rowIndex]; - await dataSource.copyCellValue(row, columnName); + final row = source.effectiveRows[rowIndex]; + await source.copyCellValue(row, columnName); break; case 'copyRow': - final row = dataSource.effectiveRows[rowIndex]; - await dataSource.copyRowValues(row); + final row = source.effectiveRows[rowIndex]; + await source.copyRowValues(row); break; case 'copyColumn': - await dataSource.copyColumnValues( - dataSource.effectiveRows, - columnName, - ); + await source.copyColumnValues(source.effectiveRows, columnName); break; + case 'export': + await source.exportToCsv(source.effectiveRows); + break; + } + if (value != 'export') { + ScaffoldMessenger.of( + // ignore: use_build_context_synchronously + context, + ).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); } - ScaffoldMessenger.of( - context, - ).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); } }); }, diff --git a/win_text_editor/lib/shared/components/my_sf_data_source.dart b/win_text_editor/lib/shared/base/my_sf_data_source.dart similarity index 58% rename from win_text_editor/lib/shared/components/my_sf_data_source.dart rename to win_text_editor/lib/shared/base/my_sf_data_source.dart index 2d9e599..56900d6 100644 --- a/win_text_editor/lib/shared/components/my_sf_data_source.dart +++ b/win_text_editor/lib/shared/base/my_sf_data_source.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; import 'package:flutter/services.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:win_text_editor/shared/base/selectable_data_source.dart'; @@ -40,4 +43,38 @@ abstract class MySfDataSource extends SelectableDataSo .join('\n'); await Clipboard.setData(ClipboardData(text: values)); } + + Future exportToCsv(List rows) async { + if (rows.length <= 1) return; + // 构建表头(不包含序号列) + String csvData = ""; + for (int i = 1; i < rows[0].getCells().length; i++) { + csvData += rows[0].getCells()[i].columnName.toString(); + if (i < rows[0].getCells().length - 1) csvData += '\t'; + } + + csvData += '\n'; + + // 填充数据(跳过第一列的序号) + for (final row in rows) { + // 从第1列开始(跳过第0列的序号) + for (int i = 1; i < row.getCells().length; i++) { + csvData += row.getCells()[i].value.toString(); + if (i < row.getCells().length - 1) csvData += '\t'; + } + csvData += '\n'; + } + + // 保存文件 + final filePath = await FilePicker.platform.saveFile( + dialogTitle: '保存导出结果', + fileName: 'template_results.csv', + type: FileType.custom, + allowedExtensions: ['csv'], + ); + + if (filePath != null) { + await File(filePath).writeAsString(csvData); + } + } } diff --git a/win_text_editor/lib/shared/uft_std_fields/field_data_source.dart b/win_text_editor/lib/shared/uft_std_fields/field_data_source.dart index 6eacfa4..b7f9471 100644 --- a/win_text_editor/lib/shared/uft_std_fields/field_data_source.dart +++ b/win_text_editor/lib/shared/uft_std_fields/field_data_source.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_datagrid/datagrid.dart'; -import 'package:win_text_editor/shared/components/my_sf_data_source.dart'; +import 'package:win_text_editor/shared/base/my_sf_data_source.dart'; import 'package:win_text_editor/shared/models/std_filed.dart'; class FieldsDataSource extends MySfDataSource { diff --git a/win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart b/win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart index 9efaecf..8906f2e 100644 --- a/win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart +++ b/win_text_editor/lib/shared/uft_std_fields/fields_data_grid.dart @@ -13,8 +13,9 @@ class FieldsDataGrid extends StatelessWidget { @override Widget build(BuildContext context) { return MySfDataGrid( - dataSource: fieldsSource, + source: fieldsSource, onSelectionChanged: onSelectionChanged, + selectable: true, columns: [ MyGridColumn(columnName: 'id', label: '序号', minimumWidth: 80), MyGridColumn(columnName: 'name', label: '名称', minimumWidth: 120),