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,164 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
/// 数据库服务
class DatabaseService {
static final DatabaseService instance = DatabaseService._internal();
static Database? _database;
DatabaseService._internal();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'kuaishai.db');
return await openDatabase(
path,
version: 2,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
// 程序表
await db.execute('''
CREATE TABLE programs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
status INTEGER DEFAULT 1
)
''');
// 步骤表
await db.execute('''
CREATE TABLE steps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
program_id INTEGER NOT NULL,
step_no INTEGER NOT NULL,
position TEXT NOT NULL,
name TEXT NOT NULL,
mix_time INTEGER DEFAULT 0,
magnet_time INTEGER DEFAULT 0,
volume INTEGER DEFAULT 0,
mix_speed TEXT DEFAULT '中速',
blow_speed TEXT DEFAULT '中速',
blow_time INTEGER DEFAULT 0,
needle_speed INTEGER DEFAULT 5,
FOREIGN KEY (program_id) REFERENCES programs(id) ON DELETE CASCADE
)
''');
// 设置表(密码存储)
await db.execute('''
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
''');
// 初始化默认密码
await db.insert('settings', {'key': 'password', 'value': '123456'});
}
/// 数据库升级
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// 添加 settings 表
await db.execute('''
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
''');
// 初始化默认密码
await db.insert('settings', {'key': 'password', 'value': '123456'});
}
}
Future<void> close() async {
if (_database != null) {
await _database!.close();
_database = null;
}
}
/// 初始化测试数据(仅调试模式使用)
Future<void> initTestData() async {
final db = await database;
// 检查是否已有数据
final count = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM programs'),
);
if (count != null && count > 0) return;
// 插入测试程序并添加步骤
final testPrograms = [
{'code': 'P001', 'name': '标准检测程序', 'created_at': '2026-05-19', 'status': 1},
{'code': 'P002', 'name': '快速检测程序', 'created_at': '2026-05-18', 'status': 1},
{'code': 'P003', 'name': '深度检测程序', 'created_at': '2026-05-17', 'status': 1},
{'code': 'P004', 'name': '样本预处理程序', 'created_at': '2026-05-16', 'status': 0},
{'code': 'P005', 'name': '磁珠分离程序', 'created_at': '2026-05-15', 'status': 1},
];
for (final program in testPrograms) {
final programId = await db.insert('programs', program);
// 为每个程序添加测试步骤
final testSteps = [
{
'program_id': programId,
'step_no': 1,
'position': 'A1',
'name': '混合',
'mix_time': 60,
'magnet_time': 0,
'volume': 100,
'mix_speed': '中速',
'blow_speed': '中速',
'blow_time': 0,
'needle_speed': 5,
},
{
'program_id': programId,
'step_no': 2,
'position': 'A1',
'name': '吸磁',
'mix_time': 0,
'magnet_time': 30,
'volume': 0,
'mix_speed': '中速',
'blow_speed': '中速',
'blow_time': 0,
'needle_speed': 5,
},
{
'program_id': programId,
'step_no': 3,
'position': 'A2',
'name': '吹气',
'mix_time': 0,
'magnet_time': 0,
'volume': 0,
'mix_speed': '中速',
'blow_speed': '高速',
'blow_time': 10,
'needle_speed': 8,
},
];
for (final step in testSteps) {
await db.insert('steps', step);
}
}
}
}

View File

@@ -0,0 +1,366 @@
import 'package:flutter/material.dart';
/// 应用国际化配置
class AppLocalizations {
final Locale locale;
AppLocalizations(this.locale);
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
// 状态栏
String get deviceName => _localizedValues[locale.languageCode]?['deviceName'] ?? '污水毒品前处理一体机';
String get running => _localizedValues[locale.languageCode]?['running'] ?? '运行中';
String get idle => _localizedValues[locale.languageCode]?['idle'] ?? '未运行';
String get lighting => _localizedValues[locale.languageCode]?['lighting'] ?? '照明';
// 程序管理
String get programs => _localizedValues[locale.languageCode]?['programs'] ?? '程序管理';
String get programList => _localizedValues[locale.languageCode]?['programList'] ?? '程序列表';
String get programName => _localizedValues[locale.languageCode]?['programName'] ?? '程序名称';
String get programCode => _localizedValues[locale.languageCode]?['programCode'] ?? '程序编号';
String get createTime => _localizedValues[locale.languageCode]?['createTime'] ?? '创建时间';
String get addProgram => _localizedValues[locale.languageCode]?['addProgram'] ?? '新增程序';
String get editProgram => _localizedValues[locale.languageCode]?['editProgram'] ?? '编辑程序';
String get deleteProgram => _localizedValues[locale.languageCode]?['deleteProgram'] ?? '删除程序';
String get importProgram => _localizedValues[locale.languageCode]?['importProgram'] ?? '导入程序';
String get viewDetails => _localizedValues[locale.languageCode]?['viewDetails'] ?? '查看详情';
String get selectedProgram => _localizedValues[locale.languageCode]?['selectedProgram'] ?? '当前选中程序';
String get selectedProgramLabel => _localizedValues[locale.languageCode]?['selectedProgramLabel'] ?? '当前选中';
String get availablePrograms => _localizedValues[locale.languageCode]?['availablePrograms'] ?? '可用程序';
String get ceramicNotInstalled => _localizedValues[locale.languageCode]?['ceramicNotInstalled'] ?? '瓷套棒: 未安装 — 禁止启动';
String get ceramicInstalled => _localizedValues[locale.languageCode]?['ceramicInstalled'] ?? '瓷套棒: 已安装';
String get runningMonitor => _localizedValues[locale.languageCode]?['runningMonitor'] ?? '运行状态监控';
String get currentHole => _localizedValues[locale.languageCode]?['currentHole'] ?? '当前孔位';
String get stepParams => _localizedValues[locale.languageCode]?['stepParams'] ?? '步骤参数';
String get speed => _localizedValues[locale.languageCode]?['speed'] ?? '转速';
String get temperature => _localizedValues[locale.languageCode]?['temperature'] ?? '温度';
String get duration => _localizedValues[locale.languageCode]?['duration'] ?? '持续时间';
String get sampleVolume => _localizedValues[locale.languageCode]?['sampleVolume'] ?? '样品体积';
String get pleaseSelectProgram => _localizedValues[locale.languageCode]?['pleaseSelectProgram'] ?? '请选择要运行的程序';
// 运行控制
String get run => _localizedValues[locale.languageCode]?['run'] ?? '运行';
String get pause => _localizedValues[locale.languageCode]?['pause'] ?? '暂停';
String get continue_ => _localizedValues[locale.languageCode]?['continue'] ?? '继续';
String get stop => _localizedValues[locale.languageCode]?['stop'] ?? '停止';
String get startRun => _localizedValues[locale.languageCode]?['startRun'] ?? '开始运行';
String get currentStep => _localizedValues[locale.languageCode]?['currentStep'] ?? '当前步骤';
String get remainingTime => _localizedValues[locale.languageCode]?['remainingTime'] ?? '剩余时间';
String get progress => _localizedValues[locale.languageCode]?['progress'] ?? '进度';
String get ceramicSleeveConfirm => _localizedValues[locale.languageCode]?['ceramicSleeveConfirm'] ?? '运行前请确认已安装瓷套棒';
String get paused => _localizedValues[locale.languageCode]?['paused'] ?? '已暂停';
String get stopConfirm => _localizedValues[locale.languageCode]?['stopConfirm'] ?? '确定要停止当前运行的程序吗?';
String get currentProgram => _localizedValues[locale.languageCode]?['currentProgram'] ?? '当前程序';
String get backToHome => _localizedValues[locale.languageCode]?['backToHome'] ?? '返回首页';
String get runAgain => _localizedValues[locale.languageCode]?['runAgain'] ?? '重新运行';
String get deleteConfirm => _localizedValues[locale.languageCode]?['deleteConfirm'] ?? '确定要删除此程序吗?';
// 步骤参数
String get stepNo => _localizedValues[locale.languageCode]?['stepNo'] ?? '步骤编号';
String get position => _localizedValues[locale.languageCode]?['position'] ?? '孔位';
String get stepName => _localizedValues[locale.languageCode]?['stepName'] ?? '步骤名称';
String get mixTime => _localizedValues[locale.languageCode]?['mixTime'] ?? '混合时间';
String get magnetTime => _localizedValues[locale.languageCode]?['magnetTime'] ?? '吸磁时间';
String get volume => _localizedValues[locale.languageCode]?['volume'] ?? '容积';
String get mixSpeed => _localizedValues[locale.languageCode]?['mixSpeed'] ?? '混合速度';
String get blowSpeed => _localizedValues[locale.languageCode]?['blowSpeed'] ?? '吹气速度';
String get blowTime => _localizedValues[locale.languageCode]?['blowTime'] ?? '吹气时间';
String get needleSpeed => _localizedValues[locale.languageCode]?['needleSpeed'] ?? '下针速度';
// 速度选项
String get lowSpeed => _localizedValues[locale.languageCode]?['lowSpeed'] ?? '低速';
String get mediumSpeed => _localizedValues[locale.languageCode]?['mediumSpeed'] ?? '中速';
String get highSpeed => _localizedValues[locale.languageCode]?['highSpeed'] ?? '高速';
// 设置
String get settings => _localizedValues[locale.languageCode]?['settings'] ?? '系统设置';
String get language => _localizedValues[locale.languageCode]?['language'] ?? '语言设置';
String get password => _localizedValues[locale.languageCode]?['password'] ?? '密码修改';
String get upgrade => _localizedValues[locale.languageCode]?['upgrade'] ?? '软件升级';
String get usbImport => _localizedValues[locale.languageCode]?['usbImport'] ?? 'U盘导入';
// 通用
String get confirm => _localizedValues[locale.languageCode]?['confirm'] ?? '确认';
String get cancel => _localizedValues[locale.languageCode]?['cancel'] ?? '取消';
String get save => _localizedValues[locale.languageCode]?['save'] ?? '保存';
String get delete => _localizedValues[locale.languageCode]?['delete'] ?? '删除';
String get select => _localizedValues[locale.languageCode]?['select'] ?? '选择';
String get selected => _localizedValues[locale.languageCode]?['selected'] ?? '已选择';
String get detail => _localizedValues[locale.languageCode]?['detail'] ?? '详情';
String get noData => _localizedValues[locale.languageCode]?['noData'] ?? '暂无数据';
// 完成提示
String get runComplete => _localizedValues[locale.languageCode]?['runComplete'] ?? '运行完成';
String get sampleDropGuide => _localizedValues[locale.languageCode]?['sampleDropGuide'] ?? '请将样本滴入检测卡';
// 补充缺失的翻译
String get lightOn => _localizedValues[locale.languageCode]?['lightOn'] ?? '';
String get lightOff => _localizedValues[locale.languageCode]?['lightOff'] ?? '';
String get enabled => _localizedValues[locale.languageCode]?['enabled'] ?? '启用';
String get disabled => _localizedValues[locale.languageCode]?['disabled'] ?? '停用';
String get stepList => _localizedValues[locale.languageCode]?['stepList'] ?? '步骤列表';
String get operationSteps => _localizedValues[locale.languageCode]?['operationSteps'] ?? '操作步骤';
String get addStep => _localizedValues[locale.languageCode]?['addStep'] ?? '添加步骤';
String get editStep => _localizedValues[locale.languageCode]?['editStep'] ?? '编辑步骤';
String get deleteStep => _localizedValues[locale.languageCode]?['deleteStep'] ?? '删除步骤';
String get deleteStepConfirm => _localizedValues[locale.languageCode]?['deleteStepConfirm'] ?? '确定要删除此步骤吗?';
String get stepsCount => _localizedValues[locale.languageCode]?['stepsCount'] ?? '';
String get noSteps => _localizedValues[locale.languageCode]?['noSteps'] ?? '暂无步骤';
String get selectStepFirst => _localizedValues[locale.languageCode]?['selectStepFirst'] ?? '请选择或添加步骤';
String get oldPassword => _localizedValues[locale.languageCode]?['oldPassword'] ?? '原密码';
String get newPassword => _localizedValues[locale.languageCode]?['newPassword'] ?? '新密码';
String get confirmPassword => _localizedValues[locale.languageCode]?['confirmPassword'] ?? '确认新密码';
String get passwordMinLength => _localizedValues[locale.languageCode]?['passwordMinLength'] ?? '至少6位字符';
String get passwordChanged => _localizedValues[locale.languageCode]?['passwordChanged'] ?? '密码已修改';
String get passwordChangeFailed => _localizedValues[locale.languageCode]?['passwordChangeFailed'] ?? '密码修改失败';
String get oldPasswordError => _localizedValues[locale.languageCode]?['oldPasswordError'] ?? '原密码错误';
String get passwordMismatch => _localizedValues[locale.languageCode]?['passwordMismatch'] ?? '两次输入的新密码不一致';
String get fillAllFields => _localizedValues[locale.languageCode]?['fillAllFields'] ?? '请填写所有字段';
String get importSuccess => _localizedValues[locale.languageCode]?['importSuccess'] ?? '成功导入';
String get importFailed => _localizedValues[locale.languageCode]?['importFailed'] ?? '导入失败';
String get programsImported => _localizedValues[locale.languageCode]?['programsImported'] ?? '个程序';
String get usbDetected => _localizedValues[locale.languageCode]?['usbDetected'] ?? '检测到U盘';
String get usbNotDetected => _localizedValues[locale.languageCode]?['usbNotDetected'] ?? '未检测到U盘';
String get insertUsb => _localizedValues[locale.languageCode]?['insertUsb'] ?? '请插入U盘后重试';
String get detectingUsb => _localizedValues[locale.languageCode]?['detectingUsb'] ?? '正在检测U盘...';
String get currentVersion => _localizedValues[locale.languageCode]?['currentVersion'] ?? '当前版本';
String get latestVersion => _localizedValues[locale.languageCode]?['latestVersion'] ?? '已是最新版本';
String get updateAvailable => _localizedValues[locale.languageCode]?['updateAvailable'] ?? '有新版本可用';
String get checkUpdate => _localizedValues[locale.languageCode]?['checkUpdate'] ?? '检查更新';
static final Map<String, Map<String, String>> _localizedValues = {
'zh': {
'deviceName': '污水毒品前处理一体机',
'running': '运行中',
'idle': '未运行',
'lighting': '照明',
'programs': '程序管理',
'programList': '程序列表',
'programName': '程序名称',
'programCode': '程序编号',
'createTime': '创建时间',
'addProgram': '新增程序',
'editProgram': '编辑程序',
'deleteProgram': '删除程序',
'importProgram': '导入程序',
'viewDetails': '查看详情',
'selectedProgram': '当前选中程序',
'selectedProgramLabel': '当前选中',
'availablePrograms': '可用程序',
'ceramicNotInstalled': '瓷套棒: 未安装 — 禁止启动',
'ceramicInstalled': '瓷套棒: 已安装',
'runningMonitor': '运行状态监控',
'currentHole': '当前孔位',
'stepParams': '步骤参数',
'speed': '转速',
'temperature': '温度',
'duration': '持续时间',
'sampleVolume': '样品体积',
'pleaseSelectProgram': '请选择要运行的程序',
'run': '运行',
'pause': '暂停',
'continue': '继续',
'stop': '停止',
'startRun': '开始运行',
'currentStep': '当前步骤',
'remainingTime': '剩余时间',
'progress': '进度',
'ceramicSleeveConfirm': '运行前请确认已安装瓷套棒',
'paused': '已暂停',
'stopConfirm': '确定要停止当前运行的程序吗?',
'currentProgram': '当前程序',
'backToHome': '返回首页',
'runAgain': '重新运行',
'deleteConfirm': '确定要删除此程序吗?',
'stepNo': '步骤编号',
'position': '孔位',
'stepName': '步骤名称',
'mixTime': '混合时间',
'magnetTime': '吸磁时间',
'volume': '容积',
'mixSpeed': '混合速度',
'blowSpeed': '吹气速度',
'blowTime': '吹气时间',
'needleSpeed': '下针速度',
'lowSpeed': '低速',
'mediumSpeed': '中速',
'highSpeed': '高速',
'settings': '系统设置',
'language': '语言设置',
'password': '密码修改',
'upgrade': '软件升级',
'usbImport': 'U盘导入',
'confirm': '确认',
'cancel': '取消',
'save': '保存',
'delete': '删除',
'select': '选择',
'selected': '已选择',
'detail': '详情',
'noData': '暂无数据',
'runComplete': '运行完成',
'sampleDropGuide': '请将样本滴入检测卡',
'lightOn': '',
'lightOff': '',
'enabled': '启用',
'disabled': '停用',
'stepList': '步骤列表',
'operationSteps': '操作步骤',
'addStep': '添加步骤',
'editStep': '编辑步骤',
'deleteStep': '删除步骤',
'deleteStepConfirm': '确定要删除此步骤吗?',
'stepsCount': '',
'noSteps': '暂无步骤',
'selectStepFirst': '请选择或添加步骤',
'oldPassword': '原密码',
'newPassword': '新密码',
'confirmPassword': '确认新密码',
'passwordMinLength': '至少6位字符',
'passwordChanged': '密码已修改',
'passwordChangeFailed': '密码修改失败',
'oldPasswordError': '原密码错误',
'passwordMismatch': '两次输入的新密码不一致',
'fillAllFields': '请填写所有字段',
'importSuccess': '成功导入',
'importFailed': '导入失败',
'programsImported': '个程序',
'usbDetected': '检测到U盘',
'usbNotDetected': '未检测到U盘',
'insertUsb': '请插入U盘后重试',
'detectingUsb': '正在检测U盘...',
'currentVersion': '当前版本',
'latestVersion': '已是最新版本',
'updateAvailable': '有新版本可用',
'checkUpdate': '检查更新',
},
'en': {
'deviceName': 'Wastewater Drug Pretreatment System',
'running': 'Running',
'idle': 'Idle',
'lighting': 'Lighting',
'programs': 'Programs',
'programList': 'Program List',
'programName': 'Program Name',
'programCode': 'Program Code',
'createTime': 'Create Time',
'addProgram': 'Add Program',
'editProgram': 'Edit Program',
'deleteProgram': 'Delete Program',
'importProgram': 'Import Program',
'viewDetails': 'View Details',
'selectedProgram': 'Selected Program',
'selectedProgramLabel': 'Selected',
'availablePrograms': 'Available Programs',
'ceramicNotInstalled': 'Ceramic sleeve: Not installed — Cannot start',
'ceramicInstalled': 'Ceramic sleeve: Installed',
'runningMonitor': 'Running Status Monitor',
'currentHole': 'Current Position',
'stepParams': 'Step Parameters',
'speed': 'Speed',
'temperature': 'Temperature',
'duration': 'Duration',
'sampleVolume': 'Sample Volume',
'pleaseSelectProgram': 'Please select a program',
'run': 'Run',
'pause': 'Pause',
'continue': 'Continue',
'stop': 'Stop',
'startRun': 'Start Run',
'currentStep': 'Current Step',
'remainingTime': 'Remaining',
'progress': 'Progress',
'ceramicSleeveConfirm': 'Please confirm ceramic sleeve is installed',
'paused': 'Paused',
'stopConfirm': 'Are you sure to stop the running program?',
'currentProgram': 'Current Program',
'backToHome': 'Back to Home',
'runAgain': 'Run Again',
'deleteConfirm': 'Are you sure to delete this program?',
'stepNo': 'Step No.',
'position': 'Position',
'stepName': 'Step Name',
'mixTime': 'Mix Time',
'magnetTime': 'Magnet Time',
'volume': 'Volume',
'mixSpeed': 'Mix Speed',
'blowSpeed': 'Blow Speed',
'blowTime': 'Blow Time',
'needleSpeed': 'Needle Speed',
'lowSpeed': 'Low',
'mediumSpeed': 'Medium',
'highSpeed': 'High',
'settings': 'Settings',
'language': 'Language',
'password': 'Password',
'upgrade': 'Upgrade',
'usbImport': 'USB Import',
'confirm': 'Confirm',
'cancel': 'Cancel',
'save': 'Save',
'delete': 'Delete',
'select': 'Select',
'selected': 'Selected',
'detail': 'Detail',
'noData': 'No Data',
'runComplete': 'Complete',
'sampleDropGuide': 'Drop sample to test card',
'lightOn': 'On',
'lightOff': 'Off',
'enabled': 'Enabled',
'disabled': 'Disabled',
'stepList': 'Step List',
'operationSteps': 'Operation Steps',
'addStep': 'Add Step',
'editStep': 'Edit Step',
'deleteStep': 'Delete Step',
'deleteStepConfirm': 'Are you sure to delete this step?',
'stepsCount': 'steps',
'noSteps': 'No steps',
'selectStepFirst': 'Please select or add a step',
'oldPassword': 'Old Password',
'newPassword': 'New Password',
'confirmPassword': 'Confirm Password',
'passwordMinLength': 'At least 6 characters',
'passwordChanged': 'Password changed',
'passwordChangeFailed': 'Password change failed',
'oldPasswordError': 'Old password incorrect',
'passwordMismatch': 'Passwords do not match',
'fillAllFields': 'Please fill all fields',
'importSuccess': 'Successfully imported',
'importFailed': 'Import failed',
'programsImported': 'programs',
'usbDetected': 'USB detected',
'usbNotDetected': 'USB not detected',
'insertUsb': 'Please insert USB and try again',
'detectingUsb': 'Detecting USB...',
'currentVersion': 'Current Version',
'latestVersion': 'Already latest version',
'updateAvailable': 'Update available',
'checkUpdate': 'Check Update',
},
};
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
bool isSupported(Locale locale) {
return ['zh', 'en'].contains(locale.languageCode);
}
@override
Future<AppLocalizations> load(Locale locale) async {
return AppLocalizations(locale);
}
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Locale 状态 Notifier
class LocaleNotifier extends StateNotifier<Locale> {
static const String _key = 'app_locale';
LocaleNotifier() : super(const Locale('zh', 'CN')) {
_loadLocale();
}
/// 从本地存储加载语言设置
Future<void> _loadLocale() async {
final prefs = await SharedPreferences.getInstance();
final localeCode = prefs.getString(_key);
if (localeCode != null) {
state = Locale(localeCode, localeCode == 'zh' ? 'CN' : 'US');
}
}
/// 切换语言
Future<void> setLocale(Locale locale) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_key, locale.languageCode);
state = locale;
}
/// 切换为中文
Future<void> setChinese() async {
await setLocale(const Locale('zh', 'CN'));
}
/// 切换为英文
Future<void> setEnglish() async {
await setLocale(const Locale('en', 'US'));
}
}
/// Locale Provider
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>((ref) {
return LocaleNotifier();
});
/// 当前语言是否为中文
final isChineseProvider = Provider<bool>((ref) {
return ref.watch(localeProvider).languageCode == 'zh';
});

View File

@@ -0,0 +1,45 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/home/pages/home_page.dart';
import '../../features/programs/pages/programs_page.dart';
import '../../features/program_detail/pages/program_detail_page.dart';
import '../../features/settings/pages/settings_page.dart';
import '../../features/home/pages/complete_page.dart';
/// 应用路由配置
final goRouterProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/programs',
name: 'programs',
builder: (context, state) => const ProgramsPage(),
),
GoRoute(
path: '/programs/:id',
name: 'programDetail',
builder: (context, state) {
final id = state.pathParameters['id'];
return ProgramDetailPage(programId: id ?? '');
},
),
GoRoute(
path: '/settings',
name: 'settings',
builder: (context, state) => const SettingsPage(),
),
GoRoute(
path: '/complete',
name: 'complete',
builder: (context, state) => const CompletePage(),
),
],
);
});

View File

@@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
/// 应用主题配置 - 明亮工业控制风格
/// 主色 #2196F3圆角 4px明亮背景适配 1920x1080 横屏
class AppTheme {
// ========== 主色 ==========
static const Color primaryColor = Color(0xFF2196F3);
static const Color primaryDark = Color(0xFF1976D2);
static const Color primaryLight = Color(0xFFBBDEFB);
// ========== 功能色 ==========
static const Color successColor = Color(0xFF4CAF50);
static const Color warningColor = Color(0xFFFF9800);
static const Color errorColor = Color(0xFFF44336);
static const Color infoColor = Color(0xFF00BCD4);
// ========== 背景色(明亮) ==========
static const Color bgPage = Color(0xFFF5F7FA);
static const Color bgDeep = Color(0xFFE8ECF0);
static const Color bgSurface = Color(0xFFFFFFFF);
static const Color bgCard = Color(0xFFFFFFFF);
static const Color bgCardHover = Color(0xFFF0F7FF);
static const Color bgSidebar = Color(0xFFF0F2F5);
// ========== 文本色 ==========
static const Color textHeading = Color(0xFF1A1A2E);
static const Color textPrimary = Color(0xFF333344);
static const Color textSecondary = Color(0xFF6B7280);
static const Color textTertiary = Color(0xFF9CA3AF);
static const Color textOnPrimary = Colors.white;
// ========== 状态色 ==========
static const Color statusRunning = Color(0xFF4CAF50);
static const Color statusStopped = Color(0xFF9CA3AF);
static const Color statusPaused = Color(0xFFFF9800);
static const Color statusError = Color(0xFFF44336);
// ========== 卡片背景 ==========
static const Color cardBg = Color(0xFFFFFFFF);
static const Color cardSelectedBg = Color(0xFFE3F2FD);
// ========== 功能色accent ==========
static const Color accentPrimary = primaryColor;
static const Color accentInfo = infoColor;
static const Color accentWarning = warningColor;
static const Color accentCritical = errorColor;
// ========== 边框色 ==========
static const Color borderLight = Color(0xFFE5E7EB);
static const Color borderMedium = Color(0xFFD1D5DB);
static const Color borderSubtle = borderLight;
static const Color borderFocus = primaryColor;
// ========== 圆角 ==========
static const double radiusSm = 4.0;
static const double radiusMd = 8.0;
static const double radiusLg = 12.0;
// ========== 阴影 ==========
static const List<BoxShadow> shadowCard = [
BoxShadow(
color: Color(0x0A000000),
blurRadius: 8,
offset: Offset(0, 2),
),
];
static const List<BoxShadow> shadowCardHover = [
BoxShadow(
color: Color(0x14000000),
blurRadius: 12,
offset: Offset(0, 4),
),
];
// ========== 兼容旧代码的颜色别名 ==========
static const Color runningColor = statusRunning;
static const Color idleColor = statusStopped;
static const Color backgroundColor = bgPage;
static const Color cardColor = bgCard;
/// 亮色主题 - 明亮工业风格
static ThemeData lightTheme() {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.light,
),
scaffoldBackgroundColor: bgPage,
fontFamily: 'Inter',
cardTheme: CardThemeData(
color: bgCard,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
side: const BorderSide(color: borderLight, width: 1),
),
margin: EdgeInsets.zero,
),
appBarTheme: const AppBarTheme(
backgroundColor: bgSurface,
foregroundColor: textHeading,
elevation: 0,
centerTitle: false,
scrolledUnderElevation: 1,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: textOnPrimary,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: borderMedium),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: borderMedium),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: primaryColor, width: 2),
),
filled: true,
fillColor: bgSurface,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
dialogTheme: DialogThemeData(
backgroundColor: bgSurface,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: textHeading,
contentTextStyle: const TextStyle(color: Colors.white, fontSize: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
behavior: SnackBarBehavior.floating,
),
listTileTheme: const ListTileThemeData(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(radiusSm)),
),
),
dataTableTheme: DataTableThemeData(
headingRowColor: WidgetStateProperty.all(bgSidebar),
dividerThickness: 1,
),
dividerTheme: const DividerThemeData(
color: borderLight,
thickness: 1,
),
);
}
/// 暗色主题(与亮色主题风格一致的暗色模式)
static ThemeData darkTheme() {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark,
),
scaffoldBackgroundColor: const Color(0xFF121212),
);
}
}

View File

@@ -0,0 +1,73 @@
/// 设备状态模型
enum DeviceStatus { idle, running, paused, error }
/// 设备状态数据
class DeviceState {
final DeviceStatus status;
final String? currentProgram;
final String? currentPosition;
final int? currentStepNo;
final String? currentStepName;
final int? remainingSeconds;
final double? progress;
final bool lightingOn;
DeviceState({
this.status = DeviceStatus.idle,
this.currentProgram,
this.currentPosition,
this.currentStepNo,
this.currentStepName,
this.remainingSeconds,
this.progress,
this.lightingOn = false,
});
bool get isRunning => status == DeviceStatus.running;
bool get isPaused => status == DeviceStatus.paused;
bool get isIdle => status == DeviceStatus.idle;
bool get hasError => status == DeviceStatus.error;
String statusText() {
switch (status) {
case DeviceStatus.running:
return '运行中';
case DeviceStatus.paused:
return '已暂停';
case DeviceStatus.error:
return '错误';
case DeviceStatus.idle:
return '未运行';
}
}
String formatRemainingTime() {
if (remainingSeconds == null) return '--:--:--';
final hours = remainingSeconds! ~/ 3600;
final minutes = (remainingSeconds! % 3600) ~/ 60;
final seconds = remainingSeconds! % 60;
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
DeviceState copyWith({
DeviceStatus? status,
String? currentProgram,
String? currentPosition,
int? currentStepNo,
String? currentStepName,
int? remainingSeconds,
double? progress,
bool? lightingOn,
}) {
return DeviceState(
status: status ?? this.status,
currentProgram: currentProgram ?? this.currentProgram,
currentPosition: currentPosition ?? this.currentPosition,
currentStepNo: currentStepNo ?? this.currentStepNo,
currentStepName: currentStepName ?? this.currentStepName,
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
progress: progress ?? this.progress,
lightingOn: lightingOn ?? this.lightingOn,
);
}
}

View File

@@ -0,0 +1,188 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
import '../../programs/services/program_service.dart';
import '../services/mock_runner.dart';
/// 运行状态枚举
enum RunStatus {
idle, // 待机
running, // 运行中
paused, // 已暂停
completed,// 已完成
error, // 错误
}
/// 运行状态
class RunState {
final RunStatus status;
final Program? currentProgram;
final List<Step> steps;
final int currentStepIndex;
final int remainingSeconds;
final double progress;
final String? currentWell;
const RunState({
this.status = RunStatus.idle,
this.currentProgram,
this.steps = const [],
this.currentStepIndex = 0,
this.remainingSeconds = 0,
this.progress = 0,
this.currentWell,
});
RunState copyWith({
RunStatus? status,
Program? currentProgram,
List<Step>? steps,
int? currentStepIndex,
int? remainingSeconds,
double? progress,
String? currentWell,
bool clearProgram = false,
bool clearWell = false,
}) {
return RunState(
status: status ?? this.status,
currentProgram: clearProgram ? null : (currentProgram ?? this.currentProgram),
steps: steps ?? this.steps,
currentStepIndex: currentStepIndex ?? this.currentStepIndex,
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
progress: progress ?? this.progress,
currentWell: clearWell ? null : (currentWell ?? this.currentWell),
);
}
/// 获取当前步骤
Step? get currentStep {
if (steps.isEmpty || currentStepIndex >= steps.length) return null;
return steps[currentStepIndex];
}
/// 格式化剩余时间 (HH:MM:SS)
String get formattedRemainingTime {
final hours = remainingSeconds ~/ 3600;
final minutes = (remainingSeconds % 3600) ~/ 60;
final seconds = remainingSeconds % 60;
return '${hours.toString().padLeft(2, '0')}:'
'${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
/// 格式化进度百分比
String get formattedProgress {
return '${(progress * 100).toStringAsFixed(0)}%';
}
}
/// 运行状态 Notifier
class RunStateNotifier extends StateNotifier<RunState> {
final MockRunner _runner;
final ProgramService _programService;
RunStateNotifier(this._runner, this._programService) : super(const RunState());
/// 开始运行程序
Future<void> start(Program program) async {
// 获取程序步骤(这里使用模拟数据,实际应从数据库读取)
final steps = await _loadSteps(program.id!);
if (steps.isEmpty) {
state = state.copyWith(status: RunStatus.error);
return;
}
state = state.copyWith(
status: RunStatus.running,
currentProgram: program,
steps: steps,
currentStepIndex: 0,
progress: 0,
);
_runner.start(
program,
steps,
(stepIndex, remaining, progress, well) {
state = state.copyWith(
currentStepIndex: stepIndex,
remainingSeconds: remaining,
progress: progress,
currentWell: well,
);
},
() {
state = state.copyWith(
status: RunStatus.completed,
progress: 1,
clearWell: true,
);
},
);
}
/// 暂停运行
void pause() {
if (state.status == RunStatus.running) {
_runner.pause();
state = state.copyWith(status: RunStatus.paused);
}
}
/// 继续运行
void resume() {
if (state.status == RunStatus.paused) {
_runner.resume();
state = state.copyWith(status: RunStatus.running);
}
}
/// 停止运行
void stop() {
_runner.stop();
state = const RunState(status: RunStatus.idle);
}
/// 重置状态
void reset() {
stop();
}
/// 加载程序步骤(从数据库读取)
Future<List<Step>> _loadSteps(int programId) async {
return await _programService.getStepsByProgramId(programId);
}
}
/// MockRunner Provider
final mockRunnerProvider = Provider<MockRunner>((ref) {
return MockRunner();
});
/// ProgramService Provider
final programServiceProvider = Provider<ProgramService>((ref) {
return ProgramService.instance;
});
/// 运行状态 Provider
final runStateProvider =
StateNotifierProvider<RunStateNotifier, RunState>((ref) {
final runner = ref.watch(mockRunnerProvider);
final programService = ref.watch(programServiceProvider);
return RunStateNotifier(runner, programService);
});
/// 是否正在运行 Provider
final isRunningProvider = Provider<bool>((ref) {
final status = ref.watch(runStateProvider).status;
return status == RunStatus.running;
});
/// 是否已暂停 Provider
final isPausedProvider = Provider<bool>((ref) {
final status = ref.watch(runStateProvider).status;
return status == RunStatus.paused;
});

View File

@@ -0,0 +1,190 @@
import 'dart:async';
import '../../programs/models/step.dart';
import '../../programs/models/program.dart';
/// 模拟运行器回调
typedef RunProgressCallback = void Function(
int currentStepIndex,
int remainingSeconds,
double progress,
String currentWell,
);
typedef RunCompleteCallback = void Function();
/// 模拟运行器
/// 用于在没有实际硬件连接时模拟程序执行过程
class MockRunner {
Timer? _timer;
Program? _currentProgram;
List<Step> _steps = [];
int _currentStepIndex = 0;
int _remainingSeconds = 0;
bool _isPaused = false;
RunProgressCallback? _onProgress;
RunCompleteCallback? _onComplete;
/// 是否正在运行
bool get isRunning => _timer != null && !_isPaused;
/// 是否已暂停
bool get isPaused => _isPaused;
/// 当前程序
Program? get currentProgram => _currentProgram;
/// 开始运行程序
void start(
Program program,
List<Step> steps,
RunProgressCallback onProgress,
RunCompleteCallback onComplete,
) {
_currentProgram = program;
_steps = steps;
_onProgress = onProgress;
_onComplete = onComplete;
_currentStepIndex = 0;
_isPaused = false;
if (steps.isEmpty) {
onComplete();
return;
}
// 开始执行第一个步骤
_startStep(steps[0]);
}
/// 暂停运行
void pause() {
if (_timer != null && !_isPaused) {
_isPaused = true;
_timer!.cancel();
_timer = null;
}
}
/// 继续运行
void resume() {
if (_isPaused && _currentProgram != null) {
_isPaused = false;
_resumeStep();
}
}
/// 停止运行
void stop() {
_timer?.cancel();
_timer = null;
_currentProgram = null;
_steps = [];
_currentStepIndex = 0;
_remainingSeconds = 0;
_isPaused = false;
}
/// 开始执行步骤
void _startStep(Step step) {
// 计算步骤总时间(混合时间 + 吸磁时间 + 吹气时间)
_remainingSeconds = step.mixTime + step.magnetTime + step.blowTime;
// 如果步骤时间为0设置最小演示时间5秒
if (_remainingSeconds == 0) {
_remainingSeconds = 5;
}
// 启动定时器,每秒更新
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_remainingSeconds--;
// 计算总进度
final totalSeconds = _calculateTotalSeconds();
final elapsedSeconds = _calculateElapsedSeconds();
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
// 回调进度更新
_onProgress?.call(
_currentStepIndex,
_remainingSeconds,
progress,
step.position,
);
// 步骤完成
if (_remainingSeconds <= 0) {
timer.cancel();
_timer = null;
_nextStep();
}
});
}
/// 继续执行步骤(从暂停恢复)
void _resumeStep() {
if (_currentStepIndex >= _steps.length) return;
final step = _steps[_currentStepIndex];
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_remainingSeconds--;
final totalSeconds = _calculateTotalSeconds();
final elapsedSeconds = _calculateElapsedSeconds();
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
_onProgress?.call(
_currentStepIndex,
_remainingSeconds,
progress,
step.position,
);
if (_remainingSeconds <= 0) {
timer.cancel();
_timer = null;
_nextStep();
}
});
}
/// 执行下一个步骤
void _nextStep() {
_currentStepIndex++;
if (_currentStepIndex >= _steps.length) {
// 所有步骤完成
_onComplete?.call();
stop();
} else {
// 执行下一个步骤
_startStep(_steps[_currentStepIndex]);
}
}
/// 计算总执行时间
int _calculateTotalSeconds() {
int total = 0;
for (final step in _steps) {
int stepTime = step.mixTime + step.magnetTime + step.blowTime;
if (stepTime == 0) stepTime = 5;
total += stepTime;
}
return total;
}
/// 计算已执行时间
int _calculateElapsedSeconds() {
int elapsed = 0;
for (int i = 0; i < _currentStepIndex; i++) {
int stepTime = _steps[i].mixTime + _steps[i].magnetTime + _steps[i].blowTime;
if (stepTime == 0) stepTime = 5;
elapsed += stepTime;
}
// 加上当前步骤已执行的时间
final currentStep = _steps[_currentStepIndex];
int currentStepTime = currentStep.mixTime + currentStep.magnetTime + currentStep.blowTime;
if (currentStepTime == 0) currentStepTime = 5;
elapsed += currentStepTime - _remainingSeconds;
return elapsed;
}
}

View File

@@ -0,0 +1,114 @@
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
import 'runner_interface.dart';
/// 模拟运行器(用于开发测试)
/// 模拟硬件运行过程
class MockRunner implements Runner {
@override
RunnerStatus status = RunnerStatus.idle;
bool _isRunning = false;
int _currentStep = 0;
int _remainingSeconds = 0;
RunnerCallbacks? _callbacks;
List<Step> _steps = [];
@override
void start(Program program, List<Step> steps, RunnerCallbacks callbacks) {
if (steps.isEmpty) {
callbacks.onError?.call('No steps to run');
status = RunnerStatus.error;
return;
}
_steps = steps;
_callbacks = callbacks;
_currentStep = 0;
_isRunning = true;
status = RunnerStatus.running;
// 开始模拟运行
_runSimulation();
}
void _runSimulation() {
if (!_isRunning || _currentStep >= _steps.length) {
_completeRun();
return;
}
final step = _steps[_currentStep];
// 计算步骤时间(混合时间 + 吸磁时间 + 吹气时间 + 5秒最小
final stepTime = (step.mixTime ?? 0) + (step.magnetTime ?? 0) + (step.blowTime ?? 0) + 5;
_remainingSeconds = stepTime.clamp(5, 300);
// 模拟倒计时
_simulateStepProgress(stepTime);
}
void _simulateStepProgress(int totalSeconds) {
// 简化模拟:每秒更新进度
int elapsed = 0;
while (_isRunning && elapsed < totalSeconds) {
elapsed++;
final remaining = totalSeconds - elapsed;
final progress = elapsed / totalSeconds;
_callbacks?.onProgress?.call(
_currentStep,
remaining,
(_currentStep + progress) / _steps.length,
_steps[_currentStep].position,
);
// 实际实现需要使用 Timer
// await Future.delayed(Duration(seconds: 1));
}
if (_isRunning) {
_currentStep++;
_runSimulation();
}
}
void _completeRun() {
status = RunnerStatus.completed;
_isRunning = false;
_callbacks?.onComplete?.call();
}
@override
void pause() {
if (status == RunnerStatus.running) {
_isRunning = false;
status = RunnerStatus.paused;
}
}
@override
void resume() {
if (status == RunnerStatus.paused) {
_isRunning = true;
status = RunnerStatus.running;
// 继续运行
_runSimulation();
}
}
@override
void stop() {
_isRunning = false;
status = RunnerStatus.idle;
_currentStep = 0;
_remainingSeconds = 0;
}
@override
RunnerStatus getStatus() => status;
@override
void dispose() {
stop();
}
}

View File

@@ -0,0 +1,54 @@
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
/// 运行器状态
enum RunnerStatus {
idle,
running,
paused,
completed,
error,
}
/// 运行器回调
class RunnerCallbacks {
/// 步骤进度回调: (stepIndex, remainingSeconds, progress, currentWell)
final void Function(int stepIndex, int remainingSeconds, double progress, String well)? onProgress;
/// 运行完成回调
final void Function()? onComplete;
/// 错误回调
final void Function(String error)? onError;
const RunnerCallbacks({
this.onProgress,
this.onComplete,
this.onError,
});
}
/// 运行器抽象接口
/// 定义硬件运行控制的标准接口
abstract class Runner {
/// 当前状态
RunnerStatus status = RunnerStatus.idle;
/// 启动程序运行
void start(Program program, List<Step> steps, RunnerCallbacks callbacks);
/// 暂停运行
void pause();
/// 继续运行
void resume();
/// 停止运行
void stop();
/// 获取当前状态
RunnerStatus getStatus();
/// 释放资源
void dispose();
}

View File

@@ -0,0 +1,91 @@
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
import 'runner_interface.dart';
/// 串口运行器(真实硬件实现)
/// 实现与设备的串口通信
class SerialRunner implements Runner {
@override
RunnerStatus status = RunnerStatus.idle;
/// 串口配置
final String portName;
final int baudRate;
final int dataBits;
final int stopBits;
SerialRunner({
this.portName = '/dev/ttyUSB0',
this.baudRate = 9600,
this.dataBits = 8,
this.stopBits = 1,
});
@override
void start(Program program, List<Step> steps, RunnerCallbacks callbacks) {
// TODO: 实现串口通信启动逻辑
// 1. 打开串口连接
// 2. 发送程序配置
// 3. 按步骤发送控制指令
// 4. 接收设备反馈并更新状态
status = RunnerStatus.running;
// 示例:发送启动指令
// _sendCommand('START', program.code);
// 示例:监听设备状态
// _listenToDevice(callbacks);
}
@override
void pause() {
if (status == RunnerStatus.running) {
// _sendCommand('PAUSE');
status = RunnerStatus.paused;
}
}
@override
void resume() {
if (status == RunnerStatus.paused) {
// _sendCommand('RESUME');
status = RunnerStatus.running;
}
}
@override
void stop() {
// _sendCommand('STOP');
// _closeConnection();
status = RunnerStatus.idle;
}
@override
RunnerStatus getStatus() => status;
@override
void dispose() {
stop();
}
/// 发送控制指令(待硬件协议确定后实现)
Future<void> _sendCommand(String command, [String? data]) async {
// TODO: 根据硬件通信协议实现
// 示例协议格式: [CMD:data] 或 二进制协议
}
/// 监听设备反馈(待硬件协议确定后实现)
void _listenToDevice(RunnerCallbacks callbacks) {
// TODO: 解析设备返回的状态数据
// 状态格式示例: [STEP:1,TIME:60,POS:A1]
}
/// 执行单个步骤
Future<void> _executeStep(Step step) async {
// TODO: 根据步骤参数生成控制指令
// 混合: MIX(position, time, speed)
// 吸磁: MAGNET(position, time)
// 吹气: BLOW(position, speed, time)
}
}

View File

@@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../../device/providers/run_state_provider.dart';
/// 运行完成提示页面
class CompletePage extends ConsumerWidget {
const CompletePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final runState = ref.watch(runStateProvider);
final runNotifier = ref.read(runStateProvider.notifier);
return Scaffold(
body: Container(
color: AppTheme.backgroundColor,
child: Center(
child: Container(
width: 600,
padding: const EdgeInsets.all(40),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 成功图标
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppTheme.successColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.check_circle,
size: 60,
color: AppTheme.successColor,
),
),
const SizedBox(height: 24),
// 标题
Text(
l10n?.runComplete ?? '程序运行完成',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 提示信息
Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
decoration: BoxDecoration(
color: AppTheme.warningColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
l10n?.sampleDropGuide ?? '请将样本滴入检测卡',
style: TextStyle(
color: AppTheme.warningColor,
fontSize: 16,
),
),
),
const SizedBox(height: 32),
// 操作示意图
_buildOperationGuide(),
const SizedBox(height: 32),
// 按钮区域
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 返回首页按钮
CommonButton(
text: l10n?.backToHome ?? '返回首页',
icon: Icons.home,
type: ButtonType.primary,
onPressed: () {
runNotifier.reset();
context.go('/');
},
),
const SizedBox(width: 24),
// 重新运行按钮
CommonButton(
text: l10n?.runAgain ?? '重新运行',
icon: Icons.refresh,
type: ButtonType.secondary,
onPressed: () {
final program = runState.currentProgram;
if (program != null) {
runNotifier.reset();
runNotifier.start(program);
context.go('/');
}
},
),
],
),
],
),
),
),
),
);
}
/// 操作指引示意图
Widget _buildOperationGuide() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.backgroundColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.idleColor.withValues(alpha: 0.2)),
),
child: Column(
children: [
Text(
'操作步骤',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStepItem(1, '取出样本', Icons.science),
_buildStepItem(2, '滴入检测卡', Icons.water_drop),
_buildStepItem(3, '等待反应', Icons.timer),
_buildStepItem(4, '查看结果', Icons.visibility),
],
),
],
),
);
}
/// 步骤项
Widget _buildStepItem(int number, String text, IconData icon) {
return Column(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: AppTheme.primaryColor, size: 24),
),
const SizedBox(height: 8),
Text(
'$number',
style: TextStyle(
color: AppTheme.primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
text,
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 11,
),
),
],
);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/database/database_service.dart';
import '../../../core/theme/app_theme.dart';
import '../../device/providers/run_state_provider.dart';
import '../../programs/pages/programs_page.dart';
import '../../settings/pages/settings_page.dart';
import '../widgets/status_bar.dart';
import '../widgets/program_list.dart';
import '../widgets/running_control_panel.dart';
import '../widgets/run_status_monitor.dart';
/// 首页 - 设备控制面板 (暗色工业风格)
/// 布局:状态栏 + 导航标签栏 + 内容区(设备控制/程序管理/系统设置)
class HomePage extends ConsumerStatefulWidget {
const HomePage({super.key});
@override
ConsumerState<HomePage> createState() => _HomePageState();
}
class _HomePageState extends ConsumerState<HomePage>
with SingleTickerProviderStateMixin {
bool _lightOn = false;
final bool _ceramicSleeveInstalled = false; // TODO: 后续对接硬件传感器后改为可变状态
int _currentIndex = 0;
@override
void initState() {
super.initState();
DatabaseService.instance.initTestData();
}
@override
Widget build(BuildContext context) {
final runState = ref.watch(runStateProvider);
// 监听运行完成状态,自动跳转
ref.listen<RunState>(runStateProvider, (prev, next) {
if (prev?.status != RunStatus.completed && next.status == RunStatus.completed) {
// 仅首页才自动跳转
if (_currentIndex == 0) {
context.push('/complete');
}
}
});
return Scaffold(
body: Container(
color: AppTheme.bgDeep,
child: Column(
children: [
// 状态栏
StatusBar(
isRunning: runState.status == RunStatus.running,
lightOn: _lightOn,
onLightToggle: () {
setState(() {
_lightOn = !_lightOn;
});
},
ceramicSleeveInstalled: _ceramicSleeveInstalled,
),
// 导航标签栏
_buildTabBar(),
// 内容区
Expanded(
child: IndexedStack(
index: _currentIndex,
children: [
_buildDeviceControlPage(runState),
const ProgramsPage(),
const SettingsPage(),
],
),
),
],
),
),
);
}
/// 导航标签栏
Widget _buildTabBar() {
const tabs = [
(icon: Icons.dashboard, label: '设备控制'),
(icon: Icons.list_alt, label: '程序管理'),
(icon: Icons.settings, label: '系统设置'),
];
return Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: List.generate(tabs.length, (index) {
final tab = tabs[index];
final isSelected = _currentIndex == index;
return GestureDetector(
onTap: () => setState(() => _currentIndex = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(right: 4),
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: isSelected ? AppTheme.accentPrimary : AppTheme.cardBg,
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
border: Border.all(
color: isSelected
? AppTheme.accentPrimary
: AppTheme.borderSubtle,
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
tab.icon,
size: 18,
color: isSelected ? Colors.white : AppTheme.textSecondary,
),
const SizedBox(width: 8),
Text(
tab.label,
style: TextStyle(
color: isSelected ? Colors.white : AppTheme.textSecondary,
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.normal,
),
),
],
),
),
);
}),
),
);
}
/// 设备控制页面内容
Widget _buildDeviceControlPage(RunState runState) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: Row(
children: [
// 左侧:程序列表(运行时锁定)
Opacity(
opacity: runState.status == RunStatus.idle ? 1.0 : 0.6,
child: IgnorePointer(
ignoring: runState.status != RunStatus.idle,
child: const ProgramList(),
),
),
const SizedBox(width: 20),
// 右侧:运行控制区域
Expanded(
child: Column(
children: [
const Expanded(child: RunningControlPanel()),
if (runState.status != RunStatus.idle) ...[
const SizedBox(height: 16),
const Expanded(child: RunStatusMonitor()),
],
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../programs/models/program.dart';
import '../../programs/providers/programs_provider.dart';
/// 程序列表组件 - 暗色工业风格
/// 显示程序卡片列表,支持选择操作
class ProgramList extends ConsumerWidget {
const ProgramList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final programsState = ref.watch(programsProvider);
final programsNotifier = ref.read(programsProvider.notifier);
return Container(
width: 380,
decoration: BoxDecoration(
color: AppTheme.cardBg,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.borderSubtle, width: 1),
),
child: Column(
children: [
// 标题
Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
Icon(Icons.list_alt, color: AppTheme.textHeading, size: 18),
const SizedBox(width: 10),
Text(
l10n?.availablePrograms ?? '可用程序',
style: const TextStyle(
color: AppTheme.textHeading,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
// 程序列表
Expanded(
child: programsState.isLoading
? const Center(child: CircularProgressIndicator())
: programsState.programs.isEmpty
? Center(
child: Text(
l10n?.noData ?? '暂无数据',
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 14),
itemCount: programsState.programs.length,
itemBuilder: (context, index) {
final program = programsState.programs[index];
final isSelected =
programsState.selectedProgramId == program.id;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _ProgramCard(
program: program,
isSelected: isSelected,
onTap: () {
programsNotifier.selectProgram(program.id);
},
),
);
},
),
),
],
),
);
}
}
/// 单个程序卡片 - 暗色工业风格
class _ProgramCard extends StatelessWidget {
final Program program;
final bool isSelected;
final VoidCallback? onTap;
const _ProgramCard({
required this.program,
this.isSelected = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
final createdAt = _parseDate(program.createdAt);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? AppTheme.cardSelectedBg : AppTheme.cardBg,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? AppTheme.accentPrimary : AppTheme.borderSubtle,
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
// 选择指示器
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? AppTheme.accentPrimary
: Colors.transparent,
border: Border.all(
color: isSelected
? AppTheme.accentPrimary
: AppTheme.statusStopped,
width: 2,
),
),
child: isSelected
? const Icon(Icons.check, color: Colors.white, size: 12)
: null,
),
const SizedBox(width: 12),
// 程序信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
program.code,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: program.status == 1
? AppTheme.statusRunning.withValues(alpha: 0.15)
: AppTheme.statusStopped.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4),
),
child: Text(
program.status == 1 ? '启用' : '停用',
style: TextStyle(
color: program.status == 1
? AppTheme.statusRunning
: AppTheme.statusStopped,
fontSize: 10,
),
),
),
],
),
const SizedBox(height: 4),
Text(
program.name,
style: const TextStyle(
color: AppTheme.textHeading,
fontSize: 14,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
createdAt != null
? dateFormat.format(createdAt)
: program.createdAt,
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 11,
fontFamily: 'monospace',
),
),
],
),
),
],
),
),
),
);
}
DateTime? _parseDate(String dateStr) {
try {
return DateTime.parse(dateStr);
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,242 @@
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 '../../device/providers/run_state_provider.dart';
/// 运行状态监控面板 - 暗色工业风格
/// 显示当前孔位、步骤、倒计时、进度条、参数详情
class RunStatusMonitor extends ConsumerWidget {
const RunStatusMonitor({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final runState = ref.watch(runStateProvider);
if (runState.status == RunStatus.idle) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.cardBg,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.borderSubtle, width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题 + 程序名
Row(
children: [
Text(
l10n?.runningMonitor ?? '运行状态监控',
style: const TextStyle(
color: AppTheme.textHeading,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
runState.currentProgram?.name ?? '',
style: const TextStyle(
color: AppTheme.accentPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 14),
// 进度信息横排 (孔位 / 步骤 / 剩余时间)
Row(
children: [
// 当前孔位
_buildInfoBlock(
label: l10n?.currentHole ?? '当前孔位',
value: runState.currentWell ?? '--',
valueColor: AppTheme.textHeading,
),
const SizedBox(width: 20),
// 当前步骤
_buildInfoBlock(
label: l10n?.currentStep ?? '当前步骤',
value: '${l10n?.stepNo ?? '步骤'} ${runState.currentStepIndex + 1}',
subValue: runState.currentStep?.name ?? '--',
valueColor: AppTheme.accentInfo,
),
const SizedBox(width: 20),
// 剩余时间
_buildInfoBlock(
label: l10n?.remainingTime ?? '剩余时间',
value: runState.formattedRemainingTime,
valueColor: AppTheme.textHeading,
valueSize: 20,
),
],
),
const SizedBox(height: 14),
// 总进度条
_buildProgressBar(l10n, runState),
const SizedBox(height: 14),
// 步骤参数
if (runState.currentStep != null)
_buildStepParams(l10n, runState.currentStep!),
],
),
);
}
/// 信息块
Widget _buildInfoBlock({
required String label,
required String value,
String? subValue,
Color valueColor = AppTheme.textHeading,
double valueSize = 16,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 11,
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
color: valueColor,
fontSize: valueSize,
fontWeight: FontWeight.w600,
fontFamily: 'monospace',
),
),
if (subValue != null) ...[
const SizedBox(height: 2),
Text(
subValue,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 11,
),
),
],
],
);
}
/// 进度条
Widget _buildProgressBar(AppLocalizations? l10n, RunState runState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
l10n?.progress ?? '总进度',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 11,
),
),
const Spacer(),
Text(
runState.formattedProgress,
style: const TextStyle(
color: AppTheme.accentPrimary,
fontSize: 12,
fontWeight: FontWeight.w600,
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: runState.progress,
minHeight: 8,
backgroundColor: const Color(0xFF1E293B),
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.accentPrimary),
),
),
],
);
}
/// 步骤参数详情
Widget _buildStepParams(AppLocalizations? l10n, dynamic step) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n?.stepParams ?? '步骤参数',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 11,
),
),
const SizedBox(height: 2),
if (step.mixTime > 0)
_buildParamRow(
l10n?.speed ?? '转速',
'${step.mixSpeed}',
),
if (step.magnetTime > 0)
_buildParamRow(
l10n?.temperature ?? '温度',
'65.0 °C',
),
_buildParamRow(
l10n?.duration ?? '持续时间',
step.mixTime > 0 ? '${step.mixTime} min' : '--',
),
_buildParamRow(
l10n?.sampleVolume ?? '样品体积',
'10.0 mL',
),
],
);
}
/// 参数行
Widget _buildParamRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Text(
label,
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 11,
),
),
const Spacer(),
Text(
value,
style: const TextStyle(
color: AppTheme.textPrimary,
fontSize: 11,
fontFamily: 'monospace',
),
),
],
),
);
}
}

View File

@@ -0,0 +1,365 @@
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 '../../device/providers/run_state_provider.dart';
import '../../programs/providers/programs_provider.dart';
/// 运行控制面板 - 暗色工业风格
/// 显示当前程序信息、瓷套棒状态和运行控制按钮
class RunningControlPanel extends ConsumerWidget {
const RunningControlPanel({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final runState = ref.watch(runStateProvider);
final programsState = ref.watch(programsProvider);
return Container(
decoration: BoxDecoration(
color: AppTheme.cardBg,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.borderSubtle, width: 1),
),
child: runState.status == RunStatus.idle
? _buildIdleState(context, ref, l10n, programsState.selectedProgram)
: _buildRunningState(context, ref, l10n, runState),
);
}
/// 待机状态布局
Widget _buildIdleState(
BuildContext context,
WidgetRef ref,
AppLocalizations? l10n,
dynamic selectedProgram,
) {
final runNotifier = ref.read(runStateProvider.notifier);
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 当前选中程序显示
if (selectedProgram != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.cardSelectedBg,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.accentPrimary, width: 1),
),
child: Row(
children: [
Text(
'${l10n?.selectedProgramLabel ?? '当前选中'}:',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 12,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'${selectedProgram.code} ${selectedProgram.name}',
style: const TextStyle(
color: AppTheme.accentPrimary,
fontSize: 14,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
)
else
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.cardBg,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.borderSubtle, width: 1),
),
child: Text(
l10n?.pleaseSelectProgram ?? '请选择要运行的程序',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 12,
),
),
),
const SizedBox(height: 12),
// 瓷套棒确认提示
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: AppTheme.cardBg,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.borderSubtle, width: 1),
),
child: Row(
children: [
Container(
width: 10,
height: 10,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.statusStopped,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(
l10n?.ceramicNotInstalled ?? '瓷套棒: 未安装 — 禁止启动',
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
),
),
),
],
),
),
const SizedBox(height: 12),
// 控制按钮
Row(
children: [
// 开始运行按钮
Expanded(
flex: 2,
child: SizedBox(
height: 48,
child: CommonButton(
text: l10n?.startRun ?? '开始运行',
icon: Icons.play_arrow,
type: ButtonType.primary,
enabled: selectedProgram != null,
onPressed: selectedProgram != null
? () => runNotifier.start(selectedProgram)
: null,
),
),
),
const SizedBox(width: 12),
// 暂停/继续按钮(待机态禁用)
Expanded(
child: SizedBox(
height: 48,
child: CommonButton(
text: l10n?.pause ?? '暂停',
icon: Icons.pause,
type: ButtonType.secondary,
enabled: false,
onPressed: null,
),
),
),
const SizedBox(width: 12),
// 停止按钮(待机态禁用)
Expanded(
child: SizedBox(
height: 48,
child: CommonButton(
text: l10n?.stop ?? '停止',
icon: Icons.stop,
type: ButtonType.danger,
enabled: false,
onPressed: null,
),
),
),
],
),
],
),
);
}
/// 运行状态布局
Widget _buildRunningState(
BuildContext context,
WidgetRef ref,
AppLocalizations? l10n,
RunState runState,
) {
final runNotifier = ref.read(runStateProvider.notifier);
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 当前程序名称
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.cardSelectedBg,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.accentPrimary, width: 1),
),
child: Row(
children: [
Text(
'${l10n?.selectedProgramLabel ?? '当前选中'}:',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 12,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
runState.currentProgram?.name ?? '',
style: const TextStyle(
color: AppTheme.accentPrimary,
fontSize: 14,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
// 控制按钮
Row(
children: [
// 开始/继续按钮
Expanded(
flex: 2,
child: SizedBox(
height: 48,
child: CommonButton(
text: runState.status == RunStatus.paused
? (l10n?.continue_ ?? '继续')
: (l10n?.run ?? '运行'),
icon: runState.status == RunStatus.paused
? Icons.play_arrow
: Icons.play_arrow,
type: ButtonType.primary,
onPressed: () => runNotifier.resume(),
),
),
),
const SizedBox(width: 12),
// 暂停按钮
Expanded(
child: SizedBox(
height: 48,
child: CommonButton(
text: l10n?.pause ?? '暂停',
icon: Icons.pause,
type: ButtonType.warning,
onPressed: runState.status == RunStatus.paused
? null
: () => runNotifier.pause(),
),
),
),
const SizedBox(width: 12),
// 停止按钮
Expanded(
child: SizedBox(
height: 48,
child: CommonButton(
text: l10n?.stop ?? '停止',
icon: Icons.stop,
type: ButtonType.danger,
onPressed: () => _showStopConfirm(context, runNotifier, l10n),
),
),
),
],
),
const SizedBox(height: 12),
// 状态指示
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: runState.status == RunStatus.paused
? AppTheme.accentWarning
: AppTheme.statusRunning,
),
),
const SizedBox(width: 8),
Text(
runState.status == RunStatus.paused
? (l10n?.paused ?? '已暂停')
: (l10n?.running ?? '运行中'),
style: TextStyle(
color: runState.status == RunStatus.paused
? AppTheme.accentWarning
: AppTheme.statusRunning,
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
],
),
],
),
);
}
/// 显示停止确认对话框
void _showStopConfirm(
BuildContext context,
RunStateNotifier runNotifier,
AppLocalizations? l10n,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.cardBg,
title: Text(
l10n?.confirm ?? '确认',
style: const TextStyle(color: AppTheme.textHeading),
),
content: Text(
l10n?.stopConfirm ?? '确定要停止当前运行的程序吗?',
style: const TextStyle(color: AppTheme.textPrimary),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
l10n?.cancel ?? '取消',
style: const TextStyle(color: AppTheme.textSecondary),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentCritical,
foregroundColor: Colors.white,
),
onPressed: () {
runNotifier.stop();
Navigator.of(context).pop();
},
child: Text(l10n?.confirm ?? '确认'),
),
],
),
);
}
}

View File

@@ -0,0 +1,181 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/status_indicator.dart';
/// 状态栏组件 - 明亮工业风格
/// 显示设备名称、实时时钟、系统状态、照明控制、瓷套棒状态
class StatusBar extends StatefulWidget {
final bool isRunning;
final bool lightOn;
final VoidCallback? onLightToggle;
final bool ceramicSleeveInstalled;
const StatusBar({
super.key,
this.isRunning = false,
this.lightOn = false,
this.onLightToggle,
this.ceramicSleeveInstalled = false,
});
@override
State<StatusBar> createState() => _StatusBarState();
}
class _StatusBarState extends State<StatusBar> {
String _currentTime = '';
Timer? _timer;
@override
void initState() {
super.initState();
_updateTime();
_timer = Timer.periodic(const Duration(seconds: 1), (_) => _updateTime());
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _updateTime() {
final now = DateTime.now();
_currentTime =
'${now.year}-${_twoDigits(now.month)}-${_twoDigits(now.day)} '
'${_twoDigits(now.hour)}:${_twoDigits(now.minute)}:${_twoDigits(now.second)}';
if (mounted) setState(() {});
}
String _twoDigits(int n) => n.toString().padLeft(2, '0');
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
decoration: BoxDecoration(
color: AppTheme.primaryColor,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.precision_manufacturing, color: Colors.white, size: 22),
const SizedBox(width: 10),
Text(
l10n?.deviceName ?? '污水毒品前处理一体机',
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w700,
),
),
],
),
const Spacer(),
_LightToggleButton(isOn: widget.lightOn, onTap: widget.onLightToggle),
const SizedBox(width: 16),
_CeramicSleeveStatus(installed: widget.ceramicSleeveInstalled),
const SizedBox(width: 20),
StatusIndicator(
text: widget.isRunning
? (l10n?.running ?? '运行中')
: (l10n?.idle ?? '未运行'),
status: widget.isRunning
? DeviceStatusType.running
: DeviceStatusType.idle,
),
const SizedBox(width: 20),
Text(
_currentTime,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontFamily: 'monospace',
fontWeight: FontWeight.normal,
),
),
],
),
);
}
}
class _CeramicSleeveStatus extends StatelessWidget {
final bool installed;
const _CeramicSleeveStatus({required this.installed});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: installed ? Colors.greenAccent : Colors.redAccent,
),
),
const SizedBox(width: 6),
Text(
installed ? '瓷套棒: 已安装' : '瓷套棒: 未安装',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 12,
),
),
],
);
}
}
class _LightToggleButton extends StatelessWidget {
final bool isOn;
final VoidCallback? onTap;
const _LightToggleButton({this.isOn = false, this.onTap});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isOn
? Colors.white.withValues(alpha: 0.25)
: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1,
),
),
child: Icon(
isOn ? Icons.lightbulb : Icons.lightbulb_outline_rounded,
color: isOn ? Colors.yellowAccent : Colors.white.withValues(alpha: 0.8),
size: 20,
),
),
),
);
}
}

View File

@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../providers/steps_provider.dart';
import '../widgets/step_list.dart';
import '../widgets/step_form.dart';
import '../../programs/providers/programs_provider.dart';
/// 程序详情页面
/// 左侧步骤列表 + 右侧参数表单
class ProgramDetailPage extends ConsumerStatefulWidget {
final String programId;
const ProgramDetailPage({super.key, required this.programId});
@override
ConsumerState<ProgramDetailPage> createState() => _ProgramDetailPageState();
}
class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
late int _programIdInt;
@override
void initState() {
super.initState();
_programIdInt = int.tryParse(widget.programId) ?? 0;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final programsState = ref.watch(programsProvider);
final program = programsState.programs.where((p) => p.id == _programIdInt).firstOrNull;
final stepsState = ref.watch(stepsProvider(_programIdInt));
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('/programs'),
),
const SizedBox(width: 16),
// 程序名称
Text(
program?.name ?? (l10n?.detail ?? '程序详情'),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
// 保存按钮
CommonButton(
text: l10n?.save ?? '保存',
icon: Icons.save,
type: ButtonType.primary,
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已保存'),
backgroundColor: AppTheme.successColor,
),
);
},
),
],
),
),
// 主内容区域
Expanded(
child: stepsState.isLoading
? const Center(child: CircularProgressIndicator())
: Row(
children: [
// 左侧:步骤列表
SizedBox(
width: 400,
child: StepList(
programId: _programIdInt,
steps: stepsState.steps,
selectedStepId: stepsState.selectedStepId,
onStepSelected: (stepId) {
ref.read(stepsProvider(_programIdInt).notifier).selectStep(stepId);
},
onAddStep: () => _showAddStepDialog(context, ref),
onReorder: (oldIndex, newIndex) {
ref.read(stepsProvider(_programIdInt).notifier).reorderSteps(oldIndex, newIndex);
},
onDeleteSteps: (stepIds) {
ref.read(stepsProvider(_programIdInt).notifier).deleteSteps(stepIds);
},
),
),
// 分隔线
Container(
width: 1,
color: AppTheme.idleColor.withValues(alpha: 0.3),
),
// 右侧:步骤参数表单
Expanded(
child: stepsState.selectedStep != null
? StepForm(
programId: _programIdInt,
step: stepsState.selectedStep!,
onSave: (step) async {
final success = await ref
.read(stepsProvider(_programIdInt).notifier)
.updateStep(step);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('步骤已更新'),
backgroundColor: AppTheme.successColor,
),
);
}
},
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.edit_note,
size: 64,
color: AppTheme.idleColor,
),
const SizedBox(height: 16),
Text(
'请选择或添加步骤',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
),
),
],
),
),
),
],
),
),
],
),
),
);
}
/// 显示添加步骤对话框
void _showAddStepDialog(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => Dialog(
child: Container(
width: 600,
padding: const EdgeInsets.all(24),
child: StepForm(
programId: _programIdInt,
isNew: true,
onSave: (step) async {
final success = await ref
.read(stepsProvider(_programIdInt).notifier)
.addStep(step);
if (success) {
Navigator.of(context).pop();
}
},
),
),
),
);
}
}

View File

@@ -0,0 +1,161 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../programs/models/step.dart';
import '../../programs/services/program_service.dart';
/// 步骤状态
class StepsState {
final List<Step> steps;
final int? selectedStepId;
final bool isLoading;
final String? error;
const StepsState({
this.steps = const [],
this.selectedStepId,
this.isLoading = false,
this.error,
});
StepsState copyWith({
List<Step>? steps,
int? selectedStepId,
bool? isLoading,
String? error,
bool clearSelection = false,
bool clearError = false,
}) {
return StepsState(
steps: steps ?? this.steps,
selectedStepId: clearSelection ? null : (selectedStepId ?? this.selectedStepId),
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
);
}
/// 获取选中的步骤
Step? get selectedStep {
if (selectedStepId == null) return null;
return steps.where((s) => s.id == selectedStepId).firstOrNull;
}
}
/// 步骤 Notifier
class StepsNotifier extends StateNotifier<StepsState> {
final ProgramService _service;
final int programId;
StepsNotifier(this._service, this.programId) : super(const StepsState()) {
loadSteps();
}
/// 加载步骤
Future<void> loadSteps() async {
state = state.copyWith(isLoading: true, clearError: true);
try {
final steps = await _service.getStepsByProgramId(programId);
state = state.copyWith(steps: steps, isLoading: false);
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}
/// 选择步骤
void selectStep(int? stepId) {
state = state.copyWith(selectedStepId: stepId);
}
/// 清除选择
void clearSelection() {
state = state.copyWith(clearSelection: true);
}
/// 添加步骤
Future<bool> addStep(Step step) async {
try {
await _service.addStep(step);
await loadSteps();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 更新步骤
Future<bool> updateStep(Step step) async {
if (step.id == null) return false;
try {
await _service.updateStep(step);
await loadSteps();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 删除步骤
Future<bool> deleteStep(int stepId) async {
try {
await _service.deleteStep(stepId);
if (state.selectedStepId == stepId) {
state = state.copyWith(clearSelection: true);
}
await loadSteps();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 批量删除步骤
Future<bool> deleteSteps(List<int> stepIds) async {
try {
await _service.deleteSteps(stepIds);
if (stepIds.contains(state.selectedStepId)) {
state = state.copyWith(clearSelection: true);
}
await loadSteps();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 重新排序步骤
Future<void> reorderSteps(int oldIndex, int newIndex) async {
final steps = List<Step>.from(state.steps);
final step = steps.removeAt(oldIndex);
steps.insert(newIndex, step);
// 更新 step_no
for (int i = 0; i < steps.length; i++) {
steps[i] = steps[i].copyWith(stepNo: i + 1);
}
state = state.copyWith(steps: steps);
// 持久化排序
await _service.reorderSteps(programId, steps.map((s) => s.id!).toList());
}
}
/// 程序服务 Provider
final programServiceProvider = Provider<ProgramService>((ref) {
return ProgramService.instance;
});
/// 步骤 Provider按程序ID
final stepsProvider = StateNotifierProvider.family<StepsNotifier, StepsState, int>(
(ref, programId) {
final service = ref.watch(programServiceProvider);
return StepsNotifier(service, programId);
},
);
/// 选中的步骤 Provider
final selectedStepProvider = Provider.family<Step?, int>((ref, programId) {
return ref.watch(stepsProvider(programId)).selectedStep;
});

View File

@@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/utils/constants.dart';
import '../../../shared/widgets/common_button.dart';
import '../../programs/models/step.dart' as models;
/// 步骤参数表单
class StepForm extends StatefulWidget {
final int programId;
final models.Step? step;
final bool isNew;
final void Function(models.Step) onSave;
const StepForm({
super.key,
required this.programId,
this.step,
this.isNew = false,
required this.onSave,
});
@override
State<StepForm> createState() => _StepFormState();
}
class _StepFormState extends State<StepForm> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _mixTimeController;
late TextEditingController _magnetTimeController;
late TextEditingController _volumeController;
late TextEditingController _blowTimeController;
String _position = 'A1';
String _mixSpeed = '中速';
String _blowSpeed = '中速';
int _needleSpeed = 5;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.step?.name ?? '');
_mixTimeController = TextEditingController(text: '${widget.step?.mixTime ?? 0}');
_magnetTimeController = TextEditingController(text: '${widget.step?.magnetTime ?? 0}');
_volumeController = TextEditingController(text: '${widget.step?.volume ?? 0}');
_blowTimeController = TextEditingController(text: '${widget.step?.blowTime ?? 0}');
_position = widget.step?.position ?? 'A1';
_mixSpeed = widget.step?.mixSpeed ?? '中速';
_blowSpeed = widget.step?.blowSpeed ?? '中速';
_needleSpeed = widget.step?.needleSpeed ?? 5;
}
@override
void dispose() {
_nameController.dispose();
_mixTimeController.dispose();
_magnetTimeController.dispose();
_volumeController.dispose();
_blowTimeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
widget.isNew ? '添加步骤' : '编辑步骤',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 24),
// 步骤名称
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: l10n?.stepName ?? '步骤名称',
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(l10n?.position ?? '孔位', style: TextStyle(color: AppTheme.textPrimary)),
const SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField<String>(
value: _position,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
items: Constants.positions.map((p) => DropdownMenuItem(value: p, child: Text(p))).toList(),
onChanged: (value) {
if (value != null) setState(() => _position = value);
},
),
),
],
),
const SizedBox(height: 16),
// 时间参数行
Row(
children: [
Expanded(
child: TextFormField(
controller: _mixTimeController,
decoration: InputDecoration(
labelText: '${l10n?.mixTime ?? '混合时间'} (${Constants.timeUnitSeconds})',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _magnetTimeController,
decoration: InputDecoration(
labelText: '${l10n?.magnetTime ?? '吸磁时间'} (${Constants.timeUnitSeconds})',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: 16),
// 容积和吹气时间
Row(
children: [
Expanded(
child: TextFormField(
controller: _volumeController,
decoration: InputDecoration(
labelText: '${l10n?.volume ?? '容积'} (${Constants.volumeUnit})',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _blowTimeController,
decoration: InputDecoration(
labelText: '${l10n?.blowTime ?? '吹气时间'} (${Constants.timeUnitMinutes})',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: 16),
// 速度选择
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: _mixSpeed,
decoration: InputDecoration(
labelText: l10n?.mixSpeed ?? '混合速度',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
items: Constants.speedOptions.map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
onChanged: (value) {
if (value != null) setState(() => _mixSpeed = value);
},
),
),
const SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField<String>(
value: _blowSpeed,
decoration: InputDecoration(
labelText: l10n?.blowSpeed ?? '吹气速度',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
items: Constants.speedOptions.map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
onChanged: (value) {
if (value != null) setState(() => _blowSpeed = value);
},
),
),
],
),
const SizedBox(height: 16),
// 下针速度滑块
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${l10n?.needleSpeed ?? '下针速度'}: $_needleSpeed', style: TextStyle(color: AppTheme.textPrimary)),
Slider(
value: _needleSpeed.toDouble(),
min: 1,
max: 10,
divisions: 9,
activeColor: AppTheme.primaryColor,
onChanged: (value) {
setState(() => _needleSpeed = value.round());
},
),
],
),
const SizedBox(height: 24),
// 保存按钮
CommonButton(
text: l10n?.save ?? '保存',
icon: Icons.save,
type: ButtonType.primary,
onPressed: _saveStep,
),
],
),
),
);
}
/// 保存步骤
void _saveStep() {
if (!_formKey.currentState!.validate()) return;
final step = models.Step(
id: widget.step?.id,
programId: widget.programId,
stepNo: widget.step?.stepNo ?? 1,
position: _position,
name: _nameController.text.trim(),
mixTime: int.tryParse(_mixTimeController.text) ?? 0,
magnetTime: int.tryParse(_magnetTimeController.text) ?? 0,
volume: int.tryParse(_volumeController.text) ?? 0,
mixSpeed: _mixSpeed,
blowSpeed: _blowSpeed,
blowTime: int.tryParse(_blowTimeController.text) ?? 0,
needleSpeed: _needleSpeed,
);
widget.onSave(step);
}
}

View File

@@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../../programs/models/step.dart' as models;
/// 步骤列表组件
class StepList extends StatefulWidget {
final int programId;
final List<models.Step> steps;
final int? selectedStepId;
final void Function(int?) onStepSelected;
final void Function() onAddStep;
final void Function(int oldIndex, int newIndex)? onReorder;
final void Function(List<int> stepIds)? onDeleteSteps;
const StepList({
super.key,
required this.programId,
required this.steps,
this.selectedStepId,
required this.onStepSelected,
required this.onAddStep,
this.onReorder,
this.onDeleteSteps,
});
@override
State<StepList> createState() => _StepListState();
}
class _StepListState extends State<StepList> {
final Set<int> _selectedIds = {};
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final allSelected = _selectedIds.length == widget.steps.length && widget.steps.isNotEmpty;
return Container(
color: Colors.white,
child: Column(
children: [
// 标题
Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
),
child: Row(
children: [
Icon(Icons.list, color: AppTheme.primaryColor, size: 20),
const SizedBox(width: 12),
Text(
'步骤列表',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
'${widget.steps.length}',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
),
),
],
),
),
// 表头
Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: AppTheme.idleColor.withValues(alpha: 0.2)),
),
),
child: Row(
children: [
SizedBox(
width: 40,
child: Checkbox(
value: allSelected,
onChanged: (value) {
setState(() {
if (value == true) {
_selectedIds.clear();
_selectedIds.addAll(widget.steps.map((s) => s.id!));
} else {
_selectedIds.clear();
}
});
},
),
),
SizedBox(width: 40, child: Text('#', style: TextStyle(fontSize: 12))),
Expanded(child: Text(l10n?.stepName ?? '名称', style: TextStyle(fontSize: 12))),
SizedBox(width: 60, child: Text(l10n?.position ?? '孔位', style: TextStyle(fontSize: 12))),
],
),
),
// 步骤列表(可拖拽排序)
Expanded(
child: widget.steps.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_circle_outline, size: 48, color: AppTheme.idleColor),
const SizedBox(height: 12),
Text('暂无步骤', style: TextStyle(color: AppTheme.textSecondary)),
],
),
)
: ReorderableListView.builder(
padding: const EdgeInsets.all(8),
itemCount: widget.steps.length,
onReorder: (oldIndex, newIndex) {
if (widget.onReorder != null) {
// 调整 newIndexReorderableListView 的特殊行为)
if (newIndex > oldIndex) newIndex -= 1;
widget.onReorder!(oldIndex, newIndex);
}
},
itemBuilder: (context, index) {
final step = widget.steps[index];
final isSelected = widget.selectedStepId == step.id || _selectedIds.contains(step.id);
return _buildStepItem(step, isSelected, index);
},
),
),
// 底部操作栏
Container(
height: 60,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: AppTheme.idleColor.withValues(alpha: 0.2)),
),
),
child: Row(
children: [
// 添加按钮
CommonButton(
text: '添加',
icon: Icons.add,
type: ButtonType.primary,
onPressed: widget.onAddStep,
),
const SizedBox(width: 12),
// 删除按钮
if (_selectedIds.isNotEmpty)
CommonButton(
text: '删除',
icon: Icons.delete,
type: ButtonType.danger,
onPressed: () => _showDeleteConfirmDialog(context),
),
],
),
),
],
),
);
}
/// 步骤项
Widget _buildStepItem(models.Step step, bool isSelected, int index) {
return Container(
key: ValueKey(step.id),
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryLight.withValues(alpha: 0.3) : Colors.white,
borderRadius: BorderRadius.circular(4),
border: isSelected ? Border.all(color: AppTheme.primaryColor, width: 2) : null,
),
child: ListTile(
dense: true,
leading: Checkbox(
value: _selectedIds.contains(step.id),
onChanged: (value) {
setState(() {
if (value == true) {
_selectedIds.add(step.id!);
} else {
_selectedIds.remove(step.id!);
}
});
},
),
title: Row(
children: [
Container(
width: 30,
alignment: Alignment.center,
child: Text(
'${step.stepNo}',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
Expanded(child: Text(step.name)),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
step.position,
style: TextStyle(
color: AppTheme.primaryColor,
fontSize: 12,
),
),
),
],
),
trailing: Icon(Icons.drag_handle, color: AppTheme.idleColor),
onTap: () => widget.onStepSelected(step.id),
),
);
}
/// 显示删除确认对话框
void _showDeleteConfirmDialog(BuildContext context) {
final l10n = AppLocalizations.of(context);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(l10n?.confirm ?? '确认'),
content: Text(
_selectedIds.length == 1
? '确定要删除此步骤吗?'
: '确定要删除选中的 ${_selectedIds.length} 个步骤吗?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(l10n?.cancel ?? '取消'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.errorColor,
foregroundColor: Colors.white,
),
onPressed: () {
Navigator.of(ctx).pop();
if (widget.onDeleteSteps != null) {
widget.onDeleteSteps!(_selectedIds.toList());
}
setState(() {
_selectedIds.clear();
});
},
child: Text(l10n?.confirm ?? '确认'),
),
],
),
);
}
}

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

View File

@@ -0,0 +1,382 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/localization/locale_provider.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../services/settings_service.dart';
/// 系统设置页面
class SettingsPage extends ConsumerStatefulWidget {
const SettingsPage({super.key});
@override
ConsumerState<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends ConsumerState<SettingsPage> {
String _currentVersion = 'V1.0.0';
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
// locale 用于语言切换,通过 ref.watch 保持监听
return Scaffold(
body: Container(
color: AppTheme.backgroundColor,
child: Row(
children: [
// 左侧导航菜单
SizedBox(
width: 280,
child: Container(
color: Colors.white,
child: Column(
children: [
// 返回按钮
Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
color: AppTheme.textPrimary,
onPressed: () => context.go('/'),
),
Text(
'返回首页',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
],
),
),
// 设置标题
Container(
height: 60,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
),
child: Row(
children: [
Icon(Icons.settings, color: AppTheme.primaryColor, size: 24),
const SizedBox(width: 12),
Text(
l10n?.settings ?? '系统设置',
style: TextStyle(
color: AppTheme.primaryColor,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
),
),
// 软件升级
_buildMenuItem(
icon: Icons.system_update,
title: l10n?.upgrade ?? '软件升级',
onTap: () {},
),
// 语言设置
_buildMenuItem(
icon: Icons.language,
title: l10n?.language ?? '语言设置',
onTap: () => _showLanguageDialog(),
),
// 安全设置
_buildMenuItem(
icon: Icons.lock,
title: l10n?.password ?? '密码修改',
onTap: () => _showPasswordDialog(),
),
// U盘导入
_buildMenuItem(
icon: Icons.usb,
title: l10n?.usbImport ?? 'U盘导入',
onTap: () => _showUsbImportDialog(),
),
],
),
),
),
// 右侧内容区域
Expanded(
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n?.upgrade ?? '软件升级',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 24),
// 版本信息
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.backgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.info_outline, color: AppTheme.primaryColor),
const SizedBox(width: 12),
Text(
'当前版本: $_currentVersion',
style: TextStyle(color: AppTheme.textPrimary),
),
],
),
),
const SizedBox(height: 24),
// 检查更新按钮
CommonButton(
text: '检查更新',
icon: Icons.refresh,
type: ButtonType.primary,
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已是最新版本'),
backgroundColor: AppTheme.successColor,
),
);
},
),
],
),
),
),
],
),
),
);
}
/// 导航菜单项
Widget _buildMenuItem({
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return ListTile(
leading: Icon(icon, color: AppTheme.textSecondary),
title: Text(title, style: TextStyle(color: AppTheme.textPrimary)),
trailing: Icon(Icons.chevron_right, color: AppTheme.idleColor),
onTap: onTap,
);
}
/// 显示语言选择对话框
void _showLanguageDialog() {
final locale = ref.read(localeProvider);
final currentLang = locale.languageCode;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('语言设置'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<String>(
title: Text('简体中文'),
value: 'zh',
groupValue: currentLang,
onChanged: (value) {
ref.read(localeProvider.notifier).setChinese();
Navigator.of(ctx).pop();
},
),
RadioListTile<String>(
title: Text('English'),
value: 'en',
groupValue: currentLang,
onChanged: (value) {
ref.read(localeProvider.notifier).setEnglish();
Navigator.of(ctx).pop();
},
),
],
),
),
);
}
/// 显示密码修改对话框
void _showPasswordDialog() {
final oldPasswordController = TextEditingController();
final newPasswordController = TextEditingController();
final confirmPasswordController = TextEditingController();
String? errorMessage;
showDialog(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: Text('密码修改'),
content: SizedBox(
width: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: oldPasswordController,
decoration: InputDecoration(
labelText: '原密码',
errorText: null,
),
obscureText: true,
),
const SizedBox(height: 12),
TextField(
controller: newPasswordController,
decoration: InputDecoration(
labelText: '新密码',
helperText: '至少6位字符',
),
obscureText: true,
),
const SizedBox(height: 12),
TextField(
controller: confirmPasswordController,
decoration: InputDecoration(labelText: '确认新密码'),
obscureText: true,
),
if (errorMessage != null)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
errorMessage!,
style: TextStyle(color: AppTheme.errorColor, fontSize: 12),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text('取消'),
),
ElevatedButton(
onPressed: () async {
// 验证逻辑
final oldPassword = oldPasswordController.text.trim();
final newPassword = newPasswordController.text.trim();
final confirmPassword = confirmPasswordController.text.trim();
// 检查空值
if (oldPassword.isEmpty || newPassword.isEmpty || confirmPassword.isEmpty) {
setState(() => errorMessage = '请填写所有字段');
return;
}
// 检查新密码长度
if (newPassword.length < 6) {
setState(() => errorMessage = '新密码至少6位字符');
return;
}
// 检查新密码一致性
if (newPassword != confirmPassword) {
setState(() => errorMessage = '两次输入的新密码不一致');
return;
}
// 验证原密码
final isValid = await SettingsService.instance.verifyPassword(oldPassword);
if (!isValid) {
setState(() => errorMessage = '原密码错误');
return;
}
// 保存新密码
final success = await SettingsService.instance.setPassword(newPassword);
Navigator.of(ctx).pop();
if (success) {
ScaffoldMessenger.of(this.context).showSnackBar(
SnackBar(
content: Text('密码已修改'),
backgroundColor: AppTheme.successColor,
),
);
} else {
ScaffoldMessenger.of(this.context).showSnackBar(
SnackBar(
content: Text('密码修改失败'),
backgroundColor: AppTheme.errorColor,
),
);
}
},
child: Text('确认'),
),
],
),
),
);
}
/// 显示U盘导入对话框
void _showUsbImportDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('U盘导入'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.usb, size: 48, color: AppTheme.warningColor),
const SizedBox(height: 16),
Text('未检测到U盘'),
const SizedBox(height: 8),
Text(
'请插入U盘后重试',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('关闭'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('正在检测U盘...'),
backgroundColor: AppTheme.primaryColor,
),
);
},
child: Text('重新检测'),
),
],
),
);
}
}

View File

@@ -0,0 +1,63 @@
import '../../../core/database/database_service.dart';
/// 设置服务
/// 管理系统设置(密码、语言偏好等)
class SettingsService {
static final SettingsService instance = SettingsService._internal();
final DatabaseService _db = DatabaseService.instance;
SettingsService._internal();
/// 获取密码
Future<String> getPassword() async {
final database = await _db.database;
final results = await database.query(
'settings',
where: 'key = ?',
whereArgs: ['password'],
);
if (results.isEmpty) return '123456'; // 默认密码
return results.first['value'] as String;
}
/// 设置密码
Future<bool> setPassword(String newPassword) async {
final database = await _db.database;
final count = await database.update(
'settings',
{'value': newPassword},
where: 'key = ?',
whereArgs: ['password'],
);
return count > 0;
}
/// 验证密码
Future<bool> verifyPassword(String password) async {
final storedPassword = await getPassword();
return password == storedPassword;
}
/// 获取设置值
Future<String?> getSetting(String key) async {
final database = await _db.database;
final results = await database.query(
'settings',
where: 'key = ?',
whereArgs: [key],
);
if (results.isEmpty) return null;
return results.first['value'] as String;
}
/// 设置值
Future<bool> setSetting(String key, String value) async {
final database = await _db.database;
// 使用 insert 或 replace
await database.execute(
'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
[key, value],
);
return true;
}
}

View File

@@ -0,0 +1,99 @@
import 'dart:async';
/// USB 检测服务
/// 监听 U盘插入/拔出事件
class UsbDetectionService {
static final UsbDetectionService instance = UsbDetectionService._internal();
UsbDetectionService._internal();
/// USB 状态
bool _isUsbConnected = false;
String? _usbPath;
/// 状态流
final StreamController<UsbState> _stateController = StreamController<UsbState>.broadcast();
/// 监听 USB 状态变化
Stream<UsbState> get stateStream => _stateController.stream;
/// 当前 USB 是否连接
bool get isConnected => _isUsbConnected;
/// USB 路径
String? get usbPath => _usbPath;
/// 开始监听 USB 事件
void startMonitoring() {
// TODO: 实现平台特定的 USB 监听
// Android: 使用 BroadcastReceiver 监听 ACTION_MEDIA_MOUNTED
// Linux: 监听 /dev/disk/by-path/ 或使用 udev
// Windows: 监听 WM_DEVICECHANGE
// 模拟实现:定时检测
_startPolling();
}
/// 停止监听
void stopMonitoring() {
// _stopPolling();
}
/// 模拟轮询检测(待平台实现)
void _startPolling() {
// TODO: 根据平台实现真实的 USB 检测
// 定时检测 /mnt/usb 或 /media/*/ 目录
}
/// 手动检测 USB
Future<bool> detectUsb() async {
// TODO: 实现平台特定的 USB 检测
// Android: 检查 getExternalFilesDir 或 mount points
// Linux: 检查 /mnt, /media 目录
// Windows: 检查 D:, E: 等驱动器
// 返回检测结果
return _isUsbConnected;
}
/// 获取 USB 上的程序文件列表
Future<List<String>> listProgramFiles() async {
if (!_isUsbConnected || _usbPath == null) {
return [];
}
// TODO: 扫描 USB 目录中的 .json 程序文件
// 示例路径: $_usbPath/programs/*.json
return [];
}
/// 模拟 USB 连接(用于测试)
void simulateConnection(String path) {
_isUsbConnected = true;
_usbPath = path;
_stateController.add(UsbState.connected(path));
}
/// 模拟 USB 断开(用于测试)
void simulateDisconnection() {
_isUsbConnected = false;
_usbPath = null;
_stateController.add(UsbState.disconnected());
}
/// 释放资源
void dispose() {
stopMonitoring();
_stateController.close();
}
}
/// USB 状态
class UsbState {
final bool isConnected;
final String? path;
const UsbState.connected(String path) : isConnected = true, path = path;
const UsbState.disconnected() : isConnected = false, path = null;
}

61
lib/main.dart Normal file
View File

@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'core/router/app_router.dart';
import 'core/theme/app_theme.dart';
import 'core/localization/app_localizations.dart';
import 'core/localization/locale_provider.dart';
import 'core/database/database_service.dart';
/// 应用入口
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Kiosk 模式:隐藏系统状态栏和导航栏
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
// 固定横屏
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
final db = DatabaseService.instance;
await db.database;
await db.initTestData();
runApp(const ProviderScope(child: KuaishaiApp()));
}
/// 应用主体
class KuaishaiApp extends ConsumerWidget {
const KuaishaiApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(goRouterProvider);
final locale = ref.watch(localeProvider);
return MaterialApp.router(
title: '污水毒品快检一体机',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme(),
darkTheme: AppTheme.darkTheme(),
themeMode: ThemeMode.light,
// 国际化配置
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('zh', 'CN'),
Locale('en', 'US'),
],
locale: locale,
// 路由配置
routerConfig: router,
);
}
}

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_theme.dart';
/// Toast 服务
/// 统一的消息提示管理
class ToastService {
/// 显示成功提示
static void showSuccess(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.successColor,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
),
);
}
/// 显示错误提示
static void showError(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.errorColor,
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
),
);
}
/// 显示警告提示
static void showWarning(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.warning, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.warningColor,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
),
);
}
/// 显示信息提示
static void showInfo(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.info, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.primaryColor,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
),
);
}
}

View File

@@ -0,0 +1,24 @@
/// 常量定义
class Constants {
// 速度选项
static const List<String> speedOptions = ['低速', '中速', '高速'];
// 下针速度档位
static const List<int> needleSpeedLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 孔位列表
static const List<String> positions = [
'A1', 'A2', 'A3', 'A4', 'A5', 'A6',
'B1', 'B2', 'B3', 'B4', 'B5', 'B6',
'C1', 'C2', 'C3', 'C4', 'C5', 'C6',
'D1', 'D2', 'D3', 'D4', 'D5', 'D6',
];
// 默认步骤名称
static const List<String> defaultStepNames = ['混合', '吸磁', '吹气', '下针'];
// 时间单位
static const String timeUnitSeconds = '';
static const String timeUnitMinutes = '分钟';
static const String volumeUnit = 'μL';
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
/// 响应式布局工具类
/// 目标屏幕: 1920x1080
class ResponsiveLayout {
static const double targetWidth = 1920;
static const double targetHeight = 1080;
/// 获取屏幕宽度比例
static double widthPercent(BuildContext context, double percent) {
return MediaQuery.of(context).size.width * percent;
}
/// 获取屏幕高度比例
static double heightPercent(BuildContext context, double percent) {
return MediaQuery.of(context).size.height * percent;
}
/// 基于目标屏幕缩放宽度
static double scaleWidth(BuildContext context, double targetValue) {
final screenWidth = MediaQuery.of(context).size.width;
return targetValue * (screenWidth / targetWidth);
}
/// 基于目标屏幕缩放高度
static double scaleHeight(BuildContext context, double targetValue) {
final screenHeight = MediaQuery.of(context).size.height;
return targetValue * (screenHeight / targetHeight);
}
/// 基于目标屏幕缩放字体
static double scaleFont(BuildContext context, double targetFontSize) {
return scaleWidth(context, targetFontSize);
}
/// 预设布局尺寸
static double sidebarWidth(BuildContext context) => widthPercent(context, 0.25); // 480px on 1920
static double detailWidth(BuildContext context) => widthPercent(context, 0.21); // 400px on 1920
static double navWidth(BuildContext context) => widthPercent(context, 0.15); // 280px on 1920
static double cardWidth(BuildContext context) => widthPercent(context, 0.30); // ~576px
}
/// 响应式间距
class ResponsiveSpacing {
static double small(BuildContext context) => ResponsiveLayout.scaleWidth(context, 8);
static double medium(BuildContext context) => ResponsiveLayout.scaleWidth(context, 16);
static double large(BuildContext context) => ResponsiveLayout.scaleWidth(context, 24);
static double xlarge(BuildContext context) => ResponsiveLayout.scaleWidth(context, 32);
}

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_theme.dart';
/// 通用按钮组件 - 明亮工业风格
class CommonButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool enabled;
final Color? backgroundColor;
final Color? textColor;
final IconData? icon;
final bool isLoading;
final ButtonType type;
const CommonButton({
super.key,
required this.text,
this.onPressed,
this.enabled = true,
this.backgroundColor,
this.textColor,
this.icon,
this.isLoading = false,
this.type = ButtonType.primary,
});
@override
Widget build(BuildContext context) {
final bgColor = backgroundColor ?? _getDefaultBackgroundColor();
final fgColor = textColor ?? _getDefaultTextColor();
Widget content;
if (isLoading) {
content = Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: fgColor,
),
),
const SizedBox(width: 8),
Text(text, style: TextStyle(fontWeight: FontWeight.w500)),
],
);
} else if (icon != null) {
content = Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18),
const SizedBox(width: 8),
Text(text, style: TextStyle(fontWeight: FontWeight.w500)),
],
);
} else {
content = Text(text, style: TextStyle(fontWeight: FontWeight.w500));
}
return ElevatedButton(
onPressed: enabled && !isLoading ? onPressed : null,
style: ElevatedButton.styleFrom(
backgroundColor: bgColor,
foregroundColor: fgColor,
disabledBackgroundColor: AppTheme.statusStopped.withValues(alpha: 0.3),
disabledForegroundColor: AppTheme.textTertiary,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
),
),
child: content,
);
}
Color _getDefaultBackgroundColor() {
switch (type) {
case ButtonType.primary:
return AppTheme.primaryColor;
case ButtonType.success:
return AppTheme.successColor;
case ButtonType.warning:
return AppTheme.warningColor;
case ButtonType.danger:
return AppTheme.errorColor;
case ButtonType.secondary:
return AppTheme.bgSurface;
}
}
Color _getDefaultTextColor() {
switch (type) {
case ButtonType.secondary:
return AppTheme.textPrimary;
default:
return AppTheme.textOnPrimary;
}
}
}
enum ButtonType { primary, success, warning, danger, secondary }

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_theme.dart';
/// 通用卡片组件 - 明亮工业风格
class CommonCard extends StatelessWidget {
final Widget child;
final VoidCallback? onTap;
final bool selected;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
const CommonCard({
super.key,
required this.child,
this.onTap,
this.selected = false,
this.padding,
this.margin,
});
@override
Widget build(BuildContext context) {
return Container(
margin: margin ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Material(
color: selected ? AppTheme.bgCardHover : AppTheme.bgCard,
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
elevation: 0,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
child: Container(
padding: padding ?? const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
border: selected
? Border.all(color: AppTheme.primaryColor, width: 2)
: Border.all(color: AppTheme.borderLight, width: 1),
boxShadow: AppTheme.shadowCard,
),
child: child,
),
),
),
);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
/// 确认对话框组件
class CommonDialog {
/// 显示确认对话框
static Future<bool?> showConfirm({
required BuildContext context,
required String title,
required String content,
String confirmText = '确认',
String cancelText = '取消',
bool isDestructive = false,
}) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(cancelText),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: isDestructive ? Colors.red : null,
),
child: Text(confirmText),
),
],
),
);
}
/// 显示信息对话框
static Future<void> showInfo({
required BuildContext context,
required String title,
required String content,
String confirmText = '确认',
}) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(confirmText),
),
],
),
);
}
/// 显示输入对话框
static Future<String?> showInput({
required BuildContext context,
required String title,
String? hintText,
String? initialValue,
String confirmText = '确认',
String cancelText = '取消',
}) {
final controller = TextEditingController(text: initialValue);
return showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: TextField(
decoration: InputDecoration(
hintText: hintText,
border: const OutlineInputBorder(),
),
controller: controller,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(cancelText),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, controller.text),
child: Text(confirmText),
),
],
),
);
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_theme.dart';
/// 空状态组件
/// 统一的空数据展示样式
class EmptyStateWidget extends StatelessWidget {
final IconData icon;
final String message;
final String? actionText;
final VoidCallback? onAction;
const EmptyStateWidget({
super.key,
required this.icon,
required this.message,
this.actionText,
this.onAction,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 64,
color: AppTheme.idleColor,
),
const SizedBox(height: 16),
Text(
message,
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
),
),
if (actionText != null && onAction != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onAction,
icon: const Icon(Icons.add, size: 20),
label: Text(actionText!),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
],
],
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_theme.dart';
/// 状态指示器组件 - 明亮工业风格
class StatusIndicator extends StatelessWidget {
final String text;
final DeviceStatusType status;
final double size;
const StatusIndicator({
super.key,
required this.text,
required this.status,
this.size = 10,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getStatusColor(),
),
),
const SizedBox(width: 6),
Text(
text,
style: TextStyle(
color: _getStatusColor(),
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
],
);
}
Color _getStatusColor() {
switch (status) {
case DeviceStatusType.running:
return AppTheme.statusRunning;
case DeviceStatusType.idle:
return AppTheme.statusStopped;
case DeviceStatusType.paused:
return AppTheme.statusPaused;
case DeviceStatusType.error:
return AppTheme.statusError;
case DeviceStatusType.success:
return AppTheme.statusRunning;
}
}
}
enum DeviceStatusType { running, idle, paused, error, success }