Files
kuaishai2/lib/features/programs/pages/programs_page.dart
Developer 3d849bd468 feat(i18n): 完成全量 UI 文本国际化,替换所有硬编码中文为 AppLocalizations 调用
- core/localization: 新增约 60 个翻译键(含参数化方法),中英双语覆盖
- shared/widgets: CommonDialog 默认参数国际化
- features/home: 完成页操作步骤指引、状态栏串口连接状态、程序列表状态标签
- features/programs: 表头状态列、表单验证提示、导入/模板操作反馈、删除确认(参数化)
- features/program_detail: 步骤列表/表单标题、删除确认、速度档位显示(参数化)
- features/device: run_state_provider 错误消息改为错误码
- features/settings: 升级页、密码面板、语言面板、U盘导入面板、串口配置面板全部替换

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-12 15:09:47 +08:00

516 lines
17 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
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/excel_import_service.dart';
import '../services/excel_template_service.dart';
/// 程序管理页面
class ProgramsPage extends ConsumerStatefulWidget {
const ProgramsPage({super.key});
@override
ConsumerState<ProgramsPage> createState() => _ProgramsPageState();
}
class _ProgramsPageState extends ConsumerState<ProgramsPage> {
final Set<int> _selectedIds = {};
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final programsState = ref.watch(programsProvider);
return Scaffold(
body: Container(
color: AppTheme.backgroundColor,
child: Column(
children: [
// 顶部导航栏
Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Text(
l10n?.programs ?? '程序管理',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
// 新增按钮
CommonButton(
text: l10n?.addProgram ?? '新增',
icon: Icons.add,
type: ButtonType.primary,
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 ?? '导入',
icon: Icons.file_upload,
type: ButtonType.secondary,
onPressed: () => _importPrograms(context, ref),
),
],
),
),
// 程序列表表格
Expanded(
child: programsState.isLoading
? const Center(child: CircularProgressIndicator())
: programsState.programs.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_open,
size: 64,
color: AppTheme.idleColor,
),
const SizedBox(height: 16),
Text(
l10n?.noData ?? '暂无数据',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
),
),
],
),
)
: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(2, 2),
),
],
),
child: Column(
children: [
// 表头
_buildTableHeader(l10n, programsState.programs),
// 表格内容
Expanded(
child: ListView.builder(
itemCount: programsState.programs.length,
itemBuilder: (context, index) {
final program = programsState.programs[index];
final isSelected = _selectedIds.contains(program.id);
return _buildTableRow(
context,
ref,
l10n,
program,
isSelected,
index == programsState.programs.length - 1,
);
},
),
),
],
),
),
),
// 底部操作栏
if (programsState.programs.isNotEmpty)
Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
Text(
'${l10n?.selected ?? '已选择'}: ${_selectedIds.length}',
style: TextStyle(color: AppTheme.textSecondary),
),
const Spacer(),
if (_selectedIds.isNotEmpty)
CommonButton(
text: l10n?.deleteProgram ?? '删除',
icon: Icons.delete,
type: ButtonType.danger,
onPressed: () => _showDeleteConfirmDialog(
context,
ref,
l10n,
_selectedIds.toList(),
),
),
],
),
),
],
),
),
);
}
/// 表头
Widget _buildTableHeader(AppLocalizations? l10n, List<Program> programs) {
final allSelected = _selectedIds.length == programs.length && programs.isNotEmpty;
return Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
),
child: Row(
children: [
// 复选框
SizedBox(
width: 50,
child: Checkbox(
value: allSelected,
onChanged: (value) {
setState(() {
if (value == true) {
_selectedIds.clear();
_selectedIds.addAll(programs.map((p) => p.id!));
} else {
_selectedIds.clear();
}
});
},
),
),
// 编号
SizedBox(
width: 100,
child: Text(
l10n?.programCode ?? '编号',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
// 名称
Expanded(
flex: 2,
child: Text(
l10n?.programName ?? '名称',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
// 创建时间
Expanded(
child: Text(
l10n?.createTime ?? '创建时间',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
// 状态
SizedBox(
width: 80,
child: Text(
l10n?.status ?? '状态',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
// 操作
SizedBox(
width: 150,
child: Text(
l10n?.detail ?? '操作',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
),
),
],
),
);
}
/// 表格行
Widget _buildTableRow(
BuildContext context,
WidgetRef ref,
AppLocalizations? l10n,
Program program,
bool isSelected,
bool isLast,
) {
return Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryLight.withValues(alpha: 0.2) : null,
border: isLast
? null
: Border(
bottom: BorderSide(
color: AppTheme.idleColor.withValues(alpha: 0.2),
),
),
),
child: Row(
children: [
// 复选框
SizedBox(
width: 50,
child: Checkbox(
value: isSelected,
onChanged: (value) {
setState(() {
if (value == true) {
_selectedIds.add(program.id!);
} else {
_selectedIds.remove(program.id!);
}
});
},
),
),
// 编号
SizedBox(
width: 100,
child: Text(
program.code,
style: TextStyle(color: AppTheme.textPrimary),
),
),
// 名称
Expanded(
flex: 2,
child: Text(
program.name,
style: TextStyle(color: AppTheme.textPrimary),
),
),
// 创建时间
Expanded(
child: Text(
program.createdAt,
style: TextStyle(color: AppTheme.textSecondary),
),
),
// 状态
SizedBox(
width: 80,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: program.status == 1
? AppTheme.successColor.withValues(alpha: 0.1)
: AppTheme.idleColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
program.status == 1 ? (l10n?.enabled ?? '启用') : (l10n?.disabled ?? '停用'),
style: TextStyle(
color: program.status == 1
? AppTheme.successColor
: AppTheme.idleColor,
fontSize: 12,
),
textAlign: TextAlign.center,
),
),
),
// 操作按钮
SizedBox(
width: 150,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.edit, size: 20),
color: AppTheme.primaryColor,
onPressed: () => _showEditDialog(context, ref, program),
),
IconButton(
icon: const Icon(Icons.delete, size: 20),
color: AppTheme.errorColor,
onPressed: () => _showDeleteConfirmDialog(
context,
ref,
l10n,
[program.id!],
),
),
IconButton(
icon: const Icon(Icons.visibility, size: 20),
color: AppTheme.textSecondary,
onPressed: () => context.go('/programs/${program.id}'),
),
],
),
),
],
),
);
}
/// 显示新增对话框
void _showAddDialog(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => const ProgramFormDialog(),
);
}
/// 导入程序
Future<void> _importPrograms(BuildContext context, WidgetRef ref) async {
final l10n = AppLocalizations.of(context);
try {
// 选择文件
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['xlsx'],
allowMultiple: false,
);
if (result == null || result.files.isEmpty) {
return;
}
final file = result.files.first;
if (file.path == null) {
if (!context.mounted) return;
ToastService.showError(context, l10n?.cannotReadFile ?? '无法读取文件');
return;
}
// 解析并写入数据库
final importedCount =
await ExcelImportService.instance.importFromExcel(File(file.path!));
// 刷新程序列表
ref.read(programsProvider.notifier).loadPrograms();
if (!context.mounted) return;
if (importedCount > 0) {
ToastService.showSuccess(context, l10n?.processedPrograms(importedCount) ?? '成功处理 $importedCount 个程序');
} else {
ToastService.showWarning(context, l10n?.noValidProgramData ?? 'Excel 中无有效程序数据');
}
} catch (e) {
if (!context.mounted) return;
ToastService.showError(context, '${l10n?.importFailed ?? '导入失败'}: ${e.toString()}');
}
}
/// 下载 Excel 模板
Future<void> _downloadTemplate(BuildContext context) async {
final l10n = AppLocalizations.of(context);
try {
final path = await ExcelTemplateService.instance.generateTemplate();
if (!context.mounted) return;
ToastService.showSuccess(context, '${l10n?.templateSaved ?? '模板已保存'}: $path');
} catch (e) {
if (!context.mounted) return;
ToastService.showError(context, '${l10n?.generateTemplateFailed ?? '生成模板失败'}: ${e.toString()}');
}
}
/// 显示编辑对话框
void _showEditDialog(BuildContext context, WidgetRef ref, Program program) {
showDialog(
context: context,
builder: (context) => ProgramFormDialog(program: program),
);
}
/// 显示删除确认对话框
void _showDeleteConfirmDialog(
BuildContext context,
WidgetRef ref,
AppLocalizations? l10n,
List<int> ids,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n?.confirm ?? '确认'),
content: Text(
ids.length == 1
? (l10n?.deleteConfirmSingle ?? '确定要删除此程序吗?')
: (l10n?.deleteConfirmMultiple(ids.length) ?? '确定要删除选中的 ${ids.length} 个程序吗?'),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n?.cancel ?? '取消'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.errorColor,
foregroundColor: Colors.white,
),
onPressed: () async {
final notifier = ref.read(programsProvider.notifier);
await notifier.deletePrograms(ids);
setState(() {
_selectedIds.removeAll(ids);
});
Navigator.of(context).pop();
},
child: Text(l10n?.confirm ?? '确认'),
),
],
),
);
}
}