|
|
|
@ -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(), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|