Browse Source

优化前夕

master
hejl 2 months ago
parent
commit
6246220695
  1. 56
      win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart
  2. 30
      win_text_editor/lib/modules/template_parser/models/template_node.dart
  3. 242
      win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart
  4. 2
      win_text_editor/lib/shared/components/tree_view.dart

56
win_text_editor/lib/modules/template_parser/controllers/template_parser_controller.dart

@ -63,17 +63,61 @@ class TemplateParserController extends BaseContentController { @@ -63,17 +63,61 @@ class TemplateParserController extends BaseContentController {
Future<void> _parseXmlContent(String xmlContent) async {
final document = xml.XmlDocument.parse(xmlContent);
Logger().debug('开始解析XML,根元素: ${document.rootElement.name}');
_treeNodes = _buildTreeNodes(document.rootElement, depth: 0);
Logger().debug('解析完成,共生成 ${_treeNodes.length} 个顶级节点');
_treeNodes = _buildTreeNodes(document.rootElement, document.rootElement.localName, depth: 0);
// templateItems
_templateItems = _parseAllNodeValues(document);
notifyListeners();
}
List<TemplateItem> _parseAllNodeValues(xml.XmlDocument document) {
final items = <TemplateItem>[];
int id = 0;
//
void traverse(xml.XmlElement element, String currentPath) {
// 1.
for (final attr in element.attributes) {
items.add(TemplateItem(
id: id++,
content: attr.value,
xPath: '$currentPath/@${attr.name.local}',
value: attr.value,
nodeType: NodeType.attribute,
));
}
// 2.
final textNodes = element.children.whereType<xml.XmlText>().where((t) => t.text.trim().isNotEmpty);
if (textNodes.isNotEmpty) {
items.add(TemplateItem(
id: id++,
content: textNodes.first.text,
xPath: currentPath,
value: textNodes.first.text,
nodeType: NodeType.text,
));
}
// 3.
for (final child in element.childElements) {
traverse(child, '$currentPath/${child.name.local}');
}
}
traverse(document.rootElement, document.rootElement.localName);
return items;
}
List<TemplateNode> _buildTreeNodes(
xml.XmlElement element, {
xml.XmlElement element,
String path, {
required int depth,
int repreatCount = 1,
}) {
final node = TemplateNode(
path: path,
name: element.name.local,
children: [],
depth: depth,
@ -87,6 +131,7 @@ class TemplateParserController extends BaseContentController { @@ -87,6 +131,7 @@ class TemplateParserController extends BaseContentController {
node.children.addAll(
element.attributes.map(
(attr) => TemplateNode(
path: '$path/@${attr.name.local}',
name: '@${attr.name.local}',
children: [],
depth: depth + 1,
@ -107,13 +152,14 @@ class TemplateParserController extends BaseContentController { @@ -107,13 +152,14 @@ class TemplateParserController extends BaseContentController {
//
groupedChildren.forEach((name, elements) {
String path0 = '$path/${elements.first.name.local}';
if (elements.length == 1) {
//
node.children.addAll(_buildTreeNodes(elements.first, depth: depth + 1));
node.children.addAll(_buildTreeNodes(elements.first, path0, depth: depth + 1));
} else {
//
node.children.addAll(
_buildTreeNodes(elements.first, depth: depth + 1, repreatCount: elements.length),
_buildTreeNodes(elements.first, path0, depth: depth + 1, repreatCount: elements.length),
);
}
});

30
win_text_editor/lib/modules/template_parser/models/template_node.dart

@ -5,6 +5,7 @@ class TemplateNode implements TreeNode { @@ -5,6 +5,7 @@ class TemplateNode implements TreeNode {
final String name;
final List<TemplateNode> children;
final int depth;
final String path;
bool isExpanded;
bool isRepeated;
bool isAttribute;
@ -13,6 +14,7 @@ class TemplateNode implements TreeNode { @@ -13,6 +14,7 @@ class TemplateNode implements TreeNode {
TemplateNode({
required this.name,
required this.children,
required this.path,
required this.depth,
this.isExpanded = false,
this.isRepeated = false,
@ -20,30 +22,34 @@ class TemplateNode implements TreeNode { @@ -20,30 +22,34 @@ class TemplateNode implements TreeNode {
this.repreatCount = 1,
});
// TreeNode接口
@override
String get id => '$name-$depth';
@override
bool get isDirectory => children.isNotEmpty;
@override
IconData? get iconData => isAttribute ? Icons.code : Icons.label_outline;
@override
String get id => path;
}
enum NodeType { element, attribute, text }
class TemplateItem {
final int id;
final String content;
final String xPath;
final String value;
final NodeType nodeType;
TemplateItem({
required this.id,
required this.content,
required this.xPath,
required this.value,
required this.nodeType,
});
TemplateItem({required this.id, required this.content, required this.xPath, required this.value});
bool matches(TemplateNode node) {
if (node.name.startsWith('@')) {
final attrName = node.name.substring(1);
return xPath.contains('@$attrName');
}
return xPath.contains(node.name);
bool matchesPath(String path) {
return xPath == path;
}
}

242
win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart

@ -1,5 +1,9 @@ @@ -1,5 +1,9 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
import 'package:win_text_editor/modules/template_parser/controllers/template_parser_controller.dart';
import 'package:win_text_editor/modules/template_parser/models/template_node.dart';
@ -82,102 +86,214 @@ class _TemplateParserViewState extends State<TemplateParserView> { @@ -82,102 +86,214 @@ class _TemplateParserViewState extends State<TemplateParserView> {
child: Card(child: _buildTreeView(controller.treeNodes)),
),
const SizedBox(width: 8),
Expanded(child: Card(child: _buildGridView(controller.templateItems))),
Expanded(
child: Card(
child: GestureDetector(
onSecondaryTapDown: (details) {
_showContextMenu(context, details.globalPosition);
},
child: _buildGridView(controller.templateItems),
),
),
),
],
);
},
);
}
Widget _buildTreeView(List<TemplateNode> nodes) {
if (nodes.isEmpty) {
return const Center(child: Text('No XML data available'));
}
Future<void> _showContextMenu(BuildContext context, Offset position) async {
//
final renderBox = context.findRenderObject() as RenderBox;
final localPosition = renderBox.globalToLocal(position);
return TreeView(
nodes: _processXmlNodes(nodes),
config: const TreeViewConfig(
showIcons: true,
singleSelect: true,
selectedColor: Colors.lightBlueAccent,
icons: {'element': Icons.label_outline, 'attribute': Icons.code},
//
final result = await showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(
position.dx,
position.dy,
position.dx + renderBox.size.width - localPosition.dx,
position.dy + renderBox.size.height - localPosition.dy,
),
onNodeTap: (node) {
final templateNode = node as TemplateNode;
Provider.of<TemplateParserController>(context, listen: false).selectTreeNode(templateNode);
},
nodeBuilder: (context, node, isSelected, onTap) => _buildCustomNode(node),
items: [const PopupMenuItem<String>(value: 'export', child: Text('导出(csv)'))],
);
//
if (result == 'export' && context.mounted) {
try {
await _exportToCsv();
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}')));
}
}
}
}
List<TreeNode> _processXmlNodes(List<TemplateNode> nodes) {
return nodes.cast<TreeNode>();
Future<void> _exportToCsv() async {
String csvData = '序号\t内容\n';
final items = _controller.templateItems;
for (var i = 0; i < items.length; i++) {
final item = items[i];
csvData += '${i + 1}\t${item.value}\n';
}
final filePath = await FilePicker.platform.saveFile(
dialogTitle: '保存导出结果',
fileName: 'template_results.csv',
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (filePath != null) {
final file = File(filePath);
await file.writeAsString(csvData);
}
}
Widget _buildCustomNode(TreeNode node) {
final templateNode = node as TemplateNode;
final isAttribute = node.isAttribute;
Widget _buildTreeView(List<TemplateNode> nodes) {
if (nodes.isEmpty) {
return const Center(child: Text('No XML data available'));
}
return Consumer<TemplateParserController>(
builder: (context, controller, _) {
return Padding(
padding: EdgeInsets.only(left: 12.0 * node.depth),
child: ListTile(
dense: true,
leading:
isAttribute
? const Icon(Icons.code, size: 16, color: Colors.grey)
: const Icon(Icons.label_outline, size: 18, color: Colors.blue),
title: Text(
isAttribute ? templateNode.name.substring(1) : templateNode.name,
style: TextStyle(
color: isAttribute ? Colors.grey[600] : Colors.black,
fontWeight: isAttribute ? FontWeight.normal : FontWeight.w500,
),
),
trailing:
templateNode.isRepeated
? Text(
"(${templateNode.repreatCount.toString()})",
style: const TextStyle(color: Colors.grey),
)
: null,
onTap: () => controller.selectTreeNode(templateNode),
return TreeView(
nodes: _processXmlNodes(nodes),
config: const TreeViewConfig(
showIcons: true,
singleSelect: true,
selectedColor: Colors.lightBlueAccent,
icons: {'element': Icons.label_outline, 'attribute': Icons.code},
),
onNodeTap: (node) {
final templateNode = node as TemplateNode;
controller.selectTreeNode(templateNode);
},
nodeBuilder: (context, node, isSelected, onTap) {
final templateNode = node as TemplateNode;
final isAttribute = node.isAttribute;
// 使 selectedNode
final isActuallySelected = controller.selectedNode?.id == templateNode.id;
return Container(
color:
isActuallySelected ? Colors.lightBlueAccent.withOpacity(0.2) : Colors.transparent,
child: Padding(
padding: EdgeInsets.only(left: 12.0 * node.depth),
child: ListTile(
dense: true,
leading:
isAttribute
? const Icon(Icons.code, size: 16, color: Colors.grey)
: const Icon(Icons.label_outline, size: 18, color: Colors.blue),
title: Text(
isAttribute ? templateNode.name.substring(1) : templateNode.name,
style: TextStyle(
color: isAttribute ? Colors.grey[600] : Colors.black,
fontWeight: isAttribute ? FontWeight.normal : FontWeight.w500,
),
),
trailing:
templateNode.isRepeated
? Text(
"(${templateNode.repreatCount.toString()})",
style: const TextStyle(color: Colors.grey),
)
: null,
onTap: onTap,
),
),
);
},
);
},
);
}
List<TreeNode> _processXmlNodes(List<TemplateNode> nodes) {
return nodes.cast<TreeNode>();
}
Widget _buildGridView(List<TemplateItem> items) {
return Consumer<TemplateParserController>(
builder: (context, controller, _) {
// /
// path过滤数
final filteredItems =
controller.selectedNode != null
? items.where((item) => item.matches(controller.selectedNode!))
? items.where((item) => item.matchesPath(controller.selectedNode!.path)).toList()
: items;
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 5,
),
itemCount: filteredItems.length,
itemBuilder: (context, index) {
final item = filteredItems.elementAt(index);
return Card(
child: Padding(
// DataGridSource
final dataSource = _TemplateItemDataSource(
items: filteredItems,
selectedNode: controller.selectedNode,
);
return SfDataGrid(
source: dataSource,
columns: [
GridColumn(
columnName: 'index',
width: 60,
label: Container(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [Text('Path: ${item.xPath}'), Text('Value: ${item.value}')],
),
color: Colors.grey[200],
alignment: Alignment.center,
child: const Text('序号'),
),
);
},
),
GridColumn(
columnName: 'content',
label: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.center,
color: Colors.grey[200],
child: const Text('内容'),
),
),
],
gridLinesVisibility: GridLinesVisibility.both,
headerGridLinesVisibility: GridLinesVisibility.both,
columnWidthMode: ColumnWidthMode.fill,
);
},
);
}
}
class _TemplateItemDataSource extends DataGridSource {
final List<TemplateItem> items;
final TemplateNode? selectedNode;
_TemplateItemDataSource({required this.items, required this.selectedNode});
@override
List<DataGridRow> get rows =>
items.map((item) {
return DataGridRow(
cells: [
DataGridCell<int>(columnName: 'index', value: items.indexOf(item) + 1),
DataGridCell<String>(columnName: 'content', value: item.value),
],
);
}).toList();
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells:
row.getCells().map<Widget>((dataGridCell) {
return Container(
padding: const EdgeInsets.all(8.0),
alignment:
dataGridCell.columnName == 'index' ? Alignment.center : Alignment.centerLeft,
child: Text(dataGridCell.value.toString()),
);
}).toList(),
);
}
}

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

@ -119,7 +119,7 @@ class _TreeViewState extends State<TreeView> { @@ -119,7 +119,7 @@ class _TreeViewState extends State<TreeView> {
}
void _handleNodeTap(TreeNode node) {
if (widget.config.singleSelect) {
if (widget.config.singleSelect && !node.isDirectory) { //
setState(() {
_selectedIds.clear();
_selectedIds.add(node.id);

Loading…
Cancel
Save