diff --git a/documents/~$ UFT模块迁移方案.docx b/documents/~$ UFT模块迁移方案.docx new file mode 100644 index 0000000..1feb1b5 Binary files /dev/null and b/documents/~$ UFT模块迁移方案.docx differ diff --git a/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart b/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart index 96c66c2..34f3ff5 100644 --- a/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart +++ b/win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.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'; @@ -20,8 +21,10 @@ class ContentSearchController extends BaseContentController { bool _customRule = false; SearchMode _searchMode = SearchMode.locate; final List _results = []; - List _allResults = []; + final List _allResults = []; bool _includeZeroCounts = false; + bool _isSearching = false; + bool _shouldStop = false; // Getters String get searchQuery => _searchQuery; @@ -35,6 +38,24 @@ class ContentSearchController extends BaseContentController { List get results => _results; bool get includeZeroCounts => _includeZeroCounts; + double _progress = 0; + double get progress => _progress; + + bool get isSearching => _isSearching; + + // 添加进度更新方法 + void _updateProgress(double value) { + _progress = value.clamp(0.0, 100.0); + notifyListeners(); + } + + // 添加停止搜索方法 + void stopSearch() { + _shouldStop = true; + _isSearching = false; + notifyListeners(); + } + set customRule(bool value) { _customRule = value; notifyListeners(); @@ -111,8 +132,12 @@ class ContentSearchController extends BaseContentController { } Future startSearch() async { + _shouldStop = false; + _isSearching = true; + _progress = 0; _results.clear(); _allResults.clear(); + notifyListeners(); // 校验搜索内容 if (searchQuery.isEmpty) { @@ -127,23 +152,25 @@ class ContentSearchController extends BaseContentController { return; } - if (customRule) { - final validationResult = ContentSearchService.validateJsRule(searchQuery); - if (validationResult.isError) { - Logger().error('JavaScript 语法错误: ${validationResult.rawResult.toString()}'); - return; - } + try { + if (customRule) { + final validationResult = ContentSearchService.validateJsRule(searchQuery); + if (validationResult.isError) { + Logger().error('JavaScript 语法错误: ${validationResult.rawResult.toString()}'); + return; + } - _allResults.addAll( - await ContentSearchService.performCustomSearch( - directory: searchDirectory, - fileType: fileType, - jsFunction: searchQuery, - searchMode: searchMode, - ), - ); - } else { - try { + _allResults.addAll( + await ContentSearchService.performCustomSearch( + directory: searchDirectory, + fileType: fileType, + jsFunction: searchQuery, + searchMode: searchMode, + onProgress: _updateProgress, + shouldStop: () => _shouldStop, + ), + ); + } else { if (searchMode == SearchMode.locate) { _allResults.addAll( await ContentSearchService.performLocateSearch( @@ -153,6 +180,8 @@ class ContentSearchController extends BaseContentController { caseSensitive: caseSensitive, wholeWord: wholeWord, useRegex: useRegex, + onProgress: _updateProgress, + shouldStop: () => _shouldStop, ), ); } else { @@ -163,6 +192,8 @@ class ContentSearchController extends BaseContentController { caseSensitive: caseSensitive, wholeWord: wholeWord, useRegex: useRegex, + onProgress: _updateProgress, + shouldStop: () => _shouldStop, ); counts.forEach((keyword, count) { @@ -177,12 +208,15 @@ class ContentSearchController extends BaseContentController { ); }); } - } catch (e) { - Logger().error("搜索出错: $e"); - } finally { - _filterResults(); - notifyListeners(); } + } catch (e) { + Logger().error("搜索出错: $e"); + } finally { + _isSearching = false; + _progress = 100; + _shouldStop = false; + _filterResults(); + notifyListeners(); } } diff --git a/win_text_editor/lib/modules/content_search/services/content_search_service.dart b/win_text_editor/lib/modules/content_search/services/content_search_service.dart index 8950ee5..86eb4ed 100644 --- a/win_text_editor/lib/modules/content_search/services/content_search_service.dart +++ b/win_text_editor/lib/modules/content_search/services/content_search_service.dart @@ -9,6 +9,8 @@ import 'package:win_text_editor/modules/content_search/models/match_result.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'; +typedef ProgressCallback = void Function(double progress); + class ContentSearchService { /// 执行定位搜索(返回所有匹配项) static Future> performLocateSearch({ @@ -18,29 +20,57 @@ class ContentSearchService { required bool caseSensitive, required bool wholeWord, required bool useRegex, + ProgressCallback? onProgress, + bool Function()? shouldStop, }) async { final results = []; final dir = Directory(directory); - final queries = _splitQuery(query); // 分割查询字符串 - int speed = 0; + final queries = _splitQuery(query); + + // 先统计文件总数 + int totalFiles = 0; + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return results; + if (entity is File && _matchesFileType(entity.path, fileType)) { + totalFiles++; + } + } + + onProgress?.call(1); + + int words = 0; + int processedFiles = 0; + int oldProgress = 1; for (final q in queries) { + if (shouldStop?.call() == true) return results; + final pattern = _buildSearchPattern( query: q, caseSensitive: caseSensitive, wholeWord: wholeWord, useRegex: useRegex, ); - Logger().info("搜索词 $q 开始[${++speed}/${queries.length}]"); - int fileCount = 0; + + Logger().info("搜索词 $q 开始[${++words}/${queries.length}]"); + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return results; + if (entity is File && _matchesFileType(entity.path, fileType)) { - fileCount++; - await _searchInFile(entity, pattern, results, q); // 传递当前查询项 + processedFiles++; + final progress = (processedFiles / (totalFiles * queries.length)) * 99 + 1; + if (progress - oldProgress >= 1) { + oldProgress = progress.floor(); + onProgress?.call(progress); + } + + await _searchInFile(entity, pattern, results, q); } } - Logger().info("搜索词 $q 结束[$speed/${queries.length}],搜索文件 $fileCount 个,"); + Logger().info("搜索词 $q 结束[$words/${queries.length}],搜索文件 $totalFiles 个,"); } + onProgress?.call(100); return results; } @@ -52,12 +82,29 @@ class ContentSearchService { required bool caseSensitive, required bool wholeWord, required bool useRegex, + ProgressCallback? onProgress, + bool Function()? shouldStop, }) async { final counts = {}; final dir = Directory(directory); final queries = _splitQuery(query); // 分割查询字符串 - int speed = 0; + + int totalFiles = 0; + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return counts; + if (entity is File && _matchesFileType(entity.path, fileType)) { + totalFiles++; + } + } + + onProgress?.call(1); + + int words = 0; + int processedFiles = 0; + int oldProgress = 1; for (final q in queries) { + if (shouldStop?.call() == true) return counts; + final pattern = _buildSearchPattern( query: q, caseSensitive: caseSensitive, @@ -65,17 +112,25 @@ class ContentSearchService { useRegex: useRegex, ); - Logger().info("搜索词 $q 开始[${++speed}/${queries.length}]"); + Logger().info("搜索词 $q 开始[${++words}/${queries.length}]"); counts[q] = 0; - int fileCount = 0; + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return counts; + if (entity is File && _matchesFileType(entity.path, fileType)) { - fileCount++; + processedFiles++; + final progress = (processedFiles / (totalFiles * queries.length)) * 99 + 1; + if (progress - oldProgress >= 1) { + oldProgress = progress.floor(); + onProgress?.call(progress); + } + await _countInFile(entity, pattern, counts, q); // 传递当前查询项 } } - Logger().info("搜索词 $q 结束[$speed/${queries.length}],搜索文件 $fileCount 个,"); + Logger().info("搜索词 $q 结束[$words/${queries.length}],搜索文件 $totalFiles 个,"); } return counts; @@ -87,6 +142,8 @@ class ContentSearchService { required String fileType, required String jsFunction, required SearchMode searchMode, + ProgressCallback? onProgress, + bool Function()? shouldStop, }) async { final results = []; int count = 0; @@ -99,11 +156,26 @@ class ContentSearchService { jsRuntime.evaluate(jsCode); + int totalFiles = 0; await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return results; + if (entity is File && _matchesFileType(entity.path, fileType)) { + totalFiles++; + } + } + + onProgress?.call(1); + + int processedFiles = 0; + int calledProcessedFiles = 1; + + await for (final entity in dir.list(recursive: true)) { + if (shouldStop?.call() == true) return results; if (entity is File && _matchesFileType(entity.path, fileType)) { try { final lines = await entity.readAsLines(); for (int i = 0; i < lines.length; i++) { + if (shouldStop?.call() == true) return results; final line = lines[i].trim(); if (line.length < 3) continue; // 跳过短行 @@ -131,6 +203,13 @@ class ContentSearchService { } catch (e) { Logger().error('Error in file ${entity.path}: $e'); } + + processedFiles++; + final progress = (processedFiles / totalFiles) * 99 + 1; + if (processedFiles - calledProcessedFiles >= 1) { + calledProcessedFiles = processedFiles; + onProgress?.call(progress); + } } } @@ -147,6 +226,7 @@ class ContentSearchService { ); } } finally { + onProgress?.call(100); jsRuntime.dispose(); } diff --git a/win_text_editor/lib/modules/content_search/widgets/content_search_view.dart b/win_text_editor/lib/modules/content_search/widgets/content_search_view.dart index b02a52e..16b9971 100644 --- a/win_text_editor/lib/modules/content_search/widgets/content_search_view.dart +++ b/win_text_editor/lib/modules/content_search/widgets/content_search_view.dart @@ -34,13 +34,27 @@ class ContentSearchViewState extends State { Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _controller, - child: const Padding( - padding: EdgeInsets.all(4.0), + child: Padding( + padding: const EdgeInsets.all(4.0), child: Column( children: [ - DirectorySettings(), // 不再手动传递controller - SearchSettings(), - Expanded(child: ResultsView()), + const DirectorySettings(), // 不再手动传递controller + const SearchSettings(), + Consumer( + builder: (context, controller, _) { + return Column( + children: [ + LinearProgressIndicator( + value: controller.progress / 100, + backgroundColor: Colors.grey[200], + valueColor: const AlwaysStoppedAnimation(Colors.green), + minHeight: 8, + ), + ], + ); + }, + ), + const Expanded(child: ResultsView()), ], ), ), diff --git a/win_text_editor/lib/modules/content_search/widgets/search_settings.dart b/win_text_editor/lib/modules/content_search/widgets/search_settings.dart index a619aa2..b8a1b07 100644 --- a/win_text_editor/lib/modules/content_search/widgets/search_settings.dart +++ b/win_text_editor/lib/modules/content_search/widgets/search_settings.dart @@ -20,7 +20,7 @@ class SearchSettings extends StatelessWidget { // 搜索内容框 (保持原样) SizedBox( width: MediaQuery.of(context).size.width * 0.5, - height: 300, + height: 360, child: TextEditor( tabId: 'search_content_${controller.hashCode}', title: '搜索内容[列表以半角逗号分隔]', @@ -34,7 +34,7 @@ class SearchSettings extends StatelessWidget { // 设置按钮区域 Expanded( child: Container( - height: 300, + height: 360, decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(4), @@ -212,15 +212,31 @@ class SearchSettings extends StatelessWidget { ), ), // 开始搜索按钮 - Align( - alignment: Alignment.bottomCenter, - child: ElevatedButton.icon( - icon: const Icon(Icons.search, size: 20), - label: const Text('开始搜索'), - onPressed: () { - controller.startSearch(); - }, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.search, size: 20), + label: const Text('开始搜索'), + onPressed: + controller.isSearching ? null : () => controller.startSearch(), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.stop, size: 20), + label: const Text('停止'), + onPressed: + controller.isSearching ? () => controller.stopSearch() : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + ), + ), + ], ), ], ),