chore(project): 初始化项目基础配置文件
- 添加 CodeGraph、Android 和通用 gitignore 配置 - 创建项目元数据文件跟踪 Flutter 项目属性 - 添加 Codex AI 指导文档 AGENTS.md 说明项目架构 - 配置代码分析选项 analysis_options.yaml - 设置 Android 应用清单权限和 Kiosk 模式配置 - 实现中英文国际化支持 AppLocalizations - 配置 GoRouter 应用路由导航 - 创建明亮工业控制风格的主题配置 AppTheme
This commit is contained in:
509
lib/features/programs/pages/programs_page.dart
Normal file
509
lib/features/programs/pages/programs_page.dart
Normal file
@@ -0,0 +1,509 @@
|
||||
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/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';
|
||||
|
||||
/// 程序管理页面
|
||||
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: [
|
||||
// 返回按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
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?.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(
|
||||
'状态',
|
||||
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 ? '启用' : '停用',
|
||||
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 {
|
||||
try {
|
||||
// 选择文件
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
allowMultiple: false,
|
||||
);
|
||||
|
||||
if (result == null || result.files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final file = result.files.first;
|
||||
if (file.path == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('无法读取文件'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
final jsonContent = await File(file.path!).readAsString();
|
||||
|
||||
// 导入程序
|
||||
final importedCount = await ProgramImportService.instance.importFromJson(jsonContent);
|
||||
|
||||
// 刷新程序列表
|
||||
ref.read(programsProvider.notifier).loadPrograms();
|
||||
|
||||
// 显示结果
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('成功导入 $importedCount 个程序'),
|
||||
backgroundColor: importedCount > 0 ? AppTheme.successColor : AppTheme.warningColor,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('导入失败: ${e.toString()}'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示编辑对话框
|
||||
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
|
||||
? '确定要删除此程序吗?'
|
||||
: '确定要删除选中的 ${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 ?? '确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user