feat(device): 添加USB设备通信支持和程序参数优化

- 在AndroidManifest.xml中添加USB Host权限和设备过滤器配置
- 新增设备控制国际化词条包括速度档位、吹气时间等
- 重构数据库结构将速度相关字段统一为档位数值存储
- 添加通用KV存储方法用于settings表数据读写
- 优化首页导航实现tab间跳转和状态保持功能
- 更新程序详情页面布局和参数表单界面
- 移除模拟运行器相关测试代码
- 添加USB串口通信依赖包usb_serial
This commit is contained in:
Developer
2026-06-04 15:13:36 +08:00
parent 67e2c7c76c
commit d53c41c300
24 changed files with 795 additions and 635 deletions

View File

@@ -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

View 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>

View 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|

View File

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

View File

@@ -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',

View File

@@ -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',

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

View File

@@ -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);
});
@@ -185,4 +199,4 @@ final isRunningProvider = Provider<bool>((ref) {
final isPausedProvider = Provider<bool>((ref) {
final status = ref.watch(runStateProvider).status;
return status == RunStatus.paused;
});
});

View File

@@ -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;
}
}

View File

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

View File

@@ -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: [
// 左侧:程序列表(运行时锁定)

View File

@@ -194,7 +194,7 @@ class RunStatusMonitor extends ConsumerWidget {
if (step.mixTime > 0)
_buildParamRow(
l10n?.speed ?? '转速',
'${step.mixSpeed}',
'${step.speed}',
),
if (step.magnetTime > 0)
_buildParamRow(

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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);

View File

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

View File

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

View File

@@ -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(

View File

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

View File

@@ -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;

View File

@@ -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 = [

View File

@@ -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:

View File

@@ -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