From dc6c3e5aa5a6bf33029554851cbfd99222453e2c Mon Sep 17 00:00:00 2001 From: hejl Date: Thu, 22 May 2025 08:45:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E8=B0=83=E6=95=B4=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- win_text_editor/lib/menus/menu_actions.dart | 6 +- .../controllers/grid_view_controller.dart | 48 +-- .../data_format/widgets/data_format_view.dart | 17 +- .../widgets/format_text_panel.dart | 48 +++ .../data_format/widgets/grid_view.dart | 297 ++++++++---------- .../lib/modules/module_router.dart | 4 + .../lib/shared/components/editor_toolbar.dart | 46 ++- .../lib/shared/components/text_editor.dart | 33 +- 8 files changed, 278 insertions(+), 221 deletions(-) create mode 100644 win_text_editor/lib/modules/data_format/widgets/format_text_panel.dart diff --git a/win_text_editor/lib/menus/menu_actions.dart b/win_text_editor/lib/menus/menu_actions.dart index e887bbd..817cbb4 100644 --- a/win_text_editor/lib/menus/menu_actions.dart +++ b/win_text_editor/lib/menus/menu_actions.dart @@ -82,9 +82,7 @@ class MenuActions { final tabManager = Provider.of(context, listen: false); // 使用 firstWhereOrNull 查找选项卡 - final existingTab = tabManager.tabs.firstWhereOrNull( - (tab) => tab.type == RouterKey.templateParser, - ); + final existingTab = tabManager.tabs.firstWhereOrNull((tab) => tab.type == RouterKey.dataFormat); if (existingTab != null) { // 如果存在,激活该选项卡 @@ -95,7 +93,7 @@ class MenuActions { tabId, title: "数据格式化", type: RouterKey.dataFormat, - icon: Icons.auto_awesome_mosaic, + icon: Icons.date_range, content: "", ); } diff --git a/win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart b/win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart index 982e5c5..75b05ea 100644 --- a/win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart +++ b/win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart @@ -1,53 +1,9 @@ // 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 _templateItems = []; - List _filteredItems = []; - bool _isFilterApplied = false; - - List get displayedItems => _isFilterApplied ? _filteredItems : _templateItems; - bool get isFilterApplied => _isFilterApplied; - List get templateItems => _templateItems; - - // 新增方法:更新节点引用 - List? _currentTreeNodes; - void updateTreeNodesRef(List nodes) { - _currentTreeNodes = nodes; - safeNotify(); - } - - List getSelectedNodes() { - if (_currentTreeNodes == null) return []; - - List 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 items) { - _templateItems = items; - safeNotify(); - } - - void applyFilter(List filteredItems) { - _filteredItems = filteredItems; - _isFilterApplied = true; - safeNotify(); - } - - void clearFilter() { - _isFilterApplied = false; + void reset() { + // 重置状态的方法 safeNotify(); } } diff --git a/win_text_editor/lib/modules/data_format/widgets/data_format_view.dart b/win_text_editor/lib/modules/data_format/widgets/data_format_view.dart index 78b350a..a1f59f0 100644 --- a/win_text_editor/lib/modules/data_format/widgets/data_format_view.dart +++ b/win_text_editor/lib/modules/data_format/widgets/data_format_view.dart @@ -3,6 +3,7 @@ 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'; +import 'package:win_text_editor/modules/data_format/widgets/format_text_panel.dart'; class DataFormatView extends StatefulWidget { final String tabId; @@ -47,7 +48,21 @@ class _DataFormatViewState extends State { return Consumer( builder: (context, controller, _) { return const Row( - children: [SizedBox(width: 8), Expanded(child: Card(child: DataGridView()))], + children: [ + // 左侧 GridView (50%宽度) + Expanded( + flex: 1, + child: Padding( + padding: EdgeInsets.only(right: 4.0), + child: Card(child: DataGridView()), + ), + ), + // 右侧 FormatText 面板 (50%宽度) + Expanded( + flex: 1, + child: Padding(padding: EdgeInsets.only(left: 4.0), child: FormatTextPanel()), + ), + ], ); }, ); diff --git a/win_text_editor/lib/modules/data_format/widgets/format_text_panel.dart b/win_text_editor/lib/modules/data_format/widgets/format_text_panel.dart new file mode 100644 index 0000000..75bbd96 --- /dev/null +++ b/win_text_editor/lib/modules/data_format/widgets/format_text_panel.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:win_text_editor/shared/components/editor_toolbar.dart'; +import 'package:win_text_editor/shared/components/text_editor.dart'; + +class FormatTextPanel extends StatelessWidget { + const FormatTextPanel({super.key}); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + children: [ + // 上部模板编辑器 (固定高度200px) + const SizedBox( + height: 200, + child: TextEditor(tabId: 'format_template', title: 'Mustache模板'), + ), + const Divider(height: 1), + // 下部结果编辑器 (扩展高度) + Expanded( + child: TextEditor( + tabId: 'format_result', + title: '转换结果', + toolbarBuilder: + (context, state) => EditorToolbar( + title: '转换结果', + text: state.currentText, + isLoading: state.isLoading, + showOpenFileButton: false, // 隐藏打开文件按钮 + customButtons: [ + ToolbarButtonConfig( + icon: Icons.code, + tooltip: '格式化', + onPressed: () => _applyFormat(state.currentText), + ), + ], + onCopyToClipboard: state.copyToClipboard, + onSaveFile: state.saveFile, + ), + ), + ), + ], + ), + ); + } + + void _applyFormat(String currentText) {} +} diff --git a/win_text_editor/lib/modules/data_format/widgets/grid_view.dart b/win_text_editor/lib/modules/data_format/widgets/grid_view.dart index e79c575..04c5946 100644 --- a/win_text_editor/lib/modules/data_format/widgets/grid_view.dart +++ b/win_text_editor/lib/modules/data_format/widgets/grid_view.dart @@ -1,199 +1,177 @@ 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'; +import 'package:csv/csv.dart'; -class DataGridView extends StatelessWidget { +class DataGridView extends StatefulWidget { const DataGridView({super.key}); + @override + State createState() => _DataGridViewState(); +} + +class _DataGridViewState extends State { + final TextEditingController _filePathController = TextEditingController(); + List> _csvData = []; + String? _delimiter; + final ScrollController _horizontalScrollController = ScrollController(); + + @override + void dispose() { + _filePathController.dispose(); + _horizontalScrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return Consumer( - builder: (context, controller, _) { - return GestureDetector( - onSecondaryTapDown: (details) { - _showContextMenu(context, details.globalPosition, controller); - }, - child: _buildGridView(controller), - ); - }, + return Column( + children: [ + // 文件选择区域 + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _filePathController, + decoration: const InputDecoration( + labelText: 'CSV文件路径', + border: OutlineInputBorder(), + ), + readOnly: true, + ), + ), + const SizedBox(width: 8), + ElevatedButton(onPressed: _pickAndLoadCsvFile, child: const Text('选择文件')), + ], + ), + ), + // 数据表格区域 + Expanded( + child: + _csvData.isEmpty ? const Center(child: Text('请选择CSV文件')) : _buildScrollableDataGrid(), + ), + ], ); } - Future _showContextMenu( - BuildContext context, - Offset position, - GridViewController controller, - ) async { - final renderBox = context.findRenderObject() as RenderBox; - final localPosition = renderBox.globalToLocal(position); - - final result = await showMenu( - context: context, - position: RelativeRect.fromLTRB( - position.dx, - position.dy, - position.dx + renderBox.size.width - localPosition.dx, - position.dy + renderBox.size.height - localPosition.dy, + Widget _buildScrollableDataGrid() { + return Align( + // 添加 Align 组件使整个表格左对齐 + alignment: Alignment.topLeft, // 设置为左上对齐 + child: Scrollbar( + controller: _horizontalScrollController, + thumbVisibility: true, + trackVisibility: true, + child: SingleChildScrollView( + controller: _horizontalScrollController, + scrollDirection: Axis.horizontal, + child: SizedBox(width: _calculateTableWidth(), child: _buildCsvDataGrid()), + ), ), - items: [const PopupMenuItem(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 _exportToCsv(GridViewController controller) async { - final selectedNodes = controller.getSelectedNodes(); - if (selectedNodes.isEmpty) return; + double _calculateTableWidth() { + if (_csvData.isEmpty || _csvData.length < 2) return 0; + final columnCount = _csvData.first.length; + return columnCount * 150.0; // 每列宽度150 + } - // 直接从数据源获取数据 - final dataSource = _TemplateItemDataSource( - rows: _buildDataRows(selectedNodes, controller.displayedItems), - selectedNodes: selectedNodes, - ); + Future _pickAndLoadCsvFile() async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['csv'], + ); - // 构建表头 - 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'; + if (result != null) { + final file = File(result.files.single.path!); + _filePathController.text = file.path; + final content = await file.readAsString(); + + // 检测分隔符(第一行是标题行) + final firstLine = content.split('\n').first.trim(); + _delimiter = firstLine.contains('\t') ? '\t' : ','; + + // 解析CSV - 使用更健壮的解析方式 + final csvTable = const CsvToListConverter( + shouldParseNumbers: false, + allowInvalid: false, + eol: '\n', + ).convert(content, fieldDelimiter: _delimiter); + + // 清理数据 + final cleanedData = + csvTable + .where( + (row) => row.isNotEmpty && row.any((cell) => cell.toString().trim().isNotEmpty), + ) + .map((row) => row.map((cell) => cell.toString().trim()).toList()) + .toList(); + + setState(() { + _csvData = cleanedData; + }); } - 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); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('加载CSV文件失败: $e'))); } } - Widget _buildGridView(GridViewController controller) { - final selectedNodes = controller.getSelectedNodes(); - - if (selectedNodes.isEmpty) { - return const Center(child: Text('请在左侧树中选择要显示的节点(勾选复选框)')); + Widget _buildCsvDataGrid() { + if (_csvData.isEmpty || _csvData.length < 2) { + return const Center(child: Text('没有有效数据或数据格式不正确')); } - // 获取所有需要显示的数据项 - final allItems = controller.displayedItems; + final headers = _csvData.first; + final dataRows = _csvData.sublist(1); - // 构建数据行 - 每个父节点实例为一行 - final rows = _buildDataRows(selectedNodes, allItems); - - final dataSource = _TemplateItemDataSource(rows: rows, selectedNodes: selectedNodes); + final columns = + headers.map((header) { + return GridColumn( + columnName: header.toString(), + width: 150, // 固定列宽 + label: Container( + padding: const EdgeInsets.all(8.0), + color: Colors.grey[200], + alignment: Alignment.center, + child: Text(header.toString(), overflow: TextOverflow.ellipsis, maxLines: 1), + ), + ); + }).toList(); - // 构建列 - final columns = [ - 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(), - ]; + final dataSource = _CsvDataSource(headers: headers, rows: dataRows); return SfDataGrid( source: dataSource, columns: columns, gridLinesVisibility: GridLinesVisibility.both, headerGridLinesVisibility: GridLinesVisibility.both, - columnWidthMode: ColumnWidthMode.fill, + columnWidthMode: ColumnWidthMode.fitByCellValue, // 使用固定列宽模式 ); } - - List> _buildDataRows( - List selectedNodes, - List allItems, - ) { - final instanceMap = >{}; - - // 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> _rows; - final List selectedNodes; +class _CsvDataSource extends DataGridSource { + final List headers; + final List> _rows; - _TemplateItemDataSource({required List> rows, required this.selectedNodes}) - : _rows = rows; + _CsvDataSource({required this.headers, required List> rows}) : _rows = rows; @override List get rows { - // print("[DEBUG] 原始可加载记录数:${_rows.length}"); - return _rows.asMap().entries.map((entry) { - final index = entry.key; - final rowData = entry.value; - + return _rows.map((row) { return DataGridRow( - cells: [ - DataGridCell(columnName: 'index', value: index + 1), - ...selectedNodes.map((node) { - return DataGridCell( - columnName: node.path, - value: rowData[node.path]?.toString() ?? '', - ); - }).toList(), - ], + cells: + headers.asMap().entries.map((entry) { + final columnIndex = entry.key; + final columnName = entry.value.toString(); + final cellValue = columnIndex < row.length ? row[columnIndex].toString() : ''; + return DataGridCell(columnName: columnName, value: cellValue); + }).toList(), ); }).toList(); } @@ -205,9 +183,12 @@ class _TemplateItemDataSource extends DataGridSource { row.getCells().map((dataGridCell) { return Container( padding: const EdgeInsets.all(8.0), - alignment: - dataGridCell.columnName == 'index' ? Alignment.center : Alignment.centerLeft, - child: Text(dataGridCell.value.toString()), + alignment: Alignment.centerLeft, + child: Text( + dataGridCell.value.toString(), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), ); }).toList(), ); diff --git a/win_text_editor/lib/modules/module_router.dart b/win_text_editor/lib/modules/module_router.dart index 078bd83..1304d7c 100644 --- a/win_text_editor/lib/modules/module_router.dart +++ b/win_text_editor/lib/modules/module_router.dart @@ -1,6 +1,8 @@ // modules/tab_content_registry.dart import 'package:flutter/material.dart'; import 'package:win_text_editor/framework/models/tab_model.dart'; +import 'package:win_text_editor/modules/data_format/controllers/data_format_controller.dart'; +import 'package:win_text_editor/modules/data_format/widgets/data_format_view.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart'; import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart'; import 'package:win_text_editor/modules/template_parser/controllers/template_parser_controller.dart'; @@ -19,12 +21,14 @@ class ModuleRouter { static final Map _controllerCreators = { RouterKey.contentSearch: (tab) => ContentSearchController(), RouterKey.templateParser: (tab) => TemplateParserController(), + RouterKey.dataFormat: (tab) => DataFormatController(), }; // 映射UI组件 static final Map _widgetBuilders = { RouterKey.contentSearch: (tab, controller) => ContentSearchView(tabId: tab.id), RouterKey.templateParser: (tab, controller) => TemplateParserView(tabId: tab.id), + RouterKey.dataFormat: (tab, controller) => DataFormatView(tabId: tab.id), }; static BaseContentController? createControllerForTab(AppTab tab) { diff --git a/win_text_editor/lib/shared/components/editor_toolbar.dart b/win_text_editor/lib/shared/components/editor_toolbar.dart index 6a8a7cd..3109a21 100644 --- a/win_text_editor/lib/shared/components/editor_toolbar.dart +++ b/win_text_editor/lib/shared/components/editor_toolbar.dart @@ -1,21 +1,44 @@ import 'package:flutter/material.dart'; +/// 工具栏按钮配置 +class ToolbarButtonConfig { + final IconData icon; + final String tooltip; + final bool isEnabled; + final VoidCallback? onPressed; + + const ToolbarButtonConfig({ + required this.icon, + required this.tooltip, + this.isEnabled = true, + this.onPressed, + }); +} + class EditorToolbar extends StatelessWidget { final String title; final String text; final bool isLoading; - final VoidCallback onOpenFile; - final VoidCallback onCopyToClipboard; - final VoidCallback onSaveFile; + final bool showOpenFileButton; + final bool showCopyButton; + final bool showSaveButton; + final List customButtons; + final VoidCallback? onOpenFile; + final VoidCallback? onCopyToClipboard; + final VoidCallback? onSaveFile; const EditorToolbar({ super.key, required this.title, required this.text, - required this.isLoading, - required this.onOpenFile, - required this.onCopyToClipboard, - required this.onSaveFile, + this.isLoading = false, + this.showOpenFileButton = true, + this.showCopyButton = true, + this.showSaveButton = true, + this.customButtons = const [], + this.onOpenFile, + this.onCopyToClipboard, + this.onSaveFile, }); @override @@ -37,21 +60,30 @@ class EditorToolbar extends StatelessWidget { Widget _buildActionButtons(BuildContext context) { return Row( children: [ + if (showOpenFileButton) IconButton( icon: const Icon(Icons.folder_open, size: 20), tooltip: '打开文件', onPressed: isLoading ? null : onOpenFile, ), + if (showCopyButton) IconButton( icon: const Icon(Icons.content_copy, size: 20), tooltip: '复制内容', onPressed: text.isEmpty ? null : onCopyToClipboard, ), + if (showSaveButton) IconButton( icon: const Icon(Icons.save, size: 20), tooltip: '保存到文件', onPressed: text.isEmpty ? null : onSaveFile, ), + // 添加自定义按钮 + ...customButtons.map((button) => IconButton( + icon: Icon(button.icon, size: 20), + tooltip: button.tooltip, + onPressed: button.isEnabled ? button.onPressed : null, + )), if (isLoading) const Padding( padding: EdgeInsets.only(left: 8), diff --git a/win_text_editor/lib/shared/components/text_editor.dart b/win_text_editor/lib/shared/components/text_editor.dart index eed6cbd..e29f7d0 100644 --- a/win_text_editor/lib/shared/components/text_editor.dart +++ b/win_text_editor/lib/shared/components/text_editor.dart @@ -6,13 +6,14 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'editor_toolbar.dart'; // 添加这行导入 +import 'editor_toolbar.dart'; class TextEditor extends StatefulWidget { final String tabId; final String? initialContent; final String title; final ValueChanged? onContentChanged; + final Widget Function(BuildContext, TextEditorState)? toolbarBuilder; // 新增:外部工具栏构建器 const TextEditor({ super.key, @@ -20,6 +21,7 @@ class TextEditor extends StatefulWidget { this.initialContent, this.title = '未命名', this.onContentChanged, + this.toolbarBuilder, // 新增:可选参数 }); @override @@ -33,6 +35,21 @@ class TextEditorState extends State with AutomaticKeepAliveClientMix String _lastSyncedText = ''; Timer? _debounceTimer; + // 暴露给外部使用的公共方法 + String get currentText => _textController.text; + set currentText(String text) { + _textController.text = text; + _lastSyncedText = text; + } + + bool get isLoading => _isLoading; + FocusNode get focusNode => _focusNode; + + // 暴露给外部使用的操作函数 + Future openFile() => _openFile(context); + Future copyToClipboard() => _copyToClipboard(context); + Future saveFile() => _saveFile(context); + @override bool get wantKeepAlive => true; @@ -82,16 +99,22 @@ class TextEditorState extends State with AutomaticKeepAliveClientMix super.build(context); return Column( children: [ - EditorToolbar( + // 使用外部传入的工具栏构建器或默认实现 + widget.toolbarBuilder?.call(context, this) ?? _buildDefaultToolbar(), + Expanded(child: _buildEditorField(context)), + ], + ); + } + + // 默认工具栏实现 + Widget _buildDefaultToolbar() { + return EditorToolbar( title: widget.title, text: _textController.text, isLoading: _isLoading, onOpenFile: () => _openFile(context), onCopyToClipboard: () => _copyToClipboard(context), onSaveFile: () => _saveFile(context), - ), - Expanded(child: _buildEditorField(context)), - ], ); }