chore(project): 初始化项目基础配置文件

- 添加 CodeGraph、Android 和通用 gitignore 配置
- 创建项目元数据文件跟踪 Flutter 项目属性
- 添加 Codex AI 指导文档 AGENTS.md 说明项目架构
- 配置代码分析选项 analysis_options.yaml
- 设置 Android 应用清单权限和 Kiosk 模式配置
- 实现中英文国际化支持 AppLocalizations
- 配置 GoRouter 应用路由导航
- 创建明亮工业控制风格的主题配置 AppTheme
This commit is contained in:
Developer
2026-06-04 11:19:44 +08:00
commit 5d28bf631b
85 changed files with 21423 additions and 0 deletions

View 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,
);
}
}

View 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,
);
}
}

View 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 ?? '确认'),
),
],
),
);
}
}

View 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();
});

View 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});
}
}

View 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],
);
}
}
}

View 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,
),
);
}
}
}