diff --git a/win_text_editor/lib/modules/outline/controllers/outline_provider.dart b/win_text_editor/lib/modules/outline/controllers/outline_provider.dart index a054777..9f3db49 100644 --- a/win_text_editor/lib/modules/outline/controllers/outline_provider.dart +++ b/win_text_editor/lib/modules/outline/controllers/outline_provider.dart @@ -8,11 +8,21 @@ class OutlineProvider with ChangeNotifier { bool _isLoading = true; String? _currentRootPath; // 跟踪当前根路径 static OutlineNode rootNode = OutlineNode( - name: "所有Tag", - title: "所有Tag", - value: 'UFTTable', - frequency: 0, + name: "所有", + title: "所有", + value: 'All', isDirectory: true, + isRoot: true, + isExpanded: true, + ); + + static OutlineNode searchNode = OutlineNode( + name: "搜索结果", + title: "搜索结果", + value: 'Query', + isDirectory: true, + isRoot: true, + isExpanded: true, ); bool get isLoading => _isLoading; @@ -29,25 +39,7 @@ class OutlineProvider with ChangeNotifier { return; } _currentRootPath = path; - await _loadRootDirectory(); - } - - List get outlineNode { - rootNode.children = _outlineNode; - return [rootNode]; - } - - void toggleExpand(OutlineNode node) { - node.isExpanded = !node.isExpanded; - notifyListeners(); - } - - Future loadDirectory(String path) async { - _isLoading = true; - notifyListeners(); - - _isLoading = false; - notifyListeners(); + _loadRootDirectory(); } // outline_provider.dart 中的 _loadRootDirectory 方法 @@ -79,35 +71,16 @@ class OutlineProvider with ChangeNotifier { } } - Future toggleDirectory(OutlineNode dirNode) async { - if (dirNode.children.isEmpty) { - // 首次点击:加载内容 - _isLoading = true; - notifyListeners(); - - _isLoading = false; - notifyListeners(); - } else { - // 已加载过:只切换展开状态 - dirNode.isExpanded = !dirNode.isExpanded; - notifyListeners(); - } - } - Future loadDirectoryContents(OutlineNode dirNode) async { if (dirNode.children.isNotEmpty) { - // 如果已经加载过,只切换展开状态 - dirNode.isExpanded = !dirNode.isExpanded; - notifyListeners(); return; } - _isLoading = true; notifyListeners(); if (_currentRootPath != null) { await OutlineService.loadChildren(_currentRootPath!, dirNode); - dirNode.isExpanded = !dirNode.isExpanded; + dirNode.isExpanded = true; } _isLoading = false; notifyListeners(); @@ -116,8 +89,19 @@ class OutlineProvider with ChangeNotifier { Future refreshOutlineTree({bool loadContent = false}) async { _isLoading = true; notifyListeners(); - + Logger().info('正在刷新大纲树...'); _isLoading = false; notifyListeners(); } + + // 根据搜索查询过滤节点 + List applyFilter(String searchQuery) { + if (searchQuery.isEmpty) { + if (rootNode.children.isEmpty) rootNode.children = _outlineNode; + return [rootNode]; + } + + OutlineService.applySearchFilter(searchNode, searchQuery); + return [searchNode]; + } } diff --git a/win_text_editor/lib/modules/outline/models/outline_node.dart b/win_text_editor/lib/modules/outline/models/outline_node.dart index 4f1c03a..375b1b1 100644 --- a/win_text_editor/lib/modules/outline/models/outline_node.dart +++ b/win_text_editor/lib/modules/outline/models/outline_node.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:win_text_editor/shared/components/tree_view.dart'; @@ -6,14 +5,13 @@ class OutlineNode implements TreeNode { @override final String name; + @override String title = ""; List _children = []; - OutlineNode? parent; - @override - final bool isDirectory; + bool isDirectory; final bool isRoot; @override final int depth; @@ -23,10 +21,7 @@ class OutlineNode implements TreeNode { bool isExpanded; final int frequency; - late String uuid; - - @override - bool isVisible; + late String id; final String value; String? wordClass; @@ -34,24 +29,30 @@ class OutlineNode implements TreeNode { OutlineNode({ required this.name, required this.value, - required this.frequency, - required this.isDirectory, + this.isDirectory = false, + this.id = "", this.isRoot = false, this.depth = 0, + this.frequency = 0, this.isExpanded = false, this.wordClass, List? children, this.title = "", - this.isVisible = true, }) : _children = children ?? [] { - uuid = DateTime.now().microsecondsSinceEpoch.toRadixString(36); - for (var child in _children) { - child.parent = this; - } + id = DateTime.now().microsecondsSinceEpoch.toRadixString(36); } - @override - String get id => uuid; + OutlineNode copyWith({bool? isExpanded, List? children}) { + return OutlineNode( + id: id, + name: name, + value: value, + title: title, + isExpanded: isExpanded ?? this.isExpanded, + isDirectory: isDirectory, + children: children ?? this.children, + ); + } // 获取文件图标数据 @override @@ -65,9 +66,6 @@ class OutlineNode implements TreeNode { set children(List nodes) { _children = nodes; - for (var child in _children) { - child.parent = this; - } } // 获取构建好的图标组件 @@ -78,22 +76,11 @@ class OutlineNode implements TreeNode { @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is OutlineNode && - other.name == name && - other.frequency == frequency && - other.isDirectory == isDirectory && - other.isRoot == isRoot && - listEquals(other.children, children) && - other.isExpanded == isExpanded; + return other is OutlineNode && other.id == id; } @override int get hashCode { - return name.hashCode ^ - frequency.hashCode ^ - isDirectory.hashCode ^ - isRoot.hashCode ^ - children.hashCode ^ - isExpanded.hashCode; + return id.hashCode; } } diff --git a/win_text_editor/lib/modules/outline/services/component_service.dart b/win_text_editor/lib/modules/outline/services/component_service.dart new file mode 100644 index 0000000..8ef8f8f --- /dev/null +++ b/win_text_editor/lib/modules/outline/services/component_service.dart @@ -0,0 +1,80 @@ +import 'dart:io'; + +import 'package:win_text_editor/framework/controllers/logger.dart'; +import 'package:win_text_editor/modules/outline/models/outline_node.dart'; +import 'package:xml/xml.dart'; + +class ComponentService { + //组件索引 + static Map> componentFieldMap = {}; + + //初始化组件索引 + static Future initComponentFieldMap(String rootPath) async { + final componentFile = File('$rootPath/metadata/component.xml'); + if (!await componentFile.exists()) { + Logger().error('component.xml文件不存在'); + return; + } + + componentFieldMap.clear(); + + try { + final content = await componentFile.readAsString(); + final document = XmlDocument.parse(content); + + // 查找所有items节点 + for (final item in document.findAllElements('items')) { + final name = item.getAttribute('name'); + final chineseName = item.getAttribute('chineseName'); + if (name != null && chineseName != null) { + componentFieldMap[name] = [chineseName]; + + componentFieldMap[name]?.addAll( + item + .findElements('items') + .map((item) => item.getAttribute('name')) + .where((name) => name != null) + .cast(), + ); + } + } + } catch (e) { + Logger().error('加载Component失败: $e'); + } + } + + //组合匹配 + static Future> matchComponent(String fieldName) async { + Map matchComponents = {}; + + componentFieldMap.forEach((key, value) { + if (value.contains(fieldName)) matchComponents[key] = value[0]; + }); + + return matchComponents; + } + + static Future loadComponent(String? fieldName, OutlineNode parentNode) async { + if (fieldName == null || fieldName.isEmpty) return; + + final matches = await matchComponent(fieldName); + matches.forEach( + (key, value) => parentNode.children.add( + OutlineNode(name: key, title: value, value: 'Component', isDirectory: false, depth: 4), + ), + ); + } + + static List searchComponents(String searchQuery) { + final List componentNodes = []; + componentFieldMap.forEach((key, value) { + if (key.contains(searchQuery) || value[0].contains(searchQuery)) { + componentNodes.add( + OutlineNode(name: key, title: value[0], value: 'Component', isDirectory: false, depth: 4), + ); + } + }); + + return componentNodes; + } +} diff --git a/win_text_editor/lib/modules/outline/services/functions_service.dart b/win_text_editor/lib/modules/outline/services/functions_service.dart new file mode 100644 index 0000000..211a299 --- /dev/null +++ b/win_text_editor/lib/modules/outline/services/functions_service.dart @@ -0,0 +1,157 @@ +import 'dart:io'; + +import 'package:win_text_editor/framework/controllers/logger.dart'; +import 'package:win_text_editor/modules/outline/models/outline_node.dart'; +import 'package:win_text_editor/modules/outline/services/component_service.dart'; +import 'package:xml/xml.dart'; + +class FunctionsService { + //服务层索引 + static Map> uftbusinessMap = {}; + + //原子层索引 + static Map> uftatomMap = {}; + + static Future initFunctionsMap(String rootPath) async { + await _initUftMap('uftbusiness', rootPath, uftbusinessMap, ['.uffunction']); + + await _initUftMap('uftatom', rootPath, uftatomMap, ['.uftatomfunction', '.uftatomservice']); + } + + static Future _initUftMap( + String dirName, + String rootPath, + Map> targetMap, + List fileExtensions, + ) async { + final dir = Directory('$rootPath\\$dirName'); + if (!await dir.exists()) { + Logger().error('$dirName目录不存在'); + return; + } + + targetMap.clear(); + + try { + final files = + await dir + .list(recursive: true) + .where((entity) => fileExtensions.any((ext) => entity.path.endsWith(ext))) + .cast() + .toList(); + + await Future.wait(files.map((file) => _processUftFile(file, targetMap))); + } catch (e) { + Logger().error('加载$dirName对象失败: $e'); + rethrow; + } + } + + static Future _processUftFile(File file, Map> targetMap) async { + try { + final content = await file.readAsString(); + final document = XmlDocument.parse(content); + final businessNode = document.findAllElements('business:Function').firstOrNull; + if (businessNode == null) return; + + final chineseName = businessNode.getAttribute('chineseName') ?? '未命名'; + targetMap[chineseName] = [file.path]; + + final parameterNodes = [ + ...businessNode.findElements('inputParameters'), + ...businessNode.findElements('outputParameters'), + ...businessNode.findElements('internalParams'), + ]; + + for (final item in parameterNodes) { + final id = item.getAttribute('id'); + if (id == null) continue; + + if (item.getAttribute('paramType') == null || + item.getAttribute('paramType') != 'COMPONENT') { + targetMap[chineseName]?.add(id); + } else { + targetMap[chineseName]?.addAll(ComponentService.componentFieldMap[id]?.sublist(1) ?? []); + } + } + } catch (e) { + Logger().error('解析文件 ${file.path} 失败: $e'); + } + } + + //加载原子逻辑层 + static Future loadAtom(String? fieldName, OutlineNode parentNode) async { + if (fieldName == null || fieldName.isEmpty) return; + + uftatomMap.forEach((key, value) { + if (value.contains(fieldName)) { + parentNode.children.add( + OutlineNode( + name: value[0], // 文件名 + title: key, + value: 'Atom', + isDirectory: false, // 这些是叶子节点 + depth: 4, + ), + ); + } + }); + } + + //加载业务逻辑层 + static Future loadBusiness(String? fieldName, OutlineNode parentNode) async { + if (fieldName == null || fieldName.isEmpty) return; + + uftbusinessMap.forEach((key, value) { + if (value.contains(fieldName)) { + parentNode.children.add( + OutlineNode( + name: value[0], // 文件名 + title: key, + value: 'Atom', + isDirectory: false, // 这些是叶子节点 + depth: 4, + ), + ); + } + }); + } + + static List searchBusiness(String searchQuery) { + final List nodes = []; + uftbusinessMap.forEach((key, value) { + if (key.contains(searchQuery)) { + nodes.add( + OutlineNode( + name: value[0], // 文件名 + title: key, + value: 'Atom', + isDirectory: false, // 这些是叶子节点 + depth: 4, + ), + ); + } + }); + + return nodes; + } + + static List searchAtoms(String searchQuery) { + final List nodes = []; + uftatomMap.forEach((key, value) { + if (key.contains(searchQuery)) { + nodes.add( + OutlineNode( + name: value[0], // 文件名 + title: key, + value: 'Atom', + isDirectory: false, // 这些是叶子节点 + depth: 4, + ), + ); + } + }); + + return nodes; + } +} diff --git a/win_text_editor/lib/modules/outline/services/outline_service.dart b/win_text_editor/lib/modules/outline/services/outline_service.dart index a427bef..0cc7140 100644 --- a/win_text_editor/lib/modules/outline/services/outline_service.dart +++ b/win_text_editor/lib/modules/outline/services/outline_service.dart @@ -3,6 +3,10 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:jieba_flutter/analysis/jieba_segmenter.dart'; import 'package:win_text_editor/modules/outline/models/outline_node.dart'; +import 'package:win_text_editor/modules/outline/services/component_service.dart'; +import 'package:win_text_editor/modules/outline/services/functions_service.dart'; +import 'package:win_text_editor/modules/outline/services/std_field_service.dart'; +import 'package:win_text_editor/modules/outline/services/uft_object_service.dart'; import 'package:xml/xml.dart'; import 'package:yaml/yaml.dart'; import 'package:win_text_editor/framework/controllers/logger.dart'; @@ -19,23 +23,16 @@ class OutlineService { // ignore: constant_identifier_names static const List REQUIRED_DIRS = ['uftstructure', 'uftatom', 'uftbusiness', 'uftfactor']; - // ignore: constant_identifier_names - static const Map FIELD_ACTIONS = { - "UFTTable": "UFT对象", - "Component": "标准组件", - "Business": "业务层", - "Atom": "原子层", - }; - // ignore: constant_identifier_names static const List FILTERED_WORD_CLASSES = ['v', 'a', 'ad', 'f', 'd', 't', 'r']; - // 静态变量 + //黑名单 static List _blackList = []; + + static OutlineNode? _searchNode; + //结巴初始化标记 static bool _isJiebaInitialized = false; static JiebaSegmenter? _segmenter; - static Map _wordClassDict = {}; - static bool _isWordClassDictInitialized = false; // 私有方法:初始化分词器 static Future _initJieba() async { @@ -76,26 +73,22 @@ class OutlineService { } // 私有方法:初始化词性字典(优化版) - static Future _initWordClassDict() async { - if (_isWordClassDictInitialized) return; - + static Future> _initWordClassDict() async { + Map wordClassDict = {}; try { Logger().info('开始加载词性字典...'); final dictContent = await rootBundle.loadString('assets/dict.txt'); - _wordClassDict = Map.fromEntries( + wordClassDict = Map.fromEntries( dictContent.split('\n').where((line) => line.trim().isNotEmpty).map((line) { final parts = line.trim().split(RegExp(r'\s+')); return (parts.length >= 3) ? MapEntry(parts[0], parts[2]) : null; }).whereType>(), ); - - _isWordClassDictInitialized = true; - Logger().info('加载词性字典成功: ${_wordClassDict.length}个词'); } catch (e) { Logger().error('加载词性字典失败: $e'); - _wordClassDict = {}; } + return wordClassDict; } // 私有方法:解析stdfield文件 @@ -137,15 +130,10 @@ class OutlineService { // 公开方法:加载子节点 static Future loadChildren(String rootPath, OutlineNode dirNode) async { try { - switch (dirNode.depth) { - case 1: // 关键词(字段分类) - await _loadStdfields(rootPath, dirNode); - break; - case 3: // 标准字段的操作 - await _loadFieldActions(rootPath, dirNode); - break; - default: - Logger().error("节点层次不支持: ${dirNode.depth}"); + if (dirNode.depth == 1) { + await StdFieldService.loadStdfields(rootPath, dirNode); + } else { + Logger().error("节点层次不支持: ${dirNode.depth}"); } } catch (e) { Logger().error('加载子节点失败: $e'); @@ -153,410 +141,22 @@ class OutlineService { } } - // 私有方法:加载字段操作 - static Future _loadFieldActions(String rootPath, OutlineNode dirNode) async { - switch (dirNode.name) { - case 'UFTTable': - await _loadUftObject(rootPath, dirNode.value, dirNode); - break; - case 'Component': - await _loadComponent(rootPath, dirNode.value, dirNode); - break; - case 'Business': - await _loadBusiness(rootPath, dirNode.value, dirNode); - break; - case 'Atom': - await _loadAtom(rootPath, dirNode.value, dirNode); - break; - default: - Logger().error("操作节点类型不支持: ${dirNode.value}"); - } - } - - static Future _loadComponent( - String rootPath, - String? fieldName, - OutlineNode parentNode, - ) async { - if (fieldName == null || fieldName.isEmpty) return; - - final matches = await _matchComponent(rootPath, fieldName); - matches.forEach( - (key, value) => parentNode.children.add( - OutlineNode( - name: key, - title: value, - value: 'Component', - frequency: 0, - isDirectory: false, - depth: 4, - ), - ), - ); - } - - //组合匹配 - static Future> _matchComponent(String rootPath, String fieldName) async { - Map matchComponents = {}; - final componentFile = File('$rootPath/metadata/component.xml'); - if (!await componentFile.exists()) { - Logger().error('component.xml文件不存在'); - return matchComponents; - } - - try { - final content = await componentFile.readAsString(); - final document = XmlDocument.parse(content); - - // 查找所有items节点 - for (final item in document.findAllElements('items')) { - // 检查name属性是否匹配fieldName - if (item.getAttribute('name') == fieldName) { - // 获取父节点 - final parentElement = item.parent; - if (parentElement != null) { - // 获取父节点的name和chineseName属性 - final parentName = parentElement.getAttribute('name'); - final parentChineseName = parentElement.getAttribute('chineseName'); - - if (parentName != null && parentChineseName != null) { - matchComponents[parentName] = parentChineseName; - } - } - } - } - } catch (e) { - Logger().error('加载Component失败: $e'); - } - return matchComponents; - } - - //加载原子逻辑层 - static Future _loadAtom(String rootPath, String? fieldName, OutlineNode parentNode) async { - if (fieldName == null || fieldName.isEmpty) return; - - final uftatomDir = Directory('$rootPath\\uftatom'); - if (!await uftatomDir.exists()) { - Logger().error('uftatom目录不存在'); - return; - } - - try { - //获取对应组合 - final matchComponents = await _matchComponent(rootPath, fieldName); - - // 遍历所有.uftstructure文件 - final uftatomFiles = - await uftatomDir - .list(recursive: true) - .where( - (entity) => - entity.path.endsWith('.uftatomfunction') || - entity.path.endsWith('.uftatomservice'), - ) - .cast() - .toList(); - - for (final file in uftatomFiles) { - try { - final content = await file.readAsString(); - final document = XmlDocument.parse(content); - String matchType = "I"; - - // 查找匹配的入参 - final List matchingProperties = - document - .findAllElements('inputParameters') - .where( - (element) => - element.getAttribute('id') == fieldName || - element.getAttribute("paramType") == "COMPONENT" && - matchComponents.containsKey(element.getAttribute('id')), - ) - .toList(); - - //匹配出参 - if (matchingProperties.isEmpty) { - matchingProperties.addAll( - document - .findAllElements('outputParameters') - .where( - (element) => - element.getAttribute('id') == fieldName || - element.getAttribute("paramType") == "COMPONENT" && - matchComponents.containsKey(element.getAttribute('id')), - ), - ); - matchType = "O"; - } - - //匹配内部变量 - if (matchingProperties.isEmpty) { - matchingProperties.addAll( - document - .findAllElements('internalParams') - .where( - (element) => - element.getAttribute('id') == fieldName || - element.getAttribute("paramType") == "COMPONENT" && - matchComponents.containsKey(element.getAttribute('id')), - ), - ); - matchType = "X"; - } - - //匹配组合 - - if (matchingProperties.isNotEmpty) { - // 获取structure:Structure节点的chineseName - final businessNode = document.findAllElements('business:Function').firstOrNull; - final chineseName = businessNode?.getAttribute('chineseName') ?? '未命名'; - - // 获取文件名(不带路径) - final fileName = file.path; - - // 创建并添加子节点 - parentNode.children.add( - OutlineNode( - name: fileName, - title: '$chineseName($matchType)', - value: 'Atom', - frequency: 0, - isDirectory: false, // 这些是叶子节点 - depth: 4, - ), - ); - } - } catch (e) { - Logger().error('解析文件 ${file.path} 失败: $e'); - } - } - - Logger().info('为 $fieldName 找到 ${parentNode.children.length} 个匹配项'); - } catch (e) { - Logger().error('加载UFT对象失败: $e'); - rethrow; - } - } - - //加载业务逻辑层 - static Future _loadBusiness( - String rootPath, - String? fieldName, - OutlineNode parentNode, - ) async { - if (fieldName == null || fieldName.isEmpty) return; - - final uftbusinessDir = Directory('$rootPath\\uftbusiness'); - if (!await uftbusinessDir.exists()) { - Logger().error('uftbusiness目录不存在'); - return; - } - - try { - final matchComponents = await _matchComponent(rootPath, fieldName); - - // 遍历所有.uftstructure文件 - final uftbusinessFiles = - await uftbusinessDir - .list(recursive: true) - .where((entity) => entity.path.endsWith('.uftfunction')) - .cast() - .toList(); - - for (final file in uftbusinessFiles) { - try { - final content = await file.readAsString(); - final document = XmlDocument.parse(content); - String matchType = "I"; - - // 查找匹配的入参 - final List matchingProperties = - document - .findAllElements('inputParameters') - .where( - (element) => - element.getAttribute('id') == fieldName || - element.getAttribute("paramType") == "COMPONENT" && - matchComponents.containsKey(element.getAttribute('id')), - ) - .toList(); - - if (matchingProperties.isEmpty) { - matchingProperties.addAll( - document - .findAllElements('outputParameters') - .where( - (element) => - element.getAttribute('id') == fieldName || - element.getAttribute("paramType") == "COMPONENT" && - matchComponents.containsKey(element.getAttribute('id')), - ), - ); - matchType = "O"; - } - - if (matchingProperties.isEmpty) { - matchingProperties.addAll( - document - .findAllElements('internalParams') - .where( - (element) => - element.getAttribute('id') == fieldName || - element.getAttribute("paramType") == "COMPONENT" && - matchComponents.containsKey(element.getAttribute('id')), - ), - ); - matchType = "X"; - } - - if (matchingProperties.isNotEmpty) { - // 获取structure:Structure节点的chineseName - final businessNode = document.findAllElements('business:Function').firstOrNull; - final chineseName = businessNode?.getAttribute('chineseName') ?? '未命名'; - - // 获取文件名(不带路径) - final fileName = file.path; - - // 创建并添加子节点 - parentNode.children.add( - OutlineNode( - name: fileName, - title: '$chineseName($matchType)', - value: 'Business', - frequency: 0, - isDirectory: false, // 这些是叶子节点 - depth: 4, - ), - ); - } - } catch (e) { - Logger().error('解析文件 ${file.path} 失败: $e'); - } - } - - Logger().info('为 $fieldName 找到 ${parentNode.children.length} 个匹配项'); - } catch (e) { - Logger().error('加载UFT对象失败: $e'); - rethrow; - } - } - - // 私有方法:加载UFT对象 - static Future _loadUftObject( - String rootPath, - String? fieldName, - OutlineNode parentNode, - ) async { - if (fieldName == null || fieldName.isEmpty) return; - - final uftStructureDir = Directory('$rootPath/uftstructure'); - if (!await uftStructureDir.exists()) { - Logger().error('uftstructure目录不存在'); - return; - } - - try { - // 遍历所有.uftstructure文件 - final uftStructureFiles = - await uftStructureDir - .list(recursive: true) - .where((entity) => entity.path.endsWith('.uftstructure')) - .cast() - .toList(); - - for (final file in uftStructureFiles) { - try { - final content = await file.readAsString(); - final document = XmlDocument.parse(content); - - // 查找匹配的properties节点 - final matchingProperties = document - .findAllElements('properties') - .where((element) => element.getAttribute('id') == fieldName); - - if (matchingProperties.isNotEmpty) { - // 获取structure:Structure节点的chineseName - final structureNode = document.findAllElements('structure:Structure').firstOrNull; - final chineseName = structureNode?.getAttribute('chineseName') ?? '未命名'; - - // 获取文件名(不带路径) - final fileName = file.path; - - // 创建并添加子节点 - parentNode.children.add( - OutlineNode( - name: fileName, - title: chineseName, - value: 'UFTTable', - frequency: 0, - isDirectory: false, // 这些是叶子节点 - depth: 4, - ), - ); - } - } catch (e) { - Logger().error('解析文件 ${file.path} 失败: $e'); - } - } - - Logger().info('为 $fieldName 找到 ${parentNode.children.length} 个匹配项'); - } catch (e) { - Logger().error('加载UFT对象失败: $e'); - rethrow; - } - } - - // 私有方法:加载标准字段 - static Future _loadStdfields(String rootPath, OutlineNode dirNode) async { - final stdfieldFile = File('$rootPath/metadata/stdfield.stdfield'); - if (!await stdfieldFile.exists()) { - throw Exception('stdfield文件不存在'); - } - - final content = await stdfieldFile.readAsString(); - final document = XmlDocument.parse(content); - - dirNode.children.clear(); - - for (final item in document.findAllElements('items')) { - final chineseName = item.getAttribute('chineseName'); - final name = item.getAttribute('name'); - - if (chineseName != null && name != null && chineseName.contains(dirNode.name)) { - final fieldNode = OutlineNode( - name: chineseName, - title: '$chineseName($name)', - value: name, - frequency: 0, - isDirectory: true, - depth: 2, - ); - _createActionNodes(fieldNode); - dirNode.children.add(fieldNode); - } + // 公开方法:获取分词结果节点 + static Future> getWordNodes(String rootPath) async { + if (rootPath.isEmpty) { + throw Exception('根路径不能为空'); } - } - // 私有方法:创建操作节点 - static void _createActionNodes(OutlineNode parentNode) { - parentNode.children.addAll( - FIELD_ACTIONS.entries.map( - (entry) => OutlineNode( - name: entry.key, - value: parentNode.value, - title: entry.value, - frequency: 0, - isDirectory: true, - depth: 3, - ), - ), - ); - } + final wordClassDict = await _initWordClassDict(); - // 公开方法:获取分词结果节点 - static Future> getWordNodes(String rootPath) async { - await Future.wait([_initBlackList(), _initWordClassDict()]); + //初始化数据 + await Future.wait([ + _initBlackList(), + ComponentService.initComponentFieldMap(rootPath), + StdFieldService.initStdFieldMap(rootPath), + UftObjectService.initUftObjectMap(rootPath), + FunctionsService.initFunctionsMap(rootPath), + ]); final stdfieldPath = '$rootPath/metadata/stdfield.stdfield'; if (!await File(stdfieldPath).exists()) { @@ -580,8 +180,7 @@ class OutlineService { frequency: entry.value, isDirectory: true, depth: 1, - isRoot: true, - wordClass: _wordClassDict[entry.key], + wordClass: wordClassDict[entry.key], ), ) .where((node) => !FILTERED_WORD_CLASSES.contains(node.wordClass)) @@ -617,4 +216,108 @@ class OutlineService { return false; } } + + static String _oldSearchQuery = ""; + + static void applySearchFilter(OutlineNode root, String searchQuery) { + if (_searchNode != null && _oldSearchQuery == searchQuery) { + return; + } + + _oldSearchQuery = searchQuery; + + //创建搜索词第一层节点 + _searchNode = OutlineNode( + name: searchQuery, + title: searchQuery, + value: 'Search', + isDirectory: true, + isExpanded: true, + depth: 1, + ); + + //搜索标准字段 + final List stdFieldNodes = StdFieldService.searchStdFields(searchQuery); + _searchNode?.children.addAll(stdFieldNodes); + + final virtualNode = OutlineNode( + name: "virtualNode", + title: "【非字段匹配】", + value: 'virtualNode', + isDirectory: true, + depth: 2, + isExpanded: true, + ); + + //搜索UFT对象 + final List uftObjectNodes = UftObjectService.searchUftObjects(searchQuery); + if (uftObjectNodes.isNotEmpty) { + final uftObjectActionNode = OutlineNode( + name: 'UFTTable', + title: 'UFT对象(${uftObjectNodes.length})', + value: 'virtualNode', + isDirectory: true, + depth: 3, + ); + uftObjectActionNode.children.addAll(uftObjectNodes); + virtualNode.children.add(uftObjectActionNode); + } + + //搜索组件 + final List componentNodes = ComponentService.searchComponents(searchQuery); + if (componentNodes.isNotEmpty) { + final componentActionNode = OutlineNode( + name: 'Component', + title: '标准组件(${componentNodes.length})', + value: 'virtualNode', + isDirectory: true, + depth: 3, + ); + componentActionNode.children.addAll(componentNodes); + virtualNode.children.add(componentActionNode); + } + + //搜索函数 + final List businessNodes = FunctionsService.searchBusiness(searchQuery); + if (businessNodes.isNotEmpty) { + final businessActionNode = OutlineNode( + name: 'Business', + title: "业务层(${businessNodes.length})", + value: 'virtualNode', + isDirectory: true, + depth: 3, + ); + businessActionNode.children.addAll(businessNodes); + virtualNode.children.add(businessActionNode); + } + + //搜索函数 + final List atomNodes = FunctionsService.searchAtoms(searchQuery); + if (atomNodes.isNotEmpty) { + final atomActionNode = OutlineNode( + name: 'Atom', + title: '原子层(${atomNodes.length})', + value: 'virtualNode', + isDirectory: true, + depth: 3, + ); + atomActionNode.children.addAll(atomNodes); + virtualNode.children.add(atomActionNode); + } + + if (virtualNode.children.isNotEmpty) { + _searchNode?.children.add(virtualNode); + } + + //如果没有子节点,返回空列表 + if (_searchNode!.children.isNotEmpty) { + root.title = "搜索结果"; + root.children.clear(); + root.isExpanded = false; + root.children.add(_searchNode!); + root.isExpanded = true; + } else { + root.title = "无结果"; + } + } } diff --git a/win_text_editor/lib/modules/outline/services/std_field_service.dart b/win_text_editor/lib/modules/outline/services/std_field_service.dart new file mode 100644 index 0000000..f2a8f44 --- /dev/null +++ b/win_text_editor/lib/modules/outline/services/std_field_service.dart @@ -0,0 +1,135 @@ +import 'dart:io'; + +import 'package:win_text_editor/framework/controllers/logger.dart'; +import 'package:win_text_editor/modules/outline/models/outline_node.dart'; +import 'package:win_text_editor/modules/outline/services/component_service.dart'; +import 'package:win_text_editor/modules/outline/services/functions_service.dart'; +import 'package:win_text_editor/modules/outline/services/uft_object_service.dart'; +import 'package:xml/xml.dart'; + +class StdFieldService { + // ignore: constant_identifier_names + static const Map FIELD_ACTIONS = { + "UFTTable": "UFT对象", + "Component": "标准组件", + "Business": "业务层", + "Atom": "原子层", + }; + //字段索引 + static Map stdfieldMap = {}; + + //初始化标准字段 + static Future initStdFieldMap(String rootPath) async { + final stdfieldFile = File('$rootPath/metadata/stdfield.stdfield'); + if (!await stdfieldFile.exists()) { + throw Exception('stdfield文件不存在'); + } + + stdfieldMap.clear(); + + final content = await stdfieldFile.readAsString(); + final document = XmlDocument.parse(content); + + for (final item in document.findAllElements('items')) { + final chineseName = item.getAttribute('chineseName'); + final name = item.getAttribute('name'); + + if (chineseName != null && name != null) { + stdfieldMap[chineseName] = name; + } + } + } + + // 加载字段操作 + static Future _loadFieldActions(OutlineNode dirNode) async { + switch (dirNode.name) { + case 'UFTTable': + await UftObjectService.loadUftObject(dirNode.value, dirNode); + break; + case 'Component': + await ComponentService.loadComponent(dirNode.value, dirNode); + break; + case 'Business': + await FunctionsService.loadBusiness(dirNode.value, dirNode); + break; + case 'Atom': + await FunctionsService.loadAtom(dirNode.value, dirNode); + break; + default: + Logger().error("操作节点类型不支持: ${dirNode.value}"); + } + } + + // 加载标准字段 + static Future loadStdfields(String rootPath, OutlineNode dirNode) async { + dirNode.children.clear(); + Logger().info('搜索标准字段:${dirNode.name}'); + + stdfieldMap.forEach((key, value) { + if (key.contains(dirNode.name)) { + final fieldNode = OutlineNode( + name: key, + title: '$key($value)', + value: value, + isDirectory: true, + depth: 2, + ); + _createActionNodes(fieldNode); + + fieldNode.isDirectory = fieldNode.children.isNotEmpty; + if (fieldNode.isDirectory) { + dirNode.children.insert(0, fieldNode); + } else { + dirNode.children.add(fieldNode); + } + } + }); + } + + // 私有方法:创建操作节点 + static void _createActionNodes(OutlineNode parentNode) { + for (var entry in FIELD_ACTIONS.entries) { + final actionNode = OutlineNode( + name: entry.key, + value: parentNode.value, + title: entry.value, + isDirectory: true, + depth: 3, + ); + + //加载子节点 + _loadFieldActions(actionNode); + + if (actionNode.children.isNotEmpty) { + actionNode.title = '${entry.value}(${actionNode.children.length})'; + parentNode.children.add(actionNode); + } + } + } + + static List searchStdFields(String searchQuery) { + final List children = []; + stdfieldMap.forEach((key, value) { + if (searchQuery.contains(key) || + searchQuery.contains(value) || + key.contains(searchQuery) || + value.contains(searchQuery)) { + final fieldNode = OutlineNode( + name: key, + title: '$key($value)', + value: value, + isDirectory: false, + depth: 2, + ); + _createActionNodes(fieldNode); + if (fieldNode.children.isNotEmpty) { + fieldNode.isDirectory = true; + children.insert(0, fieldNode); + } else { + children.add(fieldNode); + } + } + }); + return children; + } +} diff --git a/win_text_editor/lib/modules/outline/services/uft_object_service.dart b/win_text_editor/lib/modules/outline/services/uft_object_service.dart new file mode 100644 index 0000000..c4cc334 --- /dev/null +++ b/win_text_editor/lib/modules/outline/services/uft_object_service.dart @@ -0,0 +1,95 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:win_text_editor/framework/controllers/logger.dart'; +import 'package:win_text_editor/modules/outline/models/outline_node.dart'; +import 'package:xml/xml.dart'; + +class UftObjectService { + //内存表索引 + static Map> uftObjectMap = {}; + + //初始化内存表列表 + static Future initUftObjectMap(String rootPath) async { + final uftStructureDir = Directory('$rootPath/uftstructure'); + if (!await uftStructureDir.exists()) { + Logger().error('uftstructure目录不存在'); + return; + } + + uftObjectMap.clear(); + + try { + // 遍历所有.uftstructure文件 + final uftStructureFiles = + await uftStructureDir + .list(recursive: true) + .where((entity) => entity.path.endsWith('.uftstructure')) + .cast() + .toList(); + + for (final file in uftStructureFiles) { + try { + final content = await file.readAsString(); + final document = XmlDocument.parse(content); + + final fileName = file.path; + final structureNode = document.findAllElements('structure:Structure').firstOrNull; + + if (structureNode == null) continue; + + final chineseName = structureNode.getAttribute('chineseName') ?? '未命名'; + + uftObjectMap[chineseName] = [fileName]; + + uftObjectMap[chineseName]?.addAll( + structureNode + .findElements('properties') + .map((item) => item.getAttribute('id')) + .where((name) => name != null) + .cast(), + ); + } catch (e) { + Logger().error('解析文件 ${file.path} 失败: $e'); + } + } + } catch (e) { + Logger().error('加载UFT对象失败: $e'); + rethrow; + } + } + + // 私有方法:加载UFT对象 + static Future loadUftObject(String? fieldName, OutlineNode parentNode) async { + if (fieldName == null || fieldName.isEmpty) return; + + uftObjectMap.forEach((key, value) { + if (value.contains(fieldName)) { + parentNode.children.add( + OutlineNode( + name: value[0], + title: key, + value: 'UFTTable', + isDirectory: false, // 这些是叶子节点 + depth: 4, + ), + ); + } + }); + } + + static List searchUftObjects(String searchQuery) { + final List searchNodes = []; + + uftObjectMap.forEach((key, value) { + if (key.contains(searchQuery) || + path.basenameWithoutExtension(value[0]).contains(searchQuery)) { + searchNodes.add( + OutlineNode(name: value[0], title: key, value: 'UFTTable', isDirectory: false, depth: 4), + ); + } + }); + + return searchNodes; + } +} diff --git a/win_text_editor/lib/modules/outline/widgets/outline_explorer.dart b/win_text_editor/lib/modules/outline/widgets/outline_explorer.dart index 1532047..93ef6e5 100644 --- a/win_text_editor/lib/modules/outline/widgets/outline_explorer.dart +++ b/win_text_editor/lib/modules/outline/widgets/outline_explorer.dart @@ -1,4 +1,4 @@ -import 'dart:math'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -22,64 +22,19 @@ class _OutlineExplorerState extends State { final ScrollController _scrollController = ScrollController(); final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; + Timer? _searchDebounceTimer; @override void dispose() { _scrollController.dispose(); _searchController.dispose(); + _searchDebounceTimer?.cancel(); super.dispose(); } - // 动态计算总宽度(根据层级深度调整) - double calculateTotalWidth(BuildContext context, OutlineProvider outlineProvider) { - final maxDepth = _getMaxDepth(outlineProvider.outlineNode); - return maxDepth * 60 + MediaQuery.of(context).size.width * 0.2; - } - - int _getMaxDepth(List nodes) { - int maxDepth = 0; - for (final node in nodes) { - if (node.isDirectory && node.isExpanded) { - maxDepth = max(maxDepth, _getMaxDepth(node.children) + 1); - } - } - return maxDepth; - } - // 过滤节点方法 - 递归版本 - List _filterNodes(List nodes) { - void resetVisibility(List nodes) { - for (final node in nodes) { - node.isVisible = true; - if (node.isDirectory) { - resetVisibility(node.children); - } - } - } - - void applySearchFilter(List nodes, String query) { - for (final node in nodes) { - // 递归处理子节点 - if (node.isDirectory && node.depth < 2) { - applySearchFilter(node.children, query); - } - - // 设置可见性:自身匹配或子节点可见 - final bool isMatch = node.title.toLowerCase().contains(query.toLowerCase()); - final bool hasVisibleChild = - node.depth < 2 && node.isDirectory && node.children.any((child) => child.isVisible); - - node.isVisible = isMatch || hasVisibleChild; - } - } - - if (_searchQuery.isEmpty) { - resetVisibility(nodes); - return nodes; - } - - applySearchFilter(nodes, _searchQuery); - return nodes; + List _filterNodes(OutlineProvider outlineProvider) { + return outlineProvider.applyFilter(_searchQuery); } @override @@ -101,7 +56,7 @@ class _OutlineExplorerState extends State { controller: _searchController, style: const TextStyle(fontSize: 12), // 输入文字大小为10 decoration: InputDecoration( - hintText: '搜索Tag...', + hintText: '搜索...', hintStyle: const TextStyle(fontSize: 12), // 提示文字大小为10 prefixIcon: const Icon( Icons.search, @@ -134,10 +89,14 @@ class _OutlineExplorerState extends State { ), ), onChanged: (value) { - setState(() { - _searchQuery = value; + _searchDebounceTimer?.cancel(); + _searchDebounceTimer = Timer(const Duration(milliseconds: 500), () { + if (mounted) setState(() => _searchQuery = value); }); }, + onEditingComplete: () { + if (mounted) setState(() => _searchQuery = _searchController.text); + }, ), ), ), @@ -155,11 +114,19 @@ class _OutlineExplorerState extends State { child: SizedBox( width: 300, child: TreeView( - nodes: _filterNodes(outlineProvider.outlineNode), - config: const TreeViewConfig(showIcons: true, lazyLoad: true), + nodes: _filterNodes(outlineProvider), + config: const TreeViewConfig( + showIcons: true, + lazyLoad: true, + showRefreshButton: true, + ), onNodeTap: (node) => _handleNodeTap(context, node as OutlineNode), onNodeDoubleTap: (node) => _handleNodeDoubleTap(node as OutlineNode, fileProvider), + onRefresh: () async { + await outlineProvider.refreshOutlineTree(); + setState(() {}); + }, ), ), ), @@ -191,6 +158,9 @@ class _OutlineExplorerState extends State { Future _handleNodeTap(BuildContext context, OutlineNode node) async { final outlineProvider = Provider.of(context, listen: false); if (node.isDirectory) { + setState(() { + node.isExpanded = !node.isExpanded; // 触发更新 + }); await outlineProvider.loadDirectoryContents(node); } } @@ -226,14 +196,14 @@ class _OutlineExplorerState extends State { ) async { // 使用 fileProvider 的路径 if (fileProvider?.rootPath != null && fileProvider!.rootPath!.isNotEmpty) { - await outlineProvider.setRootPath(fileProvider.rootPath!); + outlineProvider.setRootPath(fileProvider.rootPath!); return; } // 最后尝试从存储中加载 final String? lastOpenedFolder = await FilePathManager.getLastOpenedFolder(); if (lastOpenedFolder != null) { - await outlineProvider.setRootPath(lastOpenedFolder); + outlineProvider.setRootPath(lastOpenedFolder); } } } diff --git a/win_text_editor/lib/shared/components/tree_view.dart b/win_text_editor/lib/shared/components/tree_view.dart index a6e9a81..7351eeb 100644 --- a/win_text_editor/lib/shared/components/tree_view.dart +++ b/win_text_editor/lib/shared/components/tree_view.dart @@ -10,7 +10,6 @@ abstract class TreeNode { List get children; int get depth; IconData? get iconData; - bool get isVisible; } /// 树视图配置 @@ -21,7 +20,9 @@ class TreeViewConfig { final bool showIcons; final Color? selectedColor; final double indentWidth; - final Map icons; // 改为final + final Map icons; + final bool showRefreshButton; + final IconData refreshIcon; const TreeViewConfig({ this.lazyLoad = false, @@ -30,7 +31,9 @@ class TreeViewConfig { this.showIcons = true, this.selectedColor, this.indentWidth = 24.0, - this.icons = const {}, // 提供空Map作为默认值 + this.icons = const {}, + this.showRefreshButton = false, + this.refreshIcon = Icons.refresh, }); } @@ -41,8 +44,9 @@ class TreeView extends StatefulWidget { final Function(TreeNode)? onNodeTap; final Function(TreeNode)? onNodeDoubleTap; final Function(TreeNode, bool?)? onNodeCheckChanged; - final Widget Function(BuildContext, TreeNode, bool, VoidCallback)? nodeBuilder; // 新增参数 + final Widget Function(BuildContext, TreeNode, bool, VoidCallback)? nodeBuilder; final ScrollController? scrollController; + final VoidCallback? onRefresh; const TreeView({ super.key, @@ -53,6 +57,7 @@ class TreeView extends StatefulWidget { this.onNodeCheckChanged, this.nodeBuilder, this.scrollController, + this.onRefresh, }); @override @@ -91,27 +96,48 @@ class _TreeViewState extends State { @override Widget build(BuildContext context) { - return ListView.builder( - controller: _effectiveController, - physics: const ClampingScrollPhysics(), - itemCount: _countVisibleNodes(widget.nodes), - itemBuilder: (context, index) { - final node = _getVisibleNode(widget.nodes, index); - final isSelected = _selectedIds.contains(node.id); + return Stack( + children: [ + ListView.builder( + controller: _effectiveController, + physics: const ClampingScrollPhysics(), + itemCount: _countVisibleNodes(widget.nodes), + itemBuilder: (context, index) { + final node = _getVisibleNode(widget.nodes, index); + final isSelected = _selectedIds.contains(node.id); + + return widget.nodeBuilder != null + ? widget.nodeBuilder!(context, node, isSelected, () => _handleNodeTap(node)) + : TreeNodeWidget( + key: ValueKey(node.id), + node: node, + config: widget.config, + isSelected: isSelected, + isChecked: _checkedIds.contains(node.id), + onTap: () => _handleNodeTap(node), + onDoubleTap: () => widget.onNodeDoubleTap?.call(node), + onCheckChanged: (value) => _handleNodeCheckChanged(node, value), + ); + }, + ), - // 使用自定义构建器或默认构建器 - return widget.nodeBuilder != null - ? widget.nodeBuilder!(context, node, isSelected, () => _handleNodeTap(node)) - : TreeNodeWidget( - node: node, - config: widget.config, - isSelected: isSelected, - isChecked: _checkedIds.contains(node.id), - onTap: () => _handleNodeTap(node), - onDoubleTap: () => widget.onNodeDoubleTap?.call(node), - onCheckChanged: (value) => _handleNodeCheckChanged(node, value), - ); - }, + // 刷新按钮 + if (widget.config.showRefreshButton && widget.onRefresh != null) + Positioned( + right: 8.0, + top: 8.0, + child: IconButton( + icon: Icon( + widget.config.refreshIcon, + size: 20.0, + color: Theme.of(context).primaryColor, + ), + onPressed: widget.onRefresh, + tooltip: '刷新', + splashRadius: 16.0, + ), + ), + ], ); } @@ -142,7 +168,6 @@ class _TreeViewState extends State { int _countVisibleNodes(List nodes) { int count = 0; for (final node in nodes) { - if (!node.isVisible) continue; // 跳过不可见节点 count++; if (node.isDirectory && node.isExpanded) { count += _countVisibleNodes(node.children); @@ -154,8 +179,6 @@ class _TreeViewState extends State { TreeNode _getVisibleNode(List nodes, int index) { int current = 0; for (final node in nodes) { - if (!node.isVisible) continue; // 跳过不可见节点 - if (current == index) return node; current++; @@ -181,6 +204,7 @@ class TreeNodeWidget extends StatelessWidget { final Function(bool?)? onCheckChanged; const TreeNodeWidget({ + super.key, required this.node, required this.config, required this.isSelected, @@ -192,8 +216,8 @@ class TreeNodeWidget extends StatelessWidget { @override Widget build(BuildContext context) { - if (!node.isVisible) return const SizedBox.shrink(); // 额外保护 return InkWell( + key: ValueKey(node.id), onTap: onTap, onDoubleTap: onDoubleTap, splashColor: Colors.transparent,