16 changed files with 462 additions and 14 deletions
@ -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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
// template_notifier.dart |
// template_notifier.dart |
||||||
import 'package:flutter/foundation.dart'; |
import 'package:flutter/foundation.dart'; |
||||||
|
|
||||||
abstract class TemplateNotifier extends ChangeNotifier { |
abstract class SafeNotifier extends ChangeNotifier { |
||||||
@protected |
@protected |
||||||
void safeNotify() { |
void safeNotify() { |
||||||
if (hasListeners) notifyListeners(); |
if (hasListeners) notifyListeners(); |
Loading…
Reference in new issue