chore(project): 初始化项目基础配置文件
- 添加 CodeGraph、Android 和通用 gitignore 配置 - 创建项目元数据文件跟踪 Flutter 项目属性 - 添加 Codex AI 指导文档 AGENTS.md 说明项目架构 - 配置代码分析选项 analysis_options.yaml - 设置 Android 应用清单权限和 Kiosk 模式配置 - 实现中英文国际化支持 AppLocalizations - 配置 GoRouter 应用路由导航 - 创建明亮工业控制风格的主题配置 AppTheme
This commit is contained in:
164
lib/core/database/database_service.dart
Normal file
164
lib/core/database/database_service.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
366
lib/core/localization/app_localizations.dart
Normal file
366
lib/core/localization/app_localizations.dart
Normal 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;
|
||||
}
|
||||
48
lib/core/localization/locale_provider.dart
Normal file
48
lib/core/localization/locale_provider.dart
Normal 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';
|
||||
});
|
||||
45
lib/core/router/app_router.dart
Normal file
45
lib/core/router/app_router.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
179
lib/core/theme/app_theme.dart
Normal file
179
lib/core/theme/app_theme.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
73
lib/features/device/models/device_state.dart
Normal file
73
lib/features/device/models/device_state.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
188
lib/features/device/providers/run_state_provider.dart
Normal file
188
lib/features/device/providers/run_state_provider.dart
Normal 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;
|
||||
});
|
||||
190
lib/features/device/services/mock_runner.dart
Normal file
190
lib/features/device/services/mock_runner.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
114
lib/features/device/services/mock_runner_impl.dart
Normal file
114
lib/features/device/services/mock_runner_impl.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
54
lib/features/device/services/runner_interface.dart
Normal file
54
lib/features/device/services/runner_interface.dart
Normal 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();
|
||||
}
|
||||
91
lib/features/device/services/serial_runner.dart
Normal file
91
lib/features/device/services/serial_runner.dart
Normal 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)
|
||||
}
|
||||
}
|
||||
202
lib/features/home/pages/complete_page.dart
Normal file
202
lib/features/home/pages/complete_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
176
lib/features/home/pages/home_page.dart
Normal file
176
lib/features/home/pages/home_page.dart
Normal 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()),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
222
lib/features/home/widgets/program_list.dart
Normal file
222
lib/features/home/widgets/program_list.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
242
lib/features/home/widgets/run_status_monitor.dart
Normal file
242
lib/features/home/widgets/run_status_monitor.dart
Normal 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',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
365
lib/features/home/widgets/running_control_panel.dart
Normal file
365
lib/features/home/widgets/running_control_panel.dart
Normal 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 ?? '确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
181
lib/features/home/widgets/status_bar.dart
Normal file
181
lib/features/home/widgets/status_bar.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
199
lib/features/program_detail/pages/program_detail_page.dart
Normal file
199
lib/features/program_detail/pages/program_detail_page.dart
Normal 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();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
161
lib/features/program_detail/providers/steps_provider.dart
Normal file
161
lib/features/program_detail/providers/steps_provider.dart
Normal 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;
|
||||
});
|
||||
270
lib/features/program_detail/widgets/step_form.dart
Normal file
270
lib/features/program_detail/widgets/step_form.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
272
lib/features/program_detail/widgets/step_list.dart
Normal file
272
lib/features/program_detail/widgets/step_list.dart
Normal 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) {
|
||||
// 调整 newIndex(ReorderableListView 的特殊行为)
|
||||
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 ?? '确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
lib/features/programs/models/program.dart
Normal file
52
lib/features/programs/models/program.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
/// 程序模型
|
||||
class Program {
|
||||
final int? id;
|
||||
final String code;
|
||||
final String name;
|
||||
final String createdAt;
|
||||
final int status; // 1: 启用, 0: 停用
|
||||
|
||||
Program({
|
||||
this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
this.status = 1,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'code': code,
|
||||
'name': name,
|
||||
'created_at': createdAt,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
|
||||
factory Program.fromMap(Map<String, dynamic> map) {
|
||||
return Program(
|
||||
id: map['id'] as int?,
|
||||
code: map['code'] as String,
|
||||
name: map['name'] as String,
|
||||
createdAt: map['created_at'] as String,
|
||||
status: map['status'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
|
||||
Program copyWith({
|
||||
int? id,
|
||||
String? code,
|
||||
String? name,
|
||||
String? createdAt,
|
||||
int? status,
|
||||
}) {
|
||||
return Program(
|
||||
id: id ?? this.id,
|
||||
code: code ?? this.code,
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
94
lib/features/programs/models/step.dart
Normal file
94
lib/features/programs/models/step.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
/// 步骤模型
|
||||
class Step {
|
||||
final int? id;
|
||||
final int programId;
|
||||
final int stepNo;
|
||||
final String position;
|
||||
final String name;
|
||||
final int mixTime;
|
||||
final int magnetTime;
|
||||
final int volume;
|
||||
final String mixSpeed;
|
||||
final String blowSpeed;
|
||||
final int blowTime;
|
||||
final int needleSpeed;
|
||||
|
||||
Step({
|
||||
this.id,
|
||||
required this.programId,
|
||||
required this.stepNo,
|
||||
required this.position,
|
||||
required this.name,
|
||||
this.mixTime = 0,
|
||||
this.magnetTime = 0,
|
||||
this.volume = 0,
|
||||
this.mixSpeed = '中速',
|
||||
this.blowSpeed = '中速',
|
||||
this.blowTime = 0,
|
||||
this.needleSpeed = 5,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'program_id': programId,
|
||||
'step_no': stepNo,
|
||||
'position': position,
|
||||
'name': name,
|
||||
'mix_time': mixTime,
|
||||
'magnet_time': magnetTime,
|
||||
'volume': volume,
|
||||
'mix_speed': mixSpeed,
|
||||
'blow_speed': blowSpeed,
|
||||
'blow_time': blowTime,
|
||||
'needle_speed': needleSpeed,
|
||||
};
|
||||
}
|
||||
|
||||
factory Step.fromMap(Map<String, dynamic> map) {
|
||||
return Step(
|
||||
id: map['id'] as int?,
|
||||
programId: map['program_id'] as int,
|
||||
stepNo: map['step_no'] as int,
|
||||
position: map['position'] as String,
|
||||
name: map['name'] as String,
|
||||
mixTime: map['mix_time'] as int? ?? 0,
|
||||
magnetTime: map['magnet_time'] as int? ?? 0,
|
||||
volume: map['volume'] as int? ?? 0,
|
||||
mixSpeed: map['mix_speed'] as String? ?? '中速',
|
||||
blowSpeed: map['blow_speed'] as String? ?? '中速',
|
||||
blowTime: map['blow_time'] as int? ?? 0,
|
||||
needleSpeed: map['needle_speed'] as int? ?? 5,
|
||||
);
|
||||
}
|
||||
|
||||
Step copyWith({
|
||||
int? id,
|
||||
int? programId,
|
||||
int? stepNo,
|
||||
String? position,
|
||||
String? name,
|
||||
int? mixTime,
|
||||
int? magnetTime,
|
||||
int? volume,
|
||||
String? mixSpeed,
|
||||
String? blowSpeed,
|
||||
int? blowTime,
|
||||
int? needleSpeed,
|
||||
}) {
|
||||
return Step(
|
||||
id: id ?? this.id,
|
||||
programId: programId ?? this.programId,
|
||||
stepNo: stepNo ?? this.stepNo,
|
||||
position: position ?? this.position,
|
||||
name: name ?? this.name,
|
||||
mixTime: mixTime ?? this.mixTime,
|
||||
magnetTime: magnetTime ?? this.magnetTime,
|
||||
volume: volume ?? this.volume,
|
||||
mixSpeed: mixSpeed ?? this.mixSpeed,
|
||||
blowSpeed: blowSpeed ?? this.blowSpeed,
|
||||
blowTime: blowTime ?? this.blowTime,
|
||||
needleSpeed: needleSpeed ?? this.needleSpeed,
|
||||
);
|
||||
}
|
||||
}
|
||||
509
lib/features/programs/pages/programs_page.dart
Normal file
509
lib/features/programs/pages/programs_page.dart
Normal file
@@ -0,0 +1,509 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'dart:io';
|
||||
import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/common_button.dart';
|
||||
import '../models/program.dart';
|
||||
import '../providers/programs_provider.dart';
|
||||
import '../widgets/program_form_dialog.dart';
|
||||
import '../services/program_import_service.dart';
|
||||
|
||||
/// 程序管理页面
|
||||
class ProgramsPage extends ConsumerStatefulWidget {
|
||||
const ProgramsPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProgramsPage> createState() => _ProgramsPageState();
|
||||
}
|
||||
|
||||
class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
||||
final Set<int> _selectedIds = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final programsState = ref.watch(programsProvider);
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: AppTheme.backgroundColor,
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部导航栏
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
l10n?.programs ?? '程序管理',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// 新增按钮
|
||||
CommonButton(
|
||||
text: l10n?.addProgram ?? '新增',
|
||||
icon: Icons.add,
|
||||
type: ButtonType.primary,
|
||||
onPressed: () => _showAddDialog(context, ref),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 导入按钮
|
||||
CommonButton(
|
||||
text: l10n?.importProgram ?? '导入',
|
||||
icon: Icons.file_upload,
|
||||
type: ButtonType.secondary,
|
||||
onPressed: () => _importPrograms(context, ref),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 程序列表表格
|
||||
Expanded(
|
||||
child: programsState.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: programsState.programs.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.folder_open,
|
||||
size: 64,
|
||||
color: AppTheme.idleColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n?.noData ?? '暂无数据',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(2, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// 表头
|
||||
_buildTableHeader(l10n, programsState.programs),
|
||||
// 表格内容
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: programsState.programs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final program = programsState.programs[index];
|
||||
final isSelected = _selectedIds.contains(program.id);
|
||||
return _buildTableRow(
|
||||
context,
|
||||
ref,
|
||||
l10n,
|
||||
program,
|
||||
isSelected,
|
||||
index == programsState.programs.length - 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 底部操作栏
|
||||
if (programsState.programs.isNotEmpty)
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${l10n?.selected ?? '已选择'}: ${_selectedIds.length}',
|
||||
style: TextStyle(color: AppTheme.textSecondary),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_selectedIds.isNotEmpty)
|
||||
CommonButton(
|
||||
text: l10n?.deleteProgram ?? '删除',
|
||||
icon: Icons.delete,
|
||||
type: ButtonType.danger,
|
||||
onPressed: () => _showDeleteConfirmDialog(
|
||||
context,
|
||||
ref,
|
||||
l10n,
|
||||
_selectedIds.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 表头
|
||||
Widget _buildTableHeader(AppLocalizations? l10n, List<Program> programs) {
|
||||
final allSelected = _selectedIds.length == programs.length && programs.isNotEmpty;
|
||||
|
||||
return Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 复选框
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Checkbox(
|
||||
value: allSelected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedIds.clear();
|
||||
_selectedIds.addAll(programs.map((p) => p.id!));
|
||||
} else {
|
||||
_selectedIds.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
// 编号
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
l10n?.programCode ?? '编号',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 名称
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
l10n?.programName ?? '名称',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 创建时间
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n?.createTime ?? '创建时间',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 状态
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Text(
|
||||
'状态',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 操作
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Text(
|
||||
l10n?.detail ?? '操作',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 表格行
|
||||
Widget _buildTableRow(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppLocalizations? l10n,
|
||||
Program program,
|
||||
bool isSelected,
|
||||
bool isLast,
|
||||
) {
|
||||
return Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppTheme.primaryLight.withValues(alpha: 0.2) : null,
|
||||
border: isLast
|
||||
? null
|
||||
: Border(
|
||||
bottom: BorderSide(
|
||||
color: AppTheme.idleColor.withValues(alpha: 0.2),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 复选框
|
||||
SizedBox(
|
||||
width: 50,
|
||||
child: Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedIds.add(program.id!);
|
||||
} else {
|
||||
_selectedIds.remove(program.id!);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
// 编号
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
program.code,
|
||||
style: TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
),
|
||||
// 名称
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Text(
|
||||
program.name,
|
||||
style: TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
),
|
||||
// 创建时间
|
||||
Expanded(
|
||||
child: Text(
|
||||
program.createdAt,
|
||||
style: TextStyle(color: AppTheme.textSecondary),
|
||||
),
|
||||
),
|
||||
// 状态
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: program.status == 1
|
||||
? AppTheme.successColor.withValues(alpha: 0.1)
|
||||
: AppTheme.idleColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
program.status == 1 ? '启用' : '停用',
|
||||
style: TextStyle(
|
||||
color: program.status == 1
|
||||
? AppTheme.successColor
|
||||
: AppTheme.idleColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
// 操作按钮
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, size: 20),
|
||||
color: AppTheme.primaryColor,
|
||||
onPressed: () => _showEditDialog(context, ref, program),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete, size: 20),
|
||||
color: AppTheme.errorColor,
|
||||
onPressed: () => _showDeleteConfirmDialog(
|
||||
context,
|
||||
ref,
|
||||
l10n,
|
||||
[program.id!],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.visibility, size: 20),
|
||||
color: AppTheme.textSecondary,
|
||||
onPressed: () => context.go('/programs/${program.id}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示新增对话框
|
||||
void _showAddDialog(BuildContext context, WidgetRef ref) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const ProgramFormDialog(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 导入程序
|
||||
Future<void> _importPrograms(BuildContext context, WidgetRef ref) async {
|
||||
try {
|
||||
// 选择文件
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
allowMultiple: false,
|
||||
);
|
||||
|
||||
if (result == null || result.files.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final file = result.files.first;
|
||||
if (file.path == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('无法读取文件'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
final jsonContent = await File(file.path!).readAsString();
|
||||
|
||||
// 导入程序
|
||||
final importedCount = await ProgramImportService.instance.importFromJson(jsonContent);
|
||||
|
||||
// 刷新程序列表
|
||||
ref.read(programsProvider.notifier).loadPrograms();
|
||||
|
||||
// 显示结果
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('成功导入 $importedCount 个程序'),
|
||||
backgroundColor: importedCount > 0 ? AppTheme.successColor : AppTheme.warningColor,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('导入失败: ${e.toString()}'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 显示编辑对话框
|
||||
void _showEditDialog(BuildContext context, WidgetRef ref, Program program) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ProgramFormDialog(program: program),
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示删除确认对话框
|
||||
void _showDeleteConfirmDialog(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppLocalizations? l10n,
|
||||
List<int> ids,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n?.confirm ?? '确认'),
|
||||
content: Text(
|
||||
ids.length == 1
|
||||
? '确定要删除此程序吗?'
|
||||
: '确定要删除选中的 ${ids.length} 个程序吗?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(l10n?.cancel ?? '取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
onPressed: () async {
|
||||
final notifier = ref.read(programsProvider.notifier);
|
||||
await notifier.deletePrograms(ids);
|
||||
setState(() {
|
||||
_selectedIds.removeAll(ids);
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(l10n?.confirm ?? '确认'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
192
lib/features/programs/providers/programs_provider.dart
Normal file
192
lib/features/programs/providers/programs_provider.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/database/database_service.dart';
|
||||
import '../models/program.dart';
|
||||
|
||||
/// 程序列表状态
|
||||
class ProgramsState {
|
||||
final List<Program> programs;
|
||||
final int? selectedProgramId;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const ProgramsState({
|
||||
this.programs = const [],
|
||||
this.selectedProgramId,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
ProgramsState copyWith({
|
||||
List<Program>? programs,
|
||||
int? selectedProgramId,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
bool clearSelection = false,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return ProgramsState(
|
||||
programs: programs ?? this.programs,
|
||||
selectedProgramId: clearSelection ? null : (selectedProgramId ?? this.selectedProgramId),
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取选中的程序
|
||||
Program? get selectedProgram {
|
||||
if (selectedProgramId == null) return null;
|
||||
return programs.where((p) => p.id == selectedProgramId).firstOrNull;
|
||||
}
|
||||
}
|
||||
|
||||
/// 程序列表 Notifier
|
||||
class ProgramsNotifier extends StateNotifier<ProgramsState> {
|
||||
final DatabaseService _db;
|
||||
|
||||
ProgramsNotifier(this._db) : super(const ProgramsState()) {
|
||||
loadPrograms();
|
||||
}
|
||||
|
||||
/// 加载所有程序
|
||||
Future<void> loadPrograms() async {
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
final db = await _db.database;
|
||||
final maps = await db.query('programs', orderBy: 'created_at DESC');
|
||||
final programs = maps.map((m) => Program.fromMap(m)).toList();
|
||||
|
||||
state = state.copyWith(programs: programs, isLoading: false);
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 选择程序
|
||||
void selectProgram(int? programId) {
|
||||
state = state.copyWith(selectedProgramId: programId);
|
||||
}
|
||||
|
||||
/// 清除选择
|
||||
void clearSelection() {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
/// 新增程序
|
||||
Future<bool> addProgram(Program program) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.insert('programs', program.toMap());
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新程序
|
||||
Future<bool> updateProgram(Program program) async {
|
||||
if (program.id == null) return false;
|
||||
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.update(
|
||||
'programs',
|
||||
program.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [program.id],
|
||||
);
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除程序
|
||||
Future<bool> deleteProgram(int programId) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.delete('programs', where: 'id = ?', whereArgs: [programId]);
|
||||
|
||||
// 如果删除的是选中的程序,清除选择
|
||||
if (state.selectedProgramId == programId) {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量删除程序
|
||||
Future<bool> deletePrograms(List<int> programIds) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
await db.delete(
|
||||
'programs',
|
||||
where: 'id IN (${programIds.map((_) => '?').join(',')})',
|
||||
whereArgs: programIds,
|
||||
);
|
||||
|
||||
// 如果删除的是选中的程序,清除选择
|
||||
if (programIds.contains(state.selectedProgramId)) {
|
||||
state = state.copyWith(clearSelection: true);
|
||||
}
|
||||
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换程序状态
|
||||
Future<bool> toggleStatus(int programId) async {
|
||||
try {
|
||||
final db = await _db.database;
|
||||
final program = state.programs.where((p) => p.id == programId).firstOrNull;
|
||||
if (program == null) return false;
|
||||
|
||||
await db.update(
|
||||
'programs',
|
||||
{'status': program.status == 1 ? 0 : 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [programId],
|
||||
);
|
||||
await loadPrograms();
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(error: e.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 数据库服务 Provider
|
||||
final databaseServiceProvider = Provider<DatabaseService>((ref) {
|
||||
return DatabaseService.instance;
|
||||
});
|
||||
|
||||
/// 程序列表 Provider
|
||||
final programsProvider =
|
||||
StateNotifierProvider<ProgramsNotifier, ProgramsState>((ref) {
|
||||
final db = ref.watch(databaseServiceProvider);
|
||||
return ProgramsNotifier(db);
|
||||
});
|
||||
|
||||
/// 选中的程序 Provider
|
||||
final selectedProgramProvider = Provider<Program?>((ref) {
|
||||
return ref.watch(programsProvider).selectedProgram;
|
||||
});
|
||||
|
||||
/// 启用的程序列表 Provider
|
||||
final enabledProgramsProvider = Provider<List<Program>>((ref) {
|
||||
return ref.watch(programsProvider).programs.where((p) => p.status == 1).toList();
|
||||
});
|
||||
126
lib/features/programs/services/program_import_service.dart
Normal file
126
lib/features/programs/services/program_import_service.dart
Normal file
@@ -0,0 +1,126 @@
|
||||
import 'dart:convert';
|
||||
import '../../programs/models/program.dart';
|
||||
import '../../programs/models/step.dart';
|
||||
import '../../programs/services/program_service.dart';
|
||||
|
||||
/// 程序导入服务
|
||||
class ProgramImportService {
|
||||
static final ProgramImportService instance = ProgramImportService._internal();
|
||||
final ProgramService _programService = ProgramService.instance;
|
||||
|
||||
ProgramImportService._internal();
|
||||
|
||||
/// 从 JSON 字符串导入程序
|
||||
/// 返回导入的程序数量
|
||||
Future<int> importFromJson(String jsonContent) async {
|
||||
final data = jsonDecode(jsonContent);
|
||||
|
||||
// 支持单个程序或程序数组
|
||||
final List<dynamic> programsData;
|
||||
if (data is List) {
|
||||
programsData = data;
|
||||
} else if (data is Map && data.containsKey('programs')) {
|
||||
programsData = data['programs'] as List;
|
||||
} else {
|
||||
programsData = [data];
|
||||
}
|
||||
|
||||
int importedCount = 0;
|
||||
|
||||
for (final programData in programsData) {
|
||||
try {
|
||||
// 验证必填字段
|
||||
if (!_validateProgramData(programData)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查编号是否已存在
|
||||
final existingPrograms = await _programService.getAllPrograms();
|
||||
final code = programData['code'] as String;
|
||||
if (existingPrograms.any((p) => p.code == code)) {
|
||||
// 编号已存在,跳过或使用新编号
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建程序
|
||||
final program = Program(
|
||||
code: code,
|
||||
name: programData['name'] as String,
|
||||
createdAt: programData['createdAt'] ?? DateTime.now().toString().split('.')[0],
|
||||
status: programData['status'] ?? 1,
|
||||
);
|
||||
|
||||
final programId = await _programService.addProgram(program);
|
||||
|
||||
// 导入步骤
|
||||
final stepsData = programData['steps'] as List?;
|
||||
if (stepsData != null) {
|
||||
for (int i = 0; i < stepsData.length; i++) {
|
||||
final stepData = stepsData[i];
|
||||
final step = Step(
|
||||
programId: programId,
|
||||
stepNo: i + 1,
|
||||
position: stepData['position'] as String? ?? 'A1',
|
||||
name: stepData['name'] as String? ?? '步骤${i + 1}',
|
||||
mixTime: stepData['mixTime'] as int? ?? 0,
|
||||
magnetTime: stepData['magnetTime'] as int? ?? 0,
|
||||
volume: stepData['volume'] as int? ?? 0,
|
||||
mixSpeed: stepData['mixSpeed'] as String? ?? '中速',
|
||||
blowSpeed: stepData['blowSpeed'] as String? ?? '中速',
|
||||
blowTime: stepData['blowTime'] as int? ?? 0,
|
||||
needleSpeed: stepData['needleSpeed'] as int? ?? 5,
|
||||
);
|
||||
await _programService.addStep(step);
|
||||
}
|
||||
}
|
||||
|
||||
importedCount++;
|
||||
} catch (e) {
|
||||
// 忽略单个程序导入错误
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return importedCount;
|
||||
}
|
||||
|
||||
/// 验证程序数据
|
||||
bool _validateProgramData(Map<String, dynamic> data) {
|
||||
return data.containsKey('code') &&
|
||||
data.containsKey('name') &&
|
||||
data['code'] is String &&
|
||||
data['name'] is String;
|
||||
}
|
||||
|
||||
/// 导出程序为 JSON
|
||||
Future<String> exportToJson(List<int> programIds) async {
|
||||
final programs = [];
|
||||
|
||||
for (final id in programIds) {
|
||||
final program = await _programService.getProgramById(id);
|
||||
if (program == null) continue;
|
||||
|
||||
final steps = await _programService.getStepsByProgramId(id);
|
||||
|
||||
programs.add({
|
||||
'code': program.code,
|
||||
'name': program.name,
|
||||
'createdAt': program.createdAt,
|
||||
'status': program.status,
|
||||
'steps': steps.map((s) => {
|
||||
'position': s.position,
|
||||
'name': s.name,
|
||||
'mixTime': s.mixTime,
|
||||
'magnetTime': s.magnetTime,
|
||||
'volume': s.volume,
|
||||
'mixSpeed': s.mixSpeed,
|
||||
'blowSpeed': s.blowSpeed,
|
||||
'blowTime': s.blowTime,
|
||||
'needleSpeed': s.needleSpeed,
|
||||
}).toList(),
|
||||
});
|
||||
}
|
||||
|
||||
return jsonEncode({'programs': programs});
|
||||
}
|
||||
}
|
||||
156
lib/features/programs/services/program_service.dart
Normal file
156
lib/features/programs/services/program_service.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import '../../../core/database/database_service.dart';
|
||||
import '../models/program.dart';
|
||||
import '../models/step.dart';
|
||||
|
||||
/// 程序服务
|
||||
/// 封装程序和步骤的数据库操作
|
||||
class ProgramService {
|
||||
static final ProgramService instance = ProgramService._internal();
|
||||
final DatabaseService _db = DatabaseService.instance;
|
||||
|
||||
ProgramService._internal();
|
||||
|
||||
/// 获取所有程序
|
||||
Future<List<Program>> getAllPrograms() async {
|
||||
final database = await _db.database;
|
||||
final maps = await database.query('programs', orderBy: 'created_at DESC');
|
||||
return maps.map((m) => Program.fromMap(m)).toList();
|
||||
}
|
||||
|
||||
/// 根据ID获取程序
|
||||
Future<Program?> getProgramById(int id) async {
|
||||
final database = await _db.database;
|
||||
final maps = await database.query(
|
||||
'programs',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
if (maps.isEmpty) return null;
|
||||
return Program.fromMap(maps.first);
|
||||
}
|
||||
|
||||
/// 新增程序
|
||||
Future<int> addProgram(Program program) async {
|
||||
final database = await _db.database;
|
||||
return await database.insert('programs', program.toMap());
|
||||
}
|
||||
|
||||
/// 更新程序
|
||||
Future<bool> updateProgram(Program program) async {
|
||||
if (program.id == null) return false;
|
||||
final database = await _db.database;
|
||||
final count = await database.update(
|
||||
'programs',
|
||||
program.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [program.id],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 删除程序(含步骤)
|
||||
Future<bool> deleteProgram(int id) async {
|
||||
final database = await _db.database;
|
||||
// 先删除关联的步骤
|
||||
await database.delete('steps', where: 'program_id = ?', whereArgs: [id]);
|
||||
// 再删除程序
|
||||
final count = await database.delete('programs', where: 'id = ?', whereArgs: [id]);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 批量删除程序
|
||||
Future<bool> deletePrograms(List<int> ids) async {
|
||||
if (ids.isEmpty) return true;
|
||||
final database = await _db.database;
|
||||
// 先删除关联的步骤
|
||||
await database.delete(
|
||||
'steps',
|
||||
where: 'program_id IN (${ids.map((_) => '?').join(',')})',
|
||||
whereArgs: ids,
|
||||
);
|
||||
// 再删除程序
|
||||
final count = await database.delete(
|
||||
'programs',
|
||||
where: 'id IN (${ids.map((_) => '?').join(',')})',
|
||||
whereArgs: ids,
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 切换程序状态
|
||||
Future<bool> toggleProgramStatus(int id) async {
|
||||
final database = await _db.database;
|
||||
final program = await getProgramById(id);
|
||||
if (program == null) return false;
|
||||
final count = await database.update(
|
||||
'programs',
|
||||
{'status': program.status == 1 ? 0 : 1},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 获取程序的步骤列表
|
||||
Future<List<Step>> getStepsByProgramId(int programId) async {
|
||||
final database = await _db.database;
|
||||
final maps = await database.query(
|
||||
'steps',
|
||||
where: 'program_id = ?',
|
||||
whereArgs: [programId],
|
||||
orderBy: 'step_no ASC',
|
||||
);
|
||||
return maps.map((m) => Step.fromMap(m)).toList();
|
||||
}
|
||||
|
||||
/// 新增步骤
|
||||
Future<int> addStep(Step step) async {
|
||||
final database = await _db.database;
|
||||
return await database.insert('steps', step.toMap());
|
||||
}
|
||||
|
||||
/// 更新步骤
|
||||
Future<bool> updateStep(Step step) async {
|
||||
if (step.id == null) return false;
|
||||
final database = await _db.database;
|
||||
final count = await database.update(
|
||||
'steps',
|
||||
step.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [step.id],
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 删除步骤
|
||||
Future<bool> deleteStep(int id) async {
|
||||
final database = await _db.database;
|
||||
final count = await database.delete('steps', where: 'id = ?', whereArgs: [id]);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 批量删除步骤
|
||||
Future<bool> deleteSteps(List<int> ids) async {
|
||||
if (ids.isEmpty) return true;
|
||||
final database = await _db.database;
|
||||
final count = await database.delete(
|
||||
'steps',
|
||||
where: 'id IN (${ids.map((_) => '?').join(',')})',
|
||||
whereArgs: ids,
|
||||
);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/// 更新步骤排序
|
||||
Future<void> reorderSteps(int programId, List<int> stepIds) async {
|
||||
final database = await _db.database;
|
||||
for (int i = 0; i < stepIds.length; i++) {
|
||||
await database.update(
|
||||
'steps',
|
||||
{'step_no': i + 1},
|
||||
where: 'id = ? AND program_id = ?',
|
||||
whereArgs: [stepIds[i], programId],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
188
lib/features/programs/widgets/program_form_dialog.dart
Normal file
188
lib/features/programs/widgets/program_form_dialog.dart
Normal file
@@ -0,0 +1,188 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/localization/app_localizations.dart';
|
||||
import '../../../core/theme/app_theme.dart';
|
||||
import '../../../shared/widgets/common_button.dart';
|
||||
import '../models/program.dart';
|
||||
import '../providers/programs_provider.dart';
|
||||
|
||||
/// 程序表单弹窗
|
||||
/// 用于新增和编辑程序
|
||||
class ProgramFormDialog extends ConsumerStatefulWidget {
|
||||
final Program? program;
|
||||
|
||||
const ProgramFormDialog({super.key, this.program});
|
||||
|
||||
@override
|
||||
ConsumerState<ProgramFormDialog> createState() => _ProgramFormDialogState();
|
||||
}
|
||||
|
||||
class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late TextEditingController _codeController;
|
||||
late TextEditingController _nameController;
|
||||
bool _isEnabled = true;
|
||||
bool _isSaving = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_codeController = TextEditingController(text: widget.program?.code ?? '');
|
||||
_nameController = TextEditingController(text: widget.program?.name ?? '');
|
||||
_isEnabled = widget.program?.status == 1;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_codeController.dispose();
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final isEditing = widget.program != null;
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
isEditing
|
||||
? (l10n?.editProgram ?? '编辑程序')
|
||||
: (l10n?.addProgram ?? '新增程序'),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// 编号输入
|
||||
TextFormField(
|
||||
controller: _codeController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n?.programCode ?? '编号',
|
||||
hintText: '例如: P001',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入编号';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 名称输入
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n?.programName ?? '名称',
|
||||
hintText: '请输入程序名称',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return '请输入名称';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 状态开关
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'状态',
|
||||
style: TextStyle(color: AppTheme.textPrimary),
|
||||
),
|
||||
const Spacer(),
|
||||
Switch(
|
||||
value: _isEnabled,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isEnabled = value;
|
||||
});
|
||||
},
|
||||
activeColor: AppTheme.successColor,
|
||||
),
|
||||
Text(
|
||||
_isEnabled ? '启用' : '停用',
|
||||
style: TextStyle(
|
||||
color: _isEnabled ? AppTheme.successColor : AppTheme.idleColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isSaving ? null : () => Navigator.of(context).pop(),
|
||||
child: Text(l10n?.cancel ?? '取消'),
|
||||
),
|
||||
CommonButton(
|
||||
text: l10n?.save ?? '保存',
|
||||
icon: Icons.save,
|
||||
type: ButtonType.primary,
|
||||
isLoading: _isSaving,
|
||||
onPressed: _isSaving ? null : () => _saveProgram(context, ref, l10n),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 保存程序
|
||||
Future<void> _saveProgram(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
AppLocalizations? l10n,
|
||||
) async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isSaving = true;
|
||||
});
|
||||
|
||||
final notifier = ref.read(programsProvider.notifier);
|
||||
final now = DateTime.now().toString().substring(0, 10);
|
||||
|
||||
final program = Program(
|
||||
id: widget.program?.id,
|
||||
code: _codeController.text.trim(),
|
||||
name: _nameController.text.trim(),
|
||||
createdAt: widget.program?.createdAt ?? now,
|
||||
status: _isEnabled ? 1 : 0,
|
||||
);
|
||||
|
||||
bool success;
|
||||
if (widget.program != null) {
|
||||
success = await notifier.updateProgram(program);
|
||||
} else {
|
||||
success = await notifier.addProgram(program);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isSaving = false;
|
||||
});
|
||||
|
||||
if (success) {
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('保存失败,请检查编号是否重复'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
382
lib/features/settings/pages/settings_page.dart
Normal file
382
lib/features/settings/pages/settings_page.dart
Normal 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('重新检测'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
63
lib/features/settings/services/settings_service.dart
Normal file
63
lib/features/settings/services/settings_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
99
lib/features/settings/services/usb_detection_service.dart
Normal file
99
lib/features/settings/services/usb_detection_service.dart
Normal 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
61
lib/main.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/shared/services/toast_service.dart
Normal file
82
lib/shared/services/toast_service.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
24
lib/shared/utils/constants.dart
Normal file
24
lib/shared/utils/constants.dart
Normal 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';
|
||||
}
|
||||
49
lib/shared/utils/responsive_layout.dart
Normal file
49
lib/shared/utils/responsive_layout.dart
Normal 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);
|
||||
}
|
||||
103
lib/shared/widgets/common_button.dart
Normal file
103
lib/shared/widgets/common_button.dart
Normal 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 }
|
||||
47
lib/shared/widgets/common_card.dart
Normal file
47
lib/shared/widgets/common_card.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
93
lib/shared/widgets/common_dialog.dart
Normal file
93
lib/shared/widgets/common_dialog.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
55
lib/shared/widgets/empty_state_widget.dart
Normal file
55
lib/shared/widgets/empty_state_widget.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/shared/widgets/status_indicator.dart
Normal file
59
lib/shared/widgets/status_indicator.dart
Normal 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 }
|
||||
Reference in New Issue
Block a user