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:
Developer
2026-06-04 15:27:34 +08:00
parent d53c41c300
commit d91791edaf
7 changed files with 504 additions and 153 deletions

View File

@@ -5,11 +5,13 @@ import 'package:file_picker/file_picker.dart';
import 'dart:io';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/services/toast_service.dart';
import '../../../shared/widgets/common_button.dart';
import '../models/program.dart';
import '../providers/programs_provider.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 {
@@ -64,6 +66,14 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
onPressed: () => _showAddDialog(context, ref),
),
const SizedBox(width: 12),
// 下载模板按钮
CommonButton(
text: l10n?.downloadTemplate ?? '下载模板',
icon: Icons.file_download,
type: ButtonType.secondary,
onPressed: () => _downloadTemplate(context),
),
const SizedBox(width: 12),
// 导入按钮
CommonButton(
text: l10n?.importProgram ?? '导入',
@@ -407,7 +417,7 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
// 选择文件
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
allowedExtensions: ['xlsx'],
allowMultiple: false,
);
@@ -417,38 +427,39 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
final file = result.files.first;
if (file.path == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('无法读取文件'),
backgroundColor: AppTheme.errorColor,
),
);
if (!context.mounted) return;
ToastService.showError(context, '无法读取文件');
return;
}
// 读取文件内容
final jsonContent = await File(file.path!).readAsString();
// 导入程序
final importedCount = await ProgramImportService.instance.importFromJson(jsonContent);
// 解析并写入数据库
final importedCount =
await ExcelImportService.instance.importFromExcel(File(file.path!));
// 刷新程序列表
ref.read(programsProvider.notifier).loadPrograms();
// 显示结果
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('成功导入 $importedCount 个程序'),
backgroundColor: importedCount > 0 ? AppTheme.successColor : AppTheme.warningColor,
),
);
if (!context.mounted) return;
if (importedCount > 0) {
ToastService.showSuccess(context, '成功导入 $importedCount 个程序');
} else {
ToastService.showWarning(context, '未导入新程序(编号可能已存在)');
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('导入失败: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
),
);
if (!context.mounted) return;
ToastService.showError(context, '导入失败: ${e.toString()}');
}
}
/// 下载 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()}');
}
}