|
|
@@ -0,0 +1,174 @@
|
|
|
+import 'dart:io';
|
|
|
+import 'package:chicken_farm/core/config/breed_config.dart';
|
|
|
+import 'package:chicken_farm/core/config/excel_header_map.dart';
|
|
|
+import 'package:chicken_farm/core/utils/datetime_util.dart';
|
|
|
+import 'package:chicken_farm/core/utils/logger.dart';
|
|
|
+import 'package:excel/excel.dart';
|
|
|
+import 'package:path_provider/path_provider.dart';
|
|
|
+import 'package:permission_handler/permission_handler.dart';
|
|
|
+import 'package:open_filex/open_filex.dart';
|
|
|
+
|
|
|
+/// Excel 导出工具类(仅支持 excel: ^4.0.0)
|
|
|
+class ExcelExportUtil {
|
|
|
+ /// 预配置表头模板
|
|
|
+ /// 格式:{模板key: {Excel列名: 数据字段名}}
|
|
|
+ static final Map<String, Map<String, String>> _headerTemplates =
|
|
|
+ ExcelHeaderMap.templates;
|
|
|
+
|
|
|
+ /// 注册自定义表头模板
|
|
|
+ static void registerHeaderTemplate(
|
|
|
+ String templateKey,
|
|
|
+ Map<String, String> headerMap,
|
|
|
+ ) {
|
|
|
+ if (templateKey.isEmpty || headerMap.isEmpty) {
|
|
|
+ throw ArgumentError('模板key和表头映射不能为空');
|
|
|
+ }
|
|
|
+ _headerTemplates[templateKey] = headerMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ static Future<String> export(
|
|
|
+ String templateKey,
|
|
|
+ List<Map<String, dynamic>> dataList,
|
|
|
+ ) async {
|
|
|
+ return await exportExcel(
|
|
|
+ templateKey: templateKey,
|
|
|
+ dataList: dataList,
|
|
|
+ fileName:
|
|
|
+ '${BreedConfig.getName(templateKey)}_${DateTimeUtil.format(DateTime.now(), pattern: "yyyyMMddHHmmss")}',
|
|
|
+ filePath:
|
|
|
+ 'excels/${DateTimeUtil.format(DateTime.now(), pattern: "yyyy-MM/yyyy-MM-dd")}',
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 核心导出方法
|
|
|
+ /// [dataList] 待导出数据(List<Map>)
|
|
|
+ /// [templateKey] 表头模板key
|
|
|
+ /// [fileName] 导出文件名(无需.xlsx后缀)
|
|
|
+ /// [sheetName] 工作表名称(默认Sheet1)
|
|
|
+ static Future<String> exportExcel({
|
|
|
+ required String templateKey,
|
|
|
+ required List<Map<String, dynamic>> dataList,
|
|
|
+ required String fileName,
|
|
|
+ String filePath = "excel",
|
|
|
+ String sheetName = 'Sheet1',
|
|
|
+ }) async {
|
|
|
+ try {
|
|
|
+ // 1. 参数校验
|
|
|
+ if (dataList.isEmpty) throw Exception('导出数据不能为空');
|
|
|
+ if (!_headerTemplates.containsKey(templateKey)) {
|
|
|
+ throw Exception('模板[$templateKey]未注册');
|
|
|
+ }
|
|
|
+ if (fileName.isEmpty) throw Exception('文件名不能为空');
|
|
|
+
|
|
|
+ // 2. Android权限申请
|
|
|
+ if (Platform.isAndroid) {
|
|
|
+ final permission = Permission.manageExternalStorage;
|
|
|
+ if (!await permission.isGranted) {
|
|
|
+ if (!await permission.request().isGranted) {
|
|
|
+ throw Exception('存储权限被拒绝,请前往设置开启');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 获取表头和字段映射
|
|
|
+ final headerMap = _headerTemplates[templateKey]!;
|
|
|
+ final titles = headerMap.keys.toList();
|
|
|
+ final fields = headerMap.values.toList();
|
|
|
+
|
|
|
+ // 4. 创建Excel和工作表
|
|
|
+ final excel = Excel.createExcel();
|
|
|
+ final sheet = excel[sheetName];
|
|
|
+
|
|
|
+ // 5. 写入表头(excel 4.0.0 专用API)
|
|
|
+ for (int col = 0; col < titles.length; col++) {
|
|
|
+ final cell = sheet.cell(
|
|
|
+ CellIndex.indexByColumnRow(columnIndex: col, rowIndex: 0),
|
|
|
+ );
|
|
|
+ cell.value = TextCellValue(titles[col]);
|
|
|
+ cell.cellStyle = CellStyle(
|
|
|
+ bold: true,
|
|
|
+ fontSize: 14,
|
|
|
+ backgroundColorHex: ExcelColor.fromHexString('#4285F4'),
|
|
|
+ fontColorHex: ExcelColor.white,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 写入数据行
|
|
|
+ for (int row = 0; row < dataList.length; row++) {
|
|
|
+ final data = dataList[row];
|
|
|
+ for (int col = 0; col < fields.length; col++) {
|
|
|
+ final field = fields[col];
|
|
|
+ dynamic value = data[field] ?? '';
|
|
|
+
|
|
|
+ final cell = sheet.cell(
|
|
|
+ CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row + 1),
|
|
|
+ );
|
|
|
+ // 数据格式化
|
|
|
+ if (value is DateTime) {
|
|
|
+ value = value.toString().substring(0, 19);
|
|
|
+ } else if (value is bool) {
|
|
|
+ value = value ? '是' : '否';
|
|
|
+ } else if (value is num) {
|
|
|
+ value = value.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ cell.value = TextCellValue(value.toString());
|
|
|
+ cell.cellStyle = CellStyle(
|
|
|
+ fontSize: 12,
|
|
|
+ fontColorHex: ExcelColor.fromHexString('#333333'),
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 调整列宽
|
|
|
+ for (int col = 0; col < titles.length; col++) {
|
|
|
+ sheet.setColumnWidth(col, 22);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 8. 处理保存路径(核心修改:创建自定义文件夹)
|
|
|
+ // 8.1 获取下载目录
|
|
|
+ Directory? downloadDir = await getDownloadsDirectory();
|
|
|
+ if (downloadDir == null) {
|
|
|
+ if (Platform.isAndroid) {
|
|
|
+ // Android 下载目录
|
|
|
+ downloadDir = await getExternalStorageDirectory();
|
|
|
+ if (downloadDir != null) {
|
|
|
+ downloadDir = Directory(
|
|
|
+ '${downloadDir.parent.parent.parent.parent}/Download',
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } else if (Platform.isIOS) {
|
|
|
+ // iOS 下载目录
|
|
|
+ downloadDir = await getApplicationSupportDirectory();
|
|
|
+ downloadDir = Directory('${downloadDir.path}/Download');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ downloadDir ??= await getApplicationDocumentsDirectory();
|
|
|
+ // 8.2 拼接自定义文件夹路径并创建
|
|
|
+ final customDir = Directory('${downloadDir.path}/$filePath');
|
|
|
+ if (!await customDir.exists()) {
|
|
|
+ await customDir.create(recursive: true);
|
|
|
+ logger.d('自定义文件夹创建成功:${customDir.path}');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 8.3 处理文件名(过滤特殊字符)
|
|
|
+ final safeFileName = fileName.replaceAll(RegExp(r'[\/:*?"<>|]'), '_');
|
|
|
+ final fullFilePath = '${customDir.path}/$safeFileName.xlsx';
|
|
|
+
|
|
|
+ // 8.4 保存文件
|
|
|
+ await File(fullFilePath).writeAsBytes(excel.save()!);
|
|
|
+
|
|
|
+ // 9. 打开文件
|
|
|
+ await OpenFilex.open(fullFilePath);
|
|
|
+ logger.d('导出成功:$fullFilePath');
|
|
|
+ return fullFilePath;
|
|
|
+ } catch (e) {
|
|
|
+ logger.d('导出失败:$e');
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 获取已注册的模板列表
|
|
|
+ static List<String> getRegisteredTemplates() =>
|
|
|
+ _headerTemplates.keys.toList();
|
|
|
+}
|