Compare commits
10 Commits
67e2c7c76c
...
8c2e26ec87
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c2e26ec87 | ||
|
|
87c4b669a0 | ||
|
|
736c36a98e | ||
|
|
37d2af70b7 | ||
|
|
3ab2232845 | ||
|
|
55bdaa9211 | ||
|
|
cbe1e6b470 | ||
|
|
16fbb7d54b | ||
|
|
d91791edaf | ||
|
|
d53c41c300 |
@@ -163,4 +163,4 @@ AppLocalizations 支持中文(zh)和英文(en),使用 `AppLocalizations.of(con
|
|||||||
## 其他说明
|
## 其他说明
|
||||||
|
|
||||||
1. 需求文档:[已确认-污水毒品快检一体机_功能需求文档.md](docs/%E5%B7%B2%E7%A1%AE%E8%AE%A4-%E6%B1%A1%E6%B0%B4%E6%AF%92%E5%93%81%E5%BF%AB%E6%A3%80%E4%B8%80%E4%BD%93%E6%9C%BA_%E5%8A%9F%E8%83%BD%E9%9C%80%E6%B1%82%E6%96%87%E6%A1%A3.md)
|
1. 需求文档:[已确认-污水毒品快检一体机_功能需求文档.md](docs/%E5%B7%B2%E7%A1%AE%E8%AE%A4-%E6%B1%A1%E6%B0%B4%E6%AF%92%E5%93%81%E5%BF%AB%E6%A3%80%E4%B8%80%E4%BD%93%E6%9C%BA_%E5%8A%9F%E8%83%BD%E9%9C%80%E6%B1%82%E6%96%87%E6%A1%A3.md)
|
||||||
2. 运行设备屏幕尺寸为1920*1080(横屏),UI设计必须支持此尺寸
|
2. 运行设备屏幕尺寸为1024x600(横屏),屏幕密度为200,UI设计必须支持此尺寸
|
||||||
@@ -4,6 +4,9 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
|
<!-- USB Host:与下位机(USB 转串口)通信 -->
|
||||||
|
<uses-feature android:name="android.hardware.usb.host" android:required="true" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="kuaishai2"
|
android:label="kuaishai2"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@@ -28,6 +31,15 @@
|
|||||||
<category android:name="android.intent.category.HOME"/>
|
<category android:name="android.intent.category.HOME"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
</intent-filter>
|
</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
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
package com.xiarui.kuaishai2
|
package com.xiarui.kuaishai2
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kiosk 模式主 Activity
|
* Kiosk 模式主 Activity
|
||||||
@@ -14,12 +22,80 @@ import io.flutter.embedding.android.FlutterActivity
|
|||||||
*/
|
*/
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
|
private val downloadsChannel = "com.xiarui.kuaishai2/downloads"
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
// 保持屏幕常亮
|
// 保持屏幕常亮
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, downloadsChannel)
|
||||||
|
.setMethodCallHandler { call, result ->
|
||||||
|
when (call.method) {
|
||||||
|
"saveToDownloads" -> handleSaveToDownloads(call, result)
|
||||||
|
else -> result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSaveToDownloads(
|
||||||
|
call: io.flutter.plugin.common.MethodCall,
|
||||||
|
result: MethodChannel.Result,
|
||||||
|
) {
|
||||||
|
val filename = call.argument<String>("filename")
|
||||||
|
val bytes = call.argument<ByteArray>("bytes")
|
||||||
|
if (filename.isNullOrEmpty() || bytes == null) {
|
||||||
|
result.error("ARG_ERROR", "filename 和 bytes 必填", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val savedPath = saveToDownloads(filename, bytes)
|
||||||
|
result.success(savedPath)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
result.error("SAVE_FAILED", e.message ?: "保存失败", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存到公共 Downloads 目录。
|
||||||
|
* - Android 10+:通过 MediaStore.Downloads 写入,无需 WRITE_EXTERNAL_STORAGE 权限
|
||||||
|
* - Android 9 及以下:直接写 /storage/emulated/0/Download/,需 WRITE_EXTERNAL_STORAGE
|
||||||
|
*/
|
||||||
|
private fun saveToDownloads(filename: String, bytes: ByteArray): String {
|
||||||
|
val mimeType =
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put(MediaStore.Downloads.DISPLAY_NAME, filename)
|
||||||
|
put(MediaStore.Downloads.MIME_TYPE, mimeType)
|
||||||
|
put(MediaStore.Downloads.IS_PENDING, 1)
|
||||||
|
}
|
||||||
|
val resolver = contentResolver
|
||||||
|
val uri = resolver.insert(
|
||||||
|
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
|
||||||
|
values,
|
||||||
|
) ?: throw IllegalStateException("无法创建 MediaStore 记录")
|
||||||
|
resolver.openOutputStream(uri)?.use { it.write(bytes) }
|
||||||
|
?: throw IllegalStateException("无法打开输出流")
|
||||||
|
values.clear()
|
||||||
|
values.put(MediaStore.Downloads.IS_PENDING, 0)
|
||||||
|
resolver.update(uri, values, null, null)
|
||||||
|
"${Environment.DIRECTORY_DOWNLOADS}/$filename"
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
val dir = Environment.getExternalStoragePublicDirectory(
|
||||||
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
|
)
|
||||||
|
if (!dir.exists()) dir.mkdirs()
|
||||||
|
val file = File(dir, filename)
|
||||||
|
FileOutputStream(file).use { it.write(bytes) }
|
||||||
|
file.absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
super.onWindowFocusChanged(hasFocus)
|
super.onWindowFocusChanged(hasFocus)
|
||||||
if (hasFocus) {
|
if (hasFocus) {
|
||||||
|
|||||||
12
android/app/src/main/res/xml/usb_device_filter.xml
Normal file
12
android/app/src/main/res/xml/usb_device_filter.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
USB 设备过滤:下位机使用常见 USB 转串口芯片时自动识别
|
||||||
|
包含:CH340 / CH341 / CH343 / CH9103、FTDI、CP210x、PL2303
|
||||||
|
若下位机使用其他芯片,可继续追加 vendor-id + product-id
|
||||||
|
-->
|
||||||
|
<resources>
|
||||||
|
<usb-device vendor-id="0x1A86" /> <!-- CH340 / CH341 / CH343 / CH9103 系列 -->
|
||||||
|
<usb-device vendor-id="0x0403" /> <!-- FTDI 系列 -->
|
||||||
|
<usb-device vendor-id="0x10C4" /> <!-- Silicon Labs CP210x 系列 -->
|
||||||
|
<usb-device vendor-id="0x067B" /> <!-- Prolific PL2303 系列 -->
|
||||||
|
</resources>
|
||||||
178
docs/下位机交互数据模型.md
Normal file
178
docs/下位机交互数据模型.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
title: 默认模块
|
||||||
|
language_tabs:
|
||||||
|
- shell: Shell
|
||||||
|
- http: HTTP
|
||||||
|
- javascript: JavaScript
|
||||||
|
- ruby: Ruby
|
||||||
|
- python: Python
|
||||||
|
- php: PHP
|
||||||
|
- java: Java
|
||||||
|
- go: Go
|
||||||
|
toc_footers: []
|
||||||
|
includes: []
|
||||||
|
search: true
|
||||||
|
code_clipboard: true
|
||||||
|
highlight_theme: darkula
|
||||||
|
headingLevel: 2
|
||||||
|
generator: "@tarslib/widdershins v4.0.30"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 默认模块
|
||||||
|
|
||||||
|
Base URLs:
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
|
||||||
|
# 数据模型
|
||||||
|
|
||||||
|
<h2 id="tocS_设备基本信息">设备基本信息</h2>
|
||||||
|
|
||||||
|
<a id="schema设备基本信息"></a>
|
||||||
|
<a id="schema_设备基本信息"></a>
|
||||||
|
<a id="tocS设备基本信息"></a>
|
||||||
|
<a id="tocs设备基本信息"></a>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "string",
|
||||||
|
"type": "string",
|
||||||
|
"ack": "string",
|
||||||
|
"need_ack": true,
|
||||||
|
"data": {
|
||||||
|
"door_status": "string",
|
||||||
|
"task_status": "string",
|
||||||
|
"light_status": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 属性
|
||||||
|
|
||||||
|
|名称|类型|必选|约束|中文名|说明|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|message_id|string|true|none||none|
|
||||||
|
|type|string|true|none|类型|device_info|
|
||||||
|
|ack|string|true|none||none|
|
||||||
|
|need_ack|boolean|true|none||none|
|
||||||
|
|data|object|true|none||none|
|
||||||
|
|» door_status|string|true|none|门状态|open=开,close=关|
|
||||||
|
|» task_status|string|true|none|任务运行状态|running=运行中,pause=暂停,idle=空闲|
|
||||||
|
|» light_status|string|true|none|灯状态|on=开,off= 关|
|
||||||
|
|
||||||
|
<h2 id="tocS_下发任务">下发任务</h2>
|
||||||
|
|
||||||
|
<a id="schema下发任务"></a>
|
||||||
|
<a id="schema_下发任务"></a>
|
||||||
|
<a id="tocS下发任务"></a>
|
||||||
|
<a id="tocs下发任务"></a>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "string",
|
||||||
|
"type": "string",
|
||||||
|
"ack": "string",
|
||||||
|
"need_ack": true,
|
||||||
|
"data": {
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"no": 0,
|
||||||
|
"slot": 0,
|
||||||
|
"name": "string",
|
||||||
|
"mixtime": 0,
|
||||||
|
"pulltime": 0,
|
||||||
|
"volume": 0,
|
||||||
|
"speed": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"temperature": 0,
|
||||||
|
"airflowtime": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 属性
|
||||||
|
|
||||||
|
|名称|类型|必选|约束|中文名|说明|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|message_id|string|true|none||由uuid生成唯一识别码|
|
||||||
|
|type|string|true|none|指令类型|create_task|
|
||||||
|
|ack|string|true|none||需要响应的消息id|
|
||||||
|
|need_ack|boolean|true|none|是否需要响应|true|
|
||||||
|
|data|object|true|none||none|
|
||||||
|
|» steps|[object]|true|none|步骤列表|none|
|
||||||
|
|»» no|integer|true|none|步骤号|none|
|
||||||
|
|»» slot|integer|true|none|槽位号|none|
|
||||||
|
|»» name|string|true|none|步骤名称|none|
|
||||||
|
|»» mixtime|integer|true|none|搅拌时间|单位:秒|
|
||||||
|
|»» pulltime|integer|true|none|吸磁时间|单位:秒|
|
||||||
|
|»» volume|integer|true|none|容积|范围 0-2000|
|
||||||
|
|»» speed|integer|true|none|速度等级|范围:1-10|
|
||||||
|
|» temperature|integer|true|none|加热温度|none|
|
||||||
|
|» airflowtime|integer|true|none|吹气时间|单位:秒|
|
||||||
|
|
||||||
|
<h2 id="tocS_灯光控制">灯光控制</h2>
|
||||||
|
|
||||||
|
<a id="schema灯光控制"></a>
|
||||||
|
<a id="schema_灯光控制"></a>
|
||||||
|
<a id="tocS灯光控制"></a>
|
||||||
|
<a id="tocs灯光控制"></a>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "string",
|
||||||
|
"type": "string",
|
||||||
|
"ack": " ",
|
||||||
|
"need_ack": true,
|
||||||
|
"data": {
|
||||||
|
"status": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 属性
|
||||||
|
|
||||||
|
|名称|类型|必选|约束|中文名|说明|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|message_id|string|true|none||none|
|
||||||
|
|type|string|true|none|类型|none|
|
||||||
|
|ack|string|true|none||none|
|
||||||
|
|need_ack|boolean|true|none||none|
|
||||||
|
|data|object|true|none||none|
|
||||||
|
|» status|string|true|none|开光|on / off|
|
||||||
|
|
||||||
|
<h2 id="tocS_任务控制">任务控制</h2>
|
||||||
|
|
||||||
|
<a id="schema任务控制"></a>
|
||||||
|
<a id="schema_任务控制"></a>
|
||||||
|
<a id="tocS任务控制"></a>
|
||||||
|
<a id="tocs任务控制"></a>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "string",
|
||||||
|
"type": "string",
|
||||||
|
"ack": "string",
|
||||||
|
"need_ack": true,
|
||||||
|
"data": {
|
||||||
|
"status": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 属性
|
||||||
|
|
||||||
|
|名称|类型|必选|约束|中文名|说明|
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|message_id|string|true|none||none|
|
||||||
|
|type|string|true|none|类型|control|
|
||||||
|
|ack|string¦null|true|none||none|
|
||||||
|
|need_ack|boolean|true|none||none|
|
||||||
|
|data|object|true|none||none|
|
||||||
|
|» status|string|true|none|状态|continue=继续,stop=停止,暂停=pause|
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ class DatabaseService {
|
|||||||
|
|
||||||
return await openDatabase(
|
return await openDatabase(
|
||||||
path,
|
path,
|
||||||
version: 2,
|
version: 3,
|
||||||
onCreate: _onCreate,
|
onCreate: _onCreate,
|
||||||
onUpgrade: _onUpgrade,
|
onUpgrade: _onUpgrade,
|
||||||
);
|
);
|
||||||
@@ -34,7 +34,9 @@ class DatabaseService {
|
|||||||
code TEXT NOT NULL UNIQUE,
|
code TEXT NOT NULL UNIQUE,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
created_at 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,
|
mix_time INTEGER DEFAULT 0,
|
||||||
magnet_time INTEGER DEFAULT 0,
|
magnet_time INTEGER DEFAULT 0,
|
||||||
volume INTEGER DEFAULT 0,
|
volume INTEGER DEFAULT 0,
|
||||||
mix_speed TEXT DEFAULT '中速',
|
|
||||||
blow_speed TEXT DEFAULT '中速',
|
|
||||||
blow_time INTEGER DEFAULT 0,
|
blow_time INTEGER DEFAULT 0,
|
||||||
needle_speed INTEGER DEFAULT 5,
|
speed INTEGER DEFAULT 5,
|
||||||
FOREIGN KEY (program_id) REFERENCES programs(id) ON DELETE CASCADE
|
FOREIGN KEY (program_id) REFERENCES programs(id) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
@@ -82,6 +82,15 @@ class DatabaseService {
|
|||||||
// 初始化默认密码
|
// 初始化默认密码
|
||||||
await db.insert('settings', {'key': 'password', 'value': '123456'});
|
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 {
|
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 {
|
Future<void> initTestData() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
@@ -103,11 +136,11 @@ class DatabaseService {
|
|||||||
|
|
||||||
// 插入测试程序并添加步骤
|
// 插入测试程序并添加步骤
|
||||||
final testPrograms = [
|
final testPrograms = [
|
||||||
{'code': 'P001', 'name': '标准检测程序', 'created_at': '2026-05-19', '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},
|
{'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},
|
{'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},
|
{'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},
|
{'code': 'P005', 'name': '磁珠分离程序', 'created_at': '2026-05-15', 'status': 1, 'temperature': 55, 'airflow_time': 60},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (final program in testPrograms) {
|
for (final program in testPrograms) {
|
||||||
@@ -123,10 +156,8 @@ class DatabaseService {
|
|||||||
'mix_time': 60,
|
'mix_time': 60,
|
||||||
'magnet_time': 0,
|
'magnet_time': 0,
|
||||||
'volume': 100,
|
'volume': 100,
|
||||||
'mix_speed': '中速',
|
|
||||||
'blow_speed': '中速',
|
|
||||||
'blow_time': 0,
|
'blow_time': 0,
|
||||||
'needle_speed': 5,
|
'speed': 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'program_id': programId,
|
'program_id': programId,
|
||||||
@@ -136,10 +167,8 @@ class DatabaseService {
|
|||||||
'mix_time': 0,
|
'mix_time': 0,
|
||||||
'magnet_time': 30,
|
'magnet_time': 30,
|
||||||
'volume': 0,
|
'volume': 0,
|
||||||
'mix_speed': '中速',
|
|
||||||
'blow_speed': '中速',
|
|
||||||
'blow_time': 0,
|
'blow_time': 0,
|
||||||
'needle_speed': 5,
|
'speed': 5,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'program_id': programId,
|
'program_id': programId,
|
||||||
@@ -149,10 +178,8 @@ class DatabaseService {
|
|||||||
'mix_time': 0,
|
'mix_time': 0,
|
||||||
'magnet_time': 0,
|
'magnet_time': 0,
|
||||||
'volume': 0,
|
'volume': 0,
|
||||||
'mix_speed': '中速',
|
|
||||||
'blow_speed': '高速',
|
|
||||||
'blow_time': 10,
|
'blow_time': 10,
|
||||||
'needle_speed': 8,
|
'speed': 8,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class AppLocalizations {
|
|||||||
String get running => _localizedValues[locale.languageCode]?['running'] ?? '运行中';
|
String get running => _localizedValues[locale.languageCode]?['running'] ?? '运行中';
|
||||||
String get idle => _localizedValues[locale.languageCode]?['idle'] ?? '未运行';
|
String get idle => _localizedValues[locale.languageCode]?['idle'] ?? '未运行';
|
||||||
String get lighting => _localizedValues[locale.languageCode]?['lighting'] ?? '照明';
|
String get lighting => _localizedValues[locale.languageCode]?['lighting'] ?? '照明';
|
||||||
|
String get deviceControl => _localizedValues[locale.languageCode]?['deviceControl'] ?? '设备控制';
|
||||||
|
|
||||||
// 程序管理
|
// 程序管理
|
||||||
String get programs => _localizedValues[locale.languageCode]?['programs'] ?? '程序管理';
|
String get programs => _localizedValues[locale.languageCode]?['programs'] ?? '程序管理';
|
||||||
@@ -29,17 +30,18 @@ class AppLocalizations {
|
|||||||
String get editProgram => _localizedValues[locale.languageCode]?['editProgram'] ?? '编辑程序';
|
String get editProgram => _localizedValues[locale.languageCode]?['editProgram'] ?? '编辑程序';
|
||||||
String get deleteProgram => _localizedValues[locale.languageCode]?['deleteProgram'] ?? '删除程序';
|
String get deleteProgram => _localizedValues[locale.languageCode]?['deleteProgram'] ?? '删除程序';
|
||||||
String get importProgram => _localizedValues[locale.languageCode]?['importProgram'] ?? '导入程序';
|
String get importProgram => _localizedValues[locale.languageCode]?['importProgram'] ?? '导入程序';
|
||||||
|
String get downloadTemplate => _localizedValues[locale.languageCode]?['downloadTemplate'] ?? '下载模板';
|
||||||
String get viewDetails => _localizedValues[locale.languageCode]?['viewDetails'] ?? '查看详情';
|
String get viewDetails => _localizedValues[locale.languageCode]?['viewDetails'] ?? '查看详情';
|
||||||
String get selectedProgram => _localizedValues[locale.languageCode]?['selectedProgram'] ?? '当前选中程序';
|
String get selectedProgram => _localizedValues[locale.languageCode]?['selectedProgram'] ?? '当前选中程序';
|
||||||
String get selectedProgramLabel => _localizedValues[locale.languageCode]?['selectedProgramLabel'] ?? '当前选中';
|
String get selectedProgramLabel => _localizedValues[locale.languageCode]?['selectedProgramLabel'] ?? '当前选中';
|
||||||
String get availablePrograms => _localizedValues[locale.languageCode]?['availablePrograms'] ?? '可用程序';
|
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 runningMonitor => _localizedValues[locale.languageCode]?['runningMonitor'] ?? '运行状态监控';
|
||||||
String get currentHole => _localizedValues[locale.languageCode]?['currentHole'] ?? '当前孔位';
|
String get currentHole => _localizedValues[locale.languageCode]?['currentHole'] ?? '当前孔位';
|
||||||
String get stepParams => _localizedValues[locale.languageCode]?['stepParams'] ?? '步骤参数';
|
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 temperature => _localizedValues[locale.languageCode]?['temperature'] ?? '温度';
|
||||||
|
String get airflowTime => _localizedValues[locale.languageCode]?['airflowTime'] ?? '吹气时间';
|
||||||
String get duration => _localizedValues[locale.languageCode]?['duration'] ?? '持续时间';
|
String get duration => _localizedValues[locale.languageCode]?['duration'] ?? '持续时间';
|
||||||
String get sampleVolume => _localizedValues[locale.languageCode]?['sampleVolume'] ?? '样品体积';
|
String get sampleVolume => _localizedValues[locale.languageCode]?['sampleVolume'] ?? '样品体积';
|
||||||
String get pleaseSelectProgram => _localizedValues[locale.languageCode]?['pleaseSelectProgram'] ?? '请选择要运行的程序';
|
String get pleaseSelectProgram => _localizedValues[locale.languageCode]?['pleaseSelectProgram'] ?? '请选择要运行的程序';
|
||||||
@@ -54,6 +56,7 @@ class AppLocalizations {
|
|||||||
String get remainingTime => _localizedValues[locale.languageCode]?['remainingTime'] ?? '剩余时间';
|
String get remainingTime => _localizedValues[locale.languageCode]?['remainingTime'] ?? '剩余时间';
|
||||||
String get progress => _localizedValues[locale.languageCode]?['progress'] ?? '进度';
|
String get progress => _localizedValues[locale.languageCode]?['progress'] ?? '进度';
|
||||||
String get ceramicSleeveConfirm => _localizedValues[locale.languageCode]?['ceramicSleeveConfirm'] ?? '运行前请确认已安装瓷套棒';
|
String get ceramicSleeveConfirm => _localizedValues[locale.languageCode]?['ceramicSleeveConfirm'] ?? '运行前请确认已安装瓷套棒';
|
||||||
|
String get ceramicSleeveConfirmMessage => _localizedValues[locale.languageCode]?['ceramicSleeveConfirmMessage'] ?? '请确认已放置瓷套棒后再启动程序。';
|
||||||
String get paused => _localizedValues[locale.languageCode]?['paused'] ?? '已暂停';
|
String get paused => _localizedValues[locale.languageCode]?['paused'] ?? '已暂停';
|
||||||
String get stopConfirm => _localizedValues[locale.languageCode]?['stopConfirm'] ?? '确定要停止当前运行的程序吗?';
|
String get stopConfirm => _localizedValues[locale.languageCode]?['stopConfirm'] ?? '确定要停止当前运行的程序吗?';
|
||||||
String get currentProgram => _localizedValues[locale.languageCode]?['currentProgram'] ?? '当前程序';
|
String get currentProgram => _localizedValues[locale.languageCode]?['currentProgram'] ?? '当前程序';
|
||||||
@@ -68,15 +71,7 @@ class AppLocalizations {
|
|||||||
String get mixTime => _localizedValues[locale.languageCode]?['mixTime'] ?? '混合时间';
|
String get mixTime => _localizedValues[locale.languageCode]?['mixTime'] ?? '混合时间';
|
||||||
String get magnetTime => _localizedValues[locale.languageCode]?['magnetTime'] ?? '吸磁时间';
|
String get magnetTime => _localizedValues[locale.languageCode]?['magnetTime'] ?? '吸磁时间';
|
||||||
String get volume => _localizedValues[locale.languageCode]?['volume'] ?? '容积';
|
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 blowTime => _localizedValues[locale.languageCode]?['blowTime'] ?? '吹气时间';
|
||||||
String get needleSpeed => _localizedValues[locale.languageCode]?['needleSpeed'] ?? '下针速度';
|
|
||||||
|
|
||||||
// 速度选项
|
|
||||||
String get lowSpeed => _localizedValues[locale.languageCode]?['lowSpeed'] ?? '低速';
|
|
||||||
String get mediumSpeed => _localizedValues[locale.languageCode]?['mediumSpeed'] ?? '中速';
|
|
||||||
String get highSpeed => _localizedValues[locale.languageCode]?['highSpeed'] ?? '高速';
|
|
||||||
|
|
||||||
// 设置
|
// 设置
|
||||||
String get settings => _localizedValues[locale.languageCode]?['settings'] ?? '系统设置';
|
String get settings => _localizedValues[locale.languageCode]?['settings'] ?? '系统设置';
|
||||||
@@ -98,6 +93,7 @@ class AppLocalizations {
|
|||||||
// 完成提示
|
// 完成提示
|
||||||
String get runComplete => _localizedValues[locale.languageCode]?['runComplete'] ?? '运行完成';
|
String get runComplete => _localizedValues[locale.languageCode]?['runComplete'] ?? '运行完成';
|
||||||
String get sampleDropGuide => _localizedValues[locale.languageCode]?['sampleDropGuide'] ?? '请将样本滴入检测卡';
|
String get sampleDropGuide => _localizedValues[locale.languageCode]?['sampleDropGuide'] ?? '请将样本滴入检测卡';
|
||||||
|
String get complete => _localizedValues[locale.languageCode]?['complete'] ?? '完成';
|
||||||
|
|
||||||
// 补充缺失的翻译
|
// 补充缺失的翻译
|
||||||
String get lightOn => _localizedValues[locale.languageCode]?['lightOn'] ?? '亮';
|
String get lightOn => _localizedValues[locale.languageCode]?['lightOn'] ?? '亮';
|
||||||
@@ -140,6 +136,7 @@ class AppLocalizations {
|
|||||||
'running': '运行中',
|
'running': '运行中',
|
||||||
'idle': '未运行',
|
'idle': '未运行',
|
||||||
'lighting': '照明',
|
'lighting': '照明',
|
||||||
|
'deviceControl': '设备控制',
|
||||||
'programs': '程序管理',
|
'programs': '程序管理',
|
||||||
'programList': '程序列表',
|
'programList': '程序列表',
|
||||||
'programName': '程序名称',
|
'programName': '程序名称',
|
||||||
@@ -149,17 +146,18 @@ class AppLocalizations {
|
|||||||
'editProgram': '编辑程序',
|
'editProgram': '编辑程序',
|
||||||
'deleteProgram': '删除程序',
|
'deleteProgram': '删除程序',
|
||||||
'importProgram': '导入程序',
|
'importProgram': '导入程序',
|
||||||
|
'downloadTemplate': '下载模板',
|
||||||
'viewDetails': '查看详情',
|
'viewDetails': '查看详情',
|
||||||
'selectedProgram': '当前选中程序',
|
'selectedProgram': '当前选中程序',
|
||||||
'selectedProgramLabel': '当前选中',
|
'selectedProgramLabel': '当前选中',
|
||||||
'availablePrograms': '可用程序',
|
'availablePrograms': '可用程序',
|
||||||
'ceramicNotInstalled': '瓷套棒: 未安装 — 禁止启动',
|
|
||||||
'ceramicInstalled': '瓷套棒: 已安装',
|
|
||||||
'runningMonitor': '运行状态监控',
|
'runningMonitor': '运行状态监控',
|
||||||
'currentHole': '当前孔位',
|
'currentHole': '当前孔位',
|
||||||
'stepParams': '步骤参数',
|
'stepParams': '步骤参数',
|
||||||
'speed': '转速',
|
'speed': '速度',
|
||||||
|
'speedLevel': '档',
|
||||||
'temperature': '温度',
|
'temperature': '温度',
|
||||||
|
'airflowTime': '吹气时间',
|
||||||
'duration': '持续时间',
|
'duration': '持续时间',
|
||||||
'sampleVolume': '样品体积',
|
'sampleVolume': '样品体积',
|
||||||
'pleaseSelectProgram': '请选择要运行的程序',
|
'pleaseSelectProgram': '请选择要运行的程序',
|
||||||
@@ -172,6 +170,7 @@ class AppLocalizations {
|
|||||||
'remainingTime': '剩余时间',
|
'remainingTime': '剩余时间',
|
||||||
'progress': '进度',
|
'progress': '进度',
|
||||||
'ceramicSleeveConfirm': '运行前请确认已安装瓷套棒',
|
'ceramicSleeveConfirm': '运行前请确认已安装瓷套棒',
|
||||||
|
'ceramicSleeveConfirmMessage': '请确认已放置瓷套棒后再启动程序。',
|
||||||
'paused': '已暂停',
|
'paused': '已暂停',
|
||||||
'stopConfirm': '确定要停止当前运行的程序吗?',
|
'stopConfirm': '确定要停止当前运行的程序吗?',
|
||||||
'currentProgram': '当前程序',
|
'currentProgram': '当前程序',
|
||||||
@@ -184,13 +183,7 @@ class AppLocalizations {
|
|||||||
'mixTime': '混合时间',
|
'mixTime': '混合时间',
|
||||||
'magnetTime': '吸磁时间',
|
'magnetTime': '吸磁时间',
|
||||||
'volume': '容积',
|
'volume': '容积',
|
||||||
'mixSpeed': '混合速度',
|
|
||||||
'blowSpeed': '吹气速度',
|
|
||||||
'blowTime': '吹气时间',
|
'blowTime': '吹气时间',
|
||||||
'needleSpeed': '下针速度',
|
|
||||||
'lowSpeed': '低速',
|
|
||||||
'mediumSpeed': '中速',
|
|
||||||
'highSpeed': '高速',
|
|
||||||
'settings': '系统设置',
|
'settings': '系统设置',
|
||||||
'language': '语言设置',
|
'language': '语言设置',
|
||||||
'password': '密码修改',
|
'password': '密码修改',
|
||||||
@@ -206,6 +199,7 @@ class AppLocalizations {
|
|||||||
'noData': '暂无数据',
|
'noData': '暂无数据',
|
||||||
'runComplete': '运行完成',
|
'runComplete': '运行完成',
|
||||||
'sampleDropGuide': '请将样本滴入检测卡',
|
'sampleDropGuide': '请将样本滴入检测卡',
|
||||||
|
'complete': '完成',
|
||||||
'lightOn': '亮',
|
'lightOn': '亮',
|
||||||
'lightOff': '暗',
|
'lightOff': '暗',
|
||||||
'enabled': '启用',
|
'enabled': '启用',
|
||||||
@@ -245,6 +239,7 @@ class AppLocalizations {
|
|||||||
'running': 'Running',
|
'running': 'Running',
|
||||||
'idle': 'Idle',
|
'idle': 'Idle',
|
||||||
'lighting': 'Lighting',
|
'lighting': 'Lighting',
|
||||||
|
'deviceControl': 'Device Control',
|
||||||
'programs': 'Programs',
|
'programs': 'Programs',
|
||||||
'programList': 'Program List',
|
'programList': 'Program List',
|
||||||
'programName': 'Program Name',
|
'programName': 'Program Name',
|
||||||
@@ -254,17 +249,18 @@ class AppLocalizations {
|
|||||||
'editProgram': 'Edit Program',
|
'editProgram': 'Edit Program',
|
||||||
'deleteProgram': 'Delete Program',
|
'deleteProgram': 'Delete Program',
|
||||||
'importProgram': 'Import Program',
|
'importProgram': 'Import Program',
|
||||||
|
'downloadTemplate': 'Download Template',
|
||||||
'viewDetails': 'View Details',
|
'viewDetails': 'View Details',
|
||||||
'selectedProgram': 'Selected Program',
|
'selectedProgram': 'Selected Program',
|
||||||
'selectedProgramLabel': 'Selected',
|
'selectedProgramLabel': 'Selected',
|
||||||
'availablePrograms': 'Available Programs',
|
'availablePrograms': 'Available Programs',
|
||||||
'ceramicNotInstalled': 'Ceramic sleeve: Not installed — Cannot start',
|
|
||||||
'ceramicInstalled': 'Ceramic sleeve: Installed',
|
|
||||||
'runningMonitor': 'Running Status Monitor',
|
'runningMonitor': 'Running Status Monitor',
|
||||||
'currentHole': 'Current Position',
|
'currentHole': 'Current Position',
|
||||||
'stepParams': 'Step Parameters',
|
'stepParams': 'Step Parameters',
|
||||||
'speed': 'Speed',
|
'speed': 'Speed',
|
||||||
|
'speedLevel': 'level',
|
||||||
'temperature': 'Temperature',
|
'temperature': 'Temperature',
|
||||||
|
'airflowTime': 'Airflow Time',
|
||||||
'duration': 'Duration',
|
'duration': 'Duration',
|
||||||
'sampleVolume': 'Sample Volume',
|
'sampleVolume': 'Sample Volume',
|
||||||
'pleaseSelectProgram': 'Please select a program',
|
'pleaseSelectProgram': 'Please select a program',
|
||||||
@@ -277,6 +273,7 @@ class AppLocalizations {
|
|||||||
'remainingTime': 'Remaining',
|
'remainingTime': 'Remaining',
|
||||||
'progress': 'Progress',
|
'progress': 'Progress',
|
||||||
'ceramicSleeveConfirm': 'Please confirm ceramic sleeve is installed',
|
'ceramicSleeveConfirm': 'Please confirm ceramic sleeve is installed',
|
||||||
|
'ceramicSleeveConfirmMessage': 'Please make sure the ceramic sleeve is in place before starting the program.',
|
||||||
'paused': 'Paused',
|
'paused': 'Paused',
|
||||||
'stopConfirm': 'Are you sure to stop the running program?',
|
'stopConfirm': 'Are you sure to stop the running program?',
|
||||||
'currentProgram': 'Current Program',
|
'currentProgram': 'Current Program',
|
||||||
@@ -289,13 +286,7 @@ class AppLocalizations {
|
|||||||
'mixTime': 'Mix Time',
|
'mixTime': 'Mix Time',
|
||||||
'magnetTime': 'Magnet Time',
|
'magnetTime': 'Magnet Time',
|
||||||
'volume': 'Volume',
|
'volume': 'Volume',
|
||||||
'mixSpeed': 'Mix Speed',
|
|
||||||
'blowSpeed': 'Blow Speed',
|
|
||||||
'blowTime': 'Blow Time',
|
'blowTime': 'Blow Time',
|
||||||
'needleSpeed': 'Needle Speed',
|
|
||||||
'lowSpeed': 'Low',
|
|
||||||
'mediumSpeed': 'Medium',
|
|
||||||
'highSpeed': 'High',
|
|
||||||
'settings': 'Settings',
|
'settings': 'Settings',
|
||||||
'language': 'Language',
|
'language': 'Language',
|
||||||
'password': 'Password',
|
'password': 'Password',
|
||||||
@@ -311,6 +302,7 @@ class AppLocalizations {
|
|||||||
'noData': 'No Data',
|
'noData': 'No Data',
|
||||||
'runComplete': 'Complete',
|
'runComplete': 'Complete',
|
||||||
'sampleDropGuide': 'Drop sample to test card',
|
'sampleDropGuide': 'Drop sample to test card',
|
||||||
|
'complete': 'Done',
|
||||||
'lightOn': 'On',
|
'lightOn': 'On',
|
||||||
'lightOff': 'Off',
|
'lightOff': 'Off',
|
||||||
'enabled': 'Enabled',
|
'enabled': 'Enabled',
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ final goRouterProvider = Provider<GoRouter>((ref) {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'home',
|
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(
|
GoRoute(
|
||||||
path: '/programs',
|
path: '/programs',
|
||||||
|
|||||||
126
lib/features/device/models/serial_config.dart
Normal file
126
lib/features/device/models/serial_config.dart
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
/// 串口奇偶校验
|
||||||
|
enum SerialParity { none, odd, even, mark, space }
|
||||||
|
|
||||||
|
/// 串口流控
|
||||||
|
enum SerialFlowControl { none, rtsCts, xonXoff, dtrDsr }
|
||||||
|
|
||||||
|
/// 串口配置
|
||||||
|
///
|
||||||
|
/// 持久化到 settings 表的 `serial_config` key 中;
|
||||||
|
/// 打开串口时根据此配置构造底层 `UsbConfig`。
|
||||||
|
class SerialConfig {
|
||||||
|
/// 设备 Vendor ID(十六进制)
|
||||||
|
final int vendorId;
|
||||||
|
|
||||||
|
/// 设备 Product ID(十六进制),0 表示不指定
|
||||||
|
final int productId;
|
||||||
|
|
||||||
|
/// 波特率
|
||||||
|
final int baudRate;
|
||||||
|
|
||||||
|
/// 数据位 (5/6/7/8)
|
||||||
|
final int dataBits;
|
||||||
|
|
||||||
|
/// 停止位 (1/2)
|
||||||
|
final int stopBits;
|
||||||
|
|
||||||
|
/// 校验位
|
||||||
|
final SerialParity parity;
|
||||||
|
|
||||||
|
/// 流控
|
||||||
|
final SerialFlowControl flowControl;
|
||||||
|
|
||||||
|
/// 读超时(毫秒)
|
||||||
|
final int readTimeoutMs;
|
||||||
|
|
||||||
|
/// 写超时(毫秒)
|
||||||
|
final int writeTimeoutMs;
|
||||||
|
|
||||||
|
const SerialConfig({
|
||||||
|
this.vendorId = 0x1A86,
|
||||||
|
this.productId = 0x7523,
|
||||||
|
this.baudRate = 9600,
|
||||||
|
this.dataBits = 8,
|
||||||
|
this.stopBits = 1,
|
||||||
|
this.parity = SerialParity.none,
|
||||||
|
this.flowControl = SerialFlowControl.none,
|
||||||
|
this.readTimeoutMs = 2000,
|
||||||
|
this.writeTimeoutMs = 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 默认配置
|
||||||
|
static const SerialConfig defaults = SerialConfig();
|
||||||
|
|
||||||
|
/// 常用波特率
|
||||||
|
static const List<int> commonBaudRates = [
|
||||||
|
1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800,
|
||||||
|
];
|
||||||
|
|
||||||
|
SerialConfig copyWith({
|
||||||
|
int? vendorId,
|
||||||
|
int? productId,
|
||||||
|
int? baudRate,
|
||||||
|
int? dataBits,
|
||||||
|
int? stopBits,
|
||||||
|
SerialParity? parity,
|
||||||
|
SerialFlowControl? flowControl,
|
||||||
|
int? readTimeoutMs,
|
||||||
|
int? writeTimeoutMs,
|
||||||
|
}) {
|
||||||
|
return SerialConfig(
|
||||||
|
vendorId: vendorId ?? this.vendorId,
|
||||||
|
productId: productId ?? this.productId,
|
||||||
|
baudRate: baudRate ?? this.baudRate,
|
||||||
|
dataBits: dataBits ?? this.dataBits,
|
||||||
|
stopBits: stopBits ?? this.stopBits,
|
||||||
|
parity: parity ?? this.parity,
|
||||||
|
flowControl: flowControl ?? this.flowControl,
|
||||||
|
readTimeoutMs: readTimeoutMs ?? this.readTimeoutMs,
|
||||||
|
writeTimeoutMs: writeTimeoutMs ?? this.writeTimeoutMs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 编码为 JSON 字符串(用于持久化)
|
||||||
|
String toJsonString() => jsonEncode({
|
||||||
|
'vendorId': vendorId,
|
||||||
|
'productId': productId,
|
||||||
|
'baudRate': baudRate,
|
||||||
|
'dataBits': dataBits,
|
||||||
|
'stopBits': stopBits,
|
||||||
|
'parity': parity.name,
|
||||||
|
'flowControl': flowControl.name,
|
||||||
|
'readTimeoutMs': readTimeoutMs,
|
||||||
|
'writeTimeoutMs': writeTimeoutMs,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 从 JSON 字符串解码;解析失败时返回默认值
|
||||||
|
factory SerialConfig.fromJsonString(String? raw) {
|
||||||
|
if (raw == null || raw.isEmpty) return defaults;
|
||||||
|
try {
|
||||||
|
final map = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
|
return SerialConfig(
|
||||||
|
vendorId: (map['vendorId'] as num?)?.toInt() ?? defaults.vendorId,
|
||||||
|
productId: (map['productId'] as num?)?.toInt() ?? defaults.productId,
|
||||||
|
baudRate: (map['baudRate'] as num?)?.toInt() ?? defaults.baudRate,
|
||||||
|
dataBits: (map['dataBits'] as num?)?.toInt() ?? defaults.dataBits,
|
||||||
|
stopBits: (map['stopBits'] as num?)?.toInt() ?? defaults.stopBits,
|
||||||
|
parity: SerialParity.values.firstWhere(
|
||||||
|
(e) => e.name == map['parity'],
|
||||||
|
orElse: () => defaults.parity,
|
||||||
|
),
|
||||||
|
flowControl: SerialFlowControl.values.firstWhere(
|
||||||
|
(e) => e.name == map['flowControl'],
|
||||||
|
orElse: () => defaults.flowControl,
|
||||||
|
),
|
||||||
|
readTimeoutMs:
|
||||||
|
(map['readTimeoutMs'] as num?)?.toInt() ?? defaults.readTimeoutMs,
|
||||||
|
writeTimeoutMs:
|
||||||
|
(map['writeTimeoutMs'] as num?)?.toInt() ?? defaults.writeTimeoutMs,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../programs/models/program.dart';
|
import '../../programs/models/program.dart';
|
||||||
import '../../programs/models/step.dart';
|
import '../../programs/models/step.dart';
|
||||||
import '../../programs/services/program_service.dart';
|
import '../../programs/services/program_service.dart';
|
||||||
import '../services/mock_runner.dart';
|
import '../services/runner_interface.dart';
|
||||||
|
import 'serial_provider.dart';
|
||||||
|
|
||||||
/// 运行状态枚举
|
/// 运行状态枚举
|
||||||
enum RunStatus {
|
enum RunStatus {
|
||||||
idle, // 待机
|
idle, // 待机
|
||||||
running, // 运行中
|
running, // 运行中
|
||||||
paused, // 已暂停
|
paused, // 已暂停
|
||||||
completed,// 已完成
|
completed, // 已完成
|
||||||
error, // 错误
|
error, // 错误
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 运行状态
|
/// 运行状态
|
||||||
@@ -23,6 +24,7 @@ class RunState {
|
|||||||
final int remainingSeconds;
|
final int remainingSeconds;
|
||||||
final double progress;
|
final double progress;
|
||||||
final String? currentWell;
|
final String? currentWell;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
const RunState({
|
const RunState({
|
||||||
this.status = RunStatus.idle,
|
this.status = RunStatus.idle,
|
||||||
@@ -32,6 +34,7 @@ class RunState {
|
|||||||
this.remainingSeconds = 0,
|
this.remainingSeconds = 0,
|
||||||
this.progress = 0,
|
this.progress = 0,
|
||||||
this.currentWell,
|
this.currentWell,
|
||||||
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
RunState copyWith({
|
RunState copyWith({
|
||||||
@@ -42,17 +45,21 @@ class RunState {
|
|||||||
int? remainingSeconds,
|
int? remainingSeconds,
|
||||||
double? progress,
|
double? progress,
|
||||||
String? currentWell,
|
String? currentWell,
|
||||||
|
String? errorMessage,
|
||||||
bool clearProgram = false,
|
bool clearProgram = false,
|
||||||
bool clearWell = false,
|
bool clearWell = false,
|
||||||
|
bool clearError = false,
|
||||||
}) {
|
}) {
|
||||||
return RunState(
|
return RunState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
currentProgram: clearProgram ? null : (currentProgram ?? this.currentProgram),
|
currentProgram:
|
||||||
|
clearProgram ? null : (currentProgram ?? this.currentProgram),
|
||||||
steps: steps ?? this.steps,
|
steps: steps ?? this.steps,
|
||||||
currentStepIndex: currentStepIndex ?? this.currentStepIndex,
|
currentStepIndex: currentStepIndex ?? this.currentStepIndex,
|
||||||
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
|
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
|
||||||
progress: progress ?? this.progress,
|
progress: progress ?? this.progress,
|
||||||
currentWell: clearWell ? null : (currentWell ?? this.currentWell),
|
currentWell: clearWell ? null : (currentWell ?? this.currentWell),
|
||||||
|
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +75,8 @@ class RunState {
|
|||||||
final minutes = (remainingSeconds % 3600) ~/ 60;
|
final minutes = (remainingSeconds % 3600) ~/ 60;
|
||||||
final seconds = remainingSeconds % 60;
|
final seconds = remainingSeconds % 60;
|
||||||
return '${hours.toString().padLeft(2, '0')}:'
|
return '${hours.toString().padLeft(2, '0')}:'
|
||||||
'${minutes.toString().padLeft(2, '0')}:'
|
'${minutes.toString().padLeft(2, '0')}:'
|
||||||
'${seconds.toString().padLeft(2, '0')}';
|
'${seconds.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 格式化进度百分比
|
/// 格式化进度百分比
|
||||||
@@ -80,18 +87,24 @@ class RunState {
|
|||||||
|
|
||||||
/// 运行状态 Notifier
|
/// 运行状态 Notifier
|
||||||
class RunStateNotifier extends StateNotifier<RunState> {
|
class RunStateNotifier extends StateNotifier<RunState> {
|
||||||
final MockRunner _runner;
|
final Runner _runner;
|
||||||
final ProgramService _programService;
|
final ProgramService _programService;
|
||||||
|
|
||||||
RunStateNotifier(this._runner, this._programService) : super(const RunState());
|
RunStateNotifier(this._runner, this._programService) : super(const RunState());
|
||||||
|
|
||||||
/// 开始运行程序
|
/// 开始运行程序
|
||||||
Future<void> start(Program program) async {
|
Future<void> start(Program program) async {
|
||||||
// 获取程序步骤(这里使用模拟数据,实际应从数据库读取)
|
if (state.status == RunStatus.running ||
|
||||||
final steps = await _loadSteps(program.id!);
|
state.status == RunStatus.paused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final steps = await _programService.getStepsByProgramId(program.id!);
|
||||||
if (steps.isEmpty) {
|
if (steps.isEmpty) {
|
||||||
state = state.copyWith(status: RunStatus.error);
|
state = state.copyWith(
|
||||||
|
status: RunStatus.error,
|
||||||
|
errorMessage: '程序步骤为空',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,26 +114,36 @@ class RunStateNotifier extends StateNotifier<RunState> {
|
|||||||
steps: steps,
|
steps: steps,
|
||||||
currentStepIndex: 0,
|
currentStepIndex: 0,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
|
currentWell: steps.first.position,
|
||||||
|
clearError: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
_runner.start(
|
_runner.start(
|
||||||
program,
|
program,
|
||||||
steps,
|
steps,
|
||||||
(stepIndex, remaining, progress, well) {
|
RunnerCallbacks(
|
||||||
state = state.copyWith(
|
onProgress: (stepIndex, remaining, progress, well) {
|
||||||
currentStepIndex: stepIndex,
|
state = state.copyWith(
|
||||||
remainingSeconds: remaining,
|
currentStepIndex: stepIndex,
|
||||||
progress: progress,
|
remainingSeconds: remaining,
|
||||||
currentWell: well,
|
progress: progress,
|
||||||
);
|
currentWell: well,
|
||||||
},
|
);
|
||||||
() {
|
},
|
||||||
state = state.copyWith(
|
onComplete: () {
|
||||||
status: RunStatus.completed,
|
state = state.copyWith(
|
||||||
progress: 1,
|
status: RunStatus.completed,
|
||||||
clearWell: true,
|
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() {
|
void stop() {
|
||||||
_runner.stop();
|
if (state.status == RunStatus.running ||
|
||||||
|
state.status == RunStatus.paused) {
|
||||||
|
_runner.stop();
|
||||||
|
}
|
||||||
state = const RunState(status: RunStatus.idle);
|
state = const RunState(status: RunStatus.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重置状态
|
/// 重置状态
|
||||||
void reset() {
|
void reset() => stop();
|
||||||
stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 加载程序步骤(从数据库读取)
|
|
||||||
Future<List<Step>> _loadSteps(int programId) async {
|
|
||||||
return await _programService.getStepsByProgramId(programId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// MockRunner Provider
|
|
||||||
final mockRunnerProvider = Provider<MockRunner>((ref) {
|
|
||||||
return MockRunner();
|
|
||||||
});
|
|
||||||
|
|
||||||
/// ProgramService Provider
|
/// ProgramService Provider
|
||||||
final programServiceProvider = Provider<ProgramService>((ref) {
|
final programServiceProvider = Provider<ProgramService>((ref) {
|
||||||
return ProgramService.instance;
|
return ProgramService.instance;
|
||||||
@@ -170,7 +184,7 @@ final programServiceProvider = Provider<ProgramService>((ref) {
|
|||||||
/// 运行状态 Provider
|
/// 运行状态 Provider
|
||||||
final runStateProvider =
|
final runStateProvider =
|
||||||
StateNotifierProvider<RunStateNotifier, RunState>((ref) {
|
StateNotifierProvider<RunStateNotifier, RunState>((ref) {
|
||||||
final runner = ref.watch(mockRunnerProvider);
|
final runner = ref.watch(runnerProvider);
|
||||||
final programService = ref.watch(programServiceProvider);
|
final programService = ref.watch(programServiceProvider);
|
||||||
return RunStateNotifier(runner, programService);
|
return RunStateNotifier(runner, programService);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../../core/database/database_service.dart';
|
import '../../../core/database/database_service.dart';
|
||||||
import '../models/serial_config.dart';
|
import '../models/serial_config.dart';
|
||||||
|
import '../services/auto_serial_connect.dart';
|
||||||
import '../services/device_message_service.dart';
|
import '../services/device_message_service.dart';
|
||||||
import '../services/json_protocol.dart';
|
import '../services/json_protocol.dart';
|
||||||
import '../services/runner_interface.dart';
|
import '../services/runner_interface.dart';
|
||||||
@@ -15,6 +18,46 @@ final serialPortServiceProvider = Provider<SerialPortService>((ref) {
|
|||||||
return service;
|
return service;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// 串口连接状态(响应式)
|
||||||
|
///
|
||||||
|
/// 直接 `ref.watch(serialPortServiceProvider).state` 不会触发 UI 重建,
|
||||||
|
/// 因为 [SerialPortService] 内部 `_state` 的变化不会冒泡到 Provider 层。
|
||||||
|
/// 这里把状态抽出为独立的 StateNotifierProvider,让状态栏等 UI 能即时
|
||||||
|
/// 反映连接/断开事件,避免出现"标题栏显示已连接、实际下发失败"的错觉。
|
||||||
|
class SerialConnectionStateNotifier
|
||||||
|
extends StateNotifier<SerialConnectionState> {
|
||||||
|
SerialConnectionStateNotifier(this._service) : super(_service.state) {
|
||||||
|
_sub = _service.connectionStateChanges.listen((s) => state = s);
|
||||||
|
}
|
||||||
|
|
||||||
|
final SerialPortService _service;
|
||||||
|
late final StreamSubscription<SerialConnectionState> _sub;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_sub.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final serialConnectionStateProvider = StateNotifierProvider<
|
||||||
|
SerialConnectionStateNotifier, SerialConnectionState>((ref) {
|
||||||
|
final service = ref.watch(serialPortServiceProvider);
|
||||||
|
return SerialConnectionStateNotifier(service);
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 启动自动连接服务
|
||||||
|
///
|
||||||
|
/// 通过 [main] 中的 ProviderContainer 在 runApp 之前触发一次,
|
||||||
|
/// 服务内部立即尝试连接第一个 USB 串口设备,失败时按 3s 间隔重试。
|
||||||
|
final autoSerialConnectProvider = Provider<AutoSerialConnect>((ref) {
|
||||||
|
final service = ref.watch(serialPortServiceProvider);
|
||||||
|
final auto = AutoSerialConnect(service);
|
||||||
|
auto.start();
|
||||||
|
ref.onDispose(auto.dispose);
|
||||||
|
return auto;
|
||||||
|
});
|
||||||
|
|
||||||
/// JSON 协议编解码器(可在调试/真机协议不一致时整体替换)
|
/// JSON 协议编解码器(可在调试/真机协议不一致时整体替换)
|
||||||
final jsonProtocolProvider = Provider<JsonProtocolService>((ref) {
|
final jsonProtocolProvider = Provider<JsonProtocolService>((ref) {
|
||||||
return JsonProtocolService();
|
return JsonProtocolService();
|
||||||
|
|||||||
109
lib/features/device/services/auto_serial_connect.dart
Normal file
109
lib/features/device/services/auto_serial_connect.dart
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:usb_serial/usb_serial.dart';
|
||||||
|
|
||||||
|
import '../models/serial_config.dart';
|
||||||
|
import 'device_log.dart';
|
||||||
|
import 'serial_port_service.dart';
|
||||||
|
|
||||||
|
/// 启动自动连接服务
|
||||||
|
///
|
||||||
|
/// App 启动时调用 [start],自动连接第一个可用的 USB 串口设备:
|
||||||
|
/// - 固定 115200 波特率 / 8 数据位 / 1 停止位 / 默认其它
|
||||||
|
/// - 连接失败时每 3 秒重试一次
|
||||||
|
/// - 连接成功后停止重试;连接断开后重新进入重试循环
|
||||||
|
class AutoSerialConnect {
|
||||||
|
/// 重试间隔
|
||||||
|
static const Duration retryInterval = Duration(seconds: 3);
|
||||||
|
|
||||||
|
/// 自动连接使用的固定参数
|
||||||
|
static const SerialConfig autoConfig = SerialConfig(
|
||||||
|
baudRate: 115200,
|
||||||
|
dataBits: 8,
|
||||||
|
stopBits: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final SerialPortService _service;
|
||||||
|
Timer? _retryTimer;
|
||||||
|
StreamSubscription<SerialConnectionState>? _stateSub;
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
|
AutoSerialConnect(this._service);
|
||||||
|
|
||||||
|
/// 启动自动连接:立即尝试一次,失败则按 [retryInterval] 周期重试
|
||||||
|
void start() {
|
||||||
|
if (_stateSub != null) return;
|
||||||
|
_stateSub = _service.connectionStateChanges.listen(_onStateChange);
|
||||||
|
unawaited(_tryConnect());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onStateChange(SerialConnectionState s) {
|
||||||
|
if (_disposed) return;
|
||||||
|
switch (s) {
|
||||||
|
case SerialConnectionState.connected:
|
||||||
|
// 连接成功:停止重试
|
||||||
|
_cancelRetry();
|
||||||
|
case SerialConnectionState.disconnected:
|
||||||
|
case SerialConnectionState.error:
|
||||||
|
// 设备断开或出错:进入重试
|
||||||
|
_scheduleRetry();
|
||||||
|
case SerialConnectionState.connecting:
|
||||||
|
// 正在连接中,忽略
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _tryConnect() async {
|
||||||
|
if (_disposed) return;
|
||||||
|
if (_service.isConnected ||
|
||||||
|
_service.state == SerialConnectionState.connecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<UsbDevice> devices;
|
||||||
|
try {
|
||||||
|
devices = await _service.listDevices();
|
||||||
|
} catch (e) {
|
||||||
|
DeviceLog.warn('列出 USB 设备失败: $e,${retryInterval.inSeconds}s 后重试');
|
||||||
|
_scheduleRetry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
DeviceLog.info('未检测到 USB 串口设备,${retryInterval.inSeconds}s 后重试');
|
||||||
|
_scheduleRetry();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final device = devices.first;
|
||||||
|
final ok = await _service.connect(device, autoConfig);
|
||||||
|
if (!ok) {
|
||||||
|
DeviceLog.warn('串口自动连接失败: '
|
||||||
|
'${_service.lastError ?? "未知错误"},${retryInterval.inSeconds}s 后重试');
|
||||||
|
_scheduleRetry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleRetry() {
|
||||||
|
if (_disposed) return;
|
||||||
|
if (_retryTimer != null) return;
|
||||||
|
_retryTimer = Timer(retryInterval, () {
|
||||||
|
_retryTimer = null;
|
||||||
|
unawaited(_tryConnect());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelRetry() {
|
||||||
|
_retryTimer?.cancel();
|
||||||
|
_retryTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 释放资源
|
||||||
|
Future<void> dispose() async {
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
_cancelRetry();
|
||||||
|
await _stateSub?.cancel();
|
||||||
|
_stateSub = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
/// 下位机消息类型
|
/// 下位机消息类型
|
||||||
///
|
///
|
||||||
/// 对应《下位机交互数据模型.md》中定义的四种 type 字段。
|
/// 对应《下位机交互数据模型.md》中定义的四种 type 字段。
|
||||||
@@ -147,18 +149,11 @@ class DeviceMessage {
|
|||||||
|
|
||||||
/// 消息 ID 生成器
|
/// 消息 ID 生成器
|
||||||
///
|
///
|
||||||
/// 使用时间戳 + 随机数生成全局唯一 ID(避免引入 uuid 依赖)。
|
/// 基于 [Uuid.v4] 生成全局唯一 ID(128 位随机,标准 36 字符 hex-with-dashes 形式)。
|
||||||
/// 格式:`<millis>-<rand>`,例如 `1717500000000-1a2b3c`
|
/// 例如 `550e8400-e29b-41d4-a716-446655440000`。
|
||||||
class MessageIdGenerator {
|
class MessageIdGenerator {
|
||||||
int _counter = 0;
|
static const Uuid _uuid = Uuid();
|
||||||
|
|
||||||
/// 生成下一个唯一 ID
|
/// 生成下一个唯一 ID
|
||||||
String next() {
|
String next() => _uuid.v4();
|
||||||
_counter = (_counter + 1) & 0xFFFFFF;
|
|
||||||
final ts = DateTime.now().millisecondsSinceEpoch.toRadixString(36);
|
|
||||||
final rand = (_counter.toRadixString(36) +
|
|
||||||
(DateTime.now().microsecondsSinceEpoch & 0xFFFF).toRadixString(36))
|
|
||||||
.padLeft(4, '0');
|
|
||||||
return '$ts-$rand';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'device_log.dart';
|
||||||
import 'device_message.dart';
|
import 'device_message.dart';
|
||||||
|
|
||||||
/// JSON 协议层帧编解码器
|
/// JSON 协议层帧编解码器
|
||||||
@@ -67,7 +68,7 @@ class JsonProtocolService {
|
|||||||
(_buffer[2] << 8) |
|
(_buffer[2] << 8) |
|
||||||
_buffer[3];
|
_buffer[3];
|
||||||
if (len <= 0 || len > _maxFrameBytes) {
|
if (len <= 0 || len > _maxFrameBytes) {
|
||||||
// 长度异常,丢弃首字节重新对齐
|
DeviceLog.warn('tryDecode: 异常长度=$len 丢弃首字节 0x${_buffer[0].toRadixString(16).padLeft(2, '0')}');
|
||||||
_buffer.removeAt(0);
|
_buffer.removeAt(0);
|
||||||
return (null, 0);
|
return (null, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import '../../programs/models/step.dart';
|
|
||||||
import '../../programs/models/program.dart';
|
|
||||||
|
|
||||||
/// 模拟运行器回调
|
|
||||||
typedef RunProgressCallback = void Function(
|
|
||||||
int currentStepIndex,
|
|
||||||
int remainingSeconds,
|
|
||||||
double progress,
|
|
||||||
String currentWell,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef RunCompleteCallback = void Function();
|
|
||||||
|
|
||||||
/// 模拟运行器
|
|
||||||
/// 用于在没有实际硬件连接时模拟程序执行过程
|
|
||||||
class MockRunner {
|
|
||||||
Timer? _timer;
|
|
||||||
Program? _currentProgram;
|
|
||||||
List<Step> _steps = [];
|
|
||||||
int _currentStepIndex = 0;
|
|
||||||
int _remainingSeconds = 0;
|
|
||||||
bool _isPaused = false;
|
|
||||||
RunProgressCallback? _onProgress;
|
|
||||||
RunCompleteCallback? _onComplete;
|
|
||||||
|
|
||||||
/// 是否正在运行
|
|
||||||
bool get isRunning => _timer != null && !_isPaused;
|
|
||||||
|
|
||||||
/// 是否已暂停
|
|
||||||
bool get isPaused => _isPaused;
|
|
||||||
|
|
||||||
/// 当前程序
|
|
||||||
Program? get currentProgram => _currentProgram;
|
|
||||||
|
|
||||||
/// 开始运行程序
|
|
||||||
void start(
|
|
||||||
Program program,
|
|
||||||
List<Step> steps,
|
|
||||||
RunProgressCallback onProgress,
|
|
||||||
RunCompleteCallback onComplete,
|
|
||||||
) {
|
|
||||||
_currentProgram = program;
|
|
||||||
_steps = steps;
|
|
||||||
_onProgress = onProgress;
|
|
||||||
_onComplete = onComplete;
|
|
||||||
_currentStepIndex = 0;
|
|
||||||
_isPaused = false;
|
|
||||||
|
|
||||||
if (steps.isEmpty) {
|
|
||||||
onComplete();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始执行第一个步骤
|
|
||||||
_startStep(steps[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 暂停运行
|
|
||||||
void pause() {
|
|
||||||
if (_timer != null && !_isPaused) {
|
|
||||||
_isPaused = true;
|
|
||||||
_timer!.cancel();
|
|
||||||
_timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 继续运行
|
|
||||||
void resume() {
|
|
||||||
if (_isPaused && _currentProgram != null) {
|
|
||||||
_isPaused = false;
|
|
||||||
_resumeStep();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 停止运行
|
|
||||||
void stop() {
|
|
||||||
_timer?.cancel();
|
|
||||||
_timer = null;
|
|
||||||
_currentProgram = null;
|
|
||||||
_steps = [];
|
|
||||||
_currentStepIndex = 0;
|
|
||||||
_remainingSeconds = 0;
|
|
||||||
_isPaused = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 开始执行步骤
|
|
||||||
void _startStep(Step step) {
|
|
||||||
// 计算步骤总时间(混合时间 + 吸磁时间 + 吹气时间)
|
|
||||||
_remainingSeconds = step.mixTime + step.magnetTime + step.blowTime;
|
|
||||||
|
|
||||||
// 如果步骤时间为0,设置最小演示时间(5秒)
|
|
||||||
if (_remainingSeconds == 0) {
|
|
||||||
_remainingSeconds = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动定时器,每秒更新
|
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
||||||
_remainingSeconds--;
|
|
||||||
|
|
||||||
// 计算总进度
|
|
||||||
final totalSeconds = _calculateTotalSeconds();
|
|
||||||
final elapsedSeconds = _calculateElapsedSeconds();
|
|
||||||
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
|
|
||||||
|
|
||||||
// 回调进度更新
|
|
||||||
_onProgress?.call(
|
|
||||||
_currentStepIndex,
|
|
||||||
_remainingSeconds,
|
|
||||||
progress,
|
|
||||||
step.position,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 步骤完成
|
|
||||||
if (_remainingSeconds <= 0) {
|
|
||||||
timer.cancel();
|
|
||||||
_timer = null;
|
|
||||||
_nextStep();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 继续执行步骤(从暂停恢复)
|
|
||||||
void _resumeStep() {
|
|
||||||
if (_currentStepIndex >= _steps.length) return;
|
|
||||||
|
|
||||||
final step = _steps[_currentStepIndex];
|
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
||||||
_remainingSeconds--;
|
|
||||||
|
|
||||||
final totalSeconds = _calculateTotalSeconds();
|
|
||||||
final elapsedSeconds = _calculateElapsedSeconds();
|
|
||||||
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
|
|
||||||
|
|
||||||
_onProgress?.call(
|
|
||||||
_currentStepIndex,
|
|
||||||
_remainingSeconds,
|
|
||||||
progress,
|
|
||||||
step.position,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (_remainingSeconds <= 0) {
|
|
||||||
timer.cancel();
|
|
||||||
_timer = null;
|
|
||||||
_nextStep();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 执行下一个步骤
|
|
||||||
void _nextStep() {
|
|
||||||
_currentStepIndex++;
|
|
||||||
|
|
||||||
if (_currentStepIndex >= _steps.length) {
|
|
||||||
// 所有步骤完成
|
|
||||||
_onComplete?.call();
|
|
||||||
stop();
|
|
||||||
} else {
|
|
||||||
// 执行下一个步骤
|
|
||||||
_startStep(_steps[_currentStepIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 计算总执行时间
|
|
||||||
int _calculateTotalSeconds() {
|
|
||||||
int total = 0;
|
|
||||||
for (final step in _steps) {
|
|
||||||
int stepTime = step.mixTime + step.magnetTime + step.blowTime;
|
|
||||||
if (stepTime == 0) stepTime = 5;
|
|
||||||
total += stepTime;
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 计算已执行时间
|
|
||||||
int _calculateElapsedSeconds() {
|
|
||||||
int elapsed = 0;
|
|
||||||
for (int i = 0; i < _currentStepIndex; i++) {
|
|
||||||
int stepTime = _steps[i].mixTime + _steps[i].magnetTime + _steps[i].blowTime;
|
|
||||||
if (stepTime == 0) stepTime = 5;
|
|
||||||
elapsed += stepTime;
|
|
||||||
}
|
|
||||||
// 加上当前步骤已执行的时间
|
|
||||||
final currentStep = _steps[_currentStepIndex];
|
|
||||||
int currentStepTime = currentStep.mixTime + currentStep.magnetTime + currentStep.blowTime;
|
|
||||||
if (currentStepTime == 0) currentStepTime = 5;
|
|
||||||
elapsed += currentStepTime - _remainingSeconds;
|
|
||||||
return elapsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import '../../programs/models/program.dart';
|
|
||||||
import '../../programs/models/step.dart';
|
|
||||||
import 'runner_interface.dart';
|
|
||||||
|
|
||||||
/// 模拟运行器(用于开发测试)
|
|
||||||
/// 模拟硬件运行过程
|
|
||||||
class MockRunner implements Runner {
|
|
||||||
@override
|
|
||||||
RunnerStatus status = RunnerStatus.idle;
|
|
||||||
|
|
||||||
bool _isRunning = false;
|
|
||||||
int _currentStep = 0;
|
|
||||||
int _remainingSeconds = 0;
|
|
||||||
RunnerCallbacks? _callbacks;
|
|
||||||
List<Step> _steps = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void start(Program program, List<Step> steps, RunnerCallbacks callbacks) {
|
|
||||||
if (steps.isEmpty) {
|
|
||||||
callbacks.onError?.call('No steps to run');
|
|
||||||
status = RunnerStatus.error;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_steps = steps;
|
|
||||||
_callbacks = callbacks;
|
|
||||||
_currentStep = 0;
|
|
||||||
_isRunning = true;
|
|
||||||
status = RunnerStatus.running;
|
|
||||||
|
|
||||||
// 开始模拟运行
|
|
||||||
_runSimulation();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _runSimulation() {
|
|
||||||
if (!_isRunning || _currentStep >= _steps.length) {
|
|
||||||
_completeRun();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final step = _steps[_currentStep];
|
|
||||||
// 计算步骤时间(混合时间 + 吸磁时间 + 吹气时间 + 5秒最小)
|
|
||||||
final stepTime = (step.mixTime ?? 0) + (step.magnetTime ?? 0) + (step.blowTime ?? 0) + 5;
|
|
||||||
_remainingSeconds = stepTime.clamp(5, 300);
|
|
||||||
|
|
||||||
// 模拟倒计时
|
|
||||||
_simulateStepProgress(stepTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _simulateStepProgress(int totalSeconds) {
|
|
||||||
// 简化模拟:每秒更新进度
|
|
||||||
int elapsed = 0;
|
|
||||||
while (_isRunning && elapsed < totalSeconds) {
|
|
||||||
elapsed++;
|
|
||||||
final remaining = totalSeconds - elapsed;
|
|
||||||
final progress = elapsed / totalSeconds;
|
|
||||||
|
|
||||||
_callbacks?.onProgress?.call(
|
|
||||||
_currentStep,
|
|
||||||
remaining,
|
|
||||||
(_currentStep + progress) / _steps.length,
|
|
||||||
_steps[_currentStep].position,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 实际实现需要使用 Timer
|
|
||||||
// await Future.delayed(Duration(seconds: 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isRunning) {
|
|
||||||
_currentStep++;
|
|
||||||
_runSimulation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _completeRun() {
|
|
||||||
status = RunnerStatus.completed;
|
|
||||||
_isRunning = false;
|
|
||||||
_callbacks?.onComplete?.call();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void pause() {
|
|
||||||
if (status == RunnerStatus.running) {
|
|
||||||
_isRunning = false;
|
|
||||||
status = RunnerStatus.paused;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void resume() {
|
|
||||||
if (status == RunnerStatus.paused) {
|
|
||||||
_isRunning = true;
|
|
||||||
status = RunnerStatus.running;
|
|
||||||
// 继续运行
|
|
||||||
_runSimulation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void stop() {
|
|
||||||
_isRunning = false;
|
|
||||||
status = RunnerStatus.idle;
|
|
||||||
_currentStep = 0;
|
|
||||||
_remainingSeconds = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
RunnerStatus getStatus() => status;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -66,6 +66,9 @@ class JsonSerialRunner implements Runner {
|
|||||||
final messageId = _msg.nextId();
|
final messageId = _msg.nextId();
|
||||||
_pendingCreateTaskId = messageId;
|
_pendingCreateTaskId = messageId;
|
||||||
final msg = payload.toMessage(messageId, needAck: true);
|
final msg = payload.toMessage(messageId, needAck: true);
|
||||||
|
// 乐观更新:与 RunStateNotifier 的运行态保持一致,
|
||||||
|
// 避免在 create_task 应答到达前的窗口里,pause/stop 被状态守卫静默丢弃。
|
||||||
|
status = RunnerStatus.running;
|
||||||
DeviceLog.info('Runner.start: program="${program.name}" steps=${steps.length} '
|
DeviceLog.info('Runner.start: program="${program.name}" steps=${steps.length} '
|
||||||
'temperature=${program.temperature} airflow=${program.airflowTime}');
|
'temperature=${program.temperature} airflow=${program.airflowTime}');
|
||||||
_msg.send(msg).then((ok) {
|
_msg.send(msg).then((ok) {
|
||||||
@@ -132,9 +135,9 @@ class JsonSerialRunner implements Runner {
|
|||||||
if (ack.ack != _pendingCreateTaskId) return;
|
if (ack.ack != _pendingCreateTaskId) return;
|
||||||
_pendingCreateTaskId = null;
|
_pendingCreateTaskId = null;
|
||||||
DeviceLog.info('Runner received create_task ack: id=${ack.messageId}');
|
DeviceLog.info('Runner received create_task ack: id=${ack.messageId}');
|
||||||
// ack 即视为下位机已接受任务,进入 running 状态
|
// 状态已由 start() 乐观置为 running,此处仅启动本地兜底倒计时。
|
||||||
if (status == RunnerStatus.idle || status == RunnerStatus.error) {
|
// 若发送失败,.then() 已将 status 置为 error,不应再启动倒计时。
|
||||||
status = RunnerStatus.running;
|
if (status == RunnerStatus.running) {
|
||||||
_startLocalTicker();
|
_startLocalTicker();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ import '../../../shared/widgets/common_button.dart';
|
|||||||
import '../../device/providers/run_state_provider.dart';
|
import '../../device/providers/run_state_provider.dart';
|
||||||
|
|
||||||
/// 运行完成提示页面
|
/// 运行完成提示页面
|
||||||
|
///
|
||||||
|
/// 页面提供两种返回设备控制页的方式:
|
||||||
|
/// 1. 顶部 AppBar 的返回箭头
|
||||||
|
/// 2. 卡片底部的「完成」主按钮
|
||||||
|
///
|
||||||
|
/// 内部使用 [SingleChildScrollView] 包裹,避免在父级高度受限时按钮被裁切。
|
||||||
class CompletePage extends ConsumerWidget {
|
class CompletePage extends ConsumerWidget {
|
||||||
const CompletePage({super.key});
|
const CompletePage({super.key});
|
||||||
|
|
||||||
@@ -16,115 +22,143 @@ class CompletePage extends ConsumerWidget {
|
|||||||
final runState = ref.watch(runStateProvider);
|
final runState = ref.watch(runStateProvider);
|
||||||
final runNotifier = ref.read(runStateProvider.notifier);
|
final runNotifier = ref.read(runStateProvider.notifier);
|
||||||
|
|
||||||
|
/// 完成并返回设备控制页
|
||||||
|
void finishAndGoHome() {
|
||||||
|
runNotifier.reset();
|
||||||
|
context.go('/');
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Container(
|
backgroundColor: AppTheme.backgroundColor,
|
||||||
color: AppTheme.backgroundColor,
|
appBar: AppBar(
|
||||||
|
backgroundColor: AppTheme.backgroundColor,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
color: AppTheme.textPrimary,
|
||||||
|
tooltip: l10n?.complete ?? '完成',
|
||||||
|
onPressed: finishAndGoHome,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
l10n?.runComplete ?? '运行完成',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.textPrimary,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: ConstrainedBox(
|
||||||
width: 600,
|
constraints: const BoxConstraints(maxWidth: 600),
|
||||||
padding: const EdgeInsets.all(40),
|
child: SingleChildScrollView(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
color: Colors.white,
|
child: Container(
|
||||||
borderRadius: BorderRadius.circular(16),
|
padding: const EdgeInsets.all(32),
|
||||||
boxShadow: [
|
decoration: BoxDecoration(
|
||||||
BoxShadow(
|
color: Colors.white,
|
||||||
color: Colors.black.withValues(alpha: 0.15),
|
borderRadius: BorderRadius.circular(16),
|
||||||
blurRadius: 20,
|
boxShadow: [
|
||||||
offset: const Offset(0, 10),
|
BoxShadow(
|
||||||
),
|
color: Colors.black.withValues(alpha: 0.15),
|
||||||
],
|
blurRadius: 20,
|
||||||
),
|
offset: const Offset(0, 10),
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// 成功图标
|
|
||||||
Container(
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.successColor.withValues(alpha: 0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
size: 60,
|
|
||||||
color: AppTheme.successColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// 标题
|
|
||||||
Text(
|
|
||||||
l10n?.runComplete ?? '程序运行完成',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppTheme.textPrimary,
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// 提示信息
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.warningColor.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
l10n?.sampleDropGuide ?? '请将样本滴入检测卡',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppTheme.warningColor,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// 操作示意图
|
|
||||||
_buildOperationGuide(),
|
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// 按钮区域
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
// 返回首页按钮
|
|
||||||
CommonButton(
|
|
||||||
text: l10n?.backToHome ?? '返回首页',
|
|
||||||
icon: Icons.home,
|
|
||||||
type: ButtonType.primary,
|
|
||||||
onPressed: () {
|
|
||||||
runNotifier.reset();
|
|
||||||
context.go('/');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(width: 24),
|
|
||||||
|
|
||||||
// 重新运行按钮
|
|
||||||
CommonButton(
|
|
||||||
text: l10n?.runAgain ?? '重新运行',
|
|
||||||
icon: Icons.refresh,
|
|
||||||
type: ButtonType.secondary,
|
|
||||||
onPressed: () {
|
|
||||||
final program = runState.currentProgram;
|
|
||||||
if (program != null) {
|
|
||||||
runNotifier.reset();
|
|
||||||
runNotifier.start(program);
|
|
||||||
context.go('/');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// 成功图标
|
||||||
|
Container(
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.successColor.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
size: 48,
|
||||||
|
color: AppTheme.successColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
Text(
|
||||||
|
l10n?.runComplete ?? '程序运行完成',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.textPrimary,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 提示信息
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.warningColor.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
l10n?.sampleDropGuide ?? '请将样本滴入检测卡',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppTheme.warningColor,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// 操作示意图
|
||||||
|
_buildOperationGuide(),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 按钮区域:完成(主按钮)+ 重新运行(次按钮)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// 完成按钮 - 主操作,返回设备控制页
|
||||||
|
CommonButton(
|
||||||
|
text: l10n?.complete ?? '完成',
|
||||||
|
icon: Icons.check_circle,
|
||||||
|
type: ButtonType.primary,
|
||||||
|
onPressed: finishAndGoHome,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
|
||||||
|
// 重新运行按钮
|
||||||
|
CommonButton(
|
||||||
|
text: l10n?.runAgain ?? '重新运行',
|
||||||
|
icon: Icons.refresh,
|
||||||
|
type: ButtonType.secondary,
|
||||||
|
onPressed: () {
|
||||||
|
final program = runState.currentProgram;
|
||||||
|
if (program != null) {
|
||||||
|
runNotifier.reset();
|
||||||
|
runNotifier.start(program);
|
||||||
|
context.go('/');
|
||||||
|
} else {
|
||||||
|
finishAndGoHome();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -135,7 +169,7 @@ class CompletePage extends ConsumerWidget {
|
|||||||
/// 操作指引示意图
|
/// 操作指引示意图
|
||||||
Widget _buildOperationGuide() {
|
Widget _buildOperationGuide() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppTheme.backgroundColor,
|
color: AppTheme.backgroundColor,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -151,7 +185,7 @@ class CompletePage extends ConsumerWidget {
|
|||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 12),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
@@ -171,15 +205,15 @@ class CompletePage extends ConsumerWidget {
|
|||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 50,
|
width: 44,
|
||||||
height: 50,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(icon, color: AppTheme.primaryColor, size: 24),
|
child: Icon(icon, color: AppTheme.primaryColor, size: 22),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'$number',
|
'$number',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -188,7 +222,7 @@ class CompletePage extends ConsumerWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import '../../../core/database/database_service.dart';
|
import '../../../core/database/database_service.dart';
|
||||||
|
import '../../../core/localization/app_localizations.dart';
|
||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
import '../../device/providers/run_state_provider.dart';
|
import '../../device/providers/run_state_provider.dart';
|
||||||
import '../../programs/pages/programs_page.dart';
|
import '../../programs/pages/programs_page.dart';
|
||||||
@@ -12,9 +13,13 @@ import '../widgets/running_control_panel.dart';
|
|||||||
import '../widgets/run_status_monitor.dart';
|
import '../widgets/run_status_monitor.dart';
|
||||||
|
|
||||||
/// 首页 - 设备控制面板 (暗色工业风格)
|
/// 首页 - 设备控制面板 (暗色工业风格)
|
||||||
/// 布局:状态栏 + 导航标签栏 + 内容区(设备控制/程序管理/系统设置)
|
/// 布局:状态栏(含标签导航) + 内容区(设备控制/程序管理/系统设置)
|
||||||
|
///
|
||||||
|
/// [initialTab] 用于从子页面(如程序详情页)跳转回首页时指定要显示的 tab
|
||||||
class HomePage extends ConsumerStatefulWidget {
|
class HomePage extends ConsumerStatefulWidget {
|
||||||
const HomePage({super.key});
|
final int initialTab;
|
||||||
|
|
||||||
|
const HomePage({super.key, this.initialTab = 0});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<HomePage> createState() => _HomePageState();
|
ConsumerState<HomePage> createState() => _HomePageState();
|
||||||
@@ -22,17 +27,19 @@ class HomePage extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _HomePageState extends ConsumerState<HomePage>
|
class _HomePageState extends ConsumerState<HomePage>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
int _currentIndex = 0;
|
late int _currentIndex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_currentIndex = widget.initialTab.clamp(0, 2);
|
||||||
DatabaseService.instance.initTestData();
|
DatabaseService.instance.initTestData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final runState = ref.watch(runStateProvider);
|
final runState = ref.watch(runStateProvider);
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
||||||
// 监听运行完成状态,自动跳转
|
// 监听运行完成状态,自动跳转
|
||||||
ref.listen<RunState>(runStateProvider, (prev, next) {
|
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(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
color: AppTheme.bgDeep,
|
color: AppTheme.bgDeep,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 状态栏
|
// 状态栏(内嵌标签导航)
|
||||||
StatusBar(
|
StatusBar(
|
||||||
isRunning: runState.status == RunStatus.running,
|
isRunning: runState.status == RunStatus.running,
|
||||||
|
tabs: tabs,
|
||||||
|
currentTabIndex: _currentIndex,
|
||||||
|
onTabChanged: (index) => setState(() => _currentIndex = index),
|
||||||
),
|
),
|
||||||
|
|
||||||
// 导航标签栏
|
|
||||||
_buildTabBar(),
|
|
||||||
|
|
||||||
// 内容区
|
// 内容区
|
||||||
Expanded(
|
Expanded(
|
||||||
child: IndexedStack(
|
child: IndexedStack(
|
||||||
@@ -74,85 +87,30 @@ 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) {
|
Widget _buildDeviceControlPage(RunState runState) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 左侧:程序列表(运行时锁定)
|
// 左侧:程序列表(运行时锁定),占 2/5 宽度
|
||||||
Opacity(
|
Expanded(
|
||||||
opacity: runState.status == RunStatus.idle ? 1.0 : 0.6,
|
flex: 2,
|
||||||
child: IgnorePointer(
|
child: Opacity(
|
||||||
ignoring: runState.status != RunStatus.idle,
|
opacity: runState.status == RunStatus.idle ? 1.0 : 0.6,
|
||||||
child: const ProgramList(),
|
child: IgnorePointer(
|
||||||
|
ignoring: runState.status != RunStatus.idle,
|
||||||
|
child: const ProgramList(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
// 右侧:运行控制区域
|
// 右侧:运行控制区域,占 3/5 宽度
|
||||||
Expanded(
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Expanded(child: RunningControlPanel()),
|
const RunningControlPanel(),
|
||||||
if (runState.status != RunStatus.idle) ...[
|
if (runState.status != RunStatus.idle) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Expanded(child: RunStatusMonitor()),
|
const Expanded(child: RunStatusMonitor()),
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class ProgramList extends ConsumerWidget {
|
|||||||
final programsNotifier = ref.read(programsProvider.notifier);
|
final programsNotifier = ref.read(programsProvider.notifier);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: 380,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppTheme.cardBg,
|
color: AppTheme.cardBg,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|||||||
@@ -25,73 +25,81 @@ class RunStatusMonitor extends ConsumerWidget {
|
|||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
border: Border.all(color: AppTheme.borderSubtle, width: 1),
|
border: Border.all(color: AppTheme.borderSubtle, width: 1),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
// 标题 + 程序名
|
children: [
|
||||||
Row(
|
// 标题 + 程序名
|
||||||
children: [
|
Row(
|
||||||
Text(
|
children: [
|
||||||
l10n?.runningMonitor ?? '运行状态监控',
|
Text(
|
||||||
style: const TextStyle(
|
l10n?.runningMonitor ?? '运行状态监控',
|
||||||
color: AppTheme.textHeading,
|
style: const TextStyle(
|
||||||
fontSize: 15,
|
color: AppTheme.textHeading,
|
||||||
fontWeight: FontWeight.w600,
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const Spacer(),
|
||||||
const Spacer(),
|
Flexible(
|
||||||
Text(
|
child: Text(
|
||||||
runState.currentProgram?.name ?? '',
|
runState.currentProgram?.name ?? '',
|
||||||
style: const TextStyle(
|
maxLines: 1,
|
||||||
color: AppTheme.accentPrimary,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontSize: 18,
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w700,
|
color: AppTheme.accentPrimary,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
// 进度信息横排 (孔位 / 步骤 / 剩余时间)
|
// 进度信息横排 (孔位 / 步骤 / 剩余时间)
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// 当前孔位
|
// 当前孔位
|
||||||
_buildInfoBlock(
|
_buildInfoBlock(
|
||||||
label: l10n?.currentHole ?? '当前孔位',
|
label: l10n?.currentHole ?? '当前孔位',
|
||||||
value: runState.currentWell ?? '--',
|
value: runState.currentWell ?? '--',
|
||||||
valueColor: AppTheme.textHeading,
|
valueColor: AppTheme.textHeading,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
// 当前步骤
|
// 当前步骤
|
||||||
_buildInfoBlock(
|
Flexible(
|
||||||
label: l10n?.currentStep ?? '当前步骤',
|
child: _buildInfoBlock(
|
||||||
value: '${l10n?.stepNo ?? '步骤'} ${runState.currentStepIndex + 1}',
|
label: l10n?.currentStep ?? '当前步骤',
|
||||||
subValue: runState.currentStep?.name ?? '--',
|
value: '${l10n?.stepNo ?? '步骤'} ${runState.currentStepIndex + 1}',
|
||||||
valueColor: AppTheme.accentInfo,
|
subValue: runState.currentStep?.name ?? '--',
|
||||||
),
|
valueColor: AppTheme.accentInfo,
|
||||||
const SizedBox(width: 20),
|
),
|
||||||
// 剩余时间
|
),
|
||||||
_buildInfoBlock(
|
const SizedBox(width: 20),
|
||||||
label: l10n?.remainingTime ?? '剩余时间',
|
// 剩余时间
|
||||||
value: runState.formattedRemainingTime,
|
_buildInfoBlock(
|
||||||
valueColor: AppTheme.textHeading,
|
label: l10n?.remainingTime ?? '剩余时间',
|
||||||
valueSize: 20,
|
value: runState.formattedRemainingTime,
|
||||||
),
|
valueColor: AppTheme.textHeading,
|
||||||
],
|
valueSize: 20,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
// 总进度条
|
// 总进度条
|
||||||
_buildProgressBar(l10n, runState),
|
_buildProgressBar(l10n, runState),
|
||||||
|
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
|
|
||||||
// 步骤参数
|
// 步骤参数
|
||||||
if (runState.currentStep != null)
|
if (runState.currentStep != null)
|
||||||
_buildStepParams(l10n, runState.currentStep!),
|
_buildStepParams(l10n, runState.currentStep!),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -190,53 +198,26 @@ class RunStatusMonitor extends ConsumerWidget {
|
|||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 8),
|
||||||
if (step.mixTime > 0)
|
Row(
|
||||||
_buildParamRow(
|
children: [
|
||||||
l10n?.speed ?? '转速',
|
_buildInfoBlock(
|
||||||
'${step.mixSpeed}',
|
label: l10n?.speed ?? '速度',
|
||||||
),
|
value: '${step.speed} ${l10n?.speedLevel ?? '档'}',
|
||||||
if (step.magnetTime > 0)
|
),
|
||||||
_buildParamRow(
|
const SizedBox(width: 20),
|
||||||
l10n?.temperature ?? '温度',
|
_buildInfoBlock(
|
||||||
'65.0 °C',
|
label: l10n?.duration ?? '持续时间',
|
||||||
),
|
value: '${step.mixTime} s',
|
||||||
_buildParamRow(
|
),
|
||||||
l10n?.duration ?? '持续时间',
|
const SizedBox(width: 20),
|
||||||
step.mixTime > 0 ? '${step.mixTime} min' : '--',
|
_buildInfoBlock(
|
||||||
),
|
label: l10n?.sampleVolume ?? '样品体积',
|
||||||
_buildParamRow(
|
value: '${step.volume} μL',
|
||||||
l10n?.sampleVolume ?? '样品体积',
|
),
|
||||||
'10.0 mL',
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 参数行
|
|
||||||
Widget _buildParamRow(String label, String value) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppTheme.textTertiary,
|
|
||||||
fontSize: 11,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppTheme.textPrimary,
|
|
||||||
fontSize: 11,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import '../../../core/localization/app_localizations.dart';
|
|||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
import '../../../shared/widgets/common_button.dart';
|
import '../../../shared/widgets/common_button.dart';
|
||||||
import '../../device/providers/run_state_provider.dart';
|
import '../../device/providers/run_state_provider.dart';
|
||||||
|
import '../../programs/models/program.dart';
|
||||||
import '../../programs/providers/programs_provider.dart';
|
import '../../programs/providers/programs_provider.dart';
|
||||||
|
|
||||||
/// 运行控制面板 - 暗色工业风格
|
/// 运行控制面板 - 暗色工业风格
|
||||||
/// 显示当前程序信息、瓷套棒状态和运行控制按钮
|
/// 显示当前程序信息和运行控制按钮
|
||||||
class RunningControlPanel extends ConsumerWidget {
|
class RunningControlPanel extends ConsumerWidget {
|
||||||
const RunningControlPanel({super.key});
|
const RunningControlPanel({super.key});
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ class RunningControlPanel extends ConsumerWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// 当前选中程序显示
|
// 当前选中程序显示
|
||||||
@@ -96,40 +98,6 @@ class RunningControlPanel extends ConsumerWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// 瓷套棒确认提示
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppTheme.cardBg,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
border: Border.all(color: AppTheme.borderSubtle, width: 1),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: AppTheme.statusStopped,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
l10n?.ceramicNotInstalled ?? '瓷套棒: 未安装 — 禁止启动',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppTheme.textSecondary,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// 控制按钮
|
// 控制按钮
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -144,26 +112,12 @@ class RunningControlPanel extends ConsumerWidget {
|
|||||||
type: ButtonType.primary,
|
type: ButtonType.primary,
|
||||||
enabled: selectedProgram != null,
|
enabled: selectedProgram != null,
|
||||||
onPressed: selectedProgram != null
|
onPressed: selectedProgram != null
|
||||||
? () => runNotifier.start(selectedProgram)
|
? () => _confirmAndStart(context, runNotifier, selectedProgram, l10n)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
// 暂停/继续按钮(待机态禁用)
|
|
||||||
Expanded(
|
|
||||||
child: SizedBox(
|
|
||||||
height: 48,
|
|
||||||
child: CommonButton(
|
|
||||||
text: l10n?.pause ?? '暂停',
|
|
||||||
icon: Icons.pause,
|
|
||||||
type: ButtonType.secondary,
|
|
||||||
enabled: false,
|
|
||||||
onPressed: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
// 停止按钮(待机态禁用)
|
// 停止按钮(待机态禁用)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -196,6 +150,7 @@ class RunningControlPanel extends ConsumerWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// 当前程序名称
|
// 当前程序名称
|
||||||
@@ -237,7 +192,7 @@ class RunningControlPanel extends ConsumerWidget {
|
|||||||
// 控制按钮
|
// 控制按钮
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// 开始/继续按钮
|
// 暂停/继续按钮(运行中切换)
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -245,27 +200,18 @@ class RunningControlPanel extends ConsumerWidget {
|
|||||||
child: CommonButton(
|
child: CommonButton(
|
||||||
text: runState.status == RunStatus.paused
|
text: runState.status == RunStatus.paused
|
||||||
? (l10n?.continue_ ?? '继续')
|
? (l10n?.continue_ ?? '继续')
|
||||||
: (l10n?.run ?? '运行'),
|
: (l10n?.pause ?? '暂停'),
|
||||||
icon: runState.status == RunStatus.paused
|
icon: runState.status == RunStatus.paused
|
||||||
? Icons.play_arrow
|
? Icons.play_arrow
|
||||||
: Icons.play_arrow,
|
: Icons.pause,
|
||||||
type: ButtonType.primary,
|
type: ButtonType.primary,
|
||||||
onPressed: () => runNotifier.resume(),
|
onPressed: () {
|
||||||
),
|
if (runState.status == RunStatus.paused) {
|
||||||
),
|
runNotifier.resume();
|
||||||
),
|
} else {
|
||||||
const SizedBox(width: 12),
|
runNotifier.pause();
|
||||||
// 暂停按钮
|
}
|
||||||
Expanded(
|
},
|
||||||
child: SizedBox(
|
|
||||||
height: 48,
|
|
||||||
child: CommonButton(
|
|
||||||
text: l10n?.pause ?? '暂停',
|
|
||||||
icon: Icons.pause,
|
|
||||||
type: ButtonType.warning,
|
|
||||||
onPressed: runState.status == RunStatus.paused
|
|
||||||
? null
|
|
||||||
: () => runNotifier.pause(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -284,37 +230,48 @@ class RunningControlPanel extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
/// 显示瓷套棒放置确认对话框,确认后启动程序
|
||||||
|
void _confirmAndStart(
|
||||||
// 状态指示
|
BuildContext context,
|
||||||
Row(
|
RunStateNotifier runNotifier,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Program program,
|
||||||
children: [
|
AppLocalizations? l10n,
|
||||||
Container(
|
) {
|
||||||
width: 8,
|
showDialog(
|
||||||
height: 8,
|
context: context,
|
||||||
decoration: BoxDecoration(
|
builder: (context) => AlertDialog(
|
||||||
shape: BoxShape.circle,
|
backgroundColor: AppTheme.cardBg,
|
||||||
color: runState.status == RunStatus.paused
|
title: Text(
|
||||||
? AppTheme.accentWarning
|
l10n?.ceramicSleeveConfirm ?? '运行前请确认已安装瓷套棒',
|
||||||
: AppTheme.statusRunning,
|
style: const TextStyle(color: AppTheme.textHeading),
|
||||||
),
|
),
|
||||||
),
|
content: Text(
|
||||||
const SizedBox(width: 8),
|
l10n?.ceramicSleeveConfirmMessage ?? '请确认已放置瓷套棒后再启动程序。',
|
||||||
Text(
|
style: const TextStyle(color: AppTheme.textPrimary),
|
||||||
runState.status == RunStatus.paused
|
),
|
||||||
? (l10n?.paused ?? '已暂停')
|
actions: [
|
||||||
: (l10n?.running ?? '运行中'),
|
TextButton(
|
||||||
style: TextStyle(
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
color: runState.status == RunStatus.paused
|
child: Text(
|
||||||
? AppTheme.accentWarning
|
l10n?.cancel ?? '取消',
|
||||||
: AppTheme.statusRunning,
|
style: const TextStyle(color: AppTheme.textSecondary),
|
||||||
fontWeight: FontWeight.w500,
|
),
|
||||||
fontSize: 12,
|
),
|
||||||
),
|
ElevatedButton(
|
||||||
),
|
style: ElevatedButton.styleFrom(
|
||||||
],
|
backgroundColor: AppTheme.accentPrimary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
runNotifier.start(program);
|
||||||
|
},
|
||||||
|
child: Text(l10n?.confirm ?? '确认'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,17 +5,35 @@ import '../../../core/localization/app_localizations.dart';
|
|||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
import '../../../shared/widgets/status_indicator.dart';
|
import '../../../shared/widgets/status_indicator.dart';
|
||||||
import '../../device/providers/device_info_provider.dart';
|
import '../../device/providers/device_info_provider.dart';
|
||||||
|
import '../../device/providers/serial_provider.dart';
|
||||||
|
import '../../device/services/serial_port_service.dart';
|
||||||
|
|
||||||
|
/// 状态栏标签项数据
|
||||||
|
class StatusBarTab {
|
||||||
|
final IconData icon;
|
||||||
|
final String label;
|
||||||
|
const StatusBarTab({required this.icon, required this.label});
|
||||||
|
}
|
||||||
|
|
||||||
/// 状态栏组件 - 明亮工业风格
|
/// 状态栏组件 - 明亮工业风格
|
||||||
/// 显示设备名称、实时时钟、系统状态、照明控制
|
/// 显示: 设备名称 | 导航标签 | 灯按钮 | 状态指示器 | 实时时钟
|
||||||
|
///
|
||||||
|
/// [tabs] 不为空时,会在设备名右侧渲染胶囊样式的导航标签按钮组,
|
||||||
|
/// 由父组件通过 [currentTabIndex] 和 [onTabChanged] 控制切换。
|
||||||
class StatusBar extends ConsumerStatefulWidget {
|
class StatusBar extends ConsumerStatefulWidget {
|
||||||
final bool isRunning;
|
final bool isRunning;
|
||||||
final VoidCallback? onLightToggle;
|
final VoidCallback? onLightToggle;
|
||||||
|
final List<StatusBarTab> tabs;
|
||||||
|
final int currentTabIndex;
|
||||||
|
final ValueChanged<int>? onTabChanged;
|
||||||
|
|
||||||
const StatusBar({
|
const StatusBar({
|
||||||
super.key,
|
super.key,
|
||||||
this.isRunning = false,
|
this.isRunning = false,
|
||||||
this.onLightToggle,
|
this.onLightToggle,
|
||||||
|
this.tabs = const [],
|
||||||
|
this.currentTabIndex = 0,
|
||||||
|
this.onTabChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -39,6 +57,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 更新当前时间显示
|
||||||
void _updateTime() {
|
void _updateTime() {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
_currentTime =
|
_currentTime =
|
||||||
@@ -49,6 +68,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
|||||||
|
|
||||||
String _twoDigits(int n) => n.toString().padLeft(2, '0');
|
String _twoDigits(int n) => n.toString().padLeft(2, '0');
|
||||||
|
|
||||||
|
/// 切换照明开关
|
||||||
Future<void> _onLightTap() async {
|
Future<void> _onLightTap() async {
|
||||||
widget.onLightToggle?.call();
|
widget.onLightToggle?.call();
|
||||||
await ref.read(deviceInfoProvider.notifier).toggleLight();
|
await ref.read(deviceInfoProvider.notifier).toggleLight();
|
||||||
@@ -58,6 +78,7 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final deviceInfo = ref.watch(deviceInfoProvider);
|
final deviceInfo = ref.watch(deviceInfoProvider);
|
||||||
|
final serialState = ref.watch(serialConnectionStateProvider);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -74,27 +95,19 @@ class _StatusBarState extends ConsumerState<StatusBar> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
const Icon(Icons.precision_manufacturing, color: Colors.white, size: 22),
|
||||||
mainAxisSize: MainAxisSize.min,
|
if (widget.tabs.isNotEmpty) ...[
|
||||||
children: [
|
const SizedBox(width: 32),
|
||||||
Icon(Icons.precision_manufacturing, color: Colors.white, size: 22),
|
_buildNavTabs(),
|
||||||
const SizedBox(width: 10),
|
],
|
||||||
Text(
|
|
||||||
l10n?.deviceName ?? '污水毒品前处理一体机',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 17,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
_LightToggleButton(
|
_LightToggleButton(
|
||||||
isOn: deviceInfo.lightingOn,
|
isOn: deviceInfo.lightingOn,
|
||||||
onTap: _onLightTap,
|
onTap: _onLightTap,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
|
_SerialConnectionIndicator(state: serialState),
|
||||||
|
const SizedBox(width: 20),
|
||||||
StatusIndicator(
|
StatusIndicator(
|
||||||
text: widget.isRunning
|
text: widget.isRunning
|
||||||
? (l10n?.running ?? '运行中')
|
? (l10n?.running ?? '运行中')
|
||||||
@@ -117,6 +130,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 {
|
class _LightToggleButton extends StatelessWidget {
|
||||||
@@ -154,3 +253,49 @@ class _LightToggleButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 串口连接状态指示器
|
||||||
|
///
|
||||||
|
/// 位于「设备运行状态」之前,反映当前 USB 串口的连接情况。
|
||||||
|
class _SerialConnectionIndicator extends StatelessWidget {
|
||||||
|
final SerialConnectionState state;
|
||||||
|
const _SerialConnectionIndicator({required this.state});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final connected = state == SerialConnectionState.connected;
|
||||||
|
final connecting = state == SerialConnectionState.connecting;
|
||||||
|
final text = connected
|
||||||
|
? '已连接'
|
||||||
|
: connecting
|
||||||
|
? '连接中'
|
||||||
|
: '未连接';
|
||||||
|
final color = connected
|
||||||
|
? AppTheme.statusRunning
|
||||||
|
: connecting
|
||||||
|
? AppTheme.statusPaused
|
||||||
|
: AppTheme.statusStopped;
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import '../../../shared/widgets/common_button.dart';
|
|||||||
import '../providers/steps_provider.dart';
|
import '../providers/steps_provider.dart';
|
||||||
import '../widgets/step_list.dart';
|
import '../widgets/step_list.dart';
|
||||||
import '../widgets/step_form.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';
|
import '../../programs/providers/programs_provider.dart';
|
||||||
|
|
||||||
/// 程序详情页面
|
/// 程序详情页面
|
||||||
/// 左侧步骤列表 + 右侧参数表单
|
/// 布局:顶部状态栏(含导航tab) + 子工具栏(返回/程序名/保存) + 左侧步骤列表 + 右侧参数表单
|
||||||
class ProgramDetailPage extends ConsumerStatefulWidget {
|
class ProgramDetailPage extends ConsumerStatefulWidget {
|
||||||
final String programId;
|
final String programId;
|
||||||
|
|
||||||
@@ -35,61 +37,31 @@ class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
|
|||||||
final programsState = ref.watch(programsProvider);
|
final programsState = ref.watch(programsProvider);
|
||||||
final program = programsState.programs.where((p) => p.id == _programIdInt).firstOrNull;
|
final program = programsState.programs.where((p) => p.id == _programIdInt).firstOrNull;
|
||||||
final stepsState = ref.watch(stepsProvider(_programIdInt));
|
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(
|
return Scaffold(
|
||||||
body: Container(
|
body: Container(
|
||||||
color: AppTheme.backgroundColor,
|
color: AppTheme.backgroundColor,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 顶部导航栏
|
// 顶部状态栏(含导航tab),tab点击跳回首页对应 tab
|
||||||
Container(
|
StatusBar(
|
||||||
height: 60,
|
isRunning: runState.status == RunStatus.running,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
tabs: tabs,
|
||||||
decoration: BoxDecoration(
|
currentTabIndex: 1,
|
||||||
color: Colors.white,
|
onTabChanged: (index) => context.go('/?tab=$index'),
|
||||||
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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 子工具栏:返回按钮 + 程序名 + 保存按钮
|
||||||
|
_buildSubToolbar(context, l10n, program?.name),
|
||||||
|
|
||||||
// 主内容区域
|
// 主内容区域
|
||||||
Expanded(
|
Expanded(
|
||||||
child: stepsState.isLoading
|
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) {
|
void _showAddStepDialog(BuildContext context, WidgetRef ref) {
|
||||||
showDialog(
|
showDialog(
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ class _StepFormState extends State<StepForm> {
|
|||||||
late TextEditingController _blowTimeController;
|
late TextEditingController _blowTimeController;
|
||||||
|
|
||||||
String _position = 'A1';
|
String _position = 'A1';
|
||||||
String _mixSpeed = '中速';
|
int _speed = 5;
|
||||||
String _blowSpeed = '中速';
|
|
||||||
int _needleSpeed = 5;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -48,9 +46,7 @@ class _StepFormState extends State<StepForm> {
|
|||||||
_blowTimeController = TextEditingController(text: '${widget.step?.blowTime ?? 0}');
|
_blowTimeController = TextEditingController(text: '${widget.step?.blowTime ?? 0}');
|
||||||
|
|
||||||
_position = widget.step?.position ?? 'A1';
|
_position = widget.step?.position ?? 'A1';
|
||||||
_mixSpeed = widget.step?.mixSpeed ?? '中速';
|
_speed = widget.step?.speed ?? 5;
|
||||||
_blowSpeed = widget.step?.blowSpeed ?? '中速';
|
|
||||||
_needleSpeed = widget.step?.needleSpeed ?? 5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -179,53 +175,22 @@ class _StepFormState extends State<StepForm> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('${l10n?.needleSpeed ?? '下针速度'}: $_needleSpeed 档', style: TextStyle(color: AppTheme.textPrimary)),
|
Text(
|
||||||
|
'${l10n?.speed ?? '速度'}: $_speed 档',
|
||||||
|
style: TextStyle(color: AppTheme.textPrimary),
|
||||||
|
),
|
||||||
Slider(
|
Slider(
|
||||||
value: _needleSpeed.toDouble(),
|
value: _speed.toDouble(),
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 10,
|
max: 10,
|
||||||
divisions: 9,
|
divisions: 9,
|
||||||
activeColor: AppTheme.primaryColor,
|
activeColor: AppTheme.primaryColor,
|
||||||
onChanged: (value) {
|
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,
|
mixTime: int.tryParse(_mixTimeController.text) ?? 0,
|
||||||
magnetTime: int.tryParse(_magnetTimeController.text) ?? 0,
|
magnetTime: int.tryParse(_magnetTimeController.text) ?? 0,
|
||||||
volume: int.tryParse(_volumeController.text) ?? 0,
|
volume: int.tryParse(_volumeController.text) ?? 0,
|
||||||
mixSpeed: _mixSpeed,
|
|
||||||
blowSpeed: _blowSpeed,
|
|
||||||
blowTime: int.tryParse(_blowTimeController.text) ?? 0,
|
blowTime: int.tryParse(_blowTimeController.text) ?? 0,
|
||||||
needleSpeed: _needleSpeed,
|
speed: _speed,
|
||||||
);
|
);
|
||||||
|
|
||||||
widget.onSave(step);
|
widget.onSave(step);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ class Program {
|
|||||||
final String name;
|
final String name;
|
||||||
final String createdAt;
|
final String createdAt;
|
||||||
final int status; // 1: 启用, 0: 停用
|
final int status; // 1: 启用, 0: 停用
|
||||||
|
final int temperature;
|
||||||
|
final int airflowTime;
|
||||||
|
|
||||||
Program({
|
Program({
|
||||||
this.id,
|
this.id,
|
||||||
@@ -12,6 +14,8 @@ class Program {
|
|||||||
required this.name,
|
required this.name,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
this.status = 1,
|
this.status = 1,
|
||||||
|
this.temperature = 50,
|
||||||
|
this.airflowTime = 60,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
@@ -21,6 +25,8 @@ class Program {
|
|||||||
'name': name,
|
'name': name,
|
||||||
'created_at': createdAt,
|
'created_at': createdAt,
|
||||||
'status': status,
|
'status': status,
|
||||||
|
'temperature': temperature,
|
||||||
|
'airflow_time': airflowTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +37,8 @@ class Program {
|
|||||||
name: map['name'] as String,
|
name: map['name'] as String,
|
||||||
createdAt: map['created_at'] as String,
|
createdAt: map['created_at'] as String,
|
||||||
status: map['status'] as int? ?? 1,
|
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? name,
|
||||||
String? createdAt,
|
String? createdAt,
|
||||||
int? status,
|
int? status,
|
||||||
|
int? temperature,
|
||||||
|
int? airflowTime,
|
||||||
}) {
|
}) {
|
||||||
return Program(
|
return Program(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -47,6 +57,8 @@ class Program {
|
|||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
|
temperature: temperature ?? this.temperature,
|
||||||
|
airflowTime: airflowTime ?? this.airflowTime,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,10 +8,8 @@ class Step {
|
|||||||
final int mixTime;
|
final int mixTime;
|
||||||
final int magnetTime;
|
final int magnetTime;
|
||||||
final int volume;
|
final int volume;
|
||||||
final String mixSpeed;
|
|
||||||
final String blowSpeed;
|
|
||||||
final int blowTime;
|
final int blowTime;
|
||||||
final int needleSpeed;
|
final int speed;
|
||||||
|
|
||||||
Step({
|
Step({
|
||||||
this.id,
|
this.id,
|
||||||
@@ -22,10 +20,8 @@ class Step {
|
|||||||
this.mixTime = 0,
|
this.mixTime = 0,
|
||||||
this.magnetTime = 0,
|
this.magnetTime = 0,
|
||||||
this.volume = 0,
|
this.volume = 0,
|
||||||
this.mixSpeed = '中速',
|
|
||||||
this.blowSpeed = '中速',
|
|
||||||
this.blowTime = 0,
|
this.blowTime = 0,
|
||||||
this.needleSpeed = 5,
|
this.speed = 5,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
@@ -38,10 +34,8 @@ class Step {
|
|||||||
'mix_time': mixTime,
|
'mix_time': mixTime,
|
||||||
'magnet_time': magnetTime,
|
'magnet_time': magnetTime,
|
||||||
'volume': volume,
|
'volume': volume,
|
||||||
'mix_speed': mixSpeed,
|
|
||||||
'blow_speed': blowSpeed,
|
|
||||||
'blow_time': blowTime,
|
'blow_time': blowTime,
|
||||||
'needle_speed': needleSpeed,
|
'speed': speed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +49,8 @@ class Step {
|
|||||||
mixTime: map['mix_time'] as int? ?? 0,
|
mixTime: map['mix_time'] as int? ?? 0,
|
||||||
magnetTime: map['magnet_time'] as int? ?? 0,
|
magnetTime: map['magnet_time'] as int? ?? 0,
|
||||||
volume: map['volume'] 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,
|
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? mixTime,
|
||||||
int? magnetTime,
|
int? magnetTime,
|
||||||
int? volume,
|
int? volume,
|
||||||
String? mixSpeed,
|
|
||||||
String? blowSpeed,
|
|
||||||
int? blowTime,
|
int? blowTime,
|
||||||
int? needleSpeed,
|
int? speed,
|
||||||
}) {
|
}) {
|
||||||
return Step(
|
return Step(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -85,10 +75,8 @@ class Step {
|
|||||||
mixTime: mixTime ?? this.mixTime,
|
mixTime: mixTime ?? this.mixTime,
|
||||||
magnetTime: magnetTime ?? this.magnetTime,
|
magnetTime: magnetTime ?? this.magnetTime,
|
||||||
volume: volume ?? this.volume,
|
volume: volume ?? this.volume,
|
||||||
mixSpeed: mixSpeed ?? this.mixSpeed,
|
|
||||||
blowSpeed: blowSpeed ?? this.blowSpeed,
|
|
||||||
blowTime: blowTime ?? this.blowTime,
|
blowTime: blowTime ?? this.blowTime,
|
||||||
needleSpeed: needleSpeed ?? this.needleSpeed,
|
speed: speed ?? this.speed,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,11 +5,13 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import '../../../core/localization/app_localizations.dart';
|
import '../../../core/localization/app_localizations.dart';
|
||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
|
import '../../../shared/services/toast_service.dart';
|
||||||
import '../../../shared/widgets/common_button.dart';
|
import '../../../shared/widgets/common_button.dart';
|
||||||
import '../models/program.dart';
|
import '../models/program.dart';
|
||||||
import '../providers/programs_provider.dart';
|
import '../providers/programs_provider.dart';
|
||||||
import '../widgets/program_form_dialog.dart';
|
import '../widgets/program_form_dialog.dart';
|
||||||
import '../services/program_import_service.dart';
|
import '../services/excel_import_service.dart';
|
||||||
|
import '../services/excel_template_service.dart';
|
||||||
|
|
||||||
/// 程序管理页面
|
/// 程序管理页面
|
||||||
class ProgramsPage extends ConsumerStatefulWidget {
|
class ProgramsPage extends ConsumerStatefulWidget {
|
||||||
@@ -48,12 +50,6 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// 返回按钮
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
onPressed: () => context.go('/'),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Text(
|
Text(
|
||||||
l10n?.programs ?? '程序管理',
|
l10n?.programs ?? '程序管理',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -70,6 +66,14 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
|||||||
onPressed: () => _showAddDialog(context, ref),
|
onPressed: () => _showAddDialog(context, ref),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
// 下载模板按钮
|
||||||
|
CommonButton(
|
||||||
|
text: l10n?.downloadTemplate ?? '下载模板',
|
||||||
|
icon: Icons.file_download,
|
||||||
|
type: ButtonType.secondary,
|
||||||
|
onPressed: () => _downloadTemplate(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
// 导入按钮
|
// 导入按钮
|
||||||
CommonButton(
|
CommonButton(
|
||||||
text: l10n?.importProgram ?? '导入',
|
text: l10n?.importProgram ?? '导入',
|
||||||
@@ -413,7 +417,7 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
|||||||
// 选择文件
|
// 选择文件
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: ['json'],
|
allowedExtensions: ['xlsx'],
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -423,38 +427,39 @@ class _ProgramsPageState extends ConsumerState<ProgramsPage> {
|
|||||||
|
|
||||||
final file = result.files.first;
|
final file = result.files.first;
|
||||||
if (file.path == null) {
|
if (file.path == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (!context.mounted) return;
|
||||||
SnackBar(
|
ToastService.showError(context, '无法读取文件');
|
||||||
content: Text('无法读取文件'),
|
|
||||||
backgroundColor: AppTheme.errorColor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取文件内容
|
// 解析并写入数据库
|
||||||
final jsonContent = await File(file.path!).readAsString();
|
final importedCount =
|
||||||
|
await ExcelImportService.instance.importFromExcel(File(file.path!));
|
||||||
// 导入程序
|
|
||||||
final importedCount = await ProgramImportService.instance.importFromJson(jsonContent);
|
|
||||||
|
|
||||||
// 刷新程序列表
|
// 刷新程序列表
|
||||||
ref.read(programsProvider.notifier).loadPrograms();
|
ref.read(programsProvider.notifier).loadPrograms();
|
||||||
|
|
||||||
// 显示结果
|
if (!context.mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (importedCount > 0) {
|
||||||
SnackBar(
|
ToastService.showSuccess(context, '成功处理 $importedCount 个程序');
|
||||||
content: Text('成功导入 $importedCount 个程序'),
|
} else {
|
||||||
backgroundColor: importedCount > 0 ? AppTheme.successColor : AppTheme.warningColor,
|
ToastService.showWarning(context, 'Excel 中无有效程序数据');
|
||||||
),
|
}
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (!context.mounted) return;
|
||||||
SnackBar(
|
ToastService.showError(context, '导入失败: ${e.toString()}');
|
||||||
content: Text('导入失败: ${e.toString()}'),
|
}
|
||||||
backgroundColor: AppTheme.errorColor,
|
}
|
||||||
),
|
|
||||||
);
|
/// 下载 Excel 模板
|
||||||
|
Future<void> _downloadTemplate(BuildContext context) async {
|
||||||
|
try {
|
||||||
|
final path = await ExcelTemplateService.instance.generateTemplate();
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ToastService.showSuccess(context, '模板已保存: $path');
|
||||||
|
} catch (e) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ToastService.showError(context, '生成模板失败: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
237
lib/features/programs/services/excel_import_service.dart
Normal file
237
lib/features/programs/services/excel_import_service.dart
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:excel/excel.dart';
|
||||||
|
|
||||||
|
import '../models/program.dart';
|
||||||
|
import '../models/step.dart';
|
||||||
|
import '../services/program_service.dart';
|
||||||
|
import 'excel_template_service.dart';
|
||||||
|
|
||||||
|
/// Excel 导入服务
|
||||||
|
///
|
||||||
|
/// 解析用户填好的 .xlsx 并通过 [ProgramService] 写入数据库。
|
||||||
|
/// 模板结构与 [ExcelTemplateService] 一致:Programs + Steps 双表,
|
||||||
|
/// 通过 program_code 关联。
|
||||||
|
class ExcelImportService {
|
||||||
|
static final ExcelImportService instance = ExcelImportService._internal();
|
||||||
|
final ProgramService _programService = ProgramService.instance;
|
||||||
|
|
||||||
|
ExcelImportService._internal();
|
||||||
|
|
||||||
|
/// 从 .xlsx 文件导入程序,返回成功处理的程序数量(新建 + 覆盖)
|
||||||
|
///
|
||||||
|
/// 行为:
|
||||||
|
/// - code 不存在:新建程序 + 写入步骤
|
||||||
|
/// - code 已存在:全量覆盖程序字段,并删除旧步骤后写入新步骤
|
||||||
|
Future<int> importFromExcel(File file) async {
|
||||||
|
final bytes = await file.readAsBytes();
|
||||||
|
final excel = Excel.decodeBytes(bytes);
|
||||||
|
|
||||||
|
final programsSheet = _findSheet(excel, ExcelTemplateService.sheetPrograms);
|
||||||
|
if (programsSheet == null) {
|
||||||
|
throw const ExcelImportException('缺少 Programs 工作表');
|
||||||
|
}
|
||||||
|
|
||||||
|
final programs = _parsePrograms(programsSheet);
|
||||||
|
if (programs.isEmpty) {
|
||||||
|
throw const ExcelImportException('Programs 表无有效数据');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已有 code → 完整 Program(用于覆盖时取 id)
|
||||||
|
final existing = await _programService.getAllPrograms();
|
||||||
|
final existingByCode = <String, Program>{
|
||||||
|
for (final p in existing) p.code: p,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析步骤
|
||||||
|
final stepsSheet = _findSheet(excel, ExcelTemplateService.sheetSteps);
|
||||||
|
final stepsByCode = stepsSheet == null
|
||||||
|
? <String, List<_RawStep>>{}
|
||||||
|
: _parseSteps(stepsSheet);
|
||||||
|
|
||||||
|
int processedCount = 0;
|
||||||
|
for (final program in programs) {
|
||||||
|
try {
|
||||||
|
final existingProgram = existingByCode[program.code];
|
||||||
|
final int programId;
|
||||||
|
if (existingProgram == null) {
|
||||||
|
programId = await _programService.addProgram(program);
|
||||||
|
} else {
|
||||||
|
// 全量覆盖:保留 id,其余字段取 Excel
|
||||||
|
programId = existingProgram.id!;
|
||||||
|
await _programService.updateProgram(
|
||||||
|
existingProgram.copyWith(
|
||||||
|
name: program.name,
|
||||||
|
temperature: program.temperature,
|
||||||
|
airflowTime: program.airflowTime,
|
||||||
|
status: program.status,
|
||||||
|
createdAt: program.createdAt,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// 清空旧步骤
|
||||||
|
final oldSteps =
|
||||||
|
await _programService.getStepsByProgramId(programId);
|
||||||
|
if (oldSteps.isNotEmpty) {
|
||||||
|
await _programService.deleteSteps(
|
||||||
|
oldSteps.map((s) => s.id!).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入新步骤(按 step_no 排序后重新编号为 1..N)
|
||||||
|
final rawSteps = stepsByCode[program.code] ?? const <_RawStep>[];
|
||||||
|
rawSteps.sort((a, b) => a.stepNo.compareTo(b.stepNo));
|
||||||
|
for (var i = 0; i < rawSteps.length; i++) {
|
||||||
|
final raw = rawSteps[i];
|
||||||
|
final step = Step(
|
||||||
|
programId: programId,
|
||||||
|
stepNo: i + 1,
|
||||||
|
position: raw.position,
|
||||||
|
name: raw.name,
|
||||||
|
mixTime: raw.mixTime,
|
||||||
|
magnetTime: raw.magnetTime,
|
||||||
|
volume: raw.volume,
|
||||||
|
blowTime: raw.blowTime,
|
||||||
|
speed: raw.speed,
|
||||||
|
);
|
||||||
|
await _programService.addStep(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
processedCount++;
|
||||||
|
} catch (_) {
|
||||||
|
// 单条失败不影响其他程序
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sheet? _findSheet(Excel excel, String name) {
|
||||||
|
for (final entry in excel.tables.entries) {
|
||||||
|
if (entry.key == name) return entry.value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 Programs 表
|
||||||
|
/// 返回不含 id 的 Program 列表(createdAt 由调用方填充)
|
||||||
|
List<Program> _parsePrograms(Sheet sheet) {
|
||||||
|
final rows = _dataRows(sheet);
|
||||||
|
final results = <Program>[];
|
||||||
|
|
||||||
|
for (final cells in rows) {
|
||||||
|
final code = _readString(cells, 0);
|
||||||
|
final name = _readString(cells, 1);
|
||||||
|
if (code.isEmpty || name.isEmpty) continue;
|
||||||
|
|
||||||
|
results.add(
|
||||||
|
Program(
|
||||||
|
code: code,
|
||||||
|
name: name,
|
||||||
|
createdAt: DateTime.now().toString().split('.').first,
|
||||||
|
status: _readInt(cells, 4, 1) == 0 ? 0 : 1,
|
||||||
|
temperature: _readInt(cells, 2, 50),
|
||||||
|
airflowTime: _readInt(cells, 3, 60),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 Steps 表
|
||||||
|
/// 以 program_code 为 key 分组
|
||||||
|
Map<String, List<_RawStep>> _parseSteps(Sheet sheet) {
|
||||||
|
final rows = _dataRows(sheet);
|
||||||
|
final map = <String, List<_RawStep>>{};
|
||||||
|
|
||||||
|
for (final cells in rows) {
|
||||||
|
final programCode = _readString(cells, 0);
|
||||||
|
if (programCode.isEmpty) continue;
|
||||||
|
final position = _readString(cells, 2, 'A1');
|
||||||
|
final name = _readString(cells, 3);
|
||||||
|
if (name.isEmpty) continue;
|
||||||
|
|
||||||
|
final raw = _RawStep(
|
||||||
|
stepNo: _readInt(cells, 1, 0),
|
||||||
|
position: position,
|
||||||
|
name: name,
|
||||||
|
mixTime: _readInt(cells, 4, 0),
|
||||||
|
magnetTime: _readInt(cells, 5, 0),
|
||||||
|
volume: _readInt(cells, 6, 0),
|
||||||
|
blowTime: _readInt(cells, 7, 0),
|
||||||
|
speed: _readInt(cells, 8, 5),
|
||||||
|
);
|
||||||
|
map.putIfAbsent(programCode, () => []).add(raw);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 跳过表头和说明行(首行表头,后续以"说明"开头的视为说明)
|
||||||
|
List<List<Object?>> _dataRows(Sheet sheet) {
|
||||||
|
final result = <List<Object?>>[];
|
||||||
|
for (var i = 1; i < sheet.rows.length; i++) {
|
||||||
|
final row = sheet.rows[i];
|
||||||
|
// 跳过空行
|
||||||
|
if (row.every((c) => c == null || c.value == null)) continue;
|
||||||
|
final firstCell = _asTrimmed(row.first?.value);
|
||||||
|
if (firstCell.startsWith('说明')) continue;
|
||||||
|
result.add(row.map((c) => c?.value).toList(growable: false));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _readString(List<Object?> row, int index, [String fallback = '']) {
|
||||||
|
if (index >= row.length) return fallback;
|
||||||
|
final v = row[index];
|
||||||
|
if (v == null) return fallback;
|
||||||
|
final s = v.toString().trim();
|
||||||
|
return s.isEmpty ? fallback : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _readInt(List<Object?> row, int index, int fallback) {
|
||||||
|
if (index >= row.length) return fallback;
|
||||||
|
final v = row[index];
|
||||||
|
if (v == null) return fallback;
|
||||||
|
if (v is int) return v;
|
||||||
|
if (v is double) return v.toInt();
|
||||||
|
final parsed = int.tryParse(v.toString().trim());
|
||||||
|
return parsed ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _asTrimmed(Object? v) {
|
||||||
|
if (v == null) return '';
|
||||||
|
return v.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RawStep {
|
||||||
|
final int stepNo;
|
||||||
|
final String position;
|
||||||
|
final String name;
|
||||||
|
final int mixTime;
|
||||||
|
final int magnetTime;
|
||||||
|
final int volume;
|
||||||
|
final int blowTime;
|
||||||
|
final int speed;
|
||||||
|
|
||||||
|
_RawStep({
|
||||||
|
required this.stepNo,
|
||||||
|
required this.position,
|
||||||
|
required this.name,
|
||||||
|
required this.mixTime,
|
||||||
|
required this.magnetTime,
|
||||||
|
required this.volume,
|
||||||
|
required this.blowTime,
|
||||||
|
required this.speed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Excel 导入异常
|
||||||
|
class ExcelImportException implements Exception {
|
||||||
|
final String message;
|
||||||
|
const ExcelImportException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
161
lib/features/programs/services/excel_template_service.dart
Normal file
161
lib/features/programs/services/excel_template_service.dart
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:excel/excel.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
/// Excel 模板服务
|
||||||
|
///
|
||||||
|
/// 生成 .xlsx 模板用于程序/步骤批量导入。
|
||||||
|
/// 模板包含两张表:
|
||||||
|
/// - Programs:程序基础信息
|
||||||
|
/// - Steps:步骤参数(通过 program_code 与 Programs 关联)
|
||||||
|
class ExcelTemplateService {
|
||||||
|
static final ExcelTemplateService instance = ExcelTemplateService._internal();
|
||||||
|
|
||||||
|
ExcelTemplateService._internal();
|
||||||
|
|
||||||
|
/// 工作表名称
|
||||||
|
static const String sheetPrograms = 'Programs';
|
||||||
|
static const String sheetSteps = 'Steps';
|
||||||
|
|
||||||
|
/// 模板文件名
|
||||||
|
static const String templateFilename = 'program_template.xlsx';
|
||||||
|
|
||||||
|
/// Android 端写入公共 Downloads 的 MethodChannel(对应 MainActivity.kt)
|
||||||
|
static const MethodChannel _downloadsChannel =
|
||||||
|
MethodChannel('com.xiarui.kuaishai2/downloads');
|
||||||
|
|
||||||
|
/// 生成模板并保存到下载目录,返回可显示的路径字符串
|
||||||
|
///
|
||||||
|
/// - Android:通过 MediaStore 写入公共 Downloads (`/storage/emulated/0/Download/`)
|
||||||
|
/// - 其它平台:使用 [getDownloadsDirectory],不可用时回退到应用文档目录
|
||||||
|
Future<String> generateTemplate() async {
|
||||||
|
final excel = Excel.createExcel();
|
||||||
|
final bytes = _buildTemplateBytes(excel);
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final saved = await _downloadsChannel.invokeMethod<String>(
|
||||||
|
'saveToDownloads',
|
||||||
|
<String, dynamic>{
|
||||||
|
'filename': templateFilename,
|
||||||
|
'bytes': bytes,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (saved == null) {
|
||||||
|
throw StateError('保存到 Downloads 失败:未返回路径');
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dir =
|
||||||
|
await getDownloadsDirectory() ?? await getApplicationDocumentsDirectory();
|
||||||
|
final file = File('${dir.path}/$templateFilename');
|
||||||
|
await file.writeAsBytes(bytes, flush: true);
|
||||||
|
return file.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建模板字节流(可在测试中直接调用)
|
||||||
|
List<int> _buildTemplateBytes(Excel excel) {
|
||||||
|
// 默认创建的第一个 sheet 重命名
|
||||||
|
final defaultSheet = excel.getDefaultSheet();
|
||||||
|
if (defaultSheet != null && defaultSheet != sheetPrograms) {
|
||||||
|
excel.rename(defaultSheet, sheetPrograms);
|
||||||
|
}
|
||||||
|
|
||||||
|
_writeProgramsSheet(excel);
|
||||||
|
_writeStepsSheet(excel);
|
||||||
|
return excel.encode()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeProgramsSheet(Excel excel) {
|
||||||
|
final sheet = excel[sheetPrograms];
|
||||||
|
final headerStyle = CellStyle(
|
||||||
|
bold: true,
|
||||||
|
backgroundColorHex: ExcelColor.fromHexString('FFD9E1F2'),
|
||||||
|
horizontalAlign: HorizontalAlign.Center,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 表头
|
||||||
|
final headers = ['code', 'name', 'temperature', 'airflowTime', 'status'];
|
||||||
|
sheet.appendRow(headers.map((h) => TextCellValue(h)).toList());
|
||||||
|
for (var i = 0; i < headers.length; i++) {
|
||||||
|
sheet
|
||||||
|
.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0))
|
||||||
|
.cellStyle = headerStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例行
|
||||||
|
final sample1 = ['P001', '示例程序-标准流程', 50, 60, 1];
|
||||||
|
final sample2 = ['P002', '示例程序-快速流程', 45, 30, 1];
|
||||||
|
sheet.appendRow(sample1.map((v) => TextCellValue(v.toString())).toList());
|
||||||
|
sheet.appendRow(sample2.map((v) => TextCellValue(v.toString())).toList());
|
||||||
|
|
||||||
|
// 说明行(用前缀标记,避免被解析)
|
||||||
|
final note = '说明:code 必填且唯一;name 必填;temperature/airflowTime 整数;status:1启用 0停用';
|
||||||
|
sheet.appendRow([TextCellValue(note)]);
|
||||||
|
|
||||||
|
// 列宽
|
||||||
|
sheet.setColumnWidth(0, 14);
|
||||||
|
sheet.setColumnWidth(1, 26);
|
||||||
|
sheet.setColumnWidth(2, 14);
|
||||||
|
sheet.setColumnWidth(3, 14);
|
||||||
|
sheet.setColumnWidth(4, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeStepsSheet(Excel excel) {
|
||||||
|
final sheet = excel[sheetSteps];
|
||||||
|
final headerStyle = CellStyle(
|
||||||
|
bold: true,
|
||||||
|
backgroundColorHex: ExcelColor.fromHexString('FFD9E1F2'),
|
||||||
|
horizontalAlign: HorizontalAlign.Center,
|
||||||
|
);
|
||||||
|
|
||||||
|
final headers = [
|
||||||
|
'program_code',
|
||||||
|
'step_no',
|
||||||
|
'position',
|
||||||
|
'name',
|
||||||
|
'mixTime',
|
||||||
|
'magnetTime',
|
||||||
|
'volume',
|
||||||
|
'blowTime',
|
||||||
|
'speed',
|
||||||
|
];
|
||||||
|
sheet.appendRow(headers.map((h) => TextCellValue(h)).toList());
|
||||||
|
for (var i = 0; i < headers.length; i++) {
|
||||||
|
sheet
|
||||||
|
.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0))
|
||||||
|
.cellStyle = headerStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例行:P001 / P002 各两步
|
||||||
|
final samples = <List<Object>>[
|
||||||
|
['P001', 1, 'A1', '加样', 30, 0, 500, 0, 5],
|
||||||
|
['P001', 2, 'A2', '混合', 60, 10, 300, 30, 5],
|
||||||
|
['P002', 1, 'B1', '混合', 20, 5, 400, 0, 4],
|
||||||
|
];
|
||||||
|
for (final row in samples) {
|
||||||
|
sheet.appendRow(row.map((v) {
|
||||||
|
if (v is num) return IntCellValue(v.toInt());
|
||||||
|
return TextCellValue(v.toString());
|
||||||
|
}).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 说明行
|
||||||
|
final note =
|
||||||
|
'说明:program_code 对应 Programs.code;step_no 整数从 1 开始;position 形如 A1/B2;mixTime/magnetTime/volume/blowTime 单位秒或微升;speed 1-10';
|
||||||
|
sheet.appendRow([TextCellValue(note)]);
|
||||||
|
|
||||||
|
// 列宽
|
||||||
|
sheet.setColumnWidth(0, 14);
|
||||||
|
sheet.setColumnWidth(1, 10);
|
||||||
|
sheet.setColumnWidth(2, 12);
|
||||||
|
sheet.setColumnWidth(3, 14);
|
||||||
|
sheet.setColumnWidth(4, 10);
|
||||||
|
sheet.setColumnWidth(5, 12);
|
||||||
|
sheet.setColumnWidth(6, 10);
|
||||||
|
sheet.setColumnWidth(7, 10);
|
||||||
|
sheet.setColumnWidth(8, 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import '../../programs/models/program.dart';
|
|
||||||
import '../../programs/models/step.dart';
|
|
||||||
import '../../programs/services/program_service.dart';
|
|
||||||
|
|
||||||
/// 程序导入服务
|
|
||||||
class ProgramImportService {
|
|
||||||
static final ProgramImportService instance = ProgramImportService._internal();
|
|
||||||
final ProgramService _programService = ProgramService.instance;
|
|
||||||
|
|
||||||
ProgramImportService._internal();
|
|
||||||
|
|
||||||
/// 从 JSON 字符串导入程序
|
|
||||||
/// 返回导入的程序数量
|
|
||||||
Future<int> importFromJson(String jsonContent) async {
|
|
||||||
final data = jsonDecode(jsonContent);
|
|
||||||
|
|
||||||
// 支持单个程序或程序数组
|
|
||||||
final List<dynamic> programsData;
|
|
||||||
if (data is List) {
|
|
||||||
programsData = data;
|
|
||||||
} else if (data is Map && data.containsKey('programs')) {
|
|
||||||
programsData = data['programs'] as List;
|
|
||||||
} else {
|
|
||||||
programsData = [data];
|
|
||||||
}
|
|
||||||
|
|
||||||
int importedCount = 0;
|
|
||||||
|
|
||||||
for (final programData in programsData) {
|
|
||||||
try {
|
|
||||||
// 验证必填字段
|
|
||||||
if (!_validateProgramData(programData)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查编号是否已存在
|
|
||||||
final existingPrograms = await _programService.getAllPrograms();
|
|
||||||
final code = programData['code'] as String;
|
|
||||||
if (existingPrograms.any((p) => p.code == code)) {
|
|
||||||
// 编号已存在,跳过或使用新编号
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建程序
|
|
||||||
final program = Program(
|
|
||||||
code: code,
|
|
||||||
name: programData['name'] as String,
|
|
||||||
createdAt: programData['createdAt'] ?? DateTime.now().toString().split('.')[0],
|
|
||||||
status: programData['status'] ?? 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
final programId = await _programService.addProgram(program);
|
|
||||||
|
|
||||||
// 导入步骤
|
|
||||||
final stepsData = programData['steps'] as List?;
|
|
||||||
if (stepsData != null) {
|
|
||||||
for (int i = 0; i < stepsData.length; i++) {
|
|
||||||
final stepData = stepsData[i];
|
|
||||||
final step = Step(
|
|
||||||
programId: programId,
|
|
||||||
stepNo: i + 1,
|
|
||||||
position: stepData['position'] as String? ?? 'A1',
|
|
||||||
name: stepData['name'] as String? ?? '步骤${i + 1}',
|
|
||||||
mixTime: stepData['mixTime'] as int? ?? 0,
|
|
||||||
magnetTime: stepData['magnetTime'] as int? ?? 0,
|
|
||||||
volume: stepData['volume'] as int? ?? 0,
|
|
||||||
mixSpeed: stepData['mixSpeed'] as String? ?? '中速',
|
|
||||||
blowSpeed: stepData['blowSpeed'] as String? ?? '中速',
|
|
||||||
blowTime: stepData['blowTime'] as int? ?? 0,
|
|
||||||
needleSpeed: stepData['needleSpeed'] as int? ?? 5,
|
|
||||||
);
|
|
||||||
await _programService.addStep(step);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
importedCount++;
|
|
||||||
} catch (e) {
|
|
||||||
// 忽略单个程序导入错误
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return importedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 验证程序数据
|
|
||||||
bool _validateProgramData(Map<String, dynamic> data) {
|
|
||||||
return data.containsKey('code') &&
|
|
||||||
data.containsKey('name') &&
|
|
||||||
data['code'] is String &&
|
|
||||||
data['name'] is String;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 导出程序为 JSON
|
|
||||||
Future<String> exportToJson(List<int> programIds) async {
|
|
||||||
final programs = [];
|
|
||||||
|
|
||||||
for (final id in programIds) {
|
|
||||||
final program = await _programService.getProgramById(id);
|
|
||||||
if (program == null) continue;
|
|
||||||
|
|
||||||
final steps = await _programService.getStepsByProgramId(id);
|
|
||||||
|
|
||||||
programs.add({
|
|
||||||
'code': program.code,
|
|
||||||
'name': program.name,
|
|
||||||
'createdAt': program.createdAt,
|
|
||||||
'status': program.status,
|
|
||||||
'steps': steps.map((s) => {
|
|
||||||
'position': s.position,
|
|
||||||
'name': s.name,
|
|
||||||
'mixTime': s.mixTime,
|
|
||||||
'magnetTime': s.magnetTime,
|
|
||||||
'volume': s.volume,
|
|
||||||
'mixSpeed': s.mixSpeed,
|
|
||||||
'blowSpeed': s.blowSpeed,
|
|
||||||
'blowTime': s.blowTime,
|
|
||||||
'needleSpeed': s.needleSpeed,
|
|
||||||
}).toList(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonEncode({'programs': programs});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import '../../../core/localization/app_localizations.dart';
|
import '../../../core/localization/app_localizations.dart';
|
||||||
import '../../../core/theme/app_theme.dart';
|
import '../../../core/theme/app_theme.dart';
|
||||||
|
import '../../../shared/utils/constants.dart';
|
||||||
import '../../../shared/widgets/common_button.dart';
|
import '../../../shared/widgets/common_button.dart';
|
||||||
import '../models/program.dart';
|
import '../models/program.dart';
|
||||||
import '../providers/programs_provider.dart';
|
import '../providers/programs_provider.dart';
|
||||||
@@ -21,6 +22,8 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
late TextEditingController _codeController;
|
late TextEditingController _codeController;
|
||||||
late TextEditingController _nameController;
|
late TextEditingController _nameController;
|
||||||
|
late TextEditingController _temperatureController;
|
||||||
|
late TextEditingController _airflowTimeController;
|
||||||
bool _isEnabled = true;
|
bool _isEnabled = true;
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
|
|
||||||
@@ -29,6 +32,10 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_codeController = TextEditingController(text: widget.program?.code ?? '');
|
_codeController = TextEditingController(text: widget.program?.code ?? '');
|
||||||
_nameController = TextEditingController(text: widget.program?.name ?? '');
|
_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;
|
_isEnabled = widget.program?.status == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +43,8 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_codeController.dispose();
|
_codeController.dispose();
|
||||||
_nameController.dispose();
|
_nameController.dispose();
|
||||||
|
_temperatureController.dispose();
|
||||||
|
_airflowTimeController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +104,38 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -161,6 +202,8 @@ class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
|
|||||||
name: _nameController.text.trim(),
|
name: _nameController.text.trim(),
|
||||||
createdAt: widget.program?.createdAt ?? now,
|
createdAt: widget.program?.createdAt ?? now,
|
||||||
status: _isEnabled ? 1 : 0,
|
status: _isEnabled ? 1 : 0,
|
||||||
|
temperature: int.tryParse(_temperatureController.text) ?? 50,
|
||||||
|
airflowTime: int.tryParse(_airflowTimeController.text) ?? 60,
|
||||||
);
|
);
|
||||||
|
|
||||||
bool success;
|
bool success;
|
||||||
|
|||||||
@@ -5,11 +5,9 @@ import '../../../core/theme/app_theme.dart';
|
|||||||
import '../../../shared/widgets/common_button.dart';
|
import '../../../shared/widgets/common_button.dart';
|
||||||
import '../widgets/language_panel.dart';
|
import '../widgets/language_panel.dart';
|
||||||
import '../widgets/password_panel.dart';
|
import '../widgets/password_panel.dart';
|
||||||
import '../widgets/serial_config_panel.dart';
|
|
||||||
import '../widgets/usb_import_panel.dart';
|
|
||||||
|
|
||||||
/// 设置页菜单
|
/// 设置页菜单
|
||||||
enum _SettingsMenu { upgrade, language, password, usbImport, serialConfig }
|
enum _SettingsMenu { upgrade, language, password }
|
||||||
|
|
||||||
/// 系统设置页面
|
/// 系统设置页面
|
||||||
class SettingsPage extends ConsumerStatefulWidget {
|
class SettingsPage extends ConsumerStatefulWidget {
|
||||||
@@ -70,14 +68,6 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
|||||||
onTap: () => setState(
|
onTap: () => setState(
|
||||||
() => _currentMenu = _SettingsMenu.upgrade),
|
() => _currentMenu = _SettingsMenu.upgrade),
|
||||||
),
|
),
|
||||||
// 串口配置
|
|
||||||
_buildMenuItem(
|
|
||||||
icon: Icons.settings_input_hdmi,
|
|
||||||
title: '串口配置',
|
|
||||||
selected: _currentMenu == _SettingsMenu.serialConfig,
|
|
||||||
onTap: () => setState(
|
|
||||||
() => _currentMenu = _SettingsMenu.serialConfig),
|
|
||||||
),
|
|
||||||
// 语言设置
|
// 语言设置
|
||||||
_buildMenuItem(
|
_buildMenuItem(
|
||||||
icon: Icons.language,
|
icon: Icons.language,
|
||||||
@@ -94,14 +84,6 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
|||||||
onTap: () => setState(
|
onTap: () => setState(
|
||||||
() => _currentMenu = _SettingsMenu.password),
|
() => _currentMenu = _SettingsMenu.password),
|
||||||
),
|
),
|
||||||
// U盘导入
|
|
||||||
_buildMenuItem(
|
|
||||||
icon: Icons.usb,
|
|
||||||
title: l10n?.usbImport ?? 'U盘导入',
|
|
||||||
selected: _currentMenu == _SettingsMenu.usbImport,
|
|
||||||
onTap: () => setState(
|
|
||||||
() => _currentMenu = _SettingsMenu.usbImport),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -127,10 +109,8 @@ class _SettingsPageState extends ConsumerState<SettingsPage> {
|
|||||||
|
|
||||||
Widget _buildContent() {
|
Widget _buildContent() {
|
||||||
return switch (_currentMenu) {
|
return switch (_currentMenu) {
|
||||||
_SettingsMenu.serialConfig => const SerialConfigPanel(),
|
|
||||||
_SettingsMenu.language => const LanguagePanel(),
|
_SettingsMenu.language => const LanguagePanel(),
|
||||||
_SettingsMenu.password => const PasswordPanel(),
|
_SettingsMenu.password => const PasswordPanel(),
|
||||||
_SettingsMenu.usbImport => const UsbImportPanel(),
|
|
||||||
_SettingsMenu.upgrade => _buildUpgradeContent(),
|
_SettingsMenu.upgrade => _buildUpgradeContent(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'core/theme/app_theme.dart';
|
|||||||
import 'core/localization/app_localizations.dart';
|
import 'core/localization/app_localizations.dart';
|
||||||
import 'core/localization/locale_provider.dart';
|
import 'core/localization/locale_provider.dart';
|
||||||
import 'core/database/database_service.dart';
|
import 'core/database/database_service.dart';
|
||||||
|
import 'features/device/providers/serial_provider.dart';
|
||||||
|
|
||||||
/// 应用入口
|
/// 应用入口
|
||||||
void main() async {
|
void main() async {
|
||||||
@@ -22,7 +23,17 @@ void main() async {
|
|||||||
final db = DatabaseService.instance;
|
final db = DatabaseService.instance;
|
||||||
await db.database;
|
await db.database;
|
||||||
await db.initTestData();
|
await db.initTestData();
|
||||||
runApp(const ProviderScope(child: KuaishaiApp()));
|
|
||||||
|
// 使用 ProviderContainer 在 runApp 之前触发启动自动连接
|
||||||
|
final container = ProviderContainer();
|
||||||
|
container.read(autoSerialConnectProvider);
|
||||||
|
|
||||||
|
runApp(
|
||||||
|
UncontrolledProviderScope(
|
||||||
|
container: container,
|
||||||
|
child: const KuaishaiApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 应用主体
|
/// 应用主体
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
/// 常量定义
|
/// 常量定义
|
||||||
class Constants {
|
class Constants {
|
||||||
// 速度选项
|
// 速度档位
|
||||||
static const List<String> speedOptions = ['低速', '中速', '高速'];
|
static const int minSpeed = 1;
|
||||||
|
static const int maxSpeed = 10;
|
||||||
// 下针速度档位
|
|
||||||
static const List<int> needleSpeedLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
|
||||||
|
|
||||||
// 孔位列表
|
// 孔位列表
|
||||||
static const List<String> positions = [
|
static const List<String> positions = [
|
||||||
|
|||||||
124
pubspec.lock
124
pubspec.lock
@@ -25,6 +25,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.4"
|
version: "0.13.4"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.6.1"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -89,6 +97,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
code_assets:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_assets
|
||||||
|
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -169,6 +185,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.8"
|
||||||
|
excel:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: excel
|
||||||
|
sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.6"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -277,6 +309,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.1.3"
|
version: "15.1.3"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
hotreloader:
|
hotreloader:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -293,6 +333,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
version: "0.20.2"
|
||||||
|
jni:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni
|
||||||
|
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
jni_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni_flutter
|
||||||
|
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
json_annotation:
|
json_annotation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -365,6 +421,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.0"
|
version: "1.17.0"
|
||||||
|
objective_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: objective_c
|
||||||
|
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.1"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -381,6 +445,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
path_provider:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: path_provider
|
||||||
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.5"
|
||||||
|
path_provider_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_android
|
||||||
|
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.1"
|
||||||
|
path_provider_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_provider_foundation
|
||||||
|
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.6.0"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -405,6 +493,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -437,6 +533,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -650,8 +754,16 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
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:
|
uuid:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||||
@@ -706,6 +818,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.6.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -716,4 +836,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.11.5 <4.0.0"
|
dart: ">=3.11.5 <4.0.0"
|
||||||
flutter: ">=3.38.0"
|
flutter: ">=3.38.4"
|
||||||
|
|||||||
12
pubspec.yaml
12
pubspec.yaml
@@ -31,9 +31,21 @@ dependencies:
|
|||||||
# 文件选择器
|
# 文件选择器
|
||||||
file_picker: ^8.1.7
|
file_picker: ^8.1.7
|
||||||
|
|
||||||
|
# 路径解析(保存导入/导出文件)
|
||||||
|
path_provider: ^2.1.5
|
||||||
|
|
||||||
|
# Excel 读写(生成导入模板 + 解析用户填好的 .xlsx)
|
||||||
|
excel: ^4.0.2
|
||||||
|
|
||||||
# 国际化
|
# 国际化
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
|
|
||||||
|
# USB 串口通信(Android USB Host 模式下的 CH340/FTDI/CP210x/PL2303 等芯片)
|
||||||
|
usb_serial: ^0.5.0
|
||||||
|
|
||||||
|
# 串口消息 ID 生成(UUID v4)
|
||||||
|
uuid: ^4.5.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Reference in New Issue
Block a user