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:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user