diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index e4992c3..dabb396 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/下位机交互数据模型.md b/docs/下位机交互数据模型.md
new file mode 100644
index 0000000..0323cbf
--- /dev/null
+++ b/docs/下位机交互数据模型.md
@@ -0,0 +1,178 @@
+---
+title: 默认模块
+language_tabs:
+ - shell: Shell
+ - http: HTTP
+ - javascript: JavaScript
+ - ruby: Ruby
+ - python: Python
+ - php: PHP
+ - java: Java
+ - go: Go
+toc_footers: []
+includes: []
+search: true
+code_clipboard: true
+highlight_theme: darkula
+headingLevel: 2
+generator: "@tarslib/widdershins v4.0.30"
+
+---
+
+# 默认模块
+
+Base URLs:
+
+# Authentication
+
+# 数据模型
+
+
设备基本信息
+
+
+
+
+
+
+```json
+{
+ "message_id": "string",
+ "type": "string",
+ "ack": "string",
+ "need_ack": true,
+ "data": {
+ "door_status": "string",
+ "task_status": "string",
+ "light_status": "string"
+ }
+}
+
+```
+
+### 属性
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|message_id|string|true|none||none|
+|type|string|true|none|类型|device_info|
+|ack|string|true|none||none|
+|need_ack|boolean|true|none||none|
+|data|object|true|none||none|
+|» door_status|string|true|none|门状态|open=开,close=关|
+|» task_status|string|true|none|任务运行状态|running=运行中,pause=暂停,idle=空闲|
+|» light_status|string|true|none|灯状态|on=开,off= 关|
+
+下发任务
+
+
+
+
+
+
+```json
+{
+ "message_id": "string",
+ "type": "string",
+ "ack": "string",
+ "need_ack": true,
+ "data": {
+ "steps": [
+ {
+ "no": 0,
+ "slot": 0,
+ "name": "string",
+ "mixtime": 0,
+ "pulltime": 0,
+ "volume": 0,
+ "speed": 0
+ }
+ ],
+ "temperature": 0,
+ "airflowtime": 0
+ }
+}
+
+```
+
+### 属性
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|message_id|string|true|none||由uuid生成唯一识别码|
+|type|string|true|none|指令类型|create_task|
+|ack|string|true|none||需要响应的消息id|
+|need_ack|boolean|true|none|是否需要响应|true|
+|data|object|true|none||none|
+|» steps|[object]|true|none|步骤列表|none|
+|»» no|integer|true|none|步骤号|none|
+|»» slot|integer|true|none|槽位号|none|
+|»» name|string|true|none|步骤名称|none|
+|»» mixtime|integer|true|none|搅拌时间|单位:秒|
+|»» pulltime|integer|true|none|吸磁时间|单位:秒|
+|»» volume|integer|true|none|容积|范围 0-2000|
+|»» speed|integer|true|none|速度等级|范围:1-10|
+|» temperature|integer|true|none|加热温度|none|
+|» airflowtime|integer|true|none|吹气时间|单位:秒|
+
+灯光控制
+
+
+
+
+
+
+```json
+{
+ "message_id": "string",
+ "type": "string",
+ "ack": " ",
+ "need_ack": true,
+ "data": {
+ "status": "string"
+ }
+}
+
+```
+
+### 属性
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|message_id|string|true|none||none|
+|type|string|true|none|类型|none|
+|ack|string|true|none||none|
+|need_ack|boolean|true|none||none|
+|data|object|true|none||none|
+|» status|string|true|none|开光|on / off|
+
+任务控制
+
+
+
+
+
+
+```json
+{
+ "message_id": "string",
+ "type": "string",
+ "ack": "string",
+ "need_ack": true,
+ "data": {
+ "status": "string"
+ }
+}
+
+```
+
+### 属性
+
+|名称|类型|必选|约束|中文名|说明|
+|---|---|---|---|---|---|
+|message_id|string|true|none||none|
+|type|string|true|none|类型|control|
+|ack|string¦null|true|none||none|
+|need_ack|boolean|true|none||none|
+|data|object|true|none||none|
+|» status|string|true|none|状态|continue=继续,stop=停止,暂停=pause|
+
diff --git a/lib/core/database/database_service.dart b/lib/core/database/database_service.dart
index b27f7b1..7b7f67f 100644
--- a/lib/core/database/database_service.dart
+++ b/lib/core/database/database_service.dart
@@ -20,7 +20,7 @@ class DatabaseService {
return await openDatabase(
path,
- version: 2,
+ version: 3,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
@@ -34,7 +34,9 @@ class DatabaseService {
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
- status INTEGER DEFAULT 1
+ status INTEGER DEFAULT 1,
+ temperature INTEGER DEFAULT 50,
+ airflow_time INTEGER DEFAULT 60
)
''');
@@ -49,10 +51,8 @@ class DatabaseService {
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,
+ speed INTEGER DEFAULT 5,
FOREIGN KEY (program_id) REFERENCES programs(id) ON DELETE CASCADE
)
''');
@@ -82,6 +82,15 @@ class DatabaseService {
// 初始化默认密码
await db.insert('settings', {'key': 'password', 'value': '123456'});
}
+ if (oldVersion < 3) {
+ // v3: 重构字段
+ // programs 增加 temperature, airflow_time
+ await db.execute('ALTER TABLE programs ADD COLUMN temperature INTEGER DEFAULT 50');
+ await db.execute('ALTER TABLE programs ADD COLUMN airflow_time INTEGER DEFAULT 60');
+ // steps 删除 mix_speed, blow_speed, needle_speed;增加 speed
+ // sqflite 不支持 DROP COLUMN,旧字段保留但不再使用
+ await db.execute('ALTER TABLE steps ADD COLUMN speed INTEGER DEFAULT 5');
+ }
}
Future close() async {
@@ -91,6 +100,30 @@ class DatabaseService {
}
}
+ /// 通用 KV 读:读取 settings 表中 [key] 对应的 value;不存在返回 null
+ Future readSetting(String key) async {
+ final db = await database;
+ final rows = await db.query(
+ 'settings',
+ columns: ['value'],
+ where: 'key = ?',
+ whereArgs: [key],
+ limit: 1,
+ );
+ if (rows.isEmpty) return null;
+ return rows.first['value'] as String?;
+ }
+
+ /// 通用 KV 写:以 INSERT OR REPLACE 写入 settings 表
+ Future writeSetting(String key, String value) async {
+ final db = await database;
+ await db.insert(
+ 'settings',
+ {'key': key, 'value': value},
+ conflictAlgorithm: ConflictAlgorithm.replace,
+ );
+ }
+
/// 初始化测试数据(仅调试模式使用)
Future initTestData() async {
final db = await database;
@@ -103,11 +136,11 @@ class DatabaseService {
// 插入测试程序并添加步骤
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},
+ {'code': 'P001', 'name': '标准检测程序', 'created_at': '2026-05-19', 'status': 1, 'temperature': 50, 'airflow_time': 60},
+ {'code': 'P002', 'name': '快速检测程序', 'created_at': '2026-05-18', 'status': 1, 'temperature': 45, 'airflow_time': 30},
+ {'code': 'P003', 'name': '深度检测程序', 'created_at': '2026-05-17', 'status': 1, 'temperature': 60, 'airflow_time': 90},
+ {'code': 'P004', 'name': '样本预处理程序', 'created_at': '2026-05-16', 'status': 0, 'temperature': 50, 'airflow_time': 60},
+ {'code': 'P005', 'name': '磁珠分离程序', 'created_at': '2026-05-15', 'status': 1, 'temperature': 55, 'airflow_time': 60},
];
for (final program in testPrograms) {
@@ -123,10 +156,8 @@ class DatabaseService {
'mix_time': 60,
'magnet_time': 0,
'volume': 100,
- 'mix_speed': '中速',
- 'blow_speed': '中速',
'blow_time': 0,
- 'needle_speed': 5,
+ 'speed': 5,
},
{
'program_id': programId,
@@ -136,10 +167,8 @@ class DatabaseService {
'mix_time': 0,
'magnet_time': 30,
'volume': 0,
- 'mix_speed': '中速',
- 'blow_speed': '中速',
'blow_time': 0,
- 'needle_speed': 5,
+ 'speed': 5,
},
{
'program_id': programId,
@@ -149,10 +178,8 @@ class DatabaseService {
'mix_time': 0,
'magnet_time': 0,
'volume': 0,
- 'mix_speed': '中速',
- 'blow_speed': '高速',
'blow_time': 10,
- 'needle_speed': 8,
+ 'speed': 8,
},
];
diff --git a/lib/core/localization/app_localizations.dart b/lib/core/localization/app_localizations.dart
index 578b0ac..fa46ea6 100644
--- a/lib/core/localization/app_localizations.dart
+++ b/lib/core/localization/app_localizations.dart
@@ -18,6 +18,7 @@ class AppLocalizations {
String get running => _localizedValues[locale.languageCode]?['running'] ?? '运行中';
String get idle => _localizedValues[locale.languageCode]?['idle'] ?? '未运行';
String get lighting => _localizedValues[locale.languageCode]?['lighting'] ?? '照明';
+ String get deviceControl => _localizedValues[locale.languageCode]?['deviceControl'] ?? '设备控制';
// 程序管理
String get programs => _localizedValues[locale.languageCode]?['programs'] ?? '程序管理';
@@ -33,13 +34,13 @@ class AppLocalizations {
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 speed => _localizedValues[locale.languageCode]?['speed'] ?? '速度';
+ String get speedLevel => _localizedValues[locale.languageCode]?['speedLevel'] ?? '档';
String get temperature => _localizedValues[locale.languageCode]?['temperature'] ?? '温度';
+ String get airflowTime => _localizedValues[locale.languageCode]?['airflowTime'] ?? '吹气时间';
String get duration => _localizedValues[locale.languageCode]?['duration'] ?? '持续时间';
String get sampleVolume => _localizedValues[locale.languageCode]?['sampleVolume'] ?? '样品体积';
String get pleaseSelectProgram => _localizedValues[locale.languageCode]?['pleaseSelectProgram'] ?? '请选择要运行的程序';
@@ -54,6 +55,7 @@ class AppLocalizations {
String get remainingTime => _localizedValues[locale.languageCode]?['remainingTime'] ?? '剩余时间';
String get progress => _localizedValues[locale.languageCode]?['progress'] ?? '进度';
String get ceramicSleeveConfirm => _localizedValues[locale.languageCode]?['ceramicSleeveConfirm'] ?? '运行前请确认已安装瓷套棒';
+ String get ceramicSleeveConfirmMessage => _localizedValues[locale.languageCode]?['ceramicSleeveConfirmMessage'] ?? '请确认已放置瓷套棒后再启动程序。';
String get paused => _localizedValues[locale.languageCode]?['paused'] ?? '已暂停';
String get stopConfirm => _localizedValues[locale.languageCode]?['stopConfirm'] ?? '确定要停止当前运行的程序吗?';
String get currentProgram => _localizedValues[locale.languageCode]?['currentProgram'] ?? '当前程序';
@@ -68,15 +70,7 @@ class AppLocalizations {
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'] ?? '系统设置';
@@ -140,6 +134,7 @@ class AppLocalizations {
'running': '运行中',
'idle': '未运行',
'lighting': '照明',
+ 'deviceControl': '设备控制',
'programs': '程序管理',
'programList': '程序列表',
'programName': '程序名称',
@@ -153,13 +148,13 @@ class AppLocalizations {
'selectedProgram': '当前选中程序',
'selectedProgramLabel': '当前选中',
'availablePrograms': '可用程序',
- 'ceramicNotInstalled': '瓷套棒: 未安装 — 禁止启动',
- 'ceramicInstalled': '瓷套棒: 已安装',
'runningMonitor': '运行状态监控',
'currentHole': '当前孔位',
'stepParams': '步骤参数',
- 'speed': '转速',
+ 'speed': '速度',
+ 'speedLevel': '档',
'temperature': '温度',
+ 'airflowTime': '吹气时间',
'duration': '持续时间',
'sampleVolume': '样品体积',
'pleaseSelectProgram': '请选择要运行的程序',
@@ -172,6 +167,7 @@ class AppLocalizations {
'remainingTime': '剩余时间',
'progress': '进度',
'ceramicSleeveConfirm': '运行前请确认已安装瓷套棒',
+ 'ceramicSleeveConfirmMessage': '请确认已放置瓷套棒后再启动程序。',
'paused': '已暂停',
'stopConfirm': '确定要停止当前运行的程序吗?',
'currentProgram': '当前程序',
@@ -184,13 +180,7 @@ class AppLocalizations {
'mixTime': '混合时间',
'magnetTime': '吸磁时间',
'volume': '容积',
- 'mixSpeed': '混合速度',
- 'blowSpeed': '吹气速度',
'blowTime': '吹气时间',
- 'needleSpeed': '下针速度',
- 'lowSpeed': '低速',
- 'mediumSpeed': '中速',
- 'highSpeed': '高速',
'settings': '系统设置',
'language': '语言设置',
'password': '密码修改',
@@ -245,6 +235,7 @@ class AppLocalizations {
'running': 'Running',
'idle': 'Idle',
'lighting': 'Lighting',
+ 'deviceControl': 'Device Control',
'programs': 'Programs',
'programList': 'Program List',
'programName': 'Program Name',
@@ -258,13 +249,13 @@ class AppLocalizations {
'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',
+ 'speedLevel': 'level',
'temperature': 'Temperature',
+ 'airflowTime': 'Airflow Time',
'duration': 'Duration',
'sampleVolume': 'Sample Volume',
'pleaseSelectProgram': 'Please select a program',
@@ -277,6 +268,7 @@ class AppLocalizations {
'remainingTime': 'Remaining',
'progress': 'Progress',
'ceramicSleeveConfirm': 'Please confirm ceramic sleeve is installed',
+ 'ceramicSleeveConfirmMessage': 'Please make sure the ceramic sleeve is in place before starting the program.',
'paused': 'Paused',
'stopConfirm': 'Are you sure to stop the running program?',
'currentProgram': 'Current Program',
@@ -289,13 +281,7 @@ class AppLocalizations {
'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',
diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart
index 9e67e0a..a4b6009 100644
--- a/lib/core/router/app_router.dart
+++ b/lib/core/router/app_router.dart
@@ -15,7 +15,12 @@ final goRouterProvider = Provider((ref) {
GoRoute(
path: '/',
name: 'home',
- builder: (context, state) => const HomePage(),
+ builder: (context, state) {
+ // 支持 ?tab=N 查询参数,用于从其他页面跳回首页并切换到指定 tab
+ final tabParam = state.uri.queryParameters['tab'];
+ final initialTab = int.tryParse(tabParam ?? '') ?? 0;
+ return HomePage(initialTab: initialTab);
+ },
),
GoRoute(
path: '/programs',
diff --git a/lib/features/device/models/serial_config.dart b/lib/features/device/models/serial_config.dart
new file mode 100644
index 0000000..a58dfbf
--- /dev/null
+++ b/lib/features/device/models/serial_config.dart
@@ -0,0 +1,126 @@
+import 'dart:convert';
+
+/// 串口奇偶校验
+enum SerialParity { none, odd, even, mark, space }
+
+/// 串口流控
+enum SerialFlowControl { none, rtsCts, xonXoff, dtrDsr }
+
+/// 串口配置
+///
+/// 持久化到 settings 表的 `serial_config` key 中;
+/// 打开串口时根据此配置构造底层 `UsbConfig`。
+class SerialConfig {
+ /// 设备 Vendor ID(十六进制)
+ final int vendorId;
+
+ /// 设备 Product ID(十六进制),0 表示不指定
+ final int productId;
+
+ /// 波特率
+ final int baudRate;
+
+ /// 数据位 (5/6/7/8)
+ final int dataBits;
+
+ /// 停止位 (1/2)
+ final int stopBits;
+
+ /// 校验位
+ final SerialParity parity;
+
+ /// 流控
+ final SerialFlowControl flowControl;
+
+ /// 读超时(毫秒)
+ final int readTimeoutMs;
+
+ /// 写超时(毫秒)
+ final int writeTimeoutMs;
+
+ const SerialConfig({
+ this.vendorId = 0x1A86,
+ this.productId = 0x7523,
+ this.baudRate = 9600,
+ this.dataBits = 8,
+ this.stopBits = 1,
+ this.parity = SerialParity.none,
+ this.flowControl = SerialFlowControl.none,
+ this.readTimeoutMs = 2000,
+ this.writeTimeoutMs = 2000,
+ });
+
+ /// 默认配置
+ static const SerialConfig defaults = SerialConfig();
+
+ /// 常用波特率
+ static const List commonBaudRates = [
+ 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800,
+ ];
+
+ SerialConfig copyWith({
+ int? vendorId,
+ int? productId,
+ int? baudRate,
+ int? dataBits,
+ int? stopBits,
+ SerialParity? parity,
+ SerialFlowControl? flowControl,
+ int? readTimeoutMs,
+ int? writeTimeoutMs,
+ }) {
+ return SerialConfig(
+ vendorId: vendorId ?? this.vendorId,
+ productId: productId ?? this.productId,
+ baudRate: baudRate ?? this.baudRate,
+ dataBits: dataBits ?? this.dataBits,
+ stopBits: stopBits ?? this.stopBits,
+ parity: parity ?? this.parity,
+ flowControl: flowControl ?? this.flowControl,
+ readTimeoutMs: readTimeoutMs ?? this.readTimeoutMs,
+ writeTimeoutMs: writeTimeoutMs ?? this.writeTimeoutMs,
+ );
+ }
+
+ /// 编码为 JSON 字符串(用于持久化)
+ String toJsonString() => jsonEncode({
+ 'vendorId': vendorId,
+ 'productId': productId,
+ 'baudRate': baudRate,
+ 'dataBits': dataBits,
+ 'stopBits': stopBits,
+ 'parity': parity.name,
+ 'flowControl': flowControl.name,
+ 'readTimeoutMs': readTimeoutMs,
+ 'writeTimeoutMs': writeTimeoutMs,
+ });
+
+ /// 从 JSON 字符串解码;解析失败时返回默认值
+ factory SerialConfig.fromJsonString(String? raw) {
+ if (raw == null || raw.isEmpty) return defaults;
+ try {
+ final map = jsonDecode(raw) as Map;
+ return SerialConfig(
+ vendorId: (map['vendorId'] as num?)?.toInt() ?? defaults.vendorId,
+ productId: (map['productId'] as num?)?.toInt() ?? defaults.productId,
+ baudRate: (map['baudRate'] as num?)?.toInt() ?? defaults.baudRate,
+ dataBits: (map['dataBits'] as num?)?.toInt() ?? defaults.dataBits,
+ stopBits: (map['stopBits'] as num?)?.toInt() ?? defaults.stopBits,
+ parity: SerialParity.values.firstWhere(
+ (e) => e.name == map['parity'],
+ orElse: () => defaults.parity,
+ ),
+ flowControl: SerialFlowControl.values.firstWhere(
+ (e) => e.name == map['flowControl'],
+ orElse: () => defaults.flowControl,
+ ),
+ readTimeoutMs:
+ (map['readTimeoutMs'] as num?)?.toInt() ?? defaults.readTimeoutMs,
+ writeTimeoutMs:
+ (map['writeTimeoutMs'] as num?)?.toInt() ?? defaults.writeTimeoutMs,
+ );
+ } catch (_) {
+ return defaults;
+ }
+ }
+}
diff --git a/lib/features/device/providers/run_state_provider.dart b/lib/features/device/providers/run_state_provider.dart
index f003e6f..b550e01 100644
--- a/lib/features/device/providers/run_state_provider.dart
+++ b/lib/features/device/providers/run_state_provider.dart
@@ -1,17 +1,18 @@
-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';
+import '../services/runner_interface.dart';
+import 'serial_provider.dart';
/// 运行状态枚举
enum RunStatus {
- idle, // 待机
- running, // 运行中
- paused, // 已暂停
- completed,// 已完成
- error, // 错误
+ idle, // 待机
+ running, // 运行中
+ paused, // 已暂停
+ completed, // 已完成
+ error, // 错误
}
/// 运行状态
@@ -23,6 +24,7 @@ class RunState {
final int remainingSeconds;
final double progress;
final String? currentWell;
+ final String? errorMessage;
const RunState({
this.status = RunStatus.idle,
@@ -32,6 +34,7 @@ class RunState {
this.remainingSeconds = 0,
this.progress = 0,
this.currentWell,
+ this.errorMessage,
});
RunState copyWith({
@@ -42,17 +45,21 @@ class RunState {
int? remainingSeconds,
double? progress,
String? currentWell,
+ String? errorMessage,
bool clearProgram = false,
bool clearWell = false,
+ bool clearError = false,
}) {
return RunState(
status: status ?? this.status,
- currentProgram: clearProgram ? null : (currentProgram ?? this.currentProgram),
+ 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),
+ errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
);
}
@@ -68,8 +75,8 @@ class RunState {
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')}';
+ '${minutes.toString().padLeft(2, '0')}:'
+ '${seconds.toString().padLeft(2, '0')}';
}
/// 格式化进度百分比
@@ -80,18 +87,24 @@ class RunState {
/// 运行状态 Notifier
class RunStateNotifier extends StateNotifier {
- final MockRunner _runner;
+ final Runner _runner;
final ProgramService _programService;
RunStateNotifier(this._runner, this._programService) : super(const RunState());
/// 开始运行程序
Future start(Program program) async {
- // 获取程序步骤(这里使用模拟数据,实际应从数据库读取)
- final steps = await _loadSteps(program.id!);
+ if (state.status == RunStatus.running ||
+ state.status == RunStatus.paused) {
+ return;
+ }
+ final steps = await _programService.getStepsByProgramId(program.id!);
if (steps.isEmpty) {
- state = state.copyWith(status: RunStatus.error);
+ state = state.copyWith(
+ status: RunStatus.error,
+ errorMessage: '程序步骤为空',
+ );
return;
}
@@ -101,26 +114,36 @@ class RunStateNotifier extends StateNotifier {
steps: steps,
currentStepIndex: 0,
progress: 0,
+ currentWell: steps.first.position,
+ clearError: true,
);
_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,
- );
- },
+ RunnerCallbacks(
+ onProgress: (stepIndex, remaining, progress, well) {
+ state = state.copyWith(
+ currentStepIndex: stepIndex,
+ remainingSeconds: remaining,
+ progress: progress,
+ currentWell: well,
+ );
+ },
+ onComplete: () {
+ state = state.copyWith(
+ status: RunStatus.completed,
+ progress: 1,
+ currentWell: steps.last.position,
+ );
+ },
+ onError: (msg) {
+ state = state.copyWith(
+ status: RunStatus.error,
+ errorMessage: msg,
+ );
+ },
+ ),
);
}
@@ -142,26 +165,17 @@ class RunStateNotifier extends StateNotifier {
/// 停止运行
void stop() {
- _runner.stop();
+ if (state.status == RunStatus.running ||
+ state.status == RunStatus.paused) {
+ _runner.stop();
+ }
state = const RunState(status: RunStatus.idle);
}
/// 重置状态
- void reset() {
- stop();
- }
-
- /// 加载程序步骤(从数据库读取)
- Future> _loadSteps(int programId) async {
- return await _programService.getStepsByProgramId(programId);
- }
+ void reset() => stop();
}
-/// MockRunner Provider
-final mockRunnerProvider = Provider((ref) {
- return MockRunner();
-});
-
/// ProgramService Provider
final programServiceProvider = Provider((ref) {
return ProgramService.instance;
@@ -170,7 +184,7 @@ final programServiceProvider = Provider((ref) {
/// 运行状态 Provider
final runStateProvider =
StateNotifierProvider((ref) {
- final runner = ref.watch(mockRunnerProvider);
+ final runner = ref.watch(runnerProvider);
final programService = ref.watch(programServiceProvider);
return RunStateNotifier(runner, programService);
});
@@ -185,4 +199,4 @@ final isRunningProvider = Provider((ref) {
final isPausedProvider = Provider((ref) {
final status = ref.watch(runStateProvider).status;
return status == RunStatus.paused;
-});
\ No newline at end of file
+});
diff --git a/lib/features/device/services/mock_runner.dart b/lib/features/device/services/mock_runner.dart
deleted file mode 100644
index 27134a8..0000000
--- a/lib/features/device/services/mock_runner.dart
+++ /dev/null
@@ -1,190 +0,0 @@
-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 _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 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;
- }
-}
\ No newline at end of file
diff --git a/lib/features/device/services/mock_runner_impl.dart b/lib/features/device/services/mock_runner_impl.dart
deleted file mode 100644
index c807b07..0000000
--- a/lib/features/device/services/mock_runner_impl.dart
+++ /dev/null
@@ -1,114 +0,0 @@
-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 _steps = [];
-
- @override
- void start(Program program, List 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();
- }
-}
\ No newline at end of file
diff --git a/lib/features/home/pages/home_page.dart b/lib/features/home/pages/home_page.dart
index 82858b2..4ea26c2 100644
--- a/lib/features/home/pages/home_page.dart
+++ b/lib/features/home/pages/home_page.dart
@@ -2,6 +2,7 @@ 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/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../device/providers/run_state_provider.dart';
import '../../programs/pages/programs_page.dart';
@@ -12,9 +13,13 @@ import '../widgets/running_control_panel.dart';
import '../widgets/run_status_monitor.dart';
/// 首页 - 设备控制面板 (暗色工业风格)
-/// 布局:状态栏 + 导航标签栏 + 内容区(设备控制/程序管理/系统设置)
+/// 布局:状态栏(含标签导航) + 内容区(设备控制/程序管理/系统设置)
+///
+/// [initialTab] 用于从子页面(如程序详情页)跳转回首页时指定要显示的 tab
class HomePage extends ConsumerStatefulWidget {
- const HomePage({super.key});
+ final int initialTab;
+
+ const HomePage({super.key, this.initialTab = 0});
@override
ConsumerState createState() => _HomePageState();
@@ -22,17 +27,19 @@ class HomePage extends ConsumerStatefulWidget {
class _HomePageState extends ConsumerState
with SingleTickerProviderStateMixin {
- int _currentIndex = 0;
+ late int _currentIndex;
@override
void initState() {
super.initState();
+ _currentIndex = widget.initialTab.clamp(0, 2);
DatabaseService.instance.initTestData();
}
@override
Widget build(BuildContext context) {
final runState = ref.watch(runStateProvider);
+ final l10n = AppLocalizations.of(context);
// 监听运行完成状态,自动跳转
ref.listen(runStateProvider, (prev, next) {
@@ -44,19 +51,25 @@ class _HomePageState extends ConsumerState
}
});
+ final tabs = [
+ StatusBarTab(icon: Icons.dashboard, label: l10n?.deviceControl ?? '设备控制'),
+ StatusBarTab(icon: Icons.list_alt, label: l10n?.programs ?? '程序管理'),
+ StatusBarTab(icon: Icons.settings, label: l10n?.settings ?? '系统设置'),
+ ];
+
return Scaffold(
body: Container(
color: AppTheme.bgDeep,
child: Column(
children: [
- // 状态栏
+ // 状态栏(内嵌标签导航)
StatusBar(
isRunning: runState.status == RunStatus.running,
+ tabs: tabs,
+ currentTabIndex: _currentIndex,
+ onTabChanged: (index) => setState(() => _currentIndex = index),
),
- // 导航标签栏
- _buildTabBar(),
-
// 内容区
Expanded(
child: IndexedStack(
@@ -74,69 +87,10 @@ class _HomePageState extends ConsumerState
);
}
- /// 导航标签栏
- 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),
+ padding: const EdgeInsets.all(20),
child: Row(
children: [
// 左侧:程序列表(运行时锁定)
diff --git a/lib/features/home/widgets/run_status_monitor.dart b/lib/features/home/widgets/run_status_monitor.dart
index 09b82c7..16ff1e2 100644
--- a/lib/features/home/widgets/run_status_monitor.dart
+++ b/lib/features/home/widgets/run_status_monitor.dart
@@ -194,7 +194,7 @@ class RunStatusMonitor extends ConsumerWidget {
if (step.mixTime > 0)
_buildParamRow(
l10n?.speed ?? '转速',
- '${step.mixSpeed}',
+ '${step.speed} 档',
),
if (step.magnetTime > 0)
_buildParamRow(
diff --git a/lib/features/home/widgets/running_control_panel.dart b/lib/features/home/widgets/running_control_panel.dart
index 03b479d..5594189 100644
--- a/lib/features/home/widgets/running_control_panel.dart
+++ b/lib/features/home/widgets/running_control_panel.dart
@@ -4,10 +4,11 @@ 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/models/program.dart';
import '../../programs/providers/programs_provider.dart';
/// 运行控制面板 - 暗色工业风格
-/// 显示当前程序信息、瓷套棒状态和运行控制按钮
+/// 显示当前程序信息和运行控制按钮
class RunningControlPanel extends ConsumerWidget {
const RunningControlPanel({super.key});
@@ -96,40 +97,6 @@ class RunningControlPanel extends ConsumerWidget {
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: [
@@ -144,7 +111,7 @@ class RunningControlPanel extends ConsumerWidget {
type: ButtonType.primary,
enabled: selectedProgram != null,
onPressed: selectedProgram != null
- ? () => runNotifier.start(selectedProgram)
+ ? () => _confirmAndStart(context, runNotifier, selectedProgram, l10n)
: null,
),
),
@@ -321,6 +288,49 @@ class RunningControlPanel extends ConsumerWidget {
);
}
+ /// 显示瓷套棒放置确认对话框,确认后启动程序
+ void _confirmAndStart(
+ BuildContext context,
+ RunStateNotifier runNotifier,
+ Program program,
+ AppLocalizations? l10n,
+ ) {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ backgroundColor: AppTheme.cardBg,
+ title: Text(
+ l10n?.ceramicSleeveConfirm ?? '运行前请确认已安装瓷套棒',
+ style: const TextStyle(color: AppTheme.textHeading),
+ ),
+ content: Text(
+ l10n?.ceramicSleeveConfirmMessage ?? '请确认已放置瓷套棒后再启动程序。',
+ 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.accentPrimary,
+ foregroundColor: Colors.white,
+ ),
+ onPressed: () {
+ Navigator.of(context).pop();
+ runNotifier.start(program);
+ },
+ child: Text(l10n?.confirm ?? '确认'),
+ ),
+ ],
+ ),
+ );
+ }
+
/// 显示停止确认对话框
void _showStopConfirm(
BuildContext context,
diff --git a/lib/features/home/widgets/status_bar.dart b/lib/features/home/widgets/status_bar.dart
index 62a411d..9c32911 100644
--- a/lib/features/home/widgets/status_bar.dart
+++ b/lib/features/home/widgets/status_bar.dart
@@ -6,16 +6,32 @@ import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/status_indicator.dart';
import '../../device/providers/device_info_provider.dart';
+/// 状态栏标签项数据
+class StatusBarTab {
+ final IconData icon;
+ final String label;
+ const StatusBarTab({required this.icon, required this.label});
+}
+
/// 状态栏组件 - 明亮工业风格
-/// 显示设备名称、实时时钟、系统状态、照明控制
+/// 显示: 设备名称 | 导航标签 | 灯按钮 | 状态指示器 | 实时时钟
+///
+/// [tabs] 不为空时,会在设备名右侧渲染胶囊样式的导航标签按钮组,
+/// 由父组件通过 [currentTabIndex] 和 [onTabChanged] 控制切换。
class StatusBar extends ConsumerStatefulWidget {
final bool isRunning;
final VoidCallback? onLightToggle;
+ final List tabs;
+ final int currentTabIndex;
+ final ValueChanged? onTabChanged;
const StatusBar({
super.key,
this.isRunning = false,
this.onLightToggle,
+ this.tabs = const [],
+ this.currentTabIndex = 0,
+ this.onTabChanged,
});
@override
@@ -39,6 +55,7 @@ class _StatusBarState extends ConsumerState {
super.dispose();
}
+ /// 更新当前时间显示
void _updateTime() {
final now = DateTime.now();
_currentTime =
@@ -49,6 +66,7 @@ class _StatusBarState extends ConsumerState {
String _twoDigits(int n) => n.toString().padLeft(2, '0');
+ /// 切换照明开关
Future _onLightTap() async {
widget.onLightToggle?.call();
await ref.read(deviceInfoProvider.notifier).toggleLight();
@@ -77,7 +95,7 @@ class _StatusBarState extends ConsumerState {
Row(
mainAxisSize: MainAxisSize.min,
children: [
- Icon(Icons.precision_manufacturing, color: Colors.white, size: 22),
+ const Icon(Icons.precision_manufacturing, color: Colors.white, size: 22),
const SizedBox(width: 10),
Text(
l10n?.deviceName ?? '污水毒品前处理一体机',
@@ -89,6 +107,10 @@ class _StatusBarState extends ConsumerState {
),
],
),
+ if (widget.tabs.isNotEmpty) ...[
+ const SizedBox(width: 32),
+ _buildNavTabs(),
+ ],
const Spacer(),
_LightToggleButton(
isOn: deviceInfo.lightingOn,
@@ -117,6 +139,92 @@ class _StatusBarState extends ConsumerState {
),
);
}
+
+ /// 构建标题栏内嵌的导航标签按钮组(胶囊样式)
+ Widget _buildNavTabs() {
+ return Row(
+ mainAxisSize: MainAxisSize.min,
+ children: List.generate(widget.tabs.length, (index) {
+ final tab = widget.tabs[index];
+ return Padding(
+ padding: EdgeInsets.only(right: index == widget.tabs.length - 1 ? 0 : 6),
+ child: _NavTabButton(
+ icon: tab.icon,
+ label: tab.label,
+ selected: index == widget.currentTabIndex,
+ onTap: () => widget.onTabChanged?.call(index),
+ ),
+ );
+ }),
+ );
+ }
+}
+
+/// 标题栏内嵌的导航标签按钮(胶囊样式)
+/// 选中时使用半透明白色背景突出,未选中时仅显示文字
+class _NavTabButton extends StatelessWidget {
+ final IconData icon;
+ final String label;
+ final bool selected;
+ final VoidCallback? onTap;
+
+ const _NavTabButton({
+ required this.icon,
+ required this.label,
+ required this.selected,
+ this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: onTap,
+ borderRadius: BorderRadius.circular(18),
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 180),
+ height: 36,
+ padding: const EdgeInsets.symmetric(horizontal: 14),
+ decoration: BoxDecoration(
+ color: selected
+ ? Colors.white.withValues(alpha: 0.22)
+ : Colors.transparent,
+ borderRadius: BorderRadius.circular(18),
+ border: Border.all(
+ color: selected
+ ? Colors.white.withValues(alpha: 0.35)
+ : Colors.transparent,
+ width: 1,
+ ),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ icon,
+ size: 16,
+ color: selected
+ ? Colors.white
+ : Colors.white.withValues(alpha: 0.78),
+ ),
+ const SizedBox(width: 6),
+ Text(
+ label,
+ style: TextStyle(
+ color: selected
+ ? Colors.white
+ : Colors.white.withValues(alpha: 0.85),
+ fontSize: 14,
+ fontWeight: selected ? FontWeight.w600 : FontWeight.w500,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
}
class _LightToggleButton extends StatelessWidget {
diff --git a/lib/features/program_detail/pages/program_detail_page.dart b/lib/features/program_detail/pages/program_detail_page.dart
index d0ade1c..0a4816e 100644
--- a/lib/features/program_detail/pages/program_detail_page.dart
+++ b/lib/features/program_detail/pages/program_detail_page.dart
@@ -7,10 +7,12 @@ import '../../../shared/widgets/common_button.dart';
import '../providers/steps_provider.dart';
import '../widgets/step_list.dart';
import '../widgets/step_form.dart';
+import '../../home/widgets/status_bar.dart';
+import '../../device/providers/run_state_provider.dart';
import '../../programs/providers/programs_provider.dart';
/// 程序详情页面
-/// 左侧步骤列表 + 右侧参数表单
+/// 布局:顶部状态栏(含导航tab) + 子工具栏(返回/程序名/保存) + 左侧步骤列表 + 右侧参数表单
class ProgramDetailPage extends ConsumerStatefulWidget {
final String programId;
@@ -35,61 +37,31 @@ class _ProgramDetailPageState extends ConsumerState {
final programsState = ref.watch(programsProvider);
final program = programsState.programs.where((p) => p.id == _programIdInt).firstOrNull;
final stepsState = ref.watch(stepsProvider(_programIdInt));
+ final runState = ref.watch(runStateProvider);
+
+ // 详情页从程序管理进入,高亮"程序管理"tab(index=1)
+ final tabs = [
+ StatusBarTab(icon: Icons.dashboard, label: l10n?.deviceControl ?? '设备控制'),
+ StatusBarTab(icon: Icons.list_alt, label: l10n?.programs ?? '程序管理'),
+ StatusBarTab(icon: Icons.settings, label: l10n?.settings ?? '系统设置'),
+ ];
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,
- ),
- );
- },
- ),
- ],
- ),
+ // 顶部状态栏(含导航tab),tab点击跳回首页对应 tab
+ StatusBar(
+ isRunning: runState.status == RunStatus.running,
+ tabs: tabs,
+ currentTabIndex: 1,
+ onTabChanged: (index) => context.go('/?tab=$index'),
),
+ // 子工具栏:返回按钮 + 程序名 + 保存按钮
+ _buildSubToolbar(context, l10n, program?.name),
+
// 主内容区域
Expanded(
child: stepsState.isLoading
@@ -172,6 +144,57 @@ class _ProgramDetailPageState extends ConsumerState {
);
}
+ /// 子工具栏:返回按钮 + 程序名 + 保存按钮
+ /// 位于状态栏下方,提供详情页特有的操作入口
+ Widget _buildSubToolbar(BuildContext context, AppLocalizations? l10n, String? programName) {
+ return Container(
+ height: 56,
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(alpha: 0.06),
+ blurRadius: 4,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ child: Row(
+ children: [
+ IconButton(
+ icon: const Icon(Icons.arrow_back),
+ tooltip: l10n?.backToHome ?? '返回',
+ onPressed: () => context.go('/?tab=1'),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ programName ?? (l10n?.detail ?? '程序详情'),
+ style: const TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ color: AppTheme.textHeading,
+ ),
+ ),
+ const Spacer(),
+ CommonButton(
+ text: l10n?.save ?? '保存',
+ icon: Icons.save,
+ type: ButtonType.primary,
+ onPressed: () {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text('已保存'),
+ backgroundColor: AppTheme.successColor,
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
/// 显示添加步骤对话框
void _showAddStepDialog(BuildContext context, WidgetRef ref) {
showDialog(
diff --git a/lib/features/program_detail/widgets/step_form.dart b/lib/features/program_detail/widgets/step_form.dart
index a66388f..de13caa 100644
--- a/lib/features/program_detail/widgets/step_form.dart
+++ b/lib/features/program_detail/widgets/step_form.dart
@@ -34,9 +34,7 @@ class _StepFormState extends State {
late TextEditingController _blowTimeController;
String _position = 'A1';
- String _mixSpeed = '中速';
- String _blowSpeed = '中速';
- int _needleSpeed = 5;
+ int _speed = 5;
@override
void initState() {
@@ -48,9 +46,7 @@ class _StepFormState extends State {
_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;
+ _speed = widget.step?.speed ?? 5;
}
@override
@@ -179,53 +175,22 @@ class _StepFormState extends State {
),
const SizedBox(height: 16),
- // 速度选择
- Row(
- children: [
- Expanded(
- child: DropdownButtonFormField(
- 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(
- 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)),
+ Text(
+ '${l10n?.speed ?? '速度'}: $_speed 档',
+ style: TextStyle(color: AppTheme.textPrimary),
+ ),
Slider(
- value: _needleSpeed.toDouble(),
+ value: _speed.toDouble(),
min: 1,
max: 10,
divisions: 9,
activeColor: AppTheme.primaryColor,
onChanged: (value) {
- setState(() => _needleSpeed = value.round());
+ setState(() => _speed = value.round());
},
),
],
@@ -259,10 +224,8 @@ class _StepFormState extends State {
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,
+ speed: _speed,
);
widget.onSave(step);
diff --git a/lib/features/programs/models/program.dart b/lib/features/programs/models/program.dart
index aaa967f..5d2f202 100644
--- a/lib/features/programs/models/program.dart
+++ b/lib/features/programs/models/program.dart
@@ -5,6 +5,8 @@ class Program {
final String name;
final String createdAt;
final int status; // 1: 启用, 0: 停用
+ final int temperature;
+ final int airflowTime;
Program({
this.id,
@@ -12,6 +14,8 @@ class Program {
required this.name,
required this.createdAt,
this.status = 1,
+ this.temperature = 50,
+ this.airflowTime = 60,
});
Map toMap() {
@@ -21,6 +25,8 @@ class Program {
'name': name,
'created_at': createdAt,
'status': status,
+ 'temperature': temperature,
+ 'airflow_time': airflowTime,
};
}
@@ -31,6 +37,8 @@ class Program {
name: map['name'] as String,
createdAt: map['created_at'] as String,
status: map['status'] as int? ?? 1,
+ temperature: map['temperature'] as int? ?? 50,
+ airflowTime: map['airflow_time'] as int? ?? 60,
);
}
@@ -40,6 +48,8 @@ class Program {
String? name,
String? createdAt,
int? status,
+ int? temperature,
+ int? airflowTime,
}) {
return Program(
id: id ?? this.id,
@@ -47,6 +57,8 @@ class Program {
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
status: status ?? this.status,
+ temperature: temperature ?? this.temperature,
+ airflowTime: airflowTime ?? this.airflowTime,
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/features/programs/models/step.dart b/lib/features/programs/models/step.dart
index ce8635b..89083e3 100644
--- a/lib/features/programs/models/step.dart
+++ b/lib/features/programs/models/step.dart
@@ -8,10 +8,8 @@ class Step {
final int mixTime;
final int magnetTime;
final int volume;
- final String mixSpeed;
- final String blowSpeed;
final int blowTime;
- final int needleSpeed;
+ final int speed;
Step({
this.id,
@@ -22,10 +20,8 @@ class Step {
this.mixTime = 0,
this.magnetTime = 0,
this.volume = 0,
- this.mixSpeed = '中速',
- this.blowSpeed = '中速',
this.blowTime = 0,
- this.needleSpeed = 5,
+ this.speed = 5,
});
Map toMap() {
@@ -38,10 +34,8 @@ class Step {
'mix_time': mixTime,
'magnet_time': magnetTime,
'volume': volume,
- 'mix_speed': mixSpeed,
- 'blow_speed': blowSpeed,
'blow_time': blowTime,
- 'needle_speed': needleSpeed,
+ 'speed': speed,
};
}
@@ -55,10 +49,8 @@ class Step {
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,
+ speed: map['speed'] as int? ?? 5,
);
}
@@ -71,10 +63,8 @@ class Step {
int? mixTime,
int? magnetTime,
int? volume,
- String? mixSpeed,
- String? blowSpeed,
int? blowTime,
- int? needleSpeed,
+ int? speed,
}) {
return Step(
id: id ?? this.id,
@@ -85,10 +75,8 @@ class Step {
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,
+ speed: speed ?? this.speed,
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/features/programs/pages/programs_page.dart b/lib/features/programs/pages/programs_page.dart
index 75934c1..9616dfa 100644
--- a/lib/features/programs/pages/programs_page.dart
+++ b/lib/features/programs/pages/programs_page.dart
@@ -48,12 +48,6 @@ class _ProgramsPageState extends ConsumerState {
),
child: Row(
children: [
- // 返回按钮
- IconButton(
- icon: const Icon(Icons.arrow_back),
- onPressed: () => context.go('/'),
- ),
- const SizedBox(width: 16),
Text(
l10n?.programs ?? '程序管理',
style: const TextStyle(
diff --git a/lib/features/programs/services/program_import_service.dart b/lib/features/programs/services/program_import_service.dart
index 0dceecf..7dfbf03 100644
--- a/lib/features/programs/services/program_import_service.dart
+++ b/lib/features/programs/services/program_import_service.dart
@@ -48,6 +48,8 @@ class ProgramImportService {
name: programData['name'] as String,
createdAt: programData['createdAt'] ?? DateTime.now().toString().split('.')[0],
status: programData['status'] ?? 1,
+ temperature: programData['temperature'] as int? ?? 50,
+ airflowTime: programData['airflowTime'] as int? ?? 60,
);
final programId = await _programService.addProgram(program);
@@ -65,10 +67,8 @@ class ProgramImportService {
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,
+ speed: stepData['speed'] as int? ?? 5,
);
await _programService.addStep(step);
}
@@ -107,16 +107,16 @@ class ProgramImportService {
'name': program.name,
'createdAt': program.createdAt,
'status': program.status,
+ 'temperature': program.temperature,
+ 'airflowTime': program.airflowTime,
'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,
+ 'speed': s.speed,
}).toList(),
});
}
diff --git a/lib/features/programs/widgets/program_form_dialog.dart b/lib/features/programs/widgets/program_form_dialog.dart
index 8d21402..985a160 100644
--- a/lib/features/programs/widgets/program_form_dialog.dart
+++ b/lib/features/programs/widgets/program_form_dialog.dart
@@ -2,6 +2,7 @@ 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/utils/constants.dart';
import '../../../shared/widgets/common_button.dart';
import '../models/program.dart';
import '../providers/programs_provider.dart';
@@ -21,6 +22,8 @@ class _ProgramFormDialogState extends ConsumerState {
final _formKey = GlobalKey();
late TextEditingController _codeController;
late TextEditingController _nameController;
+ late TextEditingController _temperatureController;
+ late TextEditingController _airflowTimeController;
bool _isEnabled = true;
bool _isSaving = false;
@@ -29,6 +32,10 @@ class _ProgramFormDialogState extends ConsumerState {
super.initState();
_codeController = TextEditingController(text: widget.program?.code ?? '');
_nameController = TextEditingController(text: widget.program?.name ?? '');
+ _temperatureController =
+ TextEditingController(text: '${widget.program?.temperature ?? 50}');
+ _airflowTimeController =
+ TextEditingController(text: '${widget.program?.airflowTime ?? 60}');
_isEnabled = widget.program?.status == 1;
}
@@ -36,6 +43,8 @@ class _ProgramFormDialogState extends ConsumerState {
void dispose() {
_codeController.dispose();
_nameController.dispose();
+ _temperatureController.dispose();
+ _airflowTimeController.dispose();
super.dispose();
}
@@ -95,6 +104,38 @@ class _ProgramFormDialogState extends ConsumerState {
),
const SizedBox(height: 16),
+ // 温度和吹气时间
+ Row(
+ children: [
+ Expanded(
+ child: TextFormField(
+ controller: _temperatureController,
+ decoration: InputDecoration(
+ labelText: '${l10n?.temperature ?? '温度'} (°C)',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ keyboardType: TextInputType.number,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: TextFormField(
+ controller: _airflowTimeController,
+ decoration: InputDecoration(
+ labelText: '${l10n?.airflowTime ?? '吹气时间'} (${Constants.timeUnitSeconds})',
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ keyboardType: TextInputType.number,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 16),
+
// 状态开关
Row(
children: [
@@ -161,6 +202,8 @@ class _ProgramFormDialogState extends ConsumerState {
name: _nameController.text.trim(),
createdAt: widget.program?.createdAt ?? now,
status: _isEnabled ? 1 : 0,
+ temperature: int.tryParse(_temperatureController.text) ?? 50,
+ airflowTime: int.tryParse(_airflowTimeController.text) ?? 60,
);
bool success;
diff --git a/lib/shared/utils/constants.dart b/lib/shared/utils/constants.dart
index 2d21963..6e48da8 100644
--- a/lib/shared/utils/constants.dart
+++ b/lib/shared/utils/constants.dart
@@ -1,10 +1,8 @@
/// 常量定义
class Constants {
- // 速度选项
- static const List speedOptions = ['低速', '中速', '高速'];
-
- // 下针速度档位
- static const List needleSpeedLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+ // 速度档位
+ static const int minSpeed = 1;
+ static const int maxSpeed = 10;
// 孔位列表
static const List positions = [
diff --git a/pubspec.lock b/pubspec.lock
index 53c40a3..f8efc4a 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -650,6 +650,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
+ usb_serial:
+ dependency: "direct main"
+ description:
+ name: usb_serial
+ sha256: a605a600e34e7f28d4e80851ca3999ef747e42e406138887b8a88b8c382a8b07
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.2"
uuid:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index a79bd94..4b2d54a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -34,6 +34,9 @@ dependencies:
# 国际化
intl: ^0.20.2
+ # USB 串口通信(Android USB Host 模式下的 CH340/FTDI/CP210x/PL2303 等芯片)
+ usb_serial: ^0.5.0
+
dev_dependencies:
flutter_test:
sdk: flutter