|
|
|
@ -2,41 +2,71 @@
@@ -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<String> REQUIRED_FILES = [ |
|
|
|
|
'metadata/stdfield.stdfield', |
|
|
|
|
'metadata/stdobj.xml', |
|
|
|
|
'metadata/component.xml', |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
// ignore: constant_identifier_names |
|
|
|
|
static const List<String> REQUIRED_DIRS = ['uftstructure', 'uftatom', 'uftbusiness', 'uftfactor']; |
|
|
|
|
|
|
|
|
|
// ignore: constant_identifier_names |
|
|
|
|
static const Map<String, String> FIELD_ACTIONS = { |
|
|
|
|
"UFTTable": "UFT对象", |
|
|
|
|
"Component": "标准组件", |
|
|
|
|
"Business": "业务层", |
|
|
|
|
"Atom": "原子层", |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// ignore: constant_identifier_names |
|
|
|
|
static const List<String> FILTERED_WORD_CLASSES = ['v', 'a', 'ad', 'f', 'd', 't', 'r']; |
|
|
|
|
|
|
|
|
|
// 静态变量 |
|
|
|
|
static List<String> _blackList = []; |
|
|
|
|
static bool _isJiebaInitialized = false; // 标记分词器是否初始化 |
|
|
|
|
static JiebaSegmenter? _segmenter; // 分词器实例 |
|
|
|
|
static bool _isJiebaInitialized = false; |
|
|
|
|
static JiebaSegmenter? _segmenter; |
|
|
|
|
static Map<String, String> _wordClassDict = {}; |
|
|
|
|
static bool _isWordClassDictInitialized = false; |
|
|
|
|
|
|
|
|
|
// 初始化分词器 |
|
|
|
|
// 私有方法:初始化分词器 |
|
|
|
|
static Future<void> _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<void> _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<String>() ?? []; |
|
|
|
|
|
|
|
|
|
// 处理逗号分隔的黑名单项 |
|
|
|
|
_blackList = |
|
|
|
|
blackListItems.expand<String>((item) { |
|
|
|
|
return item.split(',').map((word) => word.trim()).where((word) => word.isNotEmpty); |
|
|
|
|
}).toList(); |
|
|
|
|
blackListItems |
|
|
|
|
.expand<String>( |
|
|
|
|
(item) => |
|
|
|
|
item.split(',').map((word) => word.trim()).where((word) => word.isNotEmpty), |
|
|
|
|
) |
|
|
|
|
.toList(); |
|
|
|
|
|
|
|
|
|
Logger().info('加载黑名单成功: ${_blackList.length}个词'); |
|
|
|
|
} catch (e) { |
|
|
|
@ -45,14 +75,34 @@ class OutlineService {
@@ -45,14 +75,34 @@ class OutlineService {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 解析stdfield文件获取中文名称 |
|
|
|
|
static Future<List<String>> _parseChineseNames(String filePath) async { |
|
|
|
|
// 私有方法:初始化词性字典(优化版) |
|
|
|
|
static Future<void> _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<MapEntry<String, String>>(), |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
return document |
|
|
|
|
_isWordClassDictInitialized = true; |
|
|
|
|
Logger().info('加载词性字典成功: ${_wordClassDict.length}个词'); |
|
|
|
|
} catch (e) { |
|
|
|
|
Logger().error('加载词性字典失败: $e'); |
|
|
|
|
_wordClassDict = {}; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 私有方法:解析stdfield文件 |
|
|
|
|
static Future<List<String>> _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 {
@@ -63,13 +113,15 @@ class OutlineService {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 分词并统计词频 |
|
|
|
|
// 私有方法:分词并统计词频 |
|
|
|
|
static Future<Map<String, int>> _analyzeWords(List<String> chineseNames) async { |
|
|
|
|
await _initJieba(); // 确保分词器已初始化 |
|
|
|
|
await _initJieba(); |
|
|
|
|
final wordFrequency = <String, int>{}; |
|
|
|
|
|
|
|
|
|
Logger().info('开始分词,共有 ${chineseNames.length} 个中文名称'); |
|
|
|
|
|
|
|
|
|
for (final name in chineseNames) { |
|
|
|
|
List<SegToken> 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 {
@@ -77,26 +129,170 @@ class OutlineService {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Logger().info('分词完成,共找到 ${wordFrequency.length} 个有效词语'); |
|
|
|
|
return wordFrequency; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 获取分词结果 |
|
|
|
|
// 公开方法:加载子节点 |
|
|
|
|
static Future<void> 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<void> _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<void> _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<File>() |
|
|
|
|
.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<void> _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<List<OutlineNode>> 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 {
@@ -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<bool> 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; |
|
|
|
|