Browse Source

增加数据格式化菜单及初始页面,重命名template_notifier为safe_notifier

master
hejl 2 months ago
parent
commit
8ecf715f79
  1. 2
      win_text_editor/lib/framework/widgets/tab_view.dart
  2. 3
      win_text_editor/lib/menus/app_menu.dart
  3. 28
      win_text_editor/lib/menus/menu_actions.dart
  4. 1
      win_text_editor/lib/menus/menu_constants.dart
  5. 34
      win_text_editor/lib/modules/data_format/controllers/data_format_controller.dart
  6. 53
      win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart
  7. 64
      win_text_editor/lib/modules/data_format/models/template_node.dart
  8. 55
      win_text_editor/lib/modules/data_format/widgets/data_format_view.dart
  9. 215
      win_text_editor/lib/modules/data_format/widgets/grid_view.dart
  10. 1
      win_text_editor/lib/modules/module_router.dart
  11. 4
      win_text_editor/lib/modules/template_parser/controllers/filter_controller.dart
  12. 4
      win_text_editor/lib/modules/template_parser/controllers/grid_view_controller.dart
  13. 4
      win_text_editor/lib/modules/template_parser/controllers/tree_view_controller.dart
  14. 4
      win_text_editor/lib/modules/template_parser/widgets/grid_view.dart
  15. 2
      win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart
  16. 2
      win_text_editor/lib/shared/base/safe_notifier.dart

2
win_text_editor/lib/framework/widgets/tab_view.dart

@ -42,7 +42,7 @@ class _TabViewState extends State<TabView> {
Widget _buildTabContent() { Widget _buildTabContent() {
final tabManager = Provider.of<TabItemsController>(context, listen: false); final tabManager = Provider.of<TabItemsController>(context, listen: false);
final activeIndex = widget.tabs.indexWhere((t) => t.id == widget.currentTabId); final activeIndex = widget.tabs.indexWhere((t) => t.id == widget.currentTabId);
if (activeIndex == -1) return const Center(child: Text('无活动标签页')); if (activeIndex == -1) return const Center(child: Text('欢迎光临,敬请指导!'));
return IndexedStack( return IndexedStack(
index: activeIndex, index: activeIndex,

3
win_text_editor/lib/menus/app_menu.dart

@ -25,8 +25,9 @@ class AppMenu extends StatelessWidget {
List<PopupMenuEntry<String>> _buildToolsMenuItems() { List<PopupMenuEntry<String>> _buildToolsMenuItems() {
return [ return [
const PopupMenuItem<String>(value: MenuConstants.templateParser, child: Text('XML解析')),
const PopupMenuItem<String>(value: MenuConstants.contentSearch, child: Text('内容搜索')), const PopupMenuItem<String>(value: MenuConstants.contentSearch, child: Text('内容搜索')),
const PopupMenuItem<String>(value: MenuConstants.templateParser, child: Text('模板解析')), const PopupMenuItem<String>(value: MenuConstants.dataFormat, child: Text('格式化')),
]; ];
} }

28
win_text_editor/lib/menus/menu_actions.dart

@ -21,6 +21,9 @@ class MenuActions {
case MenuConstants.templateParser: case MenuConstants.templateParser:
await _openTemplateParser(context); await _openTemplateParser(context);
break; break;
case MenuConstants.dataFormat:
await _dataFormat(context);
break;
case MenuConstants.exit: case MenuConstants.exit:
_exitApplication(); _exitApplication();
break; break;
@ -67,7 +70,7 @@ class MenuActions {
final tabId = DateTime.now().millisecondsSinceEpoch.toString(); final tabId = DateTime.now().millisecondsSinceEpoch.toString();
await tabManager.addTab( await tabManager.addTab(
tabId, tabId,
title: "模板解析", title: "XML解析",
type: RouterKey.templateParser, type: RouterKey.templateParser,
icon: Icons.auto_awesome_mosaic, icon: Icons.auto_awesome_mosaic,
content: "", content: "",
@ -75,6 +78,29 @@ class MenuActions {
} }
} }
static Future<void> _dataFormat(BuildContext context) async {
final tabManager = Provider.of<TabItemsController>(context, listen: false);
// 使 firstWhereOrNull
final existingTab = tabManager.tabs.firstWhereOrNull(
(tab) => tab.type == RouterKey.templateParser,
);
if (existingTab != null) {
//
tabManager.setActiveTab(existingTab.id);
} else {
final tabId = DateTime.now().millisecondsSinceEpoch.toString();
await tabManager.addTab(
tabId,
title: "数据格式化",
type: RouterKey.dataFormat,
icon: Icons.auto_awesome_mosaic,
content: "",
);
}
}
static void _exitApplication() { static void _exitApplication() {
exit(0); exit(0);
} }

1
win_text_editor/lib/menus/menu_constants.dart

@ -15,6 +15,7 @@ class MenuConstants {
// //
static const String contentSearch = "content_search"; static const String contentSearch = "content_search";
static const String templateParser = 'template_parser'; static const String templateParser = 'template_parser';
static const String dataFormat = 'data_format';
// //
static const String undo = 'undo'; static const String undo = 'undo';

34
win_text_editor/lib/modules/data_format/controllers/data_format_controller.dart

@ -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();
}
}

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

@ -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();
}
}

64
win_text_editor/lib/modules/data_format/models/template_node.dart

@ -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;
}
}

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

@ -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()))],
);
},
);
}
}

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

@ -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
win_text_editor/lib/modules/module_router.dart

@ -10,6 +10,7 @@ import 'package:win_text_editor/modules/template_parser/widgets/template_parser_
class RouterKey { class RouterKey {
static const String contentSearch = 'content_search'; static const String contentSearch = 'content_search';
static const String templateParser = 'template_parser'; static const String templateParser = 'template_parser';
static const String dataFormat = 'data_format';
static const String textEditor = 'text_editor'; static const String textEditor = 'text_editor';
} }

4
win_text_editor/lib/modules/template_parser/controllers/filter_controller.dart

@ -2,9 +2,9 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; import 'package:win_text_editor/modules/template_parser/models/template_node.dart';
import 'template_notifier.dart'; import '../../../shared/base/safe_notifier.dart';
class FilterController extends TemplateNotifier { class FilterController extends SafeNotifier {
String? _selectedFilterField; String? _selectedFilterField;
String? _selectedFilterOperator; String? _selectedFilterOperator;
String _filterValue = ''; String _filterValue = '';

4
win_text_editor/lib/modules/template_parser/controllers/grid_view_controller.dart

@ -1,8 +1,8 @@
// grid_view_controller.dart // grid_view_controller.dart
import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; import 'package:win_text_editor/modules/template_parser/models/template_node.dart';
import 'template_notifier.dart'; import '../../../shared/base/safe_notifier.dart';
class GridViewController extends TemplateNotifier { class GridViewController extends SafeNotifier {
List<TemplateItem> _templateItems = []; List<TemplateItem> _templateItems = [];
List<TemplateItem> _filteredItems = []; List<TemplateItem> _filteredItems = [];
bool _isFilterApplied = false; bool _isFilterApplied = false;

4
win_text_editor/lib/modules/template_parser/controllers/tree_view_controller.dart

@ -1,9 +1,9 @@
// tree_view_controller.dart // tree_view_controller.dart
import 'package:win_text_editor/modules/template_parser/models/template_node.dart'; import 'package:win_text_editor/modules/template_parser/models/template_node.dart';
import 'template_notifier.dart'; import '../../../shared/base/safe_notifier.dart';
class TreeViewController extends TemplateNotifier { class TreeViewController extends SafeNotifier {
// //
List<TemplateNode> _treeNodes = []; List<TemplateNode> _treeNodes = [];
TemplateNode? _selectedNode; TemplateNode? _selectedNode;

4
win_text_editor/lib/modules/template_parser/widgets/grid_view.dart

@ -7,9 +7,7 @@ import 'package:file_picker/file_picker.dart';
import 'dart:io'; import 'dart:io';
class TemplateGridView extends StatelessWidget { class TemplateGridView extends StatelessWidget {
final GridViewController controller; const TemplateGridView({super.key});
const TemplateGridView({super.key, required this.controller});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

2
win_text_editor/lib/modules/template_parser/widgets/template_parser_view.dart

@ -99,7 +99,7 @@ class _TemplateParserViewState extends State<TemplateParserView> {
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: Card(child: TemplateGridView(controller: controller.gridController))), const Expanded(child: Card(child: TemplateGridView())),
], ],
); );
}, },

2
win_text_editor/lib/modules/template_parser/controllers/template_notifier.dart → win_text_editor/lib/shared/base/safe_notifier.dart

@ -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…
Cancel
Save