chore(project): 初始化项目基础配置文件
- 添加 CodeGraph、Android 和通用 gitignore 配置 - 创建项目元数据文件跟踪 Flutter 项目属性 - 添加 Codex AI 指导文档 AGENTS.md 说明项目架构 - 配置代码分析选项 analysis_options.yaml - 设置 Android 应用清单权限和 Kiosk 模式配置 - 实现中英文国际化支持 AppLocalizations - 配置 GoRouter 应用路由导航 - 创建明亮工业控制风格的主题配置 AppTheme
This commit is contained in:
52
lib/features/programs/models/program.dart
Normal file
52
lib/features/programs/models/program.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
/// 程序模型
|
||||
class Program {
|
||||
final int? id;
|
||||
final String code;
|
||||
final String name;
|
||||
final String createdAt;
|
||||
final int status; // 1: 启用, 0: 停用
|
||||
|
||||
Program({
|
||||
this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
this.status = 1,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'code': code,
|
||||
'name': name,
|
||||
'created_at': createdAt,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
|
||||
factory Program.fromMap(Map<String, dynamic> map) {
|
||||
return Program(
|
||||
id: map['id'] as int?,
|
||||
code: map['code'] as String,
|
||||
name: map['name'] as String,
|
||||
createdAt: map['created_at'] as String,
|
||||
status: map['status'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
|
||||
Program copyWith({
|
||||
int? id,
|
||||
String? code,
|
||||
String? name,
|
||||
String? createdAt,
|
||||
int? status,
|
||||
}) {
|
||||
return Program(
|
||||
id: id ?? this.id,
|
||||
code: code ?? this.code,
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/features/programs/models/step.dart
Normal file
94
lib/features/programs/models/step.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
/// 步骤模型
|
||||
class Step {
|
||||
final int? id;
|
||||
final int programId;
|
||||
final int stepNo;
|
||||
final String position;
|
||||
final String name;
|
||||
final int mixTime;
|
||||
final int magnetTime;
|
||||
final int volume;
|
||||
final String mixSpeed;
|
||||
final String blowSpeed;
|
||||
final int blowTime;
|
||||
final int needleSpeed;
|
||||
|
||||
Step({
|
||||
this.id,
|
||||
required this.programId,
|
||||
required this.stepNo,
|
||||
required this.position,
|
||||
required this.name,
|
||||
this.mixTime = 0,
|
||||
this.magnetTime = 0,
|
||||
this.volume = 0,
|
||||
this.mixSpeed = '中速',
|
||||
this.blowSpeed = '中速',
|
||||
this.blowTime = 0,
|
||||
this.needleSpeed = 5,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'program_id': programId,
|
||||
'step_no': stepNo,
|
||||
'position': position,
|
||||
'name': name,
|
||||
'mix_time': mixTime,
|
||||
'magnet_time': magnetTime,
|
||||
'volume': volume,
|
||||
'mix_speed': mixSpeed,
|
||||
'blow_speed': blowSpeed,
|
||||
'blow_time': blowTime,
|
||||
'needle_speed': needleSpeed,
|
||||
};
|
||||
}
|
||||
|
||||
factory Step.fromMap(Map<String, dynamic> map) {
|
||||
return Step(
|
||||
id: map['id'] as int?,
|
||||
programId: map['program_id'] as int,
|
||||
stepNo: map['step_no'] as int,
|
||||
position: map['position'] as String,
|
||||
name: map['name'] as String,
|
||||
mixTime: map['mix_time'] as int? ?? 0,
|
||||
magnetTime: map['magnet_time'] as int? ?? 0,
|
||||
volume: map['volume'] as int? ?? 0,
|
||||
mixSpeed: map['mix_speed'] as String? ?? '中速',
|
||||
blowSpeed: map['blow_speed'] as String? ?? '中速',
|
||||
blowTime: map['blow_time'] as int? ?? 0,
|
||||
needleSpeed: map['needle_speed'] as int? ?? 5,
|
||||
);
|
||||
}
|
||||
|
||||
Step copyWith({
|
||||
int? id,
|
||||
int? programId,
|
||||
int? stepNo,
|
||||
String? position,
|
||||
String? name,
|
||||
int? mixTime,
|
||||
int? magnetTime,
|
||||
int? volume,
|
||||
String? mixSpeed,
|
||||
String? blowSpeed,
|
||||
int? blowTime,
|
||||
int? needleSpeed,
|
||||
}) {
|
||||
return Step(
|
||||
id: id ?? this.id,
|
||||
programId: programId ?? this.programId,
|
||||
stepNo: stepNo ?? this.stepNo,
|
||||
position: position ?? this.position,
|
||||
name: name ?? this.name,
|
||||
mixTime: mixTime ?? this.mixTime,
|
||||
magnetTime: magnetTime ?? this.magnetTime,
|
||||
volume: volume ?? this.volume,
|
||||
mixSpeed: mixSpeed ?? this.mixSpeed,
|
||||
blowSpeed: blowSpeed ?? this.blowSpeed,
|
||||
blowTime: blowTime ?? this.blowTime,
|
||||
needleSpeed: needleSpeed ?? this.needleSpeed,
|
||||
);
|
||||
}
|
||||
}
|
||||
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 ?? '确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
192
lib/features/programs/providers/programs_provider.dart
Normal file
192
lib/features/programs/providers/programs_provider.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/database/database_service.dart';
|
||||
import '../models/program.dart';
|
||||
|
||||
/// 程序列表状态
|
||||
class ProgramsState {
|
||||
final List<Program> programs;
|
||||
final int? selectedProgramId;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const ProgramsState({
|
||||
this.programs = const [],
|
||||
this.selectedProgramId,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
ProgramsState copyWith({
|
||||
List<Program>? programs,
|
||||
int? selectedProgramId,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
bool clearSelection = false,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return ProgramsState(
|
||||
programs: programs ?? this.programs,
|
||||
selectedProgramId: clearSelection ? null : (selectedProgramId ?? this.selectedProgramId),
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取选中的程序
|
||||
Program? get selectedProgram {
|
||||
if (selectedProgramId == null) return null;
|
||||
return programs.where((p) => p.id == selectedProgramId).firstOrNull;
|
||||
}
|
||||
}
|
||||
|
||||
/// 程序列表 Notifier
|
||||
class ProgramsNotifier extends StateNotifier<ProgramsState> {
|
||||
final DatabaseService _db;
|
||||
|
||||
ProgramsNotifier(this._db) : super(const ProgramsState()) {
|
||||
loadPrograms();
|
||||
}
|
||||
|
||||
/// 加载所有程序
|
||||
Future<void> loadPrograms() async {
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
final db = await _db.database;
|
||||
final maps = await db.query('programs', orderBy: 'created_at DESC');
|
||||
final programs = maps.map((m) => Program.fromMap(m)).toList();
|
||||
|
||||
state = state.copyWith(programs: programs, isLoading: false);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 选择程序
|
||||
void selectProgram(int? programId) {
|
||||
state = state.copyWith(selectedProgramId: programId);
|
||||
}
|
||||
|
||||
/// 清除选择
|
||||
void clearSelection() {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
/// 新增程序
|
||||
Future<bool> addProgram(Program program) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.insert('programs', program.toMap());
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新程序
|
||||
Future<bool> updateProgram(Program program) async {
|
||||
if (program.id == null) return false;
|
||||
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.update(
|
||||
'programs',
|
||||
program.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [program.id],
|
||||
);
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除程序
|
||||
Future<bool> deleteProgram(int programId) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.delete('programs', where: 'id = ?', whereArgs: [programId]);
|
||||
|
||||
// 如果删除的是选中的程序,清除选择
|
||||
if (state.selectedProgramId == programId) {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量删除程序
|
||||
Future<bool> deletePrograms(List<int> programIds) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.delete(
|
||||
'programs',
|
||||
where: 'id IN (${programIds.map((_) => '?').join(',')})',
|
||||
whereArgs: programIds,
|
||||
);
|
||||
|
||||
// 如果删除的是选中的程序,清除选择
|
||||
if (programIds.contains(state.selectedProgramId)) {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换程序状态
|
||||
Future<bool> toggleStatus(int programId) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
final program = state.programs.where((p) => p.id == programId).firstOrNull;
|
||||
if (program == null) return false;
|
||||
|
||||
await db.update(
|
||||
'programs',
|
||||
{'status': program.status == 1 ? 0 : 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [programId],
|
||||
);
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 数据库服务 Provider
|
||||
final databaseServiceProvider = Provider<DatabaseService>((ref) {
|
||||
return DatabaseService.instance;
|
||||
});
|
||||
|
||||
/// 程序列表 Provider
|
||||
final programsProvider =
|
||||
StateNotifierProvider<ProgramsNotifier, ProgramsState>((ref) {
|
||||
final db = ref.watch(databaseServiceProvider);
|
||||
return ProgramsNotifier(db);
|
||||
});
|
||||
|
||||
/// 选中的程序 Provider
|
||||
final selectedProgramProvider = Provider<Program?>((ref) {
|
||||
return ref.watch(programsProvider).selectedProgram;
|
||||
});
|
||||
|
||||
/// 启用的程序列表 Provider
|
||||
final enabledProgramsProvider = Provider<List<Program>>((ref) {
|
||||
return ref.watch(programsProvider).programs.where((p) => p.status == 1).toList();
|
||||
});
|
||||
126
lib/features/programs/services/program_import_service.dart
Normal file
126
lib/features/programs/services/program_import_service.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
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,
|
||||
);
|
||||
|
||||
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,
|
||||
mixSpeed: stepData['mixSpeed'] as String? ?? '中速',
|
||||
blowSpeed: stepData['blowSpeed'] as String? ?? '中速',
|
||||
blowTime: stepData['blowTime'] as int? ?? 0,
|
||||
needleSpeed: stepData['needleSpeed'] 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,
|
||||
'steps': steps.map((s) => {
|
||||
'position': s.position,
|
||||
'name': s.name,
|
||||
'mixTime': s.mixTime,
|
||||
'magnetTime': s.magnetTime,
|
||||
'volume': s.volume,
|
||||
'mixSpeed': s.mixSpeed,
|
||||
'blowSpeed': s.blowSpeed,
|
||||
'blowTime': s.blowTime,
|
||||
'needleSpeed': s.needleSpeed,
|
||||
}).toList(),
|
||||
});
|
||||
}
|
||||
|
||||
return jsonEncode({'programs': programs});
|
||||
}
|
||||
}
|
||||
156
lib/features/programs/services/program_service.dart
Normal file
156
lib/features/programs/services/program_service.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import '../../../core/database/database_service.dart';
|
||||
import '../models/program.dart';
|
||||
import '../models/step.dart';
|
||||
|
||||
/// 程序服务
|
||||
/// 封装程序和步骤的数据库操作
|
||||
class ProgramService {
|
||||
static final ProgramService instance = ProgramService._internal();
|
||||
final DatabaseService _db = DatabaseService.instance;
|
||||
|
||||
ProgramService._internal();
|
||||
|
||||
/// 获取所有程序
|
||||
Future<List<Program>> getAllPrograms() async {
|
||||
final database = await _db.database;
|
||||
final maps = await database.query('programs', orderBy: 'created_at DESC');
|
||||
return maps.map((m) => Program.fromMap(m)).toList();
|
||||
}
|
||||
|
||||
/// 根据ID获取程序
|
||||
Future<Program?> getProgramById(int id) async {
|
||||
final database = await _db.database;
|
||||
final maps = await database.query(
|
||||
'programs',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (maps.isEmpty) return null;
|
||||
return Program.fromMap(maps.first);
|
||||
}
|
||||
|
||||
/// 新增程序
|
||||
Future<int> addProgram(Program program) async {
|
||||
final database = await _db.database;
|
||||
return await database.insert('programs', program.toMap());
|
||||
}
|
||||
|
||||
/// 更新程序
|
||||
Future<bool> updateProgram(Program program) async {
|
||||
if (program.id == null) return false;
|
||||
final database = await _db.database;
|
||||
final count = await database.update(
|
||||
'programs',
|
||||
program.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [program.id],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 删除程序(含步骤)
|
||||
Future<bool> deleteProgram(int id) async {
|
||||
final database = await _db.database;
|
||||
// 先删除关联的步骤
|
||||
await database.delete('steps', where: 'program_id = ?', whereArgs: [id]);
|
||||
// 再删除程序
|
||||
final count = await database.delete('programs', where: 'id = ?', whereArgs: [id]);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 批量删除程序
|
||||
Future<bool> deletePrograms(List<int> ids) async {
|
||||
if (ids.isEmpty) return true;
|
||||
final database = await _db.database;
|
||||
// 先删除关联的步骤
|
||||
await database.delete(
|
||||
'steps',
|
||||
where: 'program_id IN (${ids.map((_) => '?').join(',')})',
|
||||
whereArgs: ids,
|
||||
);
|
||||
// 再删除程序
|
||||
final count = await database.delete(
|
||||
'programs',
|
||||
where: 'id IN (${ids.map((_) => '?').join(',')})',
|
||||
whereArgs: ids,
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 切换程序状态
|
||||
Future<bool> toggleProgramStatus(int id) async {
|
||||
final database = await _db.database;
|
||||
final program = await getProgramById(id);
|
||||
if (program == null) return false;
|
||||
final count = await database.update(
|
||||
'programs',
|
||||
{'status': program.status == 1 ? 0 : 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 获取程序的步骤列表
|
||||
Future<List<Step>> getStepsByProgramId(int programId) async {
|
||||
final database = await _db.database;
|
||||
final maps = await database.query(
|
||||
'steps',
|
||||
where: 'program_id = ?',
|
||||
whereArgs: [programId],
|
||||
orderBy: 'step_no ASC',
|
||||
);
|
||||
return maps.map((m) => Step.fromMap(m)).toList();
|
||||
}
|
||||
|
||||
/// 新增步骤
|
||||
Future<int> addStep(Step step) async {
|
||||
final database = await _db.database;
|
||||
return await database.insert('steps', step.toMap());
|
||||
}
|
||||
|
||||
/// 更新步骤
|
||||
Future<bool> updateStep(Step step) async {
|
||||
if (step.id == null) return false;
|
||||
final database = await _db.database;
|
||||
final count = await database.update(
|
||||
'steps',
|
||||
step.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [step.id],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 删除步骤
|
||||
Future<bool> deleteStep(int id) async {
|
||||
final database = await _db.database;
|
||||
final count = await database.delete('steps', where: 'id = ?', whereArgs: [id]);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 批量删除步骤
|
||||
Future<bool> deleteSteps(List<int> ids) async {
|
||||
if (ids.isEmpty) return true;
|
||||
final database = await _db.database;
|
||||
final count = await database.delete(
|
||||
'steps',
|
||||
where: 'id IN (${ids.map((_) => '?').join(',')})',
|
||||
whereArgs: ids,
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 更新步骤排序
|
||||
Future<void> reorderSteps(int programId, List<int> stepIds) async {
|
||||
final database = await _db.database;
|
||||
for (int i = 0; i < stepIds.length; i++) {
|
||||
await database.update(
|
||||
'steps',
|
||||
{'step_no': i + 1},
|
||||
where: 'id = ? AND program_id = ?',
|
||||
whereArgs: [stepIds[i], programId],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
188
lib/features/programs/widgets/program_form_dialog.dart
Normal file
188
lib/features/programs/widgets/program_form_dialog.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
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';
|
||||
|
||||
/// 程序表单弹窗
|
||||
/// 用于新增和编辑程序
|
||||
class ProgramFormDialog extends ConsumerStatefulWidget {
|
||||
final Program? program;
|
||||
|
||||
const ProgramFormDialog({super.key, this.program});
|
||||
|
||||
@override
|
||||
ConsumerState<ProgramFormDialog> createState() => _ProgramFormDialogState();
|
||||
}
|
||||
|
||||
class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _codeController;
|
||||
late TextEditingController _nameController;
|
||||
bool _isEnabled = true;
|
||||
bool _isSaving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_codeController = TextEditingController(text: widget.program?.code ?? '');
|
||||
_nameController = TextEditingController(text: widget.program?.name ?? '');
|
||||
_isEnabled = widget.program?.status == 1;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_codeController.dispose();
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final isEditing = widget.program != null;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
isEditing
|
||||
? (l10n?.editProgram ?? '编辑程序')
|
||||
: (l10n?.addProgram ?? '新增程序'),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 编号输入
|
||||
TextFormField(
|
||||
controller: _codeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n?.programCode ?? '编号',
|
||||
hintText: '例如: P001',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入编号';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 名称输入
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n?.programName ?? '名称',
|
||||
hintText: '请输入程序名称',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 状态开关
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'状态',
|
||||
style: TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: _isEnabled,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isEnabled = value;
|
||||
});
|
||||
},
|
||||
activeColor: AppTheme.successColor,
|
||||
),
|
||||
Text(
|
||||
_isEnabled ? '启用' : '停用',
|
||||
style: TextStyle(
|
||||
color: _isEnabled ? AppTheme.successColor : AppTheme.idleColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isSaving ? null : () => Navigator.of(context).pop(),
|
||||
child: Text(l10n?.cancel ?? '取消'),
|
||||
),
|
||||
CommonButton(
|
||||
text: l10n?.save ?? '保存',
|
||||
icon: Icons.save,
|
||||
type: ButtonType.primary,
|
||||
isLoading: _isSaving,
|
||||
onPressed: _isSaving ? null : () => _saveProgram(context, ref, l10n),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 保存程序
|
||||
Future<void> _saveProgram(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppLocalizations? l10n,
|
||||
) async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isSaving = true;
|
||||
});
|
||||
|
||||
final notifier = ref.read(programsProvider.notifier);
|
||||
final now = DateTime.now().toString().substring(0, 10);
|
||||
|
||||
final program = Program(
|
||||
id: widget.program?.id,
|
||||
code: _codeController.text.trim(),
|
||||
name: _nameController.text.trim(),
|
||||
createdAt: widget.program?.createdAt ?? now,
|
||||
status: _isEnabled ? 1 : 0,
|
||||
);
|
||||
|
||||
bool success;
|
||||
if (widget.program != null) {
|
||||
success = await notifier.updateProgram(program);
|
||||
} else {
|
||||
success = await notifier.addProgram(program);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
});
|
||||
|
||||
if (success) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('保存失败,请检查编号是否重复'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user