diff --git a/win_text_editor/lib/app/modules/content_search/content_search_service.dart b/win_text_editor/lib/app/modules/content_search/content_search_service.dart index cfef201..b85b0ac 100644 --- a/win_text_editor/lib/app/modules/content_search/content_search_service.dart +++ b/win_text_editor/lib/app/modules/content_search/content_search_service.dart @@ -171,7 +171,7 @@ class ContentSearchService { try { final lines = await file.readAsLines(); for (int i = 0; i < lines.length; i++) { - final line = lines[i]; + final line = lines[i].trim(); final matches = pattern.allMatches(line); if (matches.isNotEmpty) { diff --git a/win_text_editor/lib/app/modules/content_search/results_view.dart b/win_text_editor/lib/app/modules/content_search/results_view.dart index 2045898..9b76e61 100644 --- a/win_text_editor/lib/app/modules/content_search/results_view.dart +++ b/win_text_editor/lib/app/modules/content_search/results_view.dart @@ -4,6 +4,8 @@ import 'package:syncfusion_flutter_datagrid/datagrid.dart'; import 'package:path/path.dart' as path; import 'package:win_text_editor/app/modules/content_search/content_search_controller.dart'; import 'package:win_text_editor/app/modules/content_search/content_search_service.dart'; +import 'package:file_picker/file_picker.dart'; +import 'dart:io'; class ResultsView extends StatelessWidget { const ResultsView({super.key}); @@ -20,7 +22,77 @@ class ResultsView extends StatelessWidget { controller.clearResults(); } - return Card(child: _buildResultsGrid(controller)); + return Card( + child: GestureDetector( + onSecondaryTapDown: (details) { + _showContextMenu(context, details.globalPosition, controller); + }, + child: _buildResultsGrid(controller), + ), + ); + } + + 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 = '文件(行号),内容\n'; + for (var result in controller.results) { + csvData += + '${path.basename(result.filePath)}(${result.lineNumber}),${result.lineContent.replaceAll(',', ',')}\n'; + } + } else { + csvData = '关键词,匹配数量\n'; + for (var result in controller.results) { + csvData += '${result.filePath},${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) {