Compare commits

...

10 Commits

Author SHA1 Message Date
Developer
8c2e26ec87 feat(home): 更新完成页面UI并优化串口连接状态管理
- 更新设备屏幕尺寸配置从1920*1080调整为1024x600
- 添加完成页面的AppBar导航和返回功能
- 重构CompletePage布局,使用SafeArea和ConstrainedBox适配不同屏幕
- 添加国际化支持的完成按钮文本
- 优化完成页面视觉元素,包括图标大小和间距调整
- 实现串口连接状态的响应式管理,解决UI状态同步问题
- 优化串口运行器的状态更新逻辑,实现乐观更新机制
- 调整完成页面按钮布局,提供完成和重新运行选项
2026-06-05 10:03:41 +08:00
Developer
87c4b669a0 refactor(home): 优化运行状态监控界面布局和参数显示
- 将程序名文本包装在Flexible组件中并添加省略号处理
- 为步骤信息添加Flexible组件以改善布局
- 使用ScrollableVIew包装内容以支持滚动
- 重构步骤参数显示为三列布局
- 移除硬编码的温度和磁力时间参数
- 更新速度、持续时间和样品体积的单位显示
- 从状态栏移除设备名称显示
- 从设置菜单移除USB导入功能选项
2026-06-04 17:47:12 +08:00
Developer
736c36a98e refactor(home): 优化主页布局和运行控制面板
- 将主页左右布局改为弹性布局,左侧程序列表占2/5宽度,右侧控制区域占3/5宽度
- 移除程序列表组件的固定宽度设置,使其能够自适应布局
- 在运行控制面板中添加主轴最小尺寸限制以优化空间使用
- 移除暂停/继续按钮中的占位按钮,简化按钮逻辑
- 修改开始/继续按钮为暂停/继续按钮,支持运行中状态切换
- 更新按钮图标和文字根据当前运行状态动态显示
- 移除运行状态指示器,精简界面元素
2026-06-04 17:22:38 +08:00
Developer
37d2af70b7 feat(device): 启动自动连接 USB 串口 + 隐藏设置页配置项 + 标题栏连接状态
- 新增 AutoSerialConnect 服务:启动后自动连接第一个 USB 串口设备,
  固定 115200/8/N/1,连接失败时每 3s 重试,断开后重新进入重试循环
- main.dart 通过 ProviderContainer 在 runApp 之前触发 autoSerialConnectProvider
- 移除设置页「串口配置」菜单项及对应面板分支
- StatusBar 在「设备运行状态」前增加串口连接状态指示(已连接/连接中/未连接)
2026-06-04 16:57:45 +08:00
Developer
3ab2232845 refactor(device): 替换消息ID生成器为UUID实现
- 移除自定义时间戳+随机数ID生成逻辑
- 集成uuid包依赖并配置版本
- 使用Uuid.v4()替换原有next()方法实现
- 更新MessageIdGenerator类文档注释
- 在JSON协议层添加设备日志警告输出
- 修改pubspec.yaml添加uuid依赖声明
2026-06-04 16:45:06 +08:00
Developer
55bdaa9211 feat(programs): Excel 导入改为全量覆盖模式
已存在 code 的程序不再跳过,而是:
- 用 Excel 中的字段更新 program(保留 id)
- 删除该 program 的全部旧步骤
- 按 Excel 中的步骤重新写入

返回值变量名 importedCount -> processedCount 更准确。
Toast 文案同步:成功处理 / Excel 无有效数据。
2026-06-04 15:51:03 +08:00
Developer
cbe1e6b470 feat(programs): Android 端通过 MediaStore 写入公共 Downloads
Android 10+ 受 scoped storage 限制,getDownloadsDirectory() 返回的是
app-specific 目录 (/storage/emulated/0/Android/data/.../files/Download/),
而非用户可见的 /storage/emulated/0/Download/。

新增 MainActivity 端 MethodChannel com.xiarui.kuaishai2/downloads:
- API 29+:MediaStore.Downloads 写入公共 Downloads,无需权限
- API <=28:直接写 /storage/emulated/0/Download/(需 WRITE_EXTERNAL_STORAGE)

Dart 端 ExcelTemplateService 改用 MethodChannel,Android 平台返回
Download/<filename> 显示路径;其它平台保留 getDownloadsDirectory 行为。
返回值由 File 改为 String,调用方已同步更新。
2026-06-04 15:45:23 +08:00
Developer
16fbb7d54b refactor(programs): 模板保存目录改为下载目录
- getDownloadsDirectory() 优先,失败时回退到应用文档目录
- 更新 dartdoc 说明
2026-06-04 15:35:52 +08:00
Developer
d91791edaf feat(programs): Excel 模板下载 + .xlsx 解析导入
- 新增 excel 4.0.6 / path_provider 2.1.5 依赖
- ExcelTemplateService:生成 Programs + Steps 双表模板(保存到应用文档目录)
- ExcelImportService:解析 .xlsx 并写入数据库,跳过已存在 code、按 program_code 关联步骤
- programs_page 顶部新增「下载模板」按钮,导入按钮改用 Excel 解析
- 移除被取代的 program_import_service.dart
- AppLocalizations 新增 downloadTemplate 键
- 验证:flutter analyze 无新增 issue;flutter build apk --debug 通过
2026-06-04 15:27:34 +08:00
Developer
d53c41c300 feat(device): 添加USB设备通信支持和程序参数优化
- 在AndroidManifest.xml中添加USB Host权限和设备过滤器配置
- 新增设备控制国际化词条包括速度档位、吹气时间等
- 重构数据库结构将速度相关字段统一为档位数值存储
- 添加通用KV存储方法用于settings表数据读写
- 优化首页导航实现tab间跳转和状态保持功能
- 更新程序详情页面布局和参数表单界面
- 移除模拟运行器相关测试代码
- 添加USB串口通信依赖包usb_serial
2026-06-04 15:13:36 +08:00
37 changed files with 1912 additions and 1122 deletions

View File

@@ -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(横屏),屏幕密度为200UI设计必须支持此尺寸

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
USB 设备过滤:下位机使用常见 USB 转串口芯片时自动识别
包含CH340 / CH341 / CH343 / CH9103、FTDI、CP210x、PL2303
若下位机使用其他芯片,可继续追加 vendor-id + product-id
-->
<resources>
<usb-device vendor-id="0x1A86" /> <!-- CH340 / CH341 / CH343 / CH9103 系列 -->
<usb-device vendor-id="0x0403" /> <!-- FTDI 系列 -->
<usb-device vendor-id="0x10C4" /> <!-- Silicon Labs CP210x 系列 -->
<usb-device vendor-id="0x067B" /> <!-- Prolific PL2303 系列 -->
</resources>

View File

@@ -0,0 +1,178 @@
---
title: 默认模块
language_tabs:
- shell: Shell
- http: HTTP
- javascript: JavaScript
- ruby: Ruby
- python: Python
- php: PHP
- java: Java
- go: Go
toc_footers: []
includes: []
search: true
code_clipboard: true
highlight_theme: darkula
headingLevel: 2
generator: "@tarslib/widdershins v4.0.30"
---
# 默认模块
Base URLs:
# Authentication
# 数据模型
<h2 id="tocS_设备基本信息">设备基本信息</h2>
<a id="schema设备基本信息"></a>
<a id="schema_设备基本信息"></a>
<a id="tocS设备基本信息"></a>
<a id="tocs设备基本信息"></a>
```json
{
"message_id": "string",
"type": "string",
"ack": "string",
"need_ack": true,
"data": {
"door_status": "string",
"task_status": "string",
"light_status": "string"
}
}
```
### 属性
|名称|类型|必选|约束|中文名|说明|
|---|---|---|---|---|---|
|message_id|string|true|none||none|
|type|string|true|none|类型|device_info|
|ack|string|true|none||none|
|need_ack|boolean|true|none||none|
|data|object|true|none||none|
|» door_status|string|true|none|门状态|open=开close=关|
|» task_status|string|true|none|任务运行状态|running=运行中pause=暂停idle=空闲|
|» light_status|string|true|none|灯状态|on=开off= 关|
<h2 id="tocS_下发任务">下发任务</h2>
<a id="schema下发任务"></a>
<a id="schema_下发任务"></a>
<a id="tocS下发任务"></a>
<a id="tocs下发任务"></a>
```json
{
"message_id": "string",
"type": "string",
"ack": "string",
"need_ack": true,
"data": {
"steps": [
{
"no": 0,
"slot": 0,
"name": "string",
"mixtime": 0,
"pulltime": 0,
"volume": 0,
"speed": 0
}
],
"temperature": 0,
"airflowtime": 0
}
}
```
### 属性
|名称|类型|必选|约束|中文名|说明|
|---|---|---|---|---|---|
|message_id|string|true|none||由uuid生成唯一识别码|
|type|string|true|none|指令类型|create_task|
|ack|string|true|none||需要响应的消息id|
|need_ack|boolean|true|none|是否需要响应|true|
|data|object|true|none||none|
|» steps|[object]|true|none|步骤列表|none|
|»» no|integer|true|none|步骤号|none|
|»» slot|integer|true|none|槽位号|none|
|»» name|string|true|none|步骤名称|none|
|»» mixtime|integer|true|none|搅拌时间|单位:秒|
|»» pulltime|integer|true|none|吸磁时间|单位:秒|
|»» volume|integer|true|none|容积|范围 0-2000|
|»» speed|integer|true|none|速度等级|范围1-10|
|» temperature|integer|true|none|加热温度|none|
|» airflowtime|integer|true|none|吹气时间|单位:秒|
<h2 id="tocS_灯光控制">灯光控制</h2>
<a id="schema灯光控制"></a>
<a id="schema_灯光控制"></a>
<a id="tocS灯光控制"></a>
<a id="tocs灯光控制"></a>
```json
{
"message_id": "string",
"type": "string",
"ack": " ",
"need_ack": true,
"data": {
"status": "string"
}
}
```
### 属性
|名称|类型|必选|约束|中文名|说明|
|---|---|---|---|---|---|
|message_id|string|true|none||none|
|type|string|true|none|类型|none|
|ack|string|true|none||none|
|need_ack|boolean|true|none||none|
|data|object|true|none||none|
|» status|string|true|none|开光|on / off|
<h2 id="tocS_任务控制">任务控制</h2>
<a id="schema任务控制"></a>
<a id="schema_任务控制"></a>
<a id="tocS任务控制"></a>
<a id="tocs任务控制"></a>
```json
{
"message_id": "string",
"type": "string",
"ack": "string",
"need_ack": true,
"data": {
"status": "string"
}
}
```
### 属性
|名称|类型|必选|约束|中文名|说明|
|---|---|---|---|---|---|
|message_id|string|true|none||none|
|type|string|true|none|类型|control|
|ack|string¦null|true|none||none|
|need_ack|boolean|true|none||none|
|data|object|true|none||none|
|» status|string|true|none|状态|continue=继续stop=停止,暂停=pause|

View File

@@ -20,7 +20,7 @@ class DatabaseService {
return await openDatabase( 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,
}, },
]; ];

View File

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

View File

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

View File

@@ -0,0 +1,126 @@
import 'dart:convert';
/// 串口奇偶校验
enum SerialParity { none, odd, even, mark, space }
/// 串口流控
enum SerialFlowControl { none, rtsCts, xonXoff, dtrDsr }
/// 串口配置
///
/// 持久化到 settings 表的 `serial_config` key 中;
/// 打开串口时根据此配置构造底层 `UsbConfig`。
class SerialConfig {
/// 设备 Vendor ID十六进制
final int vendorId;
/// 设备 Product ID十六进制0 表示不指定
final int productId;
/// 波特率
final int baudRate;
/// 数据位 (5/6/7/8)
final int dataBits;
/// 停止位 (1/2)
final int stopBits;
/// 校验位
final SerialParity parity;
/// 流控
final SerialFlowControl flowControl;
/// 读超时(毫秒)
final int readTimeoutMs;
/// 写超时(毫秒)
final int writeTimeoutMs;
const SerialConfig({
this.vendorId = 0x1A86,
this.productId = 0x7523,
this.baudRate = 9600,
this.dataBits = 8,
this.stopBits = 1,
this.parity = SerialParity.none,
this.flowControl = SerialFlowControl.none,
this.readTimeoutMs = 2000,
this.writeTimeoutMs = 2000,
});
/// 默认配置
static const SerialConfig defaults = SerialConfig();
/// 常用波特率
static const List<int> commonBaudRates = [
1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800,
];
SerialConfig copyWith({
int? vendorId,
int? productId,
int? baudRate,
int? dataBits,
int? stopBits,
SerialParity? parity,
SerialFlowControl? flowControl,
int? readTimeoutMs,
int? writeTimeoutMs,
}) {
return SerialConfig(
vendorId: vendorId ?? this.vendorId,
productId: productId ?? this.productId,
baudRate: baudRate ?? this.baudRate,
dataBits: dataBits ?? this.dataBits,
stopBits: stopBits ?? this.stopBits,
parity: parity ?? this.parity,
flowControl: flowControl ?? this.flowControl,
readTimeoutMs: readTimeoutMs ?? this.readTimeoutMs,
writeTimeoutMs: writeTimeoutMs ?? this.writeTimeoutMs,
);
}
/// 编码为 JSON 字符串(用于持久化)
String toJsonString() => jsonEncode({
'vendorId': vendorId,
'productId': productId,
'baudRate': baudRate,
'dataBits': dataBits,
'stopBits': stopBits,
'parity': parity.name,
'flowControl': flowControl.name,
'readTimeoutMs': readTimeoutMs,
'writeTimeoutMs': writeTimeoutMs,
});
/// 从 JSON 字符串解码;解析失败时返回默认值
factory SerialConfig.fromJsonString(String? raw) {
if (raw == null || raw.isEmpty) return defaults;
try {
final map = jsonDecode(raw) as Map<String, dynamic>;
return SerialConfig(
vendorId: (map['vendorId'] as num?)?.toInt() ?? defaults.vendorId,
productId: (map['productId'] as num?)?.toInt() ?? defaults.productId,
baudRate: (map['baudRate'] as num?)?.toInt() ?? defaults.baudRate,
dataBits: (map['dataBits'] as num?)?.toInt() ?? defaults.dataBits,
stopBits: (map['stopBits'] as num?)?.toInt() ?? defaults.stopBits,
parity: SerialParity.values.firstWhere(
(e) => e.name == map['parity'],
orElse: () => defaults.parity,
),
flowControl: SerialFlowControl.values.firstWhere(
(e) => e.name == map['flowControl'],
orElse: () => defaults.flowControl,
),
readTimeoutMs:
(map['readTimeoutMs'] as num?)?.toInt() ?? defaults.readTimeoutMs,
writeTimeoutMs:
(map['writeTimeoutMs'] as num?)?.toInt() ?? defaults.writeTimeoutMs,
);
} catch (_) {
return defaults;
}
}
}

View File

@@ -1,17 +1,18 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import '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);
}); });

View File

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

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

View File

@@ -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] 生成全局唯一 ID128 位随机,标准 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';
}
} }

View File

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

View File

@@ -1,190 +0,0 @@
import 'dart:async';
import '../../programs/models/step.dart';
import '../../programs/models/program.dart';
/// 模拟运行器回调
typedef RunProgressCallback = void Function(
int currentStepIndex,
int remainingSeconds,
double progress,
String currentWell,
);
typedef RunCompleteCallback = void Function();
/// 模拟运行器
/// 用于在没有实际硬件连接时模拟程序执行过程
class MockRunner {
Timer? _timer;
Program? _currentProgram;
List<Step> _steps = [];
int _currentStepIndex = 0;
int _remainingSeconds = 0;
bool _isPaused = false;
RunProgressCallback? _onProgress;
RunCompleteCallback? _onComplete;
/// 是否正在运行
bool get isRunning => _timer != null && !_isPaused;
/// 是否已暂停
bool get isPaused => _isPaused;
/// 当前程序
Program? get currentProgram => _currentProgram;
/// 开始运行程序
void start(
Program program,
List<Step> steps,
RunProgressCallback onProgress,
RunCompleteCallback onComplete,
) {
_currentProgram = program;
_steps = steps;
_onProgress = onProgress;
_onComplete = onComplete;
_currentStepIndex = 0;
_isPaused = false;
if (steps.isEmpty) {
onComplete();
return;
}
// 开始执行第一个步骤
_startStep(steps[0]);
}
/// 暂停运行
void pause() {
if (_timer != null && !_isPaused) {
_isPaused = true;
_timer!.cancel();
_timer = null;
}
}
/// 继续运行
void resume() {
if (_isPaused && _currentProgram != null) {
_isPaused = false;
_resumeStep();
}
}
/// 停止运行
void stop() {
_timer?.cancel();
_timer = null;
_currentProgram = null;
_steps = [];
_currentStepIndex = 0;
_remainingSeconds = 0;
_isPaused = false;
}
/// 开始执行步骤
void _startStep(Step step) {
// 计算步骤总时间(混合时间 + 吸磁时间 + 吹气时间)
_remainingSeconds = step.mixTime + step.magnetTime + step.blowTime;
// 如果步骤时间为0设置最小演示时间5秒
if (_remainingSeconds == 0) {
_remainingSeconds = 5;
}
// 启动定时器,每秒更新
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_remainingSeconds--;
// 计算总进度
final totalSeconds = _calculateTotalSeconds();
final elapsedSeconds = _calculateElapsedSeconds();
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
// 回调进度更新
_onProgress?.call(
_currentStepIndex,
_remainingSeconds,
progress,
step.position,
);
// 步骤完成
if (_remainingSeconds <= 0) {
timer.cancel();
_timer = null;
_nextStep();
}
});
}
/// 继续执行步骤(从暂停恢复)
void _resumeStep() {
if (_currentStepIndex >= _steps.length) return;
final step = _steps[_currentStepIndex];
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_remainingSeconds--;
final totalSeconds = _calculateTotalSeconds();
final elapsedSeconds = _calculateElapsedSeconds();
final progress = totalSeconds > 0 ? elapsedSeconds / totalSeconds : 0.0;
_onProgress?.call(
_currentStepIndex,
_remainingSeconds,
progress,
step.position,
);
if (_remainingSeconds <= 0) {
timer.cancel();
_timer = null;
_nextStep();
}
});
}
/// 执行下一个步骤
void _nextStep() {
_currentStepIndex++;
if (_currentStepIndex >= _steps.length) {
// 所有步骤完成
_onComplete?.call();
stop();
} else {
// 执行下一个步骤
_startStep(_steps[_currentStepIndex]);
}
}
/// 计算总执行时间
int _calculateTotalSeconds() {
int total = 0;
for (final step in _steps) {
int stepTime = step.mixTime + step.magnetTime + step.blowTime;
if (stepTime == 0) stepTime = 5;
total += stepTime;
}
return total;
}
/// 计算已执行时间
int _calculateElapsedSeconds() {
int elapsed = 0;
for (int i = 0; i < _currentStepIndex; i++) {
int stepTime = _steps[i].mixTime + _steps[i].magnetTime + _steps[i].blowTime;
if (stepTime == 0) stepTime = 5;
elapsed += stepTime;
}
// 加上当前步骤已执行的时间
final currentStep = _steps[_currentStepIndex];
int currentStepTime = currentStep.mixTime + currentStep.magnetTime + currentStep.blowTime;
if (currentStepTime == 0) currentStepTime = 5;
elapsed += currentStepTime - _remainingSeconds;
return elapsed;
}
}

View File

@@ -1,114 +0,0 @@
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
import 'runner_interface.dart';
/// 模拟运行器(用于开发测试)
/// 模拟硬件运行过程
class MockRunner implements Runner {
@override
RunnerStatus status = RunnerStatus.idle;
bool _isRunning = false;
int _currentStep = 0;
int _remainingSeconds = 0;
RunnerCallbacks? _callbacks;
List<Step> _steps = [];
@override
void start(Program program, List<Step> steps, RunnerCallbacks callbacks) {
if (steps.isEmpty) {
callbacks.onError?.call('No steps to run');
status = RunnerStatus.error;
return;
}
_steps = steps;
_callbacks = callbacks;
_currentStep = 0;
_isRunning = true;
status = RunnerStatus.running;
// 开始模拟运行
_runSimulation();
}
void _runSimulation() {
if (!_isRunning || _currentStep >= _steps.length) {
_completeRun();
return;
}
final step = _steps[_currentStep];
// 计算步骤时间(混合时间 + 吸磁时间 + 吹气时间 + 5秒最小
final stepTime = (step.mixTime ?? 0) + (step.magnetTime ?? 0) + (step.blowTime ?? 0) + 5;
_remainingSeconds = stepTime.clamp(5, 300);
// 模拟倒计时
_simulateStepProgress(stepTime);
}
void _simulateStepProgress(int totalSeconds) {
// 简化模拟:每秒更新进度
int elapsed = 0;
while (_isRunning && elapsed < totalSeconds) {
elapsed++;
final remaining = totalSeconds - elapsed;
final progress = elapsed / totalSeconds;
_callbacks?.onProgress?.call(
_currentStep,
remaining,
(_currentStep + progress) / _steps.length,
_steps[_currentStep].position,
);
// 实际实现需要使用 Timer
// await Future.delayed(Duration(seconds: 1));
}
if (_isRunning) {
_currentStep++;
_runSimulation();
}
}
void _completeRun() {
status = RunnerStatus.completed;
_isRunning = false;
_callbacks?.onComplete?.call();
}
@override
void pause() {
if (status == RunnerStatus.running) {
_isRunning = false;
status = RunnerStatus.paused;
}
}
@override
void resume() {
if (status == RunnerStatus.paused) {
_isRunning = true;
status = RunnerStatus.running;
// 继续运行
_runSimulation();
}
}
@override
void stop() {
_isRunning = false;
status = RunnerStatus.idle;
_currentStep = 0;
_remainingSeconds = 0;
}
@override
RunnerStatus getStatus() => status;
@override
void dispose() {
stop();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 ?? '确认'),
), ),
], ],
), ),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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.codestep_no 整数从 1 开始position 形如 A1/B2mixTime/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);
}
}

View File

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

View File

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

View File

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

View File

@@ -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(),
),
);
} }
/// 应用主体 /// 应用主体

View File

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

View File

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

View File

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