已存在 code 的程序不再跳过,而是: - 用 Excel 中的字段更新 program(保留 id) - 删除该 program 的全部旧步骤 - 按 Excel 中的步骤重新写入 返回值变量名 importedCount -> processedCount 更准确。 Toast 文案同步:成功处理 / Excel 无有效数据。
238 lines
7.1 KiB
Dart
238 lines
7.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 文件导入程序,返回成功处理的程序数量(新建 + 覆盖)
|
||
///
|
||
/// 行为:
|
||
/// - 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;
|
||
}
|