- 新增 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 通过
212 lines
6.1 KiB
Dart
212 lines
6.1 KiB
Dart
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;
|
||
}
|