Browse Source

差不多完工了

master
hejl 4 weeks ago
parent
commit
2f5589b36f
  1. 72
      win_text_editor/lib/modules/outline/controllers/outline_provider.dart
  2. 53
      win_text_editor/lib/modules/outline/models/outline_node.dart
  3. 80
      win_text_editor/lib/modules/outline/services/component_service.dart
  4. 157
      win_text_editor/lib/modules/outline/services/functions_service.dart
  5. 563
      win_text_editor/lib/modules/outline/services/outline_service.dart
  6. 135
      win_text_editor/lib/modules/outline/services/std_field_service.dart
  7. 95
      win_text_editor/lib/modules/outline/services/uft_object_service.dart
  8. 84
      win_text_editor/lib/modules/outline/widgets/outline_explorer.dart
  9. 44
      win_text_editor/lib/shared/components/tree_view.dart

72
win_text_editor/lib/modules/outline/controllers/outline_provider.dart

@ -8,11 +8,21 @@ class OutlineProvider with ChangeNotifier {
bool _isLoading = true; bool _isLoading = true;
String? _currentRootPath; // String? _currentRootPath; //
static OutlineNode rootNode = OutlineNode( static OutlineNode rootNode = OutlineNode(
name: "所有Tag", name: "所有",
title: "所有Tag", title: "所有",
value: 'UFTTable', value: 'All',
frequency: 0,
isDirectory: true, isDirectory: true,
isRoot: true,
isExpanded: true,
);
static OutlineNode searchNode = OutlineNode(
name: "搜索结果",
title: "搜索结果",
value: 'Query',
isDirectory: true,
isRoot: true,
isExpanded: true,
); );
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
@ -29,25 +39,7 @@ class OutlineProvider with ChangeNotifier {
return; return;
} }
_currentRootPath = path; _currentRootPath = path;
await _loadRootDirectory(); _loadRootDirectory();
}
List<OutlineNode> get outlineNode {
rootNode.children = _outlineNode;
return [rootNode];
}
void toggleExpand(OutlineNode node) {
node.isExpanded = !node.isExpanded;
notifyListeners();
}
Future<void> loadDirectory(String path) async {
_isLoading = true;
notifyListeners();
_isLoading = false;
notifyListeners();
} }
// outline_provider.dart _loadRootDirectory // outline_provider.dart _loadRootDirectory
@ -79,35 +71,16 @@ class OutlineProvider with ChangeNotifier {
} }
} }
Future<void> toggleDirectory(OutlineNode dirNode) async {
if (dirNode.children.isEmpty) {
//
_isLoading = true;
notifyListeners();
_isLoading = false;
notifyListeners();
} else {
//
dirNode.isExpanded = !dirNode.isExpanded;
notifyListeners();
}
}
Future<void> loadDirectoryContents(OutlineNode dirNode) async { Future<void> loadDirectoryContents(OutlineNode dirNode) async {
if (dirNode.children.isNotEmpty) { if (dirNode.children.isNotEmpty) {
//
dirNode.isExpanded = !dirNode.isExpanded;
notifyListeners();
return; return;
} }
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
if (_currentRootPath != null) { if (_currentRootPath != null) {
await OutlineService.loadChildren(_currentRootPath!, dirNode); await OutlineService.loadChildren(_currentRootPath!, dirNode);
dirNode.isExpanded = !dirNode.isExpanded; dirNode.isExpanded = true;
} }
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
@ -116,8 +89,19 @@ class OutlineProvider with ChangeNotifier {
Future<void> refreshOutlineTree({bool loadContent = false}) async { Future<void> refreshOutlineTree({bool loadContent = false}) async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
Logger().info('正在刷新大纲树...');
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
} }
//
List<OutlineNode> applyFilter(String searchQuery) {
if (searchQuery.isEmpty) {
if (rootNode.children.isEmpty) rootNode.children = _outlineNode;
return [rootNode];
}
OutlineService.applySearchFilter(searchNode, searchQuery);
return [searchNode];
}
} }

53
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:flutter/material.dart';
import 'package:win_text_editor/shared/components/tree_view.dart'; import 'package:win_text_editor/shared/components/tree_view.dart';
@ -6,14 +5,13 @@ class OutlineNode implements TreeNode {
@override @override
final String name; final String name;
@override
String title = ""; String title = "";
List<OutlineNode> _children = []; List<OutlineNode> _children = [];
OutlineNode? parent;
@override @override
final bool isDirectory; bool isDirectory;
final bool isRoot; final bool isRoot;
@override @override
final int depth; final int depth;
@ -23,10 +21,7 @@ class OutlineNode implements TreeNode {
bool isExpanded; bool isExpanded;
final int frequency; final int frequency;
late String uuid; late String id;
@override
bool isVisible;
final String value; final String value;
String? wordClass; String? wordClass;
@ -34,24 +29,30 @@ class OutlineNode implements TreeNode {
OutlineNode({ OutlineNode({
required this.name, required this.name,
required this.value, required this.value,
required this.frequency, this.isDirectory = false,
required this.isDirectory, this.id = "",
this.isRoot = false, this.isRoot = false,
this.depth = 0, this.depth = 0,
this.frequency = 0,
this.isExpanded = false, this.isExpanded = false,
this.wordClass, this.wordClass,
List<OutlineNode>? children, List<OutlineNode>? children,
this.title = "", this.title = "",
this.isVisible = true,
}) : _children = children ?? [] { }) : _children = children ?? [] {
uuid = DateTime.now().microsecondsSinceEpoch.toRadixString(36); id = DateTime.now().microsecondsSinceEpoch.toRadixString(36);
for (var child in _children) {
child.parent = this;
}
} }
@override OutlineNode copyWith({bool? isExpanded, List<OutlineNode>? children}) {
String get id => uuid; return OutlineNode(
id: id,
name: name,
value: value,
title: title,
isExpanded: isExpanded ?? this.isExpanded,
isDirectory: isDirectory,
children: children ?? this.children,
);
}
// //
@override @override
@ -65,9 +66,6 @@ class OutlineNode implements TreeNode {
set children(List<OutlineNode> nodes) { set children(List<OutlineNode> nodes) {
_children = nodes; _children = nodes;
for (var child in _children) {
child.parent = this;
}
} }
// //
@ -78,22 +76,11 @@ class OutlineNode implements TreeNode {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
return other is OutlineNode && return other is OutlineNode && other.id == id;
other.name == name &&
other.frequency == frequency &&
other.isDirectory == isDirectory &&
other.isRoot == isRoot &&
listEquals(other.children, children) &&
other.isExpanded == isExpanded;
} }
@override @override
int get hashCode { int get hashCode {
return name.hashCode ^ return id.hashCode;
frequency.hashCode ^
isDirectory.hashCode ^
isRoot.hashCode ^
children.hashCode ^
isExpanded.hashCode;
} }
} }

80
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<String, List<String>> componentFieldMap = {};
//
static Future<void> 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<String>(),
);
}
}
} catch (e) {
Logger().error('加载Component失败: $e');
}
}
//
static Future<Map<String, String>> matchComponent(String fieldName) async {
Map<String, String> matchComponents = {};
componentFieldMap.forEach((key, value) {
if (value.contains(fieldName)) matchComponents[key] = value[0];
});
return matchComponents;
}
static Future<void> 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<OutlineNode> searchComponents(String searchQuery) {
final List<OutlineNode> 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;
}
}

157
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<String, List<String>> uftbusinessMap = {};
//
static Map<String, List<String>> uftatomMap = {};
static Future<void> initFunctionsMap(String rootPath) async {
await _initUftMap('uftbusiness', rootPath, uftbusinessMap, ['.uffunction']);
await _initUftMap('uftatom', rootPath, uftatomMap, ['.uftatomfunction', '.uftatomservice']);
}
static Future<void> _initUftMap(
String dirName,
String rootPath,
Map<String, List<String>> targetMap,
List<String> 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<File>()
.toList();
await Future.wait(files.map((file) => _processUftFile(file, targetMap)));
} catch (e) {
Logger().error('加载$dirName对象失败: $e');
rethrow;
}
}
static Future<void> _processUftFile(File file, Map<String, List<String>> 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 = <XmlElement>[
...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<void> 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<void> 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<OutlineNode> searchBusiness(String searchQuery) {
final List<OutlineNode> 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<OutlineNode> searchAtoms(String searchQuery) {
final List<OutlineNode> 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;
}
}

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

@ -3,6 +3,10 @@ import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:jieba_flutter/analysis/jieba_segmenter.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/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:xml/xml.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/framework/controllers/logger.dart';
@ -19,23 +23,16 @@ class OutlineService {
// ignore: constant_identifier_names // ignore: constant_identifier_names
static const List<String> REQUIRED_DIRS = ['uftstructure', 'uftatom', 'uftbusiness', 'uftfactor']; 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 // ignore: constant_identifier_names
static const List<String> FILTERED_WORD_CLASSES = ['v', 'a', 'ad', 'f', 'd', 't', 'r']; static const List<String> FILTERED_WORD_CLASSES = ['v', 'a', 'ad', 'f', 'd', 't', 'r'];
// //
static List<String> _blackList = []; static List<String> _blackList = [];
static OutlineNode? _searchNode;
//
static bool _isJiebaInitialized = false; static bool _isJiebaInitialized = false;
static JiebaSegmenter? _segmenter; static JiebaSegmenter? _segmenter;
static Map<String, String> _wordClassDict = {};
static bool _isWordClassDictInitialized = false;
// //
static Future<void> _initJieba() async { static Future<void> _initJieba() async {
@ -76,26 +73,22 @@ class OutlineService {
} }
// //
static Future<void> _initWordClassDict() async { static Future<Map<String, String>> _initWordClassDict() async {
if (_isWordClassDictInitialized) return; Map<String, String> wordClassDict = {};
try { try {
Logger().info('开始加载词性字典...'); Logger().info('开始加载词性字典...');
final dictContent = await rootBundle.loadString('assets/dict.txt'); final dictContent = await rootBundle.loadString('assets/dict.txt');
_wordClassDict = Map.fromEntries( wordClassDict = Map.fromEntries(
dictContent.split('\n').where((line) => line.trim().isNotEmpty).map((line) { dictContent.split('\n').where((line) => line.trim().isNotEmpty).map((line) {
final parts = line.trim().split(RegExp(r'\s+')); final parts = line.trim().split(RegExp(r'\s+'));
return (parts.length >= 3) ? MapEntry(parts[0], parts[2]) : null; return (parts.length >= 3) ? MapEntry(parts[0], parts[2]) : null;
}).whereType<MapEntry<String, String>>(), }).whereType<MapEntry<String, String>>(),
); );
_isWordClassDictInitialized = true;
Logger().info('加载词性字典成功: ${_wordClassDict.length}个词');
} catch (e) { } catch (e) {
Logger().error('加载词性字典失败: $e'); Logger().error('加载词性字典失败: $e');
_wordClassDict = {};
} }
return wordClassDict;
} }
// stdfield文件 // stdfield文件
@ -137,14 +130,9 @@ class OutlineService {
// //
static Future<void> loadChildren(String rootPath, OutlineNode dirNode) async { static Future<void> loadChildren(String rootPath, OutlineNode dirNode) async {
try { try {
switch (dirNode.depth) { if (dirNode.depth == 1) {
case 1: // () await StdFieldService.loadStdfields(rootPath, dirNode);
await _loadStdfields(rootPath, dirNode); } else {
break;
case 3: //
await _loadFieldActions(rootPath, dirNode);
break;
default:
Logger().error("节点层次不支持: ${dirNode.depth}"); Logger().error("节点层次不支持: ${dirNode.depth}");
} }
} catch (e) { } catch (e) {
@ -153,410 +141,22 @@ class OutlineService {
} }
} }
// //
static Future<void> _loadFieldActions(String rootPath, OutlineNode dirNode) async { static Future<List<OutlineNode>> getWordNodes(String rootPath) async {
switch (dirNode.name) { if (rootPath.isEmpty) {
case 'UFTTable': throw Exception('根路径不能为空');
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<void> _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<Map<String, String>> _matchComponent(String rootPath, String fieldName) async {
Map<String, String> 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<void> _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<File>()
.toList();
for (final file in uftatomFiles) {
try {
final content = await file.readAsString();
final document = XmlDocument.parse(content);
String matchType = "I";
//
final List<XmlElement> 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<void> _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<File>()
.toList();
for (final file in uftbusinessFiles) {
try {
final content = await file.readAsString();
final document = XmlDocument.parse(content);
String matchType = "I";
//
final List<XmlElement> 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<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;
//
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<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);
}
}
} }
// final wordClassDict = await _initWordClassDict();
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,
),
),
);
}
// //
static Future<List<OutlineNode>> getWordNodes(String rootPath) async { await Future.wait([
await Future.wait([_initBlackList(), _initWordClassDict()]); _initBlackList(),
ComponentService.initComponentFieldMap(rootPath),
StdFieldService.initStdFieldMap(rootPath),
UftObjectService.initUftObjectMap(rootPath),
FunctionsService.initFunctionsMap(rootPath),
]);
final stdfieldPath = '$rootPath/metadata/stdfield.stdfield'; final stdfieldPath = '$rootPath/metadata/stdfield.stdfield';
if (!await File(stdfieldPath).exists()) { if (!await File(stdfieldPath).exists()) {
@ -580,8 +180,7 @@ class OutlineService {
frequency: entry.value, frequency: entry.value,
isDirectory: true, isDirectory: true,
depth: 1, depth: 1,
isRoot: true, wordClass: wordClassDict[entry.key],
wordClass: _wordClassDict[entry.key],
), ),
) )
.where((node) => !FILTERED_WORD_CLASSES.contains(node.wordClass)) .where((node) => !FILTERED_WORD_CLASSES.contains(node.wordClass))
@ -617,4 +216,108 @@ class OutlineService {
return false; 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<OutlineNode> 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<OutlineNode> 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<OutlineNode> 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<OutlineNode> 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<OutlineNode> 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 = "无结果";
}
}
} }

135
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<String, String> FIELD_ACTIONS = {
"UFTTable": "UFT对象",
"Component": "标准组件",
"Business": "业务层",
"Atom": "原子层",
};
//
static Map<String, String> stdfieldMap = {};
//
static Future<void> 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<void> _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<void> 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<OutlineNode> searchStdFields(String searchQuery) {
final List<OutlineNode> 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;
}
}

95
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<String, List<String>> uftObjectMap = {};
//
static Future<void> 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<File>()
.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<String>(),
);
} catch (e) {
Logger().error('解析文件 ${file.path} 失败: $e');
}
}
} catch (e) {
Logger().error('加载UFT对象失败: $e');
rethrow;
}
}
// UFT对象
static Future<void> 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<OutlineNode> searchUftObjects(String searchQuery) {
final List<OutlineNode> 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;
}
}

84
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:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -22,64 +22,19 @@ class _OutlineExplorerState extends State<OutlineExplorer> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
String _searchQuery = ''; String _searchQuery = '';
Timer? _searchDebounceTimer;
@override @override
void dispose() { void dispose() {
_scrollController.dispose(); _scrollController.dispose();
_searchController.dispose(); _searchController.dispose();
_searchDebounceTimer?.cancel();
super.dispose(); 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<OutlineNode> nodes) {
int maxDepth = 0;
for (final node in nodes) {
if (node.isDirectory && node.isExpanded) {
maxDepth = max(maxDepth, _getMaxDepth(node.children) + 1);
}
}
return maxDepth;
}
// - // -
List<OutlineNode> _filterNodes(List<OutlineNode> nodes) { List<OutlineNode> _filterNodes(OutlineProvider outlineProvider) {
void resetVisibility(List<OutlineNode> nodes) { return outlineProvider.applyFilter(_searchQuery);
for (final node in nodes) {
node.isVisible = true;
if (node.isDirectory) {
resetVisibility(node.children);
}
}
}
void applySearchFilter(List<OutlineNode> 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;
} }
@override @override
@ -101,7 +56,7 @@ class _OutlineExplorerState extends State<OutlineExplorer> {
controller: _searchController, controller: _searchController,
style: const TextStyle(fontSize: 12), // 10 style: const TextStyle(fontSize: 12), // 10
decoration: InputDecoration( decoration: InputDecoration(
hintText: '搜索Tag...', hintText: '搜索...',
hintStyle: const TextStyle(fontSize: 12), // 10 hintStyle: const TextStyle(fontSize: 12), // 10
prefixIcon: const Icon( prefixIcon: const Icon(
Icons.search, Icons.search,
@ -134,10 +89,14 @@ class _OutlineExplorerState extends State<OutlineExplorer> {
), ),
), ),
onChanged: (value) { onChanged: (value) {
setState(() { _searchDebounceTimer?.cancel();
_searchQuery = value; _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<OutlineExplorer> {
child: SizedBox( child: SizedBox(
width: 300, width: 300,
child: TreeView( child: TreeView(
nodes: _filterNodes(outlineProvider.outlineNode), nodes: _filterNodes(outlineProvider),
config: const TreeViewConfig(showIcons: true, lazyLoad: true), config: const TreeViewConfig(
showIcons: true,
lazyLoad: true,
showRefreshButton: true,
),
onNodeTap: (node) => _handleNodeTap(context, node as OutlineNode), onNodeTap: (node) => _handleNodeTap(context, node as OutlineNode),
onNodeDoubleTap: onNodeDoubleTap:
(node) => _handleNodeDoubleTap(node as OutlineNode, fileProvider), (node) => _handleNodeDoubleTap(node as OutlineNode, fileProvider),
onRefresh: () async {
await outlineProvider.refreshOutlineTree();
setState(() {});
},
), ),
), ),
), ),
@ -191,6 +158,9 @@ class _OutlineExplorerState extends State<OutlineExplorer> {
Future<void> _handleNodeTap(BuildContext context, OutlineNode node) async { Future<void> _handleNodeTap(BuildContext context, OutlineNode node) async {
final outlineProvider = Provider.of<OutlineProvider>(context, listen: false); final outlineProvider = Provider.of<OutlineProvider>(context, listen: false);
if (node.isDirectory) { if (node.isDirectory) {
setState(() {
node.isExpanded = !node.isExpanded; //
});
await outlineProvider.loadDirectoryContents(node); await outlineProvider.loadDirectoryContents(node);
} }
} }
@ -226,14 +196,14 @@ class _OutlineExplorerState extends State<OutlineExplorer> {
) async { ) async {
// 使 fileProvider // 使 fileProvider
if (fileProvider?.rootPath != null && fileProvider!.rootPath!.isNotEmpty) { if (fileProvider?.rootPath != null && fileProvider!.rootPath!.isNotEmpty) {
await outlineProvider.setRootPath(fileProvider.rootPath!); outlineProvider.setRootPath(fileProvider.rootPath!);
return; return;
} }
// //
final String? lastOpenedFolder = await FilePathManager.getLastOpenedFolder(); final String? lastOpenedFolder = await FilePathManager.getLastOpenedFolder();
if (lastOpenedFolder != null) { if (lastOpenedFolder != null) {
await outlineProvider.setRootPath(lastOpenedFolder); outlineProvider.setRootPath(lastOpenedFolder);
} }
} }
} }

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

@ -10,7 +10,6 @@ abstract class TreeNode {
List<TreeNode> get children; List<TreeNode> get children;
int get depth; int get depth;
IconData? get iconData; IconData? get iconData;
bool get isVisible;
} }
/// ///
@ -21,7 +20,9 @@ class TreeViewConfig {
final bool showIcons; final bool showIcons;
final Color? selectedColor; final Color? selectedColor;
final double indentWidth; final double indentWidth;
final Map<String, IconData> icons; // final final Map<String, IconData> icons;
final bool showRefreshButton;
final IconData refreshIcon;
const TreeViewConfig({ const TreeViewConfig({
this.lazyLoad = false, this.lazyLoad = false,
@ -30,7 +31,9 @@ class TreeViewConfig {
this.showIcons = true, this.showIcons = true,
this.selectedColor, this.selectedColor,
this.indentWidth = 24.0, 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)? onNodeTap;
final Function(TreeNode)? onNodeDoubleTap; final Function(TreeNode)? onNodeDoubleTap;
final Function(TreeNode, bool?)? onNodeCheckChanged; 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 ScrollController? scrollController;
final VoidCallback? onRefresh;
const TreeView({ const TreeView({
super.key, super.key,
@ -53,6 +57,7 @@ class TreeView extends StatefulWidget {
this.onNodeCheckChanged, this.onNodeCheckChanged,
this.nodeBuilder, this.nodeBuilder,
this.scrollController, this.scrollController,
this.onRefresh,
}); });
@override @override
@ -91,7 +96,9 @@ class _TreeViewState extends State<TreeView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListView.builder( return Stack(
children: [
ListView.builder(
controller: _effectiveController, controller: _effectiveController,
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
itemCount: _countVisibleNodes(widget.nodes), itemCount: _countVisibleNodes(widget.nodes),
@ -99,10 +106,10 @@ class _TreeViewState extends State<TreeView> {
final node = _getVisibleNode(widget.nodes, index); final node = _getVisibleNode(widget.nodes, index);
final isSelected = _selectedIds.contains(node.id); final isSelected = _selectedIds.contains(node.id);
// 使
return widget.nodeBuilder != null return widget.nodeBuilder != null
? widget.nodeBuilder!(context, node, isSelected, () => _handleNodeTap(node)) ? widget.nodeBuilder!(context, node, isSelected, () => _handleNodeTap(node))
: TreeNodeWidget( : TreeNodeWidget(
key: ValueKey(node.id),
node: node, node: node,
config: widget.config, config: widget.config,
isSelected: isSelected, isSelected: isSelected,
@ -112,6 +119,25 @@ class _TreeViewState extends State<TreeView> {
onCheckChanged: (value) => _handleNodeCheckChanged(node, value), 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<TreeView> {
int _countVisibleNodes(List<TreeNode> nodes) { int _countVisibleNodes(List<TreeNode> nodes) {
int count = 0; int count = 0;
for (final node in nodes) { for (final node in nodes) {
if (!node.isVisible) continue; //
count++; count++;
if (node.isDirectory && node.isExpanded) { if (node.isDirectory && node.isExpanded) {
count += _countVisibleNodes(node.children); count += _countVisibleNodes(node.children);
@ -154,8 +179,6 @@ class _TreeViewState extends State<TreeView> {
TreeNode _getVisibleNode(List<TreeNode> nodes, int index) { TreeNode _getVisibleNode(List<TreeNode> nodes, int index) {
int current = 0; int current = 0;
for (final node in nodes) { for (final node in nodes) {
if (!node.isVisible) continue; //
if (current == index) return node; if (current == index) return node;
current++; current++;
@ -181,6 +204,7 @@ class TreeNodeWidget extends StatelessWidget {
final Function(bool?)? onCheckChanged; final Function(bool?)? onCheckChanged;
const TreeNodeWidget({ const TreeNodeWidget({
super.key,
required this.node, required this.node,
required this.config, required this.config,
required this.isSelected, required this.isSelected,
@ -192,8 +216,8 @@ class TreeNodeWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (!node.isVisible) return const SizedBox.shrink(); //
return InkWell( return InkWell(
key: ValueKey(node.id),
onTap: onTap, onTap: onTap,
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
splashColor: Colors.transparent, splashColor: Colors.transparent,

Loading…
Cancel
Save