16 changed files with 462 additions and 14 deletions
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
import 'package:win_text_editor/shared/base/base_content_controller.dart'; |
||||
import 'grid_view_controller.dart'; |
||||
|
||||
class DataFormatController extends BaseContentController { |
||||
final GridViewController gridController; |
||||
|
||||
//---------------初始化方法---- |
||||
|
||||
DataFormatController() : gridController = GridViewController() { |
||||
_setupCrossControllerCommunication(); |
||||
} |
||||
|
||||
//设置跨控制器状态协同 |
||||
void _setupCrossControllerCommunication() {} |
||||
|
||||
//----------------业务入口方法----- |
||||
|
||||
//--------------------私有方法--------- |
||||
|
||||
//-----------框架回调-- |
||||
@override |
||||
void onOpenFile(String filePath) {} |
||||
|
||||
@override |
||||
void onOpenFolder(String folderPath) { |
||||
// 不支持打开文件夹 |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
gridController.dispose(); |
||||
super.dispose(); |
||||
} |
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
// grid_view_controller.dart |
||||
import 'package:win_text_editor/shared/base/safe_notifier.dart'; |
||||
import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; |
||||
|
||||
class GridViewController extends SafeNotifier { |
||||
List<TemplateItem> _templateItems = []; |
||||
List<TemplateItem> _filteredItems = []; |
||||
bool _isFilterApplied = false; |
||||
|
||||
List<TemplateItem> get displayedItems => _isFilterApplied ? _filteredItems : _templateItems; |
||||
bool get isFilterApplied => _isFilterApplied; |
||||
List<TemplateItem> get templateItems => _templateItems; |
||||
|
||||
// 新增方法:更新节点引用 |
||||
List<TemplateNode>? _currentTreeNodes; |
||||
void updateTreeNodesRef(List<TemplateNode> nodes) { |
||||
_currentTreeNodes = nodes; |
||||
safeNotify(); |
||||
} |
||||
|
||||
List<TemplateNode> getSelectedNodes() { |
||||
if (_currentTreeNodes == null) return []; |
||||
|
||||
List<TemplateNode> selectedNodes = []; |
||||
void traverse(TemplateNode node) { |
||||
if (node.isChecked) selectedNodes.add(node); |
||||
for (var child in node.children) { |
||||
traverse(child); |
||||
} |
||||
} |
||||
|
||||
for (var node in _currentTreeNodes!) { |
||||
traverse(node); |
||||
} |
||||
return selectedNodes; |
||||
} |
||||
|
||||
void updateTemplateItems(List<TemplateItem> items) { |
||||
_templateItems = items; |
||||
safeNotify(); |
||||
} |
||||
|
||||
void applyFilter(List<TemplateItem> filteredItems) { |
||||
_filteredItems = filteredItems; |
||||
_isFilterApplied = true; |
||||
safeNotify(); |
||||
} |
||||
|
||||
void clearFilter() { |
||||
_isFilterApplied = false; |
||||
safeNotify(); |
||||
} |
||||
} |
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:win_text_editor/shared/components/tree_view.dart'; |
||||
|
||||
class TemplateNode implements TreeNode { |
||||
@override |
||||
final String name; |
||||
@override |
||||
final List<TemplateNode> children; |
||||
@override |
||||
final int depth; |
||||
@override |
||||
bool isExpanded; |
||||
|
||||
final String path; |
||||
bool isRepeated; |
||||
bool isAttribute; |
||||
int repreatCount; |
||||
bool isChecked; // 新增属性,用于记录节点是否被选中 |
||||
|
||||
TemplateNode({ |
||||
required this.name, |
||||
required this.children, |
||||
required this.depth, |
||||
required this.path, |
||||
this.isExpanded = false, |
||||
this.isRepeated = false, |
||||
this.isAttribute = false, |
||||
this.repreatCount = 1, |
||||
this.isChecked = false, // 初始化默认未选中 |
||||
}); |
||||
|
||||
@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 rowId; |
||||
final String content; |
||||
final String xPath; |
||||
final String value; |
||||
final NodeType nodeType; |
||||
|
||||
TemplateItem({ |
||||
required this.id, |
||||
required this.rowId, |
||||
required this.content, |
||||
required this.xPath, |
||||
required this.value, |
||||
required this.nodeType, |
||||
}); |
||||
|
||||
bool matchesPath(String path) { |
||||
return xPath == path; |
||||
} |
||||
} |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; |
||||
import 'package:win_text_editor/modules/data_format/controllers/data_format_controller.dart'; |
||||
import 'package:win_text_editor/modules/data_format/widgets/grid_view.dart'; |
||||
|
||||
class DataFormatView extends StatefulWidget { |
||||
final String tabId; |
||||
const DataFormatView({super.key, required this.tabId}); |
||||
|
||||
@override |
||||
State<DataFormatView> createState() => _DataFormatViewState(); |
||||
} |
||||
|
||||
class _DataFormatViewState extends State<DataFormatView> { |
||||
late final DataFormatController _controller; |
||||
|
||||
get tabManager => Provider.of<TabItemsController>(context, listen: false); |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
_controller = tabManager.getController(widget.tabId) ?? DataFormatController(); |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
_controller.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return MultiProvider( |
||||
providers: [ |
||||
ChangeNotifierProvider.value(value: _controller), |
||||
ChangeNotifierProvider.value(value: _controller.gridController), |
||||
], |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: Column(children: [const SizedBox(height: 8), Expanded(child: _buildMainContent())]), |
||||
), |
||||
); |
||||
} |
||||
|
||||
Widget _buildMainContent() { |
||||
return Consumer<DataFormatController>( |
||||
builder: (context, controller, _) { |
||||
return const Row( |
||||
children: [SizedBox(width: 8), Expanded(child: Card(child: DataGridView()))], |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,215 @@
@@ -0,0 +1,215 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:syncfusion_flutter_datagrid/datagrid.dart'; |
||||
import 'package:win_text_editor/modules/template_parser/controllers/grid_view_controller.dart'; |
||||
import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; |
||||
import 'package:file_picker/file_picker.dart'; |
||||
import 'dart:io'; |
||||
|
||||
class DataGridView extends StatelessWidget { |
||||
const DataGridView({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Consumer<GridViewController>( |
||||
builder: (context, controller, _) { |
||||
return GestureDetector( |
||||
onSecondaryTapDown: (details) { |
||||
_showContextMenu(context, details.globalPosition, controller); |
||||
}, |
||||
child: _buildGridView(controller), |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
|
||||
Future<void> _showContextMenu( |
||||
BuildContext context, |
||||
Offset position, |
||||
GridViewController controller, |
||||
) async { |
||||
final renderBox = context.findRenderObject() as RenderBox; |
||||
final localPosition = renderBox.globalToLocal(position); |
||||
|
||||
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, |
||||
), |
||||
items: [const PopupMenuItem<String>(value: 'export', child: Text('导出(csv)'))], |
||||
); |
||||
|
||||
if (result == 'export' && context.mounted) { |
||||
try { |
||||
await _exportToCsv(controller); |
||||
} catch (e) { |
||||
if (context.mounted) { |
||||
ScaffoldMessenger.of( |
||||
context, |
||||
).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
Future<void> _exportToCsv(GridViewController controller) async { |
||||
final selectedNodes = controller.getSelectedNodes(); |
||||
if (selectedNodes.isEmpty) return; |
||||
|
||||
// 直接从数据源获取数据 |
||||
final dataSource = _TemplateItemDataSource( |
||||
rows: _buildDataRows(selectedNodes, controller.displayedItems), |
||||
selectedNodes: selectedNodes, |
||||
); |
||||
|
||||
// 构建表头 |
||||
String csvData = '序号\t'; |
||||
csvData += selectedNodes |
||||
.map((node) => node.isAttribute ? node.name.substring(1) : node.name) |
||||
.join('\t'); |
||||
csvData += '\n'; |
||||
|
||||
// 填充数据 |
||||
for (final row in dataSource.rows) { |
||||
csvData += '${row.getCells()[0].value}\t'; // 序号 |
||||
for (int i = 1; i < row.getCells().length; i++) { |
||||
csvData += row.getCells()[i].value.toString(); |
||||
if (i < row.getCells().length - 1) csvData += '\t'; |
||||
} |
||||
csvData += '\n'; |
||||
} |
||||
|
||||
// 保存文件 |
||||
final filePath = await FilePicker.platform.saveFile( |
||||
dialogTitle: '保存导出结果', |
||||
fileName: 'template_results.csv', |
||||
type: FileType.custom, |
||||
allowedExtensions: ['csv'], |
||||
); |
||||
|
||||
if (filePath != null) { |
||||
await File(filePath).writeAsString(csvData); |
||||
} |
||||
} |
||||
|
||||
Widget _buildGridView(GridViewController controller) { |
||||
final selectedNodes = controller.getSelectedNodes(); |
||||
|
||||
if (selectedNodes.isEmpty) { |
||||
return const Center(child: Text('请在左侧树中选择要显示的节点(勾选复选框)')); |
||||
} |
||||
|
||||
// 获取所有需要显示的数据项 |
||||
final allItems = controller.displayedItems; |
||||
|
||||
// 构建数据行 - 每个父节点实例为一行 |
||||
final rows = _buildDataRows(selectedNodes, allItems); |
||||
|
||||
final dataSource = _TemplateItemDataSource(rows: rows, selectedNodes: selectedNodes); |
||||
|
||||
// 构建列 |
||||
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.isAttribute ? node.name.substring(1) : node.name), |
||||
), |
||||
); |
||||
}).toList(), |
||||
]; |
||||
|
||||
return SfDataGrid( |
||||
source: dataSource, |
||||
columns: columns, |
||||
gridLinesVisibility: GridLinesVisibility.both, |
||||
headerGridLinesVisibility: GridLinesVisibility.both, |
||||
columnWidthMode: ColumnWidthMode.fill, |
||||
); |
||||
} |
||||
|
||||
List<Map<String, dynamic>> _buildDataRows( |
||||
List<TemplateNode> selectedNodes, |
||||
List<TemplateItem> allItems, |
||||
) { |
||||
final instanceMap = <String, Map<String, dynamic>>{}; |
||||
|
||||
// 1. 先按实例分组 |
||||
for (final item in allItems) { |
||||
// 2. 只填充选中的列 |
||||
if (selectedNodes.any((n) => n.path == item.xPath)) { |
||||
final instanceId = item.rowId; // 或使用其他分组逻辑 |
||||
instanceMap.putIfAbsent(instanceId, () => {'_index': instanceMap.length + 1}); |
||||
instanceMap[instanceId]![item.xPath] = item.value; |
||||
} |
||||
} |
||||
|
||||
// 3. 确保所有选中列都存在 |
||||
return instanceMap.values.map((row) { |
||||
for (final node in selectedNodes) { |
||||
row.putIfAbsent(node.path, () => row[node.path] ?? ''); |
||||
} |
||||
return row; |
||||
}).toList(); |
||||
} |
||||
} |
||||
|
||||
class _TemplateItemDataSource extends DataGridSource { |
||||
final List<Map<String, dynamic>> _rows; |
||||
final List<TemplateNode> selectedNodes; |
||||
|
||||
_TemplateItemDataSource({required List<Map<String, dynamic>> rows, required this.selectedNodes}) |
||||
: _rows = rows; |
||||
|
||||
@override |
||||
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: [ |
||||
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) { |
||||
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(), |
||||
); |
||||
} |
||||
} |
@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
|
||||
// template_notifier.dart |
||||
import 'package:flutter/foundation.dart'; |
||||
|
||||
abstract class TemplateNotifier extends ChangeNotifier { |
||||
abstract class SafeNotifier extends ChangeNotifier { |
||||
@protected |
||||
void safeNotify() { |
||||
if (hasListeners) notifyListeners(); |
Loading…
Reference in new issue