chore(project): 初始化项目基础配置文件

- 添加 CodeGraph、Android 和通用 gitignore 配置
- 创建项目元数据文件跟踪 Flutter 项目属性
- 添加 Codex AI 指导文档 AGENTS.md 说明项目架构
- 配置代码分析选项 analysis_options.yaml
- 设置 Android 应用清单权限和 Kiosk 模式配置
- 实现中英文国际化支持 AppLocalizations
- 配置 GoRouter 应用路由导航
- 创建明亮工业控制风格的主题配置 AppTheme
This commit is contained in:
Developer
2026-06-04 11:19:44 +08:00
commit 5d28bf631b
85 changed files with 21423 additions and 0 deletions

16
.codegraph/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# CodeGraph data files
# These are local to each machine and should not be committed
# Database
*.db
*.db-wal
*.db-shm
# Cache
cache/
# Logs
*.log
# Hook markers
.dirty

6
.codegraph/daemon.pid Normal file
View File

@@ -0,0 +1,6 @@
{
"pid": 67540,
"version": "0.9.7",
"socketPath": "\\\\.\\pipe\\codegraph-7209b3ccb3579134",
"startedAt": 1780364708996
}

48
.gitignore vendored Normal file
View File

@@ -0,0 +1,48 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
.planning/
.omc/

33
.metadata Normal file
View File

@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "cc0734ac716fbb8b90f3f9db8020958b1553afa7"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
- platform: android
create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
- platform: web
create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

166
AGENTS.md Normal file
View File

@@ -0,0 +1,166 @@
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## 项目概述
污水毒品快检一体机控制软件kuaishai2用于控制设备运行程序、管理程序配置、监控运行状态。
**包名**: com.xiarui.kuaishai2
## 开发命令
```bash
# 获取依赖
flutter pub get
# 运行应用 (调试模式)
flutter run
# 构建 APK
flutter build apk
# 构建发布版 APK
flutter build apk --release
# 代码分析
flutter analyze
# 运行测试
flutter test
# 清理构建缓存
flutter clean
```
## 代码架构
项目采用 **feature-first** 分层架构,结构如下:
```
lib/
├── core/ # 核心模块
│ ├── database/ # SQLite 数据库服务 (单例)
│ ├── router/ # GoRouter 路由配置
│ ├── theme/ # AppTheme 主题定义
│ └── localization/ # AppLocalizations 国际化
├── features/ # 功能模块
│ ├── home/ # 首页(设备控制面板)
│ ├── programs/ # 程序管理
│ ├── program_detail/ # 程序详情/步骤配置
│ ├── device/ # 设备运行控制
│ └── settings/ # 系统设置
└── shared/ # 共享组件
├── widgets/ # 通用组件 (CommonButton, CommonCard, StatusIndicator)
└── utils/ # 常量定义 (Constants)
```
### 技术栈
| 层级 | 技术 | 说明 |
|------|------|------|
| 状态管理 | flutter_riverpod | StateNotifier + Provider 模式 |
| 路由 | go_router | 声明式路由,支持路径参数 |
| 数据持久化 | sqflite | SQLite 本地数据库 |
| 国际化 | intl + flutter_localizations | 中/英双语 |
### 核心数据模型
**Program** (程序): `id`, `code`, `name`, `createdAt`, `status(1启用/0停用)`
**Step** (步骤): `id`, `programId`, `stepNo`, `position`, `name`, `mixTime`, `magnetTime`, `volume`, `mixSpeed`, `blowSpeed`, `blowTime`, `needleSpeed`
### 关键 Provider
- `programsProvider` - 程序列表状态 (StateNotifier)
- `runStateProvider` - 设备运行状态 (RunStatus: idle/running/paused/completed/error)
- `goRouterProvider` - 路由配置
### 数据库
DatabaseService 为单例模式,表结构:
- `programs` - 程序表
- `steps` - 步骤表 (外键关联 program_id)
### 国际化
AppLocalizations 支持中文(zh)和英文(en),使用 `AppLocalizations.of(context)` 获取翻译。
## 代码规范
- Dart SDK 版本: ^3.11.5
- 使用 `flutter_lints` 包的推荐 lint 规则
- 代码风格遵循 Flutter 官方最佳实践
## 功能模块
根据需求文档,应用包含以下核心模块:
### 1. 首页模块(设备控制面板)
- 状态栏:设备名称、实时时钟、系统状态、照明控制
- 程序列表:卡片展示、程序选择、查看详情
- 运行控制:启动/暂停/停止程序
- 运行状态监控:当前步骤、剩余时间、总进度
### 2. 程序管理模块
- 程序列表:编号、名称、创建时间、状态
- CRUD 操作:新增、编辑、删除、导入程序
### 3. 程序详情模块
- 步骤管理:步骤列表、排序、增删改
- 步骤参数:孔位、混合时间、吸磁时间、容积、速度等
### 4. 系统设置模块
- 软件升级、语言切换(中文/英文、密码修改、U盘导入
## 步骤参数说明
| 参数 | 说明 | 取值范围 |
|------|------|----------|
| 孔位 | 操作位置 | A1, A2, B1... |
| 混合时间 | 混合持续时间 | 正整数(秒) |
| 吸磁时间 | 磁珠吸附时间 | 正整数(秒) |
| 容积 | 液体体积 | 正整数(μL) |
| 混合/吹气速度 | 操作速度 | 低速/中速/高速 |
| 下针速度 | 针头下移速度 | 1-10档 |
## 当前实现状态
已完成基础架构搭建,实现了:
- 首页:状态栏、程序列表、运行控制面板、运行状态监控
- 程序管理:列表展示、新增/编辑/删除、状态切换
- 程序详情:步骤列表、步骤参数编辑
- 运行控制:启动/暂停/停止、进度监控(使用 MockRunner 模拟)
- 系统设置:基础页面框架
- 国际化:中/英文翻译配置
待完善设备通信对接、实际硬件控制、U盘导入功能。
## UI 设计稿
**Stitch 项目链接**: https://stitch.withgoogle.com/projects/16230138564963723693
### 页面截图链接
| 页面 | 功能描述 | 截图链接 |
|------|----------|----------|
| 首页 - 待机状态 | 状态栏 + 程序列表 + 开始运行按钮 + 瓷套棒确认 | [查看](https://lh3.googleusercontent.com/aida/ADBb0ujbe00elAXxTbD-KomnaatYJ40X-ubwTDSaI--jR_FRjPow3DJn7S61WpX9NxIMfgAyup6KXC_5qQWeZi0G9qQakQpvszL-OiaTU0C_yQ5FB-E1UMrystcYL0inmiky1Z2Ai-18NSYAdr0dJ6bbTp_dmJ4-JgmK0bjqdoUSnqlzo_Q1SwyNmbJb8WdNTKIUoXNudgLOCZURLWzONviyPgs9CqlxS1KUvopuxJymQF26kwxFO33lY6_vdLzD) |
| 首页 - 运行状态 | 实时监控:孔位、步骤、倒计时、进度条、暂停/停止控制 | [查看](https://lh3.googleusercontent.com/aida/ADBb0uj1bjMG6Fu-6-yvfL-Y51mJws-NBwlkX-GCbBBjLjb-4gNvMB_y8AESOu7X-UF_SHggCttMNpY6g88WgGOSJjw-1NOyTyigDBRInEwawYt8aXSGIGknLVw6zK_99aFnfturjzoYySuHBhKP2r34XWvfAO1g0e-as3hWBOaJW0gb2w8DSLmv3MpGLvsedJNq4SQFhl50LtNFM_zCMKjv-NtqyCE3y1vndNxnxCrJUy_BHvh4XHCahf18UnOU) |
| 程序管理 - 列表视图 | 程序表格:复选框、编号、名称、状态、批量操作、分页 | [查看](https://lh3.googleusercontent.com/aida/ADBb0ujE1ZBh5_Gn-yyy1bfU-U_xCeGHREsycgxyAlWBKzWbhFzvlusSWhCEY1u6RBRqVk5XZ49x8Ljpe4yXcS5e3q_jgGMGpcd1CN0RxX0nU6D75O056euNV7fY9cBNtTVGXibmM7Am1uj5uxV7TLbW8c_ix8WubRhn0gYFU6L87b80N6Zl-2T3lmBZsp98jwSsUXP3RqHk6CwMLoQ9LYcLRQTU5Dh5XD1e9aXus9I9pDgj-ZMUsFcZJA7XnJ0y) |
| 程序详情 - 步骤配置 | 左侧步骤列表 + 右侧参数表单(孔位、时间、速度等) | [查看](https://lh3.googleusercontent.com/aida/ADBb0ujEyurJOmwUkZx3ysQMSCk4pXU9_6Jh6bsEMhwlwyC88SQZ0hCFRHMhKADFecHdLXG3D-A5Hp3Avw25j1ivrP2CyTTdeXgd4QPCARNZetrezcL1iy0b7RYsVTTnXnzIFX846pRC_rxgbh8DdqMAiHPz8Q94JciAFCAokQfduFmDWw3Wzhj_P0KtlL8UmEIJNzGFi9ySTGo6eKmz6lfdfQ6VMowhYR5Qy8VGZi8qTLlXqFSU9f-dZepaJWs) |
| 系统设置 - 控制面板 | 软件升级、语言切换、密码修改、U盘导入 | [查看](https://lh3.googleusercontent.com/aida/ADBb0ugIEqBuSXmwYtQkPXXIJ7qBdL69VyQdTRzTyXjc5M6THuTBnbJlLyIDmvo7qfTz_RwjwZE_czzN6dyR_ydqk2Er5hNYduPLypOLqqR7I2sHaKDIvCeole91oJIVkAcnfJkWQJJPgaIhqfysMV4mrsHD0rhYJpngLorfE9WMl11tG9MPz9_E8XcmwaFqXqtYvxHeBsnvSNtIQfkX9S5QfnYtOiAsnUKioY3GpVvjK6bV4PYIfCMMHXv5koS8) |
| 运行完成 - 操作指引 | 成功提示 + 样本滴入说明 + 返回/重运行按钮 | [查看](https://lh3.googleusercontent.com/aida/ADBb0ujh5-3mBPpjWkSRHGOzNRQuusc4O7p_Kov2dbd3iiJmOHlElBb9-gPWC5bnfnRuDbhIbZDhxQhTfVkRpUhQU5nMGa5y7NpV4efvG6ZF0Bx4aVdm4HJ-o2Qca-alJuSfgsggJgbZ3QptA_cAXx-VfG79sHB1zUvRDHpPkLSu0lePhmjaR6m6-pwR42Pib7OLSNqS6V9208w7HoGCX1cPC6pH-hi8rZrMbMmbk2p-pTd2tJebNsow9aLb6bLJ) |
### 设计规范
- **主色调**: #2196F3(蓝色)
- **字体**: Inter
- **圆角**: 4px工业控制风格
- **配色模式**: Light + Tonal Spot
- **注意**: Stitch 生成的画布尺寸为 2560×2048实际开发需适配 1920×1080 横屏
---
## 其他说明
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设计必须支持此尺寸

166
CLAUDE.md Normal file
View File

@@ -0,0 +1,166 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
污水毒品快检一体机控制软件kuaishai2用于控制设备运行程序、管理程序配置、监控运行状态。
**包名**: com.xiarui.kuaishai2
## 开发命令
```bash
# 获取依赖
flutter pub get
# 运行应用 (调试模式)
flutter run
# 构建 APK
flutter build apk
# 构建发布版 APK
flutter build apk --release
# 代码分析
flutter analyze
# 运行测试
flutter test
# 清理构建缓存
flutter clean
```
## 代码架构
项目采用 **feature-first** 分层架构,结构如下:
```
lib/
├── core/ # 核心模块
│ ├── database/ # SQLite 数据库服务 (单例)
│ ├── router/ # GoRouter 路由配置
│ ├── theme/ # AppTheme 主题定义
│ └── localization/ # AppLocalizations 国际化
├── features/ # 功能模块
│ ├── home/ # 首页(设备控制面板)
│ ├── programs/ # 程序管理
│ ├── program_detail/ # 程序详情/步骤配置
│ ├── device/ # 设备运行控制
│ └── settings/ # 系统设置
└── shared/ # 共享组件
├── widgets/ # 通用组件 (CommonButton, CommonCard, StatusIndicator)
└── utils/ # 常量定义 (Constants)
```
### 技术栈
| 层级 | 技术 | 说明 |
|------|------|------|
| 状态管理 | flutter_riverpod | StateNotifier + Provider 模式 |
| 路由 | go_router | 声明式路由,支持路径参数 |
| 数据持久化 | sqflite | SQLite 本地数据库 |
| 国际化 | intl + flutter_localizations | 中/英双语 |
### 核心数据模型
**Program** (程序): `id`, `code`, `name`, `createdAt`, `status(1启用/0停用)`
**Step** (步骤): `id`, `programId`, `stepNo`, `position`, `name`, `mixTime`, `magnetTime`, `volume`, `mixSpeed`, `blowSpeed`, `blowTime`, `needleSpeed`
### 关键 Provider
- `programsProvider` - 程序列表状态 (StateNotifier)
- `runStateProvider` - 设备运行状态 (RunStatus: idle/running/paused/completed/error)
- `goRouterProvider` - 路由配置
### 数据库
DatabaseService 为单例模式,表结构:
- `programs` - 程序表
- `steps` - 步骤表 (外键关联 program_id)
### 国际化
AppLocalizations 支持中文(zh)和英文(en),使用 `AppLocalizations.of(context)` 获取翻译。
## 代码规范
- Dart SDK 版本: ^3.11.5
- 使用 `flutter_lints` 包的推荐 lint 规则
- 代码风格遵循 Flutter 官方最佳实践
## 功能模块
根据需求文档,应用包含以下核心模块:
### 1. 首页模块(设备控制面板)
- 状态栏:设备名称、实时时钟、系统状态、照明控制
- 程序列表:卡片展示、程序选择、查看详情
- 运行控制:启动/暂停/停止程序
- 运行状态监控:当前步骤、剩余时间、总进度
### 2. 程序管理模块
- 程序列表:编号、名称、创建时间、状态
- CRUD 操作:新增、编辑、删除、导入程序
### 3. 程序详情模块
- 步骤管理:步骤列表、排序、增删改
- 步骤参数:孔位、混合时间、吸磁时间、容积、速度等
### 4. 系统设置模块
- 软件升级、语言切换(中文/英文、密码修改、U盘导入
## 步骤参数说明
| 参数 | 说明 | 取值范围 |
|------|------|----------|
| 孔位 | 操作位置 | A1, A2, B1... |
| 混合时间 | 混合持续时间 | 正整数(秒) |
| 吸磁时间 | 磁珠吸附时间 | 正整数(秒) |
| 容积 | 液体体积 | 正整数(μL) |
| 混合/吹气速度 | 操作速度 | 低速/中速/高速 |
| 下针速度 | 针头下移速度 | 1-10档 |
## 当前实现状态
已完成基础架构搭建,实现了:
- 首页:状态栏、程序列表、运行控制面板、运行状态监控
- 程序管理:列表展示、新增/编辑/删除、状态切换
- 程序详情:步骤列表、步骤参数编辑
- 运行控制:启动/暂停/停止、进度监控(使用 MockRunner 模拟)
- 系统设置:基础页面框架
- 国际化:中/英文翻译配置
待完善设备通信对接、实际硬件控制、U盘导入功能。
## UI 设计稿
**Stitch 项目链接**: https://stitch.withgoogle.com/projects/16230138564963723693
### 页面截图链接
| 页面 | 功能描述 | 截图链接 |
|------|----------|----------|
| 首页 - 待机状态 | 状态栏 + 程序列表 + 开始运行按钮 + 瓷套棒确认 | [查看](https://lh3.googleusercontent.com/aida/ADBb0ujbe00elAXxTbD-KomnaatYJ40X-ubwTDSaI--jR_FRjPow3DJn7S61WpX9NxIMfgAyup6KXC_5qQWeZi0G9qQakQpvszL-OiaTU0C_yQ5FB-E1UMrystcYL0inmiky1Z2Ai-18NSYAdr0dJ6bbTp_dmJ4-JgmK0bjqdoUSnqlzo_Q1SwyNmbJb8WdNTKIUoXNudgLOCZURLWzONviyPgs9CqlxS1KUvopuxJymQF26kwxFO33lY6_vdLzD) |
| 首页 - 运行状态 | 实时监控:孔位、步骤、倒计时、进度条、暂停/停止控制 | [查看](https://lh3.googleusercontent.com/aida/ADBb0uj1bjMG6Fu-6-yvfL-Y51mJws-NBwlkX-GCbBBjLjb-4gNvMB_y8AESOu7X-UF_SHggCttMNpY6g88WgGOSJjw-1NOyTyigDBRInEwawYt8aXSGIGknLVw6zK_99aFnfturjzoYySuHBhKP2r34XWvfAO1g0e-as3hWBOaJW0gb2w8DSLmv3MpGLvsedJNq4SQFhl50LtNFM_zCMKjv-NtqyCE3y1vndNxnxCrJUy_BHvh4XHCahf18UnOU) |
| 程序管理 - 列表视图 | 程序表格:复选框、编号、名称、状态、批量操作、分页 | [查看](https://lh3.googleusercontent.com/aida/ADBb0ujE1ZBh5_Gn-yyy1bfU-U_xCeGHREsycgxyAlWBKzWbhFzvlusSWhCEY1u6RBRqVk5XZ49x8Ljpe4yXcS5e3q_jgGMGpcd1CN0RxX0nU6D75O056euNV7fY9cBNtTVGXibmM7Am1uj5uxV7TLbW8c_ix8WubRhn0gYFU6L87b80N6Zl-2T3lmBZsp98jwSsUXP3RqHk6CwMLoQ9LYcLRQTU5Dh5XD1e9aXus9I9pDgj-ZMUsFcZJA7XnJ0y) |
| 程序详情 - 步骤配置 | 左侧步骤列表 + 右侧参数表单(孔位、时间、速度等) | [查看](https://lh3.googleusercontent.com/aida/ADBb0ujEyurJOmwUkZx3ysQMSCk4pXU9_6Jh6bsEMhwlwyC88SQZ0hCFRHMhKADFecHdLXG3D-A5Hp3Avw25j1ivrP2CyTTdeXgd4QPCARNZetrezcL1iy0b7RYsVTTnXnzIFX846pRC_rxgbh8DdqMAiHPz8Q94JciAFCAokQfduFmDWw3Wzhj_P0KtlL8UmEIJNzGFi9ySTGo6eKmz6lfdfQ6VMowhYR5Qy8VGZi8qTLlXqFSU9f-dZepaJWs) |
| 系统设置 - 控制面板 | 软件升级、语言切换、密码修改、U盘导入 | [查看](https://lh3.googleusercontent.com/aida/ADBb0ugIEqBuSXmwYtQkPXXIJ7qBdL69VyQdTRzTyXjc5M6THuTBnbJlLyIDmvo7qfTz_RwjwZE_czzN6dyR_ydqk2Er5hNYduPLypOLqqR7I2sHaKDIvCeole91oJIVkAcnfJkWQJJPgaIhqfysMV4mrsHD0rhYJpngLorfE9WMl11tG9MPz9_E8XcmwaFqXqtYvxHeBsnvSNtIQfkX9S5QfnYtOiAsnUKioY3GpVvjK6bV4PYIfCMMHXv5koS8) |
| 运行完成 - 操作指引 | 成功提示 + 样本滴入说明 + 返回/重运行按钮 | [查看](https://lh3.googleusercontent.com/aida/ADBb0ujh5-3mBPpjWkSRHGOzNRQuusc4O7p_Kov2dbd3iiJmOHlElBb9-gPWC5bnfnRuDbhIbZDhxQhTfVkRpUhQU5nMGa5y7NpV4efvG6ZF0Bx4aVdm4HJ-o2Qca-alJuSfgsggJgbZ3QptA_cAXx-VfG79sHB1zUvRDHpPkLSu0lePhmjaR6m6-pwR42Pib7OLSNqS6V9208w7HoGCX1cPC6pH-hi8rZrMbMmbk2p-pTd2tJebNsow9aLb6bLJ) |
### 设计规范
- **主色调**: #2196F3(蓝色)
- **字体**: Inter
- **圆角**: 4px工业控制风格
- **配色模式**: Light + Tonal Spot
- **注意**: Stitch 生成的画布尺寸为 2560×2048实际开发需适配 1920×1080 横屏
---
## 其他说明
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设计必须支持此尺寸

17
README.md Normal file
View File

@@ -0,0 +1,17 @@
# kuaishai2
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.xiarui.kuaishai2"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.xiarui.kuaishai2"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,61 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Kiosk 模式所需权限 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:label="kuaishai2"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:showWhenLocked="true"
android:turnScreenOn="true"
android:resizeableActivity="false"
android:screenOrientation="landscape">
<!-- Kiosk: 设置为 Launcher防止退出后回到桌面 -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.HOME"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
</activity>
<!-- 开机自启广播接收器 -->
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true"
android:directBootAware="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@@ -0,0 +1,22 @@
package com.xiarui.kuaishai2
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
/**
* Kiosk 开机自启广播接收器
* 设备开机后自动启动 MainActivity
*/
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
intent.action == Intent.ACTION_LOCKED_BOOT_COMPLETED ||
intent.action == "android.intent.action.QUICKBOOT_POWERON") {
val startIntent = Intent(context, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(startIntent)
}
}
}

View File

@@ -0,0 +1,49 @@
package com.xiarui.kuaishai2
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.view.WindowManager
import io.flutter.embedding.android.FlutterActivity
/**
* Kiosk 模式主 Activity
* - 全屏沉浸(隐藏状态栏、导航栏)
* - 屏蔽 Back/Home/Recent 键
* - 保持屏幕常亮
*/
class MainActivity : FlutterActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 保持屏幕常亮
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
enableImmersiveMode()
}
}
private fun enableImmersiveMode() {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return when (keyCode) {
KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_HOME,
KeyEvent.KEYCODE_APP_SWITCH -> true // 屏蔽,防止退出 Kiosk
else -> super.onKeyDown(keyCode, event)
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

26
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,26 @@
allprojects {
repositories {
maven { url = uri("https://maven.aliyun.com/repository/google") }
maven { url = uri("https://maven.aliyun.com/repository/public") }
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@@ -0,0 +1,417 @@
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
# 1. 功能结构
**一、首页模块(设备控制面板)**
**1. 状态栏**
- 设备名称显示
- 实时时钟
- 系统状态显示
**2. 任务列表**
- 程序列表展示
- 程序信息显示
- 程序选择
- 选中状态标识
- 查看详情
**3. 运行控制**
- 程序选择确认
- 运行按钮
- 暂停 / 继续
- 停止按钮
**4. 运行状态监控**
- 当前程序显示
- 当前孔位
- 步骤序号
- 步骤名称
- 步骤剩余时间
- 总进度条
- 暂停 / 继续按钮
- 停止按钮
**5. 运行完成提示**
**5.1 样本滴入提示**
- 提示信息
- 操作指引
------------------------------------------------------------------------
**二、程序管理模块**
**1. 程序列表**
- 程序列表展示
- 编号显示
- 程序名称
- 创建时间
- 状态显示
- 多选功能
- 全选 / 取消全选
**2. 程序 CRUD 操作**
- 新增程序
- 编辑程序
- 删除程序
- 删除确认
- 文件导入
- 查看详情
------------------------------------------------------------------------
**三、程序详情模块**
**1. 步骤管理**
- 步骤列表展示
- 步骤参数显示
- 多选步骤
- 全选 / 取消全选
- 拖动排序
- 添加步骤
- 编辑步骤
- 删除步骤
- 返回按钮
**2. 步骤参数配置**
- 步骤编号
- 孔位
- 步骤名称
- 混合时间
- 吸磁时间
- 容积
- 混合速度
- 吹气速度
- 吹气时间
- 下针速度
------------------------------------------------------------------------
**四、系统设置模块**
**1. 软件升级**
- 版本显示
- 检查更新
- 更新提示
**2. 语言设置**
- 语言选择
- 实时切换
**3. 安全设置**
- 密码修改
- 原密码验证
- 新密码确认
- 密码一致性校验
**4. U 盘导入**
- 自动检测
- 程序导入
- 导入确认
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
# 2. 首页模块 - 设备控制面板
## 2.1 状态栏功能
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
设备名称显示 显示\"污水毒品前处理一体机\"设备标识
实时时钟 显示当前日期时间,格式: YYYY-MM-DD HH:mm:ss
系统状态显示 实时显示设备运行状态(运行中/未运行)
照明按钮 切换设备照明灯状态(亮/暗)
-----------------------------------------------------------------------
## 2.2 程序列表功能
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
程序列表展示 以卡片形式展示所有可用程序
程序信息显示 显示程序编号、名称、创建时间
程序选择 点击卡片可选择要运行的程序
选中状态标识 选中的程序卡片高亮显示并带勾选标记
查看详情 可直接跳转查看程序详细步骤配置
-----------------------------------------------------------------------
## 2.3 运行控制功能
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
程序选择确认 显示当前选中的程序名称
运行按钮 启动选中的程序运行(未选择程序时禁用)
瓷套棒确认 运行前确认是否已安装瓷套棒(硬件传感器检测支持)
暂停/继续 运行过程中可暂停和继续程序执行
停止按钮 终止当前运行的程序
停止确认 停止前需用户确认操作
-----------------------------------------------------------------------
## 2.4 运行状态监控
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
当前程序显示 显示正在运行的程序名称
当前孔位 显示当前操作的孔位
步骤序号 显示当前执行的步骤编号
步骤名称 显示当前步骤的名称
步骤剩余时间 倒计时显示当前步骤剩余时间(HH:MM:SS)
总进度条 显示程序总体完成百分比
暂停/继续按钮 运行过程中的暂停和继续控制
停止按钮 终止当前运行的程序
步骤参数 显示当前步骤所有设置参数
-----------------------------------------------------------------------
## 2.5 运行完成提示
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
提示信息 程序运行完成后自动跳转到提示页面
样本滴入指引 提示用户将样本滴入到检测卡
操作说明 显示详细操作步骤说明
-----------------------------------------------------------------------
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
# 3. 程序管理模块
## 3.1 程序列表管理
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
程序列表展示 以表格形式展示所有程序
编号显示 显示程序唯一编号
程序名称 显示程序名称
创建时间 显示程序创建日期
状态显示 显示程序状态(启用/停用)
多选功能 支持勾选多个程序进行批量操作
全选/取消全选 表头复选框控制全选状态
-----------------------------------------------------------------------
## 3.2 程序CRUD操作
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
新增程序 创建新程序,需填写编号和名称
编辑程序 修改程序的编号、名称、状态
删除程序 删除选中的程序,支持批量删除
删除确认 删除操作需用户确认
文件导入 从文件导入程序配置
查看详情 查看程序的详细步骤配置
-----------------------------------------------------------------------
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
# 4. 程序详情模块
## 4.1 步骤管理功能
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
步骤列表展示 以表格形式展示程序的所有步骤
步骤参数显示 显示步骤的完整参数配置
多选步骤 支持勾选多个步骤
全选/取消全选 表头复选框控制全选状态
拖动排序 支持拖动步骤行调整执行顺序
添加步骤 新增步骤并配置参数
编辑步骤 修改已有步骤的参数配置
删除步骤 删除选中的步骤,支持批量删除
返回按钮 返回主页面
-----------------------------------------------------------------------
## 4.2 步骤参数配置
----------------------------------------------------------------------------
**参数名称** **说明** **取值范围** **示例**
-------------- -------------------------- ----------------- ----------------
步骤编号 步骤执行顺序(可拖动调整) 正整数 1, 2, 3\...
孔位 操作的孔位位置 如A1、A2、B1等 A1, B2, C3
步骤名称 步骤的描述名称 文本 混合、吸磁\...
混合时间 混合操作的持续时间 秒(正整数) 60, 120\...
吸磁时间 磁珠吸附时间 秒(正整数) 30, 60\...
容积 液体体积 微升μL(正整数) 100, 200\...
混合速度 混合操作速度 低速/中速/高速 低速, 中速\...
吹气速度 吹气操作速度 低速/中速/高速 中速, 高速\...
吹气时间 吹气持续时间 分钟(正整数) 5, 10\...
下针速度 针头下移速度10档可选 1-10档 1档至10档
----------------------------------------------------------------------------
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
# 5. 系统设置模块
## 5.1 软件升级
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
版本显示 显示当前软件版本号(如V1.0.0)
检查更新 检查是否有新版本可用
更新提示 有新版本时提示用户更新
-----------------------------------------------------------------------
## 5.2 语言设置
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
语言选择 支持简体中文、English语言切换
实时切换 切换后立即生效
-----------------------------------------------------------------------
## 5.3 安全设置
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
密码修改 修改用户登录密码或操作密码
原密码验证 需输入原密码进行身份验证
新密码确认 需两次输入新密码进行确认
密码一致性校验 确保两次输入的新密码一致
-----------------------------------------------------------------------
## 5.4 U盘导入
-----------------------------------------------------------------------
**功能项** **描述**
----------------- -----------------------------------------------------
自动检测 自动检测U盘插入事件
程序导入 从U盘自动导入程序配置文件
导入确认 导入前显示确认信息,用户确认后执行导入
-----------------------------------------------------------------------
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
--- 文档结束 ---

View File

@@ -0,0 +1,164 @@
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
/// 数据库服务
class DatabaseService {
static final DatabaseService instance = DatabaseService._internal();
static Database? _database;
DatabaseService._internal();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'kuaishai.db');
return await openDatabase(
path,
version: 2,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
// 程序表
await db.execute('''
CREATE TABLE programs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
status INTEGER DEFAULT 1
)
''');
// 步骤表
await db.execute('''
CREATE TABLE steps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
program_id INTEGER NOT NULL,
step_no INTEGER NOT NULL,
position TEXT NOT NULL,
name TEXT NOT NULL,
mix_time INTEGER DEFAULT 0,
magnet_time INTEGER DEFAULT 0,
volume INTEGER DEFAULT 0,
mix_speed TEXT DEFAULT '中速',
blow_speed TEXT DEFAULT '中速',
blow_time INTEGER DEFAULT 0,
needle_speed INTEGER DEFAULT 5,
FOREIGN KEY (program_id) REFERENCES programs(id) ON DELETE CASCADE
)
''');
// 设置表(密码存储)
await db.execute('''
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
''');
// 初始化默认密码
await db.insert('settings', {'key': 'password', 'value': '123456'});
}
/// 数据库升级
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// 添加 settings 表
await db.execute('''
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
''');
// 初始化默认密码
await db.insert('settings', {'key': 'password', 'value': '123456'});
}
}
Future<void> close() async {
if (_database != null) {
await _database!.close();
_database = null;
}
}
/// 初始化测试数据(仅调试模式使用)
Future<void> initTestData() async {
final db = await database;
// 检查是否已有数据
final count = Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM programs'),
);
if (count != null && count > 0) return;
// 插入测试程序并添加步骤
final testPrograms = [
{'code': 'P001', 'name': '标准检测程序', 'created_at': '2026-05-19', 'status': 1},
{'code': 'P002', 'name': '快速检测程序', 'created_at': '2026-05-18', 'status': 1},
{'code': 'P003', 'name': '深度检测程序', 'created_at': '2026-05-17', 'status': 1},
{'code': 'P004', 'name': '样本预处理程序', 'created_at': '2026-05-16', 'status': 0},
{'code': 'P005', 'name': '磁珠分离程序', 'created_at': '2026-05-15', 'status': 1},
];
for (final program in testPrograms) {
final programId = await db.insert('programs', program);
// 为每个程序添加测试步骤
final testSteps = [
{
'program_id': programId,
'step_no': 1,
'position': 'A1',
'name': '混合',
'mix_time': 60,
'magnet_time': 0,
'volume': 100,
'mix_speed': '中速',
'blow_speed': '中速',
'blow_time': 0,
'needle_speed': 5,
},
{
'program_id': programId,
'step_no': 2,
'position': 'A1',
'name': '吸磁',
'mix_time': 0,
'magnet_time': 30,
'volume': 0,
'mix_speed': '中速',
'blow_speed': '中速',
'blow_time': 0,
'needle_speed': 5,
},
{
'program_id': programId,
'step_no': 3,
'position': 'A2',
'name': '吹气',
'mix_time': 0,
'magnet_time': 0,
'volume': 0,
'mix_speed': '中速',
'blow_speed': '高速',
'blow_time': 10,
'needle_speed': 8,
},
];
for (final step in testSteps) {
await db.insert('steps', step);
}
}
}
}

View File

@@ -0,0 +1,366 @@
import 'package:flutter/material.dart';
/// 应用国际化配置
class AppLocalizations {
final Locale locale;
AppLocalizations(this.locale);
static AppLocalizations? of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> delegate =
_AppLocalizationsDelegate();
// 状态栏
String get deviceName => _localizedValues[locale.languageCode]?['deviceName'] ?? '污水毒品前处理一体机';
String get running => _localizedValues[locale.languageCode]?['running'] ?? '运行中';
String get idle => _localizedValues[locale.languageCode]?['idle'] ?? '未运行';
String get lighting => _localizedValues[locale.languageCode]?['lighting'] ?? '照明';
// 程序管理
String get programs => _localizedValues[locale.languageCode]?['programs'] ?? '程序管理';
String get programList => _localizedValues[locale.languageCode]?['programList'] ?? '程序列表';
String get programName => _localizedValues[locale.languageCode]?['programName'] ?? '程序名称';
String get programCode => _localizedValues[locale.languageCode]?['programCode'] ?? '程序编号';
String get createTime => _localizedValues[locale.languageCode]?['createTime'] ?? '创建时间';
String get addProgram => _localizedValues[locale.languageCode]?['addProgram'] ?? '新增程序';
String get editProgram => _localizedValues[locale.languageCode]?['editProgram'] ?? '编辑程序';
String get deleteProgram => _localizedValues[locale.languageCode]?['deleteProgram'] ?? '删除程序';
String get importProgram => _localizedValues[locale.languageCode]?['importProgram'] ?? '导入程序';
String get viewDetails => _localizedValues[locale.languageCode]?['viewDetails'] ?? '查看详情';
String get selectedProgram => _localizedValues[locale.languageCode]?['selectedProgram'] ?? '当前选中程序';
String get selectedProgramLabel => _localizedValues[locale.languageCode]?['selectedProgramLabel'] ?? '当前选中';
String get availablePrograms => _localizedValues[locale.languageCode]?['availablePrograms'] ?? '可用程序';
String get ceramicNotInstalled => _localizedValues[locale.languageCode]?['ceramicNotInstalled'] ?? '瓷套棒: 未安装 — 禁止启动';
String get ceramicInstalled => _localizedValues[locale.languageCode]?['ceramicInstalled'] ?? '瓷套棒: 已安装';
String get runningMonitor => _localizedValues[locale.languageCode]?['runningMonitor'] ?? '运行状态监控';
String get currentHole => _localizedValues[locale.languageCode]?['currentHole'] ?? '当前孔位';
String get stepParams => _localizedValues[locale.languageCode]?['stepParams'] ?? '步骤参数';
String get speed => _localizedValues[locale.languageCode]?['speed'] ?? '转速';
String get temperature => _localizedValues[locale.languageCode]?['temperature'] ?? '温度';
String get duration => _localizedValues[locale.languageCode]?['duration'] ?? '持续时间';
String get sampleVolume => _localizedValues[locale.languageCode]?['sampleVolume'] ?? '样品体积';
String get pleaseSelectProgram => _localizedValues[locale.languageCode]?['pleaseSelectProgram'] ?? '请选择要运行的程序';
// 运行控制
String get run => _localizedValues[locale.languageCode]?['run'] ?? '运行';
String get pause => _localizedValues[locale.languageCode]?['pause'] ?? '暂停';
String get continue_ => _localizedValues[locale.languageCode]?['continue'] ?? '继续';
String get stop => _localizedValues[locale.languageCode]?['stop'] ?? '停止';
String get startRun => _localizedValues[locale.languageCode]?['startRun'] ?? '开始运行';
String get currentStep => _localizedValues[locale.languageCode]?['currentStep'] ?? '当前步骤';
String get remainingTime => _localizedValues[locale.languageCode]?['remainingTime'] ?? '剩余时间';
String get progress => _localizedValues[locale.languageCode]?['progress'] ?? '进度';
String get ceramicSleeveConfirm => _localizedValues[locale.languageCode]?['ceramicSleeveConfirm'] ?? '运行前请确认已安装瓷套棒';
String get paused => _localizedValues[locale.languageCode]?['paused'] ?? '已暂停';
String get stopConfirm => _localizedValues[locale.languageCode]?['stopConfirm'] ?? '确定要停止当前运行的程序吗?';
String get currentProgram => _localizedValues[locale.languageCode]?['currentProgram'] ?? '当前程序';
String get backToHome => _localizedValues[locale.languageCode]?['backToHome'] ?? '返回首页';
String get runAgain => _localizedValues[locale.languageCode]?['runAgain'] ?? '重新运行';
String get deleteConfirm => _localizedValues[locale.languageCode]?['deleteConfirm'] ?? '确定要删除此程序吗?';
// 步骤参数
String get stepNo => _localizedValues[locale.languageCode]?['stepNo'] ?? '步骤编号';
String get position => _localizedValues[locale.languageCode]?['position'] ?? '孔位';
String get stepName => _localizedValues[locale.languageCode]?['stepName'] ?? '步骤名称';
String get mixTime => _localizedValues[locale.languageCode]?['mixTime'] ?? '混合时间';
String get magnetTime => _localizedValues[locale.languageCode]?['magnetTime'] ?? '吸磁时间';
String get volume => _localizedValues[locale.languageCode]?['volume'] ?? '容积';
String get mixSpeed => _localizedValues[locale.languageCode]?['mixSpeed'] ?? '混合速度';
String get blowSpeed => _localizedValues[locale.languageCode]?['blowSpeed'] ?? '吹气速度';
String get blowTime => _localizedValues[locale.languageCode]?['blowTime'] ?? '吹气时间';
String get needleSpeed => _localizedValues[locale.languageCode]?['needleSpeed'] ?? '下针速度';
// 速度选项
String get lowSpeed => _localizedValues[locale.languageCode]?['lowSpeed'] ?? '低速';
String get mediumSpeed => _localizedValues[locale.languageCode]?['mediumSpeed'] ?? '中速';
String get highSpeed => _localizedValues[locale.languageCode]?['highSpeed'] ?? '高速';
// 设置
String get settings => _localizedValues[locale.languageCode]?['settings'] ?? '系统设置';
String get language => _localizedValues[locale.languageCode]?['language'] ?? '语言设置';
String get password => _localizedValues[locale.languageCode]?['password'] ?? '密码修改';
String get upgrade => _localizedValues[locale.languageCode]?['upgrade'] ?? '软件升级';
String get usbImport => _localizedValues[locale.languageCode]?['usbImport'] ?? 'U盘导入';
// 通用
String get confirm => _localizedValues[locale.languageCode]?['confirm'] ?? '确认';
String get cancel => _localizedValues[locale.languageCode]?['cancel'] ?? '取消';
String get save => _localizedValues[locale.languageCode]?['save'] ?? '保存';
String get delete => _localizedValues[locale.languageCode]?['delete'] ?? '删除';
String get select => _localizedValues[locale.languageCode]?['select'] ?? '选择';
String get selected => _localizedValues[locale.languageCode]?['selected'] ?? '已选择';
String get detail => _localizedValues[locale.languageCode]?['detail'] ?? '详情';
String get noData => _localizedValues[locale.languageCode]?['noData'] ?? '暂无数据';
// 完成提示
String get runComplete => _localizedValues[locale.languageCode]?['runComplete'] ?? '运行完成';
String get sampleDropGuide => _localizedValues[locale.languageCode]?['sampleDropGuide'] ?? '请将样本滴入检测卡';
// 补充缺失的翻译
String get lightOn => _localizedValues[locale.languageCode]?['lightOn'] ?? '';
String get lightOff => _localizedValues[locale.languageCode]?['lightOff'] ?? '';
String get enabled => _localizedValues[locale.languageCode]?['enabled'] ?? '启用';
String get disabled => _localizedValues[locale.languageCode]?['disabled'] ?? '停用';
String get stepList => _localizedValues[locale.languageCode]?['stepList'] ?? '步骤列表';
String get operationSteps => _localizedValues[locale.languageCode]?['operationSteps'] ?? '操作步骤';
String get addStep => _localizedValues[locale.languageCode]?['addStep'] ?? '添加步骤';
String get editStep => _localizedValues[locale.languageCode]?['editStep'] ?? '编辑步骤';
String get deleteStep => _localizedValues[locale.languageCode]?['deleteStep'] ?? '删除步骤';
String get deleteStepConfirm => _localizedValues[locale.languageCode]?['deleteStepConfirm'] ?? '确定要删除此步骤吗?';
String get stepsCount => _localizedValues[locale.languageCode]?['stepsCount'] ?? '';
String get noSteps => _localizedValues[locale.languageCode]?['noSteps'] ?? '暂无步骤';
String get selectStepFirst => _localizedValues[locale.languageCode]?['selectStepFirst'] ?? '请选择或添加步骤';
String get oldPassword => _localizedValues[locale.languageCode]?['oldPassword'] ?? '原密码';
String get newPassword => _localizedValues[locale.languageCode]?['newPassword'] ?? '新密码';
String get confirmPassword => _localizedValues[locale.languageCode]?['confirmPassword'] ?? '确认新密码';
String get passwordMinLength => _localizedValues[locale.languageCode]?['passwordMinLength'] ?? '至少6位字符';
String get passwordChanged => _localizedValues[locale.languageCode]?['passwordChanged'] ?? '密码已修改';
String get passwordChangeFailed => _localizedValues[locale.languageCode]?['passwordChangeFailed'] ?? '密码修改失败';
String get oldPasswordError => _localizedValues[locale.languageCode]?['oldPasswordError'] ?? '原密码错误';
String get passwordMismatch => _localizedValues[locale.languageCode]?['passwordMismatch'] ?? '两次输入的新密码不一致';
String get fillAllFields => _localizedValues[locale.languageCode]?['fillAllFields'] ?? '请填写所有字段';
String get importSuccess => _localizedValues[locale.languageCode]?['importSuccess'] ?? '成功导入';
String get importFailed => _localizedValues[locale.languageCode]?['importFailed'] ?? '导入失败';
String get programsImported => _localizedValues[locale.languageCode]?['programsImported'] ?? '个程序';
String get usbDetected => _localizedValues[locale.languageCode]?['usbDetected'] ?? '检测到U盘';
String get usbNotDetected => _localizedValues[locale.languageCode]?['usbNotDetected'] ?? '未检测到U盘';
String get insertUsb => _localizedValues[locale.languageCode]?['insertUsb'] ?? '请插入U盘后重试';
String get detectingUsb => _localizedValues[locale.languageCode]?['detectingUsb'] ?? '正在检测U盘...';
String get currentVersion => _localizedValues[locale.languageCode]?['currentVersion'] ?? '当前版本';
String get latestVersion => _localizedValues[locale.languageCode]?['latestVersion'] ?? '已是最新版本';
String get updateAvailable => _localizedValues[locale.languageCode]?['updateAvailable'] ?? '有新版本可用';
String get checkUpdate => _localizedValues[locale.languageCode]?['checkUpdate'] ?? '检查更新';
static final Map<String, Map<String, String>> _localizedValues = {
'zh': {
'deviceName': '污水毒品前处理一体机',
'running': '运行中',
'idle': '未运行',
'lighting': '照明',
'programs': '程序管理',
'programList': '程序列表',
'programName': '程序名称',
'programCode': '程序编号',
'createTime': '创建时间',
'addProgram': '新增程序',
'editProgram': '编辑程序',
'deleteProgram': '删除程序',
'importProgram': '导入程序',
'viewDetails': '查看详情',
'selectedProgram': '当前选中程序',
'selectedProgramLabel': '当前选中',
'availablePrograms': '可用程序',
'ceramicNotInstalled': '瓷套棒: 未安装 — 禁止启动',
'ceramicInstalled': '瓷套棒: 已安装',
'runningMonitor': '运行状态监控',
'currentHole': '当前孔位',
'stepParams': '步骤参数',
'speed': '转速',
'temperature': '温度',
'duration': '持续时间',
'sampleVolume': '样品体积',
'pleaseSelectProgram': '请选择要运行的程序',
'run': '运行',
'pause': '暂停',
'continue': '继续',
'stop': '停止',
'startRun': '开始运行',
'currentStep': '当前步骤',
'remainingTime': '剩余时间',
'progress': '进度',
'ceramicSleeveConfirm': '运行前请确认已安装瓷套棒',
'paused': '已暂停',
'stopConfirm': '确定要停止当前运行的程序吗?',
'currentProgram': '当前程序',
'backToHome': '返回首页',
'runAgain': '重新运行',
'deleteConfirm': '确定要删除此程序吗?',
'stepNo': '步骤编号',
'position': '孔位',
'stepName': '步骤名称',
'mixTime': '混合时间',
'magnetTime': '吸磁时间',
'volume': '容积',
'mixSpeed': '混合速度',
'blowSpeed': '吹气速度',
'blowTime': '吹气时间',
'needleSpeed': '下针速度',
'lowSpeed': '低速',
'mediumSpeed': '中速',
'highSpeed': '高速',
'settings': '系统设置',
'language': '语言设置',
'password': '密码修改',
'upgrade': '软件升级',
'usbImport': 'U盘导入',
'confirm': '确认',
'cancel': '取消',
'save': '保存',
'delete': '删除',
'select': '选择',
'selected': '已选择',
'detail': '详情',
'noData': '暂无数据',
'runComplete': '运行完成',
'sampleDropGuide': '请将样本滴入检测卡',
'lightOn': '',
'lightOff': '',
'enabled': '启用',
'disabled': '停用',
'stepList': '步骤列表',
'operationSteps': '操作步骤',
'addStep': '添加步骤',
'editStep': '编辑步骤',
'deleteStep': '删除步骤',
'deleteStepConfirm': '确定要删除此步骤吗?',
'stepsCount': '',
'noSteps': '暂无步骤',
'selectStepFirst': '请选择或添加步骤',
'oldPassword': '原密码',
'newPassword': '新密码',
'confirmPassword': '确认新密码',
'passwordMinLength': '至少6位字符',
'passwordChanged': '密码已修改',
'passwordChangeFailed': '密码修改失败',
'oldPasswordError': '原密码错误',
'passwordMismatch': '两次输入的新密码不一致',
'fillAllFields': '请填写所有字段',
'importSuccess': '成功导入',
'importFailed': '导入失败',
'programsImported': '个程序',
'usbDetected': '检测到U盘',
'usbNotDetected': '未检测到U盘',
'insertUsb': '请插入U盘后重试',
'detectingUsb': '正在检测U盘...',
'currentVersion': '当前版本',
'latestVersion': '已是最新版本',
'updateAvailable': '有新版本可用',
'checkUpdate': '检查更新',
},
'en': {
'deviceName': 'Wastewater Drug Pretreatment System',
'running': 'Running',
'idle': 'Idle',
'lighting': 'Lighting',
'programs': 'Programs',
'programList': 'Program List',
'programName': 'Program Name',
'programCode': 'Program Code',
'createTime': 'Create Time',
'addProgram': 'Add Program',
'editProgram': 'Edit Program',
'deleteProgram': 'Delete Program',
'importProgram': 'Import Program',
'viewDetails': 'View Details',
'selectedProgram': 'Selected Program',
'selectedProgramLabel': 'Selected',
'availablePrograms': 'Available Programs',
'ceramicNotInstalled': 'Ceramic sleeve: Not installed — Cannot start',
'ceramicInstalled': 'Ceramic sleeve: Installed',
'runningMonitor': 'Running Status Monitor',
'currentHole': 'Current Position',
'stepParams': 'Step Parameters',
'speed': 'Speed',
'temperature': 'Temperature',
'duration': 'Duration',
'sampleVolume': 'Sample Volume',
'pleaseSelectProgram': 'Please select a program',
'run': 'Run',
'pause': 'Pause',
'continue': 'Continue',
'stop': 'Stop',
'startRun': 'Start Run',
'currentStep': 'Current Step',
'remainingTime': 'Remaining',
'progress': 'Progress',
'ceramicSleeveConfirm': 'Please confirm ceramic sleeve is installed',
'paused': 'Paused',
'stopConfirm': 'Are you sure to stop the running program?',
'currentProgram': 'Current Program',
'backToHome': 'Back to Home',
'runAgain': 'Run Again',
'deleteConfirm': 'Are you sure to delete this program?',
'stepNo': 'Step No.',
'position': 'Position',
'stepName': 'Step Name',
'mixTime': 'Mix Time',
'magnetTime': 'Magnet Time',
'volume': 'Volume',
'mixSpeed': 'Mix Speed',
'blowSpeed': 'Blow Speed',
'blowTime': 'Blow Time',
'needleSpeed': 'Needle Speed',
'lowSpeed': 'Low',
'mediumSpeed': 'Medium',
'highSpeed': 'High',
'settings': 'Settings',
'language': 'Language',
'password': 'Password',
'upgrade': 'Upgrade',
'usbImport': 'USB Import',
'confirm': 'Confirm',
'cancel': 'Cancel',
'save': 'Save',
'delete': 'Delete',
'select': 'Select',
'selected': 'Selected',
'detail': 'Detail',
'noData': 'No Data',
'runComplete': 'Complete',
'sampleDropGuide': 'Drop sample to test card',
'lightOn': 'On',
'lightOff': 'Off',
'enabled': 'Enabled',
'disabled': 'Disabled',
'stepList': 'Step List',
'operationSteps': 'Operation Steps',
'addStep': 'Add Step',
'editStep': 'Edit Step',
'deleteStep': 'Delete Step',
'deleteStepConfirm': 'Are you sure to delete this step?',
'stepsCount': 'steps',
'noSteps': 'No steps',
'selectStepFirst': 'Please select or add a step',
'oldPassword': 'Old Password',
'newPassword': 'New Password',
'confirmPassword': 'Confirm Password',
'passwordMinLength': 'At least 6 characters',
'passwordChanged': 'Password changed',
'passwordChangeFailed': 'Password change failed',
'oldPasswordError': 'Old password incorrect',
'passwordMismatch': 'Passwords do not match',
'fillAllFields': 'Please fill all fields',
'importSuccess': 'Successfully imported',
'importFailed': 'Import failed',
'programsImported': 'programs',
'usbDetected': 'USB detected',
'usbNotDetected': 'USB not detected',
'insertUsb': 'Please insert USB and try again',
'detectingUsb': 'Detecting USB...',
'currentVersion': 'Current Version',
'latestVersion': 'Already latest version',
'updateAvailable': 'Update available',
'checkUpdate': 'Check Update',
},
};
}
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
bool isSupported(Locale locale) {
return ['zh', 'en'].contains(locale.languageCode);
}
@override
Future<AppLocalizations> load(Locale locale) async {
return AppLocalizations(locale);
}
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}

View File

@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Locale 状态 Notifier
class LocaleNotifier extends StateNotifier<Locale> {
static const String _key = 'app_locale';
LocaleNotifier() : super(const Locale('zh', 'CN')) {
_loadLocale();
}
/// 从本地存储加载语言设置
Future<void> _loadLocale() async {
final prefs = await SharedPreferences.getInstance();
final localeCode = prefs.getString(_key);
if (localeCode != null) {
state = Locale(localeCode, localeCode == 'zh' ? 'CN' : 'US');
}
}
/// 切换语言
Future<void> setLocale(Locale locale) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_key, locale.languageCode);
state = locale;
}
/// 切换为中文
Future<void> setChinese() async {
await setLocale(const Locale('zh', 'CN'));
}
/// 切换为英文
Future<void> setEnglish() async {
await setLocale(const Locale('en', 'US'));
}
}
/// Locale Provider
final localeProvider = StateNotifierProvider<LocaleNotifier, Locale>((ref) {
return LocaleNotifier();
});
/// 当前语言是否为中文
final isChineseProvider = Provider<bool>((ref) {
return ref.watch(localeProvider).languageCode == 'zh';
});

View File

@@ -0,0 +1,45 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../features/home/pages/home_page.dart';
import '../../features/programs/pages/programs_page.dart';
import '../../features/program_detail/pages/program_detail_page.dart';
import '../../features/settings/pages/settings_page.dart';
import '../../features/home/pages/complete_page.dart';
/// 应用路由配置
final goRouterProvider = Provider<GoRouter>((ref) {
return GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
name: 'home',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/programs',
name: 'programs',
builder: (context, state) => const ProgramsPage(),
),
GoRoute(
path: '/programs/:id',
name: 'programDetail',
builder: (context, state) {
final id = state.pathParameters['id'];
return ProgramDetailPage(programId: id ?? '');
},
),
GoRoute(
path: '/settings',
name: 'settings',
builder: (context, state) => const SettingsPage(),
),
GoRoute(
path: '/complete',
name: 'complete',
builder: (context, state) => const CompletePage(),
),
],
);
});

View File

@@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
/// 应用主题配置 - 明亮工业控制风格
/// 主色 #2196F3圆角 4px明亮背景适配 1920x1080 横屏
class AppTheme {
// ========== 主色 ==========
static const Color primaryColor = Color(0xFF2196F3);
static const Color primaryDark = Color(0xFF1976D2);
static const Color primaryLight = Color(0xFFBBDEFB);
// ========== 功能色 ==========
static const Color successColor = Color(0xFF4CAF50);
static const Color warningColor = Color(0xFFFF9800);
static const Color errorColor = Color(0xFFF44336);
static const Color infoColor = Color(0xFF00BCD4);
// ========== 背景色(明亮) ==========
static const Color bgPage = Color(0xFFF5F7FA);
static const Color bgDeep = Color(0xFFE8ECF0);
static const Color bgSurface = Color(0xFFFFFFFF);
static const Color bgCard = Color(0xFFFFFFFF);
static const Color bgCardHover = Color(0xFFF0F7FF);
static const Color bgSidebar = Color(0xFFF0F2F5);
// ========== 文本色 ==========
static const Color textHeading = Color(0xFF1A1A2E);
static const Color textPrimary = Color(0xFF333344);
static const Color textSecondary = Color(0xFF6B7280);
static const Color textTertiary = Color(0xFF9CA3AF);
static const Color textOnPrimary = Colors.white;
// ========== 状态色 ==========
static const Color statusRunning = Color(0xFF4CAF50);
static const Color statusStopped = Color(0xFF9CA3AF);
static const Color statusPaused = Color(0xFFFF9800);
static const Color statusError = Color(0xFFF44336);
// ========== 卡片背景 ==========
static const Color cardBg = Color(0xFFFFFFFF);
static const Color cardSelectedBg = Color(0xFFE3F2FD);
// ========== 功能色accent ==========
static const Color accentPrimary = primaryColor;
static const Color accentInfo = infoColor;
static const Color accentWarning = warningColor;
static const Color accentCritical = errorColor;
// ========== 边框色 ==========
static const Color borderLight = Color(0xFFE5E7EB);
static const Color borderMedium = Color(0xFFD1D5DB);
static const Color borderSubtle = borderLight;
static const Color borderFocus = primaryColor;
// ========== 圆角 ==========
static const double radiusSm = 4.0;
static const double radiusMd = 8.0;
static const double radiusLg = 12.0;
// ========== 阴影 ==========
static const List<BoxShadow> shadowCard = [
BoxShadow(
color: Color(0x0A000000),
blurRadius: 8,
offset: Offset(0, 2),
),
];
static const List<BoxShadow> shadowCardHover = [
BoxShadow(
color: Color(0x14000000),
blurRadius: 12,
offset: Offset(0, 4),
),
];
// ========== 兼容旧代码的颜色别名 ==========
static const Color runningColor = statusRunning;
static const Color idleColor = statusStopped;
static const Color backgroundColor = bgPage;
static const Color cardColor = bgCard;
/// 亮色主题 - 明亮工业风格
static ThemeData lightTheme() {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.light,
),
scaffoldBackgroundColor: bgPage,
fontFamily: 'Inter',
cardTheme: CardThemeData(
color: bgCard,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
side: const BorderSide(color: borderLight, width: 1),
),
margin: EdgeInsets.zero,
),
appBarTheme: const AppBarTheme(
backgroundColor: bgSurface,
foregroundColor: textHeading,
elevation: 0,
centerTitle: false,
scrolledUnderElevation: 1,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: textOnPrimary,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: borderMedium),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: borderMedium),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: const BorderSide(color: primaryColor, width: 2),
),
filled: true,
fillColor: bgSurface,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
dialogTheme: DialogThemeData(
backgroundColor: bgSurface,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: textHeading,
contentTextStyle: const TextStyle(color: Colors.white, fontSize: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
behavior: SnackBarBehavior.floating,
),
listTileTheme: const ListTileThemeData(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(radiusSm)),
),
),
dataTableTheme: DataTableThemeData(
headingRowColor: WidgetStateProperty.all(bgSidebar),
dividerThickness: 1,
),
dividerTheme: const DividerThemeData(
color: borderLight,
thickness: 1,
),
);
}
/// 暗色主题(与亮色主题风格一致的暗色模式)
static ThemeData darkTheme() {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor,
brightness: Brightness.dark,
),
scaffoldBackgroundColor: const Color(0xFF121212),
);
}
}

View File

@@ -0,0 +1,73 @@
/// 设备状态模型
enum DeviceStatus { idle, running, paused, error }
/// 设备状态数据
class DeviceState {
final DeviceStatus status;
final String? currentProgram;
final String? currentPosition;
final int? currentStepNo;
final String? currentStepName;
final int? remainingSeconds;
final double? progress;
final bool lightingOn;
DeviceState({
this.status = DeviceStatus.idle,
this.currentProgram,
this.currentPosition,
this.currentStepNo,
this.currentStepName,
this.remainingSeconds,
this.progress,
this.lightingOn = false,
});
bool get isRunning => status == DeviceStatus.running;
bool get isPaused => status == DeviceStatus.paused;
bool get isIdle => status == DeviceStatus.idle;
bool get hasError => status == DeviceStatus.error;
String statusText() {
switch (status) {
case DeviceStatus.running:
return '运行中';
case DeviceStatus.paused:
return '已暂停';
case DeviceStatus.error:
return '错误';
case DeviceStatus.idle:
return '未运行';
}
}
String formatRemainingTime() {
if (remainingSeconds == null) return '--:--:--';
final hours = remainingSeconds! ~/ 3600;
final minutes = (remainingSeconds! % 3600) ~/ 60;
final seconds = remainingSeconds! % 60;
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
DeviceState copyWith({
DeviceStatus? status,
String? currentProgram,
String? currentPosition,
int? currentStepNo,
String? currentStepName,
int? remainingSeconds,
double? progress,
bool? lightingOn,
}) {
return DeviceState(
status: status ?? this.status,
currentProgram: currentProgram ?? this.currentProgram,
currentPosition: currentPosition ?? this.currentPosition,
currentStepNo: currentStepNo ?? this.currentStepNo,
currentStepName: currentStepName ?? this.currentStepName,
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
progress: progress ?? this.progress,
lightingOn: lightingOn ?? this.lightingOn,
);
}
}

View File

@@ -0,0 +1,188 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
import '../../programs/services/program_service.dart';
import '../services/mock_runner.dart';
/// 运行状态枚举
enum RunStatus {
idle, // 待机
running, // 运行中
paused, // 已暂停
completed,// 已完成
error, // 错误
}
/// 运行状态
class RunState {
final RunStatus status;
final Program? currentProgram;
final List<Step> steps;
final int currentStepIndex;
final int remainingSeconds;
final double progress;
final String? currentWell;
const RunState({
this.status = RunStatus.idle,
this.currentProgram,
this.steps = const [],
this.currentStepIndex = 0,
this.remainingSeconds = 0,
this.progress = 0,
this.currentWell,
});
RunState copyWith({
RunStatus? status,
Program? currentProgram,
List<Step>? steps,
int? currentStepIndex,
int? remainingSeconds,
double? progress,
String? currentWell,
bool clearProgram = false,
bool clearWell = false,
}) {
return RunState(
status: status ?? this.status,
currentProgram: clearProgram ? null : (currentProgram ?? this.currentProgram),
steps: steps ?? this.steps,
currentStepIndex: currentStepIndex ?? this.currentStepIndex,
remainingSeconds: remainingSeconds ?? this.remainingSeconds,
progress: progress ?? this.progress,
currentWell: clearWell ? null : (currentWell ?? this.currentWell),
);
}
/// 获取当前步骤
Step? get currentStep {
if (steps.isEmpty || currentStepIndex >= steps.length) return null;
return steps[currentStepIndex];
}
/// 格式化剩余时间 (HH:MM:SS)
String get formattedRemainingTime {
final hours = remainingSeconds ~/ 3600;
final minutes = (remainingSeconds % 3600) ~/ 60;
final seconds = remainingSeconds % 60;
return '${hours.toString().padLeft(2, '0')}:'
'${minutes.toString().padLeft(2, '0')}:'
'${seconds.toString().padLeft(2, '0')}';
}
/// 格式化进度百分比
String get formattedProgress {
return '${(progress * 100).toStringAsFixed(0)}%';
}
}
/// 运行状态 Notifier
class RunStateNotifier extends StateNotifier<RunState> {
final MockRunner _runner;
final ProgramService _programService;
RunStateNotifier(this._runner, this._programService) : super(const RunState());
/// 开始运行程序
Future<void> start(Program program) async {
// 获取程序步骤(这里使用模拟数据,实际应从数据库读取)
final steps = await _loadSteps(program.id!);
if (steps.isEmpty) {
state = state.copyWith(status: RunStatus.error);
return;
}
state = state.copyWith(
status: RunStatus.running,
currentProgram: program,
steps: steps,
currentStepIndex: 0,
progress: 0,
);
_runner.start(
program,
steps,
(stepIndex, remaining, progress, well) {
state = state.copyWith(
currentStepIndex: stepIndex,
remainingSeconds: remaining,
progress: progress,
currentWell: well,
);
},
() {
state = state.copyWith(
status: RunStatus.completed,
progress: 1,
clearWell: true,
);
},
);
}
/// 暂停运行
void pause() {
if (state.status == RunStatus.running) {
_runner.pause();
state = state.copyWith(status: RunStatus.paused);
}
}
/// 继续运行
void resume() {
if (state.status == RunStatus.paused) {
_runner.resume();
state = state.copyWith(status: RunStatus.running);
}
}
/// 停止运行
void stop() {
_runner.stop();
state = const RunState(status: RunStatus.idle);
}
/// 重置状态
void reset() {
stop();
}
/// 加载程序步骤(从数据库读取)
Future<List<Step>> _loadSteps(int programId) async {
return await _programService.getStepsByProgramId(programId);
}
}
/// MockRunner Provider
final mockRunnerProvider = Provider<MockRunner>((ref) {
return MockRunner();
});
/// ProgramService Provider
final programServiceProvider = Provider<ProgramService>((ref) {
return ProgramService.instance;
});
/// 运行状态 Provider
final runStateProvider =
StateNotifierProvider<RunStateNotifier, RunState>((ref) {
final runner = ref.watch(mockRunnerProvider);
final programService = ref.watch(programServiceProvider);
return RunStateNotifier(runner, programService);
});
/// 是否正在运行 Provider
final isRunningProvider = Provider<bool>((ref) {
final status = ref.watch(runStateProvider).status;
return status == RunStatus.running;
});
/// 是否已暂停 Provider
final isPausedProvider = Provider<bool>((ref) {
final status = ref.watch(runStateProvider).status;
return status == RunStatus.paused;
});

View File

@@ -0,0 +1,190 @@
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

@@ -0,0 +1,114 @@
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

@@ -0,0 +1,54 @@
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
/// 运行器状态
enum RunnerStatus {
idle,
running,
paused,
completed,
error,
}
/// 运行器回调
class RunnerCallbacks {
/// 步骤进度回调: (stepIndex, remainingSeconds, progress, currentWell)
final void Function(int stepIndex, int remainingSeconds, double progress, String well)? onProgress;
/// 运行完成回调
final void Function()? onComplete;
/// 错误回调
final void Function(String error)? onError;
const RunnerCallbacks({
this.onProgress,
this.onComplete,
this.onError,
});
}
/// 运行器抽象接口
/// 定义硬件运行控制的标准接口
abstract class Runner {
/// 当前状态
RunnerStatus status = RunnerStatus.idle;
/// 启动程序运行
void start(Program program, List<Step> steps, RunnerCallbacks callbacks);
/// 暂停运行
void pause();
/// 继续运行
void resume();
/// 停止运行
void stop();
/// 获取当前状态
RunnerStatus getStatus();
/// 释放资源
void dispose();
}

View File

@@ -0,0 +1,91 @@
import '../../programs/models/program.dart';
import '../../programs/models/step.dart';
import 'runner_interface.dart';
/// 串口运行器(真实硬件实现)
/// 实现与设备的串口通信
class SerialRunner implements Runner {
@override
RunnerStatus status = RunnerStatus.idle;
/// 串口配置
final String portName;
final int baudRate;
final int dataBits;
final int stopBits;
SerialRunner({
this.portName = '/dev/ttyUSB0',
this.baudRate = 9600,
this.dataBits = 8,
this.stopBits = 1,
});
@override
void start(Program program, List<Step> steps, RunnerCallbacks callbacks) {
// TODO: 实现串口通信启动逻辑
// 1. 打开串口连接
// 2. 发送程序配置
// 3. 按步骤发送控制指令
// 4. 接收设备反馈并更新状态
status = RunnerStatus.running;
// 示例:发送启动指令
// _sendCommand('START', program.code);
// 示例:监听设备状态
// _listenToDevice(callbacks);
}
@override
void pause() {
if (status == RunnerStatus.running) {
// _sendCommand('PAUSE');
status = RunnerStatus.paused;
}
}
@override
void resume() {
if (status == RunnerStatus.paused) {
// _sendCommand('RESUME');
status = RunnerStatus.running;
}
}
@override
void stop() {
// _sendCommand('STOP');
// _closeConnection();
status = RunnerStatus.idle;
}
@override
RunnerStatus getStatus() => status;
@override
void dispose() {
stop();
}
/// 发送控制指令(待硬件协议确定后实现)
Future<void> _sendCommand(String command, [String? data]) async {
// TODO: 根据硬件通信协议实现
// 示例协议格式: [CMD:data] 或 二进制协议
}
/// 监听设备反馈(待硬件协议确定后实现)
void _listenToDevice(RunnerCallbacks callbacks) {
// TODO: 解析设备返回的状态数据
// 状态格式示例: [STEP:1,TIME:60,POS:A1]
}
/// 执行单个步骤
Future<void> _executeStep(Step step) async {
// TODO: 根据步骤参数生成控制指令
// 混合: MIX(position, time, speed)
// 吸磁: MAGNET(position, time)
// 吹气: BLOW(position, speed, time)
}
}

View File

@@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../../device/providers/run_state_provider.dart';
/// 运行完成提示页面
class CompletePage extends ConsumerWidget {
const CompletePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final runState = ref.watch(runStateProvider);
final runNotifier = ref.read(runStateProvider.notifier);
return Scaffold(
body: Container(
color: AppTheme.backgroundColor,
child: Center(
child: Container(
width: 600,
padding: const EdgeInsets.all(40),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
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('/');
}
},
),
],
),
],
),
),
),
),
);
}
/// 操作指引示意图
Widget _buildOperationGuide() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.backgroundColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.idleColor.withValues(alpha: 0.2)),
),
child: Column(
children: [
Text(
'操作步骤',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStepItem(1, '取出样本', Icons.science),
_buildStepItem(2, '滴入检测卡', Icons.water_drop),
_buildStepItem(3, '等待反应', Icons.timer),
_buildStepItem(4, '查看结果', Icons.visibility),
],
),
],
),
);
}
/// 步骤项
Widget _buildStepItem(int number, String text, IconData icon) {
return Column(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: AppTheme.primaryColor, size: 24),
),
const SizedBox(height: 8),
Text(
'$number',
style: TextStyle(
color: AppTheme.primaryColor,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
text,
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 11,
),
),
],
);
}
}

View File

@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/database/database_service.dart';
import '../../../core/theme/app_theme.dart';
import '../../device/providers/run_state_provider.dart';
import '../../programs/pages/programs_page.dart';
import '../../settings/pages/settings_page.dart';
import '../widgets/status_bar.dart';
import '../widgets/program_list.dart';
import '../widgets/running_control_panel.dart';
import '../widgets/run_status_monitor.dart';
/// 首页 - 设备控制面板 (暗色工业风格)
/// 布局:状态栏 + 导航标签栏 + 内容区(设备控制/程序管理/系统设置)
class HomePage extends ConsumerStatefulWidget {
const HomePage({super.key});
@override
ConsumerState<HomePage> createState() => _HomePageState();
}
class _HomePageState extends ConsumerState<HomePage>
with SingleTickerProviderStateMixin {
bool _lightOn = false;
final bool _ceramicSleeveInstalled = false; // TODO: 后续对接硬件传感器后改为可变状态
int _currentIndex = 0;
@override
void initState() {
super.initState();
DatabaseService.instance.initTestData();
}
@override
Widget build(BuildContext context) {
final runState = ref.watch(runStateProvider);
// 监听运行完成状态,自动跳转
ref.listen<RunState>(runStateProvider, (prev, next) {
if (prev?.status != RunStatus.completed && next.status == RunStatus.completed) {
// 仅首页才自动跳转
if (_currentIndex == 0) {
context.push('/complete');
}
}
});
return Scaffold(
body: Container(
color: AppTheme.bgDeep,
child: Column(
children: [
// 状态栏
StatusBar(
isRunning: runState.status == RunStatus.running,
lightOn: _lightOn,
onLightToggle: () {
setState(() {
_lightOn = !_lightOn;
});
},
ceramicSleeveInstalled: _ceramicSleeveInstalled,
),
// 导航标签栏
_buildTabBar(),
// 内容区
Expanded(
child: IndexedStack(
index: _currentIndex,
children: [
_buildDeviceControlPage(runState),
const ProgramsPage(),
const SettingsPage(),
],
),
),
],
),
),
);
}
/// 导航标签栏
Widget _buildTabBar() {
const tabs = [
(icon: Icons.dashboard, label: '设备控制'),
(icon: Icons.list_alt, label: '程序管理'),
(icon: Icons.settings, label: '系统设置'),
];
return Container(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: List.generate(tabs.length, (index) {
final tab = tabs[index];
final isSelected = _currentIndex == index;
return GestureDetector(
onTap: () => setState(() => _currentIndex = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(right: 4),
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: isSelected ? AppTheme.accentPrimary : AppTheme.cardBg,
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
border: Border.all(
color: isSelected
? AppTheme.accentPrimary
: AppTheme.borderSubtle,
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
tab.icon,
size: 18,
color: isSelected ? Colors.white : AppTheme.textSecondary,
),
const SizedBox(width: 8),
Text(
tab.label,
style: TextStyle(
color: isSelected ? Colors.white : AppTheme.textSecondary,
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.normal,
),
),
],
),
),
);
}),
),
);
}
/// 设备控制页面内容
Widget _buildDeviceControlPage(RunState runState) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 20),
child: Row(
children: [
// 左侧:程序列表(运行时锁定)
Opacity(
opacity: runState.status == RunStatus.idle ? 1.0 : 0.6,
child: IgnorePointer(
ignoring: runState.status != RunStatus.idle,
child: const ProgramList(),
),
),
const SizedBox(width: 20),
// 右侧:运行控制区域
Expanded(
child: Column(
children: [
const Expanded(child: RunningControlPanel()),
if (runState.status != RunStatus.idle) ...[
const SizedBox(height: 16),
const Expanded(child: RunStatusMonitor()),
],
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../programs/models/program.dart';
import '../../programs/providers/programs_provider.dart';
/// 程序列表组件 - 暗色工业风格
/// 显示程序卡片列表,支持选择操作
class ProgramList extends ConsumerWidget {
const ProgramList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final programsState = ref.watch(programsProvider);
final programsNotifier = ref.read(programsProvider.notifier);
return Container(
width: 380,
decoration: BoxDecoration(
color: AppTheme.cardBg,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.borderSubtle, width: 1),
),
child: Column(
children: [
// 标题
Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
Icon(Icons.list_alt, color: AppTheme.textHeading, size: 18),
const SizedBox(width: 10),
Text(
l10n?.availablePrograms ?? '可用程序',
style: const TextStyle(
color: AppTheme.textHeading,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
// 程序列表
Expanded(
child: programsState.isLoading
? const Center(child: CircularProgressIndicator())
: programsState.programs.isEmpty
? Center(
child: Text(
l10n?.noData ?? '暂无数据',
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 14),
itemCount: programsState.programs.length,
itemBuilder: (context, index) {
final program = programsState.programs[index];
final isSelected =
programsState.selectedProgramId == program.id;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _ProgramCard(
program: program,
isSelected: isSelected,
onTap: () {
programsNotifier.selectProgram(program.id);
},
),
);
},
),
),
],
),
);
}
}
/// 单个程序卡片 - 暗色工业风格
class _ProgramCard extends StatelessWidget {
final Program program;
final bool isSelected;
final VoidCallback? onTap;
const _ProgramCard({
required this.program,
this.isSelected = false,
this.onTap,
});
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('yyyy-MM-dd HH:mm');
final createdAt = _parseDate(program.createdAt);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isSelected ? AppTheme.cardSelectedBg : AppTheme.cardBg,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? AppTheme.accentPrimary : AppTheme.borderSubtle,
width: isSelected ? 2 : 1,
),
),
child: Row(
children: [
// 选择指示器
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected
? AppTheme.accentPrimary
: Colors.transparent,
border: Border.all(
color: isSelected
? AppTheme.accentPrimary
: AppTheme.statusStopped,
width: 2,
),
),
child: isSelected
? const Icon(Icons.check, color: Colors.white, size: 12)
: null,
),
const SizedBox(width: 12),
// 程序信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
program.code,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: program.status == 1
? AppTheme.statusRunning.withValues(alpha: 0.15)
: AppTheme.statusStopped.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4),
),
child: Text(
program.status == 1 ? '启用' : '停用',
style: TextStyle(
color: program.status == 1
? AppTheme.statusRunning
: AppTheme.statusStopped,
fontSize: 10,
),
),
),
],
),
const SizedBox(height: 4),
Text(
program.name,
style: const TextStyle(
color: AppTheme.textHeading,
fontSize: 14,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
createdAt != null
? dateFormat.format(createdAt)
: program.createdAt,
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 11,
fontFamily: 'monospace',
),
),
],
),
),
],
),
),
),
);
}
DateTime? _parseDate(String dateStr) {
try {
return DateTime.parse(dateStr);
} catch (e) {
return null;
}
}
}

View File

@@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../device/providers/run_state_provider.dart';
/// 运行状态监控面板 - 暗色工业风格
/// 显示当前孔位、步骤、倒计时、进度条、参数详情
class RunStatusMonitor extends ConsumerWidget {
const RunStatusMonitor({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final runState = ref.watch(runStateProvider);
if (runState.status == RunStatus.idle) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.cardBg,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.borderSubtle, width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题 + 程序名
Row(
children: [
Text(
l10n?.runningMonitor ?? '运行状态监控',
style: const TextStyle(
color: AppTheme.textHeading,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
runState.currentProgram?.name ?? '',
style: const TextStyle(
color: AppTheme.accentPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 14),
// 进度信息横排 (孔位 / 步骤 / 剩余时间)
Row(
children: [
// 当前孔位
_buildInfoBlock(
label: l10n?.currentHole ?? '当前孔位',
value: runState.currentWell ?? '--',
valueColor: AppTheme.textHeading,
),
const SizedBox(width: 20),
// 当前步骤
_buildInfoBlock(
label: l10n?.currentStep ?? '当前步骤',
value: '${l10n?.stepNo ?? '步骤'} ${runState.currentStepIndex + 1}',
subValue: runState.currentStep?.name ?? '--',
valueColor: AppTheme.accentInfo,
),
const SizedBox(width: 20),
// 剩余时间
_buildInfoBlock(
label: l10n?.remainingTime ?? '剩余时间',
value: runState.formattedRemainingTime,
valueColor: AppTheme.textHeading,
valueSize: 20,
),
],
),
const SizedBox(height: 14),
// 总进度条
_buildProgressBar(l10n, runState),
const SizedBox(height: 14),
// 步骤参数
if (runState.currentStep != null)
_buildStepParams(l10n, runState.currentStep!),
],
),
);
}
/// 信息块
Widget _buildInfoBlock({
required String label,
required String value,
String? subValue,
Color valueColor = AppTheme.textHeading,
double valueSize = 16,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 11,
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
color: valueColor,
fontSize: valueSize,
fontWeight: FontWeight.w600,
fontFamily: 'monospace',
),
),
if (subValue != null) ...[
const SizedBox(height: 2),
Text(
subValue,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 11,
),
),
],
],
);
}
/// 进度条
Widget _buildProgressBar(AppLocalizations? l10n, RunState runState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
l10n?.progress ?? '总进度',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 11,
),
),
const Spacer(),
Text(
runState.formattedProgress,
style: const TextStyle(
color: AppTheme.accentPrimary,
fontSize: 12,
fontWeight: FontWeight.w600,
fontFamily: 'monospace',
),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: runState.progress,
minHeight: 8,
backgroundColor: const Color(0xFF1E293B),
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.accentPrimary),
),
),
],
);
}
/// 步骤参数详情
Widget _buildStepParams(AppLocalizations? l10n, dynamic step) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n?.stepParams ?? '步骤参数',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 11,
),
),
const SizedBox(height: 2),
if (step.mixTime > 0)
_buildParamRow(
l10n?.speed ?? '转速',
'${step.mixSpeed}',
),
if (step.magnetTime > 0)
_buildParamRow(
l10n?.temperature ?? '温度',
'65.0 °C',
),
_buildParamRow(
l10n?.duration ?? '持续时间',
step.mixTime > 0 ? '${step.mixTime} min' : '--',
),
_buildParamRow(
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

@@ -0,0 +1,365 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../../device/providers/run_state_provider.dart';
import '../../programs/providers/programs_provider.dart';
/// 运行控制面板 - 暗色工业风格
/// 显示当前程序信息、瓷套棒状态和运行控制按钮
class RunningControlPanel extends ConsumerWidget {
const RunningControlPanel({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final runState = ref.watch(runStateProvider);
final programsState = ref.watch(programsProvider);
return Container(
decoration: BoxDecoration(
color: AppTheme.cardBg,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.borderSubtle, width: 1),
),
child: runState.status == RunStatus.idle
? _buildIdleState(context, ref, l10n, programsState.selectedProgram)
: _buildRunningState(context, ref, l10n, runState),
);
}
/// 待机状态布局
Widget _buildIdleState(
BuildContext context,
WidgetRef ref,
AppLocalizations? l10n,
dynamic selectedProgram,
) {
final runNotifier = ref.read(runStateProvider.notifier);
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 当前选中程序显示
if (selectedProgram != null)
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.cardSelectedBg,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.accentPrimary, width: 1),
),
child: Row(
children: [
Text(
'${l10n?.selectedProgramLabel ?? '当前选中'}:',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 12,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'${selectedProgram.code} ${selectedProgram.name}',
style: const TextStyle(
color: AppTheme.accentPrimary,
fontSize: 14,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
)
else
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.cardBg,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.borderSubtle, width: 1),
),
child: Text(
l10n?.pleaseSelectProgram ?? '请选择要运行的程序',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 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(
children: [
// 开始运行按钮
Expanded(
flex: 2,
child: SizedBox(
height: 48,
child: CommonButton(
text: l10n?.startRun ?? '开始运行',
icon: Icons.play_arrow,
type: ButtonType.primary,
enabled: selectedProgram != null,
onPressed: selectedProgram != null
? () => runNotifier.start(selectedProgram)
: null,
),
),
),
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(
child: SizedBox(
height: 48,
child: CommonButton(
text: l10n?.stop ?? '停止',
icon: Icons.stop,
type: ButtonType.danger,
enabled: false,
onPressed: null,
),
),
),
],
),
],
),
);
}
/// 运行状态布局
Widget _buildRunningState(
BuildContext context,
WidgetRef ref,
AppLocalizations? l10n,
RunState runState,
) {
final runNotifier = ref.read(runStateProvider.notifier);
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 当前程序名称
Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.cardSelectedBg,
borderRadius: BorderRadius.circular(6),
border: Border.all(color: AppTheme.accentPrimary, width: 1),
),
child: Row(
children: [
Text(
'${l10n?.selectedProgramLabel ?? '当前选中'}:',
style: const TextStyle(
color: AppTheme.textTertiary,
fontSize: 12,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
runState.currentProgram?.name ?? '',
style: const TextStyle(
color: AppTheme.accentPrimary,
fontSize: 14,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
// 控制按钮
Row(
children: [
// 开始/继续按钮
Expanded(
flex: 2,
child: SizedBox(
height: 48,
child: CommonButton(
text: runState.status == RunStatus.paused
? (l10n?.continue_ ?? '继续')
: (l10n?.run ?? '运行'),
icon: runState.status == RunStatus.paused
? Icons.play_arrow
: Icons.play_arrow,
type: ButtonType.primary,
onPressed: () => runNotifier.resume(),
),
),
),
const SizedBox(width: 12),
// 暂停按钮
Expanded(
child: SizedBox(
height: 48,
child: CommonButton(
text: l10n?.pause ?? '暂停',
icon: Icons.pause,
type: ButtonType.warning,
onPressed: runState.status == RunStatus.paused
? null
: () => runNotifier.pause(),
),
),
),
const SizedBox(width: 12),
// 停止按钮
Expanded(
child: SizedBox(
height: 48,
child: CommonButton(
text: l10n?.stop ?? '停止',
icon: Icons.stop,
type: ButtonType.danger,
onPressed: () => _showStopConfirm(context, runNotifier, l10n),
),
),
),
],
),
const SizedBox(height: 12),
// 状态指示
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: runState.status == RunStatus.paused
? AppTheme.accentWarning
: AppTheme.statusRunning,
),
),
const SizedBox(width: 8),
Text(
runState.status == RunStatus.paused
? (l10n?.paused ?? '已暂停')
: (l10n?.running ?? '运行中'),
style: TextStyle(
color: runState.status == RunStatus.paused
? AppTheme.accentWarning
: AppTheme.statusRunning,
fontWeight: FontWeight.w500,
fontSize: 12,
),
),
],
),
],
),
);
}
/// 显示停止确认对话框
void _showStopConfirm(
BuildContext context,
RunStateNotifier runNotifier,
AppLocalizations? l10n,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.cardBg,
title: Text(
l10n?.confirm ?? '确认',
style: const TextStyle(color: AppTheme.textHeading),
),
content: Text(
l10n?.stopConfirm ?? '确定要停止当前运行的程序吗?',
style: const TextStyle(color: AppTheme.textPrimary),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
l10n?.cancel ?? '取消',
style: const TextStyle(color: AppTheme.textSecondary),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentCritical,
foregroundColor: Colors.white,
),
onPressed: () {
runNotifier.stop();
Navigator.of(context).pop();
},
child: Text(l10n?.confirm ?? '确认'),
),
],
),
);
}
}

View File

@@ -0,0 +1,181 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/status_indicator.dart';
/// 状态栏组件 - 明亮工业风格
/// 显示设备名称、实时时钟、系统状态、照明控制、瓷套棒状态
class StatusBar extends StatefulWidget {
final bool isRunning;
final bool lightOn;
final VoidCallback? onLightToggle;
final bool ceramicSleeveInstalled;
const StatusBar({
super.key,
this.isRunning = false,
this.lightOn = false,
this.onLightToggle,
this.ceramicSleeveInstalled = false,
});
@override
State<StatusBar> createState() => _StatusBarState();
}
class _StatusBarState extends State<StatusBar> {
String _currentTime = '';
Timer? _timer;
@override
void initState() {
super.initState();
_updateTime();
_timer = Timer.periodic(const Duration(seconds: 1), (_) => _updateTime());
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _updateTime() {
final now = DateTime.now();
_currentTime =
'${now.year}-${_twoDigits(now.month)}-${_twoDigits(now.day)} '
'${_twoDigits(now.hour)}:${_twoDigits(now.minute)}:${_twoDigits(now.second)}';
if (mounted) setState(() {});
}
String _twoDigits(int n) => n.toString().padLeft(2, '0');
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Container(
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
decoration: BoxDecoration(
color: AppTheme.primaryColor,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.precision_manufacturing, color: Colors.white, size: 22),
const SizedBox(width: 10),
Text(
l10n?.deviceName ?? '污水毒品前处理一体机',
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w700,
),
),
],
),
const Spacer(),
_LightToggleButton(isOn: widget.lightOn, onTap: widget.onLightToggle),
const SizedBox(width: 16),
_CeramicSleeveStatus(installed: widget.ceramicSleeveInstalled),
const SizedBox(width: 20),
StatusIndicator(
text: widget.isRunning
? (l10n?.running ?? '运行中')
: (l10n?.idle ?? '未运行'),
status: widget.isRunning
? DeviceStatusType.running
: DeviceStatusType.idle,
),
const SizedBox(width: 20),
Text(
_currentTime,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontFamily: 'monospace',
fontWeight: FontWeight.normal,
),
),
],
),
);
}
}
class _CeramicSleeveStatus extends StatelessWidget {
final bool installed;
const _CeramicSleeveStatus({required this.installed});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: installed ? Colors.greenAccent : Colors.redAccent,
),
),
const SizedBox(width: 6),
Text(
installed ? '瓷套棒: 已安装' : '瓷套棒: 未安装',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 12,
),
),
],
);
}
}
class _LightToggleButton extends StatelessWidget {
final bool isOn;
final VoidCallback? onTap;
const _LightToggleButton({this.isOn = false, this.onTap});
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isOn
? Colors.white.withValues(alpha: 0.25)
: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1,
),
),
child: Icon(
isOn ? Icons.lightbulb : Icons.lightbulb_outline_rounded,
color: isOn ? Colors.yellowAccent : Colors.white.withValues(alpha: 0.8),
size: 20,
),
),
),
);
}
}

View File

@@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../providers/steps_provider.dart';
import '../widgets/step_list.dart';
import '../widgets/step_form.dart';
import '../../programs/providers/programs_provider.dart';
/// 程序详情页面
/// 左侧步骤列表 + 右侧参数表单
class ProgramDetailPage extends ConsumerStatefulWidget {
final String programId;
const ProgramDetailPage({super.key, required this.programId});
@override
ConsumerState<ProgramDetailPage> createState() => _ProgramDetailPageState();
}
class _ProgramDetailPageState extends ConsumerState<ProgramDetailPage> {
late int _programIdInt;
@override
void initState() {
super.initState();
_programIdInt = int.tryParse(widget.programId) ?? 0;
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final programsState = ref.watch(programsProvider);
final program = programsState.programs.where((p) => p.id == _programIdInt).firstOrNull;
final stepsState = ref.watch(stepsProvider(_programIdInt));
return Scaffold(
body: Container(
color: AppTheme.backgroundColor,
child: Column(
children: [
// 顶部导航栏
Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// 返回按钮
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/programs'),
),
const SizedBox(width: 16),
// 程序名称
Text(
program?.name ?? (l10n?.detail ?? '程序详情'),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
// 保存按钮
CommonButton(
text: l10n?.save ?? '保存',
icon: Icons.save,
type: ButtonType.primary,
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已保存'),
backgroundColor: AppTheme.successColor,
),
);
},
),
],
),
),
// 主内容区域
Expanded(
child: stepsState.isLoading
? const Center(child: CircularProgressIndicator())
: Row(
children: [
// 左侧:步骤列表
SizedBox(
width: 400,
child: StepList(
programId: _programIdInt,
steps: stepsState.steps,
selectedStepId: stepsState.selectedStepId,
onStepSelected: (stepId) {
ref.read(stepsProvider(_programIdInt).notifier).selectStep(stepId);
},
onAddStep: () => _showAddStepDialog(context, ref),
onReorder: (oldIndex, newIndex) {
ref.read(stepsProvider(_programIdInt).notifier).reorderSteps(oldIndex, newIndex);
},
onDeleteSteps: (stepIds) {
ref.read(stepsProvider(_programIdInt).notifier).deleteSteps(stepIds);
},
),
),
// 分隔线
Container(
width: 1,
color: AppTheme.idleColor.withValues(alpha: 0.3),
),
// 右侧:步骤参数表单
Expanded(
child: stepsState.selectedStep != null
? StepForm(
programId: _programIdInt,
step: stepsState.selectedStep!,
onSave: (step) async {
final success = await ref
.read(stepsProvider(_programIdInt).notifier)
.updateStep(step);
if (success) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('步骤已更新'),
backgroundColor: AppTheme.successColor,
),
);
}
},
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.edit_note,
size: 64,
color: AppTheme.idleColor,
),
const SizedBox(height: 16),
Text(
'请选择或添加步骤',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
),
),
],
),
),
),
],
),
),
],
),
),
);
}
/// 显示添加步骤对话框
void _showAddStepDialog(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => Dialog(
child: Container(
width: 600,
padding: const EdgeInsets.all(24),
child: StepForm(
programId: _programIdInt,
isNew: true,
onSave: (step) async {
final success = await ref
.read(stepsProvider(_programIdInt).notifier)
.addStep(step);
if (success) {
Navigator.of(context).pop();
}
},
),
),
),
);
}
}

View File

@@ -0,0 +1,161 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../programs/models/step.dart';
import '../../programs/services/program_service.dart';
/// 步骤状态
class StepsState {
final List<Step> steps;
final int? selectedStepId;
final bool isLoading;
final String? error;
const StepsState({
this.steps = const [],
this.selectedStepId,
this.isLoading = false,
this.error,
});
StepsState copyWith({
List<Step>? steps,
int? selectedStepId,
bool? isLoading,
String? error,
bool clearSelection = false,
bool clearError = false,
}) {
return StepsState(
steps: steps ?? this.steps,
selectedStepId: clearSelection ? null : (selectedStepId ?? this.selectedStepId),
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
);
}
/// 获取选中的步骤
Step? get selectedStep {
if (selectedStepId == null) return null;
return steps.where((s) => s.id == selectedStepId).firstOrNull;
}
}
/// 步骤 Notifier
class StepsNotifier extends StateNotifier<StepsState> {
final ProgramService _service;
final int programId;
StepsNotifier(this._service, this.programId) : super(const StepsState()) {
loadSteps();
}
/// 加载步骤
Future<void> loadSteps() async {
state = state.copyWith(isLoading: true, clearError: true);
try {
final steps = await _service.getStepsByProgramId(programId);
state = state.copyWith(steps: steps, isLoading: false);
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}
/// 选择步骤
void selectStep(int? stepId) {
state = state.copyWith(selectedStepId: stepId);
}
/// 清除选择
void clearSelection() {
state = state.copyWith(clearSelection: true);
}
/// 添加步骤
Future<bool> addStep(Step step) async {
try {
await _service.addStep(step);
await loadSteps();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 更新步骤
Future<bool> updateStep(Step step) async {
if (step.id == null) return false;
try {
await _service.updateStep(step);
await loadSteps();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 删除步骤
Future<bool> deleteStep(int stepId) async {
try {
await _service.deleteStep(stepId);
if (state.selectedStepId == stepId) {
state = state.copyWith(clearSelection: true);
}
await loadSteps();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 批量删除步骤
Future<bool> deleteSteps(List<int> stepIds) async {
try {
await _service.deleteSteps(stepIds);
if (stepIds.contains(state.selectedStepId)) {
state = state.copyWith(clearSelection: true);
}
await loadSteps();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 重新排序步骤
Future<void> reorderSteps(int oldIndex, int newIndex) async {
final steps = List<Step>.from(state.steps);
final step = steps.removeAt(oldIndex);
steps.insert(newIndex, step);
// 更新 step_no
for (int i = 0; i < steps.length; i++) {
steps[i] = steps[i].copyWith(stepNo: i + 1);
}
state = state.copyWith(steps: steps);
// 持久化排序
await _service.reorderSteps(programId, steps.map((s) => s.id!).toList());
}
}
/// 程序服务 Provider
final programServiceProvider = Provider<ProgramService>((ref) {
return ProgramService.instance;
});
/// 步骤 Provider按程序ID
final stepsProvider = StateNotifierProvider.family<StepsNotifier, StepsState, int>(
(ref, programId) {
final service = ref.watch(programServiceProvider);
return StepsNotifier(service, programId);
},
);
/// 选中的步骤 Provider
final selectedStepProvider = Provider.family<Step?, int>((ref, programId) {
return ref.watch(stepsProvider(programId)).selectedStep;
});

View File

@@ -0,0 +1,270 @@
import 'package:flutter/material.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/utils/constants.dart';
import '../../../shared/widgets/common_button.dart';
import '../../programs/models/step.dart' as models;
/// 步骤参数表单
class StepForm extends StatefulWidget {
final int programId;
final models.Step? step;
final bool isNew;
final void Function(models.Step) onSave;
const StepForm({
super.key,
required this.programId,
this.step,
this.isNew = false,
required this.onSave,
});
@override
State<StepForm> createState() => _StepFormState();
}
class _StepFormState extends State<StepForm> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _mixTimeController;
late TextEditingController _magnetTimeController;
late TextEditingController _volumeController;
late TextEditingController _blowTimeController;
String _position = 'A1';
String _mixSpeed = '中速';
String _blowSpeed = '中速';
int _needleSpeed = 5;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.step?.name ?? '');
_mixTimeController = TextEditingController(text: '${widget.step?.mixTime ?? 0}');
_magnetTimeController = TextEditingController(text: '${widget.step?.magnetTime ?? 0}');
_volumeController = TextEditingController(text: '${widget.step?.volume ?? 0}');
_blowTimeController = TextEditingController(text: '${widget.step?.blowTime ?? 0}');
_position = widget.step?.position ?? 'A1';
_mixSpeed = widget.step?.mixSpeed ?? '中速';
_blowSpeed = widget.step?.blowSpeed ?? '中速';
_needleSpeed = widget.step?.needleSpeed ?? 5;
}
@override
void dispose() {
_nameController.dispose();
_mixTimeController.dispose();
_magnetTimeController.dispose();
_volumeController.dispose();
_blowTimeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
widget.isNew ? '添加步骤' : '编辑步骤',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 24),
// 步骤名称
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: l10n?.stepName ?? '步骤名称',
hintText: '例如: 混合、吸磁、吹气',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入步骤名称';
}
return null;
},
),
const SizedBox(height: 16),
// 孔位选择
Row(
children: [
Text(l10n?.position ?? '孔位', style: TextStyle(color: AppTheme.textPrimary)),
const SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField<String>(
value: _position,
decoration: InputDecoration(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
items: Constants.positions.map((p) => DropdownMenuItem(value: p, child: Text(p))).toList(),
onChanged: (value) {
if (value != null) setState(() => _position = value);
},
),
),
],
),
const SizedBox(height: 16),
// 时间参数行
Row(
children: [
Expanded(
child: TextFormField(
controller: _mixTimeController,
decoration: InputDecoration(
labelText: '${l10n?.mixTime ?? '混合时间'} (${Constants.timeUnitSeconds})',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _magnetTimeController,
decoration: InputDecoration(
labelText: '${l10n?.magnetTime ?? '吸磁时间'} (${Constants.timeUnitSeconds})',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: 16),
// 容积和吹气时间
Row(
children: [
Expanded(
child: TextFormField(
controller: _volumeController,
decoration: InputDecoration(
labelText: '${l10n?.volume ?? '容积'} (${Constants.volumeUnit})',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _blowTimeController,
decoration: InputDecoration(
labelText: '${l10n?.blowTime ?? '吹气时间'} (${Constants.timeUnitMinutes})',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: 16),
// 速度选择
Row(
children: [
Expanded(
child: DropdownButtonFormField<String>(
value: _mixSpeed,
decoration: InputDecoration(
labelText: l10n?.mixSpeed ?? '混合速度',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
items: Constants.speedOptions.map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
onChanged: (value) {
if (value != null) setState(() => _mixSpeed = value);
},
),
),
const SizedBox(width: 16),
Expanded(
child: DropdownButtonFormField<String>(
value: _blowSpeed,
decoration: InputDecoration(
labelText: l10n?.blowSpeed ?? '吹气速度',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
items: Constants.speedOptions.map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
onChanged: (value) {
if (value != null) setState(() => _blowSpeed = value);
},
),
),
],
),
const SizedBox(height: 16),
// 下针速度滑块
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${l10n?.needleSpeed ?? '下针速度'}: $_needleSpeed', style: TextStyle(color: AppTheme.textPrimary)),
Slider(
value: _needleSpeed.toDouble(),
min: 1,
max: 10,
divisions: 9,
activeColor: AppTheme.primaryColor,
onChanged: (value) {
setState(() => _needleSpeed = value.round());
},
),
],
),
const SizedBox(height: 24),
// 保存按钮
CommonButton(
text: l10n?.save ?? '保存',
icon: Icons.save,
type: ButtonType.primary,
onPressed: _saveStep,
),
],
),
),
);
}
/// 保存步骤
void _saveStep() {
if (!_formKey.currentState!.validate()) return;
final step = models.Step(
id: widget.step?.id,
programId: widget.programId,
stepNo: widget.step?.stepNo ?? 1,
position: _position,
name: _nameController.text.trim(),
mixTime: int.tryParse(_mixTimeController.text) ?? 0,
magnetTime: int.tryParse(_magnetTimeController.text) ?? 0,
volume: int.tryParse(_volumeController.text) ?? 0,
mixSpeed: _mixSpeed,
blowSpeed: _blowSpeed,
blowTime: int.tryParse(_blowTimeController.text) ?? 0,
needleSpeed: _needleSpeed,
);
widget.onSave(step);
}
}

View File

@@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../../programs/models/step.dart' as models;
/// 步骤列表组件
class StepList extends StatefulWidget {
final int programId;
final List<models.Step> steps;
final int? selectedStepId;
final void Function(int?) onStepSelected;
final void Function() onAddStep;
final void Function(int oldIndex, int newIndex)? onReorder;
final void Function(List<int> stepIds)? onDeleteSteps;
const StepList({
super.key,
required this.programId,
required this.steps,
this.selectedStepId,
required this.onStepSelected,
required this.onAddStep,
this.onReorder,
this.onDeleteSteps,
});
@override
State<StepList> createState() => _StepListState();
}
class _StepListState extends State<StepList> {
final Set<int> _selectedIds = {};
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final allSelected = _selectedIds.length == widget.steps.length && widget.steps.isNotEmpty;
return Container(
color: Colors.white,
child: Column(
children: [
// 标题
Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
),
child: Row(
children: [
Icon(Icons.list, color: AppTheme.primaryColor, size: 20),
const SizedBox(width: 12),
Text(
'步骤列表',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
'${widget.steps.length}',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 12,
),
),
],
),
),
// 表头
Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: AppTheme.idleColor.withValues(alpha: 0.2)),
),
),
child: Row(
children: [
SizedBox(
width: 40,
child: Checkbox(
value: allSelected,
onChanged: (value) {
setState(() {
if (value == true) {
_selectedIds.clear();
_selectedIds.addAll(widget.steps.map((s) => s.id!));
} else {
_selectedIds.clear();
}
});
},
),
),
SizedBox(width: 40, child: Text('#', style: TextStyle(fontSize: 12))),
Expanded(child: Text(l10n?.stepName ?? '名称', style: TextStyle(fontSize: 12))),
SizedBox(width: 60, child: Text(l10n?.position ?? '孔位', style: TextStyle(fontSize: 12))),
],
),
),
// 步骤列表(可拖拽排序)
Expanded(
child: widget.steps.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.add_circle_outline, size: 48, color: AppTheme.idleColor),
const SizedBox(height: 12),
Text('暂无步骤', style: TextStyle(color: AppTheme.textSecondary)),
],
),
)
: ReorderableListView.builder(
padding: const EdgeInsets.all(8),
itemCount: widget.steps.length,
onReorder: (oldIndex, newIndex) {
if (widget.onReorder != null) {
// 调整 newIndexReorderableListView 的特殊行为)
if (newIndex > oldIndex) newIndex -= 1;
widget.onReorder!(oldIndex, newIndex);
}
},
itemBuilder: (context, index) {
final step = widget.steps[index];
final isSelected = widget.selectedStepId == step.id || _selectedIds.contains(step.id);
return _buildStepItem(step, isSelected, index);
},
),
),
// 底部操作栏
Container(
height: 60,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: AppTheme.idleColor.withValues(alpha: 0.2)),
),
),
child: Row(
children: [
// 添加按钮
CommonButton(
text: '添加',
icon: Icons.add,
type: ButtonType.primary,
onPressed: widget.onAddStep,
),
const SizedBox(width: 12),
// 删除按钮
if (_selectedIds.isNotEmpty)
CommonButton(
text: '删除',
icon: Icons.delete,
type: ButtonType.danger,
onPressed: () => _showDeleteConfirmDialog(context),
),
],
),
),
],
),
);
}
/// 步骤项
Widget _buildStepItem(models.Step step, bool isSelected, int index) {
return Container(
key: ValueKey(step.id),
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryLight.withValues(alpha: 0.3) : Colors.white,
borderRadius: BorderRadius.circular(4),
border: isSelected ? Border.all(color: AppTheme.primaryColor, width: 2) : null,
),
child: ListTile(
dense: true,
leading: Checkbox(
value: _selectedIds.contains(step.id),
onChanged: (value) {
setState(() {
if (value == true) {
_selectedIds.add(step.id!);
} else {
_selectedIds.remove(step.id!);
}
});
},
),
title: Row(
children: [
Container(
width: 30,
alignment: Alignment.center,
child: Text(
'${step.stepNo}',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
Expanded(child: Text(step.name)),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
step.position,
style: TextStyle(
color: AppTheme.primaryColor,
fontSize: 12,
),
),
),
],
),
trailing: Icon(Icons.drag_handle, color: AppTheme.idleColor),
onTap: () => widget.onStepSelected(step.id),
),
);
}
/// 显示删除确认对话框
void _showDeleteConfirmDialog(BuildContext context) {
final l10n = AppLocalizations.of(context);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(l10n?.confirm ?? '确认'),
content: Text(
_selectedIds.length == 1
? '确定要删除此步骤吗?'
: '确定要删除选中的 ${_selectedIds.length} 个步骤吗?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(l10n?.cancel ?? '取消'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.errorColor,
foregroundColor: Colors.white,
),
onPressed: () {
Navigator.of(ctx).pop();
if (widget.onDeleteSteps != null) {
widget.onDeleteSteps!(_selectedIds.toList());
}
setState(() {
_selectedIds.clear();
});
},
child: Text(l10n?.confirm ?? '确认'),
),
],
),
);
}
}

View File

@@ -0,0 +1,52 @@
/// 程序模型
class Program {
final int? id;
final String code;
final String name;
final String createdAt;
final int status; // 1: 启用, 0: 停用
Program({
this.id,
required this.code,
required this.name,
required this.createdAt,
this.status = 1,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'code': code,
'name': name,
'created_at': createdAt,
'status': status,
};
}
factory Program.fromMap(Map<String, dynamic> map) {
return Program(
id: map['id'] as int?,
code: map['code'] as String,
name: map['name'] as String,
createdAt: map['created_at'] as String,
status: map['status'] as int? ?? 1,
);
}
Program copyWith({
int? id,
String? code,
String? name,
String? createdAt,
int? status,
}) {
return Program(
id: id ?? this.id,
code: code ?? this.code,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
status: status ?? this.status,
);
}
}

View File

@@ -0,0 +1,94 @@
/// 步骤模型
class Step {
final int? id;
final int programId;
final int stepNo;
final String position;
final String name;
final int mixTime;
final int magnetTime;
final int volume;
final String mixSpeed;
final String blowSpeed;
final int blowTime;
final int needleSpeed;
Step({
this.id,
required this.programId,
required this.stepNo,
required this.position,
required this.name,
this.mixTime = 0,
this.magnetTime = 0,
this.volume = 0,
this.mixSpeed = '中速',
this.blowSpeed = '中速',
this.blowTime = 0,
this.needleSpeed = 5,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'program_id': programId,
'step_no': stepNo,
'position': position,
'name': name,
'mix_time': mixTime,
'magnet_time': magnetTime,
'volume': volume,
'mix_speed': mixSpeed,
'blow_speed': blowSpeed,
'blow_time': blowTime,
'needle_speed': needleSpeed,
};
}
factory Step.fromMap(Map<String, dynamic> map) {
return Step(
id: map['id'] as int?,
programId: map['program_id'] as int,
stepNo: map['step_no'] as int,
position: map['position'] as String,
name: map['name'] as String,
mixTime: map['mix_time'] as int? ?? 0,
magnetTime: map['magnet_time'] as int? ?? 0,
volume: map['volume'] as int? ?? 0,
mixSpeed: map['mix_speed'] as String? ?? '中速',
blowSpeed: map['blow_speed'] as String? ?? '中速',
blowTime: map['blow_time'] as int? ?? 0,
needleSpeed: map['needle_speed'] as int? ?? 5,
);
}
Step copyWith({
int? id,
int? programId,
int? stepNo,
String? position,
String? name,
int? mixTime,
int? magnetTime,
int? volume,
String? mixSpeed,
String? blowSpeed,
int? blowTime,
int? needleSpeed,
}) {
return Step(
id: id ?? this.id,
programId: programId ?? this.programId,
stepNo: stepNo ?? this.stepNo,
position: position ?? this.position,
name: name ?? this.name,
mixTime: mixTime ?? this.mixTime,
magnetTime: magnetTime ?? this.magnetTime,
volume: volume ?? this.volume,
mixSpeed: mixSpeed ?? this.mixSpeed,
blowSpeed: blowSpeed ?? this.blowSpeed,
blowTime: blowTime ?? this.blowTime,
needleSpeed: needleSpeed ?? this.needleSpeed,
);
}
}

View File

@@ -0,0 +1,509 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:file_picker/file_picker.dart';
import 'dart:io';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../models/program.dart';
import '../providers/programs_provider.dart';
import '../widgets/program_form_dialog.dart';
import '../services/program_import_service.dart';
/// 程序管理页面
class ProgramsPage extends ConsumerStatefulWidget {
const ProgramsPage({super.key});
@override
ConsumerState<ProgramsPage> createState() => _ProgramsPageState();
}
class _ProgramsPageState extends ConsumerState<ProgramsPage> {
final Set<int> _selectedIds = {};
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final programsState = ref.watch(programsProvider);
return Scaffold(
body: Container(
color: AppTheme.backgroundColor,
child: Column(
children: [
// 顶部导航栏
Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// 返回按钮
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go('/'),
),
const SizedBox(width: 16),
Text(
l10n?.programs ?? '程序管理',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
// 新增按钮
CommonButton(
text: l10n?.addProgram ?? '新增',
icon: Icons.add,
type: ButtonType.primary,
onPressed: () => _showAddDialog(context, ref),
),
const SizedBox(width: 12),
// 导入按钮
CommonButton(
text: l10n?.importProgram ?? '导入',
icon: Icons.file_upload,
type: ButtonType.secondary,
onPressed: () => _importPrograms(context, ref),
),
],
),
),
// 程序列表表格
Expanded(
child: programsState.isLoading
? const Center(child: CircularProgressIndicator())
: programsState.programs.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.folder_open,
size: 64,
color: AppTheme.idleColor,
),
const SizedBox(height: 16),
Text(
l10n?.noData ?? '暂无数据',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
),
),
],
),
)
: Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(2, 2),
),
],
),
child: Column(
children: [
// 表头
_buildTableHeader(l10n, programsState.programs),
// 表格内容
Expanded(
child: ListView.builder(
itemCount: programsState.programs.length,
itemBuilder: (context, index) {
final program = programsState.programs[index];
final isSelected = _selectedIds.contains(program.id);
return _buildTableRow(
context,
ref,
l10n,
program,
isSelected,
index == programsState.programs.length - 1,
);
},
),
),
],
),
),
),
// 底部操作栏
if (programsState.programs.isNotEmpty)
Container(
height: 60,
padding: const EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Row(
children: [
Text(
'${l10n?.selected ?? '已选择'}: ${_selectedIds.length}',
style: TextStyle(color: AppTheme.textSecondary),
),
const Spacer(),
if (_selectedIds.isNotEmpty)
CommonButton(
text: l10n?.deleteProgram ?? '删除',
icon: Icons.delete,
type: ButtonType.danger,
onPressed: () => _showDeleteConfirmDialog(
context,
ref,
l10n,
_selectedIds.toList(),
),
),
],
),
),
],
),
),
);
}
/// 表头
Widget _buildTableHeader(AppLocalizations? l10n, List<Program> programs) {
final allSelected = _selectedIds.length == programs.length && programs.isNotEmpty;
return Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
),
child: Row(
children: [
// 复选框
SizedBox(
width: 50,
child: Checkbox(
value: allSelected,
onChanged: (value) {
setState(() {
if (value == true) {
_selectedIds.clear();
_selectedIds.addAll(programs.map((p) => p.id!));
} else {
_selectedIds.clear();
}
});
},
),
),
// 编号
SizedBox(
width: 100,
child: Text(
l10n?.programCode ?? '编号',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
// 名称
Expanded(
flex: 2,
child: Text(
l10n?.programName ?? '名称',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
// 创建时间
Expanded(
child: Text(
l10n?.createTime ?? '创建时间',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
// 状态
SizedBox(
width: 80,
child: Text(
'状态',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
),
// 操作
SizedBox(
width: 150,
child: Text(
l10n?.detail ?? '操作',
style: TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
),
),
],
),
);
}
/// 表格行
Widget _buildTableRow(
BuildContext context,
WidgetRef ref,
AppLocalizations? l10n,
Program program,
bool isSelected,
bool isLast,
) {
return Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: isSelected ? AppTheme.primaryLight.withValues(alpha: 0.2) : null,
border: isLast
? null
: Border(
bottom: BorderSide(
color: AppTheme.idleColor.withValues(alpha: 0.2),
),
),
),
child: Row(
children: [
// 复选框
SizedBox(
width: 50,
child: Checkbox(
value: isSelected,
onChanged: (value) {
setState(() {
if (value == true) {
_selectedIds.add(program.id!);
} else {
_selectedIds.remove(program.id!);
}
});
},
),
),
// 编号
SizedBox(
width: 100,
child: Text(
program.code,
style: TextStyle(color: AppTheme.textPrimary),
),
),
// 名称
Expanded(
flex: 2,
child: Text(
program.name,
style: TextStyle(color: AppTheme.textPrimary),
),
),
// 创建时间
Expanded(
child: Text(
program.createdAt,
style: TextStyle(color: AppTheme.textSecondary),
),
),
// 状态
SizedBox(
width: 80,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: program.status == 1
? AppTheme.successColor.withValues(alpha: 0.1)
: AppTheme.idleColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
program.status == 1 ? '启用' : '停用',
style: TextStyle(
color: program.status == 1
? AppTheme.successColor
: AppTheme.idleColor,
fontSize: 12,
),
textAlign: TextAlign.center,
),
),
),
// 操作按钮
SizedBox(
width: 150,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.edit, size: 20),
color: AppTheme.primaryColor,
onPressed: () => _showEditDialog(context, ref, program),
),
IconButton(
icon: const Icon(Icons.delete, size: 20),
color: AppTheme.errorColor,
onPressed: () => _showDeleteConfirmDialog(
context,
ref,
l10n,
[program.id!],
),
),
IconButton(
icon: const Icon(Icons.visibility, size: 20),
color: AppTheme.textSecondary,
onPressed: () => context.go('/programs/${program.id}'),
),
],
),
),
],
),
);
}
/// 显示新增对话框
void _showAddDialog(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => const ProgramFormDialog(),
);
}
/// 导入程序
Future<void> _importPrograms(BuildContext context, WidgetRef ref) async {
try {
// 选择文件
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['json'],
allowMultiple: false,
);
if (result == null || result.files.isEmpty) {
return;
}
final file = result.files.first;
if (file.path == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('无法读取文件'),
backgroundColor: AppTheme.errorColor,
),
);
return;
}
// 读取文件内容
final jsonContent = await File(file.path!).readAsString();
// 导入程序
final importedCount = await ProgramImportService.instance.importFromJson(jsonContent);
// 刷新程序列表
ref.read(programsProvider.notifier).loadPrograms();
// 显示结果
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('成功导入 $importedCount 个程序'),
backgroundColor: importedCount > 0 ? AppTheme.successColor : AppTheme.warningColor,
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('导入失败: ${e.toString()}'),
backgroundColor: AppTheme.errorColor,
),
);
}
}
/// 显示编辑对话框
void _showEditDialog(BuildContext context, WidgetRef ref, Program program) {
showDialog(
context: context,
builder: (context) => ProgramFormDialog(program: program),
);
}
/// 显示删除确认对话框
void _showDeleteConfirmDialog(
BuildContext context,
WidgetRef ref,
AppLocalizations? l10n,
List<int> ids,
) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n?.confirm ?? '确认'),
content: Text(
ids.length == 1
? '确定要删除此程序吗?'
: '确定要删除选中的 ${ids.length} 个程序吗?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(l10n?.cancel ?? '取消'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.errorColor,
foregroundColor: Colors.white,
),
onPressed: () async {
final notifier = ref.read(programsProvider.notifier);
await notifier.deletePrograms(ids);
setState(() {
_selectedIds.removeAll(ids);
});
Navigator.of(context).pop();
},
child: Text(l10n?.confirm ?? '确认'),
),
],
),
);
}
}

View File

@@ -0,0 +1,192 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/database/database_service.dart';
import '../models/program.dart';
/// 程序列表状态
class ProgramsState {
final List<Program> programs;
final int? selectedProgramId;
final bool isLoading;
final String? error;
const ProgramsState({
this.programs = const [],
this.selectedProgramId,
this.isLoading = false,
this.error,
});
ProgramsState copyWith({
List<Program>? programs,
int? selectedProgramId,
bool? isLoading,
String? error,
bool clearSelection = false,
bool clearError = false,
}) {
return ProgramsState(
programs: programs ?? this.programs,
selectedProgramId: clearSelection ? null : (selectedProgramId ?? this.selectedProgramId),
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : (error ?? this.error),
);
}
/// 获取选中的程序
Program? get selectedProgram {
if (selectedProgramId == null) return null;
return programs.where((p) => p.id == selectedProgramId).firstOrNull;
}
}
/// 程序列表 Notifier
class ProgramsNotifier extends StateNotifier<ProgramsState> {
final DatabaseService _db;
ProgramsNotifier(this._db) : super(const ProgramsState()) {
loadPrograms();
}
/// 加载所有程序
Future<void> loadPrograms() async {
state = state.copyWith(isLoading: true, clearError: true);
try {
final db = await _db.database;
final maps = await db.query('programs', orderBy: 'created_at DESC');
final programs = maps.map((m) => Program.fromMap(m)).toList();
state = state.copyWith(programs: programs, isLoading: false);
} catch (e) {
state = state.copyWith(isLoading: false, error: e.toString());
}
}
/// 选择程序
void selectProgram(int? programId) {
state = state.copyWith(selectedProgramId: programId);
}
/// 清除选择
void clearSelection() {
state = state.copyWith(clearSelection: true);
}
/// 新增程序
Future<bool> addProgram(Program program) async {
try {
final db = await _db.database;
await db.insert('programs', program.toMap());
await loadPrograms();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 更新程序
Future<bool> updateProgram(Program program) async {
if (program.id == null) return false;
try {
final db = await _db.database;
await db.update(
'programs',
program.toMap(),
where: 'id = ?',
whereArgs: [program.id],
);
await loadPrograms();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 删除程序
Future<bool> deleteProgram(int programId) async {
try {
final db = await _db.database;
await db.delete('programs', where: 'id = ?', whereArgs: [programId]);
// 如果删除的是选中的程序,清除选择
if (state.selectedProgramId == programId) {
state = state.copyWith(clearSelection: true);
}
await loadPrograms();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 批量删除程序
Future<bool> deletePrograms(List<int> programIds) async {
try {
final db = await _db.database;
await db.delete(
'programs',
where: 'id IN (${programIds.map((_) => '?').join(',')})',
whereArgs: programIds,
);
// 如果删除的是选中的程序,清除选择
if (programIds.contains(state.selectedProgramId)) {
state = state.copyWith(clearSelection: true);
}
await loadPrograms();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
/// 切换程序状态
Future<bool> toggleStatus(int programId) async {
try {
final db = await _db.database;
final program = state.programs.where((p) => p.id == programId).firstOrNull;
if (program == null) return false;
await db.update(
'programs',
{'status': program.status == 1 ? 0 : 1},
where: 'id = ?',
whereArgs: [programId],
);
await loadPrograms();
return true;
} catch (e) {
state = state.copyWith(error: e.toString());
return false;
}
}
}
/// 数据库服务 Provider
final databaseServiceProvider = Provider<DatabaseService>((ref) {
return DatabaseService.instance;
});
/// 程序列表 Provider
final programsProvider =
StateNotifierProvider<ProgramsNotifier, ProgramsState>((ref) {
final db = ref.watch(databaseServiceProvider);
return ProgramsNotifier(db);
});
/// 选中的程序 Provider
final selectedProgramProvider = Provider<Program?>((ref) {
return ref.watch(programsProvider).selectedProgram;
});
/// 启用的程序列表 Provider
final enabledProgramsProvider = Provider<List<Program>>((ref) {
return ref.watch(programsProvider).programs.where((p) => p.status == 1).toList();
});

View File

@@ -0,0 +1,126 @@
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

@@ -0,0 +1,156 @@
import '../../../core/database/database_service.dart';
import '../models/program.dart';
import '../models/step.dart';
/// 程序服务
/// 封装程序和步骤的数据库操作
class ProgramService {
static final ProgramService instance = ProgramService._internal();
final DatabaseService _db = DatabaseService.instance;
ProgramService._internal();
/// 获取所有程序
Future<List<Program>> getAllPrograms() async {
final database = await _db.database;
final maps = await database.query('programs', orderBy: 'created_at DESC');
return maps.map((m) => Program.fromMap(m)).toList();
}
/// 根据ID获取程序
Future<Program?> getProgramById(int id) async {
final database = await _db.database;
final maps = await database.query(
'programs',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isEmpty) return null;
return Program.fromMap(maps.first);
}
/// 新增程序
Future<int> addProgram(Program program) async {
final database = await _db.database;
return await database.insert('programs', program.toMap());
}
/// 更新程序
Future<bool> updateProgram(Program program) async {
if (program.id == null) return false;
final database = await _db.database;
final count = await database.update(
'programs',
program.toMap(),
where: 'id = ?',
whereArgs: [program.id],
);
return count > 0;
}
/// 删除程序(含步骤)
Future<bool> deleteProgram(int id) async {
final database = await _db.database;
// 先删除关联的步骤
await database.delete('steps', where: 'program_id = ?', whereArgs: [id]);
// 再删除程序
final count = await database.delete('programs', where: 'id = ?', whereArgs: [id]);
return count > 0;
}
/// 批量删除程序
Future<bool> deletePrograms(List<int> ids) async {
if (ids.isEmpty) return true;
final database = await _db.database;
// 先删除关联的步骤
await database.delete(
'steps',
where: 'program_id IN (${ids.map((_) => '?').join(',')})',
whereArgs: ids,
);
// 再删除程序
final count = await database.delete(
'programs',
where: 'id IN (${ids.map((_) => '?').join(',')})',
whereArgs: ids,
);
return count > 0;
}
/// 切换程序状态
Future<bool> toggleProgramStatus(int id) async {
final database = await _db.database;
final program = await getProgramById(id);
if (program == null) return false;
final count = await database.update(
'programs',
{'status': program.status == 1 ? 0 : 1},
where: 'id = ?',
whereArgs: [id],
);
return count > 0;
}
/// 获取程序的步骤列表
Future<List<Step>> getStepsByProgramId(int programId) async {
final database = await _db.database;
final maps = await database.query(
'steps',
where: 'program_id = ?',
whereArgs: [programId],
orderBy: 'step_no ASC',
);
return maps.map((m) => Step.fromMap(m)).toList();
}
/// 新增步骤
Future<int> addStep(Step step) async {
final database = await _db.database;
return await database.insert('steps', step.toMap());
}
/// 更新步骤
Future<bool> updateStep(Step step) async {
if (step.id == null) return false;
final database = await _db.database;
final count = await database.update(
'steps',
step.toMap(),
where: 'id = ?',
whereArgs: [step.id],
);
return count > 0;
}
/// 删除步骤
Future<bool> deleteStep(int id) async {
final database = await _db.database;
final count = await database.delete('steps', where: 'id = ?', whereArgs: [id]);
return count > 0;
}
/// 批量删除步骤
Future<bool> deleteSteps(List<int> ids) async {
if (ids.isEmpty) return true;
final database = await _db.database;
final count = await database.delete(
'steps',
where: 'id IN (${ids.map((_) => '?').join(',')})',
whereArgs: ids,
);
return count > 0;
}
/// 更新步骤排序
Future<void> reorderSteps(int programId, List<int> stepIds) async {
final database = await _db.database;
for (int i = 0; i < stepIds.length; i++) {
await database.update(
'steps',
{'step_no': i + 1},
where: 'id = ? AND program_id = ?',
whereArgs: [stepIds[i], programId],
);
}
}
}

View File

@@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../models/program.dart';
import '../providers/programs_provider.dart';
/// 程序表单弹窗
/// 用于新增和编辑程序
class ProgramFormDialog extends ConsumerStatefulWidget {
final Program? program;
const ProgramFormDialog({super.key, this.program});
@override
ConsumerState<ProgramFormDialog> createState() => _ProgramFormDialogState();
}
class _ProgramFormDialogState extends ConsumerState<ProgramFormDialog> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _codeController;
late TextEditingController _nameController;
bool _isEnabled = true;
bool _isSaving = false;
@override
void initState() {
super.initState();
_codeController = TextEditingController(text: widget.program?.code ?? '');
_nameController = TextEditingController(text: widget.program?.name ?? '');
_isEnabled = widget.program?.status == 1;
}
@override
void dispose() {
_codeController.dispose();
_nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final isEditing = widget.program != null;
return AlertDialog(
title: Text(
isEditing
? (l10n?.editProgram ?? '编辑程序')
: (l10n?.addProgram ?? '新增程序'),
),
content: SizedBox(
width: 400,
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 编号输入
TextFormField(
controller: _codeController,
decoration: InputDecoration(
labelText: l10n?.programCode ?? '编号',
hintText: '例如: P001',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入编号';
}
return null;
},
),
const SizedBox(height: 16),
// 名称输入
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: l10n?.programName ?? '名称',
hintText: '请输入程序名称',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入名称';
}
return null;
},
),
const SizedBox(height: 16),
// 状态开关
Row(
children: [
Text(
'状态',
style: TextStyle(color: AppTheme.textPrimary),
),
const Spacer(),
Switch(
value: _isEnabled,
onChanged: (value) {
setState(() {
_isEnabled = value;
});
},
activeColor: AppTheme.successColor,
),
Text(
_isEnabled ? '启用' : '停用',
style: TextStyle(
color: _isEnabled ? AppTheme.successColor : AppTheme.idleColor,
),
),
],
),
],
),
),
),
actions: [
TextButton(
onPressed: _isSaving ? null : () => Navigator.of(context).pop(),
child: Text(l10n?.cancel ?? '取消'),
),
CommonButton(
text: l10n?.save ?? '保存',
icon: Icons.save,
type: ButtonType.primary,
isLoading: _isSaving,
onPressed: _isSaving ? null : () => _saveProgram(context, ref, l10n),
),
],
);
}
/// 保存程序
Future<void> _saveProgram(
BuildContext context,
WidgetRef ref,
AppLocalizations? l10n,
) async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isSaving = true;
});
final notifier = ref.read(programsProvider.notifier);
final now = DateTime.now().toString().substring(0, 10);
final program = Program(
id: widget.program?.id,
code: _codeController.text.trim(),
name: _nameController.text.trim(),
createdAt: widget.program?.createdAt ?? now,
status: _isEnabled ? 1 : 0,
);
bool success;
if (widget.program != null) {
success = await notifier.updateProgram(program);
} else {
success = await notifier.addProgram(program);
}
setState(() {
_isSaving = false;
});
if (success) {
Navigator.of(context).pop();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('保存失败,请检查编号是否重复'),
backgroundColor: AppTheme.errorColor,
),
);
}
}
}

View File

@@ -0,0 +1,382 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/localization/app_localizations.dart';
import '../../../core/localization/locale_provider.dart';
import '../../../core/theme/app_theme.dart';
import '../../../shared/widgets/common_button.dart';
import '../services/settings_service.dart';
/// 系统设置页面
class SettingsPage extends ConsumerStatefulWidget {
const SettingsPage({super.key});
@override
ConsumerState<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends ConsumerState<SettingsPage> {
String _currentVersion = 'V1.0.0';
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
// locale 用于语言切换,通过 ref.watch 保持监听
return Scaffold(
body: Container(
color: AppTheme.backgroundColor,
child: Row(
children: [
// 左侧导航菜单
SizedBox(
width: 280,
child: Container(
color: Colors.white,
child: Column(
children: [
// 返回按钮
Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
color: AppTheme.textPrimary,
onPressed: () => context.go('/'),
),
Text(
'返回首页',
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 14,
),
),
],
),
),
// 设置标题
Container(
height: 60,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withValues(alpha: 0.1),
),
child: Row(
children: [
Icon(Icons.settings, color: AppTheme.primaryColor, size: 24),
const SizedBox(width: 12),
Text(
l10n?.settings ?? '系统设置',
style: TextStyle(
color: AppTheme.primaryColor,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
],
),
),
// 软件升级
_buildMenuItem(
icon: Icons.system_update,
title: l10n?.upgrade ?? '软件升级',
onTap: () {},
),
// 语言设置
_buildMenuItem(
icon: Icons.language,
title: l10n?.language ?? '语言设置',
onTap: () => _showLanguageDialog(),
),
// 安全设置
_buildMenuItem(
icon: Icons.lock,
title: l10n?.password ?? '密码修改',
onTap: () => _showPasswordDialog(),
),
// U盘导入
_buildMenuItem(
icon: Icons.usb,
title: l10n?.usbImport ?? 'U盘导入',
onTap: () => _showUsbImportDialog(),
),
],
),
),
),
// 右侧内容区域
Expanded(
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n?.upgrade ?? '软件升级',
style: TextStyle(
color: AppTheme.textPrimary,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 24),
// 版本信息
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.backgroundColor,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(Icons.info_outline, color: AppTheme.primaryColor),
const SizedBox(width: 12),
Text(
'当前版本: $_currentVersion',
style: TextStyle(color: AppTheme.textPrimary),
),
],
),
),
const SizedBox(height: 24),
// 检查更新按钮
CommonButton(
text: '检查更新',
icon: Icons.refresh,
type: ButtonType.primary,
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已是最新版本'),
backgroundColor: AppTheme.successColor,
),
);
},
),
],
),
),
),
],
),
),
);
}
/// 导航菜单项
Widget _buildMenuItem({
required IconData icon,
required String title,
required VoidCallback onTap,
}) {
return ListTile(
leading: Icon(icon, color: AppTheme.textSecondary),
title: Text(title, style: TextStyle(color: AppTheme.textPrimary)),
trailing: Icon(Icons.chevron_right, color: AppTheme.idleColor),
onTap: onTap,
);
}
/// 显示语言选择对话框
void _showLanguageDialog() {
final locale = ref.read(localeProvider);
final currentLang = locale.languageCode;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('语言设置'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<String>(
title: Text('简体中文'),
value: 'zh',
groupValue: currentLang,
onChanged: (value) {
ref.read(localeProvider.notifier).setChinese();
Navigator.of(ctx).pop();
},
),
RadioListTile<String>(
title: Text('English'),
value: 'en',
groupValue: currentLang,
onChanged: (value) {
ref.read(localeProvider.notifier).setEnglish();
Navigator.of(ctx).pop();
},
),
],
),
),
);
}
/// 显示密码修改对话框
void _showPasswordDialog() {
final oldPasswordController = TextEditingController();
final newPasswordController = TextEditingController();
final confirmPasswordController = TextEditingController();
String? errorMessage;
showDialog(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: Text('密码修改'),
content: SizedBox(
width: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: oldPasswordController,
decoration: InputDecoration(
labelText: '原密码',
errorText: null,
),
obscureText: true,
),
const SizedBox(height: 12),
TextField(
controller: newPasswordController,
decoration: InputDecoration(
labelText: '新密码',
helperText: '至少6位字符',
),
obscureText: true,
),
const SizedBox(height: 12),
TextField(
controller: confirmPasswordController,
decoration: InputDecoration(labelText: '确认新密码'),
obscureText: true,
),
if (errorMessage != null)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(
errorMessage!,
style: TextStyle(color: AppTheme.errorColor, fontSize: 12),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text('取消'),
),
ElevatedButton(
onPressed: () async {
// 验证逻辑
final oldPassword = oldPasswordController.text.trim();
final newPassword = newPasswordController.text.trim();
final confirmPassword = confirmPasswordController.text.trim();
// 检查空值
if (oldPassword.isEmpty || newPassword.isEmpty || confirmPassword.isEmpty) {
setState(() => errorMessage = '请填写所有字段');
return;
}
// 检查新密码长度
if (newPassword.length < 6) {
setState(() => errorMessage = '新密码至少6位字符');
return;
}
// 检查新密码一致性
if (newPassword != confirmPassword) {
setState(() => errorMessage = '两次输入的新密码不一致');
return;
}
// 验证原密码
final isValid = await SettingsService.instance.verifyPassword(oldPassword);
if (!isValid) {
setState(() => errorMessage = '原密码错误');
return;
}
// 保存新密码
final success = await SettingsService.instance.setPassword(newPassword);
Navigator.of(ctx).pop();
if (success) {
ScaffoldMessenger.of(this.context).showSnackBar(
SnackBar(
content: Text('密码已修改'),
backgroundColor: AppTheme.successColor,
),
);
} else {
ScaffoldMessenger.of(this.context).showSnackBar(
SnackBar(
content: Text('密码修改失败'),
backgroundColor: AppTheme.errorColor,
),
);
}
},
child: Text('确认'),
),
],
),
),
);
}
/// 显示U盘导入对话框
void _showUsbImportDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('U盘导入'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.usb, size: 48, color: AppTheme.warningColor),
const SizedBox(height: 16),
Text('未检测到U盘'),
const SizedBox(height: 8),
Text(
'请插入U盘后重试',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 12),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('关闭'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('正在检测U盘...'),
backgroundColor: AppTheme.primaryColor,
),
);
},
child: Text('重新检测'),
),
],
),
);
}
}

View File

@@ -0,0 +1,63 @@
import '../../../core/database/database_service.dart';
/// 设置服务
/// 管理系统设置(密码、语言偏好等)
class SettingsService {
static final SettingsService instance = SettingsService._internal();
final DatabaseService _db = DatabaseService.instance;
SettingsService._internal();
/// 获取密码
Future<String> getPassword() async {
final database = await _db.database;
final results = await database.query(
'settings',
where: 'key = ?',
whereArgs: ['password'],
);
if (results.isEmpty) return '123456'; // 默认密码
return results.first['value'] as String;
}
/// 设置密码
Future<bool> setPassword(String newPassword) async {
final database = await _db.database;
final count = await database.update(
'settings',
{'value': newPassword},
where: 'key = ?',
whereArgs: ['password'],
);
return count > 0;
}
/// 验证密码
Future<bool> verifyPassword(String password) async {
final storedPassword = await getPassword();
return password == storedPassword;
}
/// 获取设置值
Future<String?> getSetting(String key) async {
final database = await _db.database;
final results = await database.query(
'settings',
where: 'key = ?',
whereArgs: [key],
);
if (results.isEmpty) return null;
return results.first['value'] as String;
}
/// 设置值
Future<bool> setSetting(String key, String value) async {
final database = await _db.database;
// 使用 insert 或 replace
await database.execute(
'INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)',
[key, value],
);
return true;
}
}

View File

@@ -0,0 +1,99 @@
import 'dart:async';
/// USB 检测服务
/// 监听 U盘插入/拔出事件
class UsbDetectionService {
static final UsbDetectionService instance = UsbDetectionService._internal();
UsbDetectionService._internal();
/// USB 状态
bool _isUsbConnected = false;
String? _usbPath;
/// 状态流
final StreamController<UsbState> _stateController = StreamController<UsbState>.broadcast();
/// 监听 USB 状态变化
Stream<UsbState> get stateStream => _stateController.stream;
/// 当前 USB 是否连接
bool get isConnected => _isUsbConnected;
/// USB 路径
String? get usbPath => _usbPath;
/// 开始监听 USB 事件
void startMonitoring() {
// TODO: 实现平台特定的 USB 监听
// Android: 使用 BroadcastReceiver 监听 ACTION_MEDIA_MOUNTED
// Linux: 监听 /dev/disk/by-path/ 或使用 udev
// Windows: 监听 WM_DEVICECHANGE
// 模拟实现:定时检测
_startPolling();
}
/// 停止监听
void stopMonitoring() {
// _stopPolling();
}
/// 模拟轮询检测(待平台实现)
void _startPolling() {
// TODO: 根据平台实现真实的 USB 检测
// 定时检测 /mnt/usb 或 /media/*/ 目录
}
/// 手动检测 USB
Future<bool> detectUsb() async {
// TODO: 实现平台特定的 USB 检测
// Android: 检查 getExternalFilesDir 或 mount points
// Linux: 检查 /mnt, /media 目录
// Windows: 检查 D:, E: 等驱动器
// 返回检测结果
return _isUsbConnected;
}
/// 获取 USB 上的程序文件列表
Future<List<String>> listProgramFiles() async {
if (!_isUsbConnected || _usbPath == null) {
return [];
}
// TODO: 扫描 USB 目录中的 .json 程序文件
// 示例路径: $_usbPath/programs/*.json
return [];
}
/// 模拟 USB 连接(用于测试)
void simulateConnection(String path) {
_isUsbConnected = true;
_usbPath = path;
_stateController.add(UsbState.connected(path));
}
/// 模拟 USB 断开(用于测试)
void simulateDisconnection() {
_isUsbConnected = false;
_usbPath = null;
_stateController.add(UsbState.disconnected());
}
/// 释放资源
void dispose() {
stopMonitoring();
_stateController.close();
}
}
/// USB 状态
class UsbState {
final bool isConnected;
final String? path;
const UsbState.connected(String path) : isConnected = true, path = path;
const UsbState.disconnected() : isConnected = false, path = null;
}

61
lib/main.dart Normal file
View File

@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'core/router/app_router.dart';
import 'core/theme/app_theme.dart';
import 'core/localization/app_localizations.dart';
import 'core/localization/locale_provider.dart';
import 'core/database/database_service.dart';
/// 应用入口
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Kiosk 模式:隐藏系统状态栏和导航栏
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
// 固定横屏
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
final db = DatabaseService.instance;
await db.database;
await db.initTestData();
runApp(const ProviderScope(child: KuaishaiApp()));
}
/// 应用主体
class KuaishaiApp extends ConsumerWidget {
const KuaishaiApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(goRouterProvider);
final locale = ref.watch(localeProvider);
return MaterialApp.router(
title: '污水毒品快检一体机',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme(),
darkTheme: AppTheme.darkTheme(),
themeMode: ThemeMode.light,
// 国际化配置
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('zh', 'CN'),
Locale('en', 'US'),
],
locale: locale,
// 路由配置
routerConfig: router,
);
}
}

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_theme.dart';
/// Toast 服务
/// 统一的消息提示管理
class ToastService {
/// 显示成功提示
static void showSuccess(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.successColor,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
),
);
}
/// 显示错误提示
static void showError(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.errorColor,
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
),
);
}
/// 显示警告提示
static void showWarning(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.warning, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.warningColor,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
),
);
}
/// 显示信息提示
static void showInfo(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.info, color: Colors.white, size: 20),
const SizedBox(width: 12),
Expanded(child: Text(message)),
],
),
backgroundColor: AppTheme.primaryColor,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
margin: const EdgeInsets.all(16),
),
);
}
}

View File

@@ -0,0 +1,24 @@
/// 常量定义
class Constants {
// 速度选项
static const List<String> speedOptions = ['低速', '中速', '高速'];
// 下针速度档位
static const List<int> needleSpeedLevels = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 孔位列表
static const List<String> positions = [
'A1', 'A2', 'A3', 'A4', 'A5', 'A6',
'B1', 'B2', 'B3', 'B4', 'B5', 'B6',
'C1', 'C2', 'C3', 'C4', 'C5', 'C6',
'D1', 'D2', 'D3', 'D4', 'D5', 'D6',
];
// 默认步骤名称
static const List<String> defaultStepNames = ['混合', '吸磁', '吹气', '下针'];
// 时间单位
static const String timeUnitSeconds = '';
static const String timeUnitMinutes = '分钟';
static const String volumeUnit = 'μL';
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
/// 响应式布局工具类
/// 目标屏幕: 1920x1080
class ResponsiveLayout {
static const double targetWidth = 1920;
static const double targetHeight = 1080;
/// 获取屏幕宽度比例
static double widthPercent(BuildContext context, double percent) {
return MediaQuery.of(context).size.width * percent;
}
/// 获取屏幕高度比例
static double heightPercent(BuildContext context, double percent) {
return MediaQuery.of(context).size.height * percent;
}
/// 基于目标屏幕缩放宽度
static double scaleWidth(BuildContext context, double targetValue) {
final screenWidth = MediaQuery.of(context).size.width;
return targetValue * (screenWidth / targetWidth);
}
/// 基于目标屏幕缩放高度
static double scaleHeight(BuildContext context, double targetValue) {
final screenHeight = MediaQuery.of(context).size.height;
return targetValue * (screenHeight / targetHeight);
}
/// 基于目标屏幕缩放字体
static double scaleFont(BuildContext context, double targetFontSize) {
return scaleWidth(context, targetFontSize);
}
/// 预设布局尺寸
static double sidebarWidth(BuildContext context) => widthPercent(context, 0.25); // 480px on 1920
static double detailWidth(BuildContext context) => widthPercent(context, 0.21); // 400px on 1920
static double navWidth(BuildContext context) => widthPercent(context, 0.15); // 280px on 1920
static double cardWidth(BuildContext context) => widthPercent(context, 0.30); // ~576px
}
/// 响应式间距
class ResponsiveSpacing {
static double small(BuildContext context) => ResponsiveLayout.scaleWidth(context, 8);
static double medium(BuildContext context) => ResponsiveLayout.scaleWidth(context, 16);
static double large(BuildContext context) => ResponsiveLayout.scaleWidth(context, 24);
static double xlarge(BuildContext context) => ResponsiveLayout.scaleWidth(context, 32);
}

View File

@@ -0,0 +1,103 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_theme.dart';
/// 通用按钮组件 - 明亮工业风格
class CommonButton extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool enabled;
final Color? backgroundColor;
final Color? textColor;
final IconData? icon;
final bool isLoading;
final ButtonType type;
const CommonButton({
super.key,
required this.text,
this.onPressed,
this.enabled = true,
this.backgroundColor,
this.textColor,
this.icon,
this.isLoading = false,
this.type = ButtonType.primary,
});
@override
Widget build(BuildContext context) {
final bgColor = backgroundColor ?? _getDefaultBackgroundColor();
final fgColor = textColor ?? _getDefaultTextColor();
Widget content;
if (isLoading) {
content = Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: fgColor,
),
),
const SizedBox(width: 8),
Text(text, style: TextStyle(fontWeight: FontWeight.w500)),
],
);
} else if (icon != null) {
content = Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18),
const SizedBox(width: 8),
Text(text, style: TextStyle(fontWeight: FontWeight.w500)),
],
);
} else {
content = Text(text, style: TextStyle(fontWeight: FontWeight.w500));
}
return ElevatedButton(
onPressed: enabled && !isLoading ? onPressed : null,
style: ElevatedButton.styleFrom(
backgroundColor: bgColor,
foregroundColor: fgColor,
disabledBackgroundColor: AppTheme.statusStopped.withValues(alpha: 0.3),
disabledForegroundColor: AppTheme.textTertiary,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
),
),
child: content,
);
}
Color _getDefaultBackgroundColor() {
switch (type) {
case ButtonType.primary:
return AppTheme.primaryColor;
case ButtonType.success:
return AppTheme.successColor;
case ButtonType.warning:
return AppTheme.warningColor;
case ButtonType.danger:
return AppTheme.errorColor;
case ButtonType.secondary:
return AppTheme.bgSurface;
}
}
Color _getDefaultTextColor() {
switch (type) {
case ButtonType.secondary:
return AppTheme.textPrimary;
default:
return AppTheme.textOnPrimary;
}
}
}
enum ButtonType { primary, success, warning, danger, secondary }

View File

@@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_theme.dart';
/// 通用卡片组件 - 明亮工业风格
class CommonCard extends StatelessWidget {
final Widget child;
final VoidCallback? onTap;
final bool selected;
final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin;
const CommonCard({
super.key,
required this.child,
this.onTap,
this.selected = false,
this.padding,
this.margin,
});
@override
Widget build(BuildContext context) {
return Container(
margin: margin ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Material(
color: selected ? AppTheme.bgCardHover : AppTheme.bgCard,
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
elevation: 0,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
child: Container(
padding: padding ?? const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
border: selected
? Border.all(color: AppTheme.primaryColor, width: 2)
: Border.all(color: AppTheme.borderLight, width: 1),
boxShadow: AppTheme.shadowCard,
),
child: child,
),
),
),
);
}
}

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
/// 确认对话框组件
class CommonDialog {
/// 显示确认对话框
static Future<bool?> showConfirm({
required BuildContext context,
required String title,
required String content,
String confirmText = '确认',
String cancelText = '取消',
bool isDestructive = false,
}) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: Text(cancelText),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: isDestructive ? Colors.red : null,
),
child: Text(confirmText),
),
],
),
);
}
/// 显示信息对话框
static Future<void> showInfo({
required BuildContext context,
required String title,
required String content,
String confirmText = '确认',
}) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: Text(confirmText),
),
],
),
);
}
/// 显示输入对话框
static Future<String?> showInput({
required BuildContext context,
required String title,
String? hintText,
String? initialValue,
String confirmText = '确认',
String cancelText = '取消',
}) {
final controller = TextEditingController(text: initialValue);
return showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: TextField(
decoration: InputDecoration(
hintText: hintText,
border: const OutlineInputBorder(),
),
controller: controller,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(cancelText),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, controller.text),
child: Text(confirmText),
),
],
),
);
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_theme.dart';
/// 空状态组件
/// 统一的空数据展示样式
class EmptyStateWidget extends StatelessWidget {
final IconData icon;
final String message;
final String? actionText;
final VoidCallback? onAction;
const EmptyStateWidget({
super.key,
required this.icon,
required this.message,
this.actionText,
this.onAction,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 64,
color: AppTheme.idleColor,
),
const SizedBox(height: 16),
Text(
message,
style: TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
),
),
if (actionText != null && onAction != null) ...[
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: onAction,
icon: const Icon(Icons.add, size: 20),
label: Text(actionText!),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
],
],
),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import '../../core/theme/app_theme.dart';
/// 状态指示器组件 - 明亮工业风格
class StatusIndicator extends StatelessWidget {
final String text;
final DeviceStatusType status;
final double size;
const StatusIndicator({
super.key,
required this.text,
required this.status,
this.size = 10,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getStatusColor(),
),
),
const SizedBox(width: 6),
Text(
text,
style: TextStyle(
color: _getStatusColor(),
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
],
);
}
Color _getStatusColor() {
switch (status) {
case DeviceStatusType.running:
return AppTheme.statusRunning;
case DeviceStatusType.idle:
return AppTheme.statusStopped;
case DeviceStatusType.paused:
return AppTheme.statusPaused;
case DeviceStatusType.error:
return AppTheme.statusError;
case DeviceStatusType.success:
return AppTheme.statusRunning;
}
}
}
enum DeviceStatusType { running, idle, paused, error, success }

BIN
pencil/images/Untitled.fig Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

12973
pencil/untitled.pen Normal file

File diff suppressed because it is too large Load Diff

719
pubspec.lock Normal file
View File

@@ -0,0 +1,719 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
source: hosted
version: "85.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c
url: "https://pub.dev"
source: hosted
version: "7.6.0"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce
url: "https://pub.dev"
source: hosted
version: "0.13.4"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
ci:
dependency: transitive
description:
name: ci
sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
url: "https://pub.dev"
source: hosted
version: "0.3.5+2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
custom_lint:
dependency: transitive
description:
name: custom_lint
sha256: "9656925637516c5cf0f5da018b33df94025af2088fe09c8ae2ca54c53f2d9a84"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
custom_lint_builder:
dependency: transitive
description:
name: custom_lint_builder
sha256: "6cdc8e87e51baaaba9c43e283ed8d28e59a0c4732279df62f66f7b5984655414"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
custom_lint_visitor:
dependency: transitive
description:
name: custom_lint_visitor
sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2"
url: "https://pub.dev"
source: hosted
version: "1.0.0+7.7.0"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_picker:
dependency: "direct main"
description:
name: file_picker
sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810
url: "https://pub.dev"
source: hosted
version: "8.3.7"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
url: "https://pub.dev"
source: hosted
version: "2.0.34"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: b453934c36e289cef06525734d1e676c1f91da9e22e2017d9dcab6ce0f999175
url: "https://pub.dev"
source: hosted
version: "15.1.3"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf"
url: "https://pub.dev"
source: hosted
version: "4.4.0"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
url: "https://pub.dev"
source: hosted
version: "4.12.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
url: "https://pub.dev"
source: hosted
version: "2.6.1"
riverpod_analyzer_utils:
dependency: transitive
description:
name: riverpod_analyzer_utils
sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611"
url: "https://pub.dev"
source: hosted
version: "0.5.10"
riverpod_lint:
dependency: "direct dev"
description:
name: riverpod_lint
sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35"
url: "https://pub.dev"
source: hosted
version: "2.6.5"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
url: "https://pub.dev"
source: hosted
version: "2.5.5"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
url: "https://pub.dev"
source: hosted
version: "2.4.23"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a"
url: "https://pub.dev"
source: hosted
version: "2.4.2+1"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40"
url: "https://pub.dev"
source: hosted
version: "2.4.2+3"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "1581ffbf7a0e333b380d6a30737d78516b826cb35beb7fb0bf8a3ea0c678b465"
url: "https://pub.dev"
source: hosted
version: "2.5.8"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5"
url: "https://pub.dev"
source: hosted
version: "3.4.0+1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.5 <4.0.0"
flutter: ">=3.38.0"

44
pubspec.yaml Normal file
View File

@@ -0,0 +1,44 @@
name: kuaishai2
description: "污水毒品快检一体机控制软件"
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.11.5
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.8
# 状态管理
flutter_riverpod: ^2.6.0
# 路由导航
go_router: ^15.0.0
# 数据存储
sqflite: ^2.3.0
path: ^1.9.0
# 持久化存储
shared_preferences: ^2.3.3
# 文件选择器
file_picker: ^8.1.7
# 国际化
intl: ^0.20.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
riverpod_lint: ^2.6.0
flutter:
uses-material-design: true

View File

@@ -0,0 +1,38 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:kuaishai2/core/localization/app_localizations.dart';
void main() {
group('AppLocalizations', () {
test('Chinese locale should return Chinese strings', () {
const locale = Locale('zh', 'CN');
final l10n = AppLocalizations(locale);
expect(l10n.deviceName, contains('一体机'));
expect(l10n.programs, equals('程序管理'));
expect(l10n.run, equals('运行'));
expect(l10n.settings, equals('系统设置'));
});
test('English locale should return English strings', () {
const locale = Locale('en', 'US');
final l10n = AppLocalizations(locale);
expect(l10n.deviceName, contains('System'));
expect(l10n.programs, equals('Programs'));
expect(l10n.run, equals('Run'));
expect(l10n.settings, equals('Settings'));
});
test('New translation keys should work', () {
const locale = Locale('zh', 'CN');
final l10n = AppLocalizations(locale);
expect(l10n.lightOn, equals(''));
expect(l10n.lightOff, equals(''));
expect(l10n.enabled, equals('启用'));
expect(l10n.disabled, equals('停用'));
expect(l10n.stepList, equals('步骤列表'));
});
});
}

91
test/models_test.dart Normal file
View File

@@ -0,0 +1,91 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:kuaishai2/features/programs/models/program.dart';
import 'package:kuaishai2/features/programs/models/step.dart';
void main() {
group('Program Model', () {
test('toMap and fromMap should work correctly', () {
final program = Program(
id: 1,
code: 'P001',
name: 'Test Program',
createdAt: '2026-05-20',
status: 1,
);
final map = program.toMap();
final fromMap = Program.fromMap(map);
expect(fromMap.id, equals(program.id));
expect(fromMap.code, equals(program.code));
expect(fromMap.name, equals(program.name));
expect(fromMap.createdAt, equals(program.createdAt));
expect(fromMap.status, equals(program.status));
});
test('copyWith should create modified copy', () {
final program = Program(
id: 1,
code: 'P001',
name: 'Test Program',
createdAt: '2026-05-20',
status: 1,
);
final copy = program.copyWith(name: 'Updated Name', status: 0);
expect(copy.id, equals(program.id));
expect(copy.code, equals(program.code));
expect(copy.name, equals('Updated Name'));
expect(copy.status, equals(0));
});
});
group('Step Model', () {
test('toMap and fromMap should work correctly', () {
final step = Step(
id: 1,
programId: 1,
stepNo: 1,
position: 'A1',
name: 'Mix',
mixTime: 60,
magnetTime: 30,
volume: 100,
mixSpeed: '中速',
blowSpeed: '高速',
blowTime: 10,
needleSpeed: 5,
);
final map = step.toMap();
final fromMap = Step.fromMap(map);
expect(fromMap.id, equals(step.id));
expect(fromMap.programId, equals(step.programId));
expect(fromMap.stepNo, equals(step.stepNo));
expect(fromMap.position, equals(step.position));
expect(fromMap.name, equals(step.name));
expect(fromMap.mixTime, equals(step.mixTime));
expect(fromMap.magnetTime, equals(step.magnetTime));
expect(fromMap.volume, equals(step.volume));
});
test('copyWith should create modified copy', () {
final step = Step(
id: 1,
programId: 1,
stepNo: 1,
position: 'A1',
name: 'Mix',
mixTime: 60,
);
final copy = step.copyWith(stepNo: 2, mixTime: 120);
expect(copy.id, equals(step.id));
expect(copy.stepNo, equals(2));
expect(copy.mixTime, equals(120));
});
});
}

18
test/widget_test.dart Normal file
View File

@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:kuaishai2/main.dart';
void main() {
testWidgets('App launches without errors', (WidgetTester tester) async {
// 验证应用能正常启动,不抛出异常
await tester.pumpWidget(const ProviderScope(child: KuaishaiApp()));
// 等待异步加载完成
await tester.pumpAndSettle(const Duration(seconds: 3));
// 验证 Scaffold 存在(应用正常渲染)
expect(find.byType(Scaffold), findsWidgets);
});
}

BIN
web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

BIN
web/icons/Icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
web/icons/Icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

46
web/index.html Normal file
View File

@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="kuaishai2">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>kuaishai2</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!--
You can customize the "flutter_bootstrap.js" script.
This is useful to provide a custom configuration to the Flutter loader
or to give the user feedback during the initialization process.
For more details:
* https://docs.flutter.dev/platform-integration/web/initialization
-->
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

35
web/manifest.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "kuaishai2",
"short_name": "kuaishai2",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}