|
|
@ -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/framework/controllers/logger.dart'; |
|
|
|
import 'package:win_text_editor/shared/base/base_content_controller.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 { |
|
|
|
class DataCompareController extends BaseContentController { |
|
|
|
List<String> leftColumns = []; |
|
|
|
List<String> leftColumns = []; |
|
|
@ -65,7 +67,7 @@ class DataCompareController extends BaseContentController { |
|
|
|
// 1. 处理空数据情况 |
|
|
|
// 1. 处理空数据情况 |
|
|
|
if (leftData.isEmpty && rightData.isEmpty) return; |
|
|
|
if (leftData.isEmpty && rightData.isEmpty) return; |
|
|
|
|
|
|
|
|
|
|
|
// 2. 处理单边数据 |
|
|
|
// 单边数据直接全部标记为不匹配 |
|
|
|
if (leftData.isEmpty || rightData.isEmpty) { |
|
|
|
if (leftData.isEmpty || rightData.isEmpty) { |
|
|
|
final source = leftData.isEmpty ? rightData : leftData; |
|
|
|
final source = leftData.isEmpty ? rightData : leftData; |
|
|
|
comparedData = |
|
|
|
comparedData = |
|
|
@ -78,6 +80,7 @@ class DataCompareController extends BaseContentController { |
|
|
|
// 3. 双边数据比对 |
|
|
|
// 3. 双边数据比对 |
|
|
|
final rightMap = {for (var r in rightData) r['key']: r}; |
|
|
|
final rightMap = {for (var r in rightData) r['key']: r}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 先处理匹配的行 |
|
|
|
for (var leftRow in leftData) { |
|
|
|
for (var leftRow in leftData) { |
|
|
|
final rightRow = rightMap[leftRow['key']]; |
|
|
|
final rightRow = rightMap[leftRow['key']]; |
|
|
|
|
|
|
|
|
|
|
@ -108,14 +111,134 @@ class DataCompareController extends BaseContentController { |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void exportLeftTable(String matchType) { |
|
|
|
void exportLeftTable(String matchType) async { |
|
|
|
// Implement export logic based on matchType |
|
|
|
try { |
|
|
|
// 'full_match', 'key_match', or 'no_match' |
|
|
|
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<Map<String, dynamic>> 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<String, dynamic>) { |
|
|
|
|
|
|
|
return row['right_data'] as Map<String, dynamic>; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
return <String, dynamic>{}; // 返回空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<Map<String, dynamic>> data, List<String> 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<void> _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 |
|
|
|
String _getMatchTypeName(String matchType) { |
|
|
|
// 'full_match', 'key_match', or 'no_match' |
|
|
|
switch (matchType) { |
|
|
|
|
|
|
|
case 'full_match': |
|
|
|
|
|
|
|
return '整行匹配'; |
|
|
|
|
|
|
|
case 'key_match': |
|
|
|
|
|
|
|
return '主键匹配'; |
|
|
|
|
|
|
|
case 'no_match': |
|
|
|
|
|
|
|
return '不匹配'; |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
return '未知类型'; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@override |
|
|
|
@override |
|
|
|