chore(project): 初始化项目基础配置文件
- 添加 CodeGraph、Android 和通用 gitignore 配置 - 创建项目元数据文件跟踪 Flutter 项目属性 - 添加 Codex AI 指导文档 AGENTS.md 说明项目架构 - 配置代码分析选项 analysis_options.yaml - 设置 Android 应用清单权限和 Kiosk 模式配置 - 实现中英文国际化支持 AppLocalizations - 配置 GoRouter 应用路由导航 - 创建明亮工业控制风格的主题配置 AppTheme
16
.codegraph/.gitignore
vendored
Normal 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
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"pid": 67540,
|
||||||
|
"version": "0.9.7",
|
||||||
|
"socketPath": "\\\\.\\pipe\\codegraph-7209b3ccb3579134",
|
||||||
|
"startedAt": 1780364708996
|
||||||
|
}
|
||||||
48
.gitignore
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
44
android/app/build.gradle.kts
Normal 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 = "../.."
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal 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>
|
||||||
61
android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal 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>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal 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>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal 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>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal 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>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal 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
@@ -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)
|
||||||
|
}
|
||||||
2
android/gradle.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||||
26
android/settings.gradle.kts
Normal 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
@@ -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:
|
||||||
417
docs/已确认-污水毒品快检一体机_功能需求文档.md
Normal 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盘自动导入程序配置文件
|
||||||
|
|
||||||
|
导入确认 导入前显示确认信息,用户确认后执行导入
|
||||||
|
-----------------------------------------------------------------------
|
||||||
|
|
||||||
|
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||||
|
|
||||||
|
--- 文档结束 ---
|
||||||
164
lib/core/database/database_service.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
366
lib/core/localization/app_localizations.dart
Normal 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;
|
||||||
|
}
|
||||||
48
lib/core/localization/locale_provider.dart
Normal 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';
|
||||||
|
});
|
||||||
45
lib/core/router/app_router.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
179
lib/core/theme/app_theme.dart
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
lib/features/device/models/device_state.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
lib/features/device/providers/run_state_provider.dart
Normal 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;
|
||||||
|
});
|
||||||
190
lib/features/device/services/mock_runner.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
lib/features/device/services/mock_runner_impl.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
54
lib/features/device/services/runner_interface.dart
Normal 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();
|
||||||
|
}
|
||||||
91
lib/features/device/services/serial_runner.dart
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
202
lib/features/home/pages/complete_page.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
176
lib/features/home/pages/home_page.dart
Normal 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()),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
222
lib/features/home/widgets/program_list.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
242
lib/features/home/widgets/run_status_monitor.dart
Normal 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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
365
lib/features/home/widgets/running_control_panel.dart
Normal 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 ?? '确认'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
181
lib/features/home/widgets/status_bar.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
199
lib/features/program_detail/pages/program_detail_page.dart
Normal 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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
161
lib/features/program_detail/providers/steps_provider.dart
Normal 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;
|
||||||
|
});
|
||||||
270
lib/features/program_detail/widgets/step_form.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
272
lib/features/program_detail/widgets/step_list.dart
Normal 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) {
|
||||||
|
// 调整 newIndex(ReorderableListView 的特殊行为)
|
||||||
|
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 ?? '确认'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
lib/features/programs/models/program.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/features/programs/models/step.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
509
lib/features/programs/pages/programs_page.dart
Normal 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 ?? '确认'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
192
lib/features/programs/providers/programs_provider.dart
Normal 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();
|
||||||
|
});
|
||||||
126
lib/features/programs/services/program_import_service.dart
Normal 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});
|
||||||
|
}
|
||||||
|
}
|
||||||
156
lib/features/programs/services/program_service.dart
Normal 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],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
188
lib/features/programs/widgets/program_form_dialog.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
382
lib/features/settings/pages/settings_page.dart
Normal 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('重新检测'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
lib/features/settings/services/settings_service.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
lib/features/settings/services/usb_detection_service.dart
Normal 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
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
lib/shared/services/toast_service.dart
Normal 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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
lib/shared/utils/constants.dart
Normal 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';
|
||||||
|
}
|
||||||
49
lib/shared/utils/responsive_layout.dart
Normal 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);
|
||||||
|
}
|
||||||
103
lib/shared/widgets/common_button.dart
Normal 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 }
|
||||||
47
lib/shared/widgets/common_card.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
lib/shared/widgets/common_dialog.dart
Normal 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/shared/widgets/empty_state_widget.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
lib/shared/widgets/status_indicator.dart
Normal 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
BIN
pencil/images/image-import.png
Normal file
|
After Width: | Height: | Size: 339 KiB |
12973
pencil/untitled.pen
Normal file
719
pubspec.lock
Normal 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
@@ -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
|
||||||
38
test/localization_test.dart
Normal 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
@@ -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
@@ -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
|
After Width: | Height: | Size: 917 B |
BIN
web/icons/Icon-192.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
web/icons/Icon-512.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
web/icons/Icon-maskable-192.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
web/icons/Icon-maskable-512.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
46
web/index.html
Normal 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
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||