Files
kuaishai2/lib/features/programs/services/excel_import_service.dart
Developer 55bdaa9211 feat(programs): Excel 导入改为全量覆盖模式
已存在 code 的程序不再跳过,而是:
- 用 Excel 中的字段更新 program(保留 id)
- 删除该 program 的全部旧步骤
- 按 Excel 中的步骤重新写入

返回值变量名 importedCount -> processedCount 更准确。
Toast 文案同步:成功处理 / Excel 无有效数据。
2026-06-04 15:51:03 +08:00

238 lines
7.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 文件导入程序,返回成功处理的程序数量(新建 + 覆盖)
///
/// 行为:
/// - code 不存在:新建程序 + 写入步骤
/// - code 已存在:全量覆盖程序字段,并删除旧步骤后写入新步骤
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 → 完整 Program用于覆盖时取 id
final existing = await _programService.getAllPrograms();
final existingByCode = <String, Program>{
for (final p in existing) p.code: p,
};
// 解析步骤
final stepsSheet = _findSheet(excel, ExcelTemplateService.sheetSteps);
final stepsByCode = stepsSheet == null
? <String, List<_RawStep>>{}
: _parseSteps(stepsSheet);
int processedCount = 0;
for (final program in programs) {
try {
final existingProgram = existingByCode[program.code];
final int programId;
if (existingProgram == null) {
programId = await _programService.addProgram(program);
} else {
// 全量覆盖:保留 id其余字段取 Excel
programId = existingProgram.id!;
await _programService.updateProgram(
existingProgram.copyWith(
name: program.name,
temperature: program.temperature,
airflowTime: program.airflowTime,
status: program.status,
createdAt: program.createdAt,
),
);
// 清空旧步骤
final oldSteps =
await _programService.getStepsByProgramId(programId);
if (oldSteps.isNotEmpty) {
await _programService.deleteSteps(
oldSteps.map((s) => s.id!).toList(),
);
}
}
// 写入新步骤(按 step_no 排序后重新编号为 1..N
final rawSteps = stepsByCode[program.code] ?? const <_RawStep>[];
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);
}
processedCount++;
} catch (_) {
// 单条失败不影响其他程序
continue;
}
}
return processedCount;
}
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;
}