From d53c41c3000a2dd367700981e183bbf51483a242 Mon Sep 17 00:00:00 2001 From: Developer <91611@user.local> Date: Thu, 4 Jun 2026 15:13:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(device):=20=E6=B7=BB=E5=8A=A0USB=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E9=80=9A=E4=BF=A1=E6=94=AF=E6=8C=81=E5=92=8C=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F=E5=8F=82=E6=95=B0=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在AndroidManifest.xml中添加USB Host权限和设备过滤器配置 - 新增设备控制国际化词条包括速度档位、吹气时间等 - 重构数据库结构将速度相关字段统一为档位数值存储 - 添加通用KV存储方法用于settings表数据读写 - 优化首页导航实现tab间跳转和状态保持功能 - 更新程序详情页面布局和参数表单界面 - 移除模拟运行器相关测试代码 - 添加USB串口通信依赖包usb_serial --- android/app/src/main/AndroidManifest.xml | 12 ++ .../src/main/res/xml/usb_device_filter.xml | 12 ++ docs/下位机交互数据模型.md | 178 ++++++++++++++++ lib/core/database/database_service.dart | 65 ++++-- lib/core/localization/app_localizations.dart | 42 ++-- lib/core/router/app_router.dart | 7 +- lib/features/device/models/serial_config.dart | 126 ++++++++++++ .../device/providers/run_state_provider.dart | 104 +++++----- lib/features/device/services/mock_runner.dart | 190 ------------------ .../device/services/mock_runner_impl.dart | 114 ----------- lib/features/home/pages/home_page.dart | 88 ++------ .../home/widgets/run_status_monitor.dart | 2 +- .../home/widgets/running_control_panel.dart | 82 ++++---- lib/features/home/widgets/status_bar.dart | 112 ++++++++++- .../pages/program_detail_page.dart | 119 ++++++----- .../program_detail/widgets/step_form.dart | 57 +----- lib/features/programs/models/program.dart | 14 +- lib/features/programs/models/step.dart | 26 +-- .../programs/pages/programs_page.dart | 6 - .../services/program_import_service.dart | 12 +- .../programs/widgets/program_form_dialog.dart | 43 ++++ lib/shared/utils/constants.dart | 8 +- pubspec.lock | 8 + pubspec.yaml | 3 + 24 files changed, 795 insertions(+), 635 deletions(-) create mode 100644 android/app/src/main/res/xml/usb_device_filter.xml create mode 100644 docs/下位机交互数据模型.md create mode 100644 lib/features/device/models/serial_config.dart delete mode 100644 lib/features/device/services/mock_runner.dart delete mode 100644 lib/features/device/services/mock_runner_impl.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e4992c3..dabb396 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,9 @@ + + + + + + + + + + + + + + diff --git a/docs/下位机交互数据模型.md b/docs/下位机交互数据模型.md new file mode 100644 index 0000000..0323cbf --- /dev/null +++ b/docs/下位机交互数据模型.md @@ -0,0 +1,178 @@ +--- +title: 默认模块 +language_tabs: + - shell: Shell + - http: HTTP + - javascript: JavaScript + - ruby: Ruby + - python: Python + - php: PHP + - java: Java + - go: Go +toc_footers: [] +includes: [] +search: true +code_clipboard: true +highlight_theme: darkula +headingLevel: 2 +generator: "@tarslib/widdershins v4.0.30" + +--- + +# 默认模块 + +Base URLs: + +# Authentication + +# 数据模型 + +

设备基本信息

+ + + + + + +```json +{ + "message_id": "string", + "type": "string", + "ack": "string", + "need_ack": true, + "data": { + "door_status": "string", + "task_status": "string", + "light_status": "string" + } +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|message_id|string|true|none||none| +|type|string|true|none|类型|device_info| +|ack|string|true|none||none| +|need_ack|boolean|true|none||none| +|data|object|true|none||none| +|» door_status|string|true|none|门状态|open=开,close=关| +|» task_status|string|true|none|任务运行状态|running=运行中,pause=暂停,idle=空闲| +|» light_status|string|true|none|灯状态|on=开,off= 关| + +

下发任务

+ + + + + + +```json +{ + "message_id": "string", + "type": "string", + "ack": "string", + "need_ack": true, + "data": { + "steps": [ + { + "no": 0, + "slot": 0, + "name": "string", + "mixtime": 0, + "pulltime": 0, + "volume": 0, + "speed": 0 + } + ], + "temperature": 0, + "airflowtime": 0 + } +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|message_id|string|true|none||由uuid生成唯一识别码| +|type|string|true|none|指令类型|create_task| +|ack|string|true|none||需要响应的消息id| +|need_ack|boolean|true|none|是否需要响应|true| +|data|object|true|none||none| +|» steps|[object]|true|none|步骤列表|none| +|»» no|integer|true|none|步骤号|none| +|»» slot|integer|true|none|槽位号|none| +|»» name|string|true|none|步骤名称|none| +|»» mixtime|integer|true|none|搅拌时间|单位:秒| +|»» pulltime|integer|true|none|吸磁时间|单位:秒| +|»» volume|integer|true|none|容积|范围 0-2000| +|»» speed|integer|true|none|速度等级|范围:1-10| +|» temperature|integer|true|none|加热温度|none| +|» airflowtime|integer|true|none|吹气时间|单位:秒| + +

灯光控制

+ + + + + + +```json +{ + "message_id": "string", + "type": "string", + "ack": " ", + "need_ack": true, + "data": { + "status": "string" + } +} + +``` + +### 属性 + +|名称|类型|必选|约束|中文名|说明| +|---|---|---|---|---|---| +|message_id|string|true|none||none| +|type|string|true|none|类型|none| +|ack|string|true|none||none| +|need_ack|boolean|true|none||none| +|data|object|true|none||none| +|» status|string|true|none|开光|on / off| + +

任务控制

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