Browse Source

样式调整完成

master
hejl 2 months ago
parent
commit
dc6c3e5aa5
  1. 6
      win_text_editor/lib/menus/menu_actions.dart
  2. 48
      win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart
  3. 17
      win_text_editor/lib/modules/data_format/widgets/data_format_view.dart
  4. 48
      win_text_editor/lib/modules/data_format/widgets/format_text_panel.dart
  5. 297
      win_text_editor/lib/modules/data_format/widgets/grid_view.dart
  6. 4
      win_text_editor/lib/modules/module_router.dart
  7. 46
      win_text_editor/lib/shared/components/editor_toolbar.dart
  8. 33
      win_text_editor/lib/shared/components/text_editor.dart

6
win_text_editor/lib/menus/menu_actions.dart

@ -82,9 +82,7 @@ class MenuActions { @@ -82,9 +82,7 @@ class MenuActions {
final tabManager = Provider.of<TabItemsController>(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 { @@ -95,7 +93,7 @@ class MenuActions {
tabId,
title: "数据格式化",
type: RouterKey.dataFormat,
icon: Icons.auto_awesome_mosaic,
icon: Icons.date_range,
content: "",
);
}

48
win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart

@ -1,53 +1,9 @@ @@ -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<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;
void reset() {
//
safeNotify();
}
}

17
win_text_editor/lib/modules/data_format/widgets/data_format_view.dart

@ -3,6 +3,7 @@ import 'package:provider/provider.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<DataFormatView> { @@ -47,7 +48,21 @@ class _DataFormatViewState extends State<DataFormatView> {
return Consumer<DataFormatController>(
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()),
),
],
);
},
);

48
win_text_editor/lib/modules/data_format/widgets/format_text_panel.dart

@ -0,0 +1,48 @@ @@ -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) {}
}

297
win_text_editor/lib/modules/data_format/widgets/grid_view.dart

@ -1,199 +1,177 @@ @@ -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<DataGridView> createState() => _DataGridViewState();
}
class _DataGridViewState extends State<DataGridView> {
final TextEditingController _filePathController = TextEditingController();
List<List<dynamic>> _csvData = [];
String? _delimiter;
final ScrollController _horizontalScrollController = ScrollController();
@override
void dispose() {
_filePathController.dispose();
_horizontalScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Consumer<GridViewController>(
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<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,
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<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;
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<void> _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<GridColumn>((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>[
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<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;
class _CsvDataSource extends DataGridSource {
final List<dynamic> headers;
final List<List<dynamic>> _rows;
_TemplateItemDataSource({required List<Map<String, dynamic>> rows, required this.selectedNodes})
: _rows = rows;
_CsvDataSource({required this.headers, required List<List<dynamic>> rows}) : _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 _rows.map((row) {
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(),
],
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 { @@ -205,9 +183,12 @@ class _TemplateItemDataSource extends DataGridSource {
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()),
alignment: Alignment.centerLeft,
child: Text(
dataGridCell.value.toString(),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
);
}).toList(),
);

4
win_text_editor/lib/modules/module_router.dart

@ -1,6 +1,8 @@ @@ -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 { @@ -19,12 +21,14 @@ class ModuleRouter {
static final Map<String, ContentControllerCreator> _controllerCreators = {
RouterKey.contentSearch: (tab) => ContentSearchController(),
RouterKey.templateParser: (tab) => TemplateParserController(),
RouterKey.dataFormat: (tab) => DataFormatController(),
};
// UI组件
static final Map<String, ContentWidgetBuilder> _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) {

46
win_text_editor/lib/shared/components/editor_toolbar.dart

@ -1,21 +1,44 @@ @@ -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<ToolbarButtonConfig> 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 { @@ -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),

33
win_text_editor/lib/shared/components/text_editor.dart

@ -6,13 +6,14 @@ import 'package:file_picker/file_picker.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<String>? onContentChanged;
final Widget Function(BuildContext, TextEditorState)? toolbarBuilder; //
const TextEditor({
super.key,
@ -20,6 +21,7 @@ class TextEditor extends StatefulWidget { @@ -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<TextEditor> with AutomaticKeepAliveClientMix @@ -33,6 +35,21 @@ class TextEditorState extends State<TextEditor> 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<void> openFile() => _openFile(context);
Future<void> copyToClipboard() => _copyToClipboard(context);
Future<void> saveFile() => _saveFile(context);
@override
bool get wantKeepAlive => true;
@ -82,16 +99,22 @@ class TextEditorState extends State<TextEditor> with AutomaticKeepAliveClientMix @@ -82,16 +99,22 @@ class TextEditorState extends State<TextEditor> 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)),
],
);
}

Loading…
Cancel
Save