- 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>
516 lines
17 KiB
Dart
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 ?? '确认'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |