diff --git a/documents/PB UFT模块迁移方案.docx b/documents/PB UFT模块迁移方案.docx index d11e08f..b353d7b 100644 Binary files a/documents/PB UFT模块迁移方案.docx and b/documents/PB UFT模块迁移方案.docx differ diff --git a/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart b/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart index 7012fe8..a5c39ed 100644 --- a/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart +++ b/win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart @@ -1,7 +1,9 @@ -import 'package:flutter/material.dart'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; import 'package:win_text_editor/framework/controllers/logger.dart'; import 'package:win_text_editor/shared/base/base_content_controller.dart'; -import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:path/path.dart' as path; class DataCompareController extends BaseContentController { List leftColumns = []; @@ -65,7 +67,7 @@ class DataCompareController extends BaseContentController { // 1. 处理空数据情况 if (leftData.isEmpty && rightData.isEmpty) return; - // 2. 处理单边数据 + // 单边数据直接全部标记为不匹配 if (leftData.isEmpty || rightData.isEmpty) { final source = leftData.isEmpty ? rightData : leftData; comparedData = @@ -78,6 +80,7 @@ class DataCompareController extends BaseContentController { // 3. 双边数据比对 final rightMap = {for (var r in rightData) r['key']: r}; + // 先处理匹配的行 for (var leftRow in leftData) { final rightRow = rightMap[leftRow['key']]; @@ -108,14 +111,134 @@ class DataCompareController extends BaseContentController { }); } - void exportLeftTable(String matchType) { - // Implement export logic based on matchType - // 'full_match', 'key_match', or 'no_match' + void exportLeftTable(String matchType) async { + try { + final filteredData = + comparedData.where((row) { + if (!row['is_left']) return false; + return row['match_status'] == matchType; + }).toList(); + + if (filteredData.isEmpty) { + Logger().info('没有匹配类型的左表数据可导出'); + return; + } + + // 生成CSV内容 + final csvContent = _generateCsvContent(filteredData, leftColumns); + final fileName = 'left_table_${_getMatchTypeName(matchType)}.csv'; + await _saveCsvFile(fileName, csvContent); + + Logger().info('左表(${_getMatchTypeName(matchType)})导出成功'); + } catch (e) { + Logger().error('左表导出失败: $e'); + rethrow; + } + } + + void exportRightTable(String matchType) async { + try { + final List> filteredData = []; + + // 全匹配直接取左表数据 + if (matchType == 'full_match') { + filteredData.addAll( + comparedData.where((row) { + return row['match_status'] == "full_match"; + }).toList(), + ); + } else if (matchType == 'key_match') { + //取出左表数据,再从左表数据的right_data中取出右表数据 + filteredData.addAll( + comparedData + .where((row) => row['match_status'] == "key_match") + .map((row) { + // 确保right_data存在且是Map类型 + if (row['right_data'] is Map) { + return row['right_data'] as Map; + } else { + return {}; // 返回空Map作为保护 + } + }) + .where((row) => row.isNotEmpty) // 过滤掉无效数据 + .toList(), + ); + } else { + filteredData.addAll( + comparedData.where((row) { + if (row['is_left']) return false; + return row['match_status'] == matchType; + }).toList(), + ); + } + + if (filteredData.isEmpty) { + Logger().info('没有匹配类型的右表数据可导出'); + return; + } + + // 生成CSV内容 + final csvContent = _generateCsvContent(filteredData, rightColumns); + final fileName = 'right_table_${_getMatchTypeName(matchType)}.csv'; + await _saveCsvFile(fileName, csvContent); + + Logger().info('右表(${_getMatchTypeName(matchType)})导出成功'); + } catch (e) { + Logger().error('右表导出失败: $e'); + rethrow; + } + } + + // 辅助方法:生成CSV内容 + String _generateCsvContent(List> data, List columns) { + final buffer = StringBuffer(); + + // 写入标题行 (只保留主键和各列名) + buffer.write('主键'); // 不再包含"序号" + for (var column in columns) { + buffer.write(',$column'); + } + buffer.writeln(); + + // 写入数据行 + for (var row in data) { + buffer.write('${row['key']}'); // 不再包含serial + for (var column in columns) { + buffer.write(',${row[column] ?? ''}'); + } + buffer.writeln(); + } + + return buffer.toString(); + } + + // 辅助方法:保存CSV文件 + Future _saveCsvFile(String fileName, String content) async { + final filePath = await FilePicker.platform.saveFile( + dialogTitle: '保存对比结果', + fileName: fileName, + type: FileType.custom, + allowedExtensions: ['csv'], + ); + + if (filePath != null) { + final file = File(filePath); + await file.writeAsString(content); + } } - void exportRightTable(String matchType) { - // Implement export logic based on matchType - // 'full_match', 'key_match', or 'no_match' + // 辅助方法:获取匹配类型名称 + String _getMatchTypeName(String matchType) { + switch (matchType) { + case 'full_match': + return '整行匹配'; + case 'key_match': + return '主键匹配'; + case 'no_match': + return '不匹配'; + default: + return '未知类型'; + } } @override diff --git a/win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart b/win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart index a4efe27..0131032 100644 --- a/win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart +++ b/win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart @@ -26,35 +26,47 @@ class DataCompareDataSource extends DataGridSource { final rightData = row['right_data'] as Map?; final status = row['match_status'] as String; - return DataGridRow( - cells: [ - // 左表列 - DataGridCell(columnName: 'left_serial', value: isLeft ? row['serial'] : ''), - DataGridCell(columnName: 'left_key', value: isLeft ? row['key'] : ''), - ...controller.leftColumns.map( - (col) => DataGridCell(columnName: 'left_$col', value: isLeft ? row[col] : ''), + return DataGridRow(cells: [ + // 左表列 - 只显示左表数据或匹配数据的左表部分 + DataGridCell( + columnName: 'left_serial', + value: (isLeft || status != 'no_match') ? row['serial'] : '' ), + DataGridCell( + columnName: 'left_key', + value: (isLeft || status != 'no_match') ? row['key'] : '' + ), + ...controller.leftColumns.map((col) => DataGridCell( + columnName: 'left_$col', + value: (isLeft || status != 'no_match') ? row[col] : '' + )), // 对比状态 - DataGridCell(columnName: 'comparison', value: _getStatusIcon(status)), + DataGridCell( + columnName: 'comparison', + value: _getStatusIcon(status) + ), - // 右表列 + // 右表列 - 只显示右表数据或匹配数据的右表部分 DataGridCell( columnName: 'right_serial', - value: rightData?['serial'] ?? (!isLeft ? row['serial'] : ''), + value: (!isLeft || status != 'no_match') + ? (rightData?['serial'] ?? row['serial']) + : '' ), DataGridCell( columnName: 'right_key', - value: rightData?['key'] ?? (!isLeft ? row['key'] : ''), + value: (!isLeft || status != 'no_match') + ? (rightData?['key'] ?? row['key']) + : '' ), - ...controller.rightColumns.map( - (col) => DataGridCell( + ...controller.rightColumns.map((col) => DataGridCell( columnName: 'right_$col', - value: rightData?[col] ?? (!isLeft ? row[col] : ''), - ), - ), - ], - ); + value: (!isLeft || status != 'no_match') + ? (rightData?[col] ?? row[col]) + : '' + )), + ]); }).toList(); } diff --git a/win_text_editor/lib/modules/data_compare/widgets/data_compare_grid.dart b/win_text_editor/lib/modules/data_compare/widgets/data_compare_grid.dart index 3de272a..c7b4a8b 100644 --- a/win_text_editor/lib/modules/data_compare/widgets/data_compare_grid.dart +++ b/win_text_editor/lib/modules/data_compare/widgets/data_compare_grid.dart @@ -62,8 +62,6 @@ class DataCompareGrid extends StatelessWidget { ], child: _buildGroupHeader('左表', color: Colors.green[50]), ), - // 对比列留空,将在第二行合并 - // StackedHeaderCell(columnNames: ["comparison"], child: Container()), StackedHeaderCell( columnNames: [ 'right_serial', diff --git a/win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart b/win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart index 26ec4e6..62d1a3c 100644 --- a/win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart +++ b/win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart @@ -93,40 +93,45 @@ class _DataCompareViewState extends State { // 修改后的导出按钮方法 Widget _buildExportButton(bool isLeftTable) { - return ElevatedButton( - onPressed: () { - _showExportMenu(isLeftTable); + return Builder( + builder: (btnContext) { + return ElevatedButton( + onPressed: () { + final RenderBox renderBox = btnContext.findRenderObject() as RenderBox; + final buttonSize = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + _showExportMenu( + isLeftTable, + RelativeRect.fromLTRB( + offset.dx, + offset.dy + buttonSize.height, + offset.dx + buttonSize.width, + offset.dy + buttonSize.height, + ), + ); }, - style: ElevatedButton.styleFrom( - backgroundColor: isLeftTable ? Colors.green[50] : Colors.blue[50], - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - child: Row( - children: [ - const Icon(Icons.output, size: 18), - const SizedBox(width: 4), - Text('导出${isLeftTable ? '左表' : '右表'}'), - ], - ), + style: ElevatedButton.styleFrom( + backgroundColor: isLeftTable ? Colors.green[50] : Colors.blue[50], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Row( + children: [ + const Icon(Icons.output, size: 18), + const SizedBox(width: 4), + Text('导出${isLeftTable ? '左表' : '右表'}'), + ], + ), + ); + }, ); } - // 显示导出菜单 - void _showExportMenu(bool isLeftTable) { - final RenderBox button = context.findRenderObject() as RenderBox; - final RenderBox overlay = Overlay.of(context).context.findRenderObject() as RenderBox; - - final RelativeRect position = RelativeRect.fromRect( - Rect.fromPoints( - button.localToGlobal(Offset.zero, ancestor: overlay), // 按钮左上角 - button.localToGlobal(button.size.bottomRight(Offset.zero), ancestor: overlay), - ), - Offset.zero & overlay.size, - ); - +// 修改后的显示菜单方法 +void _showExportMenu(bool isLeftTable, RelativeRect position) { showMenu( context: context, - position: position, + position: position, items: [ const PopupMenuItem( value: 'full_match', @@ -151,7 +156,11 @@ class _DataCompareViewState extends State { const PopupMenuItem( value: 'no_match', child: Row( - children: [Icon(Icons.close, color: Colors.red), SizedBox(width: 8), Text('不匹配')], + children: [ + Icon(Icons.close, color: Colors.red), + SizedBox(width: 8), + Text('不匹配') + ], ), ), ], diff --git a/win_text_editor/lib/modules/demo/widgets/demo_view.dart b/win_text_editor/lib/modules/demo/widgets/demo_view.dart index 87582d7..3f17a30 100644 --- a/win_text_editor/lib/modules/demo/widgets/demo_view.dart +++ b/win_text_editor/lib/modules/demo/widgets/demo_view.dart @@ -44,10 +44,109 @@ class _DemoViewState extends State { Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: _controller, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Column(children: [Expanded(flex: 1, child: Text("Demo,就是一个空窗体,用于新增菜单时快速初始化(拷贝)。"))]), + child: const Padding(padding: EdgeInsets.all(8.0), child: ButtonPositionExample()), + ); + } +} + +class ButtonPositionExample extends StatefulWidget { + const ButtonPositionExample({Key? key}) : super(key: key); + + @override + _ButtonPositionExampleState createState() => _ButtonPositionExampleState(); +} + +class _ButtonPositionExampleState extends State { + // 用于获取按钮位置的GlobalKey + final GlobalKey _buttonKey = GlobalKey(); + + // 存储按钮位置信息 + String _buttonPosition = '未获取'; + + // 存储点击位置信息 + String _clickPosition = '未点击'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('按钮位置和点击坐标示例')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 显示按钮位置信息 + Text('按钮位置: $_buttonPosition'), + const SizedBox(height: 10), + + // 显示点击位置信息 + Text('点击位置: $_clickPosition'), + const SizedBox(height: 30), + + // 可获取位置的按钮 + ElevatedButton( + key: _buttonKey, + onPressed: () { + _getButtonPosition(); + }, + child: const Text('获取按钮位置'), + ), + const SizedBox(height: 20), + + // 可获取点击位置的按钮 + GestureDetector( + onTapDown: (TapDownDetails details) { + _getClickPosition(details); + }, + child: Container( + padding: const EdgeInsets.all(16.0), + color: Colors.blue, + child: const Text('点击此处获取点击坐标', style: TextStyle(color: Colors.white)), + ), + ), + const SizedBox(height: 20), + + // 同时获取按钮位置和点击位置 + Builder( + builder: (BuildContext context) { + return ElevatedButton( + onPressed: () { + // 这里使用context.findRenderObject()获取当前按钮的位置 + final RenderBox renderBox = context.findRenderObject() as RenderBox; + final position = renderBox.localToGlobal(Offset.zero); + setState(() { + _buttonPosition = + '按钮位置: (${position.dx.toStringAsFixed(2)}, ${position.dy.toStringAsFixed(2)})'; + }); + }, + child: const Text('使用Builder获取按钮位置'), + ); + }, + ), + ], + ), ), ); } + + // 获取按钮位置 + void _getButtonPosition() { + final RenderBox renderBox = _buttonKey.currentContext?.findRenderObject() as RenderBox; + final position = renderBox.localToGlobal(Offset.zero); + + setState(() { + _buttonPosition = + '按钮位置: (${position.dx.toStringAsFixed(2)}, ${position.dy.toStringAsFixed(2)})'; + }); + } + + // 获取点击位置 + void _getClickPosition(TapDownDetails details) { + final RenderBox renderBox = context.findRenderObject() as RenderBox; + final position = renderBox.globalToLocal(details.globalPosition); + + setState(() { + _clickPosition = + '点击位置: (${position.dx.toStringAsFixed(2)}, ${position.dy.toStringAsFixed(2)})'; + }); + } }