Browse Source

开始大搞之前

master
hejl 4 weeks ago
parent
commit
a918c9021a
  1. 3
      win_text_editor/assets/config/words_classes.yaml
  2. 3
      win_text_editor/lib/framework/models/file_node.dart
  3. 3
      win_text_editor/lib/modules/data_format/models/template_node.dart
  4. 38
      win_text_editor/lib/modules/outline/controllers/outline_provider.dart
  5. 10
      win_text_editor/lib/modules/outline/models/outline_node.dart
  6. 304
      win_text_editor/lib/modules/outline/services/outline_service.dart
  7. 3
      win_text_editor/lib/modules/outline/widgets/outline_explorer.dart
  8. 3
      win_text_editor/lib/shared/components/tree_view.dart
  9. 3
      win_text_editor/lib/shared/models/template_node.dart

3
win_text_editor/assets/config/words_classes.yaml

@ -1,3 +1,2 @@ @@ -1,3 +1,2 @@
outline_name_black_list:
- 的,了,和,是,在
- 历史,日志
- 历史,日志,名称,比例,数量,金额,次数,属性,对应,分类,姓名,单位,总数,行使,子项,占比,记录,列表,目标,字段,字符串,动作

3
win_text_editor/lib/framework/models/file_node.dart

@ -16,6 +16,9 @@ class FileNode implements TreeNode { @@ -16,6 +16,9 @@ class FileNode implements TreeNode {
@override
bool isExpanded;
@override
String get title => name;
FileNode({
required this.name,
required this.path,

3
win_text_editor/lib/modules/data_format/models/template_node.dart

@ -17,6 +17,9 @@ class TemplateNode implements TreeNode { @@ -17,6 +17,9 @@ class TemplateNode implements TreeNode {
int repreatCount;
bool isChecked; //
@override
String get title => name;
TemplateNode({
required this.name,
required this.children,

38
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'; @@ -6,7 +6,6 @@ import 'package:win_text_editor/modules/outline/services/outline_service.dart';
class OutlineProvider with ChangeNotifier {
List<OutlineNode> _outlineNode = [];
bool _isLoading = true;
String _searchQuery = '';
String? _currentRootPath; //
bool get isLoading => _isLoading;
@ -23,22 +22,7 @@ class OutlineProvider with ChangeNotifier { @@ -23,22 +22,7 @@ class OutlineProvider with ChangeNotifier {
await _loadRootDirectory();
}
List<OutlineNode> 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<OutlineNode> get outlineNode => _outlineNode;
void toggleExpand(OutlineNode node) {
node.isExpanded = !node.isExpanded;
@ -71,17 +55,7 @@ class OutlineProvider with ChangeNotifier { @@ -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 { @@ -108,8 +82,8 @@ class OutlineProvider with ChangeNotifier {
}
Future<void> 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 { @@ -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();
}

10
win_text_editor/lib/modules/outline/models/outline_node.dart

@ -5,6 +5,8 @@ import 'package:win_text_editor/shared/components/tree_view.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 { @@ -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<OutlineNode>? children,
this.title = "",
}) : children = children ?? [];
@override
@ -46,6 +54,7 @@ class OutlineNode implements TreeNode { @@ -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 { @@ -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,

304
win_text_editor/lib/modules/outline/services/outline_service.dart

@ -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;

3
win_text_editor/lib/modules/outline/widgets/outline_explorer.dart

@ -2,7 +2,6 @@ import 'dart:math'; @@ -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<OutlineExplorer> { @@ -59,7 +58,7 @@ class _OutlineExplorerState extends State<OutlineExplorer> {
for (final node in nodesToFilter) {
// (01)
if (currentDepth == 1 && node.name.toLowerCase().contains(_searchQuery.toLowerCase())) {
if (node.title.toLowerCase().contains(_searchQuery.toLowerCase())) {
result.add(node);
}

3
win_text_editor/lib/shared/components/tree_view.dart

@ -4,6 +4,7 @@ import 'package:flutter/material.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<TreeNode> get children;
@ -203,7 +204,7 @@ class TreeNodeWidget extends StatelessWidget { @@ -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(

3
win_text_editor/lib/shared/models/template_node.dart

@ -18,6 +18,9 @@ class TemplateNode implements TreeNode { @@ -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,

Loading…
Cancel
Save