|
|
|
@ -1,6 +1,7 @@
@@ -1,6 +1,7 @@
|
|
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
|
import 'package:provider/provider.dart'; |
|
|
|
|
import 'package:syncfusion_flutter_datagrid/datagrid.dart'; |
|
|
|
|
import 'package:win_text_editor/framework/controllers/logger.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'; |
|
|
|
|
import 'package:file_picker/file_picker.dart'; |
|
|
|
@ -18,9 +19,9 @@ class TemplateGridView extends StatelessWidget {
@@ -18,9 +19,9 @@ class TemplateGridView extends StatelessWidget {
|
|
|
|
|
return GestureDetector( |
|
|
|
|
onSecondaryTapDown: (details) { |
|
|
|
|
_showContextMenu(context, details.globalPosition, controller); |
|
|
|
|
}, |
|
|
|
|
child: _buildGridView(controller.templateItems, controller), |
|
|
|
|
); |
|
|
|
|
}, |
|
|
|
|
child: _buildGridView(controller), |
|
|
|
|
); |
|
|
|
|
}, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
@ -30,11 +31,9 @@ class TemplateGridView extends StatelessWidget {
@@ -30,11 +31,9 @@ class TemplateGridView extends StatelessWidget {
|
|
|
|
|
Offset position, |
|
|
|
|
TemplateParserController controller, |
|
|
|
|
) async { |
|
|
|
|
// 获取渲染对象以正确定位菜单 |
|
|
|
|
final renderBox = context.findRenderObject() as RenderBox; |
|
|
|
|
final localPosition = renderBox.globalToLocal(position); |
|
|
|
|
|
|
|
|
|
// 显示菜单并等待选择结果 |
|
|
|
|
final result = await showMenu<String>( |
|
|
|
|
context: context, |
|
|
|
|
position: RelativeRect.fromLTRB( |
|
|
|
@ -46,7 +45,6 @@ class TemplateGridView extends StatelessWidget {
@@ -46,7 +45,6 @@ class TemplateGridView extends StatelessWidget {
|
|
|
|
|
items: [const PopupMenuItem<String>(value: 'export', child: Text('导出(csv)'))], |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// 处理菜单选择结果 |
|
|
|
|
if (result == 'export' && context.mounted) { |
|
|
|
|
try { |
|
|
|
|
await _exportToCsv(controller); |
|
|
|
@ -56,29 +54,30 @@ class TemplateGridView extends StatelessWidget {
@@ -56,29 +54,30 @@ class TemplateGridView extends StatelessWidget {
|
|
|
|
|
context, |
|
|
|
|
).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Future<void> _exportToCsv(TemplateParserController controller) async { |
|
|
|
|
String csvData = '序号\t'; |
|
|
|
|
final selectedNodes = controller.getSelectedNodes(); |
|
|
|
|
for (var node in selectedNodes) { |
|
|
|
|
csvData += '${node.name}\t'; |
|
|
|
|
} |
|
|
|
|
if (selectedNodes.isEmpty) return; |
|
|
|
|
|
|
|
|
|
// 构建表头 |
|
|
|
|
String csvData = '序号\t'; |
|
|
|
|
csvData += selectedNodes.map((node) => node.name).join('\t'); |
|
|
|
|
csvData += '\n'; |
|
|
|
|
|
|
|
|
|
final filteredItems = |
|
|
|
|
controller.selectedNode != null |
|
|
|
|
? controller.templateItems.where((item) => item.matchesPath(controller.selectedNode!.path)).toList() |
|
|
|
|
: controller.templateItems; |
|
|
|
|
// 获取所有行数据 |
|
|
|
|
final rows = _getGroupedData(controller); |
|
|
|
|
|
|
|
|
|
for (var item in filteredItems) { |
|
|
|
|
csvData += '${filteredItems.indexOf(item) + 1}\t'; |
|
|
|
|
for (var node in selectedNodes) { |
|
|
|
|
// 这里需要根据实际情况填充列内容 |
|
|
|
|
csvData += '\t'; |
|
|
|
|
} |
|
|
|
|
// 填充数据 |
|
|
|
|
for (var i = 0; i < rows.length; i++) { |
|
|
|
|
csvData += '${i + 1}\t'; |
|
|
|
|
csvData += selectedNodes |
|
|
|
|
.map((node) { |
|
|
|
|
return rows[i][node.path] ?? ''; |
|
|
|
|
}) |
|
|
|
|
.join('\t'); |
|
|
|
|
csvData += '\n'; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -90,50 +89,49 @@ class TemplateGridView extends StatelessWidget {
@@ -90,50 +89,49 @@ class TemplateGridView extends StatelessWidget {
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (filePath != null) { |
|
|
|
|
final file = File(filePath); |
|
|
|
|
await file.writeAsString(csvData); |
|
|
|
|
await File(filePath).writeAsString(csvData); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Widget _buildGridView(List<TemplateItem> items, TemplateParserController controller) { |
|
|
|
|
final filteredItems = |
|
|
|
|
controller.selectedNode != null |
|
|
|
|
? items.where((item) => item.matchesPath(controller.selectedNode!.path)).toList() |
|
|
|
|
: items; |
|
|
|
|
|
|
|
|
|
Widget _buildGridView(TemplateParserController controller) { |
|
|
|
|
final selectedNodes = controller.getSelectedNodes(); |
|
|
|
|
|
|
|
|
|
final dataSource = _TemplateItemDataSource( |
|
|
|
|
items: filteredItems, |
|
|
|
|
selectedNodes: selectedNodes, |
|
|
|
|
); |
|
|
|
|
if (selectedNodes.isEmpty) { |
|
|
|
|
return const Center(child: Text('请在左侧树中选择要显示的节点(勾选复选框)')); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
List<GridColumn> columns = [ |
|
|
|
|
GridColumn( |
|
|
|
|
columnName: 'index', |
|
|
|
|
width: 60, |
|
|
|
|
label: Container( |
|
|
|
|
padding: const EdgeInsets.all(8.0), |
|
|
|
|
color: Colors.grey[200], |
|
|
|
|
alignment: Alignment.center, |
|
|
|
|
child: const Text('序号'), |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
]; |
|
|
|
|
// 获取所有需要显示的数据项 |
|
|
|
|
final allItems = controller.templateItems; |
|
|
|
|
|
|
|
|
|
// 构建数据行 - 每个父节点实例为一行 |
|
|
|
|
final rows = _buildDataRows(selectedNodes, allItems); |
|
|
|
|
|
|
|
|
|
final dataSource = _TemplateItemDataSource(rows: rows, selectedNodes: selectedNodes); |
|
|
|
|
|
|
|
|
|
for (var node in selectedNodes) { |
|
|
|
|
columns.add( |
|
|
|
|
GridColumn( |
|
|
|
|
columnName: node.id.toString(), |
|
|
|
|
// 构建列 |
|
|
|
|
final columns = <GridColumn>[ |
|
|
|
|
GridColumn( |
|
|
|
|
columnName: 'index', |
|
|
|
|
width: 60, |
|
|
|
|
label: Container( |
|
|
|
|
padding: const EdgeInsets.all(8.0), |
|
|
|
|
color: Colors.grey[200], |
|
|
|
|
alignment: Alignment.center, |
|
|
|
|
child: const Text('序号'), |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
...selectedNodes.map((node) { |
|
|
|
|
return GridColumn( |
|
|
|
|
columnName: node.path, |
|
|
|
|
label: Container( |
|
|
|
|
padding: const EdgeInsets.all(8.0), |
|
|
|
|
alignment: Alignment.center, |
|
|
|
|
color: Colors.grey[200], |
|
|
|
|
child: Text(node.name), |
|
|
|
|
child: Text(node.isAttribute ? node.name.substring(1) : node.name), |
|
|
|
|
), |
|
|
|
|
), |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
); |
|
|
|
|
}).toList(), |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
return SfDataGrid( |
|
|
|
|
source: dataSource, |
|
|
|
@ -143,28 +141,148 @@ class TemplateGridView extends StatelessWidget {
@@ -143,28 +141,148 @@ class TemplateGridView extends StatelessWidget {
|
|
|
|
|
columnWidthMode: ColumnWidthMode.fill, |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
List<Map<String, dynamic>> _buildDataRows( |
|
|
|
|
List<TemplateNode> selectedNodes, |
|
|
|
|
List<TemplateItem> allItems, |
|
|
|
|
) { |
|
|
|
|
// 1. 为每个选中的节点路径收集所有匹配项,并记录它们的原始顺序 |
|
|
|
|
final nodeValueGroups = <String, List<MapEntry<int, String>>>{}; |
|
|
|
|
|
|
|
|
|
for (final node in selectedNodes) { |
|
|
|
|
// 获取该节点路径对应的所有值,并保留它们的原始索引 |
|
|
|
|
final valuesWithIndex = |
|
|
|
|
allItems |
|
|
|
|
.asMap() |
|
|
|
|
.entries |
|
|
|
|
.where((entry) => entry.value.xPath == node.path) |
|
|
|
|
.map((entry) => MapEntry(entry.key, entry.value.value)) |
|
|
|
|
.toList(); |
|
|
|
|
|
|
|
|
|
nodeValueGroups[node.path] = valuesWithIndex; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 2. 确定最大行数(以最多数据的列为准) |
|
|
|
|
final maxRows = nodeValueGroups.values.fold( |
|
|
|
|
0, |
|
|
|
|
(max, group) => group.length > max ? group.length : max, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// 3. 构建结果行列表 |
|
|
|
|
final rows = <Map<String, dynamic>>[]; |
|
|
|
|
|
|
|
|
|
for (var rowIndex = 0; rowIndex < maxRows; rowIndex++) { |
|
|
|
|
final row = <String, dynamic>{'_index': rowIndex + 1}; |
|
|
|
|
|
|
|
|
|
// 为每个选中的节点添加当前行的值 |
|
|
|
|
for (final node in selectedNodes) { |
|
|
|
|
final values = nodeValueGroups[node.path]!; |
|
|
|
|
row[node.path] = rowIndex < values.length ? values[rowIndex].value : ''; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
rows.add(row); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return rows; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
List<String> _getInstancesForParent(String parentPath, List<TemplateItem> items) { |
|
|
|
|
// 获取该父路径下的所有唯一实例路径 |
|
|
|
|
return items |
|
|
|
|
.where((item) => item.xPath.startsWith(parentPath)) |
|
|
|
|
.map((item) { |
|
|
|
|
// 对于重复节点,提取实例标识部分 |
|
|
|
|
if (parentPath.contains('[')) { |
|
|
|
|
return item.xPath.substring(0, item.xPath.indexOf('/', parentPath.length)); |
|
|
|
|
} |
|
|
|
|
return parentPath; |
|
|
|
|
}) |
|
|
|
|
.toSet() |
|
|
|
|
.toList(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 辅助方法:根据父路径分组数据 |
|
|
|
|
List<Map<String, String>> _getGroupedData(TemplateParserController controller) { |
|
|
|
|
final selectedNodes = controller.getSelectedNodes(); |
|
|
|
|
if (selectedNodes.isEmpty) return []; |
|
|
|
|
|
|
|
|
|
// 获取所有相关数据项 |
|
|
|
|
final allItems = |
|
|
|
|
controller.templateItems.where((item) { |
|
|
|
|
return selectedNodes.any((node) => item.xPath == node.path); |
|
|
|
|
}).toList(); |
|
|
|
|
|
|
|
|
|
// 按父路径分组 |
|
|
|
|
final parentPaths = |
|
|
|
|
selectedNodes.map((node) { |
|
|
|
|
return node.path.substring(0, node.path.lastIndexOf('/')); |
|
|
|
|
}).toSet(); |
|
|
|
|
|
|
|
|
|
final groupedData = <Map<String, String>>[]; |
|
|
|
|
|
|
|
|
|
for (final parentPath in parentPaths) { |
|
|
|
|
// 获取该父路径下的所有项 |
|
|
|
|
final items = |
|
|
|
|
allItems.where((item) { |
|
|
|
|
return item.xPath.startsWith(parentPath) || |
|
|
|
|
item.xPath == parentPath || |
|
|
|
|
(item.xPath.contains('@') && |
|
|
|
|
item.xPath.substring(0, item.xPath.lastIndexOf('@')) == parentPath); |
|
|
|
|
}).toList(); |
|
|
|
|
|
|
|
|
|
// 按实例分组(对于重复节点) |
|
|
|
|
final instanceGroups = <String, Map<String, String>>{}; |
|
|
|
|
|
|
|
|
|
for (final item in items) { |
|
|
|
|
// 提取实例标识(如对于重复节点) |
|
|
|
|
final instanceId = _getInstanceId(item.xPath, parentPath); |
|
|
|
|
|
|
|
|
|
if (!instanceGroups.containsKey(instanceId)) { |
|
|
|
|
instanceGroups[instanceId] = {'_parent': parentPath}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
instanceGroups[instanceId]![item.xPath] = item.value; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
groupedData.addAll(instanceGroups.values); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return groupedData; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
String _getInstanceId(String fullPath, String parentPath) { |
|
|
|
|
// 简单实现:使用父路径后的部分作为实例ID |
|
|
|
|
return fullPath.substring(parentPath.length); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class _TemplateItemDataSource extends DataGridSource { |
|
|
|
|
final List<TemplateItem> items; |
|
|
|
|
final List<Map<String, dynamic>> _rows; |
|
|
|
|
final List<TemplateNode> selectedNodes; |
|
|
|
|
|
|
|
|
|
_TemplateItemDataSource({required this.items, required this.selectedNodes}); |
|
|
|
|
_TemplateItemDataSource({required List<Map<String, dynamic>> rows, required this.selectedNodes}) |
|
|
|
|
: _rows = rows; |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
List<DataGridRow> get rows => |
|
|
|
|
items.map((item) { |
|
|
|
|
List<DataGridCell> cells = [ |
|
|
|
|
DataGridCell<int>(columnName: 'index', value: items.indexOf(item) + 1), |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
for (var node in selectedNodes) { |
|
|
|
|
// 这里需要根据实际情况填充列内容 |
|
|
|
|
cells.add(DataGridCell<String>(columnName: node.id.toString(), value: '')); |
|
|
|
|
} |
|
|
|
|
List<DataGridRow> get rows { |
|
|
|
|
// print("[DEBUG] 原始可加载记录数:${_rows.length}"); |
|
|
|
|
return _rows.asMap().entries.map((entry) { |
|
|
|
|
final index = entry.key; |
|
|
|
|
final rowData = entry.value; |
|
|
|
|
|
|
|
|
|
return DataGridRow(cells: cells); |
|
|
|
|
}).toList(); |
|
|
|
|
return DataGridRow( |
|
|
|
|
cells: [ |
|
|
|
|
DataGridCell<int>(columnName: 'index', value: index + 1), |
|
|
|
|
...selectedNodes.map((node) { |
|
|
|
|
return DataGridCell<String>( |
|
|
|
|
columnName: node.path, |
|
|
|
|
value: rowData[node.path]?.toString() ?? '', |
|
|
|
|
); |
|
|
|
|
}).toList(), |
|
|
|
|
], |
|
|
|
|
); |
|
|
|
|
}).toList(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
DataGridRowAdapter? buildRow(DataGridRow row) { |
|
|
|
|