From d91791edaf0f18d096a78226181ea0a7ff094cac Mon Sep 17 00:00:00 2001 From: Developer <91611@user.local> Date: Thu, 4 Jun 2026 15:27:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(programs):=20Excel=20=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=20+=20.xlsx=20=E8=A7=A3=E6=9E=90=E5=AF=BC?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 excel 4.0.6 / path_provider 2.1.5 依赖 - ExcelTemplateService:生成 Programs + Steps 双表模板(保存到应用文档目录) - ExcelImportService:解析 .xlsx 并写入数据库,跳过已存在 code、按 program_code 关联步骤 - programs_page 顶部新增「下载模板」按钮,导入按钮改用 Excel 解析 - 移除被取代的 program_import_service.dart - AppLocalizations 新增 downloadTemplate 键 - 验证:flutter analyze 无新增 issue;flutter build apk --debug 通过 --- lib/core/localization/app_localizations.dart | 3 + .../programs/pages/programs_page.dart | 63 +++--- .../services/excel_import_service.dart | 211 ++++++++++++++++++ .../services/excel_template_service.dart | 134 +++++++++++ .../services/program_import_service.dart | 126 ----------- pubspec.lock | 114 +++++++++- pubspec.yaml | 6 + 7 files changed, 504 insertions(+), 153 deletions(-) create mode 100644 lib/features/programs/services/excel_import_service.dart create mode 100644 lib/features/programs/services/excel_template_service.dart delete mode 100644 lib/features/programs/services/program_import_service.dart diff --git a/lib/core/localization/app_localizations.dart b/lib/core/localization/app_localizations.dart index fa46ea6..7b0c545 100644 --- a/lib/core/localization/app_localizations.dart +++ b/lib/core/localization/app_localizations.dart @@ -30,6 +30,7 @@ class AppLocalizations { String get editProgram => _localizedValues[locale.languageCode]?['editProgram'] ?? '编辑程序'; String get deleteProgram => _localizedValues[locale.languageCode]?['deleteProgram'] ?? '删除程序'; String get importProgram => _localizedValues[locale.languageCode]?['importProgram'] ?? '导入程序'; + String get downloadTemplate => _localizedValues[locale.languageCode]?['downloadTemplate'] ?? '下载模板'; String get viewDetails => _localizedValues[locale.languageCode]?['viewDetails'] ?? '查看详情'; String get selectedProgram => _localizedValues[locale.languageCode]?['selectedProgram'] ?? '当前选中程序'; String get selectedProgramLabel => _localizedValues[locale.languageCode]?['selectedProgramLabel'] ?? '当前选中'; @@ -144,6 +145,7 @@ class AppLocalizations { 'editProgram': '编辑程序', 'deleteProgram': '删除程序', 'importProgram': '导入程序', + 'downloadTemplate': '下载模板', 'viewDetails': '查看详情', 'selectedProgram': '当前选中程序', 'selectedProgramLabel': '当前选中', @@ -245,6 +247,7 @@ class AppLocalizations { 'editProgram': 'Edit Program', 'deleteProgram': 'Delete Program', 'importProgram': 'Import Program', + 'downloadTemplate': 'Download Template', 'viewDetails': 'View Details', 'selectedProgram': 'Selected Program', 'selectedProgramLabel': 'Selected', diff --git a/lib/features/programs/pages/programs_page.dart b/lib/features/programs/pages/programs_page.dart index 9616dfa..1ea5351 100644 --- a/lib/features/programs/pages/programs_page.dart +++ b/lib/features/programs/pages/programs_page.dart @@ -5,11 +5,13 @@ import 'package:file_picker/file_picker.dart'; import 'dart:io'; import '../../../core/localization/app_localizations.dart'; import '../../../core/theme/app_theme.dart'; +import '../../../shared/services/toast_service.dart'; import '../../../shared/widgets/common_button.dart'; import '../models/program.dart'; import '../providers/programs_provider.dart'; import '../widgets/program_form_dialog.dart'; -import '../services/program_import_service.dart'; +import '../services/excel_import_service.dart'; +import '../services/excel_template_service.dart'; /// 程序管理页面 class ProgramsPage extends ConsumerStatefulWidget { @@ -64,6 +66,14 @@ class _ProgramsPageState extends ConsumerState { onPressed: () => _showAddDialog(context, ref), ), const SizedBox(width: 12), + // 下载模板按钮 + CommonButton( + text: l10n?.downloadTemplate ?? '下载模板', + icon: Icons.file_download, + type: ButtonType.secondary, + onPressed: () => _downloadTemplate(context), + ), + const SizedBox(width: 12), // 导入按钮 CommonButton( text: l10n?.importProgram ?? '导入', @@ -407,7 +417,7 @@ class _ProgramsPageState extends ConsumerState { // 选择文件 final result = await FilePicker.platform.pickFiles( type: FileType.custom, - allowedExtensions: ['json'], + allowedExtensions: ['xlsx'], allowMultiple: false, ); @@ -417,38 +427,39 @@ class _ProgramsPageState extends ConsumerState { final file = result.files.first; if (file.path == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('无法读取文件'), - backgroundColor: AppTheme.errorColor, - ), - ); + if (!context.mounted) return; + ToastService.showError(context, '无法读取文件'); return; } - // 读取文件内容 - final jsonContent = await File(file.path!).readAsString(); - - // 导入程序 - final importedCount = await ProgramImportService.instance.importFromJson(jsonContent); + // 解析并写入数据库 + final importedCount = + await ExcelImportService.instance.importFromExcel(File(file.path!)); // 刷新程序列表 ref.read(programsProvider.notifier).loadPrograms(); - // 显示结果 - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('成功导入 $importedCount 个程序'), - backgroundColor: importedCount > 0 ? AppTheme.successColor : AppTheme.warningColor, - ), - ); + if (!context.mounted) return; + if (importedCount > 0) { + ToastService.showSuccess(context, '成功导入 $importedCount 个程序'); + } else { + ToastService.showWarning(context, '未导入新程序(编号可能已存在)'); + } } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('导入失败: ${e.toString()}'), - backgroundColor: AppTheme.errorColor, - ), - ); + if (!context.mounted) return; + ToastService.showError(context, '导入失败: ${e.toString()}'); + } + } + + /// 下载 Excel 模板 + Future _downloadTemplate(BuildContext context) async { + try { + final file = await ExcelTemplateService.instance.generateTemplate(); + if (!context.mounted) return; + ToastService.showSuccess(context, '模板已保存: ${file.path}'); + } catch (e) { + if (!context.mounted) return; + ToastService.showError(context, '生成模板失败: ${e.toString()}'); } } diff --git a/lib/features/programs/services/excel_import_service.dart b/lib/features/programs/services/excel_import_service.dart new file mode 100644 index 0000000..577b10a --- /dev/null +++ b/lib/features/programs/services/excel_import_service.dart @@ -0,0 +1,211 @@ +import 'dart:io'; + +import 'package:excel/excel.dart'; + +import '../models/program.dart'; +import '../models/step.dart'; +import '../services/program_service.dart'; +import 'excel_template_service.dart'; + +/// Excel 导入服务 +/// +/// 解析用户填好的 .xlsx 并通过 [ProgramService] 写入数据库。 +/// 模板结构与 [ExcelTemplateService] 一致:Programs + Steps 双表, +/// 通过 program_code 关联。 +class ExcelImportService { + static final ExcelImportService instance = ExcelImportService._internal(); + final ProgramService _programService = ProgramService.instance; + + ExcelImportService._internal(); + + /// 从 .xlsx 文件导入程序,返回成功导入的程序数量 + Future importFromExcel(File file) async { + final bytes = await file.readAsBytes(); + final excel = Excel.decodeBytes(bytes); + + final programsSheet = _findSheet(excel, ExcelTemplateService.sheetPrograms); + if (programsSheet == null) { + throw const ExcelImportException('缺少 Programs 工作表'); + } + + final programs = _parsePrograms(programsSheet); + if (programs.isEmpty) { + throw const ExcelImportException('Programs 表无有效数据'); + } + + // 读取已有 code,避免重复 + final existing = await _programService.getAllPrograms(); + final existingCodes = existing.map((p) => p.code).toSet(); + + // 解析步骤 + final stepsSheet = _findSheet(excel, ExcelTemplateService.sheetSteps); + final stepsByCode = stepsSheet == null + ? >{} + : _parseSteps(stepsSheet); + + int importedCount = 0; + for (final program in programs) { + try { + if (existingCodes.contains(program.code)) { + continue; + } + + final programId = await _programService.addProgram(program); + + final rawSteps = stepsByCode[program.code] ?? const <_RawStep>[]; + // 按 step_no 排序后写入 + rawSteps.sort((a, b) => a.stepNo.compareTo(b.stepNo)); + for (var i = 0; i < rawSteps.length; i++) { + final raw = rawSteps[i]; + final step = Step( + programId: programId, + stepNo: i + 1, + position: raw.position, + name: raw.name, + mixTime: raw.mixTime, + magnetTime: raw.magnetTime, + volume: raw.volume, + blowTime: raw.blowTime, + speed: raw.speed, + ); + await _programService.addStep(step); + } + + importedCount++; + } catch (_) { + // 单条失败不影响其他程序 + continue; + } + } + + return importedCount; + } + + Sheet? _findSheet(Excel excel, String name) { + for (final entry in excel.tables.entries) { + if (entry.key == name) return entry.value; + } + return null; + } + + /// 解析 Programs 表 + /// 返回不含 id 的 Program 列表(createdAt 由调用方填充) + List _parsePrograms(Sheet sheet) { + final rows = _dataRows(sheet); + final results = []; + + for (final cells in rows) { + final code = _readString(cells, 0); + final name = _readString(cells, 1); + if (code.isEmpty || name.isEmpty) continue; + + results.add( + Program( + code: code, + name: name, + createdAt: DateTime.now().toString().split('.').first, + status: _readInt(cells, 4, 1) == 0 ? 0 : 1, + temperature: _readInt(cells, 2, 50), + airflowTime: _readInt(cells, 3, 60), + ), + ); + } + return results; + } + + /// 解析 Steps 表 + /// 以 program_code 为 key 分组 + Map> _parseSteps(Sheet sheet) { + final rows = _dataRows(sheet); + final map = >{}; + + for (final cells in rows) { + final programCode = _readString(cells, 0); + if (programCode.isEmpty) continue; + final position = _readString(cells, 2, 'A1'); + final name = _readString(cells, 3); + if (name.isEmpty) continue; + + final raw = _RawStep( + stepNo: _readInt(cells, 1, 0), + position: position, + name: name, + mixTime: _readInt(cells, 4, 0), + magnetTime: _readInt(cells, 5, 0), + volume: _readInt(cells, 6, 0), + blowTime: _readInt(cells, 7, 0), + speed: _readInt(cells, 8, 5), + ); + map.putIfAbsent(programCode, () => []).add(raw); + } + return map; + } + + /// 跳过表头和说明行(首行表头,后续以"说明"开头的视为说明) + List> _dataRows(Sheet sheet) { + final result = >[]; + for (var i = 1; i < sheet.rows.length; i++) { + final row = sheet.rows[i]; + // 跳过空行 + if (row.every((c) => c == null || c.value == null)) continue; + final firstCell = _asTrimmed(row.first?.value); + if (firstCell.startsWith('说明')) continue; + result.add(row.map((c) => c?.value).toList(growable: false)); + } + return result; + } + + String _readString(List row, int index, [String fallback = '']) { + if (index >= row.length) return fallback; + final v = row[index]; + if (v == null) return fallback; + final s = v.toString().trim(); + return s.isEmpty ? fallback : s; + } + + int _readInt(List row, int index, int fallback) { + if (index >= row.length) return fallback; + final v = row[index]; + if (v == null) return fallback; + if (v is int) return v; + if (v is double) return v.toInt(); + final parsed = int.tryParse(v.toString().trim()); + return parsed ?? fallback; + } + + String _asTrimmed(Object? v) { + if (v == null) return ''; + return v.toString().trim(); + } +} + +class _RawStep { + final int stepNo; + final String position; + final String name; + final int mixTime; + final int magnetTime; + final int volume; + final int blowTime; + final int speed; + + _RawStep({ + required this.stepNo, + required this.position, + required this.name, + required this.mixTime, + required this.magnetTime, + required this.volume, + required this.blowTime, + required this.speed, + }); +} + +/// Excel 导入异常 +class ExcelImportException implements Exception { + final String message; + const ExcelImportException(this.message); + + @override + String toString() => message; +} diff --git a/lib/features/programs/services/excel_template_service.dart b/lib/features/programs/services/excel_template_service.dart new file mode 100644 index 0000000..a75bfd6 --- /dev/null +++ b/lib/features/programs/services/excel_template_service.dart @@ -0,0 +1,134 @@ +import 'dart:io'; + +import 'package:excel/excel.dart'; +import 'package:path_provider/path_provider.dart'; + +/// Excel 模板服务 +/// +/// 生成 .xlsx 模板用于程序/步骤批量导入。 +/// 模板包含两张表: +/// - Programs:程序基础信息 +/// - Steps:步骤参数(通过 program_code 与 Programs 关联) +class ExcelTemplateService { + static final ExcelTemplateService instance = ExcelTemplateService._internal(); + + ExcelTemplateService._internal(); + + /// 工作表名称 + static const String sheetPrograms = 'Programs'; + static const String sheetSteps = 'Steps'; + + /// 生成模板并保存到应用文档目录,返回文件对象 + Future generateTemplate() async { + final excel = Excel.createExcel(); + final bytes = _buildTemplateBytes(excel); + final dir = await getApplicationDocumentsDirectory(); + final file = File('${dir.path}/program_template.xlsx'); + await file.writeAsBytes(bytes, flush: true); + return file; + } + + /// 构建模板字节流(可在测试中直接调用) + List _buildTemplateBytes(Excel excel) { + // 默认创建的第一个 sheet 重命名 + final defaultSheet = excel.getDefaultSheet(); + if (defaultSheet != null && defaultSheet != sheetPrograms) { + excel.rename(defaultSheet, sheetPrograms); + } + + _writeProgramsSheet(excel); + _writeStepsSheet(excel); + return excel.encode()!; + } + + void _writeProgramsSheet(Excel excel) { + final sheet = excel[sheetPrograms]; + final headerStyle = CellStyle( + bold: true, + backgroundColorHex: ExcelColor.fromHexString('FFD9E1F2'), + horizontalAlign: HorizontalAlign.Center, + ); + + // 表头 + final headers = ['code', 'name', 'temperature', 'airflowTime', 'status']; + sheet.appendRow(headers.map((h) => TextCellValue(h)).toList()); + for (var i = 0; i < headers.length; i++) { + sheet + .cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0)) + .cellStyle = headerStyle; + } + + // 示例行 + final sample1 = ['P001', '示例程序-标准流程', 50, 60, 1]; + final sample2 = ['P002', '示例程序-快速流程', 45, 30, 1]; + sheet.appendRow(sample1.map((v) => TextCellValue(v.toString())).toList()); + sheet.appendRow(sample2.map((v) => TextCellValue(v.toString())).toList()); + + // 说明行(用前缀标记,避免被解析) + final note = '说明:code 必填且唯一;name 必填;temperature/airflowTime 整数;status:1启用 0停用'; + sheet.appendRow([TextCellValue(note)]); + + // 列宽 + sheet.setColumnWidth(0, 14); + sheet.setColumnWidth(1, 26); + sheet.setColumnWidth(2, 14); + sheet.setColumnWidth(3, 14); + sheet.setColumnWidth(4, 10); + } + + void _writeStepsSheet(Excel excel) { + final sheet = excel[sheetSteps]; + final headerStyle = CellStyle( + bold: true, + backgroundColorHex: ExcelColor.fromHexString('FFD9E1F2'), + horizontalAlign: HorizontalAlign.Center, + ); + + final headers = [ + 'program_code', + 'step_no', + 'position', + 'name', + 'mixTime', + 'magnetTime', + 'volume', + 'blowTime', + 'speed', + ]; + sheet.appendRow(headers.map((h) => TextCellValue(h)).toList()); + for (var i = 0; i < headers.length; i++) { + sheet + .cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0)) + .cellStyle = headerStyle; + } + + // 示例行:P001 / P002 各两步 + final samples = >[ + ['P001', 1, 'A1', '加样', 30, 0, 500, 0, 5], + ['P001', 2, 'A2', '混合', 60, 10, 300, 30, 5], + ['P002', 1, 'B1', '混合', 20, 5, 400, 0, 4], + ]; + for (final row in samples) { + sheet.appendRow(row.map((v) { + if (v is num) return IntCellValue(v.toInt()); + return TextCellValue(v.toString()); + }).toList()); + } + + // 说明行 + final note = + '说明:program_code 对应 Programs.code;step_no 整数从 1 开始;position 形如 A1/B2;mixTime/magnetTime/volume/blowTime 单位秒或微升;speed 1-10'; + sheet.appendRow([TextCellValue(note)]); + + // 列宽 + sheet.setColumnWidth(0, 14); + sheet.setColumnWidth(1, 10); + sheet.setColumnWidth(2, 12); + sheet.setColumnWidth(3, 14); + sheet.setColumnWidth(4, 10); + sheet.setColumnWidth(5, 12); + sheet.setColumnWidth(6, 10); + sheet.setColumnWidth(7, 10); + sheet.setColumnWidth(8, 8); + } +} diff --git a/lib/features/programs/services/program_import_service.dart b/lib/features/programs/services/program_import_service.dart deleted file mode 100644 index 7dfbf03..0000000 --- a/lib/features/programs/services/program_import_service.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'dart:convert'; -import '../../programs/models/program.dart'; -import '../../programs/models/step.dart'; -import '../../programs/services/program_service.dart'; - -/// 程序导入服务 -class ProgramImportService { - static final ProgramImportService instance = ProgramImportService._internal(); - final ProgramService _programService = ProgramService.instance; - - ProgramImportService._internal(); - - /// 从 JSON 字符串导入程序 - /// 返回导入的程序数量 - Future importFromJson(String jsonContent) async { - final data = jsonDecode(jsonContent); - - // 支持单个程序或程序数组 - final List programsData; - if (data is List) { - programsData = data; - } else if (data is Map && data.containsKey('programs')) { - programsData = data['programs'] as List; - } else { - programsData = [data]; - } - - int importedCount = 0; - - for (final programData in programsData) { - try { - // 验证必填字段 - if (!_validateProgramData(programData)) { - continue; - } - - // 检查编号是否已存在 - final existingPrograms = await _programService.getAllPrograms(); - final code = programData['code'] as String; - if (existingPrograms.any((p) => p.code == code)) { - // 编号已存在,跳过或使用新编号 - continue; - } - - // 创建程序 - final program = Program( - code: code, - name: programData['name'] as String, - createdAt: programData['createdAt'] ?? DateTime.now().toString().split('.')[0], - status: programData['status'] ?? 1, - temperature: programData['temperature'] as int? ?? 50, - airflowTime: programData['airflowTime'] as int? ?? 60, - ); - - final programId = await _programService.addProgram(program); - - // 导入步骤 - final stepsData = programData['steps'] as List?; - if (stepsData != null) { - for (int i = 0; i < stepsData.length; i++) { - final stepData = stepsData[i]; - final step = Step( - programId: programId, - stepNo: i + 1, - position: stepData['position'] as String? ?? 'A1', - name: stepData['name'] as String? ?? '步骤${i + 1}', - mixTime: stepData['mixTime'] as int? ?? 0, - magnetTime: stepData['magnetTime'] as int? ?? 0, - volume: stepData['volume'] as int? ?? 0, - blowTime: stepData['blowTime'] as int? ?? 0, - speed: stepData['speed'] as int? ?? 5, - ); - await _programService.addStep(step); - } - } - - importedCount++; - } catch (e) { - // 忽略单个程序导入错误 - continue; - } - } - - return importedCount; - } - - /// 验证程序数据 - bool _validateProgramData(Map data) { - return data.containsKey('code') && - data.containsKey('name') && - data['code'] is String && - data['name'] is String; - } - - /// 导出程序为 JSON - Future exportToJson(List programIds) async { - final programs = []; - - for (final id in programIds) { - final program = await _programService.getProgramById(id); - if (program == null) continue; - - final steps = await _programService.getStepsByProgramId(id); - - programs.add({ - 'code': program.code, - 'name': program.name, - 'createdAt': program.createdAt, - 'status': program.status, - 'temperature': program.temperature, - 'airflowTime': program.airflowTime, - 'steps': steps.map((s) => { - 'position': s.position, - 'name': s.name, - 'mixTime': s.mixTime, - 'magnetTime': s.magnetTime, - 'volume': s.volume, - 'blowTime': s.blowTime, - 'speed': s.speed, - }).toList(), - }); - } - - return jsonEncode({'programs': programs}); - } -} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index f8efc4a..5e89e28 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.4" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" args: dependency: transitive description: @@ -89,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" collection: dependency: transitive description: @@ -169,6 +185,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" + excel: + dependency: "direct main" + description: + name: excel + sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780" + url: "https://pub.dev" + source: hosted + version: "4.0.6" fake_async: dependency: transitive description: @@ -277,6 +309,14 @@ packages: url: "https://pub.dev" source: hosted version: "15.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" hotreloader: dependency: transitive description: @@ -293,6 +333,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" json_annotation: dependency: transitive description: @@ -365,6 +421,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + url: "https://pub.dev" + source: hosted + version: "9.4.1" package_config: dependency: transitive description: @@ -381,6 +445,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -405,6 +493,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -437,6 +533,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" riverpod: dependency: transitive description: @@ -714,6 +818,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" yaml: dependency: transitive description: @@ -724,4 +836,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.11.5 <4.0.0" - flutter: ">=3.38.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 4b2d54a..1bfb76f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,12 @@ dependencies: # 文件选择器 file_picker: ^8.1.7 + # 路径解析(保存导入/导出文件) + path_provider: ^2.1.5 + + # Excel 读写(生成导入模板 + 解析用户填好的 .xlsx) + excel: ^4.0.2 + # 国际化 intl: ^0.20.2