From a918c9021aaa19f376d77f9b9104e9a956177578 Mon Sep 17 00:00:00 2001 From: hejl Date: Mon, 16 Jun 2025 16:56:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=80=E5=A7=8B=E5=A4=A7=E6=90=9E=E4=B9=8B?= =?UTF-8?q?=E5=89=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assets/config/words_classes.yaml | 3 +- .../lib/framework/models/file_node.dart | 3 + .../data_format/models/template_node.dart | 3 + .../outline/controllers/outline_provider.dart | 38 +-- .../modules/outline/models/outline_node.dart | 10 + .../outline/services/outline_service.dart | 304 ++++++++++++++---- .../outline/widgets/outline_explorer.dart | 3 +- .../lib/shared/components/tree_view.dart | 3 +- .../lib/shared/models/template_node.dart | 3 + 9 files changed, 280 insertions(+), 90 deletions(-) diff --git a/win_text_editor/assets/config/words_classes.yaml b/win_text_editor/assets/config/words_classes.yaml index 177785f..f69fc18 100644 --- a/win_text_editor/assets/config/words_classes.yaml +++ b/win_text_editor/assets/config/words_classes.yaml @@ -1,3 +1,2 @@ outline_name_black_list: - - 的,了,和,是,在 - - 历史,日志 \ No newline at end of file + - 历史,日志,名称,比例,数量,金额,次数,属性,对应,分类,姓名,单位,总数,行使,子项,占比,记录,列表,目标,字段,字符串,动作 \ No newline at end of file diff --git a/win_text_editor/lib/framework/models/file_node.dart b/win_text_editor/lib/framework/models/file_node.dart index 9aa50df..642e90e 100644 --- a/win_text_editor/lib/framework/models/file_node.dart +++ b/win_text_editor/lib/framework/models/file_node.dart @@ -16,6 +16,9 @@ class FileNode implements TreeNode { @override bool isExpanded; + @override + String get title => name; + FileNode({ required this.name, required this.path, diff --git a/win_text_editor/lib/modules/data_format/models/template_node.dart b/win_text_editor/lib/modules/data_format/models/template_node.dart index b10d52d..9d61697 100644 --- a/win_text_editor/lib/modules/data_format/models/template_node.dart +++ b/win_text_editor/lib/modules/data_format/models/template_node.dart @@ -17,6 +17,9 @@ class TemplateNode implements TreeNode { int repreatCount; bool isChecked; // 新增属性,用于记录节点是否被选中 + @override + String get title => name; + TemplateNode({ required this.name, required this.children, 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 32b5321..94ffb2b 100644 --- a/win_text_editor/lib/modules/outline/controllers/outline_provider.dart +++ b/win_text_editor/lib/modules/outline/controllers/outline_provider.dart @@ -6,7 +6,6 @@ import 'package:win_text_editor/modules/outline/services/outline_service.dart'; class OutlineProvider with ChangeNotifier { List _outlineNode = []; bool _isLoading = true; - String _searchQuery = ''; String? _currentRootPath; // 跟踪当前根路径 bool get isLoading => _isLoading; @@ -23,22 +22,7 @@ class OutlineProvider with ChangeNotifier { await _loadRootDirectory(); } - List get outlineNode => - _searchQuery.isEmpty - ? _outlineNode - : _outlineNode.where((node) => _filterNode(node)).toList(); - - bool _filterNode(OutlineNode node) { - if (node.name.toLowerCase().contains(_searchQuery.toLowerCase())) { - return true; - } - return node.children.any(_filterNode); - } - - void searchOutlines(String query) { - _searchQuery = query; - notifyListeners(); - } + List get outlineNode => _outlineNode; void toggleExpand(OutlineNode node) { node.isExpanded = !node.isExpanded; @@ -71,17 +55,7 @@ class OutlineProvider with ChangeNotifier { // 获取分词结果节点 final wordNodes = await OutlineService.getWordNodes(_currentRootPath!); Logger().info('获取到 ${wordNodes.length} 个分词结果'); - // 创建根节点"所有" - final rootNode = OutlineNode( - name: '所有', - frequency: 0, - isDirectory: true, - isRoot: true, - isExpanded: true, - depth: 1, - children: wordNodes, - ); - _outlineNode = [rootNode]; + _outlineNode = wordNodes; } } catch (e) { Logger().error('加载根目录时出错: $e'); @@ -108,8 +82,8 @@ class OutlineProvider with ChangeNotifier { } Future loadDirectoryContents(OutlineNode dirNode) async { - if (dirNode.children.isNotEmpty && dirNode.isExpanded) { - // 如果已经加载过且是展开状态,只切换展开状态 + if (dirNode.children.isNotEmpty) { + // 如果已经加载过,只切换展开状态 dirNode.isExpanded = !dirNode.isExpanded; notifyListeners(); return; @@ -118,6 +92,10 @@ class OutlineProvider with ChangeNotifier { _isLoading = true; notifyListeners(); + if (_currentRootPath != null) { + await OutlineService.loadChildren(_currentRootPath!, dirNode); + dirNode.isExpanded = !dirNode.isExpanded; + } _isLoading = false; notifyListeners(); } 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 716d27a..589512d 100644 --- a/win_text_editor/lib/modules/outline/models/outline_node.dart +++ b/win_text_editor/lib/modules/outline/models/outline_node.dart @@ -5,6 +5,8 @@ import 'package:win_text_editor/shared/components/tree_view.dart'; class OutlineNode implements TreeNode { @override final String name; + + String title; @override final bool isDirectory; final bool isRoot; @@ -16,14 +18,20 @@ class OutlineNode implements TreeNode { bool isExpanded; final int frequency; + final String value; + String? wordClass; + OutlineNode({ required this.name, + required this.value, required this.frequency, required this.isDirectory, this.isRoot = false, this.depth = 0, this.isExpanded = false, + this.wordClass, List? children, + this.title = "", }) : children = children ?? []; @override @@ -46,6 +54,7 @@ class OutlineNode implements TreeNode { OutlineNode copyWith({ String? name, + String? value, // value is not nullable, so we keep it as is int? frequency, bool? isDirectory, bool? isExpanded, @@ -55,6 +64,7 @@ class OutlineNode implements TreeNode { }) { return OutlineNode( name: name ?? this.name, + value: value ?? this.value, // value is not nullable, so we keep it as is frequency: frequency ?? this.frequency, isDirectory: isDirectory ?? this.isDirectory, isExpanded: isExpanded ?? this.isExpanded, 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 1438688..cb746a6 100644 --- a/win_text_editor/lib/modules/outline/services/outline_service.dart +++ b/win_text_editor/lib/modules/outline/services/outline_service.dart @@ -2,41 +2,71 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:jieba_flutter/analysis/jieba_segmenter.dart'; -import 'package:jieba_flutter/analysis/seg_token.dart'; import 'package:win_text_editor/modules/outline/models/outline_node.dart'; import 'package:xml/xml.dart'; import 'package:yaml/yaml.dart'; import 'package:win_text_editor/framework/controllers/logger.dart'; class OutlineService { + // 静态常量 + // ignore: constant_identifier_names + static const List REQUIRED_FILES = [ + 'metadata/stdfield.stdfield', + 'metadata/stdobj.xml', + 'metadata/component.xml', + ]; + + // 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 bool _isJiebaInitialized = false; // 标记分词器是否初始化 - static JiebaSegmenter? _segmenter; // 分词器实例 + static bool _isJiebaInitialized = false; + static JiebaSegmenter? _segmenter; + static Map _wordClassDict = {}; + static bool _isWordClassDictInitialized = false; - // 初始化分词器 + // 私有方法:初始化分词器 static Future _initJieba() async { if (!_isJiebaInitialized) { - await JiebaSegmenter.init(); // 使用正确的初始化方法 - _segmenter = JiebaSegmenter(); // 创建分词器实例 - _isJiebaInitialized = true; + try { + await JiebaSegmenter.init(); + _segmenter = JiebaSegmenter(); + _isJiebaInitialized = true; + } catch (e) { + Logger().error('初始化分词器失败: $e'); + throw Exception('分词器初始化失败'); + } } } - // 初始化黑名单 - // 初始化黑名单 - 修正版本 + // 私有方法:初始化黑名单 static Future _initBlackList() async { + if (_blackList.isNotEmpty) return; + try { final yamlString = await rootBundle.loadString('assets/config/words_classes.yaml'); final yamlMap = loadYaml(yamlString); - - // 安全获取黑名单列表 final blackListItems = (yamlMap['outline_name_black_list'] as List?)?.cast() ?? []; - // 处理逗号分隔的黑名单项 _blackList = - blackListItems.expand((item) { - return item.split(',').map((word) => word.trim()).where((word) => word.isNotEmpty); - }).toList(); + blackListItems + .expand( + (item) => + item.split(',').map((word) => word.trim()).where((word) => word.isNotEmpty), + ) + .toList(); Logger().info('加载黑名单成功: ${_blackList.length}个词'); } catch (e) { @@ -45,14 +75,34 @@ class OutlineService { } } - // 解析stdfield文件获取中文名称 - static Future> _parseChineseNames(String filePath) async { + // 私有方法:初始化词性字典(优化版) + static Future _initWordClassDict() async { + if (_isWordClassDictInitialized) return; + try { - final file = File(filePath); - final content = await file.readAsString(); - final document = XmlDocument.parse(content); + Logger().info('开始加载词性字典...'); + final dictContent = await rootBundle.loadString('assets/dict.txt'); + + _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>(), + ); - return document + _isWordClassDictInitialized = true; + Logger().info('加载词性字典成功: ${_wordClassDict.length}个词'); + } catch (e) { + Logger().error('加载词性字典失败: $e'); + _wordClassDict = {}; + } + } + + // 私有方法:解析stdfield文件 + static Future> _parseChineseNames(String filePath) async { + try { + final content = await File(filePath).readAsString(); + return XmlDocument.parse(content) .findAllElements('items') .map((e) => e.getAttribute('chineseName') ?? '') .where((name) => name.isNotEmpty) @@ -63,13 +113,15 @@ class OutlineService { } } - // 分词并统计词频 + // 私有方法:分词并统计词频 static Future> _analyzeWords(List chineseNames) async { - await _initJieba(); // 确保分词器已初始化 + await _initJieba(); final wordFrequency = {}; + Logger().info('开始分词,共有 ${chineseNames.length} 个中文名称'); + for (final name in chineseNames) { - List tokens = _segmenter!.process(name, SegMode.SEARCH); // 使用分词器实例 + final tokens = _segmenter!.process(name, SegMode.SEARCH); for (final token in tokens) { final word = token.word.trim(); if (word.length > 1 && !_blackList.contains(word)) { @@ -77,26 +129,170 @@ class OutlineService { } } } + Logger().info('分词完成,共找到 ${wordFrequency.length} 个有效词语'); return wordFrequency; } - // 获取分词结果 + // 公开方法:加载子节点 + 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}"); + } + } catch (e) { + Logger().error('加载子节点失败: $e'); + rethrow; + } + } + + // 私有方法:加载字段操作 + static Future _loadFieldActions(String rootPath, OutlineNode dirNode) async { + switch (dirNode.name) { + case 'UFTTable': + await _loadUftObject(rootPath, dirNode.value, dirNode); + break; + case 'Component': + case 'Business': + // 其他操作类型的处理 + break; + default: + Logger().error("操作节点类型不支持: ${dirNode.value}"); + } + } + + // 私有方法:加载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.split('/').last.replaceFirst('.uftstructure', ''); + + // 创建并添加子节点 + parentNode.children.add( + OutlineNode( + name: fileName, + title: chineseName, + value: 'UFTTable', + frequency: 0, + isDirectory: false, // 这些是叶子节点 + depth: parentNode.depth + 1, + ), + ); + } + } 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 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: parentNode.depth + 1, + ), + ), + ); + } + + // 公开方法:获取分词结果节点 static Future> getWordNodes(String rootPath) async { - await _initBlackList(); + await Future.wait([_initBlackList(), _initWordClassDict()]); final stdfieldPath = '$rootPath/metadata/stdfield.stdfield'; if (!await File(stdfieldPath).exists()) { - Logger().error('stdfield文件不存在'); - return []; + throw Exception('stdfield文件不存在'); } final chineseNames = await _parseChineseNames(stdfieldPath); if (chineseNames.isEmpty) { - Logger().error('未找到有效的中文名称'); - return []; + throw Exception('未找到有效的中文名称'); } - Logger().info('找到 ${chineseNames.length} 个标准字段'); final wordFrequency = await _analyzeWords(chineseNames); final sortedWords = wordFrequency.entries.toList()..sort((a, b) => b.value.compareTo(a.value)); @@ -104,42 +300,40 @@ class OutlineService { return sortedWords .map( (entry) => OutlineNode( - name: '${entry.key}(${entry.value})', + name: entry.key, + value: '', + title: '${entry.key}(${entry.value})', frequency: entry.value, isDirectory: true, - depth: 2, + depth: 1, + wordClass: _wordClassDict[entry.key], ), ) + .where((node) => !FILTERED_WORD_CLASSES.contains(node.wordClass)) .toList(); } - // 检查目录结构是否完整 + // 公开方法:验证目录结构 static Future validateDirectoryStructure(String rootPath) async { try { - // 检查必须的文件 - final requiredFiles = [ - 'metadata/stdfield.stdfield', - 'metadata/stdobj.xml', - 'metadata/component.xml', - ]; - - for (var filePath in requiredFiles) { - final file = File('$rootPath/$filePath'); - if (!await file.exists()) { - Logger().error('缺少必要文件: $filePath'); - return false; - } + // 检查文件 + final fileChecks = await Future.wait( + REQUIRED_FILES.map((path) => File('$rootPath/$path').exists()), + ); + + if (fileChecks.any((exists) => !exists)) { + Logger().error('缺少必要文件'); + return false; } - // 检查必须的目录 - final requiredDirs = ['uftstructure', 'uftatom', 'uftbusiness', 'uftfactor']; + // 检查目录 + final dirChecks = await Future.wait( + REQUIRED_DIRS.map((path) => Directory('$rootPath/$path').exists()), + ); - for (var dirPath in requiredDirs) { - final dir = Directory('$rootPath/$dirPath'); - if (!await dir.exists()) { - Logger().error('缺少必要目录: $dirPath'); - return false; - } + if (dirChecks.any((exists) => !exists)) { + Logger().error('缺少必要目录'); + return false; } return true; 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 878b35a..9a7f4c0 100644 --- a/win_text_editor/lib/modules/outline/widgets/outline_explorer.dart +++ b/win_text_editor/lib/modules/outline/widgets/outline_explorer.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/framework/services/file_path_manager.dart'; import 'package:win_text_editor/modules/outline/controllers/outline_provider.dart'; import 'package:win_text_editor/modules/outline/models/outline_node.dart'; @@ -59,7 +58,7 @@ class _OutlineExplorerState extends State { for (final node in nodesToFilter) { // 如果是第一层子节点(假设根节点深度为0,第一层子节点深度为1) - if (currentDepth == 1 && node.name.toLowerCase().contains(_searchQuery.toLowerCase())) { + if (node.title.toLowerCase().contains(_searchQuery.toLowerCase())) { result.add(node); } diff --git a/win_text_editor/lib/shared/components/tree_view.dart b/win_text_editor/lib/shared/components/tree_view.dart index 71acf38..ef9c67e 100644 --- a/win_text_editor/lib/shared/components/tree_view.dart +++ b/win_text_editor/lib/shared/components/tree_view.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; abstract class TreeNode { String get id; String get name; + String get title; bool get isExpanded; bool get isDirectory; List get children; @@ -203,7 +204,7 @@ class TreeNodeWidget extends StatelessWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 2), minVerticalPadding: 0, leading: _buildLeadingWidget(context), - title: Text(node.name, style: Theme.of(context).textTheme.bodyMedium), + title: Text(node.title, style: Theme.of(context).textTheme.bodyMedium), trailing: config.showCheckboxes ? Transform.scale( diff --git a/win_text_editor/lib/shared/models/template_node.dart b/win_text_editor/lib/shared/models/template_node.dart index 58c5ce4..7221508 100644 --- a/win_text_editor/lib/shared/models/template_node.dart +++ b/win_text_editor/lib/shared/models/template_node.dart @@ -18,6 +18,9 @@ class TemplateNode implements TreeNode { bool isChecked; final bool isTextNode; + @override + String get title => name; + TemplateNode({ required this.name, required this.children,