feat(programs): Excel 模板下载 + .xlsx 解析导入
- 新增 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 通过
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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<ProgramsPage> {
|
||||
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<ProgramsPage> {
|
||||
// 选择文件
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
allowedExtensions: ['xlsx'],
|
||||
allowMultiple: false,
|
||||
);
|
||||
|
||||
@@ -417,38 +427,39 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
||||
|
||||
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<void> _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()}');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
211
lib/features/programs/services/excel_import_service.dart
Normal file
211
lib/features/programs/services/excel_import_service.dart
Normal file
@@ -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<int> 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
|
||||
? <String, List<_RawStep>>{}
|
||||
: _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<Program> _parsePrograms(Sheet sheet) {
|
||||
final rows = _dataRows(sheet);
|
||||
final results = <Program>[];
|
||||
|
||||
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<String, List<_RawStep>> _parseSteps(Sheet sheet) {
|
||||
final rows = _dataRows(sheet);
|
||||
final map = <String, List<_RawStep>>{};
|
||||
|
||||
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<List<Object?>> _dataRows(Sheet sheet) {
|
||||
final result = <List<Object?>>[];
|
||||
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<Object?> 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<Object?> 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;
|
||||
}
|
||||
134
lib/features/programs/services/excel_template_service.dart
Normal file
134
lib/features/programs/services/excel_template_service.dart
Normal file
@@ -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<File> 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<int> _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 = <List<Object>>[
|
||||
['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);
|
||||
}
|
||||
}
|
||||
@@ -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<int> importFromJson(String jsonContent) async {
|
||||
final data = jsonDecode(jsonContent);
|
||||
|
||||
// 支持单个程序或程序数组
|
||||
final List<dynamic> 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<String, dynamic> data) {
|
||||
return data.containsKey('code') &&
|
||||
data.containsKey('name') &&
|
||||
data['code'] is String &&
|
||||
data['name'] is String;
|
||||
}
|
||||
|
||||
/// 导出程序为 JSON
|
||||
Future<String> exportToJson(List<int> 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});
|
||||
}
|
||||
}
|
||||
114
pubspec.lock
114
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user