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 editProgram => _localizedValues[locale.languageCode]?['editProgram'] ?? '编辑程序';
|
||||||
String get deleteProgram => _localizedValues[locale.languageCode]?['deleteProgram'] ?? '删除程序';
|
String get deleteProgram => _localizedValues[locale.languageCode]?['deleteProgram'] ?? '删除程序';
|
||||||
String get importProgram => _localizedValues[locale.languageCode]?['importProgram'] ?? '导入程序';
|
String get importProgram => _localizedValues[locale.languageCode]?['importProgram'] ?? '导入程序';
|
||||||
|
String get downloadTemplate => _localizedValues[locale.languageCode]?['downloadTemplate'] ?? '下载模板';
|
||||||
String get viewDetails => _localizedValues[locale.languageCode]?['viewDetails'] ?? '查看详情';
|
String get viewDetails => _localizedValues[locale.languageCode]?['viewDetails'] ?? '查看详情';
|
||||||
String get selectedProgram => _localizedValues[locale.languageCode]?['selectedProgram'] ?? '当前选中程序';
|
String get selectedProgram => _localizedValues[locale.languageCode]?['selectedProgram'] ?? '当前选中程序';
|
||||||
String get selectedProgramLabel => _localizedValues[locale.languageCode]?['selectedProgramLabel'] ?? '当前选中';
|
String get selectedProgramLabel => _localizedValues[locale.languageCode]?['selectedProgramLabel'] ?? '当前选中';
|
||||||
@@ -144,6 +145,7 @@ class AppLocalizations {
|
|||||||
'editProgram': '编辑程序',
|
'editProgram': '编辑程序',
|
||||||
'deleteProgram': '删除程序',
|
'deleteProgram': '删除程序',
|
||||||
'importProgram': '导入程序',
|
'importProgram': '导入程序',
|
||||||
|
'downloadTemplate': '下载模板',
|
||||||
'viewDetails': '查看详情',
|
'viewDetails': '查看详情',
|
||||||
'selectedProgram': '当前选中程序',
|
'selectedProgram': '当前选中程序',
|
||||||
'selectedProgramLabel': '当前选中',
|
'selectedProgramLabel': '当前选中',
|
||||||
@@ -245,6 +247,7 @@ class AppLocalizations {
|
|||||||
'editProgram': 'Edit Program',
|
'editProgram': 'Edit Program',
|
||||||
'deleteProgram': 'Delete Program',
|
'deleteProgram': 'Delete Program',
|
||||||
'importProgram': 'Import Program',
|
'importProgram': 'Import Program',
|
||||||
|
'downloadTemplate': 'Download Template',
|
||||||
'viewDetails': 'View Details',
|
'viewDetails': 'View Details',
|
||||||
'selectedProgram': 'Selected Program',
|
'selectedProgram': 'Selected Program',
|
||||||
'selectedProgramLabel': 'Selected',
|
'selectedProgramLabel': 'Selected',
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import '../../../core/localization/app_localizations.dart';
|
import '../../../core/localization/app_localizations.dart';
|
||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
|
import '../../../shared/services/toast_service.dart';
|
||||||
import '../../../shared/widgets/common_button.dart';
|
import '../../../shared/widgets/common_button.dart';
|
||||||
import '../models/program.dart';
|
import '../models/program.dart';
|
||||||
import '../providers/programs_provider.dart';
|
import '../providers/programs_provider.dart';
|
||||||
import '../widgets/program_form_dialog.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 {
|
class ProgramsPage extends ConsumerStatefulWidget {
|
||||||
@@ -64,6 +66,14 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
|||||||
onPressed: () => _showAddDialog(context, ref),
|
onPressed: () => _showAddDialog(context, ref),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
// 下载模板按钮
|
||||||
|
CommonButton(
|
||||||
|
text: l10n?.downloadTemplate ?? '下载模板',
|
||||||
|
icon: Icons.file_download,
|
||||||
|
type: ButtonType.secondary,
|
||||||
|
onPressed: () => _downloadTemplate(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
// 导入按钮
|
// 导入按钮
|
||||||
CommonButton(
|
CommonButton(
|
||||||
text: l10n?.importProgram ?? '导入',
|
text: l10n?.importProgram ?? '导入',
|
||||||
@@ -407,7 +417,7 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
|||||||
// 选择文件
|
// 选择文件
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: ['json'],
|
allowedExtensions: ['xlsx'],
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -417,38 +427,39 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
|||||||
|
|
||||||
final file = result.files.first;
|
final file = result.files.first;
|
||||||
if (file.path == null) {
|
if (file.path == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (!context.mounted) return;
|
||||||
SnackBar(
|
ToastService.showError(context, '无法读取文件');
|
||||||
content: Text('无法读取文件'),
|
|
||||||
backgroundColor: AppTheme.errorColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取文件内容
|
// 解析并写入数据库
|
||||||
final jsonContent = await File(file.path!).readAsString();
|
final importedCount =
|
||||||
|
await ExcelImportService.instance.importFromExcel(File(file.path!));
|
||||||
// 导入程序
|
|
||||||
final importedCount = await ProgramImportService.instance.importFromJson(jsonContent);
|
|
||||||
|
|
||||||
// 刷新程序列表
|
// 刷新程序列表
|
||||||
ref.read(programsProvider.notifier).loadPrograms();
|
ref.read(programsProvider.notifier).loadPrograms();
|
||||||
|
|
||||||
// 显示结果
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (importedCount > 0) {
|
||||||
SnackBar(
|
ToastService.showSuccess(context, '成功导入 $importedCount 个程序');
|
||||||
content: Text('成功导入 $importedCount 个程序'),
|
} else {
|
||||||
backgroundColor: importedCount > 0 ? AppTheme.successColor : AppTheme.warningColor,
|
ToastService.showWarning(context, '未导入新程序(编号可能已存在)');
|
||||||
),
|
}
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (!context.mounted) return;
|
||||||
SnackBar(
|
ToastService.showError(context, '导入失败: ${e.toString()}');
|
||||||
content: Text('导入失败: ${e.toString()}'),
|
}
|
||||||
backgroundColor: AppTheme.errorColor,
|
}
|
||||||
),
|
|
||||||
);
|
/// 下载 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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.4"
|
version: "0.13.4"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.6.1"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -89,6 +97,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
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:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -169,6 +185,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -277,6 +309,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.1.3"
|
version: "15.1.3"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
hotreloader:
|
hotreloader:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -293,6 +333,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
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:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -365,6 +421,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
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:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -381,6 +445,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
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:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -405,6 +493,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -437,6 +533,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
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:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -714,6 +818,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -724,4 +836,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.11.5 <4.0.0"
|
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
|
file_picker: ^8.1.7
|
||||||
|
|
||||||
|
# 路径解析(保存导入/导出文件)
|
||||||
|
path_provider: ^2.1.5
|
||||||
|
|
||||||
|
# Excel 读写(生成导入模板 + 解析用户填好的 .xlsx)
|
||||||
|
excel: ^4.0.2
|
||||||
|
|
||||||
# 国际化
|
# 国际化
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user