feat(device): 添加USB设备通信支持和程序参数优化
- 在AndroidManifest.xml中添加USB Host权限和设备过滤器配置 - 新增设备控制国际化词条包括速度档位、吹气时间等 - 重构数据库结构将速度相关字段统一为档位数值存储 - 添加通用KV存储方法用于settings表数据读写 - 优化首页导航实现tab间跳转和状态保持功能 - 更新程序详情页面布局和参数表单界面 - 移除模拟运行器相关测试代码 - 添加USB串口通信依赖包usb_serial
This commit is contained in:
@@ -4,6 +4,9 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- USB Host:与下位机(USB 转串口)通信 -->
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="true" />
|
||||
|
||||
<application
|
||||
android:label="kuaishai2"
|
||||
android:name="${applicationName}"
|
||||
@@ -28,6 +31,15 @@
|
||||
<category android:name="android.intent.category.HOME"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
<!-- USB 设备过滤:当下位机(USB 转串口)插入时,系统会启动本 Activity 并附带设备信息。
|
||||
配合 @xml/usb_device_filter 中声明的 vendor-id 列表使用。
|
||||
若下位机使用其它芯片,可在此追加 vendor-id。 -->
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||
android:resource="@xml/usb_device_filter" />
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
|
||||
12
android/app/src/main/res/xml/usb_device_filter.xml
Normal file
12
android/app/src/main/res/xml/usb_device_filter.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
USB 设备过滤:下位机使用常见 USB 转串口芯片时自动识别
|
||||
包含:CH340 / CH341 / CH343 / CH9103、FTDI、CP210x、PL2303
|
||||
若下位机使用其他芯片,可继续追加 vendor-id + product-id
|
||||
-->
|
||||
<resources>
|
||||
<usb-device vendor-id="0x1A86" /> <!-- CH340 / CH341 / CH343 / CH9103 系列 -->
|
||||
<usb-device vendor-id="0x0403" /> <!-- FTDI 系列 -->
|
||||
<usb-device vendor-id="0x10C4" /> <!-- Silicon Labs CP210x 系列 -->
|
||||
<usb-device vendor-id="0x067B" /> <!-- Prolific PL2303 系列 -->
|
||||
</resources>
|
||||
178
docs/下位机交互数据模型.md
Normal file
178
docs/下位机交互数据模型.md
Normal file
@@ -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
|
||||
|
||||
# 数据模型
|
||||
|
||||
<h2 id="tocS_设备基本信息">设备基本信息</h2>
|
||||
|
||||
<a id="schema设备基本信息"></a>
|
||||
<a id="schema_设备基本信息"></a>
|
||||
<a id="tocS设备基本信息"></a>
|
||||
<a id="tocs设备基本信息"></a>
|
||||
|
||||
```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= 关|
|
||||
|
||||
<h2 id="tocS_下发任务">下发任务</h2>
|
||||
|
||||
<a id="schema下发任务"></a>
|
||||
<a id="schema_下发任务"></a>
|
||||
<a id="tocS下发任务"></a>
|
||||
<a id="tocs下发任务"></a>
|
||||
|
||||
```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|吹气时间|单位:秒|
|
||||
|
||||
<h2 id="tocS_灯光控制">灯光控制</h2>
|
||||
|
||||
<a id="schema灯光控制"></a>
|
||||
<a id="schema_灯光控制"></a>
|
||||
<a id="tocS灯光控制"></a>
|
||||
<a id="tocs灯光控制"></a>
|
||||
|
||||
```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|
|
||||
|
||||
<h2 id="tocS_任务控制">任务控制</h2>
|
||||
|
||||
<a id="schema任务控制"></a>
|
||||
<a id="schema_任务控制"></a>
|
||||
<a id="tocS任务控制"></a>
|
||||
<a id="tocs任务控制"></a>
|
||||
|
||||
```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|
|
||||
|
||||
@@ -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<void> close() async {
|
||||
@@ -91,6 +100,30 @@ class DatabaseService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用 KV 读:读取 settings 表中 [key] 对应的 value;不存在返回 null
|
||||
Future<String?> 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<void> writeSetting(String key, String value) async {
|
||||
final db = await database;
|
||||
await db.insert(
|
||||
'settings',
|
||||
{'key': key, 'value': value},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
|
||||
/// 初始化测试数据(仅调试模式使用)
|
||||
Future<void> 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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -15,7 +15,12 @@ final goRouterProvider = Provider<GoRouter>((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',
|
||||
|
||||
126
lib/features/device/models/serial_config.dart
Normal file
126
lib/features/device/models/serial_config.dart
Normal file
@@ -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<int> 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<String, dynamic>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<RunState> {
|
||||
final MockRunner _runner;
|
||||
final Runner _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 (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<RunState> {
|
||||
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<RunState> {
|
||||
|
||||
/// 停止运行
|
||||
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<List<Step>> _loadSteps(int programId) async {
|
||||
return await _programService.getStepsByProgramId(programId);
|
||||
}
|
||||
void reset() => stop();
|
||||
}
|
||||
|
||||
/// MockRunner Provider
|
||||
final mockRunnerProvider = Provider<MockRunner>((ref) {
|
||||
return MockRunner();
|
||||
});
|
||||
|
||||
/// ProgramService Provider
|
||||
final programServiceProvider = Provider<ProgramService>((ref) {
|
||||
return ProgramService.instance;
|
||||
@@ -170,7 +184,7 @@ final programServiceProvider = Provider<ProgramService>((ref) {
|
||||
/// 运行状态 Provider
|
||||
final runStateProvider =
|
||||
StateNotifierProvider<RunStateNotifier, RunState>((ref) {
|
||||
final runner = ref.watch(mockRunnerProvider);
|
||||
final runner = ref.watch(runnerProvider);
|
||||
final programService = ref.watch(programServiceProvider);
|
||||
return RunStateNotifier(runner, programService);
|
||||
});
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
}
|
||||
@@ -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<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();
|
||||
}
|
||||
}
|
||||
@@ -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<HomePage> createState() => _HomePageState();
|
||||
@@ -22,17 +27,19 @@ class HomePage extends ConsumerStatefulWidget {
|
||||
|
||||
class _HomePageState extends ConsumerState<HomePage>
|
||||
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<RunState>(runStateProvider, (prev, next) {
|
||||
@@ -44,19 +51,25 @@ class _HomePageState extends ConsumerState<HomePage>
|
||||
}
|
||||
});
|
||||
|
||||
final tabs = <StatusBarTab>[
|
||||
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<HomePage>
|
||||
);
|
||||
}
|
||||
|
||||
/// 导航标签栏
|
||||
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: [
|
||||
// 左侧:程序列表(运行时锁定)
|
||||
|
||||
@@ -194,7 +194,7 @@ class RunStatusMonitor extends ConsumerWidget {
|
||||
if (step.mixTime > 0)
|
||||
_buildParamRow(
|
||||
l10n?.speed ?? '转速',
|
||||
'${step.mixSpeed}',
|
||||
'${step.speed} 档',
|
||||
),
|
||||
if (step.magnetTime > 0)
|
||||
_buildParamRow(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<StatusBarTab> tabs;
|
||||
final int currentTabIndex;
|
||||
final ValueChanged<int>? 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<StatusBar> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 更新当前时间显示
|
||||
void _updateTime() {
|
||||
final now = DateTime.now();
|
||||
_currentTime =
|
||||
@@ -49,6 +66,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
||||
|
||||
String _twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||
|
||||
/// 切换照明开关
|
||||
Future<void> _onLightTap() async {
|
||||
widget.onLightToggle?.call();
|
||||
await ref.read(deviceInfoProvider.notifier).toggleLight();
|
||||
@@ -77,7 +95,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
||||
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<StatusBar> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.tabs.isNotEmpty) ...[
|
||||
const SizedBox(width: 32),
|
||||
_buildNavTabs(),
|
||||
],
|
||||
const Spacer(),
|
||||
_LightToggleButton(
|
||||
isOn: deviceInfo.lightingOn,
|
||||
@@ -117,6 +139,92 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建标题栏内嵌的导航标签按钮组(胶囊样式)
|
||||
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 {
|
||||
|
||||
@@ -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<ProgramDetailPage> {
|
||||
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>[
|
||||
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<ProgramDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 子工具栏:返回按钮 + 程序名 + 保存按钮
|
||||
/// 位于状态栏下方,提供详情页特有的操作入口
|
||||
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(
|
||||
|
||||
@@ -34,9 +34,7 @@ class _StepFormState extends State<StepForm> {
|
||||
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<StepForm> {
|
||||
_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<StepForm> {
|
||||
),
|
||||
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)),
|
||||
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<StepForm> {
|
||||
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);
|
||||
|
||||
@@ -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<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -48,12 +48,6 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 返回按钮
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.go('/'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
l10n?.programs ?? '程序管理',
|
||||
style: const TextStyle(
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<ProgramFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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<ProgramFormDialog> {
|
||||
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<ProgramFormDialog> {
|
||||
void dispose() {
|
||||
_codeController.dispose();
|
||||
_nameController.dispose();
|
||||
_temperatureController.dispose();
|
||||
_airflowTimeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -95,6 +104,38 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
||||
),
|
||||
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<ProgramFormDialog> {
|
||||
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;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
/// 常量定义
|
||||
class Constants {
|
||||
// 速度选项
|
||||
static const List<String> speedOptions = ['低速', '中速', '高速'];
|
||||
|
||||
// 下针速度档位
|
||||
static const List<int> needleSpeedLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
// 速度档位
|
||||
static const int minSpeed = 1;
|
||||
static const int maxSpeed = 10;
|
||||
|
||||
// 孔位列表
|
||||
static const List<String> positions = [
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user