feat: API 统一 + 设备详情补全 + H5 导航优化

- API 路由统一: 移除 /api/h5/,统一 /api/pms/ 入口
- 二进制改名: api-server -> pms-api(与服务名一致)
- nginx: 合并 /api/ + /pms/api/ 为 /api/pms/
- 新增 docs/API.md 接口文档
- 后端: CabinetDetail 增加 rssi/pow_fail/address/soc 等字段
- Redis 管道批量读取仓体实时数据(SOC/电压/电流/功率)
- DB: 所有 TIMESTAMP 列改为 DATETIME(修复类型不匹配)
- 用户管理: 新增创建时间/修改时间列,状态改用 Switch
- 角色管理/用户管理: 操作按钮改为图标
- PC 设备详情: 基本信息补全 RSSI/ICCID/电源/地址
- PC 卡片: 通道统计改用 Lucide 图标,颜色统一
- PC 二维码: canvas 渲染,含编号文字,支持右键下载
- H5: 按 uni-app 规范修正导航(navigateTo 替代 reLaunch)
- H5: 详情页加返回导航条
- H5: 扫码支持摄像头(jsQR)+ ?id= 参数
- H5: 底部 tabBar 在所有页面常显
- H5: 首页统计改为 6 项(设备/通道/空闲/充电/充满/故障)
- H5: 下拉刷新 / 上拉加载
- 产品首页: 添加业务联系
- 文档: 清理过时文件,补充缺失规范章节
This commit is contained in:
12451 2026-07-04 04:03:02 +08:00
parent 721df313ee
commit b553c4e519
1315 changed files with 171710 additions and 2617 deletions

672
AGENTS.md
View File

@ -142,21 +142,15 @@ python tools/db.py import data.sql
- **禁止在服务器上编译Rust** — 2核/3.5G会OOM,本地编译传二进制 - **禁止在服务器上编译Rust** — 2核/3.5G会OOM,本地编译传二进制
- **禁止使用socat fork模式** — 进程泄漏会耗尽内存,用nginx stream代理 - **禁止使用socat fork模式** — 进程泄漏会耗尽内存,用nginx stream代理
- **禁止提交密码/密钥到仓库** — 使用环境变量或本地配置文件(已gitignore)
### 编码规范
- 任务需求写到 `tasks/` 目录的md文件
- 通过Agent子代理包裹opencode执行,异步通知
- 任务要拆的尽量细
- 质量约束模板固定追加到每个任务文件
- opencode超时必须报告用户,不能fallback手动执行
- 审查用不同模型(不用开发同款)
### 编码任务下发 ### 编码任务下发
- 任务需求写到 `tasks/` 目录的md文件 - 任务需求写到 `tasks/` 目录的md文件
- 通过Agent子代理包裹opencode执行,异步通知 - 通过Agent子代理包裹opencode执行,异步通知
- 质量约束模板固定追加到每个任务文件 - 质量约束模板固定追加到每个任务文件
- opencode超时必须报告用户,不能fallback手动执行
- 审查用不同模型(不用开发同款)
### 质量约束模板 ### 质量约束模板
@ -176,6 +170,278 @@ python tools/db.py import data.sql
2. 跑起来看效果 2. 跑起来看效果
3. 抽查关键逻辑(异常处理、权限校验、协议解析) 3. 抽查关键逻辑(异常处理、权限校验、协议解析)
---
## Rust 编码规范
### 错误处理
- 使用 `thiserror` 定义业务错误枚举,`AppError` 统一包装
- 所有 `sqlx` / `redis` 调用必须 `.map_err(AppError::Database)?`
- 网络请求/外部IO 必须 `match` 或 `if let` 处理,禁止裸 `.unwrap()`
- 关键业务路径用 `tracing::error!` 记录错误上下文
### Async 模式
- 使用 `tokio` 运行时,数据库/Redis 操作用 `.await`
- 长时间任务用 `tokio::spawn` 异步处理,不阻塞主循环
- Redis 队列消费:`blpop` 阻塞读取 + `tokio::spawn` 并发处理
### 模块划分
```
src/
├── routes/ # HTTP 路由处理器(按业务拆分)
├── middleware/ # 中间件(认证、日志、CORS)
├── db/ # 数据库连接池、迁移
├── commands/ # 设备指令下发逻辑
├── nodes/ # 设备服务节点管理
├── workers/ # 后台 Worker(report/reply)
└── error.rs # 统一错误定义
```
### 命名约定
- 路由处理器:`动词 + 名词`(如 `get_cabinets`、`create_organization`)
- 数据库查询结构体:`XxxRow`(如 `CabinetBasicRow`)
- 请求体:`XxxBody` 或 `XxxQuery`(如 `CreateCabinetBody`)
- 常量:`UPPER_SNAKE_CASE`(如 `JWT_SECRET_ENV`)
---
## React 编码规范
### 组件规范
- 纯函数组件 + Hooks,禁止 Class 组件
- 组件文件与路由页面一一对应,放 `src/pages/` 下
- 复用组件放 `src/components/`,按功能命名
- Props 接口定义在组件文件顶部,用 `interface` 声明
### 状态管理
- 页面级状态用 `useState` / `useEffect`
- 全局状态用 `zustand`(如用户信息、权限)
- 服务端数据用 `@tanstack/react-query`,配置缓存策略
### 样式规范
- 使用 Tailwind CSS,禁止内联 style(除动态计算值)
- shadcn/ui 组件优先,不重复造轮子
- 表格列统一 `min-width` + `align="center"`,无 `fixed="right"`
- 分页:上下双分页,`page-sizes="[10,20,30,50]"`
### 图标映射
| 操作 | 图标 |
|------|------|
| 编辑 | Edit |
| 删除 | Delete |
| 查看 | ViewIcon |
| 搜索 | Search |
| 重置 | Refresh |
| 新增 | Plus(保留文字) |
---
## API 设计规范
### 响应格式
```json
{
"code": 0, // 0=成功,非0=失败
"data": { ... }, // 业务数据
"message": "ok" // 提示信息
}
```
### 错误码
| code | 含义 |
|------|------|
| 0 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未认证(token缺失/无效) |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
### 分页约定
```json
{
"code": 0,
"data": {
"list": [...],
"total": 100,
"page": 1,
"page_size": 20
}
}
```
### RESTful 约定
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/xxx | 列表查询 |
| GET | /api/xxx/:id | 详情 |
| POST | /api/xxx | 创建 |
| PUT | /api/xxx/:id | 更新 |
| DELETE | /api/xxx/:id | 删除 |
---
## Git 工作流
### 分支策略
- `main` — 正式环境,只接受 PR 合并
- `develop` — 开发分支,日常开发在此分支
- `feature/*` — 功能分支,从 develop 拉出,完成后合并回 develop
- `fix/*` — 修复分支,从 main 或 develop 拉出
### Commit 规范
格式:`<type>(<scope>): <subject>`
| type | 说明 |
|------|------|
| feat | 新功能 |
| fix | 修复 bug |
| docs | 文档变更 |
| style | 代码格式(不影响逻辑) |
| refactor | 重构(不新增功能/修复bug) |
| test | 测试相关 |
| chore | 构建/工具变更 |
示例:
- `feat(cabinets): 添加设备RSSI信号显示`
- `fix(h5): 修复登录后token未持久化问题`
- `docs(api): 更新充电记录接口文档`
---
## 测试标准
### 测试类型
| 类型 | 工具 | 要求 |
|------|------|------|
| 单元测试 | Rust `#[test]` / Vitest | 核心业务逻辑必须覆盖 |
| 集成测试 | API 请求验证 | 关键接口(登录/充电/开门)必须验证 |
| 静态校验 | `cargo check` / `tsc --noEmit` | 零报错零警告 |
### 测试覆盖率
- 核心业务逻辑(充电控制、指令下发、权限校验):≥80%
- 数据库查询函数:≥60%
- 工具函数:≥90%
### 测试命名
```
test_<被测函数>_<场景>_<期望结果>
test_verify_password_correct_returns_true
test_verify_password_wrong_returns_false
test_check_permission_admin_returns_ok
test_check_permission_user_returns_forbidden
```
---
## 安全规范
### 认证授权
- JWT Token 有效期:7天,过期需重新登录
- 密码存储:argon2 哈希,禁止明文或 MD5/SHA1
- RBAC 权限码:精确到按钮级别(如 `device:door:unlock`)
- 敏感操作(删除/禁用/开门)需二次确认
### 输入校验
- 所有 API 入参必须校验(长度、格式、范围)
- SQL 查询使用参数化查询,禁止字符串拼接
- IMEI 校验:必须15位数字
- 手机号校验:11位数字,正则 `^1[3-9]\d{9}$`
### 数据安全
- 生产数据库密码不入代码,使用环境变量
- Redis 密码同上
- 日志禁止输出密码/Token/密钥
- 敏感字段(密码、auth_str)在序列化时跳过
---
## 日志规范
### 日志级别
| 级别 | 用途 | 示例 |
|------|------|------|
| `ERROR` | 系统异常、业务错误 | 数据库连接失败、指令下发超时 |
| `WARN` | 潜在问题、可恢复异常 | 设备离线、重试成功 |
| `INFO` | 关键业务事件 | 设备登录、充电开始/结束、用户操作 |
| `DEBUG` | 调试信息 | API 请求参数、SQL 执行时间 |
### 日志格式
```rust
// 使用 tracing crate
tracing::info!(imei = %imei, board_id = board_id, "设备登录成功");
tracing::error!(err = %e, "数据库查询失败");
tracing::warn!(compartment_id = id, "仓体状态异常");
```
### 关键操作日志
以下操作必须记录:
| 操作 | 日志内容 |
|------|----------|
| 用户登录/登出 | 用户ID、手机号、IP |
| 设备指令下发 | IMEI、指令类型、仓体ID |
| 充电开始/结束 | 仓体ID、时长、电量 |
| 组织/项目变更 | 操作类型、ID、名称 |
| 权限变更 | 用户ID、权限码、操作人 |
| 数据删除 | 表名、ID、操作人 |
---
## 性能规范
### 数据库
- 查询必须走索引,禁止全表扫描(`EXPLAIN` 验证)
- 分页查询:避免深分页,用游标分页(`WHERE id > ?`)
- 批量操作:用 `INSERT ... VALUES (...), (...)` 而非逐条插入
- 连接池:MySQL 最大连接数 ≤ 50,Redis 连接数 ≤ 20
### Redis
- 缓存 key 规范:`{业务}:{标识}`(如 `device:{imei}`)
- TTL 设置:在线状态 300秒,缓存数据按业务设定
- 避免大 Key:单个 Hash ≤ 500 字段,List ≤ 1000 元素
- 批量操作:用 Pipeline 减少往返次数
### API 响应
- 列表接口响应时间 ≤ 500ms
- 详情接口响应时间 ≤ 200ms
- 批量操作接口响应时间 ≤ 1s
- 响应体大小:列表 ≤ 1MB,详情 ≤ 100KB
### 前端
- 首屏加载 ≤ 3s(3G网络)
- 交互响应 ≤ 100ms
- 列表滚动:虚拟滚动(≥100条数据)
- 图片:WebP 格式,懒加载
## 部署与维护 ## 部署与维护
### SSH连接 ### SSH连接
@ -190,19 +456,48 @@ echo y | plink -ssh root@10.8.0.252 -pw "Hbhyg731024@"
### 数据库连接 ### 数据库连接
所有数据库操作通过 ZeroTier 直接连接 `10.8.0.252`,无需 SSH 隧道到服务器。
**MySQL(通过 ZeroTier):**
```bash ```bash
# MySQL(通过ZeroTier代理) # 命令行
mysql -h 10.8.0.252 -u root -p mysql -h 10.8.0.252 -u root -p
# 密码:Hbhyg731024@ # 密码:Hbhyg731024@
# 测试环境库:pms_dev # 测试环境库:pms_dev
# 正式环境库:pms # 正式环境库:pms
# Redis(通过ZeroTier代理)
redis-cli -h 10.8.0.252 -a "Hbhyg731024@"
# 测试环境:db6
# 正式环境:db5
``` ```
```python
# Python(Agent 常用方式)
import pymysql
conn = pymysql.connect(host='10.8.0.252', user='root', password='Hbhyg731024@', database='pms') # 正式环境
conn = pymysql.connect(host='10.8.0.252', user='root', password='Hbhyg731024@', database='pms_dev') # 测试环境
```
**Redis(通过 ZeroTier):**
```bash
# 命令行
redis-cli -h 10.8.0.252 -a "Hbhyg731024@"
# 测试环境:select 6
# 正式环境:select 5
```
```python
# Python(Agent 常用方式)
import redis
r = redis.Redis(host='10.8.0.252', password='Hbhyg731024@', db=5) # 正式环境
r = redis.Redis(host='10.8.0.252', password='Hbhyg731024@', db=6) # 测试环境
```
**数据库工具 `tools/db.py`(默认连接测试库 pms_dev):**
```bash
python tools/db.py tables # 列出所有表
python tools/db.py count # 统计各表数据量
python tools/db.py query "SELECT * FROM users" # 查询
python tools/db.py exec "UPDATE users SET ..." # 写入
```
> 注意:`tools/db.py` 默认连的是 `pms_dev`(测试库),要操作正式库请用上面的 Python 直连方式。
### Nginx配置 ### Nginx配置
**正式环境** (`/etc/nginx/conf.d/app.anzhizhichong.com.conf`): **正式环境** (`/etc/nginx/conf.d/app.anzhizhichong.com.conf`):
@ -235,30 +530,21 @@ server {
client_max_body_size 500m; client_max_body_size 500m;
} }
location /pms/api/ { # PMS API 入口(微服务统一前缀 /api/{服务})
proxy_pass http://127.0.0.1:9000/; location /api/pms/ {
proxy_pass http://127.0.0.1:3000/api/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
# PMS管理前端 - 不带斜杠时重定向
location = /pms {
return 301 /pms/;
}
# PMS管理前端 # PMS管理前端
location /pms/ { location /pms/ {
alias /data/pms/web/; alias /data/pms/web/;
try_files $uri $uri/ /pms/index.html; try_files $uri $uri/ /pms/index.html;
} }
# H5用户端 - 不带斜杠时重定向
location = /h5 {
return 301 /h5/;
}
# H5用户端 # H5用户端
location /h5/ { location /h5/ {
alias /data/pms/h5/; alias /data/pms/h5/;
@ -287,8 +573,9 @@ server {
root /data/pms_dev; root /data/pms_dev;
index index.html; index index.html;
location /pms/api/ { # PMS API 入口(微服务统一前缀 /api/{服务})
proxy_pass http://127.0.0.1:3000/; location /api/pms/ {
proxy_pass http://127.0.0.1:3000/api/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -304,6 +591,12 @@ server {
alias /data/pms_dev/h5/; alias /data/pms_dev/h5/;
try_files $uri $uri/ /h5/index.html; try_files $uri $uri/ /h5/index.html;
} }
# H5用户端
location /h5/ {
alias /data/pms_dev/h5/;
try_files $uri $uri/ /h5/index.html;
}
} }
``` ```
@ -328,11 +621,11 @@ npm run build
**2. 传输到服务器** **2. 传输到服务器**
```bash ```bash
# 传输后端二进制(测试环境) # 传输后端二进制(测试环境)
scp software/api-server/target/release/api-server root@118.31.119.206:/data/pms_dev/api-server/ scp software/api-server/target/release/pms-api root@118.31.119.206:/data/pms_dev/api-server/
scp software/device-server/target/release/device-server root@118.31.119.206:/data/pms_dev/device-server/ scp software/device-server/target/release/device-server root@118.31.119.206:/data/pms_dev/device-server/
# 传输后端二进制(正式环境) # 传输后端二进制(正式环境)
scp software/api-server/target/release/api-server root@118.31.119.206:/data/pms/api-server/ scp software/api-server/target/release/pms-api root@118.31.119.206:/data/pms/api-server/
scp software/device-server/target/release/device-server root@118.31.119.206:/data/pms/device-server/ scp software/device-server/target/release/device-server root@118.31.119.206:/data/pms/device-server/
# 传输前端静态文件 # 传输前端静态文件
@ -486,259 +779,78 @@ device:{imei} # 设备级 Hash
## TCP通讯协议 ## TCP通讯协议
- 长连接,无需PING包 详见 [docs/API.md](docs/API.md#tcp-设备通讯协议) — 包含签名机制、指令清单、指令详情、充电结束原因。
- 空闲1分钟上报,充电15秒上报
- 超时5000ms
- JSON去空格回车省流量,LF处理粘包
- `dev_id`:IMEI(10进制),`sub_device_id`:485 ID
- `msg_id`:自增会话ID
- 全局通道号:`dev_id-sub_device_id-通道号`(如`123456789012345-01-6`)
### 签名机制 ## 数据单位规范
- 设备首次连接调 `auth_str` 接口获取8位安全码(仅一次) 详见 [docs/API.md](docs/API.md#数据单位规范) — 所有数值字段使用整数存储,禁止浮点类型。
- `sign = SHA256(dev_id + auth_str + timestamp).substring(56, 63)`
- 时间戳秒级,平台校验±60秒
### 指令清单 **现有浮点问题(需清理):**
| 动作 | 方向 | id格式 | 说明 | | 文件 | 位置 | 问题 | 处理 |
|------|------|--------|------| |------|------|------|------|
| `login` | 设备→平台 | 无 | 登录(含sign+timestamp) | | `src/routes/charge_records.rs` | L46-47, L77-78 | `Option<f64>` 读 DECIMAL | 需 CAST AS DOUBLE [有CAST] |
| `auth_str` | 设备→平台 | 无 | 获取安全码(仅首次) | | `src/routes/energy_stats.rs` | L106, L116 | `query_scalar::<_, f64>` 直接读 DECIMAL 无 CAST ❌ | 需加 CAST AS DOUBLE 或改用整数 |
| `status_post` | 设备→平台 | 无 | 定时上报(rssi+status字典) | | `src/routes/energy_stats.rs` | L43-72 | 多个 `Option<f64>` 字段 | 待迁移为 i64 |
| `pow_fail` | 设备→平台 | 无 | 12V电源掉电上报 | | `src/db/migrate.rs` | L76-77, L97 | `DECIMAL` 列定义 | 待改为 `BIGINT` |
| `bat_in` | 设备→平台 | `IMEI-仓控板-通道` | 电池插入(含BMS数据,字段待定) |
| `bat_out` | 设备→平台 | `IMEI-仓控板-通道` | 电池拔出 |
| `on` | **双向** | `IMEI-仓控板-通道` | 开始充电(平台下发/设备上报) |
| `off` | **双向** | `IMEI-仓控板-通道` | 停止供电(平台手动/设备自动) |
| `open` | 平台→设备 | `IMEI` | 开门 |
| `pow_on` | 平台→设备 | `IMEI` | 接触器合闸(恢复380V供电) |
| `pow_off` | 平台→设备 | `IMEI` | 接触器分闸(切断380V供电) |
| `reset` | 平台→设备 | `IMEI` 或 `IMEI-仓控板` | 重启整柜或仓控板 |
### 指令详情
#### login(设备→平台)
设备登录,验证签名。
```json
// 请求
{"act":"login","msg_id":1,"dev_id":"123456789012345","sign":"abc12345","timestamp":1719800000,"h_ver":"1.0.0","s_ver":"1.0.0"}
// 响应
{"suc":1,"msg_id":1,"cell":6}
```
**字段说明:**
- `sign` — 签名(SHA256(dev_id+auth_str+timestamp)后8位)
- `timestamp` — 秒级时间戳(平台校验±60秒)
- `h_ver` — 硬件版本
- `s_ver` — 软件版本
- `cell` — 仓控板数量(响应)
---
#### auth_str(设备→平台)
获取8位安全码,仅首次连接调用。
```json
// 请求
{"act":"auth_str","msg_id":2,"dev_id":"123456789012345","ccid":"89860000000000000000"}
// 响应
{"suc":1,"msg_id":2,"data":{"auth_str":"abcd1234"}}
```
**字段说明:**
- `ccid` — SIM卡ICCID号
- `auth_str` — 8位随机安全码(响应,仅返回一次)
---
#### status_post(设备→平台)
定时上报,空闲60秒/充电15秒。
```json
// 请求
{"act":"status_post","msg_id":10,"dev_id":"123456789012345","rssi":85,"status":{"123456789012345-01":"000100020003000400050006"}}
// 响应
{"suc":1,"msg_id":10}
```
**status字段说明:**
- key: `IMEI-仓控板ID`
- value: 6个通道状态拼接(每通道2字节=4个HEX字符)
---
#### pow_fail(设备→平台)
电源掉电上报。
```json
// 请求
{"act":"pow_fail","msg_id":15,"dev_id":"123456789012345","info":"0"}
// 响应
{"suc":1,"msg_id":15}
```
**字段说明:**
- `info` — 停电类型:`0`=直流停电(12V掉电),`1`=交流停电(380V掉电)
---
#### pow_on(平台→设备)
接触器合闸,恢复380V充电电源供电。
```json
// 请求
{"act":"pow_on","msg_id":16,"dev_id":"123456789012345"}
// 响应
{"suc":1,"msg_id":16}
```
---
#### pow_off(平台→设备)
接触器分闸,切断380V充电电源供电。
```json
// 请求
{"act":"pow_off","msg_id":16,"dev_id":"123456789012345"}
// 响应
{"suc":1,"msg_id":16}
```
---
#### bat_in(设备→平台)
电池插入事件,立即上报。
```json
// 请求
{"act":"bat_in","msg_id":20,"id":"123456789012345-01-6","bms":{"soc":45,"voltage":52.3}}
// 响应
{"suc":1,"msg_id":20}
```
**平台处理:**
1. 记录电池信息
2. 验证电池合规性
3. 自动下发 `on` 指令
---
#### bat_out(设备→平台)
电池拔出事件,立即上报。
```json
// 请求
{"act":"bat_out","msg_id":21,"id":"123456789012345-01-6"}
// 响应
{"suc":1,"msg_id":21}
```
---
#### on(双向)
**平台→设备:** 开始充电
```json
// 请求
{"act":"on","msg_id":30,"id":"123456789012345-01-6"}
// 响应
{"suc":1,"msg_id":30}
```
**设备→平台:** 充电开始确认(自动充电模式)
```json
// 上报
{"act":"on","msg_id":30,"id":"123456789012345-01-6"}
```
---
#### off(双向)
**平台→设备:** 手动停止充电
```json
// 请求
{"act":"off","msg_id":31,"id":"123456789012345-01-6"}
// 响应
{"suc":1,"msg_id":31}
```
**设备→平台:** 充满自动停止
```json
// 上报
{"act":"off","msg_id":31,"id":"123456789012345-01-6"}
```
---
#### open(平台→设备)
开门,只需IMEI。
```json
// 请求
{"act":"open","msg_id":40,"id":"123456789012345"}
// 响应
{"suc":1,"msg_id":40}
```
**错误码63 = 门锁故障。**
---
#### reset(平台→设备)
重启整柜或仓控板。
```json
// 重启整柜
{"act":"reset","msg_id":60,"id":"123456789012345"}
// 重启仓控板
{"act":"reset","msg_id":61,"id":"123456789012345-01"}
// 响应
{"suc":1,"msg_id":60}
```
### 充电结束原因(平台判断)
| 平台收到 | 判断原因 |
|---------|---------|
| `off` (设备→平台) | `normal` — 正常充满 |
| `off` (平台→设备) | `user_stop` — 用户手动停止 |
| `bat_out` | `normal` — 电池拔出 |
| `bat_in` (通道已有电池) | `forced` — 新电池插入,旧电池强制结束 |
| 超时未响应 | `timeout` — 通信超时 |
## H5用户端 ## H5用户端
- 路径:`/pms/{抽象ID}` ### 信号强度显示(RSSI)
- 功能:显示柜子每个通道状态,可开始/停止充电
- 无支付/计费功能 设备卡片右上角使用 Lucide 信号图标显示 RSSI 信号强度,PC 管理端和 H5 移动端共用同一套逻辑。
**RSSI 映射:**
| RSSI 范围 | 图标 (Lucide) | 信号格 | 颜色 |
|-----------|--------------|--------|------|
| ≥ 70 | signal | 5 格(含 M22) | #16a34a 绿色 |
| ≥ 55 | signal-high | 4 格(含 M17) | #2563eb 蓝色 |
| ≥ 40 | signal-medium | 3 格(含 M12) | #f59e0b 黄色 |
| ≥ 30 | signal-low | 2 格(含 M7) | #f97316 橙色 |
| < 30 或 null | signal-zero | 1 格(仅 M2) | #9ca3af 灰色(离线) |
**实现方式(移动端 H5):**
```vue
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" ...>
<path v-if="(rssi ?? -1) >= 30" d="M7 20v-4"/>
<path v-if="(rssi ?? -1) >= 40" d="M12 20v-8"/>
<path v-if="(rssi ?? -1) >= 55" d="M17 20V8"/>
<path v-if="(rssi ?? -1) >= 70" d="M22 4v16"/>
<path d="M2 20h.01"/>
</svg>
```
- `v-if` 按 RSSI 阈值动态显示信号格
- `<text>` 不支持 `display: flex`,统一用 `<view>` 包裹
**实现方式(PC 管理端):**
React 组件 `SignalIcon`,同上逻辑用条件渲染 `<path>`。
**后端 RSSI 获取:**
设备列表接口通过 Redis pipeline 批量 HGET 查询 `device:{imei}` 的 `rssi` 字段:
```
HSET device:{imei} online 1 rssi 85
```
**相关文件:**
- PC: `software/web/src/pages/devices/CabinetGrid.tsx` — `SignalIcon` 组件
- H5: `software/h5-uniapp/src/pages/home/index.vue` — signalColor 函数 + 模板
- H5 API: `software/api-server/src/routes/h5.rs` — `get_cabinets` 加 Redis pipeline
- 类型: `software/h5-uniapp/src/api/index.ts` — `CabinetItem.rssi`
### 近期 UI 改动
1. **设备卡片头部**:去掉"在线/离线"文字 badge,改为信号图标 + RSSI 数值
2. **通道统计**:图标+数字一行排列,图标用 `<svg>` 内联 Lucide(不加容器,flex 居中)
3. **右上角扫码**:scan-line 图标(Lucide),灰色 #6b7280,居中垂直对齐
### 开发约束(uni-app 特定)
1. **`<text>` 不支持 `display: flex`** — uni-app 的 `<text>` 组件是纯文本组件,不接受 flex 布局。需要 flex 的地方用 `<view>`
2. **SVG 图标居中**:`.stat-chip svg { display: block; flex-shrink: 0 }` 用 CSS 统一控制尺寸,比内联属性更可靠
3. **内联 SVG + v-if**:Vue 3 支持在 `<path>` 上用 `v-if` 条件渲染,适用于动态图标
4. **CSS 覆盖 SVG 尺寸**:用 `.xxx svg { width: Xpx; height: Xpx }` 统一控制图标大小,不在每个 `<svg>` 上写死

View File

@ -13,7 +13,7 @@ mkdir -p /data/pms/web
# 2. 复制文件 # 2. 复制文件
echo "2. 复制服务文件..." echo "2. 复制服务文件..."
cp software/api-server/target/release/api-server /data/pms/api-server/ cp software/api-server/target/release/pms-api /data/pms/api-server/
cp software/api-server/.env.production /data/pms/api-server/.env cp software/api-server/.env.production /data/pms/api-server/.env
cp software/device-server/target/release/device-server /data/pms/device-server/ cp software/device-server/target/release/device-server /data/pms/device-server/

View File

@ -13,7 +13,7 @@ mkdir -p /data/pms_dev/web
# 2. 复制文件 # 2. 复制文件
echo "2. 复制服务文件..." echo "2. 复制服务文件..."
cp software/api-server/target/release/api-server /data/pms_dev/api-server/ cp software/api-server/target/release/pms-api /data/pms_dev/api-server/
cp software/api-server/.env.test /data/pms_dev/api-server/.env cp software/api-server/.env.test /data/pms_dev/api-server/.env
cp software/device-server/target/release/device-server /data/pms_dev/device-server/ cp software/device-server/target/release/device-server /data/pms_dev/device-server/

View File

@ -6,7 +6,7 @@ After=network.target
Type=simple Type=simple
User=root User=root
WorkingDirectory=/data/pms_dev/api-server WorkingDirectory=/data/pms_dev/api-server
ExecStart=/data/pms_dev/api-server/api-server ExecStart=/data/pms_dev/api-server/pms-api
EnvironmentFile=/data/pms_dev/api-server/.env EnvironmentFile=/data/pms_dev/api-server/.env
Restart=always Restart=always
RestartSec=5 RestartSec=5

View File

@ -1,384 +0,0 @@
--- Page 1 ---
-1-
-
技
术
标
准
标准号:YG20240118-1
换电柜用锂电池充电器
1TN 标准通信协议
RS485
2024 年1 月
版
本:1.0
苏州翌工电源科技有限公司
研发中心
--- Page 2 ---
-2-
-
版本修改记录:
版本
修改内容
时间
修改人
S1.0
初步建立1TN RS485 协议内容。
2024-01-18
Eleven
--- Page 3 ---
-3-
-
一、
概述
换电柜1TN 多路充电器支持通过RS485 总线进行通信,通信协议为客户自定义协议,
采用Mod Bus-BTU 传输模式,波特率9600。
Mod Bus-BTU 传输模式在一根通讯线上采用主从应答方式的通讯连接方式。由主机发
起,主机的信号寻址到一台唯一地址的终端设备(从机),然后终端设备发出的应答信号以
相反的方向传输给主机,即半双工的工作模式。该协议只允许在主机和终端设备之间通讯,
而不允许独立的终端设备之间的数据交换,这样各终端设备不会在它们初始化时占据通讯线
路,而仅限于响应到达本机的查询信号。
1.1 通讯格式
信息传输为异步方式,并以字节为单位,在主机和从机之间传递的通讯信息是11 位的
字格式。包含1 个起始位(0),8 个数据位和1 个停止位。
信息帧格式:
地址
通道
功能码
长度
数据区
校验码
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
-
0x03
0x00
-
0x0X
0x00
-
0xFF
0x01-0xFF
DATA
有效字节数
读写有效的数据内
容,采用BigEndian
网络字节序
SUM =(ADDR +
CHx+CMD+LEN
+DATA)按字节
相加,取低8 位
1 个字节
1 个字节
1 个字节
1 个字节
LEN 个字节
1 个字节
1.2 通讯信息过程
信息发送至从机时,从机接收通讯命令,如果SUM 校验无误则执行相应的操作,然后
把执行结果(数据)返送给主机。返回的信息中包括Header、CMD、ATS、执行后的数据以
及SUM 校验码和END。如果CRC 校验出错就不返回任何信息,则分析返回相应的错误
信息。
1.2.1
地址码(ADDR)
对应电源双拨码开关,01 代表0x01,10 代表0x02,11 代表0x03。
对应电源双拨码开关,001 代表0x01,010 代表0x02,011 代表0x03,100 代表0x04,
101 代表0x05,110 代表0x06,111 代表0x07。
1.2.2
通道码(CHx)
对应各个通道识别码。
通道
Bit1-7
Bit0
CHx Byte 解析
通道
Bit1-7
Bit0
CHx Byte 解析
CH1
00
0/1
00:CH1 主通道
01:CH1 副通道
CH4
03
0/1
06:CH4 主通道
07:CH4 副通道
--- Page 4 ---
-4-
-
CH2
01
0/1
02:CH2 主通道
03:CH2 副通道
CH5
04
0/1
08:CH5 主通道
09:CH5 副通道
CH3
02
0/1
04:CH3 主通道
05:CH3 副通道
CH6
05
0/1
0A:CH6 主通道
0B:CH6 副通道
1.2.3
功能码(CMD)
每个通讯信息帧的第4 个字节。主机发送,通过功能码告诉从机应执行什么动作。从
机响应,从机返回的功能码与从主机发送来的功能码一样,表明从机已响应主机并已执行了
相关的操作。本协议支持以下功能码:
序号
命令
CMD1
LEN
备注
1
设置通道
输出电压/电流
0x02
0x04
2
设置通道
开关机
0x03
0x01
3
通道组合指令
开关机/调压/调流
0x04
0x05
4
电源通道信息查询
0x05
0x01
5
供应商电源型号/版本
号/序列号查询
0x12
0x01
10
升级状态查询
0x18
NC
详见升级
协议
11
OTA 远程升级
0x19
NC
详见升级
协议
1.2.4
数据区长度(LEN)
见上表
1.2.5
数据区(DATA)
数据区随功能码不同而异。这些数据可以是数值、参考地址等。对于不同的从机地址和
数据信息都不相同,应给出通讯信息表,详见例程。
1.2.6
校验区(SUM)
校验区采用和校验方式,从地址码一直到数据域最后一位,求和并取低八位作为校验码。
--- Page 5 ---
-5-
-
指令解析:
1. 设置1#电源CH1主通道输出电压电流
主控发送给电源
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
0x00
0x02
0x04
0x02 58 00 64
0xC5
DATA:0x0258代表60.0V, 0x0064代表10.0A.
电源回复给主控
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
0x00
0x02
0x01
0x01
0x05
回复:0x01代表设置输出电压电流成功
2. 设置1#电源CH1主通道输出开关机
主控发送给电源
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
0x00
0x03
0x01
0x01
0x06
开关机:0x01代表开机,0x00代表关机
电源回复给主控
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
0x00
0x03
0x01
0x01
0x06
回复:0x01代表当前设置开关机成功
3. 设置1#电源CH1主通道组合控制命令
主控发送给电源
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
0x00
0x04
0x05
0x01 02 58 00 64
0xC9
DATA: 0x01代表开机状态,0258代表设置电压60.0V,0064代表设置电流10.0A
电源回复给主控
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
0x00
0x04
0x07
0x02 58 00 64 01 01 1A
0xE6
回复DATA: 0258代表当前输出电压60.0V,0064代表当前输出电流10.0A,01 01代表故障码,1A代表环温26C.
4. 读取1#电源CH1主通道运行信息
主控发送给电源
ADDR
CHx
CMD
LEN
DATA
SUM
--- Page 6 ---
-6-
-
0x01
0x00
0x05
0x01
0x00
0x07
电源回复给主控
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
0x00
0x05
0x0D
02 58 00 64 01 01 1A 55 F0 03 E8 08 98
0xBD
回复DATA: 0x0258代表设置电压60.0V,0064代表设置电流10.0A,0x01 01代表故障码,1A代表环温26C,55F0输
入电压220.00V,03E8输入电流10.00A,0898输入功率2200W
故障码解析:
故障字节1
Bit0
输入欠压
故障字节1
Bit0
硬件故障
Bit1
输入过压
Bit1
PFC故障
Bit2
NC
Bit2
风扇故障
Bit3
输出过压
Bit3
电池反接故障
Bit4
输出欠压
Bit4
NC
Bit5
输出短路
Bit5
NC
Bit6
输出过流
Bit6
NC
Bit7
过温
Bit7
NC
5. 读取1#电源供应商电源型号/版本号/序列号查询
主控发送给电源
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
0x00
0x12
0x01
0x00
0x14
电源回复给主控
ADDR
CHx
CMD
LEN
DATA
SUM
0x01
0x00
0x12
0x0F
‘YGCP1TN-A’ 01 01 18 02 17 00 01
+
回复DATA: ASCII码’YG’代表厂商,CP1TN-A代表电源型号,S1.0V1.0代表硬件版本1软件版本1, 18 02 17
代表24年2月23日,00 01代表序列号00001.

351
docs/API.md Normal file
View File

@ -0,0 +1,351 @@
# API 接口文档
> 充电柜矩阵系统(PMS)接口规范。PC 管理端与 H5 用户端共用同一套 API。
## 基础信息
- **测试环境**:`https://test.anzhizhichong.com/pms/api/`
- **正式环境**:`https://app.anzhizhichong.com/pms/api/`
- **认证方式**:Bearer Token(JWT)
- **响应格式**:`{ "code": 0, "data": ..., "message": "ok" }`
## 统一端点清单
| 方法 | 路径 | 说明 | 权限 |
|------|------|------|------|
| POST | `/api/auth/login` | 登录 | 免认证 |
| GET | `/api/auth/logout` | 登出 | 需认证 |
| GET | `/api/auth/me` | 当前用户信息 | 需认证 |
| GET | `/api/dashboard` | 仪表盘数据 | 需认证 |
| GET | `/api/cabinets` | 设备列表(含 RSSI) | `device:view` |
| POST | `/api/cabinets` | 批量创建柜子 | `device:create` |
| GET | `/api/cabinets/:id` | 设备详情(含仓板+仓体) | `device:view` |
| PUT | `/api/cabinets/:id` | 更新柜子 | `device:edit` |
| DELETE | `/api/cabinets/:id` | 删除柜子 | `device:delete` |
| GET | `/api/cabinets/abstract/:abstract_id` | 扫码查设备(免认证) | 免认证 |
| GET | `/api/cabinets/scan/:abstract_id` | 扫码验证(需认证) | 需认证 |
| GET | `/api/cabinets/filter` | 设备列表(管理端) | 需认证 |
| GET | `/api/cabinets/realtime-status` | 设备实时状态 | 需认证 |
| GET | `/api/compartments/:id` | 仓体/通道详情 | 需认证 |
| POST | `/api/charge/start` | 开始充电 | `charge:start` |
| POST | `/api/charge/stop` | 停止充电 | `charge:stop` |
| POST | `/api/door/open` | 开门 | `device:door:unlock` |
| GET | `/api/organizations` | 组织列表 | `org:view` |
| POST | `/api/organizations` | 创建组织 | `org:create` |
| GET | `/api/organizations/:org_id/projects` | 项目列表 | 需认证 |
| GET | `/api/charge-records` | 充电记录 | `charge_record:view` |
| GET | `/api/energy-stats/summary` | 能耗统计 | 需认证 |
---
## 认证接口
### POST /api/auth/login
**请求:**
```json
{
"phone": "13800138000",
"password": "admin123"
}
```
**响应:**
```json
{
"code": 0,
"data": {
"token": "eyJ0eXAi...",
"user": {
"id": 1,
"name": "管理员",
"phone": "13800138000",
"role_level": 2,
"organization_id": null,
"permissions": ["device:view", "device:create", "..."]
}
},
"message": "登录成功"
}
```
**角色等级:** 1=普通用户,2=超级管理员
---
## H5 专用接口
### GET /api/dashboard
H5 首页仪表盘数据。支持 `org_id` 参数按组织过滤。
**响应:**
```json
{
"code": 0,
"data": {
"online_cabinets": 68,
"charging_count": 214,
"idle_channels": 722,
"fault_channels": 0,
"full_count": 0,
"today_charge_count": 0,
"today_energy_kwh": 0,
"today_charge_hours": 0,
"alerts": [],
"projects": [
{ "id": 1, "name": "无锡太湖新城换电站", "cabinet_count": 2, "charging": 16, "idle": 56, "fault": 0 }
]
}
}
```
### GET /api/cabinets
设备列表,支持 `project_id` 和 `org_id` 过滤。响应包含实时 RSSI。
**响应字段:**
```json
{
"id": 1,
"abstract_id": "10-00000001",
"name": "太湖新城换电柜A",
"status": 1,
"project_id": 1,
"rssi": 85,
"channel_count": 36,
"idle_count": 30,
"charging_count": 6,
"full_count": 0,
"fault_count": 0
}
```
### GET /api/cabinets/abstract/:abstract_id
扫码入口,免认证。通过二维码中的抽象ID查找设备。
**响应:**
```json
{
"code": 0,
"data": {
"id": 1,
"abstract_id": "10-00000001",
"imei": "864720562557021",
"iccid": "8986012520983901841",
"name": "太湖新城换电柜A",
"status": 1,
"cabin_boards": [
{
"id": 1,
"board_id": 1,
"status": 0,
"compartments": [
{ "id": 1, "channel_id": 1, "status": 0 }
]
}
]
}
}
```
### GET /api/compartments/:id
仓体/通道详情,含实时 BMS 数据和充电记录。
**响应字段:**
- `voltage` — mV(整数)
- `current` — mA(整数)
- `power` — mW(整数)
- `energy` — Wh(整数)
- `soc` — %(0-100)
- `soh` — %(0-100)
- `cell_voltages` — mV 数组
### POST /api/charge/start
**请求:** `{ "compartment_id": 123 }`
### POST /api/charge/stop
**请求:** `{ "compartment_id": 123 }`
### POST /api/door/open
**请求:** `{ "compartment_id": 123 }`
### GET /api/cabinets/scan/:abstract_id
扫码验证,需认证。验证当前用户是否有权访问该设备。
---
## 数据单位规范
**核心原则:所有数值字段使用整数存储,禁止浮点类型 (f32/f64/DECIMAL/FLOAT)**
| 字段 | 类型 | 单位 | 说明 |
|------|------|------|------|
| `energy` | i64 | **Wh** | 充电量,不存小数 |
| `voltage` | i64 | **mV** | 电池组总电压 |
| `current` | i64 | **mA** | 电流,正=充电 负=放电 |
| `power` | i64 | **mW** | 功率 |
| `soc` / `soh` | i64 | **%** | 电量百分比 / 健康度 |
| `max_temperature` | i64 | **0.1°C** | 25.3°C → `253` |
| `rssi` | i64 | **dBm** | 信号强度 |
**温度转换:** 前端显示时除以10加小数点(25.3°C → `253`)
---
## TCP 设备通讯协议
### 通信规则
- TCP 长连接,无需 PING 包
- 空闲 1 分钟上报,充电 15 秒上报
- 超时 5000ms
- JSON 去空格回车省流量,LF 处理粘包
- `dev_id`:IMEI(10进制),`sub_device_id`:485 ID
- `msg_id`:自增会话 ID
- 全局通道号:`dev_id-sub_device_id-通道号`(如 `123456789012345-01-6`)
### 签名机制
- 设备首次连接调 `auth_str` 接口获取8位安全码(仅一次)
- `sign = SHA256(dev_id + auth_str + timestamp).substring(56, 63)`
- 时间戳秒级,平台校验±60秒
### 指令清单
| 动作 | 方向 | id格式 | 说明 |
|------|------|--------|------|
| `login` | 设备→平台 | 无 | 登录(含sign+timestamp) |
| `auth_str` | 设备→平台 | 无 | 获取安全码(仅首次) |
| `status_post` | 设备→平台 | 无 | 定时上报(rssi+status字典) |
| `pow_fail` | 设备→平台 | 无 | 电源掉电上报 |
| `bat_in` | 设备→平台 | `IMEI-仓控板-通道` | 电池插入 |
| `bat_out` | 设备→平台 | `IMEI-仓控板-通道` | 电池拔出 |
| `on` | **双向** | `IMEI-仓控板-通道` | 开始充电 |
| `off` | **双向** | `IMEI-仓控板-通道` | 停止供电 |
| `open` | 平台→设备 | `IMEI` | 开门 |
| `pow_on` | 平台→设备 | `IMEI` | 接触器合闸 |
| `pow_off` | 平台→设备 | `IMEI` | 接触器分闸 |
| `reset` | 平台→设备 | `IMEI` 或 `IMEI-仓控板` | 重启整柜或仓控板 |
### 指令详情
#### login(设备→平台)
```json
{"act":"login","msg_id":1,"dev_id":"123456789012345","sign":"abc12345","timestamp":1719800000,"h_ver":"1.0.0","s_ver":"1.0.0"}
// 响应:{"suc":1,"msg_id":1,"cell":6}
```
#### auth_str(设备→平台)
```json
{"act":"auth_str","msg_id":2,"dev_id":"123456789012345","ccid":"89860000000000000000"}
// 响应:{"suc":1,"msg_id":2,"data":{"auth_str":"abcd1234"}}
```
#### status_post(设备→平台)
```json
{"act":"status_post","msg_id":10,"dev_id":"123456789012345","rssi":85,
"status":{"123456789012345-01":"000100020003000400050006"}}
// 响应:{"suc":1,"msg_id":10}
```
#### bat_in / bat_out(设备→平台)
```json
{"act":"bat_in","msg_id":20,"id":"123456789012345-01-6","bms":{"soc":45,"voltage":52.3}}
{"act":"bat_out","msg_id":21,"id":"123456789012345-01-6"}
```
#### on / off(双向)
```json
// 平台→设备:开始充电
{"act":"on","msg_id":30,"id":"123456789012345-01-6"}
// 设备→平台:充电开始确认
{"act":"on","msg_id":30,"id":"123456789012345-01-6"}
```
#### open(平台→设备)
```json
{"act":"open","msg_id":40,"id":"123456789012345"}
// 错误码63 = 门锁故障
```
#### reset(平台→设备)
```json
// 重启整柜
{"act":"reset","msg_id":60,"id":"123456789012345"}
// 重启仓控板
{"act":"reset","msg_id":61,"id":"123456789012345-01"}
```
### 充电结束原因
| 平台收到 | 判断原因 |
|---------|---------|
| `off` (设备→平台) | `normal` — 正常充满 |
| `off` (平台→设备) | `user_stop` — 用户手动停止 |
| `bat_out` | `normal` — 电池拔出 |
| `bat_in` (通道已有电池) | `forced` — 新电池插入强制结束 |
| 超时未响应 | `timeout` — 通信超时 |
---
## Redis 实时状态存储
### 层级结构
```
device:{imei} # 设备级 Hash
├─ online = "1" # 在线标记(TTL 300秒)
├─ rssi = "85" # 信号强度
├─ pow_fail_dc = "0/1" # 直流停电
└─ device:{imei}:board:{board_idx} # 板级 Hash
├─ status_hex = "00010002..." # 原始hex状态
└─ device:{imei}:board:{board_idx}:ch:{0-5} # 通道级 Hash
├─ on = "0/1" # 充电中
├─ full = "0/1" # 充满
├─ fault = "0/1" # 故障
└─ hex = "00010002" # 原始hex
```
### BMS 字段
| 字段 | 类型 | 单位 | 说明 |
|------|------|------|------|
| `soc` | i64 | % | 电池电量百分比 |
| `soh` | i64 | % | 电池健康度百分比 |
| `voltage` | i64 | mV | 电池组总电压 |
| `current` | i64 | mA | 电流 |
| `power` | i64 | mW | 功率 |
| `energy` | i64 | Wh | 累计充电量 |
| `cell_voltages` | i64[] | mV | 单体电芯电压数组 |
---
## 权限码列表
| 权限码 | 说明 |
|--------|------|
| `device:view` | 查看设备 |
| `device:create` | 创建设备 |
| `device:edit` | 编辑设备 |
| `device:delete` | 删除设备 |
| `device:door:unlock` | 开门 |
| `charge:start` | 开始充电 |
| `charge:stop` | 停止充电 |
| `charge_record:view` | 查看充电记录 |
| `org:view` | 查看组织 |
| `org:create` | 创建组织 |
| `org:edit` | 编辑组织 |

View File

@ -1,272 +0,0 @@
// 开发合同.docx 生成脚本
// 标准 Word 排版:A4、宋体、清晰的层级
const {
Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
AlignmentType, HeadingLevel, BorderStyle, WidthType, ShadingType,
PageNumber, Footer, Header, LevelFormat
} = require('docx');
const fs = require('fs');
// 边框样式
const noBorder = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" };
const tableBorders = {
top: { style: BorderStyle.SINGLE, size: 6, color: "000000" },
bottom: { style: BorderStyle.SINGLE, size: 6, color: "000000" },
left: { style: BorderStyle.SINGLE, size: 6, color: "000000" },
right: { style: BorderStyle.SINGLE, size: 6, color: "000000" },
insideHorizontal: { style: BorderStyle.SINGLE, size: 4, color: "000000" },
insideVertical: { style: BorderStyle.SINGLE, size: 4, color: "000000" },
};
// 标题段落(一级)
function h1(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_1,
spacing: { before: 360, after: 200 },
children: [new TextRun({ text, bold: true, size: 32, font: "SimSun" })],
});
}
// 标题段落(二级)
function h2(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_2,
spacing: { before: 240, after: 120 },
children: [new TextRun({ text, bold: true, size: 28, font: "SimSun" })],
});
}
// 正文段落
function p(text, opts = {}) {
return new Paragraph({
spacing: { line: 360, before: 80, after: 80, lineRule: "auto" },
alignment: opts.align || AlignmentType.JUSTIFIED,
indent: opts.indent ? { firstLine: opts.indent } : undefined,
children: [new TextRun({ text, size: 24, font: "SimSun" })],
});
}
// 粗体段落
function pBold(text, opts = {}) {
return new Paragraph({
spacing: { line: 360, before: 80, after: 80, lineRule: "auto" },
alignment: opts.align || AlignmentType.LEFT,
children: [new TextRun({ text, bold: true, size: 24, font: "SimSun" })],
});
}
// 空行
function empty() {
return new Paragraph({ children: [new TextRun({ text: "" })] });
}
// 居中粗体大标题
function bigTitle(text) {
return new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { before: 200, after: 400 },
children: [new TextRun({ text, bold: true, size: 44, font: "SimHei" })],
});
}
// 表格单元格
function cell(text, opts = {}) {
return new TableCell({
borders: tableBorders,
width: { size: opts.width || 2000, type: WidthType.DXA },
shading: opts.header ? { fill: "E7E6E6", type: ShadingType.CLEAR, color: "auto" } : undefined,
margins: { top: 100, bottom: 100, left: 120, right: 120 },
children: [new Paragraph({
alignment: opts.align || AlignmentType.LEFT,
spacing: { line: 300, before: 0, after: 0 },
children: [new TextRun({
text: String(text),
bold: !!opts.header,
size: 24,
font: "SimSun",
})],
})],
});
}
// 创建表格
function makeTable(rows, colWidths) {
return new Table({
width: { size: colWidths.reduce((a, b) => a + b, 0), type: WidthType.DXA },
columnWidths: colWidths,
rows: rows.map((row, i) => new TableRow({
children: row.map((text, j) => cell(text, { width: colWidths[j], header: i === 0 })),
})),
});
}
// ==================== 内容 ====================
const children = [];
// 大标题
children.push(bigTitle("36仓体直流电瓶车充电柜"));
children.push(bigTitle("开发合同"));
// 合同信息
children.push(p("合同编号:_______________", { align: AlignmentType.LEFT }));
children.push(p("签订地点:_______________", { align: AlignmentType.LEFT }));
children.push(p("签订日期:______年______月______日", { align: AlignmentType.LEFT }));
children.push(empty());
// 第一条
children.push(h1("第一条 合同双方"));
children.push(pBold("甲方(委托方):"));
children.push(p("公司名称:____________________________________"));
children.push(p("法定代表人:________________"));
children.push(p("地址:____________________________________"));
children.push(p("联系人:________________ 联系电话:________________"));
children.push(empty());
children.push(pBold("乙方(开发方):"));
children.push(p("公司名称:____________________________________"));
children.push(p("法定代表人:________________"));
children.push(p("地址:____________________________________"));
children.push(p("联系人:________________ 联系电话:________________"));
// 第二条
children.push(h1("第二条 项目概述"));
children.push(p("1. 项目名称:36仓体直流电瓶车充电柜开发项目。"));
children.push(p("2. 项目目标:开发充电柜配套的柜控主板、仓控主板、嵌入式固件、后台管理系统及微信小程序。"));
children.push(p("3. 开发周期:合同生效后12周。"));
children.push(p("4. 交付成果:详见第五条及附件。"));
// 第三条
children.push(h1("第三条 开发内容"));
children.push(makeTable(
[
["序号", "开发内容", "详见附件"],
["1", "柜控主板硬件设计开发", "附件一"],
["2", "仓控主板硬件设计开发", "附件一"],
["3", "嵌入式固件开发(柜控板+仓控板)", "附件一"],
["4", "后台管理系统开发", "附件二"],
["5", "微信小程序开发", "附件二"],
["6", "通讯协议栈开发实现", "附件三"],
],
[1000, 5360, 3000]
));
children.push(empty());
children.push(p("乙方负责主板设计、元器件采购、焊接调试及联调支持。柜体、电源及外部器件由甲方提供。本合同不交付任何源代码。"));
// 第四条
children.push(h1("第四条 开发阶段"));
children.push(makeTable(
[
["阶段", "时间", "交付物"],
["M1 方案定稿", "第1-2周", "原理图、PCB、BOM、协议文档"],
["M2 开发完成", "第3-6周", "硬件样板、固件、后台、小程序"],
["M3 联调完成", "第7-9周", "集成测试报告"],
["M4 验收交付", "第10-12周", "完整成品、技术文档"],
],
[2500, 2000, 4860]
));
children.push(empty());
children.push(p("付款方式:______________________________________________________________________"));
// 第五条
children.push(h1("第五条 交付与验收"));
children.push(p("交付物清单及验收标准详见附件三。整机验收时乙方到甲方现场完成联调,验收所需柜体及电源由甲方提供。"));
// 第六条
children.push(h1("第六条 知识产权"));
children.push(p("1. 乙方在本合同履行过程中产生的全部技术成果(包括但不限于源代码、设计图纸、技术文档等)的知识产权归甲乙双方共同所有。"));
children.push(p("2. 任何一方均有权在自身业务范围内自由使用上述技术成果,无需征得另一方同意或另行支付费用。"));
// 第七条
children.push(h1("第七条 保密条款"));
children.push(p("1. 双方对合同履行过程中知悉的对方商业秘密、技术秘密负有保密义务。"));
children.push(p("2. 保密期限自合同生效之日起3年。"));
children.push(p("3. 未经对方书面同意,不得向第三方披露任何与本合同相关的信息。"));
// 第八条
children.push(h1("第八条 质保与维护"));
children.push(p("1. 硬件质保期自验收通过之日起12个月,质保期内免费维修或更换。"));
children.push(p("2. 软件质保期自验收通过之日起6个月,质保期内免费修复Bug。"));
children.push(p("3. 质保期外的维护服务费用由双方另行协商。"));
// 第九条
children.push(h1("第九条 违约责任"));
children.push(p("1. 甲方逾期付款的,每逾期一日按应付未付金额的千分之三支付违约金。"));
children.push(p("2. 乙方逾期交付的,每逾期一日按合同总金额的千分之三支付违约金。"));
children.push(p("3. 任何一方违反知识产权或保密条款的,应赔偿对方全部损失。"));
// 第十条
children.push(h1("第十条 合同总金额"));
children.push(p("合同总金额:人民币____________元(大写:__________________________)。"));
// 第十一条
children.push(h1("第十一条 争议解决"));
children.push(p("本合同履行过程中发生争议的,双方应协商解决;协商不成的,提交合同签订地人民法院诉讼解决。"));
// 第十二条
children.push(h1("第十二条 其他"));
children.push(p("1. 本合同一式两份,甲乙双方各执一份,具有同等法律效力。"));
children.push(p("2. 本合同附件为合同的组成部分,与本合同具有同等法律效力。"));
children.push(p("3. 未尽事宜,双方可另行签订补充协议。"));
// 签章区
children.push(empty());
children.push(empty());
children.push(pBold("甲方(盖章):__________________________"));
children.push(p("授权代表签字:________________________"));
children.push(p("日期:______年______月______日"));
children.push(empty());
children.push(pBold("乙方(盖章):__________________________"));
children.push(p("授权代表签字:________________________"));
children.push(p("日期:______年______月______日"));
// ==================== 生成文档 ====================
const doc = new Document({
creator: "CodeBuddy",
title: "36仓体直流电瓶车充电柜开发合同",
styles: {
default: {
document: { run: { font: "SimSun", size: 24 } },
},
paragraphStyles: [
{
id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 32, bold: true, font: "SimHei" },
paragraph: { spacing: { before: 360, after: 200 }, outlineLevel: 0 },
},
{
id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, font: "SimHei" },
paragraph: { spacing: { before: 240, after: 120 }, outlineLevel: 1 },
},
],
},
sections: [{
properties: {
page: {
size: { width: 11906, height: 16838 }, // A4
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
},
},
footers: {
default: new Footer({
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun({ text: "第 ", size: 20, font: "SimSun" }),
new TextRun({ children: [PageNumber.CURRENT], size: 20, font: "SimSun" }),
new TextRun({ text: " 页 / 共 ", size: 20, font: "SimSun" }),
new TextRun({ children: [PageNumber.TOTAL_PAGES], size: 20, font: "SimSun" }),
new TextRun({ text: " 页", size: 20, font: "SimSun" }),
],
})],
}),
},
children,
}],
});
Packer.toBuffer(doc).then(buffer => {
fs.writeFileSync("开发合同.docx", buffer);
console.log("OK: 开发合同.docx 已生成 (" + buffer.length + " bytes)");
});

View File

@ -1,22 +0,0 @@
const marked = require('marked');
const fs = require('fs');
const files = ['开发合同.md', '附件一-硬件规格.md', '附件二-软件规格.md', '附件三-交付与验收标准.md'];
const styles = `body{font-family:"Microsoft YaHei",sans-serif;max-width:900px;margin:40px auto;padding:20px;line-height:1.8;color:#333}
h1{text-align:center;font-size:22px;border-bottom:2px solid #333;padding-bottom:10px}
h2{font-size:18px;margin-top:30px;border-left:4px solid #333;padding-left:10px}
h3{font-size:16px;margin-top:20px}h4{font-size:15px;margin-top:15px}
table{width:100%;border-collapse:collapse;margin:15px 0;font-size:14px}
th,td{border:1px solid #999;padding:8px;text-align:left}th{background:#f0f0f0}
p,li{font-size:14px}hr{border:none;border-top:1px solid #ccc;margin:30px 0}
pre{background:#f5f5f5;padding:15px;border-radius:4px;overflow-x:auto;font-size:13px}
code{font-family:Consolas,monospace}
.blank{color:#999;border-bottom:1px solid #999;display:inline-block;min-width:150px}
.sign{margin-top:60px}.sign div{display:inline-block;width:45%;vertical-align:top}`;
files.forEach(f => {
const md = fs.readFileSync(f, 'utf8');
const html = `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>${f.replace('.md','')}</title><style>${styles}</style></head><body>${marked.parse(md)}</body></html>`;
fs.writeFileSync(f.replace('.md', '.html'), html);
console.log(`OK: ${f} -> ${f.replace('.md', '.html')}`);
});

548
docs/package-lock.json generated
View File

@ -1,548 +0,0 @@
{
"name": "docs",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"docx": "^9.7.1",
"marked": "^18.0.5",
"puppeteer": "^25.1.0"
}
},
"node_modules/@puppeteer/browsers": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/@puppeteer/browsers/-/browsers-3.0.4.tgz",
"integrity": "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA==",
"license": "Apache-2.0",
"dependencies": {
"modern-tar": "^0.7.6",
"yargs": "^17.7.2"
},
"bin": {
"browsers": "lib/main-cli.js"
},
"engines": {
"node": ">=22.12.0"
},
"peerDependencies": {
"proxy-agent": ">=8.0.1"
},
"peerDependenciesMeta": {
"proxy-agent": {
"optional": true
}
}
},
"node_modules/@types/node": {
"version": "25.9.4",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-25.9.4.tgz",
"integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==",
"license": "MIT",
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/chromium-bidi": {
"version": "16.0.1",
"resolved": "https://registry.npmmirror.com/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
"integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==",
"license": "Apache-2.0",
"dependencies": {
"mitt": "^3.0.1",
"zod": "^3.24.1"
},
"engines": {
"node": ">=20.19.0 <22.0.0 || >=22.12.0"
},
"peerDependencies": {
"devtools-protocol": "*"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/devtools-protocol": {
"version": "0.0.1624250",
"resolved": "https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1624250.tgz",
"integrity": "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA==",
"license": "BSD-3-Clause"
},
"node_modules/docx": {
"version": "9.7.1",
"resolved": "https://registry.npmmirror.com/docx/-/docx-9.7.1.tgz",
"integrity": "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==",
"license": "MIT",
"dependencies": {
"@types/node": "^25.2.3",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.1.3",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
"engines": {
"node": ">=10"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmmirror.com/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/marked": {
"version": "18.0.5",
"resolved": "https://registry.npmmirror.com/marked/-/marked-18.0.5.tgz",
"integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/modern-tar": {
"version": "0.7.6",
"resolved": "https://registry.npmmirror.com/modern-tar/-/modern-tar-0.7.6.tgz",
"integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/nanoid": {
"version": "5.1.15",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.15.tgz",
"integrity": "sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/puppeteer": {
"version": "25.1.0",
"resolved": "https://registry.npmmirror.com/puppeteer/-/puppeteer-25.1.0.tgz",
"integrity": "sha512-7L6/0JM7XStK99lIL4xQySyNEXNfII6pk0BxkI5kKBTOhR7AsoQiv067YTsE/rIXxQiq9ajlO4WcqBjS/FWK1A==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.0.4",
"chromium-bidi": "16.0.1",
"devtools-protocol": "0.0.1624250",
"lilconfig": "^3.1.3",
"puppeteer-core": "25.1.0",
"typed-query-selector": "^2.12.2"
},
"bin": {
"puppeteer": "lib/puppeteer/node/cli.js"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/puppeteer-core": {
"version": "25.1.0",
"resolved": "https://registry.npmmirror.com/puppeteer-core/-/puppeteer-core-25.1.0.tgz",
"integrity": "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "3.0.4",
"chromium-bidi": "16.0.1",
"devtools-protocol": "0.0.1624250",
"typed-query-selector": "^2.12.2",
"webdriver-bidi-protocol": "0.4.2",
"ws": "^8.21.0"
},
"engines": {
"node": ">=22.12.0"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/sax": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz",
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/typed-query-selector": {
"version": "2.12.2",
"resolved": "https://registry.npmmirror.com/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/webdriver-bidi-protocol": {
"version": "0.4.2",
"resolved": "https://registry.npmmirror.com/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
"integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
"license": "Apache-2.0"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"license": "MIT"
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmmirror.com/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"license": "MIT",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@ -1,7 +0,0 @@
{
"dependencies": {
"docx": "^9.7.1",
"marked": "^18.0.5",
"puppeteer": "^25.1.0"
}
}

View File

@ -1,26 +0,0 @@
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
const dir = 'D:\\projects\\charging-cabinet\\docs';
async function convert(htmlName, pdfName) {
const html = fs.readFileSync(path.join(dir, htmlName), 'utf8');
const browser = await puppeteer.launch({ headless: 'new', executablePath: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', args: ['--no-sandbox'] });
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.pdf({
path: path.join(dir, pdfName),
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '20mm', right: '20mm' },
printBackground: true,
});
await browser.close();
console.log(`OK: ${pdfName}`);
}
(async () => {
await convert('开发合同.html', '开发合同.pdf');
await convert('附件-开发需求规格说明书.html', '附件-开发需求规格说明书.pdf');
console.log('Done');
})();

View File

@ -765,13 +765,16 @@ TCP断连处理:
### 7.1 后台管理系统 ### 7.1 后台管理系统
#### 7.1.1 技术栈建议 #### 7.1.1 技术栈(已确定)
``` ```
后端框架:Spring Boot / Node.js (Egg.js/Nest.js) / Python (FastAPI) 后端框架:Rust + Axum
数据库:MySQL + Redis + InfluxDB(时序数据) 前端框架:React + Arco Design Pro(管理端)
通信协议:自定义TCP协议长连接(设备通讯)+ HTTP/WebSocket(前端) H5用户端:uni-app + Vue 3 + Pinia(跨平台,支持 H5 / 微信小程序)
前端框架:React / Vue3 + Element Plus / Ant Design 数据库:MySQL(阿里云RDS)
缓存:Redis(阿里云Redis)
通信协议:自定义TCP协议长连接(设备通讯)+ HTTP(API)
权限:RBAC,精确到按钮级别
``` ```
#### 7.1.2 核心模块 #### 7.1.2 核心模块
@ -833,22 +836,24 @@ TCP断连处理:
- 故障等级(预警/保护/严重) - 故障等级(预警/保护/严重)
``` ```
### 7.2 微信小程序 ### 7.2 用户端(H5 / 小程序)
> 当前阶段使用 H5 用户端(uni-app 构建),后续可转换为微信小程序。
#### 7.2.1 功能页面 #### 7.2.1 功能页面
| 页面 | 功能 | | 页面 | 功能 |
|------|------| |------|------|
| 首页 | 附近充电柜列表/地图,空闲舱位显示 | | 首页 | 附近充电柜列表,空闲舱位显示,RSSI信号强度 |
| 设备详情 | 仓控板+仓体状态,ICCID信息 |
| 扫码充电 | 扫描仓体二维码,启动充电 | | 扫码充电 | 扫描仓体二维码,启动充电 |
| 预约充电 | 选择仓体,预约时间段 |
| 充电进度 | 实时显示SOC/电压/温度/充电时间 | | 充电进度 | 实时显示SOC/电压/温度/充电时间 |
| 消息通知 | 充电完成、故障告警、系统通知 | | 消息通知 | 充电完成、故障告警、系统通知 |
| 个人中心 | 充电记录、用户信息、反馈 | | 个人中心 | 充电记录、用户信息、反馈 |
#### 7.2.2 仓体状态展示 #### 7.2.2 仓体状态展示
小程序同步显示仓体指示灯状态及门锁状态: 用户端同步显示仓体指示灯状态及门锁状态:
- 绿灯闪烁 → "充电中" - 绿灯闪烁 → "充电中"
- 绿灯常亮 + 门锁图标开 → "空闲(门已开)" - 绿灯常亮 + 门锁图标开 → "空闲(门已开)"
- 绿灯常亮 + 门锁图标关 → "空闲(门已关)" - 绿灯常亮 + 门锁图标关 → "空闲(门已关)"
@ -856,7 +861,7 @@ TCP断连处理:
扫码开门流程: 扫码开门流程:
1. 用户扫描仓体二维码 1. 用户扫描仓体二维码
2. 小程序显示仓体信息和操作按钮 2. 用户端显示仓体信息和操作按钮
3. 点击"开门"按钮 → 平台验证 → 下发开门指令 3. 点击"开门"按钮 → 平台验证 → 下发开门指令
4. 门锁打开,用户放入/取出电池 4. 门锁打开,用户放入/取出电池
5. 关门后系统自动检测门磁闭合 5. 关门后系统自动检测门磁闭合

View File

@ -1,347 +0,0 @@
# 36仓体直流电瓶车充电柜 - 开发计划
## 1. 项目概述
- **项目名称**:36仓体直流电瓶车充电柜
- **开发周期**:12周
- **核心交付**:充电柜硬件(含仓控主板×6、柜控主板×1)+ 后台管理系统 + 微信小程序
---
## 2. 阶段划分
### 第1阶段:需求确认与方案设计(第1-2周)
**目标**:完成需求确认、技术调研、方案定稿
| 任务 | 负责人 | 输出物 | 时间 |
|------|--------|--------|------|
| 需求评审与确认 | PM/研发 | 需求规格说明书 | 第1周 |
| 技术调研 | 研发 | 技术调研报告 | 第1周 |
| 系统架构设计 | 架构师 | 系统架构设计文档 | 第2周 |
| 硬件方案设计 | 硬件工程师 | 硬件方案文档 | 第2周 |
| 元器件选型 | 硬件工程师 | 选型清单(BOM) | 第2周 |
| 仓控主板详细设计 | 硬件工程师 | 仓控主板原理图+PCB设计 | 第2周 |
| 柜控主板详细设计 | 硬件工程师 | 柜控主板原理图+PCB设计 | 第2周 |
| 通讯协议设计 | 嵌入式工程师 | 协议设计文档 | 第2周 |
| 指示灯选型确认 | 硬件工程师 | 指示灯规格确认单 | 第2周 |
| 平台禁用功能方案 | PM/研发 | 功能方案文档 | 第2周 |
| 电池握手方案设计 | 嵌入式工程师 | 握手流程设计文档 | 第2周 |
| 双模式充电控制方案 | 嵌入式工程师 | 控制逻辑设计文档 | 第2周 |
| 接口定义评审 | 全组 | 接口定义确认 | 第2周 |
| 仓控板拨码ID方案 | 硬件工程师 | ID设置方案确认 | 第2周 |
| 仓控-电源独立通讯链路设计 | 嵌入式工程师 | 通讯链路设计文档 | 第2周 |
> **里程碑1(第2周末)**:需求确认完成,技术方案定稿,元器件选型完成,仓控板与柜控板定制方案明确。
---
### 第2阶段:硬件开发与软件开发(第3-6周)
#### 2.1 硬件开发
| 任务 | 负责人 | 输出物 | 时间 |
|------|--------|--------|------|
| 充电柜本体结构设计 | 结构工程师 | 3D模型+工程图 | 第3-4周 |
| 充电柜本体打样组装 | 工厂 | 样机柜体 | 第4-5周 |
| 电源选型采购 | 采购 | 电源到货 | 第3周 |
| 指示灯选型采购 | 采购 | 指示灯到货 | 第3周 |
| 通讯模块采购 | 采购 | 4G模块到货 | 第3周 |
| 仓控主板PCB打样 | PCB工厂 | 仓控板PCB | 第4周 |
| 柜控主板PCB打样 | PCB工厂 | 柜控板PCB | 第4周 |
| 仓控主板焊接调试 | 硬件工程师 | 仓控板6块(功能正常) | 第5周 |
| 柜控主板焊接调试 | 硬件工程师 | 柜控板1块(功能正常) | 第5周 |
| 继电器通断测试 | 硬件工程师 | 测试报告 | 第5周 |
| 光耦隔离效果测试 | 硬件工程师 | 测试报告 | 第5周 |
| 485通讯调试 | 硬件工程师 | 通讯功能正常 | 第5周 |
| 温控接口调试 | 硬件工程师 | 6路温控采集正常 | 第5周 |
| 仓控板拨码开关ID测试 | 硬件工程师 | ID识别准确 | 第5-6周 |
| 仓控板门锁/门磁调试 | 硬件工程师 | 6路门锁驱动+门磁检测正常 | 第6周 |
| 仓控-电源独立通讯调试 | 嵌入式工程师 | 通讯链路正常 | 第6周 |
| 4G模块调试 | 嵌入式工程师 | 4G通讯正常 | 第6周 |
| 烟感/浸水接口调试 | 硬件工程师 | 开关量检测正常 | 第6周 |
| 喷淋电磁阀调试 | 硬件工程师 | 烟感联动开启正常 | 第6周 |
| 环境温控传感器调试 | 硬件工程师 | 温度采集正常 | 第6周 |
| 加热风扇继电器调试 | 硬件工程师 | 独立通断正常 | 第6周 |
| 电磁泵继电器调试 | 硬件工程师 | 通断控制正常 | 第6周 |
| 指示灯接线与调试 | 硬件工程师 | 灯光状态正常 | 第6周 |
| 整机装配 | 工厂 | 样机完成 | 第6周 |
#### 2.2 软件开发
| 任务 | 负责人 | 输出物 | 时间 |
|------|--------|--------|------|
| 仓控板固件-485从站通讯 | 嵌入式工程师 | 代码+单元测试 | 第3-4周 |
| 仓控板固件-BMS通讯模块(Modbus-RTU) | 嵌入式工程师 | 代码+单元测试 | 第3-5周 |
| 仓控板固件-BMS通讯模块(天能协议) | 嵌入式工程师 | 代码+单元测试 | 第4-5周 |
| 仓控板固件-无协议充电模块 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 仓控板固件-电源通讯模块(1TN协议) | 嵌入式工程师 | 代码+单元测试 | 第4-5周 |
| 仓控板固件-继电器控制 | 嵌入式工程师 | 代码+单元测试 | 第4周 |
| 仓控板固件-门锁控制 | 嵌入式工程师 | 代码+单元测试 | 第4-5周 |
| 仓控板固件-门磁监测 | 嵌入式工程师 | 代码+单元测试 | 第4-5周 |
| 仓控板固件-指示灯控制 | 嵌入式工程师 | 代码+单元测试 | 第4周 |
| 仓控板固件-温控采集 | 嵌入式工程师 | 代码+单元测试 | 第4-5周 |
| 仓控板固件-ID管理(拨码开关) | 嵌入式工程师 | 代码+单元测试 | 第4周 |
| 仓控板固件-数据上传/指令接收 | 嵌入式工程师 | 代码+单元测试 | 第5周 |
| 仓控板固件-OTA升级 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 仓控板固件-BMS协议适配层 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-485主站通讯(ID寻址) | 嵌入式工程师 | 代码+单元测试 | 第4-5周 |
| 柜控板固件-4G通讯模块 | 嵌入式工程师 | 代码+单元测试 | 第4-6周 |
| 柜控板固件-数据处理与存储 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-充电控制逻辑(双模式) | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-烟感/浸水监测 | 嵌入式工程师 | 代码+单元测试 | 第5周 |
| 柜控板固件-安全监测与联动 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-加热风扇温控 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-电表采集(DL/T645) | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-电磁泵控制 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-喷淋电磁阀控制 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-门锁指令转发 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-OTA升级管理 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 柜控板固件-BMS配置管理 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
| 后台系统-设备管理模块 | 后端工程师 | 接口+管理界面 | 第3-5周 |
| 后台系统-仓体控制模块 | 后端工程师 | 接口+管理界面 | 第4-6周 |
| 后台系统-用户管理模块 | 后端工程师 | 接口+管理界面 | 第3-4周 |
| 后台系统-数据统计模块 | 后端工程师 | 接口+报表页面 | 第5-6周 |
| 后台系统-故障管理模块 | 后端工程师 | 接口+管理界面 | 第5-6周 |
| 后台系统-BMS协议配置 | 后端工程师 | 接口+配置界面 | 第5-6周 |
| 后台系统-无协议充电参数配置 | 后端工程师 | 接口+配置界面 | 第5-6周 |
| 后台系统-报警阈值管理 | 后端工程师 | 接口+配置界面 | 第5-6周 |
| 后台系统-OTA升级管理 | 后端工程师 | 接口+升级管理界面 | 第5-6周 |
| 后台系统-仓体禁用功能 | 后端工程师 | 接口+管理界面 | 第5-6周 |
| 后台系统-充电控制模式配置 | 后端工程师 | 接口+配置界面 | 第5-6周 |
| 后台系统-4G通讯管理 | 后端工程师 | 接口+监控界面 | 第5-6周 |
| 后台系统-4G MQTT服务端 | 后端工程师 | 服务+接口 | 第4-6周 |
| 小程序-扫码充电 | 前端工程师 | 小程序页面+接口对接 | 第4-6周 |
| 小程序-预约充电 | 前端工程师 | 小程序页面+接口对接 | 第4-6周 |
| 小程序-充电进度查询 | 前端工程师 | 小程序页面+接口对接 | 第5-6周 |
| 小程序-消息通知 | 前端工程师 | 推送功能 | 第5-6周 |
| 小程序-故障反馈 | 前端工程师 | 小程序页面+接口对接 | 第5-6周 |
| 电池握手验证程序 | 嵌入式/后端 | 代码+单元测试 | 第5-6周 |
| 指示灯控制程序联调 | 嵌入式工程师 | 功能验证 | 第6周 |
| 平台仓体禁用功能联调 | 全栈 | 功能验证 | 第6周 |
| 柜控板-仓控板ID寻址通讯程序 | 嵌入式工程师 | 代码+单元测试 | 第5-6周 |
> **里程碑2(第6周末)**:硬件开发调试完成,软件开发完成,各模块初步实现。
---
### 第3阶段:系统集成与联合调试(第7-9周)
| 任务 | 负责人 | 时间 |
|------|--------|------|
| 仓控板-柜控板485通讯联调 | 嵌入式工程师 | 第7周 |
| 仓控板-电源独立通讯联调 | 嵌入式工程师 | 第7周 |
| 仓控板-BMS通讯联调 | 嵌入式工程师 | 第7周 |
| 指示灯状态联动测试 | 全组 | 第7周 |
| 平台仓体禁用功能联调 | 全栈 | 第7周 |
| 电池握手验证联调 | 全组 | 第7-8周 |
| 充电双模式控制联调 | 全组 | 第7-8周 |
| 无协议充电联调 | 全组 | 第7-8周 |
| 4G数据上传联调 | 全栈 | 第7-8周 |
| 烟感/浸水+喷淋电磁阀联动联调 | 全组 | 第8周 |
| 门锁控制联调(平台开门) | 全栈 | 第8周 |
| 门磁监测+关门超时报警联调 | 全组 | 第8周 |
| BMS多协议切换联调 | 全组 | 第8周 |
| OTA固件升级联调 | 全组 | 第8周 |
| 加热风扇温控联调 | 全组 | 第8周 |
| 电磁泵远程控制联调 | 全组 | 第8周 |
| 后台管理平台联调 | 全栈 | 第8周 |
| 小程序联调 | 全栈 | 第8周 |
| 仓控板ID寻址准确性测试 | 嵌入式工程师 | 第8周 |
| 整机功能联调 | 全组 | 第8-9周 |
| 异常场景排查与修复 | 全组 | 第8-9周 |
| 性能调优 | 全组 | 第9周 |
| 安全性测试 | 全组 | 第9周 |
| 系统稳定性测试 | 全组 | 第9周 |
> **里程碑3(第9周末)**:系统集成调试完成,所有功能正常,无重大故障。
---
### 第4阶段:试点测试(第10-11周)
| 任务 | 负责人 | 时间 |
|------|--------|------|
| 36仓体同步运行测试 | 全组 | 第10周 |
| 指示灯状态准确性全面测试 | 测试 | 第10周 |
| 平台禁用功能可靠性测试 | 测试 | 第10周 |
| 主板运行稳定性测试 | 测试 | 第10周 |
| 电池握手与充电控制准确性测试 | 测试 | 第10-11周 |
| 仓控板ID拨码设置准确性测试 | 测试 | 第10周 |
| ID寻址通讯可靠性测试 | 测试 | 第10周 |
| 仓控-电源独立通讯稳定性测试 | 测试 | 第10-11周 |
| 4G通讯稳定性测试 | 测试 | 第10-11周 |
| 烟感/浸水告警有效性测试 | 测试 | 第11周 |
| 过充/过温/过流保护测试 | 测试 | 第11周 |
| 实际使用场景模拟测试 | 全组 | 第11周 |
| 问题收集与整改 | 全组 | 第11周 |
> **里程碑4(第11周末)**:试点测试完成,所有问题整改完毕。
---
### 第5阶段:验收交付(第12周)
| 任务 | 负责人 | 输出物 | 时间 |
|------|--------|--------|------|
| 开发文档整理 | 研发 | 开发文档定稿 | 第12周 |
| 测试文档整理 | 测试 | 测试报告 | 第12周 |
| 操作手册编写 | PM | 用户操作手册 | 第12周 |
| 运维手册编写 | 运维 | 运维手册 | 第12周 |
| 项目验收评审 | 全组 | 验收报告 | 第12周 |
| 项目交接 | PM | 交接清单 | 第12周 |
> **里程碑5(第12周末)**:项目验收通过,文档交付完成,正式交接。
---
## 3. 甘特图概览
```
任务\周次 | W1 | W2 | W3 | W4 | W5 | W6 | W7 | W8 | W9 | W10| W11| W12|
--------------|----|----|----|----|----|----|----|----|----|----|----|----|
需求确认 | ██ | ██ | | | | | | | | | | |
方案设计 | ██ | ██ | | | | | | | | | | |
结构设计 | | | ██ | ██ | | | | | | | | |
PCB设计打样 | | | ██ | ██ | | | | | | | | |
元器件采购 | | | ██ | | | | | | | | | |
焊接调试 | | | | ██ | ██ | ██ | | | | | | |
主机固件开发 | | | ██ | ██ | ██ | ██ | | | | | | |
后台开发 | | | ██ | ██ | ██ | ██ | | | | | | |
小程序开发 | | | | ██ | ██ | ██ | | | | | | |
系统联调 | | | | | | | ██ | ██ | ██ | | | |
性能优化 | | | | | | | | ██ | ██ | | | |
试点测试 | | | | | | | | | | ██ | ██ | |
问题整改 | | | | | | | | | | ██ | ██ | |
文档编写 | | | | | | | | | | | ██ | ██ |
验收交付 | | | | | | | | | | | | ██ |
```
---
## 4. 关键里程碑
| 里程碑 | 时间 | 交付物 |
|--------|------|--------|
| M1: 方案定稿 | 第2周末 | 需求规格说明书、技术方案文档、BOM清单、原理图 |
| M2: 开发完成 | 第6周末 | 硬件样机、固件代码、后台系统、小程序 |
| M3: 联调完成 | 第9周末 | 集成测试报告、功能确认书 |
| M4: 试点完成 | 第11周末 | 试点测试报告、问题整改清单 |
| M5: 验收交付 | 第12周末 | 开发文档、测试报告、操作手册、验收报告 |
---
## 5. 资源需求
### 5.1 人员配置
| 角色 | 人数 | 主要职责 |
|------|------|---------|
| 项目经理(PM) | 1 | 需求管理、进度把控、资源协调 |
| 硬件工程师 | 2 | 仓控/柜控主板设计、调试、测试 |
| 嵌入式工程师 | 2 | 仓控/柜控固件开发、协议对接 |
| 后端工程师 | 2 | 后台系统开发、MQTT服务、数据库 |
| 前端工程师 | 1 | 微信小程序开发 |
| 测试工程师 | 1 | 单元/集成/系统测试 |
| 结构工程师 | 1 | 柜体结构设计 |
| 采购/生产 | 1 | 元器件采购、PCB打样、组装 |
### 5.2 硬件清单(样机)
| 物料 | 数量 | 说明 |
|------|------|------|
| 充电柜柜体 | 1台 | 36仓体结构 |
| 充电电源 | 6台 | 直流充电电源,1TN协议 |
| 仓控主板PCB | 10块 | 含备板 |
| 柜控主板PCB | 3块 | 含备板 |
| 4G模块 | 3个 | 含备板 |
| 直流继电器 | 50个 | 含备件 |
| 光耦隔离芯片 | 100个 | 485隔离用 |
| 温控传感器 | 15个 | 含备件 |
| 烟感传感器 | 2个 | 含备件 |
| 浸水传感器 | 2个 | 含备件 |
| 环境温控传感器 | 3个 | 含备件,用于加热风扇自动温控(NTC/DS18B20) |
| 电子门锁(电控锁) | 40个 | 含备件,12V,每仓体1个 |
| 门磁传感器 | 40个 | 含备件,每仓体1个 |
| 喷淋电磁阀 | 2个 | 含备件,烟感联动喷淋 |
| 加热风扇 | 1台 | 含独立继电器控制 |
| 电磁泵 | 1台 | 含继电器驱动 |
| 状态指示灯 | 50个 | 含备件 |
| 拨码开关 | 10个 | 仓控板ID设置 |
| 连接线束 | 1批 | 4PIN线等 |
### 5.3 软件环境
| 类型 | 环境/工具 |
|------|-----------|
| 嵌入式IDE | Keil MDK / STM32CubeIDE |
| 后台开发 | Spring Boot / Node.js / Python + MySQL + Redis + InfluxDB |
| 小程序开发 | 微信开发者工具 |
| 协议测试 | Modbus Poll / 串口调试助手 |
| 版本管理 | Git |
| 项目管理 | 禅道 / Jira |
---
## 6. 风险与应对
| 风险 | 概率 | 影响 | 应对措施 |
|------|------|------|---------|
| 星恒BMS协议版本不匹配 | 中 | 高 | 提前获取最新协议文档,预留协议适配层 |
| 电源1TN协议兼容性问题 | 中 | 高 | 提前联调电源厂商,准备协议测试环境 |
| 485通讯干扰(36舱全开) | 中 | 高 | 光耦隔离+终端电阻+合理布线 |
| 4G信号不稳定 | 中 | 中 | 柜控板本地缓存+断线重连机制 |
| 仓控板ID寻址冲突 | 低 | 高 | 拨码开关硬件防呆+上电自检+重复ID告警 |
| 元器件供货延迟 | 中 | 中 | 提前采购,备选供应商方案 |
| 温控传感器精度不足 | 低 | 中 | 选型测试验证,软件滤波处理 |
| 指示灯亮度/视角不足 | 低 | 中 | 提前打样测试确认 |
| 电池握手失败率偏高 | 中 | 高 | 增加重试机制,完善异常处理 |
| PCB设计缺陷需改版 | 中 | 高 | 预留测试点,第一版预投少量验证 |
| 加热风扇温控失效 | 低 | 中 | 增加硬件超温保护,双路温控冗余 |
| 电磁泵继电器粘连 | 低 | 中 | 选用高品质继电器,定期巡检 |
| 门锁频繁动作损坏 | 中 | 中 | 选用工业级电控锁,预留手动开门方式 |
| BMS多协议兼容性 | 中 | 高 | 协议适配层隔离,各协议独立实现,平台可远程回退 |
| 喷淋电磁阀误触发 | 低 | 中 | 硬件滤波+软件确认延时,防误喷 |
---
## 7. 测试策略
### 7.1 测试阶段对应
| 阶段 | 测试类型 | 说明 |
|------|---------|------|
| 第5-6周 | 单元测试 | 硬件模块+软件模块单独测试 |
| 第7-9周 | 集成测试 | 模块间对接测试,协议联调 |
| 第10-11周 | 系统测试 | 全功能测试,模拟实际场景 |
### 7.2 核心测试用例
| 编号 | 测试项 | 预期结果 |
|------|--------|---------|
| TC01 | 36路独立充电同时运行 | 各舱互不干扰,电源正常输出 |
| TC02 | 指示灯状态切换 | 各状态切换准确,延迟≤0.5s |
| TC03 | 平台禁用/解除 | 禁用后红灯常亮,禁止充电;解除后恢复绿灯常亮 |
| TC04 | BMS数据读取 | SOC/电压/温度/告警数据准确 |
| TC05 | 电源参数控制 | 电压/电流设置准确,开关控制正常 |
| TC06 | 电池握手流程 | 插入→读取→上传→校验→充电启动,流程完整 |
| TC07 | 双模式充电控制 | 平台模式/柜控模式均正常 |
| TC08 | 仓控板ID识别 | 拨码ID1-6准确识别,通讯寻址无误 |
| TC09 | 仓控-电源独立通讯 | 各仓控板与对应电源通讯稳定 |
| TC10 | 烟感触发保护 | 全局断电,红灯切换,告警上传 |
| TC11 | 浸水触发保护 | 同上 |
| TC12 | 过充保护 | SOC≥98%自动断电 |
| TC13 | 过温保护 | 温度>55℃自动断电 |
| TC14 | 4G断线重连 | 断线后缓存数据,重连后补偿上传 |
| TC15 | 电池不合规红灯闪烁 | 拔掉电池后自动恢复绿灯常亮 |
| TC16 | 加热风扇温控 | 环境温度<0°C自动启动,≥25°C自动停止 |
| TC17 | 电磁泵远程控制 | 平台下发通/断指令,继电器正确响应 |
| TC18 | 加热风扇滞回控制 | 温度在0-25°C之间保持当前状态不抖动 |
| TC19 | 扫码开门 | 平台下发开门指令,门锁动作,门磁反馈正确 |
| TC20 | 关门超时报警 | 开门后超过设置阈值未关,上传告警 |
| TC21 | 烟感联动喷淋 | 烟感触发→接触器断开+喷淋电磁阀打开+告警上传 |
| TC22 | BMS协议切换 | 平台切换星恒/天能/无协议,仓控板正确切换通讯方式 |
| TC23 | 无协议充电 | 固定电压电流输出,低于停止电流阈值自动断电 |
| TC24 | OTA固件升级 | 柜控板+仓控板固件在线升级,升级后正常运行 |
| TC25 | 高温报警阈值可配置 | 平台设置不同阈值,仓体超温触发告警 |
| TC26 | 电表耗电量采集 | 柜控板按DL/T645读取电表数据,上报平台显示正确 |

View File

@ -32,34 +32,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "api-server"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"bytes",
"chrono",
"dotenvy",
"futures",
"hex",
"jsonwebtoken",
"rand",
"redis",
"rust_xlsxwriter",
"serde",
"serde_json",
"sha2",
"sqlx",
"thiserror",
"tokio",
"tokio-util",
"tower-http",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
version = "1.4.2" version = "1.4.2"
@ -1254,6 +1226,34 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "pms-api"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"bytes",
"chrono",
"dotenvy",
"futures",
"hex",
"jsonwebtoken",
"rand",
"redis",
"rust_xlsxwriter",
"serde",
"serde_json",
"sha2",
"sqlx",
"thiserror",
"tokio",
"tokio-util",
"tower-http",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.5" version = "0.1.5"

View File

@ -1,5 +1,5 @@
[package] [package]
name = "api-server" name = "pms-api"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"

View File

@ -73,8 +73,8 @@ pub async fn run(pool: &MySqlPool) -> Result<(), sqlx::Error> {
cabinet_id BIGINT NOT NULL, cabinet_id BIGINT NOT NULL,
start_time TIMESTAMP NULL COMMENT '充电开始时间', start_time TIMESTAMP NULL COMMENT '充电开始时间',
end_time TIMESTAMP NULL COMMENT '充电结束时间', end_time TIMESTAMP NULL COMMENT '充电结束时间',
energy DECIMAL(10,2) COMMENT '充电量kWh', energy BIGINT NOT NULL DEFAULT 0 COMMENT '充电量(Wh)',
max_temperature DECIMAL(5,1) COMMENT '最高温度', max_temperature BIGINT NOT NULL DEFAULT 0 COMMENT '最高温度(0.1°C)',
status TINYINT DEFAULT 0 COMMENT '0正常 1异常 2手动停止', status TINYINT DEFAULT 0 COMMENT '0正常 1异常 2手动停止',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (compartment_id) REFERENCES compartments(id), FOREIGN KEY (compartment_id) REFERENCES compartments(id),
@ -94,7 +94,7 @@ pub async fn run(pool: &MySqlPool) -> Result<(), sqlx::Error> {
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY,
cabinet_id BIGINT NOT NULL, cabinet_id BIGINT NOT NULL,
stat_date DATE NOT NULL, stat_date DATE NOT NULL,
total_energy DECIMAL(10,2) COMMENT '总能耗kWh', total_energy BIGINT NOT NULL DEFAULT 0 COMMENT '总能耗(Wh)',
charge_count INT DEFAULT 0 COMMENT '充电次数', charge_count INT DEFAULT 0 COMMENT '充电次数',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (cabinet_id) REFERENCES cabinets(id), FOREIGN KEY (cabinet_id) REFERENCES cabinets(id),

View File

@ -31,15 +31,15 @@ pub async fn login(
return Err(AppError::BadRequest("手机号和密码不能为空".into())); return Err(AppError::BadRequest("手机号和密码不能为空".into()));
} }
// 查询用户(含密码比对) // 查询用户(含角色和组织信息)
let user_row: Option<(i64, Option<String>, String, String, i32, i32)> = sqlx::query_as( let user_row: Option<(i64, Option<String>, String, String, i32, i32, Option<i64>)> = sqlx::query_as(
"SELECT id, name, phone, password, role, status FROM users WHERE phone = ?" "SELECT id, name, phone, password, status, role, organization_id FROM users WHERE phone = ?",
) )
.bind(&input.phone) .bind(&input.phone)
.fetch_optional(pool) .fetch_optional(pool)
.await?; .await?;
let (id, name, phone, stored_password, role, status) = let (id, name, phone, stored_password, status, role_level, organization_id) =
user_row.ok_or_else(|| AppError::Unauthorized("手机号或密码错误".into()))?; user_row.ok_or_else(|| AppError::Unauthorized("手机号或密码错误".into()))?;
// 检查账户状态 // 检查账户状态
@ -69,7 +69,8 @@ pub async fn login(
"id": id, "id": id,
"name": name, "name": name,
"phone": phone, "phone": phone,
"role": role, "role_level": role_level,
"organization_id": organization_id,
"permissions": perm_list, "permissions": perm_list,
} }
}, },

View File

@ -5,6 +5,7 @@
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use axum::Json; use axum::Json;
use redis;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
use sqlx::MySqlPool; use sqlx::MySqlPool;
@ -403,7 +404,7 @@ pub async fn get_cabinet_detail(
user: CurrentUser, user: CurrentUser,
State(state): State<super::AppState>, State(state): State<super::AppState>,
Path(id): Path<i64>, Path(id): Path<i64>,
) -> Result<Json<CabinetDetail>, AppError> { ) -> Result<Json<Value>, AppError> {
auth::check_permission(&user, "device:view")?; auth::check_permission(&user, "device:view")?;
let db = &state.mysql; let db = &state.mysql;
@ -429,7 +430,9 @@ pub async fn get_cabinet_detail(
.fetch_all(db) .fetch_all(db)
.await?; .await?;
let mut cabin_boards = Vec::new(); // 查询仓控板+仓体,同时构建 Value 以便注入 SOC
let mut cabin_boards: Vec<Value> = Vec::new();
let mut comp_ids: Vec<i64> = Vec::new();
for board in boards { for board in boards {
let compartments: Vec<CompartmentRow> = sqlx::query_as( let compartments: Vec<CompartmentRow> = sqlx::query_as(
r#"SELECT id, cabin_board_id, channel_id, status r#"SELECT id, cabin_board_id, channel_id, status
@ -439,23 +442,91 @@ pub async fn get_cabinet_detail(
.fetch_all(db) .fetch_all(db)
.await?; .await?;
cabin_boards.push(CabinBoardWithCompartments { for c in &compartments {
id: board.id, comp_ids.push(c.id);
board_id: board.board_id,
status: board.status,
compartments,
});
} }
Ok(Json(CabinetDetail { let comp_values: Vec<Value> = compartments.into_iter().map(|c| {
id: cabinet.id, json!({
project_id: cabinet.project_id, "id": c.id,
abstract_id: cabinet.abstract_id, "channel_id": c.channel_id,
imei: cabinet.imei, "status": c.status,
name: cabinet.name, })
status: cabinet.status, }).collect();
cabin_boards,
})) cabin_boards.push(json!({
"id": board.id,
"board_id": board.board_id,
"status": board.status,
"compartments": comp_values,
}));
}
// 从 Redis 批量读取实时状态(RSSI/电源/SOC)
let (rssi, pow_fail_dc, pow_fail_ac, mut soc_map) = if let Ok(client) = redis::Client::open(state.redis_url.as_str()) {
if let Ok(mut conn) = client.get_async_connection().await {
let key = format!("device:{}", cabinet.imei);
let mut pipe = redis::pipe();
pipe.cmd("HGET").arg(&key).arg("rssi")
.cmd("HGET").arg(&key).arg("pow_fail_dc")
.cmd("HGET").arg(&key).arg("pow_fail_ac");
for cid in &comp_ids {
pipe.cmd("GET").arg(format!("compartment:status:{}", cid));
}
let vals: Vec<Option<String>> = pipe.query_async(&mut conn).await.unwrap_or_default();
let rssi: Option<i32> = vals.get(0).and_then(|v| v.as_ref()).and_then(|v| v.parse().ok());
let dc = vals.get(1).and_then(|v| v.as_ref()).map(|v| v == "1");
let ac = vals.get(2).and_then(|v| v.as_ref()).map(|v| v == "1");
let mut soc_map = std::collections::HashMap::new();
for (i, cid) in comp_ids.iter().enumerate() {
if let Some(Some(data)) = vals.get(3 + i) {
if let Ok(v) = serde_json::from_str::<Value>(data) {
let soc = v.get("soc").and_then(|s| s.as_i64());
let bid = v.get("battery_id").and_then(|s| s.as_str()).map(|s| s.to_string());
soc_map.insert(*cid, (soc, bid));
}
}
}
(rssi, dc, ac, soc_map)
} else { (None, None, None, std::collections::HashMap::new()) }
} else { (None, None, None, std::collections::HashMap::new()) };
// 注入 SOC 到每个仓体
for board in &mut cabin_boards {
if let Some(comps) = board.get_mut("compartments").and_then(|c| c.as_array_mut()) {
for comp in comps {
if let Some(id) = comp.get("id").and_then(|i| i.as_i64()) {
if let Some((soc, bid)) = soc_map.get(&id) {
if let Some(s) = soc {
comp["soc"] = json!(s);
}
if let Some(b) = bid {
comp["battery_id"] = json!(b);
}
}
}
}
}
}
Ok(Json(json!({
"code": 0,
"data": {
"id": cabinet.id,
"project_id": cabinet.project_id,
"abstract_id": cabinet.abstract_id,
"imei": cabinet.imei,
"iccid": cabinet.iccid,
"name": cabinet.name,
"status": cabinet.status,
"rssi": rssi,
"pow_fail_dc": pow_fail_dc,
"pow_fail_ac": pow_fail_ac,
"address": cabinet.address,
"cabin_boards": cabin_boards,
},
"message": "ok"
})))
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -43,8 +43,8 @@ pub struct ChargeRecordItem {
pub channel_id: i8, pub channel_id: i8,
pub start_time: Option<String>, pub start_time: Option<String>,
pub end_time: Option<String>, pub end_time: Option<String>,
pub energy: Option<f64>, pub energy: Option<i64>,
pub max_temperature: Option<f64>, pub max_temperature: Option<i64>,
pub status: i8, pub status: i8,
pub status_text: String, pub status_text: String,
} }
@ -62,7 +62,7 @@ fn status_text(status: i8) -> &'static str {
} }
} }
/// 数据库行映射 — DECIMAL 列通过 CAST 转为 f64 /// 数据库行映射
#[derive(Debug, sqlx::FromRow)] #[derive(Debug, sqlx::FromRow)]
struct ChargeRecordRow { struct ChargeRecordRow {
id: i64, id: i64,
@ -74,8 +74,8 @@ struct ChargeRecordRow {
channel_id: Option<i8>, channel_id: Option<i8>,
start_time: Option<NaiveDateTime>, start_time: Option<NaiveDateTime>,
end_time: Option<NaiveDateTime>, end_time: Option<NaiveDateTime>,
energy: Option<f64>, energy: Option<i64>,
max_temperature: Option<f64>, max_temperature: Option<i64>,
status: i8, status: i8,
} }
@ -137,8 +137,8 @@ async fn fetch_charge_rows(
"SELECT cr.id, cr.cabinet_id, c.name AS cabinet_name, \ "SELECT cr.id, cr.cabinet_id, c.name AS cabinet_name, \
cr.compartment_id, cb.id AS cabin_board_id, cb.board_id, comp.channel_id, \ cr.compartment_id, cb.id AS cabin_board_id, cb.board_id, comp.channel_id, \
cr.start_time, cr.end_time, \ cr.start_time, cr.end_time, \
CAST(cr.energy AS DOUBLE) AS energy, \ cr.energy, \
CAST(cr.max_temperature AS DOUBLE) AS max_temperature, \ cr.max_temperature, \
cr.status \ cr.status \
FROM charge_records cr \ FROM charge_records cr \
LEFT JOIN cabinets c ON cr.cabinet_id = c.id \ LEFT JOIN cabinets c ON cr.cabinet_id = c.id \
@ -351,7 +351,7 @@ async fn generate_charge_records_excel(
let headers = [ let headers = [
"ID", "柜子", "仓板", "通道", "开始时间", "结束时间", "ID", "柜子", "仓板", "通道", "开始时间", "结束时间",
"充电量(kWh)", "最高温度(°C)", "状态", "充电量(Wh)", "最高温度(0.1°C)", "状态",
]; ];
for (col, header) in headers.iter().enumerate() { for (col, header) in headers.iter().enumerate() {
sheet.write_string(0, col as u16, *header)?; sheet.write_string(0, col as u16, *header)?;
@ -371,8 +371,8 @@ async fn generate_charge_records_excel(
row, 5, row, 5,
record.end_time.map(|t| t.to_string()).as_deref().unwrap_or("-"), record.end_time.map(|t| t.to_string()).as_deref().unwrap_or("-"),
)?; )?;
sheet.write_number(row, 6, record.energy.unwrap_or(0.0))?; sheet.write_number(row, 6, record.energy.unwrap_or(0) as f64)?;
sheet.write_number(row, 7, record.max_temperature.unwrap_or(0.0))?; sheet.write_number(row, 7, record.max_temperature.unwrap_or(0) as f64)?;
sheet.write_string(row, 8, status_text(record.status))?; sheet.write_string(row, 8, status_text(record.status))?;
} }

View File

@ -1,7 +1,7 @@
//! 能耗管理 API — 总览、排行、趋势、导出 //! 能耗管理 API — 总览、排行、趋势、导出
//! //!
//! 基于 energy_stats 表按日期聚合的能耗数据进行统计分析。 //! 基于 energy_stats 表按日期聚合的能耗数据进行统计分析。
//! 电费估算按 ¥0.80/kWh 计算(可配置)。 //! 电费估算按 ¥0.80/kWh 计算(可配置),energy 单位为 Wh。
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::Json; use axum::Json;
@ -11,8 +11,8 @@ use serde_json::{json, Value};
use crate::error::AppError; use crate::error::AppError;
use crate::middleware::auth::{self, CurrentUser}; use crate::middleware::auth::{self, CurrentUser};
/// 电费单价(元/kWh) /// 电费单价(元/Wh),energy 单位为 Wh,0.80 元/kWh = 0.0008 元/Wh
const COST_PER_KWH: f64 = 0.80; const COST_PER_WH: f64 = 0.0008;
/// 趋势查询参数 /// 趋势查询参数
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -40,8 +40,8 @@ struct CabinetRankRow {
id: i64, id: i64,
abstract_id: String, abstract_id: String,
name: Option<String>, name: Option<String>,
today_energy: Option<f64>, today_energy: Option<i64>,
month_energy: Option<f64>, month_energy: Option<i64>,
} }
/// 项目统计行 /// 项目统计行
@ -50,15 +50,15 @@ struct ProjectStatRow {
id: i64, id: i64,
name: String, name: String,
cabinet_count: i64, cabinet_count: i64,
today_energy: Option<f64>, today_energy: Option<i64>,
month_energy: Option<f64>, month_energy: Option<i64>,
} }
/// 趋势数据行 /// 趋势数据行
#[derive(Debug, sqlx::FromRow)] #[derive(Debug, sqlx::FromRow)]
struct TrendRow { struct TrendRow {
period: String, // 日期或周/月标识 period: String, // 日期或周/月标识
energy: Option<f64>, energy: Option<i64>,
charge_count: Option<i64>, charge_count: Option<i64>,
} }
@ -69,7 +69,7 @@ struct EnergyDetailRow {
cabinet_id: i64, cabinet_id: i64,
cabinet_name: Option<String>, cabinet_name: Option<String>,
project_name: Option<String>, project_name: Option<String>,
total_energy: Option<f64>, total_energy: Option<i64>,
charge_count: Option<i64>, charge_count: Option<i64>,
} }
@ -100,22 +100,22 @@ pub async fn get_summary(
// 今日总能耗 // 今日总能耗
let today_sql = format!( let today_sql = format!(
"SELECT COALESCE(SUM(total_energy), 0) FROM energy_stats es WHERE stat_date = CURDATE(){}", "SELECT CAST(COALESCE(SUM(total_energy), 0) AS SIGNED) FROM energy_stats es WHERE stat_date = CURDATE(){}",
org_filter org_filter
); );
let mut today_q = sqlx::query_scalar::<_, f64>(&today_sql); let mut today_q = sqlx::query_scalar::<_, i64>(&today_sql);
for v in &org_binds { today_q = today_q.bind(v); } for v in &org_binds { today_q = today_q.bind(v); }
let today_energy: f64 = today_q.fetch_one(pool).await?; let today_energy: i64 = today_q.fetch_one(pool).await?;
// 本月总能耗 // 本月总能耗
let month_sql = format!( let month_sql = format!(
"SELECT COALESCE(SUM(total_energy), 0) FROM energy_stats es \ "SELECT CAST(COALESCE(SUM(total_energy), 0) AS SIGNED) FROM energy_stats es \
WHERE stat_date >= DATE_FORMAT(CURDATE(), '%Y-%m-01'){}", WHERE stat_date >= DATE_FORMAT(CURDATE(), '%Y-%m-01'){}",
org_filter org_filter
); );
let mut month_q = sqlx::query_scalar::<_, f64>(&month_sql); let mut month_q = sqlx::query_scalar::<_, i64>(&month_sql);
for v in &org_binds { month_q = month_q.bind(v); } for v in &org_binds { month_q = month_q.bind(v); }
let month_energy: f64 = month_q.fetch_one(pool).await?; let month_energy: i64 = month_q.fetch_one(pool).await?;
// 柜子总数(含组织过滤) // 柜子总数(含组织过滤)
let cabinet_count: i64 = if user.role_level >= 2 { let cabinet_count: i64 = if user.role_level >= 2 {
@ -145,15 +145,12 @@ pub async fn get_summary(
0 0
}; };
let today = today_energy;
let month = month_energy;
Ok(Json(json!({ Ok(Json(json!({
"code": 0, "code": 0,
"data": { "data": {
"today_energy": (today * 100.0).round() / 100.0, "today_energy": today_energy,
"month_energy": (month * 100.0).round() / 100.0, "month_energy": month_energy,
"estimated_cost": ((month * COST_PER_KWH) * 100.0).round() / 100.0, "estimated_cost": ((month_energy as f64) * COST_PER_WH * 100.0).round() / 100.0,
"cabinet_count": cabinet_count, "cabinet_count": cabinet_count,
"online_count": online_count, "online_count": online_count,
}, },
@ -205,8 +202,8 @@ pub async fn get_cabinet_ranking(
"cabinet_id": r.id, "cabinet_id": r.id,
"abstract_id": r.abstract_id, "abstract_id": r.abstract_id,
"name": r.name, "name": r.name,
"today_energy": (r.today_energy.unwrap_or(0.0) * 100.0).round() / 100.0, "today_energy": r.today_energy.unwrap_or(0),
"month_energy": (r.month_energy.unwrap_or(0.0) * 100.0).round() / 100.0, "month_energy": r.month_energy.unwrap_or(0),
}) })
}).collect(); }).collect();
@ -238,8 +235,8 @@ pub async fn get_project_stats(
let sql = format!( let sql = format!(
"SELECT p.id, p.name, \ "SELECT p.id, p.name, \
COUNT(DISTINCT c.id) AS cabinet_count, \ COUNT(DISTINCT c.id) AS cabinet_count, \
COALESCE(SUM(es_today.total_energy), 0) AS today_energy, \ CAST(COALESCE(SUM(es_today.total_energy), 0) AS SIGNED) AS today_energy, \
COALESCE(SUM(es_month.total_energy), 0) AS month_energy \ CAST(COALESCE(SUM(es_month.total_energy), 0) AS SIGNED) AS month_energy \
FROM projects p \ FROM projects p \
LEFT JOIN cabinets c ON p.id = c.project_id \ LEFT JOIN cabinets c ON p.id = c.project_id \
LEFT JOIN energy_stats es_today \ LEFT JOIN energy_stats es_today \
@ -261,8 +258,8 @@ pub async fn get_project_stats(
"project_id": r.id, "project_id": r.id,
"name": r.name, "name": r.name,
"cabinet_count": r.cabinet_count, "cabinet_count": r.cabinet_count,
"today_energy": (r.today_energy.unwrap_or(0.0) * 100.0).round() / 100.0, "today_energy": r.today_energy.unwrap_or(0),
"month_energy": (r.month_energy.unwrap_or(0.0) * 100.0).round() / 100.0, "month_energy": r.month_energy.unwrap_or(0),
}) })
}).collect(); }).collect();
@ -301,7 +298,7 @@ pub async fn get_trend(
"week" => { "week" => {
let sql = format!( let sql = format!(
"SELECT DATE_FORMAT(stat_date, '%Y-%u') AS period, \ "SELECT DATE_FORMAT(stat_date, '%Y-%u') AS period, \
SUM(total_energy) AS energy, SUM(charge_count) AS charge_count \ CAST(SUM(total_energy) AS SIGNED) AS energy, CAST(SUM(charge_count) AS SIGNED) AS charge_count \
FROM energy_stats es \ FROM energy_stats es \
WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY){} \ WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY){} \
GROUP BY period ORDER BY period ASC", GROUP BY period ORDER BY period ASC",
@ -314,7 +311,7 @@ pub async fn get_trend(
"month" => { "month" => {
let sql = format!( let sql = format!(
"SELECT DATE_FORMAT(stat_date, '%Y-%m') AS period, \ "SELECT DATE_FORMAT(stat_date, '%Y-%m') AS period, \
SUM(total_energy) AS energy, SUM(charge_count) AS charge_count \ CAST(SUM(total_energy) AS SIGNED) AS energy, CAST(SUM(charge_count) AS SIGNED) AS charge_count \
FROM energy_stats es \ FROM energy_stats es \
WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY){} \ WHERE stat_date >= DATE_SUB(CURDATE(), INTERVAL ? DAY){} \
GROUP BY period ORDER BY period ASC", GROUP BY period ORDER BY period ASC",
@ -343,7 +340,7 @@ pub async fn get_trend(
let list: Vec<Value> = rows.iter().map(|r| { let list: Vec<Value> = rows.iter().map(|r| {
json!({ json!({
"period": r.period, "period": r.period,
"energy": (r.energy.unwrap_or(0.0) * 100.0).round() / 100.0, "energy": r.energy.unwrap_or(0),
"charge_count": r.charge_count.unwrap_or(0), "charge_count": r.charge_count.unwrap_or(0),
}) })
}).collect(); }).collect();
@ -489,7 +486,7 @@ async fn generate_energy_excel(
let sql = format!( let sql = format!(
"SELECT es.stat_date, es.cabinet_id, c.name AS cabinet_name, \ "SELECT es.stat_date, es.cabinet_id, c.name AS cabinet_name, \
p.name AS project_name, \ p.name AS project_name, \
CAST(es.total_energy AS DOUBLE) AS total_energy, \ es.total_energy, \
es.charge_count \ es.charge_count \
FROM energy_stats es \ FROM energy_stats es \
LEFT JOIN cabinets c ON es.cabinet_id = c.id \ LEFT JOIN cabinets c ON es.cabinet_id = c.id \
@ -516,7 +513,7 @@ async fn generate_energy_excel(
sheet.set_name("能耗统计")?; sheet.set_name("能耗统计")?;
let headers = [ let headers = [
"日期", "柜子ID", "柜子名称", "所属项目", "能耗(kWh)", "充电次数", "日期", "柜子ID", "柜子名称", "所属项目", "能耗(Wh)", "充电次数",
]; ];
for (col, header) in headers.iter().enumerate() { for (col, header) in headers.iter().enumerate() {
sheet.write_string(0, col as u16, *header)?; sheet.write_string(0, col as u16, *header)?;
@ -528,7 +525,7 @@ async fn generate_energy_excel(
sheet.write_number(row, 1, record.cabinet_id as f64)?; sheet.write_number(row, 1, record.cabinet_id as f64)?;
sheet.write_string(row, 2, record.cabinet_name.as_deref().unwrap_or("-"))?; sheet.write_string(row, 2, record.cabinet_name.as_deref().unwrap_or("-"))?;
sheet.write_string(row, 3, record.project_name.as_deref().unwrap_or("-"))?; sheet.write_string(row, 3, record.project_name.as_deref().unwrap_or("-"))?;
sheet.write_number(row, 4, record.total_energy.unwrap_or(0.0))?; sheet.write_number(row, 4, record.total_energy.unwrap_or(0) as f64)?;
sheet.write_number(row, 5, record.charge_count.unwrap_or(0) as f64)?; sheet.write_number(row, 5, record.charge_count.unwrap_or(0) as f64)?;
} }

View File

@ -1,19 +1,19 @@
//! H5 用户端 API //! H5 用户端 API
//! //!
//! 提供微信扫码充电的 H5 页面所需的全部后端接口: //! 提供微信扫码充电的 H5 页面所需的全部后端接口:
//! - POST /api/h5/auth/login — H5 手机号登录 //! - GET /api/dashboard — 首页仪表盘数据
//! - GET /api/h5/dashboard — 首页仪表盘数据 //! - GET /api/cabinets — 设备列表(按项目过滤)
//! - GET /api/h5/projects — 项目列表 //! - GET /api/cabinets/:id — 设备详情(含仓板+仓体状态)
//! - GET /api/h5/cabinets — 设备列表(按项目过滤) //! - GET /api/cabinets/abstract/:aid — 通过抽象ID查设备(扫码入口,免认证)
//! - GET /api/h5/cabinets/:id — 设备详情(含仓板+仓体状态) //! - GET /api/compartments/:id — 仓体/通道详情
//! - GET /api/h5/cabinets/abstract/:aid — 通过抽象ID查设备(扫码入口) //! - POST /api/charge/start — 开始充电
//! - GET /api/h5/compartments/:id — 仓体/通道详情 //! - POST /api/charge/stop — 停止充电
//! - POST /api/h5/charge/start — 开始充电 //! - POST /api/door/open — 开门
//! - POST /api/h5/charge/stop — 停止充电 //! - GET /api/cabinets/scan/:abstract_id — 扫码验证
//! - POST /api/h5/door/open — 开门
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use axum::Json; use axum::Json;
use redis;
use serde::Deserialize; use serde::Deserialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
@ -25,13 +25,6 @@ use crate::middleware::auth::{self, CurrentUser};
// 请求体定义 // 请求体定义
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// H5 登录请求体
#[derive(Debug, Deserialize)]
pub(crate) struct H5LoginInput {
phone: String,
password: String,
}
/// 充电控制请求体(开始/停止充电、开门共用) /// 充电控制请求体(开始/停止充电、开门共用)
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(crate) struct CompartmentActionInput { pub(crate) struct CompartmentActionInput {
@ -42,6 +35,13 @@ pub(crate) struct CompartmentActionInput {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub(crate) struct CabinetListQuery { pub(crate) struct CabinetListQuery {
project_id: Option<i64>, project_id: Option<i64>,
org_id: Option<i64>,
}
/// 仪表盘查询参数
#[derive(Debug, Deserialize)]
pub(crate) struct DashboardQuery {
org_id: Option<i64>,
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -55,7 +55,7 @@ struct ProjectRow {
name: String, name: String,
} }
/// 柜子列表行 /// 柜子列表行(含通道统计)
#[derive(Debug, sqlx::FromRow)] #[derive(Debug, sqlx::FromRow)]
struct H5CabinetRow { struct H5CabinetRow {
id: i64, id: i64,
@ -63,6 +63,12 @@ struct H5CabinetRow {
name: Option<String>, name: Option<String>,
status: i8, status: i8,
project_id: Option<i64>, project_id: Option<i64>,
imei: Option<String>,
channel_count: i64,
idle_count: i64,
charging_count: i64,
full_count: i64,
fault_count: i64,
} }
/// 仓控板行 /// 仓控板行
@ -96,58 +102,6 @@ struct CabinetImeiRow {
imei: String, imei: String,
} }
// ---------------------------------------------------------------------------
// H5 登录
// ---------------------------------------------------------------------------
/// POST /api/h5/auth/login — H5 用户登录(复用 users 表)
pub async fn h5_login(
State(state): State<AppState>,
Json(input): Json<H5LoginInput>,
) -> Result<Json<Value>, AppError> {
let pool = &state.mysql;
if input.phone.is_empty() || input.password.is_empty() {
return Err(AppError::BadRequest("手机号和密码不能为空".into()));
}
// 查询用户
let user_row: Option<(i64, Option<String>, String, String, i32)> = sqlx::query_as(
"SELECT id, name, phone, password, status FROM users WHERE phone = ?",
)
.bind(&input.phone)
.fetch_optional(pool)
.await?;
let (id, name, phone, stored_password, status) =
user_row.ok_or_else(|| AppError::Unauthorized("手机号或密码错误".into()))?;
if status == 0 {
return Err(AppError::Forbidden("账户已被禁用".into()));
}
let valid = auth::verify_password(&input.password, &stored_password)
.map_err(|_| AppError::Internal("密码验证失败".into()))?;
if !valid {
return Err(AppError::Unauthorized("手机号或密码错误".into()));
}
let token = auth::generate_token(id, &phone)?;
Ok(Json(json!({
"code": 0,
"data": {
"token": token,
"user": {
"id": id,
"phone": phone,
"name": name,
}
},
"message": "登录成功"
})))
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 仪表盘 // 仪表盘
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -156,15 +110,19 @@ pub async fn h5_login(
pub async fn get_dashboard( pub async fn get_dashboard(
user: CurrentUser, user: CurrentUser,
State(state): State<AppState>, State(state): State<AppState>,
Query(query): Query<DashboardQuery>,
) -> Result<Json<Value>, AppError> { ) -> Result<Json<Value>, AppError> {
let pool = &state.mysql; let pool = &state.mysql;
// 根据角色确定组织过滤条件 // 根据角色确定组织过滤条件
let org_id = user.organization_id; // 超级管理员可通过 org_id 参数切换组织
let is_super = user.role_level >= 2; let is_super = user.role_level >= 2 && query.org_id.is_none();
let org_id = query.org_id.or(user.organization_id);
// 统计在线柜子数(带组织隔离) // 统计在线柜子数(带组织隔离)
let online_cabinets = count_online_cabinets(pool, is_super, org_id).await?; let online_cabinets = count_online_cabinets(pool, is_super, org_id).await?;
// 统计总通道数(带组织隔离)
let total_channels = count_total_compartments(pool, is_super, org_id).await?;
// 统计充电中仓体数(带组织隔离) // 统计充电中仓体数(带组织隔离)
let charging_count = count_compartments_by_status(pool, is_super, org_id, 1).await?; let charging_count = count_compartments_by_status(pool, is_super, org_id, 1).await?;
@ -175,6 +133,12 @@ pub async fn get_dashboard(
// 统计故障仓体数(带组织隔离) // 统计故障仓体数(带组织隔离)
let fault_channels = count_compartments_by_status(pool, is_super, org_id, 2).await?; let fault_channels = count_compartments_by_status(pool, is_super, org_id, 2).await?;
// 统计充满仓体数(status=2,带组织隔离)
let full_count = count_compartments_by_status(pool, is_super, org_id, 2).await?;
// 统计故障数(status=3,带组织隔离)
let fault_count = count_compartments_by_status(pool, is_super, org_id, 3).await?;
// 今日充电次数(带组织隔离) // 今日充电次数(带组织隔离)
let today_charge_count = count_today_charges(pool, is_super, org_id).await?; let today_charge_count = count_today_charges(pool, is_super, org_id).await?;
@ -194,9 +158,12 @@ pub async fn get_dashboard(
"code": 0, "code": 0,
"data": { "data": {
"online_cabinets": online_cabinets, "online_cabinets": online_cabinets,
"total_channels": total_channels,
"charging_count": charging_count, "charging_count": charging_count,
"idle_channels": idle_channels, "idle_channels": idle_channels,
"full_count": full_count,
"fault_channels": fault_channels, "fault_channels": fault_channels,
"fault_count": fault_count,
"today_charge_count": today_charge_count, "today_charge_count": today_charge_count,
"today_energy_kwh": today_energy, "today_energy_kwh": today_energy,
"today_charge_hours": today_charge_hours, "today_charge_hours": today_charge_hours,
@ -233,6 +200,39 @@ async fn count_online_cabinets(
Ok(count) Ok(count)
} }
/// 统计总仓体数(带组织隔离)
async fn count_total_compartments(
pool: &sqlx::MySqlPool,
is_super: bool,
org_id: Option<i64>,
) -> Result<i64, AppError> {
if !is_super && org_id.is_none() {
return Ok(0);
}
let count: i64 = if is_super {
sqlx::query_scalar(
r#"SELECT COUNT(*) FROM compartments comp
JOIN cabin_boards cb ON comp.cabin_board_id = cb.id
JOIN cabinets c ON cb.cabinet_id = c.id
WHERE c.status = 1"#,
)
.fetch_one(pool)
.await?
} else {
sqlx::query_scalar(
r#"SELECT COUNT(*) FROM compartments comp
JOIN cabin_boards cb ON comp.cabin_board_id = cb.id
JOIN cabinets c ON cb.cabinet_id = c.id
JOIN projects p ON c.project_id = p.id
WHERE c.status = 1 AND p.organization_id = ?"#,
)
.bind(org_id.unwrap())
.fetch_one(pool)
.await?
};
Ok(count)
}
/// 按状态统计仓体数(带组织隔离,通过 cabin_boards → cabinets → projects 链路) /// 按状态统计仓体数(带组织隔离,通过 cabin_boards → cabinets → projects 链路)
async fn count_compartments_by_status( async fn count_compartments_by_status(
pool: &sqlx::MySqlPool, pool: &sqlx::MySqlPool,
@ -304,13 +304,13 @@ async fn sum_today_energy(
} }
let energy: Option<i64> = if is_super { let energy: Option<i64> = if is_super {
sqlx::query_scalar( sqlx::query_scalar(
"SELECT COALESCE(CAST(SUM(energy) AS UNSIGNED), 0) FROM charge_records WHERE DATE(start_time) = CURDATE()", "SELECT CAST(COALESCE(SUM(energy), 0) AS SIGNED) FROM charge_records WHERE DATE(start_time) = CURDATE()",
) )
.fetch_one(pool) .fetch_one(pool)
.await? .await?
} else { } else {
sqlx::query_scalar( sqlx::query_scalar(
r#"SELECT COALESCE(CAST(SUM(cr.energy) AS UNSIGNED), 0) FROM charge_records cr r#"SELECT CAST(COALESCE(SUM(cr.energy), 0) AS SIGNED) FROM charge_records cr
INNER JOIN cabinets cab ON cr.cabinet_id = cab.id INNER JOIN cabinets cab ON cr.cabinet_id = cab.id
INNER JOIN projects p ON cab.project_id = p.id INNER JOIN projects p ON cab.project_id = p.id
WHERE DATE(cr.start_time) = CURDATE() AND p.organization_id = ?"#, WHERE DATE(cr.start_time) = CURDATE() AND p.organization_id = ?"#,
@ -334,18 +334,18 @@ async fn sum_today_charge_hours(
// 计算总秒数(未结束的记录用 NOW() 代替 end_time) // 计算总秒数(未结束的记录用 NOW() 代替 end_time)
let total_seconds: Option<i64> = if is_super { let total_seconds: Option<i64> = if is_super {
sqlx::query_scalar( sqlx::query_scalar(
r#"SELECT COALESCE(SUM( r#"SELECT CAST(COALESCE(SUM(
TIMESTAMPDIFF(SECOND, start_time, COALESCE(end_time, NOW())) TIMESTAMPDIFF(SECOND, start_time, COALESCE(end_time, NOW()))
), 0) FROM charge_records ), 0) AS SIGNED) FROM charge_records
WHERE DATE(start_time) = CURDATE()"#, WHERE DATE(start_time) = CURDATE()"#,
) )
.fetch_one(pool) .fetch_one(pool)
.await? .await?
} else { } else {
sqlx::query_scalar( sqlx::query_scalar(
r#"SELECT COALESCE(SUM( r#"SELECT CAST(COALESCE(SUM(
TIMESTAMPDIFF(SECOND, cr.start_time, COALESCE(cr.end_time, NOW())) TIMESTAMPDIFF(SECOND, cr.start_time, COALESCE(cr.end_time, NOW()))
), 0) FROM charge_records cr ), 0) AS SIGNED) FROM charge_records cr
INNER JOIN cabinets cab ON cr.cabinet_id = cab.id INNER JOIN cabinets cab ON cr.cabinet_id = cab.id
INNER JOIN projects p ON cab.project_id = p.id INNER JOIN projects p ON cab.project_id = p.id
WHERE DATE(cr.start_time) = CURDATE() AND p.organization_id = ?"#, WHERE DATE(cr.start_time) = CURDATE() AND p.organization_id = ?"#,
@ -483,44 +483,6 @@ async fn fetch_project_summaries(
Ok(result) Ok(result)
} }
// ---------------------------------------------------------------------------
// 项目列表
// ---------------------------------------------------------------------------
/// GET /api/h5/projects — H5 项目列表(简要)
pub async fn get_projects(
user: CurrentUser,
State(state): State<AppState>,
) -> Result<Json<Value>, AppError> {
let pool = &state.mysql;
let projects: Vec<ProjectRow> = if user.role_level >= 2 {
sqlx::query_as::<_, ProjectRow>("SELECT id, name FROM projects ORDER BY id")
.fetch_all(pool)
.await?
} else if let Some(org_id) = user.organization_id {
sqlx::query_as::<_, ProjectRow>(
"SELECT id, name FROM projects WHERE organization_id = ? ORDER BY id",
)
.bind(org_id)
.fetch_all(pool)
.await?
} else {
vec![]
};
let list: Vec<Value> = projects
.into_iter()
.map(|p| json!({ "id": p.id, "name": p.name }))
.collect();
Ok(Json(json!({
"code": 0,
"data": list,
"message": "ok"
})))
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 设备列表 // 设备列表
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -540,16 +502,28 @@ pub async fn get_cabinets(
// 按项目过滤(同时验证组织权限) // 按项目过滤(同时验证组织权限)
if user.role_level >= 2 { if user.role_level >= 2 {
sqlx::query_as::<_, H5CabinetRow>( sqlx::query_as::<_, H5CabinetRow>(
"SELECT id, abstract_id, name, status, project_id FROM cabinets WHERE project_id = ?", r#"SELECT c.id, c.abstract_id, c.name, c.status, c.imei, c.project_id,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id) as channel_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 0) as idle_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 1) as charging_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 2) as full_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 3) as fault_count
FROM cabinets c WHERE c.project_id = ?"#,
) )
.bind(pid) .bind(pid)
.fetch_all(pool) .fetch_all(pool)
.await? .await?
} else if let Some(org_id) = user.organization_id { } else if let Some(org_id) = user.organization_id {
sqlx::query_as::<_, H5CabinetRow>( sqlx::query_as::<_, H5CabinetRow>(
r#"SELECT id, abstract_id, name, status, project_id FROM cabinets cab r#"SELECT c.id, c.abstract_id, c.name, c.status, c.imei, c.project_id,
INNER JOIN projects p ON cab.project_id = p.id (SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id) as channel_count,
WHERE cab.project_id = ? AND p.organization_id = ?"#, (SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 0) as idle_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 1) as charging_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 2) as full_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 3) as fault_count
FROM cabinets c
INNER JOIN projects p ON c.project_id = p.id
WHERE c.project_id = ? AND p.organization_id = ?"#,
) )
.bind(pid) .bind(pid)
.bind(org_id) .bind(org_id)
@ -559,17 +533,46 @@ pub async fn get_cabinets(
vec![] vec![]
} }
} else if user.role_level >= 2 { } else if user.role_level >= 2 {
// 总管理员看全部 // 总管理员:按组织过滤或看全部
if let Some(oid) = query.org_id {
sqlx::query_as::<_, H5CabinetRow>( sqlx::query_as::<_, H5CabinetRow>(
"SELECT id, abstract_id, name, status, project_id FROM cabinets", r#"SELECT c.id, c.abstract_id, c.name, c.status, c.imei, c.project_id,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id) as channel_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 0) as idle_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 1) as charging_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 2) as full_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 3) as fault_count
FROM cabinets c
INNER JOIN projects p ON c.project_id = p.id
WHERE p.organization_id = ?"#,
)
.bind(oid)
.fetch_all(pool)
.await?
} else {
sqlx::query_as::<_, H5CabinetRow>(
r#"SELECT c.id, c.abstract_id, c.name, c.status, c.imei, c.project_id,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id) as channel_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 0) as idle_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 1) as charging_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 2) as full_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 3) as fault_count
FROM cabinets c"#,
) )
.fetch_all(pool) .fetch_all(pool)
.await? .await?
}
} else if let Some(org_id) = user.organization_id { } else if let Some(org_id) = user.organization_id {
// 企业管理员只看本组织 // 企业管理员只看本组织
sqlx::query_as::<_, H5CabinetRow>( sqlx::query_as::<_, H5CabinetRow>(
r#"SELECT id, abstract_id, name, status, project_id FROM cabinets cab r#"SELECT c.id, c.abstract_id, c.name, c.status, c.imei, c.project_id,
INNER JOIN projects p ON cab.project_id = p.id (SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id) as channel_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 0) as idle_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 1) as charging_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 2) as full_count,
(SELECT COUNT(*) FROM compartments comp JOIN cabin_boards cb ON comp.cabin_board_id = cb.id WHERE cb.cabinet_id = c.id AND comp.status = 3) as fault_count
FROM cabinets c
INNER JOIN projects p ON c.project_id = p.id
WHERE p.organization_id = ?"#, WHERE p.organization_id = ?"#,
) )
.bind(org_id) .bind(org_id)
@ -579,15 +582,44 @@ pub async fn get_cabinets(
vec![] vec![]
}; };
// 从Redis批量查RSSI
let mut rssi_map: std::collections::HashMap<String, Option<i32>> = std::collections::HashMap::new();
if let Ok(client) = redis::Client::open(state.redis_url.as_str()) {
if let Ok(mut conn) = client.get_async_connection().await {
let mut pipe = redis::pipe();
for r in &rows {
if let Some(ref imei) = r.imei {
pipe.cmd("HGET").arg(&format!("device:{imei}")).arg("rssi");
}
}
if !rows.is_empty() {
let results: Vec<Option<String>> = pipe.query_async(&mut conn).await.unwrap_or_default();
for (i, r) in rows.iter().enumerate() {
if r.imei.is_some() {
let val = results.get(i).and_then(|v| v.as_ref()).and_then(|v| v.parse::<i32>().ok());
rssi_map.insert(r.imei.clone().unwrap_or_default(), val);
}
}
}
}
}
let list: Vec<Value> = rows let list: Vec<Value> = rows
.into_iter() .into_iter()
.map(|r| { .map(|r| {
let rssi = r.imei.as_ref().and_then(|imei| rssi_map.get(imei).copied()).flatten();
json!({ json!({
"id": r.id, "id": r.id,
"abstract_id": r.abstract_id, "abstract_id": r.abstract_id,
"name": r.name, "name": r.name,
"status": r.status, "status": r.status,
"project_id": r.project_id, "project_id": r.project_id,
"channel_count": r.channel_count,
"idle_count": r.idle_count,
"charging_count": r.charging_count,
"full_count": r.full_count,
"fault_count": r.fault_count,
"rssi": rssi,
}) })
}) })
.collect(); .collect();
@ -600,100 +632,22 @@ pub async fn get_cabinets(
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 设备详情 // 设备扫码(免认证)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// GET /api/h5/cabinets/:id — H5 设备详情(含仓控板+仓体,带权限校验)
pub async fn get_cabinet_detail(
user: CurrentUser,
State(state): State<AppState>,
Path(id): Path<i64>,
) -> Result<Json<Value>, AppError> {
let pool = &state.mysql;
// 权限校验
auth::check_permission(&user, "device:view")?;
// 组织隔离:验证用户是否有权访问该设备
verify_cabinet_access(&user, id, pool).await?;
// 查询柜子基本信息
let cab: Option<(i64, String, String, Option<String>, i8)> = sqlx::query_as(
"SELECT id, abstract_id, imei, name, status FROM cabinets WHERE id = ?",
)
.bind(id)
.fetch_optional(pool)
.await?;
let (cab_id, abstract_id, imei, name, status) =
cab.ok_or_else(|| AppError::NotFound("设备不存在".into()))?;
// 查询仓控板
let boards: Vec<H5CabinBoardRow> = sqlx::query_as::<_, H5CabinBoardRow>(
"SELECT id, board_id, status FROM cabin_boards WHERE cabinet_id = ? ORDER BY board_id",
)
.bind(cab_id)
.fetch_all(pool)
.await?;
// 查询每个仓控板下的仓体
let mut cabin_boards = Vec::with_capacity(boards.len());
for board in &boards {
let comps: Vec<H5CompartmentRow> = sqlx::query_as::<_, H5CompartmentRow>(
"SELECT id, channel_id, status FROM compartments WHERE cabin_board_id = ? ORDER BY channel_id",
)
.bind(board.id)
.fetch_all(pool)
.await?;
let compartments: Vec<Value> = comps
.into_iter()
.map(|c| {
json!({
"id": c.id,
"channel_id": c.channel_id,
"status": c.status,
})
})
.collect();
cabin_boards.push(json!({
"id": board.id,
"board_id": board.board_id,
"status": board.status,
"compartments": compartments,
}));
}
Ok(Json(json!({
"code": 0,
"data": {
"id": cab_id,
"abstract_id": abstract_id,
"imei": imei,
"name": name,
"status": status,
"cabin_boards": cabin_boards,
},
"message": "ok"
})))
}
/// GET /api/h5/cabinets/abstract/:abstract_id — 通过抽象ID查设备(扫码入口,免认证)
pub async fn get_cabinet_by_abstract_id( pub async fn get_cabinet_by_abstract_id(
State(state): State<AppState>, State(state): State<AppState>,
Path(abstract_id): Path<String>, Path(abstract_id): Path<String>,
) -> Result<Json<Value>, AppError> { ) -> Result<Json<Value>, AppError> {
let pool = &state.mysql; let pool = &state.mysql;
let cab: Option<(i64, String, String, Option<String>, i8)> = sqlx::query_as( let cab: Option<(i64, String, String, Option<String>, Option<String>, i8)> = sqlx::query_as(
"SELECT id, abstract_id, imei, name, status FROM cabinets WHERE abstract_id = ?", "SELECT id, abstract_id, imei, iccid, name, status FROM cabinets WHERE abstract_id = ?",
) )
.bind(&abstract_id) .bind(&abstract_id)
.fetch_optional(pool) .fetch_optional(pool)
.await?; .await?;
let (cab_id, abs_id, imei, name, status) = let (cab_id, abs_id, imei, iccid, name, status) =
cab.ok_or_else(|| AppError::NotFound("设备不存在".into()))?; cab.ok_or_else(|| AppError::NotFound("设备不存在".into()))?;
// 查询仓控板+仓体 // 查询仓控板+仓体
@ -732,6 +686,7 @@ pub async fn get_cabinet_by_abstract_id(
"id": cab_id, "id": cab_id,
"abstract_id": abs_id, "abstract_id": abs_id,
"imei": imei, "imei": imei,
"iccid": iccid,
"name": name, "name": name,
"status": status, "status": status,
"cabin_boards": cabin_boards, "cabin_boards": cabin_boards,
@ -779,17 +734,13 @@ pub async fn get_compartment_detail(
} }
// 从 Redis 获取实时数据 // 从 Redis 获取实时数据
let redis_key = format!("compartment:status:{}", comp_id); let realtime: Value = if let Ok(client) = redis::Client::open(state.redis_url.as_str()) {
let mut conn = state.redis.clone(); if let Ok(mut conn) = client.get_async_connection().await {
let realtime_data: Option<String> = redis::cmd("GET") let key = format!("compartment:status:{}", comp_id);
.arg(&redis_key) let val: Option<String> = redis::cmd("GET").arg(&key).query_async(&mut conn).await.unwrap_or(None);
.query_async(&mut conn) val.and_then(|s| serde_json::from_str(&s).ok()).unwrap_or(json!({}))
.await } else { json!({}) }
.unwrap_or(None); } else { json!({}) };
let realtime: Value = realtime_data
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or(json!({}));
// 查询最近充电记录 // 查询最近充电记录
let records: Vec<ChargeRecordBriefRow> = sqlx::query_as::<_, ChargeRecordBriefRow>( let records: Vec<ChargeRecordBriefRow> = sqlx::query_as::<_, ChargeRecordBriefRow>(
@ -837,6 +788,48 @@ pub async fn get_compartment_detail(
}))) })))
} }
// ---------------------------------------------------------------------------
// 扫码验证
// ---------------------------------------------------------------------------
/// GET /api/h5/scan/:abstract_id — 扫码验证设备访问权限
///
/// 根据二维码中的抽象ID查找设备,验证当前用户是否有权访问。
/// 有权限返回设备ID和基本信息,无权限返回403。
pub async fn scan_cabinet(
user: CurrentUser,
State(state): State<AppState>,
Path(abstract_id): Path<String>,
) -> Result<Json<Value>, AppError> {
let pool = &state.mysql;
// 查找设备
let cab: Option<(i64, String, String, Option<String>, i8)> = sqlx::query_as(
"SELECT id, abstract_id, imei, name, status FROM cabinets WHERE abstract_id = ?",
)
.bind(&abstract_id)
.fetch_optional(pool)
.await?;
let (cab_id, abs_id, imei, name, status) =
cab.ok_or_else(|| AppError::NotFound("设备不存在".into()))?;
// 验证用户是否有权限访问该设备
verify_cabinet_access(&user, cab_id, pool).await?;
Ok(Json(json!({
"code": 0,
"data": {
"id": cab_id,
"abstract_id": abs_id,
"imei": imei,
"name": name,
"status": status,
},
"message": "ok"
})))
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 充电控制 // 充电控制
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -94,24 +94,22 @@ pub fn build(state: AppState) -> Router {
.route("/api/users/{id}/status", put(users::set_user_status)) .route("/api/users/{id}/status", put(users::set_user_status))
// 操作日志 // 操作日志
.route("/api/operation-logs", get(operation_logs::list_operation_logs)) .route("/api/operation-logs", get(operation_logs::list_operation_logs))
// H5 用户端接口(需认证) // H5 用户端接口(与 PC 共用同一套 API 路由)
.route("/api/h5/dashboard", get(h5::get_dashboard)) .route("/api/dashboard", get(h5::get_dashboard))
.route("/api/h5/projects", get(h5::get_projects)) .route("/api/cabinets", get(h5::get_cabinets))
.route("/api/h5/cabinets", get(h5::get_cabinets)) .route("/api/compartments/{id}", get(h5::get_compartment_detail))
.route("/api/h5/cabinets/{id}", get(h5::get_cabinet_detail)) .route("/api/charge/start", post(h5::start_charge))
.route("/api/h5/compartments/{id}", get(h5::get_compartment_detail)) .route("/api/charge/stop", post(h5::stop_charge))
.route("/api/h5/charge/start", post(h5::start_charge)) .route("/api/door/open", post(h5::open_door))
.route("/api/h5/charge/stop", post(h5::stop_charge)) .route("/api/cabinets/scan/{abstract_id}", get(h5::scan_cabinet))
.route("/api/h5/door/open", post(h5::open_door))
// 认证中间件层 // 认证中间件层
.layer(middleware::from_fn_with_state(state.clone(), auth_middleware)); .layer(middleware::from_fn_with_state(state.clone(), auth_middleware));
Router::<AppState>::new() Router::<AppState>::new()
.route("/api/health", get(health::health_check)) .route("/api/health", get(health::health_check))
.route("/api/auth/login", post(auth::login)) .route("/api/auth/login", post(auth::login))
.route("/api/h5/auth/login", post(h5::h5_login))
// 扫码入口(免认证) // 扫码入口(免认证)
.route("/api/h5/cabinets/abstract/{abstract_id}", get(h5::get_cabinet_by_abstract_id)) .route("/api/cabinets/abstract/{abstract_id}", get(h5::get_cabinet_by_abstract_id))
.merge(protected) .merge(protected)
.with_state(state) .with_state(state)
} }

View File

@ -148,8 +148,15 @@ pub struct CabinetDetail {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub abstract_id: Option<String>, pub abstract_id: Option<String>,
pub imei: String, pub imei: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub iccid: Option<String>,
pub name: Option<String>, pub name: Option<String>,
pub status: i8, pub status: i8,
pub rssi: Option<i32>,
pub pow_fail_dc: Option<bool>,
pub pow_fail_ac: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
pub cabin_boards: Vec<CabinBoardWithCompartments>, pub cabin_boards: Vec<CabinBoardWithCompartments>,
} }

View File

@ -37,6 +37,7 @@ struct UserRow {
organization_id: Option<i64>, organization_id: Option<i64>,
status: i32, status: i32,
created_at: NaiveDateTime, created_at: NaiveDateTime,
updated_at: NaiveDateTime,
} }
/// 创建用户请求体 /// 创建用户请求体
@ -169,7 +170,7 @@ pub async fn list_users(
// 查询用户列表 // 查询用户列表
let rows: Vec<UserRow> = if has_keyword { let rows: Vec<UserRow> = if has_keyword {
let mut q = sqlx::query_as::<_, UserRow>( let mut q = sqlx::query_as::<_, UserRow>(
r#"SELECT u.id, u.phone, u.name, u.role, u.organization_id, u.status, u.created_at r#"SELECT u.id, u.phone, u.name, u.role, u.organization_id, u.status, u.created_at, u.updated_at
FROM users u FROM users u
WHERE u.phone LIKE ? OR u.name LIKE ? WHERE u.phone LIKE ? OR u.name LIKE ?
ORDER BY u.id DESC ORDER BY u.id DESC
@ -181,7 +182,7 @@ pub async fn list_users(
.bind(offset); .bind(offset);
if let Some(org_id) = org_filter { if let Some(org_id) = org_filter {
q = sqlx::query_as::<_, UserRow>( q = sqlx::query_as::<_, UserRow>(
r#"SELECT u.id, u.phone, u.name, u.role, u.organization_id, u.status, u.created_at r#"SELECT u.id, u.phone, u.name, u.role, u.organization_id, u.status, u.created_at, u.updated_at
FROM users u FROM users u
WHERE (u.phone LIKE ? OR u.name LIKE ?) AND u.organization_id = ? WHERE (u.phone LIKE ? OR u.name LIKE ?) AND u.organization_id = ?
ORDER BY u.id DESC ORDER BY u.id DESC
@ -196,7 +197,7 @@ pub async fn list_users(
q.fetch_all(pool).await? q.fetch_all(pool).await?
} else if let Some(org_id) = org_filter { } else if let Some(org_id) = org_filter {
sqlx::query_as::<_, UserRow>( sqlx::query_as::<_, UserRow>(
r#"SELECT u.id, u.phone, u.name, u.role, u.organization_id, u.status, u.created_at r#"SELECT u.id, u.phone, u.name, u.role, u.organization_id, u.status, u.created_at, u.updated_at
FROM users u FROM users u
WHERE u.organization_id = ? WHERE u.organization_id = ?
ORDER BY u.id DESC ORDER BY u.id DESC
@ -209,7 +210,7 @@ pub async fn list_users(
.await? .await?
} else { } else {
sqlx::query_as::<_, UserRow>( sqlx::query_as::<_, UserRow>(
r#"SELECT u.id, u.phone, u.name, u.role, u.organization_id, u.status, u.created_at r#"SELECT u.id, u.phone, u.name, u.role, u.organization_id, u.status, u.created_at, u.updated_at
FROM users u FROM users u
ORDER BY u.id DESC ORDER BY u.id DESC
LIMIT ? OFFSET ?"#, LIMIT ? OFFSET ?"#,
@ -256,6 +257,7 @@ pub async fn list_users(
"organization_name": org_name, "organization_name": org_name,
"status": r.status, "status": r.status,
"created_at": r.created_at.to_string(), "created_at": r.created_at.to_string(),
"updated_at": r.updated_at.to_string(),
}) })
}) })
.collect(); .collect();

View File

@ -0,0 +1,5 @@
# Generation Info
- **Source:** `sources/pinia`
- **Git SHA:** `55dbfc5c20d4461748996aa74d8c0913e89fb98e`
- **Generated:** 2026-01-28

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025-PRESENT Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,59 @@
---
name: pinia
description: Pinia official Vue state management library, type-safe and extensible. Use when defining stores, working with state/getters/actions, or implementing store patterns in Vue apps.
metadata:
author: Anthony Fu
version: "2026.1.28"
source: Generated from https://github.com/vuejs/pinia, scripts located at https://github.com/antfu/skills
---
# Pinia
Pinia is the official state management library for Vue, designed to be intuitive and type-safe. It supports both Options API and Composition API styles, with first-class TypeScript support and devtools integration.
> The skill is based on Pinia v3.0.4, generated at 2026-01-28.
## Core References
| Topic | Description | Reference |
|-------|-------------|-----------|
| Stores | Defining stores, state, getters, actions, storeToRefs, subscriptions | [core-stores](references/core-stores.md) |
## Features
### Extensibility
| Topic | Description | Reference |
|-------|-------------|-----------|
| Plugins | Extend stores with custom properties, state, and behavior | [features-plugins](references/features-plugins.md) |
### Composability
| Topic | Description | Reference |
|-------|-------------|-----------|
| Composables | Using Vue composables within stores (VueUse, etc.) | [features-composables](references/features-composables.md) |
| Composing Stores | Store-to-store communication, avoiding circular dependencies | [features-composing-stores](references/features-composing-stores.md) |
## Best Practices
| Topic | Description | Reference |
|-------|-------------|-----------|
| Testing | Unit testing with @pinia/testing, mocking, stubbing | [best-practices-testing](references/best-practices-testing.md) |
| Outside Components | Using stores in navigation guards, plugins, middlewares | [best-practices-outside-component](references/best-practices-outside-component.md) |
## Advanced
| Topic | Description | Reference |
|-------|-------------|-----------|
| SSR | Server-side rendering, state hydration | [advanced-ssr](references/advanced-ssr.md) |
| Nuxt | Nuxt integration, auto-imports, SSR best practices | [advanced-nuxt](references/advanced-nuxt.md) |
| HMR | Hot module replacement for development | [advanced-hmr](references/advanced-hmr.md) |
## Key Recommendations
- **Prefer Setup Stores** for complex logic, composables, and watchers
- **Use `storeToRefs()`** when destructuring state/getters to preserve reactivity
- **Actions can be destructured directly** - they're bound to the store
- **Call stores inside functions** not at module scope, especially for SSR
- **Add HMR support** to each store for better development experience
- **Use `@pinia/testing`** for component tests with mocked stores

View File

@ -0,0 +1,5 @@
# Sync Info
- **Source:** `vendor/antfu/skills/pinia`
- **Git SHA:** `4086f59fa12d308fb05592ca8ce1704fbb940aa3`
- **Synced:** 2026-01-30

View File

@ -0,0 +1,61 @@
---
name: hot-module-replacement
description: Enable HMR to preserve store state during development
---
# Hot Module Replacement (HMR)
Pinia supports HMR to edit stores without page reload, preserving existing state.
## Setup
Add this snippet after each store definition:
```ts
import { defineStore, acceptHMRUpdate } from 'pinia'
export const useAuth = defineStore('auth', {
// store options...
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot))
}
```
## Setup Store Example
```ts
import { defineStore, acceptHMRUpdate } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
}
```
## Bundler Support
- **Vite:** Officially supported via `import.meta.hot`
- **Webpack:** Uses `import.meta.webpackHot`
- Any bundler implementing the `import.meta.hot` spec should work
## Nuxt
With `@pinia/nuxt`, `acceptHMRUpdate` is auto-imported but you still need to add the HMR snippet manually.
## Benefits
- Edit store logic without losing state
- Add/remove state, actions, and getters on the fly
- Faster development iteration
<!--
Source references:
- https://pinia.vuejs.org/cookbook/hot-module-replacement.html
-->

View File

@ -0,0 +1,119 @@
---
name: nuxt-integration
description: Using Pinia with Nuxt - auto-imports, SSR, and best practices
---
# Nuxt Integration
Pinia works seamlessly with Nuxt 3/4, handling SSR, serialization, and XSS protection automatically.
## Installation
```bash
npx nuxi@latest module add pinia
```
This installs both `@pinia/nuxt` and `pinia`. If `pinia` isn't installed, add it manually.
> **npm users:** If you get `ERESOLVE unable to resolve dependency tree`, add to `package.json`:
> ```json
> "overrides": { "vue": "latest" }
> ```
## Configuration
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
```
## Auto Imports
These are automatically available:
- `usePinia()` - get pinia instance
- `defineStore()` - define stores
- `storeToRefs()` - extract reactive refs
- `acceptHMRUpdate()` - HMR support
**All stores in `app/stores/` (Nuxt 4) or `stores/` are auto-imported.**
### Custom Store Directories
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
pinia: {
storesDirs: ['./stores/**', './custom-folder/stores/**'],
},
})
```
## Fetching Data in Pages
Use `callOnce()` for SSR-friendly data fetching:
```vue
<script setup>
const store = useStore()
// Run once, data persists across navigations
await callOnce('user', () => store.fetchUser())
</script>
```
### Refetch on Navigation
```vue
<script setup>
const store = useStore()
// Refetch on every navigation (like useFetch)
await callOnce('user', () => store.fetchUser(), { mode: 'navigation' })
</script>
```
## Using Stores Outside Components
In navigation guards, middlewares, or other stores, pass the `pinia` instance:
```ts
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to) => {
const nuxtApp = useNuxtApp()
const store = useStore(nuxtApp.$pinia)
if (to.meta.requiresAuth && !store.isLoggedIn) {
return navigateTo('/login')
}
})
```
Most of the time, you don't need this - just use stores in components or other injection-aware contexts.
## Pinia Plugins with Nuxt
Create a Nuxt plugin:
```ts
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`)
})
return { creationTime: new Date() }
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
})
```
<!--
Source references:
- https://pinia.vuejs.org/ssr/nuxt.html
-->

View File

@ -0,0 +1,121 @@
---
name: server-side-rendering
description: SSR setup, state hydration, and avoiding cross-request state pollution
---
# Server Side Rendering (SSR)
Pinia works with SSR when stores are called at the top of `setup`, getters, or actions.
> **Using Nuxt?** See the [Nuxt integration](advanced-nuxt.md) instead.
## Basic Usage
```vue
<script setup>
// ✅ Works - pinia knows the app context in setup
const main = useMainStore()
</script>
```
## Using Store Outside setup()
Pass the `pinia` instance explicitly:
```ts
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
router.beforeEach((to) => {
// ✅ Pass pinia for correct SSR context
const main = useMainStore(pinia)
if (to.meta.requiresAuth && !main.isLoggedIn) {
return '/login'
}
})
```
## serverPrefetch()
Access pinia via `this.$pinia`:
```ts
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
return store.fetchData()
},
}
```
## onServerPrefetch()
Works normally:
```vue
<script setup>
const store = useStore()
onServerPrefetch(async () => {
await store.fetchData()
})
</script>
```
## State Hydration
Serialize state on server and hydrate on client.
### Server Side
Use [devalue](https://github.com/Rich-Harris/devalue) for XSS-safe serialization:
```ts
import devalue from 'devalue'
import { createPinia } from 'pinia'
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
// After rendering, state is available
const serializedState = devalue(pinia.state.value)
// Inject into HTML as global variable
```
### Client Side
Hydrate before any `useStore()` call:
```ts
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// Hydrate from serialized state (e.g., from window.__pinia)
if (typeof window !== 'undefined') {
pinia.state.value = JSON.parse(window.__pinia)
}
```
## SSR Examples
- [Vitesse template](https://github.com/antfu/vitesse/blob/main/src/modules/pinia.ts)
- [vite-plugin-ssr](https://vite-plugin-ssr.com/pinia)
## Key Points
1. Call stores inside functions, not at module scope
2. Pass `pinia` instance when using stores outside components in SSR
3. Hydrate state before calling any `useStore()`
4. Use `devalue` or similar for safe serialization
5. Avoid cross-request state pollution by creating fresh pinia per request
<!--
Source references:
- https://pinia.vuejs.org/ssr/
-->

View File

@ -0,0 +1,115 @@
---
name: using-stores-outside-components
description: Correctly using stores in navigation guards, plugins, and other non-component contexts
---
# Using Stores Outside Components
Stores need the `pinia` instance, which is automatically injected in components. Outside components, you may need to provide it manually.
## Single Page Applications
Call stores **after** pinia is installed:
```ts
import { useUserStore } from '@/stores/user'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
// ❌ Fails - pinia not created yet
const userStore = useUserStore()
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
// ✅ Works - pinia is active
const userStore = useUserStore()
```
## Navigation Guards
**Wrong:** Call at module level
```ts
import { createRouter } from 'vue-router'
const router = createRouter({ /* ... */ })
// ❌ May fail depending on import order
const store = useUserStore()
router.beforeEach((to) => {
if (store.isLoggedIn) { /* ... */ }
})
```
**Correct:** Call inside the guard
```ts
router.beforeEach((to) => {
// ✅ Called after pinia is installed
const store = useUserStore()
if (to.meta.requiresAuth && !store.isLoggedIn) {
return '/login'
}
})
```
## SSR Applications
Always pass the `pinia` instance to `useStore()`:
```ts
const pinia = createPinia()
const app = createApp(App)
app.use(router)
app.use(pinia)
router.beforeEach((to) => {
// ✅ Pass pinia instance
const main = useMainStore(pinia)
if (to.meta.requiresAuth && !main.isLoggedIn) {
return '/login'
}
})
```
## serverPrefetch()
Access pinia via `this.$pinia`:
```ts
export default {
serverPrefetch() {
const store = useStore(this.$pinia)
return store.fetchData()
},
}
```
## onServerPrefetch()
Works normally in `<script setup>`:
```vue
<script setup>
const store = useStore()
onServerPrefetch(async () => {
// ✅ Just works
await store.fetchData()
})
</script>
```
## Key Takeaway
Defer `useStore()` calls to functions that run after pinia is installed, rather than calling at module scope.
<!--
Source references:
- https://pinia.vuejs.org/core-concepts/outside-component-usage.html
-->

View File

@ -0,0 +1,212 @@
---
name: testing
description: Unit testing stores and components with @pinia/testing
---
# Testing Stores
## Unit Testing Stores
Create a fresh pinia instance for each test:
```ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '../src/stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('increments', () => {
const counter = useCounterStore()
expect(counter.n).toBe(0)
counter.increment()
expect(counter.n).toBe(1)
})
})
```
### With Plugins
```ts
import { setActivePinia, createPinia } from 'pinia'
import { createApp } from 'vue'
import { somePlugin } from '../src/stores/plugin'
const app = createApp({})
beforeEach(() => {
const pinia = createPinia().use(somePlugin)
app.use(pinia)
setActivePinia(pinia)
})
```
## Testing Components
Install `@pinia/testing`:
```bash
npm i -D @pinia/testing
```
Use `createTestingPinia()`:
```ts
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { useSomeStore } from '@/stores/myStore'
const wrapper = mount(Counter, {
global: {
plugins: [createTestingPinia()],
},
})
const store = useSomeStore()
// Manipulate state directly
store.name = 'new name'
store.$patch({ name: 'new name' })
// Actions are stubbed by default
store.someAction()
expect(store.someAction).toHaveBeenCalledTimes(1)
```
## Initial State
Set initial state for tests:
```ts
const wrapper = mount(Counter, {
global: {
plugins: [
createTestingPinia({
initialState: {
counter: { n: 20 }, // Store name → initial state
},
}),
],
},
})
```
## Action Stubbing
### Execute Real Actions
```ts
createTestingPinia({ stubActions: false })
```
### Selective Stubbing
```ts
// Only stub specific actions
createTestingPinia({
stubActions: ['increment', 'reset'],
})
// Or use a function
createTestingPinia({
stubActions: (actionName, store) => {
if (actionName.startsWith('set')) return true
return false
},
})
```
### Mock Action Return Values
```ts
import type { Mock } from 'vitest'
// After getting store
store.someAction.mockResolvedValue('mocked value')
```
## Mocking Getters
Getters are writable in tests:
```ts
const pinia = createTestingPinia()
const counter = useCounterStore(pinia)
counter.double = 3 // Override computed value
// Reset to default behavior
counter.double = undefined
counter.double // Now computed normally
```
## Custom Spy Function
If not using Jest/Vitest with globals:
```ts
import { vi } from 'vitest'
createTestingPinia({
createSpy: vi.fn,
})
```
With Sinon:
```ts
import sinon from 'sinon'
createTestingPinia({
createSpy: sinon.spy,
})
```
## Pinia Plugins in Tests
Pass plugins to `createTestingPinia()`:
```ts
import { somePlugin } from '../src/stores/plugin'
createTestingPinia({
stubActions: false,
plugins: [somePlugin],
})
```
**Don't use** `testingPinia.use(MyPlugin)` - pass plugins in options.
## Type-Safe Mocked Store
```ts
import type { Mock } from 'vitest'
import type { Store, StoreDefinition } from 'pinia'
function mockedStore<TStoreDef extends () => unknown>(
useStore: TStoreDef
): TStoreDef extends StoreDefinition<infer Id, infer State, infer Getters, infer Actions>
? Store<Id, State, Record<string, never>, {
[K in keyof Actions]: Actions[K] extends (...args: any[]) => any
? Mock<Actions[K]>
: Actions[K]
}>
: ReturnType<TStoreDef> {
return useStore() as any
}
// Usage
const store = mockedStore(useSomeStore)
store.someAction.mockResolvedValue('value') // Typed!
```
## E2E Tests
No special handling needed - Pinia works normally.
<!--
Source references:
- https://pinia.vuejs.org/cookbook/testing.html
-->

View File

@ -0,0 +1,389 @@
---
name: stores
description: Defining stores, state, getters, and actions in Pinia
---
# Pinia Stores
Stores are defined using `defineStore()` with a unique name. Each store has three core concepts: **state**, **getters**, and **actions**.
## Defining Stores
### Option Stores
Similar to Vue's Options API:
```ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo',
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
```
Think of `state` as `data`, `getters` as `computed`, and `actions` as `methods`.
### Setup Stores (Recommended)
Uses Composition API syntax - more flexible and powerful:
```ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Eduardo')
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, name, doubleCount, increment }
})
```
In Setup Stores: `ref()` → state, `computed()` → getters, `function()` → actions.
**Important:** You must return all state properties for Pinia to track them.
### Using Stores
```vue
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// Access: store.count, store.doubleCount, store.increment()
</script>
```
### Destructuring with storeToRefs
```vue
<script setup>
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// ❌ Breaks reactivity
const { name, doubleCount } = store
// ✅ Preserves reactivity for state/getters
const { name, doubleCount } = storeToRefs(store)
// ✅ Actions can be destructured directly
const { increment } = store
</script>
```
---
## State
State is defined as a function returning the initial state.
### TypeScript
Type inference works automatically. For complex types:
```ts
interface UserInfo {
name: string
age: number
}
export const useUserStore = defineStore('user', {
state: () => ({
userList: [] as UserInfo[],
user: null as UserInfo | null,
}),
})
```
Or use an interface for the return type:
```ts
interface State {
userList: UserInfo[]
user: UserInfo | null
}
export const useUserStore = defineStore('user', {
state: (): State => ({
userList: [],
user: null,
}),
})
```
### Accessing and Modifying
```ts
const store = useStore()
store.count++
```
```vue
<input v-model="store.count" type="number" />
```
### Mutating with $patch
Apply multiple changes at once:
```ts
// Object syntax
store.$patch({
count: store.count + 1,
name: 'DIO',
})
// Function syntax (for complex mutations)
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
```
### Resetting State
Option Stores have built-in `$reset()`. For Setup Stores, implement your own:
```ts
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function $reset() {
count.value = 0
}
return { count, $reset }
})
```
### Subscribing to State Changes
```ts
cartStore.$subscribe((mutation, state) => {
mutation.type // 'direct' | 'patch object' | 'patch function'
mutation.storeId // 'cart'
mutation.payload // patch object (only for 'patch object')
localStorage.setItem('cart', JSON.stringify(state))
})
// Options
cartStore.$subscribe(callback, { flush: 'sync' }) // Immediate
cartStore.$subscribe(callback, { detached: true }) // Keep after unmount
```
---
## Getters
Getters are computed values, equivalent to Vue's `computed()`.
### Basic Getters
```ts
getters: {
doubleCount: (state) => state.count * 2,
}
```
### Accessing Other Getters
Use `this` with explicit return type:
```ts
getters: {
doubleCount: (state) => state.count * 2,
doublePlusOne(): number {
return this.doubleCount + 1
},
},
```
### Getters with Arguments
Return a function (note: loses caching):
```ts
getters: {
getUserById: (state) => {
return (userId: string) => state.users.find((user) => user.id === userId)
},
},
```
Cache within parameterized getters:
```ts
getters: {
getActiveUserById(state) {
const activeUsers = state.users.filter((user) => user.active)
return (userId: string) => activeUsers.find((user) => user.id === userId)
},
},
```
### Accessing Other Stores in Getters
```ts
import { useOtherStore } from './other-store'
getters: {
combined(state) {
const otherStore = useOtherStore()
return state.localData + otherStore.data
},
},
```
---
## Actions
Actions are methods for business logic. Unlike getters, they can be asynchronous.
### Defining Actions
```ts
actions: {
increment() {
this.count++
},
randomizeCounter() {
this.count = Math.round(100 * Math.random())
},
},
```
### Async Actions
```ts
actions: {
async registerUser(login: string, password: string) {
try {
this.userData = await api.post({ login, password })
} catch (error) {
return error
}
},
},
```
### Accessing Other Stores in Actions
```ts
import { useAuthStore } from './auth-store'
actions: {
async fetchUserPreferences() {
const auth = useAuthStore()
if (auth.isAuthenticated) {
this.preferences = await fetchPreferences()
}
},
},
```
**SSR:** Call all `useStore()` before any `await`:
```ts
async orderCart() {
// ✅ Call stores before await
const user = useUserStore()
await apiOrderCart(user.token, this.items)
// ❌ Don't call useStore() after await in SSR
}
```
### Subscribing to Actions
```ts
const unsubscribe = someStore.$onAction(
({ name, store, args, after, onError }) => {
const startTime = Date.now()
console.log(`Start "${name}" with params [${args.join(', ')}]`)
after((result) => {
console.log(`Finished "${name}" after ${Date.now() - startTime}ms`)
})
onError((error) => {
console.warn(`Failed "${name}": ${error}`)
})
}
)
unsubscribe() // Cleanup
```
Keep subscription after component unmount:
```ts
someStore.$onAction(callback, true)
```
---
## Options API Helpers
```ts
import { mapState, mapWritableState, mapActions } from 'pinia'
import { useCounterStore } from '../stores/counter'
export default {
computed: {
// Readonly state/getters
...mapState(useCounterStore, ['count', 'doubleCount']),
// Writable state
...mapWritableState(useCounterStore, ['count']),
},
methods: {
...mapActions(useCounterStore, ['increment']),
},
}
```
---
## Accessing Global Providers in Setup Stores
```ts
import { inject } from 'vue'
import { useRoute } from 'vue-router'
import { defineStore } from 'pinia'
export const useSearchFilters = defineStore('search-filters', () => {
const route = useRoute()
const appProvided = inject('appProvided')
// Don't return these - access them directly in components
return { /* ... */ }
})
```
<!--
Source references:
- https://pinia.vuejs.org/core-concepts/
- https://pinia.vuejs.org/core-concepts/state.html
- https://pinia.vuejs.org/core-concepts/getters.html
- https://pinia.vuejs.org/core-concepts/actions.html
-->

View File

@ -0,0 +1,114 @@
---
name: composables-in-stores
description: Using Vue composables within Pinia stores
---
# Composables in Stores
Pinia stores can leverage Vue composables for reusable stateful logic.
## Option Stores
Call composables inside the `state` property, but only those returning writable refs:
```ts
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: useLocalStorage('pinia/auth/login', 'bob'),
}),
})
```
**Works:** Composables returning `ref()`:
- `useLocalStorage`
- `useAsyncState`
**Doesn't work in Option Stores:**
- Composables exposing functions
- Composables exposing readonly data
## Setup Stores
More flexible - can use almost any composable:
```ts
import { defineStore } from 'pinia'
import { useMediaControls } from '@vueuse/core'
import { ref } from 'vue'
export const useVideoPlayer = defineStore('video', () => {
const videoElement = ref<HTMLVideoElement>()
const src = ref('/data/video.mp4')
const { playing, volume, currentTime, togglePictureInPicture } =
useMediaControls(videoElement, { src })
function loadVideo(element: HTMLVideoElement, newSrc: string) {
videoElement.value = element
src.value = newSrc
}
return {
src,
playing,
volume,
currentTime,
loadVideo,
togglePictureInPicture,
}
})
```
**Note:** Don't return non-serializable DOM refs like `videoElement` - they're internal implementation details.
## SSR Considerations
### Option Stores with hydrate()
Define a `hydrate()` function to handle client-side hydration:
```ts
import { defineStore } from 'pinia'
import { useLocalStorage } from '@vueuse/core'
export const useAuthStore = defineStore('auth', {
state: () => ({
user: useLocalStorage('pinia/auth/login', 'bob'),
}),
hydrate(state, initialState) {
// Ignore server state, read from browser
state.user = useLocalStorage('pinia/auth/login', 'bob')
},
})
```
### Setup Stores with skipHydrate()
Mark state that shouldn't hydrate from server:
```ts
import { defineStore, skipHydrate } from 'pinia'
import { useEyeDropper, useLocalStorage } from '@vueuse/core'
export const useColorStore = defineStore('colors', () => {
const { isSupported, open, sRGBHex } = useEyeDropper()
const lastColor = useLocalStorage('lastColor', sRGBHex)
return {
// Skip hydration for client-only state
lastColor: skipHydrate(lastColor),
open, // Function - no hydration needed
isSupported, // Boolean - not reactive
}
})
```
`skipHydrate()` only applies to state properties (refs), not functions or non-reactive values.
<!--
Source references:
- https://pinia.vuejs.org/cookbook/composables.html
-->

View File

@ -0,0 +1,134 @@
---
name: composing-stores
description: Store-to-store communication and avoiding circular dependencies
---
# Composing Stores
Stores can use each other for shared state and logic.
## Rule: Avoid Circular Dependencies
Two stores cannot directly read each other's state during setup:
```ts
// ❌ Infinite loop
const useX = defineStore('x', () => {
const y = useY()
y.name // Don't read here!
return { name: ref('X') }
})
const useY = defineStore('y', () => {
const x = useX()
x.name // Don't read here!
return { name: ref('Y') }
})
```
**Solution:** Read in getters, computed, or actions:
```ts
const useX = defineStore('x', () => {
const y = useY()
// ✅ Read in computed/actions
function doSomething() {
const yName = y.name
}
return { name: ref('X'), doSomething }
})
```
## Setup Stores: Use Store at Top
```ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const user = useUserStore()
const list = ref([])
const summary = computed(() => {
return `Hi ${user.name}, you have ${list.value.length} items`
})
function purchase() {
return apiPurchase(user.id, list.value)
}
return { list, summary, purchase }
})
```
## Shared Getters
Call `useStore()` inside a getter:
```ts
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
getters: {
summary(state) {
const user = useUserStore()
return `Hi ${user.name}, you have ${state.list.length} items`
},
},
})
```
## Shared Actions
Call `useStore()` inside an action:
```ts
import { useUserStore } from './user'
import { apiOrderCart } from './api'
export const useCartStore = defineStore('cart', {
actions: {
async orderCart() {
const user = useUserStore()
try {
await apiOrderCart(user.token, this.items)
this.emptyCart()
} catch (err) {
displayError(err)
}
},
},
})
```
## SSR: Call Stores Before Await
In async actions, call all stores before any `await`:
```ts
actions: {
async orderCart() {
// ✅ All useStore() calls before await
const user = useUserStore()
const analytics = useAnalyticsStore()
try {
await apiOrderCart(user.token, this.items)
// ❌ Don't call useStore() after await (SSR issue)
// const otherStore = useOtherStore()
} catch (err) {
displayError(err)
}
},
}
```
This ensures the correct Pinia instance is used during SSR.
<!--
Source references:
- https://pinia.vuejs.org/cookbook/composing-stores.html
-->

View File

@ -0,0 +1,203 @@
---
name: plugins
description: Extend stores with custom properties, methods, and behavior
---
# Plugins
Plugins extend all stores with custom properties, methods, or behavior.
## Basic Plugin
```ts
import { createPinia } from 'pinia'
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
pinia.use(SecretPiniaPlugin)
// In any store
const store = useStore()
store.secret // 'the cake is a lie'
```
## Plugin Context
Plugins receive a context object:
```ts
import { PiniaPluginContext } from 'pinia'
export function myPiniaPlugin(context: PiniaPluginContext) {
context.pinia // pinia instance
context.app // Vue app instance
context.store // store being augmented
context.options // store definition options
}
```
## Adding Properties
Return an object to add properties (tracked in devtools):
```ts
pinia.use(() => ({ hello: 'world' }))
```
Or set directly on store:
```ts
pinia.use(({ store }) => {
store.hello = 'world'
// For devtools visibility in dev mode
if (process.env.NODE_ENV === 'development') {
store._customProperties.add('hello')
}
})
```
## Adding State
Add to both `store` and `store.$state` for SSR/devtools:
```ts
import { toRef, ref } from 'vue'
pinia.use(({ store }) => {
if (!store.$state.hasOwnProperty('hasError')) {
const hasError = ref(false)
store.$state.hasError = hasError
}
store.hasError = toRef(store.$state, 'hasError')
})
```
## Adding External Properties
Wrap non-reactive objects with `markRaw()`:
```ts
import { markRaw } from 'vue'
import { router } from './router'
pinia.use(({ store }) => {
store.router = markRaw(router)
})
```
## Custom Store Options
Define custom options consumed by plugins:
```ts
// Store definition
defineStore('search', {
actions: {
searchContacts() { /* ... */ },
},
debounce: {
searchContacts: 300,
},
})
// Plugin reads custom option
import debounce from 'lodash/debounce'
pinia.use(({ options, store }) => {
if (options.debounce) {
return Object.keys(options.debounce).reduce((acc, action) => {
acc[action] = debounce(store[action], options.debounce[action])
return acc
}, {})
}
})
```
For Setup Stores, pass options as third argument:
```ts
defineStore(
'search',
() => { /* ... */ },
{
debounce: { searchContacts: 300 },
}
)
```
## TypeScript Augmentation
### Custom Properties
```ts
import 'pinia'
import type { Router } from 'vue-router'
declare module 'pinia' {
export interface PiniaCustomProperties {
router: Router
hello: string
}
}
```
### Custom State
```ts
declare module 'pinia' {
export interface PiniaCustomStateProperties<S> {
hasError: boolean
}
}
```
### Custom Options
```ts
declare module 'pinia' {
export interface DefineStoreOptionsBase<S, Store> {
debounce?: Partial<Record<keyof StoreActions<Store>, number>>
}
}
```
## Subscribe in Plugins
```ts
pinia.use(({ store }) => {
store.$subscribe(() => {
// React to state changes
})
store.$onAction(() => {
// React to actions
})
})
```
## Nuxt Plugin
Create a Nuxt plugin to add Pinia plugins:
```ts
// plugins/myPiniaPlugin.ts
import { PiniaPluginContext } from 'pinia'
function MyPiniaPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation) => {
console.log(`[🍍 ${mutation.storeId}]: ${mutation.type}`)
})
return { creationTime: new Date() }
}
export default defineNuxtPlugin(({ $pinia }) => {
$pinia.use(MyPiniaPlugin)
})
```
<!--
Source references:
- https://pinia.vuejs.org/core-concepts/plugins.html
-->

View File

@ -0,0 +1,54 @@
# Generation Info
- **Source:** `sources/uniapp`
- **Git SHA:** `91423a240a5b0714fc5e7e13bd3f93a79b8cc8d7`
- **Generated:** 2026-01-30
## Generated Skills
| Category | File | Description |
|----------|------|-------------|
| Core | core-framework.md | Framework basics, platform support, condition compilation |
| Core | core-view-components.md | Container components: view, scroll-view, swiper, movable-area |
| Core | core-form-components.md | Form controls: input, textarea, picker, checkbox, radio |
| Features | feature-media-components.md | Media: image, video, camera, map, live-player |
| Features | feature-navigation.md | Navigation: navigator, routing APIs |
| Features | feature-network.md | Network: request, upload, download, WebSocket |
| Features | feature-storage.md | Storage: localStorage, file system |
| Features | feature-system-info.md | System: device info, network, screen, vibration |
| Features | feature-file-operations.md | Files: image/video selection, file operations |
| Features | feature-location.md | Location: geolocation, map component |
| Features | feature-ui-feedback.md | UI: toast, modal, loading, action sheet |
| Features | feature-lifecycle.md | Lifecycle: app and page hooks |
| Config | config-pages.md | pages.json configuration |
| Config | config-manifest.md | manifest.json configuration |
## Source Documentation Structure
```
sources/uniapp/docs/
├── README.md # Framework overview
├── api/ # API documentation
│ ├── README.md # API overview
│ ├── request/ # Network APIs
│ ├── storage/ # Storage APIs
│ ├── media/ # Media APIs
│ ├── file/ # File APIs
│ ├── location/ # Location APIs
│ ├── ui/ # UI APIs
│ └── system/ # System APIs
├── component/ # Component documentation
├── collocation/ # Configuration
│ ├── pages.md # pages.json
│ ├── manifest.md # manifest.json
│ └── manifest-app.md # App-specific manifest
├── tutorial/ # Tutorials
└── quickstart.md # Getting started
```
## Notes
- Skills focus on practical usage patterns and code examples
- Platform-specific differences are highlighted where relevant
- Configuration files include common use cases for each platform
- API skills include both callback and Promise usage patterns

View File

@ -0,0 +1,113 @@
---
name: uni-app
description: Comprehensive skill reference for uni-app cross-platform development framework
metadata:
author: FlippeDround
version: "2026.1.30"
source: Generated from https://gitcode.com/dcloud/unidocs-zh, skills located at https://github.com/antfu/skills
---
> The skill is based on uni-app documentation, generated at 2026-01-30.
uni-app is a Vue.js-based cross-platform framework for developing applications that run on iOS, Android, HarmonyOS, Web (responsive), and various mini-program platforms (WeChat/Alipay/Baidu/Douyin/Feishu/QQ/Kuaishou/DingTalk/Taobao/Jingdong/Xiaohongshu).
## Core
| Topic | Description | Reference |
|-------|-------------|-----------|
| Core Framework | Project structure, platform support, condition compilation | [core-framework](references/core-framework.md) |
| View Components | view, scroll-view, swiper, movable-area, cover-view | [core-view-components](references/core-view-components.md) |
| Form Components | input, textarea, picker, checkbox, radio, switch, slider | [core-form-components](references/core-form-components.md) |
## Features
### UI Components
| Topic | Description | Reference |
|-------|-------------|-----------|
| Media Components | image, video, camera, live-player, map | [feature-media-components](references/feature-media-components.md) |
| Navigation | navigator, routing, page navigation | [feature-navigation](references/feature-navigation.md) |
| UI Feedback | toast, modal, loading, action sheet, pull refresh | [feature-ui-feedback](references/feature-ui-feedback.md) |
### APIs
| Topic | Description | Reference |
|-------|-------------|-----------|
| Network | HTTP requests, file upload/download, WebSocket | [feature-network](references/feature-network.md) |
| Storage | Local storage, file system, caching | [feature-storage](references/feature-storage.md) |
| System Info | Device info, network status, screen, vibration | [feature-system-info](references/feature-system-info.md) |
| File Operations | Image/video selection, file system operations | [feature-file-operations](references/feature-file-operations.md) |
| Location | Geolocation, map component, address selection | [feature-location](references/feature-location.md) |
| Lifecycle | App and page lifecycle hooks | [feature-lifecycle](references/feature-lifecycle.md) |
## Configuration
| Topic | Description | Reference |
|-------|-------------|-----------|
| pages.json | Page routing, tab bar, global styles, sub-packages | [config-pages](references/config-pages.md) |
| manifest.json | App config, permissions, platform settings | [config-manifest](references/config-manifest.md) |
## Platform Support
| Platform | Support Level |
|----------|---------------|
| iOS App | Full support |
| Android App | Full support |
| HarmonyOS Next | Full support |
| H5/Web | Full support |
| WeChat Mini Program | Full support |
| Alipay Mini Program | Full support |
| Baidu Smart Program | Full support |
| Douyin Mini Program | Full support |
| QQ Mini Program | Full support |
| Kuaishou Mini Program | Full support |
| Feishu Mini Program | Full support |
| JD Mini Program | Full support |
| HarmonyOS Meta Service | Full support |
## Key Concepts
### Condition Compilation
Use special comments to write platform-specific code:
```vue
<!-- #ifdef APP-PLUS -->
<view>App only</view>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<view>WeChat only</view>
<!-- #endif -->
```
### API Promise Support
Most uni-app APIs support Promise when no callback is provided:
```javascript
const res = await uni.request({ url: 'https://api.example.com' })
```
### Cross-Platform Best Practices
1. Use uni-app components and APIs instead of platform-specific ones
2. Use condition compilation for platform-specific features
3. Test on all target platforms
4. Use rpx for responsive layouts
5. Handle platform differences in manifest.json
6. must use uni-helper tools
### MCP扩展
当需要查询 uni-app 官方文档时,优先调用 `search-docs-by-Uniapp-official` MCP 工具搜索相关 API 文档和使用示例。
**使用场景:**
- 用户询问特定 API 的详细用法
- 需要官方文档中的代码示例
- 查询组件的属性和事件
- 了解 API 的平台兼容性
**工具安装:**
如果检测到该 MCP 工具不可用,引导用户访问 https://github.com/uni-helper/mcp 进行安装。

View File

@ -0,0 +1,370 @@
---
name: Manifest.json Configuration
description: App configuration, permissions, and platform-specific settings
---
# Manifest.json Configuration
`manifest.json` is the application configuration file, defining app name, icon, permissions, and platform-specific settings.
## Basic Configuration
```json
{
"name": "My App",
"appid": "__UNI__XXXXXXX",
"description": "App description",
"versionName": "1.0.0",
"versionCode": 100,
"locale": "auto",
"debug": false
}
```
### Basic Properties
| Property | Type | Description |
|----------|------|-------------|
| name | String | App name |
| appid | String | DCloud app ID |
| description | String | App description |
| versionName | String | Version name (1.0.0) |
| versionCode | Number | Version code (integer) |
| locale | String | Default language |
| debug | Boolean | Debug mode |
| networkTimeout | Object | Network timeout settings |
| uniStatistics | Object | Statistics configuration |
## Network Timeout
```json
{
"networkTimeout": {
"request": 60000,
"connectSocket": 60000,
"uploadFile": 60000,
"downloadFile": 60000
}
}
```
## App Configuration (app-plus)
```json
{
"app-plus": {
"splashscreen": {
"alwaysShowBeforeRender": true,
"autoclose": true,
"waiting": true
},
"screenOrientation": ["portrait-primary"],
"modules": {
"OAuth": {},
"Payment": {},
"Push": {}
},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.INTERNET\" />"
],
"minSdkVersion": 21,
"targetSdkVersion": 30
},
"ios": {
"capabilities": {
"entitlements": {
"com.apple.developer.push": true
}
}
},
"sdkConfigs": {
"payment": {
"alipay": {},
"weixin": {}
}
}
},
"optimization": {
"subPackages": true
}
}
}
```
### Splash Screen
| Property | Type | Description |
|----------|------|-------------|
| alwaysShowBeforeRender | Boolean | Show until first page renders |
| autoclose | Boolean | Auto close splash screen |
| waiting | Boolean | Show loading indicator |
## Mini-Program Configuration
### WeChat (mp-weixin)
```json
{
"mp-weixin": {
"appid": "wx1234567890",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "Your location is needed"
}
},
"requiredPrivateInfos": [
"getLocation"
]
}
}
```
### Alipay (mp-alipay)
```json
{
"mp-alipay": {
"appid": "2021...",
"allowsAlignRight": true,
"component2": true
}
}
```
### Baidu (mp-baidu)
```json
{
"mp-baidu": {
"appid": "12345678",
"navigationBarForceEnable": true
}
}
```
### Douyin (mp-toutiao)
```json
{
"mp-toutiao": {
"appid": "tt...",
"setting": {
"es6": true,
"minified": true
}
}
}
```
## H5 Configuration
```json
{
"h5": {
"title": "My App",
"template": "index.html",
"router": {
"mode": "hash",
"base": "./"
},
"optimization": {
"treeShaking": {
"enable": true
}
},
"publicPath": "./",
"devServer": {
"port": 8080,
"disableHostCheck": true
},
"sdkConfigs": {
"maps": {
"qqmap": {
"key": "..."
}
}
}
}
}
```
## Vue Configuration
### Vue 2
```json
{
"vueVersion": "2",
"sassImplementationName": "dart-sass"
}
```
### Vue 3
```json
{
"vueVersion": "3"
}
```
## Permission Configuration
### Android Permissions
```json
{
"app-plus": {
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.INTERNET\" />",
"<uses-permission android:name=\"android.permission.CAMERA\" />",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />",
"<uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />",
"<uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />"
]
}
}
}
}
```
### iOS Privacy Descriptions
```json
{
"app-plus": {
"distribute": {
"ios": {
"privacyDescription": {
"NSCameraUsageDescription": "Camera access is needed for scanning QR codes",
"NSPhotoLibraryUsageDescription": "Photo access is needed for uploading images",
"NSLocationWhenInUseUsageDescription": "Location is needed for finding nearby stores"
}
}
}
}
}
```
## Common Module Configuration
### OAuth (Login)
```json
{
"app-plus": {
"modules": {
"OAuth": {}
},
"distribute": {
"sdkConfigs": {
"oauth": {
"weixin": {
"appid": "wx...",
"appsecret": "...",
"UniversalLinks": "https://..."
}
}
}
}
}
}
```
### Payment
```json
{
"app-plus": {
"modules": {
"Payment": {}
},
"distribute": {
"sdkConfigs": {
"payment": {
"alipay": {},
"weixin": {
"appid": "wx..."
}
}
}
}
}
}
```
### Push Notifications
```json
{
"app-plus": {
"modules": {
"Push": {}
},
"distribute": {
"sdkConfigs": {
"push": {
"unipush": {}
}
}
}
}
}
```
### Share
```json
{
"app-plus": {
"modules": {
"Share": {}
},
"distribute": {
"sdkConfigs": {
"share": {
"weixin": {
"appid": "wx..."
}
}
}
}
}
}
```
## Statistics Configuration
```json
{
"uniStatistics": {
"enable": true
},
"app-plus": {
"uniStatistics": {
"enable": true
}
},
"mp-weixin": {
"uniStatistics": {
"enable": true
}
}
}
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/collocation/manifest.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/collocation/manifest-app.md
-->

View File

@ -0,0 +1,270 @@
---
name: Pages.json Configuration
description: Page routing, tab bar, and global style configuration
---
# Pages.json Configuration
`pages.json` is the global configuration file for uni-app, defining page routes, window styles, native navigation bar, and tab bar.
## Basic Structure
```json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "Home"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#F8F8F8"
},
"tabBar": {
"list": [
{ "pagePath": "pages/index/index", "text": "Home" }
]
}
}
```
## Root Properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| pages | Array | Yes | Page routes |
| globalStyle | Object | No | Default window style |
| tabBar | Object | No | Tab bar configuration |
| condition | Object | No | Launch mode (dev only) |
| subPackages | Array | No | Sub-packages |
| preloadRule | Object | No | Preload rules (MP) |
| easycom | Object | No | Auto component import |
| leftWindow/topWindow/rightWindow | Object | No | Multi-window (H5) |
## Pages Configuration
```json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "Home",
"enablePullDownRefresh": true
}
},
{
"path": "pages/detail/detail",
"style": {
"navigationBarTitleText": "Detail",
"navigationStyle": "custom"
}
}
]
}
```
### Page Style Options
| Property | Type | Description |
|----------|------|-------------|
| navigationBarTitleText | String | Navigation title |
| navigationBarTextStyle | String | Title color: black/white |
| navigationBarBackgroundColor | HexColor | Nav background |
| navigationStyle | String | default/custom |
| enablePullDownRefresh | Boolean | Enable pull refresh |
| backgroundColor | HexColor | Background color |
| backgroundTextStyle | String | dark/light |
| onReachBottomDistance | Number | Bottom distance (px) |
| disableScroll | Boolean | Disable scroll (MP) |
| usingComponents | Object | Using custom components |
## Global Style
Applied to all pages unless overridden.
```json
{
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "My App",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"backgroundTextStyle": "dark",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50,
"rpxCalcMaxDeviceWidth": 960,
"rpxCalcBaseDeviceWidth": 375,
"rpxCalcIncludeWidth": 750
}
}
```
## Tab Bar Configuration
```json
{
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "Home",
"iconPath": "static/image/home.png",
"selectedIconPath": "static/image/home-active.png"
},
{
"pagePath": "pages/category/category",
"text": "Category",
"iconPath": "static/image/cat.png",
"selectedIconPath": "static/image/cat-active.png"
},
{
"pagePath": "pages/cart/cart",
"text": "Cart",
"iconPath": "static/image/cart.png",
"selectedIconPath": "static/image/cart-active.png"
},
{
"pagePath": "pages/user/user",
"text": "User",
"iconPath": "static/image/user.png",
"selectedIconPath": "static/image/user-active.png"
}
]
}
}
```
### Tab Bar Properties
| Property | Type | Description |
|----------|------|-------------|
| color | HexColor | Unselected text color |
| selectedColor | HexColor | Selected text color |
| backgroundColor | HexColor | Background color |
| borderStyle | String | black/white |
| list | Array | Tab items (2-5 items) |
| position | String | bottom/top |
### Tab Item Properties
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| pagePath | String | Yes | Page path |
| text | String | Yes | Tab text |
| iconPath | String | No | Icon path (81x81px) |
| selectedIconPath | String | No | Selected icon |
### Custom Tab Bar (WeChat/QQ/Douyin)
```json
{
"tabBar": {
"custom": true,
"list": [
{ "pagePath": "pages/index/index", "text": "Home" }
]
}
}
```
Create `custom-tab-bar/index` component in root.
## Sub-packages
Split app into smaller chunks for faster loading.
```json
{
"subPackages": [
{
"root": "packageA",
"pages": [
{ "path": "pages/cat/cat" },
{ "path": "pages/dog/dog" }
]
},
{
"root": "packageB",
"pages": [
{ "path": "pages/apple/apple" },
{ "path": "pages/banana/banana" }
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["packageA"]
}
}
}
```
## EasyCom (Auto Component Import)
```json
{
"easycom": {
"autoscan": true,
"custom": {
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
}
}
```
With this config, components are auto-imported without registration.
## Conditional Compilation in Config
```json
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "Home"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black"
},
"condition": {
"current": 0,
"list": [
{
"name": "test",
"path": "pages/test/test",
"query": "id=1"
}
]
},
"mp-weixin": {
"appid": "wx...",
"setting": {
"urlCheck": false
}
},
"app-plus": {
"splashscreen": {
"alwaysShowBeforeRender": true
}
}
}
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/collocation/pages.md
-->

View File

@ -0,0 +1,250 @@
---
name: Form Components
description: Input, selection, and form control components
---
# Form Components
## input
Single-line text input.
```vue
<template>
<input
v-model="inputValue"
type="text"
placeholder="Enter text"
:maxlength="100"
:focus="true"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
@confirm="onConfirm"
/>
</template>
```
**Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| value | String | | Input value |
| type | String | text | Input type: text/number/idcard/digit/tel |
| password | Boolean | false | Password input |
| placeholder | String | | Placeholder text |
| placeholder-style | String | | Placeholder inline styles |
| placeholder-class | String | | Placeholder CSS class |
| maxlength | Number | 140 | Max character length |
| cursor-spacing | Number | 0 | Cursor distance from keyboard bottom (px) |
| focus | Boolean | false | Auto focus |
| confirm-type | String | done | Return key type: send/search/next/go/done |
| confirm-hold | Boolean | false | Keep keyboard open after confirm |
**Events:**
- `@input` - Input value change
- `@focus` - Input focused
- `@blur` - Input blurred
- `@confirm` - Confirm button clicked
- `@keyboardheightchange` - Keyboard height changed
## textarea
Multi-line text input.
```vue
<template>
<textarea
v-model="content"
placeholder="Enter content"
:maxlength="500"
:auto-height="true"
:show-confirm-bar="false"
@linechange="onLineChange"
/>
</template>
```
**Additional Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| auto-height | Boolean | false | Auto adjust height |
| fixed | Boolean | false | Fixed position when scrolling |
| cursor | Number | | Cursor position |
| show-confirm-bar | Boolean | true | Show recommendation bar (WeChat) |
| selection-start | Number | -1 | Selection start position |
| selection-end | Number | -1 | Selection end position |
## picker
Selection from predefined options.
```vue
<template>
<!-- Selector mode -->
<picker mode="selector" :range="options" :value="selected" @change="onChange">
<view>Selected: {{ options[selected] }}</view>
</picker>
<!-- Multi-selector -->
<picker mode="multiSelector" :range="multiOptions" @change="onMultiChange">
<view>Multi-select</view>
</picker>
<!-- Date picker -->
<picker mode="date" :value="date" :start="startDate" :end="endDate" @change="onDateChange">
<view>Date: {{ date }}</view>
</picker>
<!-- Region picker -->
<picker mode="region" :value="region" @change="onRegionChange">
<view>Region: {{ region.join('-') }}</view>
</picker>
</template>
<script>
export default {
data() {
return {
options: ['Option 1', 'Option 2', 'Option 3'],
selected: 0,
multiOptions: [['A', 'B'], ['1', '2', '3']],
date: '2024-01-01',
startDate: '2020-01-01',
endDate: '2025-12-31',
region: ['Beijing', 'Beijing', 'Dongcheng']
}
},
methods: {
onChange(e) {
this.selected = e.detail.value
},
onDateChange(e) {
this.date = e.detail.value
},
onRegionChange(e) {
this.region = e.detail.value
}
}
}
</script>
```
**Mode-specific Properties:**
| Mode | Properties |
|------|------------|
| selector | range, range-key |
| multiSelector | range, range-key |
| time | start, end |
| date | start, end, fields (year/month/day) |
| region | custom-item, disable-sub-districts |
## picker-view
Embedded picker view (inline, not popup).
```vue
<template>
<picker-view :value="value" @change="onChange">
<picker-view-column>
<view v-for="item in years" :key="item">{{ item }}年</view>
</picker-view-column>
<picker-view-column>
<view v-for="item in months" :key="item">{{ item }}月</view>
</picker-view-column>
</picker-view>
</template>
```
## radio / checkbox
Selection controls.
```vue
<template>
<!-- Radio group -->
<radio-group @change="onRadioChange">
<label v-for="item in items" :key="item.value">
<radio :value="item.value" :checked="item.checked" color="#007AFF" />
{{ item.name }}
</label>
</radio-group>
<!-- Checkbox group -->
<checkbox-group @change="onCheckboxChange">
<label v-for="item in items" :key="item.value">
<checkbox :value="item.value" :checked="item.checked" color="#007AFF" />
{{ item.name }}
</label>
</checkbox-group>
</template>
```
**radio/checkbox Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| value | String | | Identifier value |
| checked | Boolean | false | Selected state |
| disabled | Boolean | false | Disabled state |
| color | Color | | Selected color |
## switch
Toggle switch.
```vue
<template>
<switch
:checked="isOn"
:disabled="false"
type="switch"
color="#007AFF"
@change="onSwitchChange"
/>
</template>
```
**Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| checked | Boolean | false | Checked state |
| disabled | Boolean | false | Disabled state |
| type | String | switch | Style: switch/checkbox |
| color | Color | | Switch color when on |
## slider
Range slider input.
```vue
<template>
<slider
:value="50"
:min="0"
:max="100"
:step="1"
:show-value="true"
:disabled="false"
activeColor="#007AFF"
backgroundColor="#e9e9e9"
block-size="28"
@change="onChange"
@changing="onChanging"
/>
</template>
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/input.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/textarea.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/picker.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/radio.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/checkbox.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/switch.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/slider.md
-->

View File

@ -0,0 +1,133 @@
---
name: Core Framework
description: UniApp core framework concepts, project structure, and platform overview
---
# UniApp Core Framework
UniApp is a Vue.js-based cross-platform framework for developing applications that run on iOS, Android, HarmonyOS, Web, and various mini-program platforms (WeChat, Alipay, Baidu, Douyin, etc.).
## Project Structure
```
project-root/
├── pages/ # Page files
│ └── index/
│ └── index.vue # Page component
├── components/ # Reusable components
├── static/ # Static assets (images, fonts)
├── App.vue # Application root component
├── main.js # Application entry
├── manifest.json # App configuration
├── pages.json # Page routing config
└── uni.scss # Global SCSS variables
```
## Platform Support
| Platform | Value | Description |
|----------|-------|-------------|
| App | APP-PLUS | iOS/Android native apps |
| App nvue | APP-PLUS-NVUE | Native rendering pages |
| H5 | H5 / WEB | Web applications |
| WeChat Mini Program | MP-WEIXIN | WeChat MP |
| Alipay | MP-ALIPAY | Alipay MP |
| Baidu | MP-BAIDU | Baidu Smart Program |
| Douyin | MP-TOUTIAO | ByteDance MP |
| QQ | MP-QQ | QQ Mini Program |
| Kuaishou | MP-KUAISHOU | Kuaishou MP |
| HarmonyOS | APP-HARMONY | HarmonyOS Next |
## Condition Compilation
Handle platform differences using special comment syntax:
```vue
<template>
<view>
<!-- #ifdef APP-PLUS -->
<text>App only content</text>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<text>WeChat MP only</text>
<!-- #endif -->
<!-- #ifndef H5 -->
<text>All platforms except H5</text>
<!-- #endif -->
</view>
</template>
<script>
export default {
methods: {
getPlatform() {
// #ifdef APP-PLUS
return 'App'
// #endif
// #ifdef H5
return 'Web'
// #endif
}
}
}
</script>
<style>
/* #ifdef APP-PLUS */
.app-style { padding: 20px; }
/* #endif */
</style>
```
## Platform Values Reference
| Value | Description |
|-------|-------------|
| VUE3 / VUE2 | Vue version |
| UNI-APP-X | UniApp X project |
| APP-PLUS | App (JS engine) |
| APP-PLUS-NVUE / APP-NVUE | App nvue pages |
| APP-ANDROID | Android platform |
| APP-IOS | iOS platform |
| APP-HARMONY | HarmonyOS Next |
| H5 / WEB | Web platform |
| MP-WEIXIN | WeChat Mini Program |
| MP-ALIPAY | Alipay Mini Program |
| MP-BAIDU | Baidu Smart Program |
| MP-TOUTIAO | Douyin Mini Program |
| MP-LARK | Feishu Mini Program |
| MP-QQ | QQ Mini Program |
| MP-KUAISHOU | Kuaishou Mini Program |
| MP-HARMONY | HarmonyOS Meta Service |
## API Promise Support
UniApp APIs support Promise when no callback is provided:
```javascript
// Promise style
uni.request({ url: 'https://api.example.com' })
.then(res => console.log(res))
.catch(err => console.error(err))
// Async/await
async function fetchData() {
try {
const res = await uni.request({ url: 'https://api.example.com' })
return res.data
} catch (err) {
console.error(err)
}
}
```
**Note:** Synchronous APIs (ending in `Sync`), `create*` methods, and `*Manager` methods do not support Promise.
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/README.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/tutorial/platform.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/README.md
-->

View File

@ -0,0 +1,202 @@
---
name: View Components
description: Core container components for layout and structure
---
# View Components
## view
Basic container component, similar to HTML `<div>`.
```vue
<template>
<view class="container">
<view class="flex-row">
<view class="item">A</view>
<view class="item">B</view>
</view>
<view class="flex-column">
<view class="item">C</view>
<view class="item">D</view>
</view>
</view>
</template>
```
**Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| hover-class | String | none | CSS class when pressed |
| hover-stop-propagation | Boolean | false | Stop propagation to ancestor nodes |
| hover-start-time | Number | 50 | Delay before hover state (ms) |
| hover-stay-time | Number | 400 | Duration to keep hover state (ms) |
**Tips:**
- `<div>` is auto-converted to `<view>` in mini-programs
- Use `<text>` to wrap text in nvue pages
## scroll-view
Scrollable container with enhanced scroll capabilities.
```vue
<template>
<!-- Vertical scroll -->
<scroll-view
scroll-y
class="scroll-container"
@scroll="onScroll"
@scrolltolower="loadMore"
:scroll-top="scrollTop"
:scroll-into-view="targetId"
>
<view id="item1">Item 1</view>
<view id="item2">Item 2</view>
</scroll-view>
<!-- Horizontal scroll -->
<scroll-view scroll-x class="horizontal-scroll">
<view class="scroll-item">1</view>
<view class="scroll-item">2</view>
</scroll-view>
</template>
```
**Properties:**
| Property | Type | Description |
|----------|------|-------------|
| scroll-x | Boolean | Enable horizontal scroll |
| scroll-y | Boolean | Enable vertical scroll |
| upper-threshold | Number | Distance from top to trigger scrolltoupper (px) |
| lower-threshold | Number | Distance from bottom to trigger scrolltolower (px) |
| scroll-top | Number | Vertical scroll position |
| scroll-left | Number | Horizontal scroll position |
| scroll-into-view | String | Element ID to scroll into view |
| scroll-with-animation | Boolean | Enable smooth scroll animation |
| enable-back-to-top | Boolean | iOS: tap status bar to scroll top |
**Events:**
- `@scroll` - Scroll event
- `@scrolltoupper` - Reached top
- `@scrolltolower` - Reached bottom
## swiper
Carousel/slider container for switching content.
```vue
<template>
<swiper
:current="currentIndex"
:autoplay="true"
:interval="3000"
:duration="500"
:circular="true"
:indicator-dots="true"
@change="onSwiperChange"
>
<swiper-item>
<view class="slide">Slide 1</view>
</swiper-item>
<swiper-item>
<view class="slide">Slide 2</view>
</swiper-item>
</swiper>
</template>
<script>
export default {
data() {
return {
currentIndex: 0
}
},
methods: {
onSwiperChange(e) {
this.currentIndex = e.detail.current
}
}
}
</script>
```
**swiper Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| indicator-dots | Boolean | false | Show pagination dots |
| indicator-color | Color | rgba(0,0,0,.3) | Inactive dot color |
| indicator-active-color | Color | #000000 | Active dot color |
| autoplay | Boolean | false | Auto-play slides |
| current | Number | 0 | Current slide index |
| interval | Number | 5000 | Auto-play interval (ms) |
| duration | Number | 500 | Transition duration (ms) |
| circular | Boolean | false | Circular sliding |
| vertical | Boolean | false | Vertical sliding |
| previous-margin | String | 0px | Previous slide margin |
| next-margin | String | 0px | Next slide margin |
## movable-area / movable-view
Draggable and scalable container.
```vue
<template>
<movable-area class="move-area">
<movable-view
:x="x"
:y="y"
direction="all"
:scale="true"
:scale-min="0.5"
:scale-max="4"
@change="onChange"
@scale="onScale"
>
Draggable content
</movable-view>
</movable-area>
</template>
```
**movable-view Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| direction | String | none | Move direction: all/vertical/horizontal/none |
| x | Number | | Initial X position |
| y | Number | | Initial Y position |
| scale | Boolean | false | Enable scaling |
| scale-min | Number | 0.5 | Min scale |
| scale-max | Number | 10 | Max scale |
| scale-value | Number | 1 | Initial scale |
## cover-view / cover-image
Native overlay components that can cover native components (map, video, canvas).
```vue
<template>
<map class="map">
<cover-view class="overlay">Overlay text</cover-view>
<cover-image class="marker" src="/static/marker.png" />
</map>
</template>
```
**Limitations:**
- Only supported in WeChat MP, App, and H5
- Limited styling capabilities
- Cannot nest regular components inside
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/view.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/scroll-view.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/swiper.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/movable-area.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/cover-view.md
-->

View File

@ -0,0 +1,391 @@
---
name: File Operations
description: File system operations, image/video selection, and file management
---
# File Operations
## Image Selection
### uni.chooseImage
Select images from album or camera.
```javascript
// Select from album
uni.chooseImage({
count: 9, // Max 9
sizeType: ['original', 'compressed'],
sourceType: ['album'],
success: (res) => {
console.log('Selected:', res.tempFilePaths)
// tempFilePaths: ['blob:xxx', 'blob:xxx']
// tempFiles: [{ path, size }]
}
})
// Take photo
uni.chooseImage({
count: 1,
sourceType: ['camera'],
success: (res) => {
const tempPath = res.tempFilePaths[0]
this.uploadImage(tempPath)
}
})
// Both album and camera
uni.chooseImage({
count: 5,
sizeType: ['compressed'], // Compressed only
sourceType: ['album', 'camera'],
success: (res) => {
res.tempFilePaths.forEach(path => {
this.previewImage(path)
})
}
})
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| count | Number | 9 | Max number of images |
| sizeType | Array | ['original', 'compressed'] | original/compressed |
| sourceType | Array | ['album', 'camera'] | album/camera |
## Video Selection
### uni.chooseVideo
Select or record video.
```javascript
uni.chooseVideo({
sourceType: ['album', 'camera'],
compressed: true,
maxDuration: 60,
camera: 'back', // front/back
success: (res) => {
console.log('Path:', res.tempFilePath)
console.log('Duration:', res.duration) // seconds
console.log('Size:', res.size) // bytes
console.log('Height:', res.height)
console.log('Width:', res.width)
}
})
```
### uni.chooseMedia (WeChat/QQ)
Choose mixed media types.
```javascript
uni.chooseMedia({
count: 9,
mediaType: ['image', 'video'],
sourceType: ['album', 'camera'],
maxDuration: 30,
camera: 'back',
success: (res) => {
console.log(res.tempFiles)
}
})
```
## File Selection
### uni.chooseFile
Select any file type.
```javascript
uni.chooseFile({
count: 1,
type: 'all', // all/video/image/file
extension: ['.pdf', '.doc', '.docx'], // Filter by extension
success: (res) => {
console.log(res.tempFilePaths)
console.log(res.tempFiles)
}
})
```
## File System Operations (App)
### Get File System Manager
```javascript
const fs = uni.getFileSystemManager()
```
### Read File
```javascript
// Read as text
fs.readFile({
filePath: `${uni.env.USER_DATA_PATH}/data.txt`,
encoding: 'utf8',
success: (res) => {
console.log(res.data)
}
})
// Read as binary
fs.readFile({
filePath: tempFilePath,
encoding: 'binary',
success: (res) => {
console.log(res.data)
}
})
```
### Write File
```javascript
fs.writeFile({
filePath: `${uni.env.USER_DATA_PATH}/config.json`,
data: JSON.stringify({ theme: 'dark' }),
encoding: 'utf8',
success: () => {
console.log('File written')
}
})
```
### Append to File
```javascript
fs.appendFile({
filePath: `${uni.env.USER_DATA_PATH}/log.txt`,
data: '\nNew log entry',
encoding: 'utf8',
success: () => {
console.log('Appended')
}
})
```
### Delete File
```javascript
fs.unlink({
filePath: `${uni.env.USER_DATA_PATH}/temp.txt`,
success: () => {
console.log('File deleted')
}
})
```
### Check File Exists
```javascript
fs.access({
path: `${uni.env.USER_DATA_PATH}/data.json`,
success: () => {
console.log('File exists')
},
fail: () => {
console.log('File not found')
}
})
```
### Create Directory
```javascript
fs.mkdir({
dirPath: `${uni.env.USER_DATA_PATH}/downloads`,
recursive: true, // Create parent directories
success: () => {
console.log('Directory created')
}
})
```
### Read Directory
```javascript
fs.readdir({
dirPath: `${uni.env.USER_DATA_PATH}/downloads`,
success: (res) => {
console.log('Files:', res.files)
}
})
```
### Get File Info
```javascript
fs.getFileInfo({
filePath: tempFilePath,
success: (res) => {
console.log('Size:', res.size)
console.log('Create time:', res.createTime)
console.log('Last access:', res.lastAccessedTime)
}
})
```
## Save and Open Files
### Save File
```javascript
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (res) => {
const savedPath = res.savedFilePath
console.log('Saved to:', savedPath)
}
})
```
### Get Saved File List
```javascript
uni.getSavedFileList({
success: (res) => {
console.log('Files:', res.fileList)
// [{ filePath, createTime, size }, ...]
}
})
```
### Get Saved File Info
```javascript
uni.getSavedFileInfo({
filePath: savedFilePath,
success: (res) => {
console.log('Size:', res.size)
console.log('Create time:', res.createTime)
}
})
```
### Remove Saved File
```javascript
uni.removeSavedFile({
filePath: savedFilePath,
success: () => {
console.log('File removed')
}
})
```
### Open Document
```javascript
uni.openDocument({
filePath: filePath,
fileType: 'pdf', // Optional hint
showMenu: true, // Show share menu (WeChat)
success: () => {
console.log('Document opened')
}
})
```
## Image Operations
### Preview Image
```javascript
uni.previewImage({
current: currentImage, // Current image URL
urls: imageList, // All image URLs
indicator: 'default',
loop: false,
longPressActions: {
itemList: ['Save Image', 'Share'],
success: (data) => {
console.log('Selected:', data.tapIndex)
}
}
})
```
### Get Image Info
```javascript
uni.getImageInfo({
src: imagePath,
success: (res) => {
console.log('Width:', res.width)
console.log('Height:', res.height)
console.log('Path:', res.path)
console.log('Orientation:', res.orientation)
console.log('Type:', res.type)
}
})
```
### Compress Image
```javascript
uni.compressImage({
src: originalPath,
quality: 80, // 0-100
success: (res) => {
console.log('Compressed:', res.tempFilePath)
}
})
```
### Save Image to Album
```javascript
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: () => {
uni.showToast({ title: 'Saved to album' })
}
})
```
## Video Operations
### Save Video to Album
```javascript
uni.saveVideoToPhotosAlbum({
filePath: videoPath,
success: () => {
uni.showToast({ title: 'Video saved' })
}
})
```
### Get Video Info
```javascript
uni.getVideoInfo({
src: videoPath,
success: (res) => {
console.log('Duration:', res.duration)
console.log('Size:', res.size)
console.log('Bitrate:', res.bitrate)
console.log('FPS:', res.fps)
}
})
```
## File Paths Reference
| Path Type | Example | Description |
|-----------|---------|-------------|
| tempFilePath | `blob:xxx` or `_doc/uniapp_temp/xxx` | Temporary file |
| savedFilePath | `_doc/uniapp_save/xxx` | Saved file |
| USER_DATA_PATH | `_doc/` | App data directory |
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/media/image.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/media/file.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/file/file.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/file/getFileSystemManager.md
-->

View File

@ -0,0 +1,423 @@
---
name: Page Lifecycle
description: Page and application lifecycle hooks
---
# Page Lifecycle
## Application Lifecycle
In `App.vue`:
```javascript
export default {
globalData: {
userInfo: null,
theme: 'light'
},
onLaunch(options) {
// App launched (only once)
console.log('App launched', options)
this.checkUpdate()
},
onShow(options) {
// App shown/foreground
console.log('App shown', options)
},
onHide() {
// App hidden/background
console.log('App hidden')
},
onError(msg) {
// Global error handler
console.error('App error:', msg)
},
onUnhandledRejection(err) {
// Unhandled promise rejection
console.error('Unhandled rejection:', err)
},
onPageNotFound(res) {
// 404 page not found
console.error('Page not found:', res.path)
uni.redirectTo({
url: '/pages/404/404'
})
},
methods: {
checkUpdate() {
// Check for app updates
const updateManager = uni.getUpdateManager()
updateManager.onCheckForUpdate((res) => {
if (res.hasUpdate) {
console.log('New version available')
}
})
}
}
}
```
## Page Lifecycle
```vue
<script>
export default {
// === Page Load ===
onLoad(options) {
// Page loaded with query parameters
console.log('Page loaded', options)
// options contains URL query params
// e.g., /pages/detail?id=123 -> options = { id: '123' }
this.id = options.id
this.loadData()
},
// === Page Show ===
onShow() {
// Page shown (every time)
console.log('Page shown')
// Good for refreshing data when returning
},
// === Page Ready ===
onReady() {
// Page ready, DOM rendered
console.log('Page ready')
// Safe to access DOM elements
this.initChart()
},
// === Page Hide ===
onHide() {
// Page hidden (navigated away)
console.log('Page hidden')
// Pause videos, timers, etc.
},
// === Page Unload ===
onUnload() {
// Page destroyed
console.log('Page unloaded')
// Clean up resources, remove listeners
clearInterval(this.timer)
},
// === Pull Down Refresh ===
onPullDownRefresh() {
// User pulled down
console.log('Pull down refresh')
this.refreshData().finally(() => {
uni.stopPullDownRefresh()
})
},
// === Reach Bottom ===
onReachBottom() {
// Scrolled to bottom
console.log('Reach bottom')
this.loadMore()
},
// === Page Scroll ===
onPageScroll(e) {
// Page scrolled
// e.scrollTop: scroll position
this.scrollTop = e.scrollTop
},
// === Resize ===
onResize(e) {
// Page resized (e.g., rotation)
console.log('Page resized', e.size)
},
// === Share ===
onShareAppMessage(res) {
// Native share (MP)
if (res.from === 'button') {
// From share button
console.log(res.target)
}
return {
title: 'Share Title',
path: '/pages/index/index',
imageUrl: '/static/share.png'
}
},
// === Timeline Share ===
onShareTimeline() {
// Share to timeline (WeChat)
return {
title: 'Timeline Title',
query: 'id=123',
imageUrl: '/static/share.png'
}
},
// === Add to Favorites ===
onAddToFavorites() {
// Add to MP favorites
return {
title: 'Favorite Title',
imageUrl: '/static/fav.png',
query: 'id=123'
}
},
data() {
return {
id: null,
scrollTop: 0,
timer: null
}
},
methods: {
loadData() {
// Load page data
},
refreshData() {
// Refresh data
},
loadMore() {
// Load more data
},
initChart() {
// Initialize chart after DOM ready
}
}
}
</script>
```
## Component Lifecycle (Vue 2)
```vue
<script>
export default {
// === Creation ===
beforeCreate() {
// Instance initialized
},
created() {
// Instance created, data observed
// Good for initial data loading
},
// === Mounting ===
beforeMount() {
// Before DOM mount
},
mounted() {
// DOM mounted
// Good for DOM operations
},
// === Updating ===
beforeUpdate() {
// Before data update
},
updated() {
// After data update
},
// === Destruction ===
beforeDestroy() {
// Before instance destroyed
// Clean up here
},
destroyed() {
// Instance destroyed
},
// === Keep-alive ===
activated() {
// Component activated (kept-alive)
},
deactivated() {
// Component deactivated (kept-alive)
}
}
</script>
```
## Component Lifecycle (Vue 3)
```vue
<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated
} from 'vue'
// Setup runs before beforeCreate
console.log('setup')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
onActivated(() => {
console.log('onActivated')
})
onDeactivated(() => {
console.log('onDeactivated')
})
</script>
```
## Lifecycle Comparison
| Scenario | UniApp Page | Vue Component |
|----------|-------------|---------------|
| Initial load | onLoad | created |
| DOM ready | onReady | mounted |
| Page show | onShow | - |
| Page hide | onHide | - |
| Page destroy | onUnload | destroyed/unmounted |
| Data refresh | onPullDownRefresh | - |
| Infinite scroll | onReachBottom | - |
| Scroll position | onPageScroll | - |
## App Update Manager
```javascript
// In App.vue onLaunch
onLaunch() {
const updateManager = uni.getUpdateManager()
updateManager.onCheckForUpdate((res) => {
console.log('Has update:', res.hasUpdate)
})
updateManager.onUpdateReady(() => {
uni.showModal({
title: 'Update Ready',
content: 'New version downloaded. Restart to apply?',
success: (res) => {
if (res.confirm) {
updateManager.applyUpdate()
}
}
})
})
updateManager.onUpdateFailed(() => {
console.error('Update failed')
})
}
```
## Best Practices
### Data Loading Pattern
```javascript
export default {
data() {
return {
loading: false,
error: null,
data: null
}
},
onLoad(options) {
this.fetchData(options.id)
},
onPullDownRefresh() {
this.fetchData(this.id).finally(() => {
uni.stopPullDownRefresh()
})
},
methods: {
async fetchData(id) {
this.loading = true
this.error = null
try {
this.data = await api.getDetail(id)
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
}
}
}
```
### Scroll Performance
```javascript
export default {
data() {
return {
scrollTop: 0,
showBackTop: false
}
},
// Throttle scroll events
onPageScroll: throttle(function(e) {
this.scrollTop = e.scrollTop
this.showBackTop = e.scrollTop > 500
}, 200),
methods: {
scrollToTop() {
uni.pageScrollTo({
scrollTop: 0,
duration: 300
})
}
}
}
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/collocation/App.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/lifecycle.md
-->

View File

@ -0,0 +1,417 @@
---
name: Location Services
description: Geolocation, map operations, and location-based services
---
# Location Services
## Get Current Location
### uni.getLocation
Get current geographic location.
```javascript
uni.getLocation({
type: 'wgs84', // wgs84/gcj02 (gcj02 for map display)
altitude: false, // Include altitude
geocode: false, // Include address info (App)
highAccuracyExpireTime: 3000, // High accuracy timeout
success: (res) => {
console.log('Latitude:', res.latitude)
console.log('Longitude:', res.longitude)
console.log('Speed:', res.speed)
console.log('Accuracy:', res.accuracy)
// App only:
console.log('Altitude:', res.altitude)
console.log('Address:', res.address)
},
fail: (err) => {
console.error('Location failed:', err)
}
})
```
**Response Properties:**
| Property | Type | Description |
|----------|------|-------------|
| latitude | Number | Latitude |
| longitude | Number | Longitude |
| speed | Number | Speed (m/s) |
| accuracy | Number | Accuracy (meters) |
| altitude | Number | Altitude (meters) |
| verticalAccuracy | Number | Vertical accuracy |
| horizontalAccuracy | Number | Horizontal accuracy |
| address | Object | Address info (App) |
### uni.getFuzzyLocation (WeChat)
Get approximate location (better privacy, faster).
```javascript
uni.getFuzzyLocation({
type: 'wgs84',
success: (res) => {
console.log(res.latitude, res.longitude)
}
})
```
## Choose Location
### uni.chooseLocation
Open map to select location.
```javascript
uni.chooseLocation({
latitude: 39.9,
longitude: 116.4,
keyword: 'restaurant',
success: (res) => {
console.log('Name:', res.name)
console.log('Address:', res.address)
console.log('Latitude:', res.latitude)
console.log('Longitude:', res.longitude)
}
})
```
## Open Location
### uni.openLocation
Open external map app.
```javascript
uni.openLocation({
latitude: 39.9,
longitude: 116.4,
name: 'Destination Name',
address: 'Full address here',
scale: 18
})
```
## Location Change Monitoring
### uni.startLocationUpdate
Start background location updates.
```javascript
uni.startLocationUpdate({
type: 'gcj02',
success: () => {
console.log('Location updates started')
}
})
```
### uni.startLocationUpdateBackground
Start background location (requires permission).
```javascript
uni.startLocationUpdateBackground({
type: 'gcj02',
success: () => {
console.log('Background location started')
}
})
```
### Listen for Location Changes
```javascript
uni.onLocationChange((res) => {
console.log('Location updated:', res.latitude, res.longitude)
})
// Stop listening
uni.offLocationChange(callback)
```
### Stop Location Updates
```javascript
uni.stopLocationUpdate({
success: () => {
console.log('Location updates stopped')
}
})
```
## Map Component
### Basic Map
```vue
<template>
<map
id="myMap"
style="width: 100%; height: 300px;"
:latitude="latitude"
:longitude="longitude"
:scale="14"
:markers="markers"
:polyline="polyline"
:circles="circles"
:show-location="true"
@markertap="onMarkerTap"
@regionchange="onRegionChange"
/>
</template>
<script>
export default {
data() {
return {
latitude: 39.909,
longitude: 116.39742,
markers: [{
id: 1,
latitude: 39.909,
longitude: 116.39742,
title: 'Marker 1',
iconPath: '/static/marker.png',
width: 30,
height: 30,
callout: {
content: 'Hello',
color: '#000',
fontSize: 14,
borderRadius: 5,
padding: 10,
display: 'BYCLICK'
}
}],
polyline: [{
points: [
{ latitude: 39.909, longitude: 116.39742 },
{ latitude: 39.91, longitude: 116.4 }
],
color: '#FF0000',
width: 2,
dottedLine: false
}],
circles: [{
latitude: 39.909,
longitude: 116.39742,
radius: 100,
strokeWidth: 2,
fillColor: '#FF000020'
}]
}
}
}
</script>
```
### Map Context Operations
```javascript
export default {
onReady() {
this.mapContext = uni.createMapContext('myMap')
},
methods: {
// Get center location
getCenter() {
this.mapContext.getCenterLocation({
success: (res) => {
console.log('Center:', res.latitude, res.longitude)
}
})
},
// Move to location
moveToLocation() {
this.mapContext.moveToLocation({
latitude: 39.9,
longitude: 116.4
})
},
// Translate marker
translateMarker() {
this.mapContext.translateMarker({
markerId: 1,
destination: {
latitude: 39.91,
longitude: 116.41
},
autoRotate: true,
rotate: 0,
duration: 1000
})
},
// Include points in view
includePoints() {
this.mapContext.includePoints({
points: [
{ latitude: 39.9, longitude: 116.4 },
{ latitude: 39.91, longitude: 116.41 }
],
padding: [10, 10, 10, 10]
})
},
// Get region
getRegion() {
this.mapContext.getRegion({
success: (res) => {
console.log('Southwest:', res.southwest)
console.log('Northeast:', res.northeast)
}
})
},
// Add markers
addMarkers() {
this.mapContext.addMarkers({
markers: [{
id: 2,
latitude: 39.91,
longitude: 116.41,
title: 'New Marker'
}],
clear: false // Don't clear existing
})
},
// Remove markers
removeMarkers() {
this.mapContext.removeMarkers({
markerIds: [1, 2]
})
}
}
}
```
## Coordinate Systems
| System | Description | Usage |
|--------|-------------|-------|
| WGS84 | GPS coordinates | International standard |
| GCJ02 | Mars coordinates | China national standard |
| BD09 | Baidu coordinates | Baidu Map only |
**Note:** For map display in China, use `gcj02`.
## Permission Configuration
### Mini-Program
Add to `manifest.json`:
```json
{
"mp-weixin": {
"permission": {
"scope.userLocation": {
"desc": "Your location is needed to find nearby stores"
}
},
"requiredPrivateInfos": [
"getLocation",
"chooseLocation"
]
}
}
```
### App (Android)
```json
{
"app-plus": {
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />"
]
}
}
}
}
```
### App (iOS)
```json
{
"app-plus": {
"distribute": {
"ios": {
"privacyDescription": {
"NSLocationWhenInUseUsageDescription": "Location is needed to find nearby stores",
"NSLocationAlwaysUsageDescription": "Background location is needed for navigation"
}
}
}
}
}
```
## Best Practices
### Permission Handling
```javascript
async function getLocationWithPermission() {
try {
// Check permission
const setting = await uni.getSetting()
if (!setting.authSetting['scope.userLocation']) {
// Request permission
await uni.authorize({ scope: 'scope.userLocation' })
}
// Get location
const res = await uni.getLocation({ type: 'gcj02' })
return res
} catch (err) {
if (err.errMsg.includes('auth deny')) {
uni.showModal({
title: 'Permission Required',
content: 'Please enable location permission in settings',
success: (res) => {
if (res.confirm) {
uni.openSetting()
}
}
})
}
throw err
}
}
```
### Distance Calculation
```javascript
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371 // Earth's radius in km
const dLat = (lat2 - lat1) * Math.PI / 180
const dLon = (lon2 - lon1) * Math.PI / 180
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
return R * c // Distance in km
}
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/location/location.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/location/location-change.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/location/open-location.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/map.md
-->

View File

@ -0,0 +1,314 @@
---
name: Media Components
description: Image, video, audio, and camera components
---
# Media Components
## image
Display images with various mode options.
```vue
<template>
<!-- Basic usage -->
<image src="/static/logo.png" mode="aspectFit" />
<!-- With event handling -->
<image
:src="imageUrl"
mode="aspectFill"
:lazy-load="true"
:show-menu-by-longpress="true"
@load="onImageLoad"
@error="onImageError"
/>
</template>
```
**Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| src | String | | Image source URL |
| mode | String | scaleToFill | Display mode (see below) |
| lazy-load | Boolean | false | Lazy load image |
| show-menu-by-longpress | Boolean | false | Show menu on long press |
| webp | Boolean | false | Parse WebP format (Android) |
**Mode Values:**
| Mode | Description |
|------|-------------|
| scaleToFill | Fill container, may distort |
| aspectFit | Contain within container |
| aspectFill | Cover container, may clip |
| widthFix | Width fixed, height auto |
| heightFix | Height fixed, width auto |
| top / bottom / center / left / right | Align to position |
| top left / top right / bottom left / bottom right | Corner alignment |
**Events:**
- `@load` - Image loaded successfully
- `@error` - Image failed to load
## video
Video player component.
```vue
<template>
<video
id="myVideo"
src="https://example.com/video.mp4"
:controls="true"
:autoplay="false"
:loop="false"
:muted="false"
initial-time="30"
:duration="300"
poster="/static/poster.jpg"
object-fit="contain"
@play="onPlay"
@pause="onPause"
@ended="onEnded"
@timeupdate="onTimeUpdate"
@fullscreenchange="onFullscreenChange"
/>
</template>
<script>
export default {
onReady() {
this.videoContext = uni.createVideoContext('myVideo')
},
methods: {
play() {
this.videoContext.play()
},
pause() {
this.videoContext.pause()
},
seek(time) {
this.videoContext.seek(time)
},
sendDanmu(danmu) {
this.videoContext.sendDanmu({
text: danmu.text,
color: danmu.color
})
},
playbackRate(rate) {
this.videoContext.playbackRate(rate)
},
onFullscreenChange(e) {
console.log('Fullscreen:', e.detail.fullScreen)
}
}
}
</script>
```
**Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| src | String | | Video source URL |
| controls | Boolean | true | Show controls |
| autoplay | Boolean | false | Auto play |
| loop | Boolean | false | Loop playback |
| muted | Boolean | false | Muted |
| initial-time | Number | 0 | Start time (s) |
| duration | Number | | Total duration (s) |
| poster | String | | Poster image URL |
| object-fit | String | contain | contain/cover/fill |
| danmu-list | Array | | Danmu list |
| danmu-btn | Boolean | false | Show danmu button |
| enable-danmu | Boolean | false | Enable danmu |
| show-center-play-btn | Boolean | true | Show center play button |
| show-play-btn | Boolean | true | Show play button |
| show-fullscreen-btn | Boolean | true | Show fullscreen button |
| page-gesture | Boolean | false | Enable page gesture |
| enable-progress-gesture | Boolean | true | Enable progress gesture |
## audio
Audio player (deprecated, use `uni.getBackgroundAudioManager` instead).
```vue
<template>
<audio
:src="audioSrc"
:poster="posterUrl"
:name="audioName"
:author="author"
:controls="true"
:loop="false"
@play="onPlay"
@pause="onPause"
@ended="onEnded"
@timeupdate="onTimeUpdate"
/>
</template>
```
## camera
Camera component for capturing photos/videos.
```vue
<template>
<camera
device-position="back"
flash="auto"
resolution="high"
frame-size="large"
@stop="onCameraStop"
@error="onCameraError"
@initdone="onCameraReady"
/>
<button @click="takePhoto">Take Photo</button>
<button @click="startRecord">Start Record</button>
<button @click="stopRecord">Stop Record</button>
</template>
<script>
export default {
onReady() {
this.cameraContext = uni.createCameraContext()
},
methods: {
takePhoto() {
this.cameraContext.takePhoto({
quality: 'high',
success: (res) => {
console.log(res.tempImagePath)
}
})
},
startRecord() {
this.cameraContext.startRecord({
success: () => console.log('Recording started')
})
},
stopRecord() {
this.cameraContext.stopRecord({
success: (res) => {
console.log(res.tempVideoPath)
}
})
}
}
}
</script>
```
**Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| mode | String | normal | normal/scanCode |
| resolution | String | medium | low/medium/high |
| device-position | String | back | front/back |
| flash | String | auto | auto/on/off |
| frame-size | String | large | small/medium/large |
## live-player / live-pusher
Live streaming components (mini-program only).
```vue
<template>
<!-- Live player -->
<live-player
src="rtmp://example.com/live/stream"
mode="live"
:autoplay="true"
:muted="false"
orientation="vertical"
object-fit="contain"
@statechange="onStateChange"
@error="onError"
/>
</template>
```
## map
Map component for displaying maps and markers.
```vue
<template>
<map
id="myMap"
style="width: 100%; height: 300px;"
:latitude="latitude"
:longitude="longitude"
:scale="14"
:markers="markers"
:polyline="polyline"
:circles="circles"
:controls="controls"
:show-location="true"
@markertap="onMarkerTap"
@regionchange="onRegionChange"
@tap="onMapTap"
/>
</template>
<script>
export default {
data() {
return {
latitude: 39.909,
longitude: 116.39742,
markers: [{
id: 1,
latitude: 39.909,
longitude: 116.39742,
title: 'Marker 1',
iconPath: '/static/marker.png',
width: 30,
height: 30
}],
polyline: [{
points: [
{ latitude: 39.909, longitude: 116.39742 },
{ latitude: 39.91, longitude: 116.4 }
],
color: '#FF0000',
width: 2
}]
}
},
onReady() {
this.mapContext = uni.createMapContext('myMap')
},
methods: {
moveToLocation() {
this.mapContext.moveToLocation({
latitude: 39.91,
longitude: 116.4
})
},
getCenterLocation() {
this.mapContext.getCenterLocation({
success: (res) => {
console.log(res.latitude, res.longitude)
}
})
}
}
}
</script>
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/image.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/video.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/audio.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/camera.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/live-player.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/map.md
-->

View File

@ -0,0 +1,257 @@
---
name: Navigation
description: Navigation, routing, and page navigation components
---
# Navigation
## navigator
Page navigation component.
```vue
<template>
<!-- Basic navigation -->
<navigator url="/pages/detail/detail" hover-class="navigator-hover">
Go to Detail
</navigator>
<!-- Open in new page -->
<navigator url="/pages/detail/detail" open-type="navigate">
Navigate (default)
</navigator>
<!-- Redirect (no back button) -->
<navigator url="/pages/login/login" open-type="redirect">
Login (redirect)
</navigator>
<!-- Switch to tab page -->
<navigator url="/pages/index/index" open-type="switchTab">
Go to Home
</navigator>
<!-- Re-launch app -->
<navigator url="/pages/start/start" open-type="reLaunch">
Restart
</navigator>
<!-- Navigate back -->
<navigator open-type="navigateBack" :delta="1">
Go Back
</navigator>
<!-- Exit app (mini-program) -->
<navigator open-type="exit" target="miniProgram">
Exit
</navigator>
</template>
```
**Properties:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| url | String | | Target page path |
| open-type | String | navigate | navigate/redirect/switchTab/reLaunch/navigateBack/exit |
| delta | Number | 1 | Back level when open-type is navigateBack |
| hover-class | String | navigator-hover | Hover state class |
| hover-stop-propagation | Boolean | false | Stop hover propagation |
| target | String | self | self/miniProgram (mini-program only) |
## Programmatic Navigation
### uni.navigateTo
Navigate to new page.
```javascript
// Basic navigation
uni.navigateTo({
url: '/pages/detail/detail'
})
// With query parameters
uni.navigateTo({
url: '/pages/detail/detail?id=123&name=test'
})
// With events (Vue 2 only)
uni.navigateTo({
url: '/pages/detail/detail',
events: {
acceptDataFromOpenedPage(data) {
console.log(data)
}
},
success(res) {
res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test' })
}
})
```
### uni.redirectTo
Redirect to new page (closes current page).
```javascript
uni.redirectTo({
url: '/pages/login/login'
})
```
### uni.reLaunch
Re-launch application to specified page.
```javascript
uni.reLaunch({
url: '/pages/index/index'
})
```
### uni.switchTab
Switch to tab bar page.
```javascript
uni.switchTab({
url: '/pages/home/home'
})
```
### uni.navigateBack
Navigate back to previous page.
```javascript
// Go back one page
uni.navigateBack()
// Go back multiple pages
uni.navigateBack({
delta: 2
})
// With animation (App only)
uni.navigateBack({
delta: 1,
animationType: 'pop-out',
animationDuration: 300
})
```
### uni.preloadPage
Preload page for faster navigation (App only).
```javascript
uni.preloadPage({
url: '/pages/detail/detail'
})
```
## Getting Page Information
### getCurrentPages
Get current page stack.
```javascript
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
console.log(currentPage.route) // Current page path
```
### getApp
Get app instance.
```javascript
const app = getApp()
console.log(app.globalData)
```
## Page Event Channel (Vue 2)
Communicate between pages using event channel.
```javascript
// Page A: Open page B
uni.navigateTo({
url: '/pages/pageB/pageB',
success(res) {
// Listen for events from page B
res.eventChannel.on('acceptDataFromPageB', (data) => {
console.log(data)
})
// Send data to page B
res.eventChannel.emit('acceptDataFromPageA', { data: 'hello' })
}
})
// Page B: Receive and send data
export default {
onLoad() {
const eventChannel = this.getOpenerEventChannel()
// Listen for events from page A
eventChannel.on('acceptDataFromPageA', (data) => {
console.log(data)
})
// Send data back to page A
eventChannel.emit('acceptDataFromPageB', { data: 'world' })
}
}
```
## Navigation Bar
### Custom Navigation Bar
Configure in `pages.json`:
```json
{
"pages": [{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom"
}
}]
}
```
### uni.setNavigationBarTitle
```javascript
uni.setNavigationBarTitle({
title: 'New Title'
})
```
### uni.setNavigationBarColor
```javascript
uni.setNavigationBarColor({
frontColor: '#ffffff',
backgroundColor: '#000000',
animation: {
duration: 400,
timingFunc: 'easeIn'
}
})
```
### uni.showNavigationBarLoading
```javascript
uni.showNavigationBarLoading()
// ...loading operation
uni.hideNavigationBarLoading()
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/component/navigator.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/router.md
-->

View File

@ -0,0 +1,345 @@
---
name: Network Requests
description: HTTP requests, file upload/download, and WebSocket
---
# Network Requests
## uni.request
Make HTTP requests to backend APIs.
```javascript
// GET request
uni.request({
url: 'https://api.example.com/data',
method: 'GET',
data: { id: 123 },
header: {
'content-type': 'application/json',
'Authorization': 'Bearer token'
},
timeout: 30000,
success: (res) => {
console.log(res.data)
},
fail: (err) => {
console.error(err)
},
complete: () => {
console.log('Request complete')
}
})
// POST request with JSON
uni.request({
url: 'https://api.example.com/submit',
method: 'POST',
data: {
name: 'John',
age: 30
},
header: {
'content-type': 'application/json'
},
success: (res) => {
if (res.statusCode === 200) {
console.log('Success:', res.data)
}
}
})
// Using Promise
uni.request({
url: 'https://api.example.com/data',
method: 'GET'
}).then(res => {
console.log(res.data)
}).catch(err => {
console.error(err)
})
// Async/await
async function fetchData() {
try {
const res = await uni.request({
url: 'https://api.example.com/data'
})
return res.data
} catch (err) {
console.error('Failed to fetch:', err)
throw err
}
}
```
**Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| url | String | Yes | Request URL |
| data | Object/String/Array/ArrayBuffer | No | Request data |
| header | Object | No | Request headers |
| method | String | No | HTTP method (default: GET) |
| timeout | Number | No | Timeout in ms (default: 60000) |
| dataType | String | No | Response data type (default: json) |
| responseType | String | No | Response type (default: text) |
| sslVerify | Boolean | No | Verify SSL certificate (default: true) |
**Response Object:**
```javascript
{
data: Object | String | ArrayBuffer, // Response data
statusCode: Number, // HTTP status code
header: Object, // Response headers
cookies: Array // Response cookies
}
```
## uni.uploadFile
Upload files to server.
```javascript
uni.chooseImage({
count: 1,
success: (chooseRes) => {
const tempFilePath = chooseRes.tempFilePaths[0]
const uploadTask = uni.uploadFile({
url: 'https://api.example.com/upload',
filePath: tempFilePath,
name: 'file',
formData: {
user: 'test',
description: 'Image upload'
},
header: {
'Authorization': 'Bearer token'
},
success: (res) => {
console.log('Upload success:', JSON.parse(res.data))
},
fail: (err) => {
console.error('Upload failed:', err)
}
})
// Track upload progress
uploadTask.onProgressUpdate((res) => {
console.log('Progress:', res.progress)
console.log('Uploaded bytes:', res.totalBytesSent)
console.log('Total bytes:', res.totalBytesExpectedToSend)
})
// Abort upload if needed
// uploadTask.abort()
}
})
```
**Upload Task Methods:**
- `onProgressUpdate(callback)` - Listen for progress updates
- `abort()` - Cancel the upload
- `offProgressUpdate(callback)` - Remove progress listener
## uni.downloadFile
Download files from server.
```javascript
const downloadTask = uni.downloadFile({
url: 'https://example.com/file.pdf',
success: (res) => {
if (res.statusCode === 200) {
console.log('Downloaded to:', res.tempFilePath)
// Save to local (App only)
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
console.log('Saved to:', saveRes.savedFilePath)
}
})
}
}
})
// Track download progress
downloadTask.onProgressUpdate((res) => {
console.log('Progress:', res.progress)
})
```
## WebSocket
Real-time bidirectional communication.
```javascript
// Connect to WebSocket
const socketTask = uni.connectSocket({
url: 'wss://api.example.com/ws',
protocols: ['protocol1'],
header: {
'Authorization': 'Bearer token'
},
success: () => {
console.log('WebSocket connecting...')
}
})
// Listen for connection open
uni.onSocketOpen((res) => {
console.log('WebSocket connected')
// Send message
uni.sendSocketMessage({
data: JSON.stringify({
type: 'message',
content: 'Hello server'
})
})
})
// Listen for messages
uni.onSocketMessage((res) => {
console.log('Received:', res.data)
const data = JSON.parse(res.data)
// Handle message...
})
// Listen for errors
uni.onSocketError((err) => {
console.error('WebSocket error:', err)
})
// Listen for close
uni.onSocketClose((res) => {
console.log('WebSocket closed:', res)
})
// Close connection
function closeSocket() {
uni.closeSocket({
code: 1000,
reason: 'User logout',
success: () => {
console.log('Socket closed successfully')
}
})
}
```
**Socket Task Methods:**
- `close(options)` - Close connection
- `send(options)` - Send message
- `onOpen(callback)` - Connection opened
- `onMessage(callback)` - Message received
- `onClose(callback)` - Connection closed
- `onError(callback)` - Error occurred
## Request Interceptors
Use `uni.addInterceptor` to intercept requests.
```javascript
// Add request interceptor
uni.addInterceptor('request', {
invoke(args) {
// Before request
console.log('Request:', args)
// Add auth token
args.header = args.header || {}
args.header.Authorization = `Bearer ${getToken()}`
return args
},
success(res) {
// After success
console.log('Response:', res)
return res
},
fail(err) {
// After failure
console.error('Request failed:', err)
return err
},
complete(res) {
// Always executed
console.log('Request complete')
}
})
// Remove interceptor
uni.removeInterceptor('request')
```
## Domain Configuration
### Mini-Program
Configure request domains in mini-program developer console:
- `request` domain: For `uni.request`
- `uploadFile` domain: For `uni.uploadFile`
- `downloadFile` domain: For `uni.downloadFile`
- `websocket` domain: For `uni.connectSocket`
### H5
H5 uses browser's same-origin policy. Configure CORS on server or use proxy in development.
```javascript
// vite.config.js proxy configuration
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}
```
## Best Practices
```javascript
// Create a request wrapper
const request = (options) => {
return new Promise((resolve, reject) => {
uni.request({
...options,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
} else if (res.statusCode === 401) {
// Handle unauthorized
uni.redirectTo({ url: '/pages/login/login' })
reject(new Error('Unauthorized'))
} else {
reject(new Error(res.data.message || 'Request failed'))
}
},
fail: reject
})
})
}
// Usage
const api = {
getUser: () => request({ url: '/user' }),
updateUser: (data) => request({ url: '/user', method: 'PUT', data })
}
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/request/request.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/request/network-file.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/request/websocket.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/interceptor.md
-->

View File

@ -0,0 +1,279 @@
---
name: Storage
description: Local data storage and caching APIs
---
# Storage
## Synchronous Storage (Recommended for small data)
### uni.setStorageSync
Store data synchronously.
```javascript
// Store simple value
uni.setStorageSync('username', 'John')
// Store object
uni.setStorageSync('userInfo', {
name: 'John',
age: 30,
email: 'john@example.com'
})
// Store array
uni.setStorageSync('tags', ['vue', 'uniapp', 'javascript'])
```
### uni.getStorageSync
Retrieve data synchronously.
```javascript
// Get simple value
const username = uni.getStorageSync('username')
console.log(username) // 'John'
// Get object
const userInfo = uni.getStorageSync('userInfo')
console.log(userInfo.name) // 'John'
// Check if exists
const value = uni.getStorageSync('nonexistent')
console.log(value) // '' (empty string if not found)
```
### uni.removeStorageSync
Remove specific key.
```javascript
uni.removeStorageSync('username')
```
### uni.clearStorageSync
Clear all storage.
```javascript
uni.clearStorageSync()
```
## Asynchronous Storage (Recommended for large data)
### uni.setStorage
```javascript
uni.setStorage({
key: 'userData',
data: {
id: 123,
preferences: { theme: 'dark', language: 'zh' }
},
success: () => {
console.log('Storage saved')
},
fail: (err) => {
console.error('Save failed:', err)
}
})
// Promise style
uni.setStorage({
key: 'config',
data: { debug: true }
}).then(() => {
console.log('Config saved')
})
```
### uni.getStorage
```javascript
uni.getStorage({
key: 'userData',
success: (res) => {
console.log('Data:', res.data)
},
fail: (err) => {
console.log('Key not found')
}
})
// Promise style
uni.getStorage({ key: 'userData' })
.then(res => console.log(res.data))
.catch(() => console.log('Not found'))
```
### uni.removeStorage
```javascript
uni.removeStorage({
key: 'tempData',
success: () => {
console.log('Removed successfully')
}
})
```
### uni.getStorageInfo
Get storage information.
```javascript
uni.getStorageInfo({
success: (res) => {
console.log('Keys:', res.keys)
console.log('Current size:', res.currentSize, 'KB')
console.log('Limit size:', res.limitSize, 'KB')
}
})
```
## Storage Limits
| Platform | Limit |
|----------|-------|
| Mini Program | 10 MB (single) / 200+ MB total |
| App | No hard limit (device dependent) |
| H5 | ~5-10 MB (browser dependent) |
## Best Practices
### Data Persistence Helper
```javascript
const storage = {
// Set with expiration (days)
setWithExpiry(key, value, days) {
const item = {
value,
expiry: Date.now() + days * 24 * 60 * 60 * 1000
}
uni.setStorageSync(key, item)
},
// Get with expiration check
getWithExpiry(key) {
const item = uni.getStorageSync(key)
if (!item) return null
if (Date.now() > item.expiry) {
uni.removeStorageSync(key)
return null
}
return item.value
},
// Safe get with default
get(key, defaultValue = null) {
try {
const value = uni.getStorageSync(key)
return value !== '' ? value : defaultValue
} catch (e) {
return defaultValue
}
},
// Batch operations
setBatch(data) {
Object.entries(data).forEach(([key, value]) => {
uni.setStorageSync(key, value)
})
},
clear() {
uni.clearStorageSync()
}
}
// Usage
storage.setWithExpiry('token', 'abc123', 7) // Expires in 7 days
const token = storage.getWithExpiry('token')
```
### User Session Management
```javascript
const session = {
setToken(token) {
uni.setStorageSync('access_token', token)
},
getToken() {
return uni.getStorageSync('access_token')
},
clearToken() {
uni.removeStorageSync('access_token')
},
setUserInfo(info) {
uni.setStorageSync('user_info', info)
},
getUserInfo() {
return uni.getStorageSync('user_info')
},
isLoggedIn() {
return !!this.getToken()
},
clear() {
this.clearToken()
uni.removeStorageSync('user_info')
}
}
```
## File Storage (App only)
### Local File System
```javascript
// Get file system manager
const fs = uni.getFileSystemManager()
// Write file
fs.writeFile({
filePath: `${uni.env.USER_DATA_PATH}/data.json`,
data: JSON.stringify({ name: 'test' }),
encoding: 'utf8',
success: () => console.log('File written')
})
// Read file
fs.readFile({
filePath: `${uni.env.USER_DATA_PATH}/data.json`,
encoding: 'utf8',
success: (res) => {
const data = JSON.parse(res.data)
console.log(data)
}
})
// Check if file exists
fs.access({
path: `${uni.env.USER_DATA_PATH}/data.json`,
success: () => console.log('File exists'),
fail: () => console.log('File not found')
})
```
## Storage Comparison
| Method | Data Type | Size Limit | Async | Use Case |
|--------|-----------|------------|-------|----------|
| StorageSync | Any | ~10MB | No | Small config data |
| Storage | Any | ~10MB | Yes | Large data objects |
| File System | Binary/Text | Large | Yes | Files, images |
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/storage/storage.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/file/file.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/file/getFileSystemManager.md
-->

View File

@ -0,0 +1,300 @@
---
name: System Information
description: Device info, system info, and environment detection APIs
---
# System Information
## uni.getSystemInfo / getSystemInfoSync
Get device and system information.
```javascript
// Synchronous (faster, recommended)
const info = uni.getSystemInfoSync()
console.log(info)
// Asynchronous
uni.getSystemInfo({
success: (res) => {
console.log(res)
}
})
// Promise
uni.getSystemInfo().then(res => {
console.log(res)
})
```
### System Info Properties
| Property | Type | Description |
|----------|------|-------------|
| brand | String | Device brand |
| model | String | Device model |
| pixelRatio | Number | Device pixel ratio |
| screenWidth | Number | Screen width (px) |
| screenHeight | Number | Screen height (px) |
| windowWidth | Number | Window width (px) |
| windowHeight | Number | Window height (px) |
| statusBarHeight | Number | Status bar height (px) |
| language | String | Language |
| system | String | OS version |
| version | String | WeChat/Runtime version |
| platform | String | Platform: ios/android/windows/mac/devtools |
| SDKVersion | String | Client base library version |
| appId | String | App ID (DCloud) |
| appName | String | App name |
| appVersion | String | App version |
| appCodeName | String | App code name |
| uniPlatform | String | uni-app platform |
| uniCompileVersion | String | Compilation version |
| uniRuntimeVersion | String | Runtime version |
| deviceId | String | Device ID |
| deviceBrand | String | Device brand |
| deviceModel | String | Device model |
| deviceType | String | Device type: phone/pad |
| osName | String | OS name |
| osVersion | String | OS version |
| osLanguage | String | OS language |
| osTheme | String | OS theme: light/dark |
| batteryLevel | Number | Battery level (0-100) |
## uni.getAppBaseInfo
Get app base information.
```javascript
const info = uni.getAppBaseInfo()
// Returns: appId, appName, appVersion, appVersionCode, etc.
```
## uni.getDeviceInfo
Get device hardware information.
```javascript
const info = uni.getDeviceInfo()
// Returns: brand, model, deviceId, deviceBrand, deviceModel, deviceType
```
## uni.getWindowInfo
Get window information.
```javascript
const info = uni.getWindowInfo()
// Returns: pixelRatio, screenWidth, screenHeight, windowWidth, windowHeight,
// statusBarHeight, safeArea, screenTop
```
## Safe Area
Handle notched devices and safe areas.
```javascript
const info = uni.getSystemInfoSync()
// Safe area info
const safeArea = info.safeArea
console.log(safeArea) // { top, left, right, bottom, width, height }
// Check if device has notch (unsafe area at top)
const hasNotch = info.safeAreaInsets && info.safeAreaInsets.top > 0
```
### Safe Area CSS Variables (App/H5)
```css
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom); /* iOS 11.0 */
padding-bottom: env(safe-area-inset-bottom); /* iOS 11.2+ */
}
```
## Network Status
### uni.getNetworkType
```javascript
uni.getNetworkType({
success: (res) => {
// res.networkType: wifi/2g/3g/4g/5g/unknown/none
console.log('Network:', res.networkType)
}
})
```
### uni.onNetworkStatusChange
```javascript
// Listen for network changes
uni.onNetworkStatusChange((res) => {
console.log('Network type:', res.networkType)
console.log('Is connected:', res.isConnected)
})
// Remove listener
uni.offNetworkStatusChange(callback)
```
## Battery Info
### uni.getBatteryInfo
```javascript
uni.getBatteryInfo({
success: (res) => {
console.log('Level:', res.level) // 0-100
console.log('Is charging:', res.isCharging)
}
})
```
## Device Orientation
### uni.onDeviceMotionChange
```javascript
uni.startDeviceMotionListening({
interval: 'normal', // game/ui/normal
success: () => {
uni.onDeviceMotionChange((res) => {
console.log('Alpha:', res.alpha) // 0-360
console.log('Beta:', res.beta) // -180 to 180
console.log('Gamma:', res.gamma) // -90 to 90
})
}
})
// Stop listening
uni.stopDeviceMotionListening()
```
### Screen Orientation
```javascript
// Get current orientation
const info = uni.getSystemInfoSync()
const isLandscape = info.screenWidth > info.screenHeight
// Lock orientation (App only)
plus.screen.lockOrientation('portrait-primary')
// Options: portrait-primary/portrait-secondary/landscape-primary/landscape-secondary
```
## Screen Brightness
```javascript
// Set brightness (0-1)
uni.setScreenBrightness({
value: 0.8
})
// Get brightness
uni.getScreenBrightness({
success: (res) => {
console.log('Brightness:', res.value)
}
})
// Keep screen on
uni.setKeepScreenOn({
keepScreenOn: true
})
```
## Vibration
```javascript
// Short vibration (15ms)
uni.vibrateShort()
// Long vibration (400ms)
uni.vibrateLong()
// Pattern vibration (App only)
uni.vibrateLong() // or custom pattern
```
## Clipboard
```javascript
// Set clipboard
uni.setClipboardData({
data: 'Text to copy',
success: () => {
uni.showToast({ title: 'Copied' })
}
})
// Get clipboard
uni.getClipboardData({
success: (res) => {
console.log('Clipboard:', res.data)
}
})
```
## Phone Call
```javascript
uni.makePhoneCall({
phoneNumber: '13800138000'
})
```
## Scan Code
```javascript
uni.scanCode({
onlyFromCamera: false, // Allow from album
scanType: ['qrCode', 'barCode'], // Types to scan
success: (res) => {
console.log('Result:', res.result)
console.log('Type:', res.scanType)
console.log('CharSet:', res.charSet)
}
})
```
## Platform Detection
```javascript
const info = uni.getSystemInfoSync()
// Platform checks
const isIOS = info.platform === 'ios'
const isAndroid = info.platform === 'android'
const isWindows = info.platform === 'windows'
const isMac = info.platform === 'mac'
const isDevtools = info.platform === 'devtools'
// App platform checks
const isApp = info.uniPlatform === 'app'
const isH5 = info.uniPlatform === 'web'
const isWeixinMP = info.uniPlatform === 'mp-weixin'
// Safe area calculation
const safeAreaTop = info.statusBarHeight + (isApp ? 44 : 0) // 44px for nav bar
```
## Storage Info
```javascript
uni.getStorageInfo({
success: (res) => {
console.log('Keys:', res.keys)
console.log('Current size:', res.currentSize)
console.log('Limit size:', res.limitSize)
}
})
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/system/info.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/system/network.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/system/phone.md
-->

View File

@ -0,0 +1,382 @@
---
name: UI Feedback
description: Toast, modal, loading, and action sheet APIs
---
# UI Feedback
## Toast Messages
### uni.showToast
Display success/error messages.
```javascript
// Success toast
uni.showToast({
title: 'Success!',
icon: 'success',
duration: 2000,
mask: false
})
// Loading toast
uni.showToast({
title: 'Loading...',
icon: 'loading',
duration: 10000
})
// Text only (no icon)
uni.showToast({
title: 'Please wait',
icon: 'none',
duration: 2000
})
// Error toast
uni.showToast({
title: 'Failed!',
icon: 'error'
})
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| title | String | Required | Message text |
| icon | String | success | success/loading/none/error |
| duration | Number | 1500 | Duration in ms |
| mask | Boolean | false | Prevent touch during display |
| position | String | | top/center/bottom (App) |
### uni.hideToast
```javascript
uni.hideToast()
```
## Loading
### uni.showLoading
```javascript
uni.showLoading({
title: 'Loading...',
mask: true // Prevent interaction
})
// Hide after operation
setTimeout(() => {
uni.hideLoading()
}, 2000)
```
### uni.hideLoading
```javascript
uni.hideLoading()
```
## Modal Dialogs
### uni.showModal
Alert and confirm dialogs.
```javascript
// Alert (single button)
uni.showModal({
title: 'Notice',
content: 'Operation completed',
showCancel: false
})
// Confirm (two buttons)
uni.showModal({
title: 'Confirm',
content: 'Are you sure?',
cancelText: 'Cancel',
cancelColor: '#999',
confirmText: 'Confirm',
confirmColor: '#007AFF',
success: (res) => {
if (res.confirm) {
console.log('User confirmed')
} else if (res.cancel) {
console.log('User cancelled')
}
}
})
// Editable modal
uni.showModal({
title: 'Input',
content: 'Enter your name',
editable: true,
placeholderText: 'Name',
success: (res) => {
if (res.confirm) {
console.log('Input:', res.content)
}
}
})
```
## Action Sheet
### uni.showActionSheet
Bottom action menu.
```javascript
uni.showActionSheet({
itemList: ['Take Photo', 'Choose from Album', 'Cancel'],
itemColor: '#000000',
success: (res) => {
// res.tapIndex: 0, 1, 2...
console.log('Selected:', res.tapIndex)
switch (res.tapIndex) {
case 0:
this.takePhoto()
break
case 1:
this.chooseFromAlbum()
break
}
},
fail: (err) => {
console.log('Cancelled')
}
})
```
## Pull to Refresh
### Enable in pages.json
```json
{
"pages": [{
"path": "pages/index/index",
"style": {
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}
}]
}
```
### Handle in Page
```javascript
export default {
onPullDownRefresh() {
console.log('Pull down triggered')
this.refreshData().finally(() => {
uni.stopPullDownRefresh()
})
}
}
```
### Programmatic Control
```javascript
// Start pull refresh
uni.startPullDownRefresh()
// Stop pull refresh
uni.stopPullDownRefresh()
```
## Navigation Bar Loading
```javascript
// Show loading in nav bar
uni.showNavigationBarLoading()
// Hide loading
uni.hideNavigationBarLoading()
```
## Tab Bar Operations
### Show/Hide Tab Bar
```javascript
// Hide tab bar
uni.hideTabBar({
animation: true
})
// Show tab bar
uni.showTabBar({
animation: true
})
```
### Set Tab Bar Style
```javascript
uni.setTabBarStyle({
color: '#999',
selectedColor: '#007AFF',
backgroundColor: '#fff',
borderStyle: 'black'
})
```
### Set Tab Bar Item
```javascript
uni.setTabBarItem({
index: 0,
text: 'Home',
iconPath: '/static/home.png',
selectedIconPath: '/static/home-active.png'
})
```
### Add/Remove Tab Bar Badge
```javascript
// Show badge
uni.showTabBarRedDot({
index: 2 // Tab index
})
// Hide badge
uni.hideTabBarRedDot({
index: 2
})
// Set badge text
uni.setTabBarBadge({
index: 2,
text: '5'
})
// Remove badge text
uni.removeTabBarBadge({
index: 2
})
```
## Preview Image
```javascript
uni.previewImage({
current: 'https://example.com/1.jpg', // Current image
urls: [
'https://example.com/1.jpg',
'https://example.com/2.jpg',
'https://example.com/3.jpg'
],
indicator: 'default', // default/number/none
loop: false,
longPressActions: {
itemList: ['Save Image', 'Share'],
success: (data) => {
console.log('Long press:', data.tapIndex)
}
}
})
```
## Save Image to Photos
```javascript
uni.saveImageToPhotosAlbum({
filePath: 'temp://path/to/image.jpg',
success: () => {
uni.showToast({ title: 'Saved' })
}
})
```
## Best Practices
### Toast Helper
```javascript
const toast = {
success(message, duration = 2000) {
uni.showToast({ title: message, icon: 'success', duration })
},
error(message, duration = 2000) {
uni.showToast({ title: message, icon: 'error', duration })
},
loading(message = 'Loading...') {
uni.showLoading({ title: message, mask: true })
},
hide() {
uni.hideLoading()
uni.hideToast()
},
text(message, duration = 2000) {
uni.showToast({ title: message, icon: 'none', duration })
}
}
// Usage
toast.loading()
fetchData()
.then(() => toast.success('Loaded'))
.catch(() => toast.error('Failed'))
.finally(() => toast.hide())
```
### Modal Helper
```javascript
const modal = {
confirm(title, content) {
return new Promise((resolve) => {
uni.showModal({
title,
content,
success: (res) => resolve(res.confirm)
})
})
},
alert(title, content) {
return new Promise((resolve) => {
uni.showModal({
title,
content,
showCancel: false,
success: () => resolve()
})
})
},
action(items) {
return new Promise((resolve, reject) => {
uni.showActionSheet({
itemList: items,
success: (res) => resolve(res.tapIndex),
fail: reject
})
})
}
}
// Usage
async function deleteItem(id) {
const confirmed = await modal.confirm('Delete', 'Are you sure?')
if (confirmed) {
await api.delete(id)
toast.success('Deleted')
}
}
```
<!--
Source references:
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/ui/prompt.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/ui/navigationbar.md
- https://gitcode.com/dcloud/unidocs-zh/blob/main/docs/api/ui/tabbar.md
-->

View File

@ -0,0 +1,5 @@
# Generation Info
- **Source:** `sources/uni-helper`
- **Git SHA:** `286d8656dc077a7d9d6a50e647284ff1ea48ac6b`
- **Generated:** 2026-01-30

View File

@ -0,0 +1,88 @@
---
name: uni-helper
description: Comprehensive skill reference for uni-helper ecosystem - AI-powered development tools for uni-app
metadata:
author: FlippeDround
version: "2026.1.30"
source: Generated from https://github.com/uni-helper/website, skills located at https://github.com/uni-helper/ai-tools
---
> The skill is based on uni-helper documentation, generated at 2026-01-30.
uni-helper is an ecosystem of AI-powered development tools for uni-app, providing Vite plugins, utility libraries, TypeScript support, and development tools to enhance the uni-app development experience.
## Vite Plugins
| Topic | Description | Reference |
|-------|-------------|-----------|
| vite-plugin-uni-pages | File-based routing system for uni-app with auto page discovery | [plugin-pages](references/plugin-pages.md) |
| vite-plugin-uni-layouts | Nuxt-like layouts system for uni-app | [plugin-layouts](references/plugin-layouts.md) |
| vite-plugin-uni-components | On-demand automatic component imports | [plugin-components](references/plugin-components.md) |
| vite-plugin-uni-manifest | Write manifest.json in TypeScript | [plugin-manifest](references/plugin-manifest.md) |
| vite-plugin-uni-platform | File-based platform compilation (*.h5|mp-weixin|app.*) | [plugin-platform](references/plugin-platform.md) |
| vite-plugin-uni-platform-modifier | Platform modifiers for attributes/directives | [plugin-platform-modifier](references/plugin-platform-modifier.md) |
| vite-plugin-uni-middleware | Middleware support for uni-app routing | [plugin-middleware](references/plugin-middleware.md) |
## Libraries
| Topic | Description | Reference |
|-------|-------------|-----------|
| uni-use | VueUse-style composable utilities for uni-app | [lib-uni-use](references/lib-uni-use.md) |
| uni-network | Promise-based HTTP client for uni-app | [lib-uni-network](references/lib-uni-network.md) |
| uni-promises | Promise wrappers for uni-app APIs | [lib-uni-promises](references/lib-uni-promises.md) |
| uni-typed | TypeScript type definitions for uni-app templates | [lib-uni-typed](references/lib-uni-typed.md) |
## Utilities
| Topic | Description | Reference |
|-------|-------------|-----------|
| uni-env | Environment detection utilities for uni-app | [util-uni-env](references/util-uni-env.md) |
| unocss-preset-uni | UnoCSS preset for uni-app | [util-unocss-preset](references/util-unocss-preset.md) |
## Project Starters
| Topic | Description | Reference |
|-------|-------------|-----------|
| create-uni | CLI scaffolding tool for uni-app projects | [starter-create-uni](references/starter-create-uni.md) |
| vitesse-uni-app | Vite-powered uni-app starter template | [starter-vitesse](references/starter-vitesse.md) |
## Plugin Order Best Practices
When using multiple uni-helper Vite plugins, the recommended order is:
```ts
// vite.config.ts
export default defineConfig({
plugins: [
UniComponents(), // 1. Component auto-import
UniPages(), // 2. File-based routing
UniLayouts(), // 3. Layout system
UniManifest(), // 4. Manifest generation
UniPlatform(), // 5. Platform-specific files
UniPlatformModifier(), // 6. Platform modifiers
UniMiddleware(), // 7. Route middleware
Uni(), // 8. Official uni-app plugin (always last)
],
})
```
## Quick Start
Create a new uni-app project with create-uni:
```bash
# npm 7+, extra double-dash is needed
npm create uni@latest
# pnpm
pnpm create uni
# yarn
yarn create uni
```
## Official Resources
- Website: https://uni-helper.js.org
- GitHub: https://github.com/uni-helper
- NPM Scope: @uni-helper

View File

@ -0,0 +1,221 @@
---
name: uni-network
description: Promise-based HTTP client for uni-app - axios-inspired API with uni.request as default transport
---
# uni-network
Promise-based HTTP client for uni-app, inspired by axios@0.27.2. Uses uni.request/uni.uploadFile/uni.downloadFile as the underlying transport.
## Installation
```bash
npm i @uni-helper/uni-network
```
## Features
- Promise-based API
- Request/response interceptors
- Request cancellation
- TypeScript support
- Composition API integration
- Upload/download support
## Basic Usage
```ts
import { un } from '@uni-helper/uni-network'
// GET request
un('/user/123')
// POST request
un({
method: 'POST',
url: '/user/123',
data: {
firstName: 'Fred',
lastName: 'Flintstone',
},
})
```
## Request Aliases
```ts
import { un } from '@uni-helper/uni-network'
// HTTP verbs
un.get('/users')
un.post('/users', { name: 'John' })
un.put('/users/1', { name: 'Jane' })
un.patch('/users/1', { age: 30 })
un.delete('/users/1')
// Upload/download
un.upload({
url: '/upload',
filePath: tempFilePath,
name: 'file',
})
un.download({
url: '/files/report.pdf',
})
```
## Creating an Instance
```ts
import { createUniNetwork } from '@uni-helper/uni-network'
const http = createUniNetwork({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
})
// Use the instance
http.get('/users')
```
## Interceptors
```ts
import { un } from '@uni-helper/uni-network'
// Request interceptor
un.interceptors.request.use(
(config) => {
// Add auth token
const token = uni.getStorageSync('token')
if (token) {
config.headers = config.headers || {}
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptor
un.interceptors.response.use(
(response) => {
// Transform response data
return response.data
},
(error) => {
// Handle errors globally
if (error.statusCode === 401) {
uni.redirectTo({ url: '/pages/login/index' })
}
return Promise.reject(error)
}
)
```
## Configuration
```ts
interface UniNetworkConfig {
// URL
url?: string
// Base URL
baseURL?: string
// Method
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | ...
// Headers
headers?: Record<string, string>
// Request data
data?: any
// Query params
params?: Record<string, any>
// Timeout in ms
timeout?: number
// Response type
responseType?: 'text' | 'arraybuffer'
// Data type
dataType?: 'json' | 'string'
// Enable http2
enableHttp2?: boolean
// Enable quic
enableQuic?: boolean
// Enable cache
enableCache?: boolean
// SSL verify
sslVerify?: boolean
}
```
## Request Cancellation
```ts
import { un } from '@uni-helper/uni-network'
const controller = new AbortController()
un.get('/long-request', {
signal: controller.signal,
})
// Cancel after 5 seconds
setTimeout(() => {
controller.abort()
}, 5000)
```
## Error Handling
```ts
import { un, UniNetworkError } from '@uni-helper/uni-network'
try {
const response = await un.get('/users')
} catch (error) {
if (error instanceof UniNetworkError) {
console.log('Request failed:', error.message)
console.log('Status:', error.statusCode)
}
}
```
## Composition API
```ts
import { useUniNetwork } from '@uni-helper/uni-network'
const { data, error, loading, execute } = useUniNetwork({
url: '/users',
method: 'GET',
})
// Execute manually
await execute()
```
## TypeScript Support
```ts
import { un } from '@uni-helper/uni-network'
interface User {
id: number
name: string
email: string
}
// Typed response
const { data } = await un.get<User>('/users/1')
console.log(data.name) // Type-safe access
```
<!--
Source references:
- https://github.com/uni-helper/uni-network
- https://uni-helper.js.org/uni-network
-->

View File

@ -0,0 +1,197 @@
---
name: uni-promises
description: Promise wrappers for uni-app APIs - convert callback-based APIs to Promise-based
---
# uni-promises
Promise wrappers for uni-app's callback-based APIs. Convert uni-app APIs to return Promises for better async/await support.
## Installation
```bash
npm i @uni-helper/uni-promises
```
## Usage
Instead of using callbacks:
```ts
// Original callback style
uni.request({
url: 'https://api.example.com',
success: (res) => {
console.log(res.data)
},
fail: (err) => {
console.error(err)
},
})
```
Use Promises:
```ts
import { request } from '@uni-helper/uni-promises'
// Promise style
const res = await request({
url: 'https://api.example.com',
})
console.log(res.data)
```
## Available APIs
Most uni-app APIs are wrapped:
### Network
- `request` - HTTP request
- `uploadFile` - File upload
- `downloadFile` - File download
- `connectSocket` - WebSocket
### Storage
- `setStorage` - Set storage
- `getStorage` - Get storage
- `removeStorage` - Remove storage
- `clearStorage` - Clear all storage
- `getStorageInfo` - Storage info
### Media
- `chooseImage` - Select images
- `chooseVideo` - Select video
- `chooseMedia` - Select media
- `previewImage` - Preview images
- `saveImageToPhotosAlbum` - Save image
- `compressImage` - Compress image
### File
- `saveFile` - Save file
- `getFileInfo` - File info
- `getSavedFileList` - Saved files list
- `removeSavedFile` - Remove saved file
- `openDocument` - Open document
### Location
- `getLocation` - Get location
- `chooseLocation` - Choose location
- `openLocation` - Open location
### Device
- `getSystemInfo` - System info
- `getNetworkType` - Network type
- `scanCode` - QR/barcode scan
- `setClipboardData` - Copy to clipboard
- `getClipboardData` - Get clipboard
### UI
- `showToast` - Show toast
- `showModal` - Show modal
- `showActionSheet` - Show action sheet
- `showLoading` - Show loading
- `hideLoading` - Hide loading
- `showNavigationBarLoading` - Nav loading
- `hideNavigationBarLoading` - Hide nav loading
- `setNavigationBarTitle` - Set nav title
- `setNavigationBarColor` - Set nav color
- `pageScrollTo` - Scroll to position
### Navigation
- `navigateTo` - Navigate to page
- `redirectTo` - Redirect to page
- `reLaunch` - Re-launch app
- `switchTab` - Switch tab
- `navigateBack` - Go back
### Login
- `login` - User login
- `checkSession` - Check session
- `getUserProfile` - Get user profile
- `getUserInfo` - Get user info
### Payment
- `requestPayment` - Request payment
## Example Usage
```ts
import {
showModal,
showToast,
getStorage,
navigateTo,
getSystemInfo,
chooseImage,
uploadFile,
} from '@uni-helper/uni-promises'
async function handleUserAction() {
// Show confirmation
const { confirm } = await showModal({
title: 'Confirm',
content: 'Are you sure?',
})
if (!confirm) return
try {
// Get cached data
const { data: token } = await getStorage({ key: 'token' })
// Get system info
const systemInfo = await getSystemInfo()
console.log('Platform:', systemInfo.platform)
// Select image
const { tempFilePaths } = await chooseImage({
count: 1,
sizeType: ['compressed'],
})
// Upload file
const uploadRes = await uploadFile({
url: 'https://api.example.com/upload',
filePath: tempFilePaths[0],
name: 'file',
})
await showToast({
title: 'Success!',
icon: 'success',
})
// Navigate
await navigateTo({
url: '/pages/success/index',
})
} catch (error) {
await showToast({
title: error.message || 'Failed',
icon: 'error',
})
}
}
```
## Error Handling
All wrapped APIs reject with standard Error objects:
```ts
import { request } from '@uni-helper/uni-promises'
try {
const res = await request({ url: 'https://api.example.com' })
} catch (error) {
// error.message contains the fail callback message
console.error('Request failed:', error.message)
}
```
<!--
Source references:
- https://github.com/uni-helper/uni-promises
- https://uni-helper.js.org/uni-promises
-->

View File

@ -0,0 +1,200 @@
---
name: uni-typed
description: TypeScript type definitions for uni-app templates - complete type safety for uni-app components
---
# uni-typed
TypeScript type definitions for uni-app template sections. Provides complete type safety and IntelliSense for uni-app components.
## Installation
```bash
# For uni-app
npm i -D @uni-helper/uni-app-types
# For uni-ui
npm i -D @uni-helper/uni-ui-types
# For uni-cloud
npm i -D @uni-helper/uni-cloud-types
```
## Setup
### Uni-app Types
```json
// tsconfig.json
{
"compilerOptions": {
"types": ["@uni-helper/uni-app-types"]
}
}
```
### Uni-ui Types
```json
// tsconfig.json
{
"compilerOptions": {
"types": [
"@uni-helper/uni-app-types",
"@uni-helper/uni-ui-types"
]
}
}
```
## Features
- Type-safe template event handlers
- Component prop type checking
- Custom event type definitions
- Ref type inference for components
## Component Types
### Template Type Safety
```vue
<script setup lang="ts">
import { ref } from 'vue'
const inputValue = ref('')
const count = ref(0)
const handleInput = (e: UniHelper.InputOnInputEvent) => {
inputValue.value = e.detail.value
}
const handleChange = (e: UniHelper.SwitchOnChangeEvent) => {
console.log(e.detail.value)
}
</script>
<template>
<!-- Type-safe component usage -->
<input
type="text"
:value="inputValue"
@input="handleInput"
/>
<switch :checked="true" @change="handleChange" />
</template>
```
### Component Ref Types
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// Type-safe component refs
const videoRef = ref<UniHelper.VideoInstance>()
const mapRef = ref<UniHelper.MapInstance>()
onMounted(() => {
// IntelliSense for component methods
videoRef.value?.play()
videoRef.value?.pause()
videoRef.value?.seek(100)
mapRef.value?.moveToLocation({
latitude: 39.9,
longitude: 116.4,
})
})
</script>
<template>
<video ref="videoRef" src="..." />
<map ref="mapRef" />
</template>
```
## Event Types
### Common Event Types
```ts
// Input events
UniHelper.InputOnInputEvent
UniHelper.InputOnBlurEvent
UniHelper.InputOnFocusEvent
// Form events
UniHelper.SwitchOnChangeEvent
UniHelper.CheckboxOnChangeEvent
UniHelper.RadioOnChangeEvent
UniHelper.PickerOnChangeEvent
UniHelper.SliderOnChangeEvent
// Media events
UniHelper.VideoOnPlayEvent
UniHelper.VideoOnPauseEvent
UniHelper.VideoOnEndedEvent
UniHelper.VideoOnTimeUpdateEvent
// Touch events
UniHelper.ViewOnTouchstartEvent
UniHelper.ViewOnTouchmoveEvent
UniHelper.ViewOnTouchendEvent
UniHelper.ViewOnTapEvent
UniHelper.ViewOnLongpressEvent
// Scroll events
UniHelper.ScrollViewOnScrollEvent
UniHelper.ScrollViewOnScrolltolowerEvent
UniHelper.ScrollViewOnScrolltoupperEvent
```
### Custom Events
Define custom event types for your components:
```vue
<script setup lang="ts">
// Child component
const emit = defineEmits<{
change: [value: string]
submit: [data: { id: number; name: string }]
}>()
</script>
```
```vue
<script setup lang="ts">
// Parent component
import type { MyComponentOnChangeEvent } from './types'
const handleChange = (e: MyComponentOnChangeEvent) => {
console.log(e.detail.value)
}
</script>
<template>
<my-component @change="handleChange" />
</template>
```
## Why uni-typed?
DCloud's `@dcloudio/types` only provides TypeScript support for script sections. uni-typed fills the gap for template section type safety, providing:
- Component prop validation
- Event handler type checking
- Method IntelliSense on component refs
- Better IDE support (VS Code, WebStorm)
## Migration from v0 to v1
See the [migration guide](https://uni-helper.js.org/uni-typed/other/migrate-v0-to-v1) for upgrading instructions.
<!--
Source references:
- https://github.com/uni-helper/uni-typed
- https://uni-helper.js.org/uni-typed
-->

View File

@ -0,0 +1,185 @@
---
name: uni-use
description: VueUse-style composable utilities for uni-app development
---
# uni-use
VueUse-style composable utilities specifically designed for uni-app. Provides reactive wrappers around uni-app APIs.
## Installation
```bash
npm i @uni-helper/uni-use @vueuse/core@9
```
## Usage
```ts
import { tryOnLoad, useClipboard, useSystemInfo } from '@uni-helper/uni-use'
tryOnLoad(() => {
console.log('Page loaded')
})
const { copy, text } = useClipboard()
const { systemInfo } = useSystemInfo()
```
## Auto Import Setup
```ts
// vite.config.ts
import { uniuseAutoImports } from '@uni-helper/uni-use'
import autoImport from 'unplugin-auto-import/vite'
export default defineConfig({
plugins: [
autoImport({
imports: [uniuseAutoImports()],
}),
uni(),
],
})
```
## Available Composables
### Lifecycle
| Composable | Description |
|------------|-------------|
| `tryOnLoad` | onLoad with try-catch |
| `tryOnShow` | onShow with try-catch |
| `tryOnHide` | onHide with try-catch |
| `tryOnReady` | onReady with try-catch |
| `tryOnUnload` | onUnload with try-catch |
| `tryOnPullDownRefresh` | onPullDownRefresh with try-catch |
| `tryOnReachBottom` | onReachBottom with try-catch |
| `tryOnPageScroll` | onPageScroll with try-catch |
| `tryOnTabItemTap` | onTabItemTap with try-catch |
### System
| Composable | Description |
|------------|-------------|
| `useSystemInfo` | Reactive system information |
| `useDeviceInfo` | Device information |
| `useWindowInfo` | Window dimensions |
| `useAppTheme` | App theme (dark/light) |
| `useNetworkStatus` | Network connection status |
| `useBattery` | Battery status |
### Storage
| Composable | Description |
|------------|-------------|
| `useStorage` | Reactive local storage |
| `useStorageAsync` | Async storage operations |
### UI
| Composable | Description |
|------------|-------------|
| `useClipboard` | Clipboard operations |
| `useToast` | Toast notifications |
| `useModal` | Modal dialogs |
| `useActionSheet` | Action sheet |
| `useLoading` | Loading indicator |
| `useNavigationBar` | Navigation bar control |
| `usePullDownRefresh` | Pull-to-refresh |
### Location
| Composable | Description |
|------------|-------------|
| `useLocation` | Current location |
| `useChooseLocation` | Location picker |
### Media
| Composable | Description |
|------------|-------------|
| `useImage` | Image operations |
| `useCamera` | Camera access |
| `useRecorder` | Audio recording |
## Example Usage
```vue
<script setup lang="ts">
import { useClipboard, useSystemInfo, useStorage, tryOnLoad } from '@uni-helper/uni-use'
// System info
const { systemInfo, safeArea } = useSystemInfo()
// Clipboard
const { text, copy, isSupported } = useClipboard()
const handleCopy = async () => {
await copy('Hello uni-app')
uni.showToast({ title: 'Copied!' })
}
// Storage
const userToken = useStorage('token', '')
const saveToken = (token: string) => {
userToken.value = token
}
// Lifecycle
tryOnLoad((options) => {
console.log('Page options:', options)
})
</script>
<template>
<view class="page">
<text>Platform: {{ systemInfo.platform }}</text>
<text>Safe area top: {{ safeArea?.top }}px</text>
<button @click="handleCopy">Copy to clipboard</button>
</view>
</template>
```
## Build Configuration
For Vite + Vue3 projects targeting ES6:
```ts
// vite.config.ts
export default defineConfig({
build: {
target: 'es6',
cssTarget: 'chrome61',
},
optimizeDeps: {
exclude: ['vue-demi'],
},
})
```
Add polyfills in `main.ts` if needed:
```ts
// main.ts
import 'core-js/actual/array/iterator'
import 'core-js/actual/promise'
import 'core-js/actual/object/assign'
```
## Limitations
- Mini Program and App environments lack some globals (window, navigator)
- Cannot use top-level await
- Some VueUse features may not work in uni-app
## See Also
- Full API docs: https://uni-helper.js.org/uni-use/apis
- VueUse: https://vueuse.org/
<!--
Source references:
- https://github.com/uni-helper/uni-use
- https://uni-helper.js.org/uni-use
-->

View File

@ -0,0 +1,93 @@
---
name: vite-plugin-uni-components
description: On-demand automatic component imports for uni-app - derived from unplugin-vue-components
---
# vite-plugin-uni-components
Automatic on-demand component imports for uni-app. Derived from unplugin-vue-components with uni-app specific adaptations.
## Installation
```bash
npm i -D @uni-helper/vite-plugin-uni-components
```
## Setup
```ts
// vite.config.ts
import Uni from '@dcloudio/vite-plugin-uni'
import Components from '@uni-helper/vite-plugin-uni-components'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
Components(), // Must be before Uni()
Uni(),
],
})
```
## Features
- Automatic component imports from `src/components`
- Supports UI library resolvers
- Generates TypeScript declarations
- Handles `default` export for third-party UI libraries
## Supported UI Libraries
Built-in resolvers for:
| Library | Import |
|---------|--------|
| Ano UI | `AnoUIResolver()` |
| uni-ui | `UniUIResolver()` |
| wot-design-uni | `WotDesignUniResolver()` |
| uv-uni | `UvUniResolver()` |
| uview-pro | `UviewProResolver()` |
| z-paging | `ZPagingResolver()` |
## TypeScript Declarations
For pnpm users, create `.npmrc`:
```ini
shamefully-hoist = true
```
Generated `components.d.ts` example:
```ts
declare module 'vue' {
export interface GlobalComponents {
AButton: typeof import('ano-ui/components/AButton/AButton.vue')['default']
UniCalendar: typeof import('@dcloudio/uni-ui/lib/uni-calendar/uni-calendar.vue')['default']
MyComponent: typeof import('./src/components/MyComponent.vue')['default']
}
}
```
## Third-Party Component Handling
For third-party components like `@dcloudio/uni-ui` and `ano-ui`, the plugin automatically adds the `default` property to handle H5 compatibility issues:
```diff
declare module 'vue' {
export interface GlobalComponents {
- AButton: typeof import('ano-ui/components/AButton/AButton.vue')['AButton']
+ AButton: typeof import('ano-ui/components/AButton/AButton.vue')['default']
}
}
```
## Full Configuration
See [unplugin-vue-components documentation](https://github.com/antfu/unplugin-vue-components) for all options.
<!--
Source references:
- https://github.com/uni-helper/vite-plugin-uni-components
- https://uni-helper.js.org/vite-plugin-uni-components
-->

View File

@ -0,0 +1,143 @@
---
name: vite-plugin-uni-layouts
description: Nuxt-like layouts system for Vite-powered uni-app projects
---
# vite-plugin-uni-layouts
Provides a Nuxt-like layout system for uni-app. Wraps pages with reusable layout components.
## Installation
```bash
npm i -D @uni-helper/vite-plugin-uni-layouts
```
## Setup
```ts
// vite.config.ts
import uni from '@dcloudio/vite-plugin-uni'
import UniLayouts from '@uni-helper/vite-plugin-uni-layouts'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [UniLayouts(), uni()],
})
```
## Creating Layouts
Create layout files in `src/layouts/` directory:
```vue
<!-- src/layouts/default.vue -->
<template>
<view class="layout">
<slot name="header" />
<slot>main content</slot>
<slot name="footer" />
</view>
</template>
```
## Applying Layouts
### Via pages.json (traditional)
```json
{
"pages": [{
"path": "pages/index/index",
"layout": "default"
},
{
"path": "pages/about/index",
"layout": false // Disable layout
}]
}
```
### Via definePage (recommended)
```vue
<script setup>
definePage({
layout: 'default',
})
</script>
```
## Dynamic Layouts
Use the `<uni-layout>` component for dynamic layout switching:
```vue
<script setup>
const layoutName = ref('default')
</script>
<template>
<!-- Disable page layout first -->
<uni-layout :name="layoutName">
<template #header>
<text>Header content</text>
</template>
Main content here
<template #footer>
<text>Footer content</text>
</template>
</uni-layout>
</template>
```
### Accessing Layout Ref
```vue
<script setup>
const uniLayout = ref()
onMounted(() => {
console.log(uniLayout.value) // Access layout component
})
</script>
<template>
<uni-layout ref="uniLayout">
Content
</uni-layout>
</template>
```
Options API:
```vue
<script>
export default {
onLoad() {
console.log(this.$refs.uniLayout)
},
}
</script>
```
## How It Works
The plugin does two things:
1. Auto-scans and globally registers components from `layouts/` directory
2. Wraps pages with the specified layout component
> **Note:** WeChat Mini Program limitation - If using `web-view` component, layout wrapping may not work properly.
## Configuration
See [type definitions](https://github.com/uni-helper/vite-plugin-uni-layouts/blob/main/src/types.ts) for all options.
<!--
Source references:
- https://github.com/uni-helper/vite-plugin-uni-layouts
- https://uni-helper.js.org/vite-plugin-uni-layouts
-->

View File

@ -0,0 +1,103 @@
---
name: vite-plugin-uni-manifest
description: Write uni-app manifest.json in TypeScript with type safety and IntelliSense
---
# vite-plugin-uni-manifest
Write your uni-app `manifest.json` configuration in TypeScript with full type safety and IntelliSense support.
## Installation
```bash
npm i -D @uni-helper/vite-plugin-uni-manifest
```
## Setup
```ts
// vite.config.ts
import Uni from '@dcloudio/vite-plugin-uni'
import UniManifest from '@uni-helper/vite-plugin-uni-manifest'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [UniManifest(), Uni()],
})
```
## Configuration File
Create `manifest.config.ts` (or .js/.json):
```ts
// manifest.config.ts
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
export default defineManifestConfig({
name: 'my-uni-app',
appid: '__UNI__XXXXXXX',
description: 'My awesome uni-app project',
versionName: '1.0.0',
versionCode: '100',
transformPx: false,
app: {
// App-specific config
},
mp-weixin: {
// WeChat Mini Program config
},
mp-alipay: {
// Alipay Mini Program config
},
h5: {
// H5 config
},
})
```
## Benefits
- **Type Safety**: Full TypeScript intellisense for all manifest properties
- **Code Reuse**: Import and reuse configuration values
- **Comments**: Add comments to explain configuration choices
- **Environment-based Config**: Use conditionals for different environments
## Example with Dynamic Config
```ts
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
import { version } from './package.json'
export default defineManifestConfig({
name: 'My App',
versionName: version,
versionCode: version.replace(/\./g, ''),
// Platform-specific config
'mp-weixin': {
appid: process.env.WX_APPID || '',
setting: {
es6: true,
enhance: true,
},
},
h5: {
title: 'My H5 App',
router: {
mode: 'hash',
},
},
})
```
## Configuration Options
See [types.ts](https://github.com/uni-helper/vite-plugin-uni-manifest/tree/main/packages/core/src/types.ts) for all available options.
<!--
Source references:
- https://github.com/uni-helper/vite-plugin-uni-manifest
- https://uni-helper.js.org/vite-plugin-uni-manifest
-->

View File

@ -0,0 +1,181 @@
---
name: vite-plugin-uni-middleware
description: Route middleware support for uni-app - implement guards, auth, and page lifecycle hooks
---
# vite-plugin-uni-middleware
Add Express-style middleware support to uni-app routing. Implement authentication guards, page analytics, and custom route logic.
## Installation
```bash
npm i -D @uni-helper/vite-plugin-uni-middleware
```
## Setup
```ts
// vite.config.ts
import Uni from '@dcloudio/vite-plugin-uni'
import UniMiddleware from '@uni-helper/vite-plugin-uni-middleware'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [UniMiddleware(), Uni()],
})
```
## Creating Middleware
Create middleware files in `src/middleware/` directory:
```ts
// src/middleware/auth.ts
import { defineMiddleware } from '@uni-helper/vite-plugin-middleware'
export default defineMiddleware((to, from, next) => {
const token = uni.getStorageSync('token')
if (!token && to.path !== '/pages/login/index') {
// Redirect to login
next('/pages/login/index')
} else {
// Continue navigation
next()
}
})
```
```ts
// src/middleware/analytics.ts
export default defineMiddleware((to, from, next) => {
// Track page view
uni.report('page_view', {
path: to.path,
from: from.path,
timestamp: Date.now(),
})
next()
})
```
```ts
// src/middleware/logger.ts
export default defineMiddleware((to, from, next) => {
console.log(`[Router] ${from.path} -> ${to.path}`)
next()
})
```
## Applying Middleware
### Via definePage (recommended)
```vue
<script setup>
definePage({
middlewares: ['auth', 'analytics'],
})
</script>
```
### Via pages.json
```json
{
"pages": [{
"path": "pages/profile/index",
"middlewares": ["auth"]
}]
}
```
### Global Middleware
Create `src/middleware/index.ts` for global middleware:
```ts
// src/middleware/index.ts
export default defineMiddleware((to, from, next) => {
// Runs on every navigation
console.log('Global middleware')
next()
})
```
## Middleware Context
```ts
interface MiddlewareContext {
to: {
path: string
query: Record<string, string>
meta?: Record<string, any>
}
from: {
path: string
query: Record<string, string>
}
next: (path?: string) => void
}
```
## Middleware Order
Middleware executes in declaration order:
```vue
<script setup>
definePage({
// Runs: logger -> auth -> analytics
middlewares: ['logger', 'auth', 'analytics'],
})
</script>
```
## Navigation Control
```ts
export default defineMiddleware((to, from, next) => {
// Continue to route
next()
// Redirect to different route
next('/pages/other/index')
// Redirect with query
next({
path: '/pages/login/index',
query: { redirect: to.path },
})
// Cancel navigation (stay on current page)
next(false)
})
```
## Async Middleware
```ts
export default defineMiddleware(async (to, from, next) => {
try {
const user = await fetchUserInfo()
if (user.isBanned) {
next('/pages/banned/index')
} else {
next()
}
} catch (error) {
next('/pages/error/index')
}
})
```
<!--
Source references:
- https://github.com/uni-helper/vite-plugin-uni-middleware
- https://uni-helper.js.org/vite-plugin-uni-middleware
-->

View File

@ -0,0 +1,186 @@
---
name: vite-plugin-uni-pages
description: File-based routing system for Vite-powered uni-app projects with automatic page discovery and TypeScript support
---
# vite-plugin-uni-pages
Provides file-based routing for uni-app projects using Vite. Automatically generates `pages.json` configuration from your file structure.
## Installation
```bash
npm i -D @uni-helper/vite-plugin-uni-pages
```
## Basic Setup
```ts
// vite.config.ts
import Uni from '@dcloudio/vite-plugin-uni'
import UniPages from '@uni-helper/vite-plugin-uni-pages'
import { defineConfig } from 'vite'
// Place UniPages before Uni
export default defineConfig({
plugins: [UniPages(), Uni()],
})
```
## TypeScript Support
Add types to `tsconfig.json`:
```json
{
"compilerOptions": {
"types": ["@uni-helper/vite-plugin-uni-pages"]
}
}
```
## Global Configuration
Create `pages.config.ts` (or .js/.json) for global settings:
```ts
// pages.config.ts
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
export default defineUniPages({
// Optional: manual pages have highest priority
pages: [],
globalStyle: {
navigationBarTextStyle: 'black',
navigationBarTitleText: '@uni-helper',
},
})
```
## Virtual Module
Access page metadata via virtual module:
```ts
/// <reference types="@uni-helper/vite-plugin-uni-pages/client" />
import { pages } from 'virtual:uni-pages'
console.log(pages)
```
## definePage Macro
Define page configuration directly in SFC files:
```vue
<script setup lang="ts">
// Object form
definePage({
style: {
navigationBarTitleText: 'hello world',
},
middlewares: ['auth'],
})
// Function form (supports TypeScript, async)
definePage(() => {
return {
style: {
navigationBarTitleText: 'dynamic title',
},
}
})
// Async form
definePage(async () => {
const title = await fetchTitle()
return {
style: { navigationBarTitleText: title },
}
})
</script>
```
### Important Notes for definePage
- Write inside `<script>` block
- `definePage` runs in a different scope than SFC (no access to SFC variables)
- Page path is auto-generated from file path
- Only one `definePage` per file
- Supports importing external libraries (pure JS only)
- Supports TypeScript type imports (types are stripped)
### Using with Condition Compile
```vue
<script setup>
import { isH5 } from '@uni-helper/uni-env'
definePage(() => {
const title = isH5 ? 'H5 Environment' : 'Other Platform'
return {
style: { navigationBarTitleText: title },
}
})
</script>
```
## Configuration Options
```ts
export interface Options {
/** Generate TypeScript declarations */
dts?: boolean | string // default: true (outputs uni-pages.d.ts)
/** Config file pattern */
configSource: string // default: 'pages.config.(ts|mts|cts|js|cjs|mjs|json)'
/** Default home page */
homePage: string // default: 'pages/index' or 'pages/index/index'
/** Merge with existing pages.json */
mergePages: boolean // default: true
/** Pages directory */
dir: string // default: 'src/pages'
/** Sub-packages directories */
subPackages: string[] // e.g., ['src/pages-sub']
/** Output directory for pages.json */
outDir: string // default: 'src'
/** Exclude patterns */
exclude: string[] // default: []
/** Route block language in SFC */
routeBlockLang: 'json5' | 'jsonc' | 'json' | 'yaml' | 'yml' // default: 'json5'
// Lifecycle hooks
onBeforeLoadUserConfig?: (ctx: PageContext) => void
onAfterLoadUserConfig?: (ctx: PageContext) => void
onBeforeScanPages?: (ctx: PageContext) => void
onAfterScanPages?: (ctx: PageContext) => void
onBeforeMergePageMetaData?: (ctx: PageContext) => void
onAfterMergePageMetaData?: (ctx: PageContext) => void
onBeforeWriteFile?: (ctx: PageContext) => void
onAfterWriteFile?: (ctx: PageContext) => void
}
```
## Usage with Layouts
```vue
<script setup>
definePage({
layout: 'default', // Specify layout
middlewares: ['auth'],
})
</script>
```
<!--
Source references:
- https://github.com/uni-helper/vite-plugin-uni-pages
- https://uni-helper.js.org/vite-plugin-uni-pages
-->

View File

@ -0,0 +1,119 @@
---
name: vite-plugin-uni-platform-modifier
description: Platform modifiers for attributes and directives in uni-app - optimize bundle size by removing unused platform code
---
# vite-plugin-uni-platform-modifier
Add platform modifiers to attributes and directives for conditional compilation at the template level. Reduces bundle size by removing code for other platforms.
## Installation
```bash
npm i -D @uni-helper/vite-plugin-uni-platform-modifier
```
## Setup
```ts
// vite.config.ts
import Uni from '@dcloudio/vite-plugin-uni'
import UniPlatformModifier from '@uni-helper/vite-plugin-uni-platform-modifier'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [UniPlatformModifier(), Uni()],
})
```
## Platform Modifiers
### Directive Modifiers
```vue
<template>
<!-- Only render on H5 -->
<view v-if.h5="condition">H5 only content</view>
<!-- Only render on WeChat -->
<button v-if.mp-weixin="true">WeChat specific</button>
<!-- Only render on App -->
<native-view v-if.app="true">Native view</native-view>
<!-- Platform-specific event -->
<view @click.h5="handleH5Click" @click.mp-weixin="handleWeChatClick">
Platform-specific click
</view>
</template>
```
### Attribute Modifiers
```vue
<template>
<!-- Different classes per platform -->
<view class.h5="h5-container" class.mp-weixin="wechat-container">
Content
</view>
<!-- Platform-specific styles -->
<view :style.h5="{ padding: '20px' }" :style.app="{ padding: '10px' }">
Styled content
</view>
</template>
```
## Supported Platforms
| Modifier | Platform |
|----------|----------|
| `.h5` | H5/Web |
| `.mp-weixin` | WeChat Mini Program |
| `.mp-alipay` | Alipay Mini Program |
| `.mp-baidu` | Baidu Smart Program |
| `.mp-toutiao` | Douyin Mini Program |
| `.mp-qq` | QQ Mini Program |
| `.app` | iOS/Android App |
## How It Works
The plugin transforms templates at build time:
```vue
<!-- Source -->
<view v-if.h5="show">H5 Only</view>
<!-- Transformed for H5 build -->
<view v-if="show">H5 Only</view>
<!-- Removed for non-H5 builds -->
```
## Use Cases
- Platform-specific UI components
- Different interactions per platform
- Platform-optimized styles
- Conditional event handlers
## Combining with Other Plugins
```ts
// vite.config.ts
export default defineConfig({
plugins: [
UniComponents(),
UniPages(),
UniPlatform(), // File-based platform
UniPlatformModifier(), // Directive/attribute modifiers
Uni(),
],
})
```
<!--
Source references:
- https://github.com/uni-helper/vite-plugin-uni-platform-modifier
- https://uni-helper.js.org/vite-plugin-uni-platform-modifier
-->

View File

@ -0,0 +1,105 @@
---
name: vite-plugin-uni-platform
description: File-based platform compilation for uni-app - compile different files based on target platform
---
# vite-plugin-uni-platform
Compile different files based on target platform using file naming conventions. Supports platform-specific implementations without conditional compilation.
## Installation
```bash
npm i -D @uni-helper/vite-plugin-uni-platform
```
## Setup
```ts
// vite.config.ts
import Uni from '@dcloudio/vite-plugin-uni'
import UniPlatform from '@uni-helper/vite-plugin-uni-platform'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [UniPlatform(), Uni()],
})
```
## File Naming Convention
Use platform suffixes in filenames:
```
src/
utils/
request.ts # Default implementation
request.h5.ts # H5-specific
request.mp-weixin.ts # WeChat Mini Program
request.app.ts # App (iOS/Android)
```
## Supported Platforms
| Suffix | Platform |
|--------|----------|
| `.h5.ts` | H5/Web |
| `.mp-weixin.ts` | WeChat Mini Program |
| `.mp-alipay.ts` | Alipay Mini Program |
| `.mp-baidu.ts` | Baidu Smart Program |
| `.mp-toutiao.ts` | Douyin Mini Program |
| `.mp-qq.ts` | QQ Mini Program |
| `.mp-kuaishou.ts` | Kuaishou Mini Program |
| `.app.ts` | iOS/Android App |
| `.app-plus.ts` | App (alternative) |
| `.harmony` | HarmonyOS |
## Import Usage
Import without the platform suffix:
```ts
// This automatically resolves to the correct platform version
import { request } from './utils/request'
```
## Example
```ts
// utils/request.ts - Default implementation
export const request = (options: RequestOptions) => {
return uni.request(options)
}
// utils/request.h5.ts - H5 with axios
import axios from 'axios'
export const request = (options: RequestOptions) => {
return axios.request(options)
}
// utils/request.mp-weixin.ts - WeChat specific
export const request = (options: RequestOptions) => {
// Add WeChat-specific headers
return uni.request({
...options,
header: {
...options.header,
'X-Platform': 'weixin',
},
})
}
```
## Benefits
- Cleaner code without `#ifdef` conditions
- Type-safe platform implementations
- Tree-shaking removes unused platform code
- Better IDE support and navigation
<!--
Source references:
- https://github.com/uni-helper/vite-plugin-uni-platform
- https://uni-helper.js.org/vite-plugin-uni-platform
-->

View File

@ -0,0 +1,153 @@
---
name: create-uni
description: CLI scaffolding tool for creating uni-app projects with best practices
---
# create-uni
Interactive CLI tool for scaffolding uni-app projects. Quickly create projects with your choice of templates, plugins, and UI libraries.
## Usage
```bash
# Create new project
npm create uni@latest
# With project name
npm create uni@latest my-project
# With options
npm create uni@latest my-project --ts --template vitesse -m pinia -u wot
```
## CLI Options
| Option | Shorthand | Description |
|--------|-----------|-------------|
| `--template` | `-t` | Select project template |
| `--typescript` | `--ts` | Enable TypeScript |
| `--eslint` | `-e` | Add ESLint |
| `--plugin` | `-p` | Add Vite plugins |
| `--module` | `-m` | Add functional modules |
| `--ui` | `-u` | Add UI component library |
| `--gui` | - | Launch GUI interface |
| `--info` | - | Show project info |
## Templates
| Template | Description | Value |
|----------|-------------|-------|
| Vitesse Uni App | Vite + uni-app starter with best practices | `vitesse` |
| Wot Starter | Based on vitesse with wot-ui | `wot-starter` |
| Wot Starter Retail | Retail/e-commerce template | `wot-starter-retail` |
| Unisave | Web-first uni-app development | `unisave` |
| TMUI 3.2 | Enterprise component library | `tmui32` |
| uView Pro Starter | uView Pro quick start | `uview-pro-starter` |
| uView Pro Demo | uView Pro full demo | `uview-pro-demo` |
## Available Plugins
| Plugin | CLI Value | Description |
|--------|-----------|-------------|
| vite-plugin-uni-components | `import` | Auto component import |
| vite-plugin-uni-pages | `pages` | File-based routing |
| vite-plugin-uni-layouts | `layouts` | Nuxt-like layouts |
| vite-plugin-uni-manifest | `manifest` | TypeScript manifest |
| vite-plugin-uni-platform | `filePlatform` | Platform-specific files |
| vite-plugin-uni-platform-modifier | `platformModifier` | Platform modifiers |
| vite-plugin-uni-middleware | `middleware` | Route middleware |
| uni-ku-root | `root` | Virtual root component |
## Available Modules
| Module | CLI Value | Description |
|--------|-----------|-------------|
| Pinia | `pinia` | State management |
| UnoCSS | `unocss` | Atomic CSS |
| uni-network | `uniNetwork` | HTTP client |
| uni-use | `uniUse` | Composable utilities |
| uni-promises | `uniPromises` | Promise wrappers |
| Uni ECharts | `uniEcharts` | Chart component |
| z-paging | `zPaging` | Pagination component |
## UI Libraries
| UI Library | CLI Value | Description |
|------------|-----------|-------------|
| Uni UI | `uni` | Official DCloud UI |
| Wot Design Uni | `wot` | Lightweight, beautiful |
| uView Pro | `uview-pro` | Vue 3 + TypeScript |
| NutUI Uniapp | `nut` | JD style, e-commerce |
| Skiyee UI | `skiyee` | Unique design language |
| UV UI | `uv` | Multi-platform |
| Ano UI | `ano` | UnoCSS-based |
## Examples
### Basic TypeScript Project
```bash
npm create uni@latest my-app --ts
```
### With Vitesse Template
```bash
npm create uni@latest my-app --ts --template vitesse
```
### Full-Featured Setup
```bash
npm create uni@latest my-app \
--ts \
--template vitesse \
-p pages -p layouts -p import \
-m pinia -m unocss -m uniUse \
-u wot \
-e
```
### GUI Mode
```bash
npm create uni@latest --gui
```
## Project Structure
Created projects follow this structure:
```
my-project/
├── src/
│ ├── components/ # Vue components
│ ├── composables/ # Composable functions
│ ├── layouts/ # Layout components (if layouts plugin)
│ ├── pages/ # Page components
│ ├── static/ # Static assets
│ ├── stores/ # Pinia stores (if pinia)
│ ├── App.vue
│ ├── main.ts
│ ├── manifest.config.ts # (if manifest plugin)
│ └── pages.config.ts # (if pages plugin)
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── uno.config.ts # (if unocss)
```
## Post-Creation
```bash
cd my-project
npm install
npm run dev
```
<!--
Source references:
- https://github.com/uni-helper/create-uni
- https://uni-helper.js.org/create-uni
-->

View File

@ -0,0 +1,263 @@
---
name: vitesse-uni-app
description: Vite-powered opinionated uni-app starter template with best practices
---
# vitesse-uni-app
Opinionated uni-app starter template powered by Vite. Includes best practices, pre-configured tooling, and modern development experience.
## Features
- ⚡️ **Vite** - Fast development and building
- 🌍 **uni-app** - Cross-platform development
- 🖖 **Vue 3** - Composition API, `<script setup>`
- 🔷 **TypeScript** - Type safety
- 🎨 **UnoCSS** - Atomic CSS
- 📦 **Auto Import** - Components and composables
- 🗂 **File-based Routing** - vite-plugin-uni-pages
- 📐 **Layout System** - Nuxt-like layouts
- 🔧 **ESLint** - Code linting
## Quick Start
```bash
# Create with create-uni
npm create uni@latest my-app --template vitesse
# Or clone directly
git clone https://github.com/uni-helper/vitesse-uni-app.git my-app
```
## Project Structure
```
vitesse-uni-app/
├── src/
│ ├── components/ # Auto-imported components
│ ├── composables/ # Auto-imported composables
│ ├── layouts/ # Layout components
│ │ ├── default.vue
│ │ └── home.vue
│ ├── pages/ # File-based routing
│ │ ├── index.vue
│ │ └── about/
│ │ └── index.vue
│ ├── static/ # Static assets
│ ├── stores/ # Pinia stores
│ ├── styles/ # Global styles
│ ├── App.vue
│ ├── main.ts
│ ├── manifest.config.ts # TypeScript manifest
│ └── pages.config.ts # Global pages config
├── types/ # Type definitions
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── uno.config.ts
└── eslint.config.js
```
## Pre-configured Plugins
The template includes these pre-configured plugins:
```ts
// vite.config.ts
export default defineConfig({
plugins: [
UniComponents({
resolvers: [AnoUIResolver()],
}),
UniPages(),
UniLayouts(),
UniManifest(),
Uni(),
],
})
```
## Auto Imports
### Components
Components in `src/components/` are auto-imported:
```vue
<template>
<!-- No import needed -->
<MyButton />
</template>
```
### Composables
Files in `src/composables/` are auto-imported:
```ts
// src/composables/useCounter.ts
export function useCounter() {
const count = ref(0)
const inc = () => count.value++
return { count, inc }
}
```
```vue
<script setup>
// No import needed
const { count, inc } = useCounter()
</script>
```
### Vue APIs
Vue APIs are auto-imported:
```vue
<script setup>
// No need to import ref, computed, etc.
const count = ref(0)
const doubled = computed(() => count.value * 2)
</script>
```
## Routing
File-based routing with pages in `src/pages/`:
```
pages/
├── index.vue # /pages/index/index
├── about.vue # /pages/about/index
└── user/
├── index.vue # /pages/user/index
└── [id].vue # /pages/user/:id
```
### Page Configuration
```vue
<script setup>
definePage({
style: {
navigationBarTitleText: 'About',
},
layout: 'default',
})
</script>
```
## Layouts
Create layouts in `src/layouts/`:
```vue
<!-- src/layouts/default.vue -->
<template>
<view class="layout">
<slot name="header" />
<slot />
<slot name="footer" />
</view>
</template>
```
Apply in pages:
```vue
<script setup>
definePage({
layout: 'default',
})
</script>
<template>
<template #header>
<NavBar />
</template>
<div>Content</div>
</template>
```
## Styling with UnoCSS
```vue
<template>
<view class="p-32 bg-gray-100">
<text class="text-32 font-bold text-primary">
Styled with UnoCSS
</text>
</view>
</template>
```
## State Management with Pinia
```ts
// src/stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const name = ref('')
const isLoggedIn = computed(() => !!name.value)
function login(userName: string) {
name.value = userName
}
return { name, isLoggedIn, login }
})
```
```vue
<script setup>
const user = useUserStore()
</script>
```
## Data Fetching
```vue
<script setup>
const { data: users, pending, error } = useFetch('/api/users')
</script>
<template>
<view v-if="pending">Loading...</view>
<view v-else-if="error">Error: {{ error.message }}</view>
<view v-else>
<view v-for="user in users" :key="user.id">
{{ user.name }}
</view>
</view>
</template>
```
## Deployment
### Build for production
```bash
npm run build
```
### Platform-specific builds
```bash
# H5
npm run build:h5
# WeChat Mini Program
npm run build:mp-weixin
# App
npm run build:app
```
<!--
Source references:
- https://github.com/uni-helper/vitesse-uni-app
- https://uni-helper.js.org/vitesse-uni-app
-->

View File

@ -0,0 +1,155 @@
---
name: uni-env
description: Environment detection utilities for uni-app - detect platform at runtime
---
# uni-env
Runtime environment detection utilities for uni-app. Detect the current platform in a type-safe way.
## Installation
```bash
npm i @uni-helper/uni-env
```
## Usage
```ts
import { isH5, isMpWeixin, isApp, isMp } from '@uni-helper/uni-env'
if (isH5) {
console.log('Running in H5')
}
if (isMpWeixin) {
console.log('Running in WeChat Mini Program')
}
```
## Available Detectors
### Platform Groups
| Detector | Description |
|----------|-------------|
| `isH5` | H5/Web platform |
| `isApp` | iOS/Android App |
| `isMp` | Any Mini Program |
| `isQuickapp` | Quick App |
### Mini Program Detectors
| Detector | Description |
|----------|-------------|
| `isMpWeixin` | WeChat Mini Program |
| `isMpAlipay` | Alipay Mini Program |
| `isMpBaidu` | Baidu Smart Program |
| `isMpToutiao` | Douyin Mini Program |
| `isMpQQ` | QQ Mini Program |
| `isMpKuaishou` | Kuaishou Mini Program |
| `isMpJd` | JD Mini Program |
| `isMpHarmony` | HarmonyOS Meta Service |
| `isMpLark` | Feishu/Lark Mini Program |
### Development Detectors
| Detector | Description |
|----------|-------------|
| `isDev` | Development environment |
| `isProd` | Production environment |
## Use Cases
### Conditional Logic
```ts
import { isH5, isMpWeixin, isApp } from '@uni-helper/uni-env'
function getStorageAdapter() {
if (isH5) {
return new LocalStorageAdapter()
}
if (isMpWeixin) {
return new WechatStorageAdapter()
}
if (isApp) {
return new NativeStorageAdapter()
}
return new UniStorageAdapter()
}
```
### Platform-Specific Features
```ts
import { isH5, isMpWeixin } from '@uni-helper/uni-env'
function share(content: ShareContent) {
if (isMpWeixin) {
// Use WeChat share API
wx.showShareMenu({ withShareTicket: true })
} else if (isH5) {
// Use Web Share API
navigator.share({
title: content.title,
url: content.url,
})
} else {
// Use uni-app generic share
uni.share({
title: content.title,
path: content.path,
})
}
}
```
### With definePage
```ts
import { isH5 } from '@uni-helper/uni-env'
definePage(() => {
const title = isH5 ? 'H5 Version' : 'Mobile Version'
return {
style: {
navigationBarTitleText: title,
},
}
})
```
## Important Note
This package is primarily for **plugin developers**. For runtime conditional compilation, use uni-app's [official conditional compilation](https://uniapp.dcloud.net.cn/tutorial/platform.html#preprocessor) with `/* #ifdef */` comments.
For better conditional compilation support, consider using [unplugin-preprocessor-directives](https://github.com/KeJunMao/unplugin-preprocessor-directives).
## Comparison with Conditional Compilation
```ts
// Runtime detection (uni-env)
import { isH5 } from '@uni-helper/uni-env'
if (isH5) {
// This code exists in all builds
doH5Thing()
}
// Build-time conditional compilation
// #ifdef H5
doH5Thing()
// #endif
// This code only exists in H5 builds
```
Choose based on your needs:
- Use **uni-env** for runtime detection when you need the same bundle for multiple platforms
- Use **conditional compilation** for build-time optimization and smaller bundles
<!--
Source references:
- https://github.com/uni-helper/uni-env
- https://uni-helper.js.org/uni-env
-->

View File

@ -0,0 +1,175 @@
---
name: unocss-preset-uni
description: UnoCSS preset for uni-app with platform-specific utilities and rpx support
---
# unocss-preset-uni
UnoCSS preset designed for uni-app development. Provides utilities for rpx units and platform-specific styling.
## Installation
```bash
npm i -D @uni-helper/unocss-preset-uni
```
## Setup
```ts
// uno.config.ts
import { defineConfig } from 'unocss'
import { presetUni } from '@uni-helper/unocss-preset-uni'
export default defineConfig({
presets: [
presetUni(),
],
})
```
## Features
- **rpx support**: Automatic conversion to rpx units
- **Platform variants**: Style targeting specific platforms
- **Safe area utilities**: Handle notches and safe areas
- **Compatible with**: Mini programs, H5, and App
## RPX Utilities
The preset automatically converts pixel values to rpx:
```html
<!-- These generate rpx units -->
<view class="w-100"></view> <!-- width: 100rpx -->
<view class="h-50"></view> <!-- height: 50rpx -->
<view class="m-20"></view> <!-- margin: 20rpx -->
<view class="p-16"></view> <!-- padding: 16rpx -->
<view class="text-32"></view> <!-- font-size: 32rpx -->
```
## Platform Variants
Apply styles to specific platforms:
```html
<!-- Only apply on H5 -->
<view class="h5:bg-red"></view>
<!-- Only apply on WeChat -->
<view class="mp-weixin:bg-blue"></view>
<!-- Only apply on App -->
<view class="app:bg-green"></view>
<!-- Combine with responsive -->
<view class="h5:p-20 mp-weixin:p-10"></view>
```
## Safe Area Utilities
```html
<!-- Safe area padding -->
<view class="pt-safe"></view> <!-- padding-top: env(safe-area-inset-top) -->
<view class="pb-safe"></view> <!-- padding-bottom: env(safe-area-inset-bottom) -->
<view class="px-safe"></view> <!-- padding-x with safe area -->
<!-- Safe area margin -->
<view class="mt-safe"></view>
<view class="mb-safe"></view>
<!-- Full safe area -->
<view class="safe-area"></view>
```
## Tab Bar Utilities
```html
<!-- Padding for tab bar -->
<view class="pb-tabbar"></view>
<!-- Margin for tab bar -->
<view class="mb-tabbar"></view>
```
## Status Bar Utilities
```html
<!-- Status bar height -->
<view class="h-statusbar"></view>
<view class="pt-statusbar"></view>
```
## Configuration
```ts
// uno.config.ts
import { defineConfig } from 'unocss'
import { presetUni } from '@uni-helper/unocss-preset-uni'
export default defineConfig({
presets: [
presetUni({
// rpx base width (default: 750)
baseWidth: 750,
// Enable platform variants
platform: true,
// Enable safe area utilities
safeArea: true,
// Custom platforms
platforms: ['mp-weixin', 'mp-alipay', 'h5', 'app'],
}),
],
})
```
## With Vite
```ts
// vite.config.ts
import Uni from '@dcloudio/vite-plugin-uni'
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
UnoCSS(),
Uni(),
],
})
```
## Example
```vue
<template>
<view class="page bg-gray-100">
<!-- Status bar spacing -->
<view class="h-statusbar"></view>
<!-- Header with safe area -->
<view class="px-32 pt-safe flex items-center justify-between">
<text class="text-36 font-bold">Title</text>
</view>
<!-- Content -->
<view class="p-32 flex flex-col gap-20">
<!-- Platform-specific styling -->
<view class="card h5:shadow-lg mp-weixin:border">
<text class="text-28 text-gray-800">Content</text>
</view>
</view>
<!-- Bottom safe area for devices with notches -->
<view class="pb-safe"></view>
</view>
</template>
```
<!--
Source references:
- https://github.com/uni-helper/unocss-preset-uni
- https://uni-helper.js.org/unocss-preset-uni
-->

View File

@ -0,0 +1,5 @@
# Generation Info
- **Source:** `sources/unocss`
- **Git SHA:** `2f7f267d0cc0c43d44357208aabb35b049359a08`
- **Generated:** 2026-01-28

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025-PRESENT Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,64 @@
---
name: unocss
description: UnoCSS instant atomic CSS engine, superset of Tailwind CSS. Use when configuring UnoCSS, writing utility rules, shortcuts, or working with presets like Wind, Icons, Attributify.
metadata:
author: Anthony Fu
version: "2026.1.28"
source: Generated from https://github.com/unocss/unocss, scripts located at https://github.com/antfu/skills
---
UnoCSS is an instant atomic CSS engine designed to be flexible and extensible. The core is un-opinionated - all CSS utilities are provided via presets. It's a superset of Tailwind CSS, so you can reuse your Tailwind knowledge for basic syntax usage.
**Important:** Before writing UnoCSS code, agents should check for `uno.config.*` or `unocss.config.*` files in the project root to understand what presets, rules, and shortcuts are available. If the project setup is unclear, avoid using attributify mode and other advanced features - stick to basic `class` usage.
> The skill is based on UnoCSS 66.x, generated at 2026-01-28.
## Core
| Topic | Description | Reference |
|-------|-------------|-----------|
| Configuration | Config file setup and all configuration options | [core-config](references/core-config.md) |
| Rules | Static and dynamic rules for generating CSS utilities | [core-rules](references/core-rules.md) |
| Shortcuts | Combine multiple rules into single shorthands | [core-shortcuts](references/core-shortcuts.md) |
| Theme | Theming system for colors, breakpoints, and design tokens | [core-theme](references/core-theme.md) |
| Variants | Apply variations like hover:, dark:, responsive to rules | [core-variants](references/core-variants.md) |
| Extracting | How UnoCSS extracts utilities from source code | [core-extracting](references/core-extracting.md) |
| Safelist & Blocklist | Force include or exclude specific utilities | [core-safelist](references/core-safelist.md) |
| Layers & Preflights | CSS layer ordering and raw CSS injection | [core-layers](references/core-layers.md) |
## Presets
### Main Presets
| Topic | Description | Reference |
|-------|-------------|-----------|
| Preset Wind3 | Tailwind CSS v3 / Windi CSS compatible preset (most common) | [preset-wind3](references/preset-wind3.md) |
| Preset Wind4 | Tailwind CSS v4 compatible preset with modern CSS features | [preset-wind4](references/preset-wind4.md) |
| Preset Mini | Minimal preset with essential utilities for custom builds | [preset-mini](references/preset-mini.md) |
### Feature Presets
| Topic | Description | Reference |
|-------|-------------|-----------|
| Preset Icons | Pure CSS icons using Iconify with any icon set | [preset-icons](references/preset-icons.md) |
| Preset Attributify | Group utilities in HTML attributes instead of class | [preset-attributify](references/preset-attributify.md) |
| Preset Typography | Prose classes for typographic defaults | [preset-typography](references/preset-typography.md) |
| Preset Web Fonts | Easy Google Fonts and other web fonts integration | [preset-web-fonts](references/preset-web-fonts.md) |
| Preset Tagify | Use utilities as HTML tag names | [preset-tagify](references/preset-tagify.md) |
| Preset Rem to Px | Convert rem units to px for utilities | [preset-rem-to-px](references/preset-rem-to-px.md) |
## Transformers
| Topic | Description | Reference |
|-------|-------------|-----------|
| Variant Group | Shorthand for grouping utilities with common prefixes | [transformer-variant-group](references/transformer-variant-group.md) |
| Directives | CSS directives: @apply, @screen, theme(), icon() | [transformer-directives](references/transformer-directives.md) |
| Compile Class | Compile multiple classes into one hashed class | [transformer-compile-class](references/transformer-compile-class.md) |
| Attributify JSX | Support valueless attributify in JSX/TSX | [transformer-attributify-jsx](references/transformer-attributify-jsx.md) |
## Integrations
| Topic | Description | Reference |
|-------|-------------|-----------|
| Vite Integration | Setting up UnoCSS with Vite and framework-specific tips | [integrations-vite](references/integrations-vite.md) |
| Nuxt Integration | UnoCSS module for Nuxt applications | [integrations-nuxt](references/integrations-nuxt.md) |

View File

@ -0,0 +1,5 @@
# Sync Info
- **Source:** `vendor/antfu/skills/unocss`
- **Git SHA:** `4086f59fa12d308fb05592ca8ce1704fbb940aa3`
- **Synced:** 2026-01-30

View File

@ -0,0 +1,187 @@
---
name: unocss-configuration
description: Config file setup and all configuration options for UnoCSS
---
# UnoCSS Configuration
UnoCSS is configured via a dedicated config file in your project root.
## Config File
**Recommended:** Use a dedicated `uno.config.ts` file for best IDE support and HMR.
```ts
// uno.config.ts
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetWebFonts,
presetWind3,
transformerDirectives,
transformerVariantGroup
} from 'unocss'
export default defineConfig({
shortcuts: [
// ...
],
theme: {
colors: {
// ...
}
},
presets: [
presetWind3(),
presetAttributify(),
presetIcons(),
presetTypography(),
presetWebFonts({
fonts: {
// ...
},
}),
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
})
```
UnoCSS automatically looks for `uno.config.{js,ts,mjs,mts}` or `unocss.config.{js,ts,mjs,mts}` in the project root.
## Key Configuration Options
### rules
Define CSS utility rules. Later entries have higher priority.
```ts
rules: [
['m-1', { margin: '0.25rem' }],
[/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
]
```
### shortcuts
Combine multiple rules into a single shorthand.
```ts
shortcuts: {
'btn': 'py-2 px-4 font-semibold rounded-lg shadow-md',
}
```
### theme
Theme object for design tokens shared between rules.
```ts
theme: {
colors: {
brand: '#942192',
},
breakpoints: {
sm: '640px',
md: '768px',
},
}
```
### presets
Predefined configurations bundling rules, variants, and themes.
```ts
presets: [
presetWind3(),
presetIcons(),
]
```
### transformers
Transform source code to support special syntax.
```ts
transformers: [
transformerDirectives(),
transformerVariantGroup(),
]
```
### variants
Preprocess selectors with ability to rewrite CSS output.
### extractors
Handle source files and extract utility class names.
### preflights
Inject raw CSS globally.
### layers
Control the order of CSS layers. Default is `0`.
```ts
layers: {
'components': -1,
'default': 1,
'utilities': 2,
}
```
### safelist
Utilities that are always included in output.
```ts
safelist: ['p-1', 'p-2', 'p-3']
```
### blocklist
Utilities that are always excluded.
```ts
blocklist: ['p-1', /^p-[2-4]$/]
```
### content
Configure where to extract utilities from.
```ts
content: {
pipeline: {
include: [/\.(vue|svelte|tsx|html)($|\?)/],
},
filesystem: ['src/**/*.php'],
}
```
### separators
Variant separator characters. Default: `[':', '-']`
### outputToCssLayers
Output UnoCSS layers as CSS Cascade Layers.
```ts
outputToCssLayers: true
```
## Specifying Config File Location
```ts
// vite.config.ts
import UnoCSS from 'unocss/vite'
export default defineConfig({
plugins: [
UnoCSS({
configFile: '../my-uno.config.ts',
}),
],
})
```
<!--
Source references:
- https://unocss.dev/guide/config-file
- https://unocss.dev/config/
-->

View File

@ -0,0 +1,137 @@
---
name: unocss-extracting
description: How UnoCSS extracts utilities from source code
---
# Extracting
UnoCSS searches for utility usages in your codebase and generates CSS on-demand.
## Content Sources
### Pipeline Extraction (Vite/Webpack)
Most efficient - extracts from build tool pipeline.
**Default file types:** `.jsx`, `.tsx`, `.vue`, `.md`, `.html`, `.svelte`, `.astro`, `.marko`
**Not included by default:** `.js`, `.ts`
```ts
export default defineConfig({
content: {
pipeline: {
include: [
/\.(vue|svelte|[jt]sx|mdx?|astro|html)($|\?)/,
'src/**/*.{js,ts}', // Add js/ts
],
},
},
})
```
### Filesystem Extraction
For files not in build pipeline:
```ts
export default defineConfig({
content: {
filesystem: [
'src/**/*.php',
'public/*.html',
],
},
})
```
### Inline Text Extraction
```ts
export default defineConfig({
content: {
inline: [
'<div class="p-4 text-red">Some text</div>',
async () => (await fetch('https://example.com')).text(),
],
},
})
```
## Magic Comments
### @unocss-include
Force scan a file:
```ts
// @unocss-include
export const classes = {
active: 'bg-primary text-white',
}
```
### @unocss-ignore
Skip entire file:
```ts
// @unocss-ignore
```
### @unocss-skip-start / @unocss-skip-end
Skip specific blocks:
```html
<p class="text-green">Extracted</p>
<!-- @unocss-skip-start -->
<p class="text-red">NOT extracted</p>
<!-- @unocss-skip-end -->
```
## Limitations
UnoCSS works at **build time** - dynamic classes don't work:
```html
<!-- Won't work! -->
<div class="p-${size}"></div>
```
### Solutions
**1. Safelist** - Pre-generate known values:
```ts
safelist: ['p-1', 'p-2', 'p-3', 'p-4']
```
**2. Static mapping** - List combinations statically:
```ts
const colors = {
red: 'text-red border-red',
blue: 'text-blue border-blue',
}
```
**3. Runtime** - Use `@unocss/runtime` for true runtime generation.
## Custom Extractors
```ts
extractors: [
{
name: 'my-extractor',
extract({ code }) {
return code.match(/class:[\w-]+/g) || []
},
},
]
```
<!--
Source references:
- https://unocss.dev/guide/extracting
-->

View File

@ -0,0 +1,104 @@
---
name: unocss-layers-preflights
description: CSS layer ordering and raw CSS injection
---
# Layers and Preflights
Control CSS output order and inject global CSS.
## Layers
Set layer on rules:
```ts
rules: [
[/^m-(\d)$/, ([, d]) => ({ margin: `${d / 4}rem` }), { layer: 'utilities' }],
['btn', { padding: '4px' }], // default layer
]
```
### Layer Ordering
```ts
layers: {
'components': -1,
'default': 1,
'utilities': 2,
}
```
### Import Layers Separately
```ts
import 'uno:components.css'
import 'uno.css'
import './my-custom.css'
import 'uno:utilities.css'
```
### CSS Cascade Layers
```ts
outputToCssLayers: true
// Or with custom names
outputToCssLayers: {
cssLayerName: (layer) => {
if (layer === 'default') return 'utilities'
if (layer === 'shortcuts') return 'utilities.shortcuts'
}
}
```
## Layer Variants
```html
<!-- UnoCSS layer -->
<p class="uno-layer-my-layer:text-xl">
<!-- CSS @layer -->
<p class="layer-my-layer:text-xl">
```
## Preflights
Inject raw CSS globally:
```ts
preflights: [
{
getCSS: ({ theme }) => `
* {
color: ${theme.colors.gray?.[700] ?? '#333'};
margin: 0;
}
`,
},
]
```
With layer:
```ts
preflights: [
{
layer: 'base',
getCSS: () => `html { font-family: system-ui; }`,
},
]
```
## preset-wind4 Layers
| Layer | Description | Order |
|-------|-------------|-------|
| `properties` | CSS @property rules | -200 |
| `theme` | Theme CSS variables | -150 |
| `base` | Reset styles | -100 |
<!--
Source references:
- https://unocss.dev/config/layers
- https://unocss.dev/config/preflights
-->

View File

@ -0,0 +1,166 @@
---
name: unocss-rules
description: Static and dynamic rules for generating CSS utilities in UnoCSS
---
# UnoCSS Rules
Rules define utility classes and the CSS they generate. UnoCSS has many built-in rules via presets and allows custom rules.
## Static Rules
Simple mapping from class name to CSS properties:
```ts
rules: [
['m-1', { margin: '0.25rem' }],
['font-bold', { 'font-weight': 700 }],
]
```
Usage: `<div class="m-1">` generates `.m-1 { margin: 0.25rem; }`
**Note:** Use CSS property syntax with hyphens (e.g., `font-weight` not `fontWeight`). Quote properties with hyphens.
## Dynamic Rules
Use RegExp matcher with function body for flexible utilities:
```ts
rules: [
// Match m-1, m-2, m-100, etc.
[/^m-(\d+)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
// Access theme and context
[/^p-(\d+)$/, (match, ctx) => ({ padding: `${match[1] / 4}rem` })],
]
```
The function receives:
1. RegExp match result (destructure to get captured groups)
2. Context object with `theme`, `symbols`, etc.
## CSS Fallback Values
Return 2D array for CSS property fallbacks (browser compatibility):
```ts
rules: [
[/^h-(\d+)dvh$/, ([_, d]) => [
['height', `${d}vh`],
['height', `${d}dvh`],
]],
]
```
Generates: `.h-100dvh { height: 100vh; height: 100dvh; }`
## Special Symbols
Control CSS output with symbols from `@unocss/core`:
```ts
import { symbols } from '@unocss/core'
rules: [
['grid', {
[symbols.parent]: '@supports (display: grid)',
display: 'grid',
}],
]
```
### Available Symbols
| Symbol | Description |
|--------|-------------|
| `symbols.parent` | Parent wrapper (e.g., `@supports`, `@media`) |
| `symbols.selector` | Function to modify the selector |
| `symbols.layer` | Set the UnoCSS layer |
| `symbols.variants` | Array of variant handlers |
| `symbols.shortcutsNoMerge` | Disable merging in shortcuts |
| `symbols.noMerge` | Disable rule merging |
| `symbols.sort` | Override sorting order |
| `symbols.body` | Full control of CSS body |
## Multi-Selector Rules
Use generator functions to yield multiple CSS rules:
```ts
rules: [
[/^button-(.*)$/, function* ([, color], { symbols }) {
yield { background: color }
yield {
[symbols.selector]: selector => `${selector}:hover`,
background: `color-mix(in srgb, ${color} 90%, black)`
}
}],
]
```
Generates both `.button-red { background: red; }` and `.button-red:hover { ... }`
## Fully Controlled Rules
Return a string for complete CSS control (advanced):
```ts
import { defineConfig, toEscapedSelector as e } from 'unocss'
rules: [
[/^custom-(.+)$/, ([, name], { rawSelector, theme }) => {
const selector = e(rawSelector)
return `
${selector} { font-size: ${theme.fontSize.sm}; }
${selector}::after { content: 'after'; }
@media (min-width: ${theme.breakpoints.sm}) {
${selector} { font-size: ${theme.fontSize.lg}; }
}
`
}],
]
```
**Warning:** Fully controlled rules don't work with variants like `hover:`.
## Symbols.body for Variant Support
Use `symbols.body` to keep variant support with custom CSS:
```ts
rules: [
['custom-red', {
[symbols.body]: `
font-size: 1rem;
&::after { content: 'after'; }
& > .bar { color: red; }
`,
[symbols.selector]: selector => `:is(${selector})`,
}]
]
```
## Rule Ordering
Later rules have higher priority. Dynamic rules output is sorted alphabetically within the group.
## Rule Merging
UnoCSS merges rules with identical CSS bodies:
```html
<div class="m-2 hover:m2">
```
Generates:
```css
.hover\:m2:hover, .m-2 { margin: 0.5rem; }
```
Use `symbols.noMerge` to disable.
<!--
Source references:
- https://unocss.dev/config/rules
-->

View File

@ -0,0 +1,105 @@
---
name: unocss-safelist-blocklist
description: Force include or exclude specific utilities
---
# Safelist and Blocklist
Control which utilities are always included or excluded.
## Safelist
Utilities always included, regardless of detection:
```ts
export default defineConfig({
safelist: [
'p-1', 'p-2', 'p-3',
// Dynamic generation
...Array.from({ length: 4 }, (_, i) => `p-${i + 1}`),
],
})
```
### Function Form
```ts
safelist: [
'p-1',
() => ['m-1', 'm-2'],
(context) => {
const colors = Object.keys(context.theme.colors || {})
return colors.map(c => `bg-${c}-500`)
},
]
```
### Common Use Cases
```ts
safelist: [
// Dynamic colors from CMS
() => ['primary', 'secondary'].flatMap(c => [
`bg-${c}`, `text-${c}`, `border-${c}`,
]),
// Component variants
() => {
const variants = ['primary', 'danger']
const sizes = ['sm', 'md', 'lg']
return variants.flatMap(v => sizes.map(s => `btn-${v}-${s}`))
},
]
```
## Blocklist
Utilities never generated:
```ts
blocklist: [
'p-1', // Exact match
/^p-[2-4]$/, // Regex
]
```
### With Messages
```ts
blocklist: [
['bg-red-500', { message: 'Use bg-red-600 instead' }],
[/^text-xs$/, { message: 'Use text-sm for accessibility' }],
]
```
## Safelist vs Blocklist
| Feature | Safelist | Blocklist |
|---------|----------|-----------|
| Purpose | Always include | Always exclude |
| Strings | ✅ | ✅ |
| Regex | ❌ | ✅ |
| Functions | ✅ | ❌ |
**Note:** Blocklist wins if utility is in both.
## Best Practice
Prefer static mappings over safelist:
```ts
// Better: UnoCSS extracts automatically
const sizes = {
sm: 'text-sm p-2',
md: 'text-base p-4',
}
// Avoid: Large safelist
safelist: ['text-sm', 'text-base', 'p-2', 'p-4']
```
<!--
Source references:
- https://unocss.dev/config/safelist
- https://unocss.dev/guide/extracting
-->

View File

@ -0,0 +1,89 @@
---
name: unocss-shortcuts
description: Combine multiple utility rules into single shorthand classes
---
# UnoCSS Shortcuts
Shortcuts combine multiple rules into a single shorthand, inspired by Windi CSS.
## Static Shortcuts
Define as an object mapping shortcut names to utility combinations:
```ts
shortcuts: {
// Multiple utilities combined
'btn': 'py-2 px-4 font-semibold rounded-lg shadow-md',
'btn-green': 'text-white bg-green-500 hover:bg-green-700',
// Single utility alias
'red': 'text-red-100',
}
```
Usage:
```html
<button class="btn btn-green">Click me</button>
```
## Dynamic Shortcuts
Use RegExp matcher with function, similar to dynamic rules:
```ts
shortcuts: [
// Static shortcuts can be in array too
{
btn: 'py-2 px-4 font-semibold rounded-lg shadow-md',
},
// Dynamic shortcut
[/^btn-(.*)$/, ([, c]) => `bg-${c}-400 text-${c}-100 py-2 px-4 rounded-lg`],
]
```
Now `btn-green` and `btn-red` generate:
```css
.btn-green {
padding: 0.5rem 1rem;
--un-bg-opacity: 1;
background-color: rgb(74 222 128 / var(--un-bg-opacity));
border-radius: 0.5rem;
--un-text-opacity: 1;
color: rgb(220 252 231 / var(--un-text-opacity));
}
```
## Accessing Theme in Shortcuts
Dynamic shortcuts receive context with theme access:
```ts
shortcuts: [
[/^badge-(.*)$/, ([, c], { theme }) => {
if (Object.keys(theme.colors).includes(c))
return `bg-${c}4:10 text-${c}5 rounded`
}],
]
```
## Shortcuts Layer
Shortcuts are output to the `shortcuts` layer by default. Configure with:
```ts
shortcutsLayer: 'my-shortcuts-layer'
```
## Key Points
- Later shortcuts have higher priority
- Shortcuts can reference other shortcuts
- Dynamic shortcuts work like dynamic rules
- Shortcuts are expanded at build time, not runtime
- All variants work with shortcuts (`hover:btn`, `dark:btn`, etc.)
<!--
Source references:
- https://unocss.dev/config/shortcuts
-->

View File

@ -0,0 +1,172 @@
---
name: unocss-theme
description: Theming system for colors, breakpoints, and design tokens
---
# UnoCSS Theme
UnoCSS supports theming similar to Tailwind CSS / Windi CSS. The `theme` property is deep-merged with the default theme.
## Basic Usage
```ts
theme: {
colors: {
veryCool: '#0000ff', // class="text-very-cool"
brand: {
primary: 'hsl(var(--hue, 217) 78% 51%)', // class="bg-brand-primary"
DEFAULT: '#942192' // class="bg-brand"
},
},
}
```
## Using Theme in Rules
Access theme values in dynamic rules:
```ts
rules: [
[/^text-(.*)$/, ([, c], { theme }) => {
if (theme.colors[c])
return { color: theme.colors[c] }
}],
]
```
## Using Theme in Variants
```ts
variants: [
{
name: 'variant-name',
match(matcher, { theme }) {
// Access theme.breakpoints, theme.colors, etc.
},
},
]
```
## Using Theme in Shortcuts
```ts
shortcuts: [
[/^badge-(.*)$/, ([, c], { theme }) => {
if (Object.keys(theme.colors).includes(c))
return `bg-${c}4:10 text-${c}5 rounded`
}],
]
```
## Breakpoints
**Warning:** Custom `breakpoints` object **overrides** the default, not merges.
```ts
theme: {
breakpoints: {
sm: '320px',
md: '640px',
},
}
```
Only `sm:` and `md:` variants will be available.
### Inherit Default Breakpoints
Use `extendTheme` to merge with defaults:
```ts
extendTheme: (theme) => {
return {
...theme,
breakpoints: {
...theme.breakpoints,
sm: '320px',
md: '640px',
},
}
}
```
**Note:** `verticalBreakpoints` works the same for vertical layout.
### Breakpoint Sorting
Breakpoints are sorted by size. Use consistent units to avoid errors:
```ts
theme: {
breakpoints: {
sm: '320px',
// Don't mix units - convert rem to px
// md: '40rem', // Bad
md: `${40 * 16}px`, // Good
lg: '960px',
},
}
```
## ExtendTheme
`extendTheme` lets you modify the merged theme object:
### Mutate Theme
```ts
extendTheme: (theme) => {
theme.colors.veryCool = '#0000ff'
theme.colors.brand = {
primary: 'hsl(var(--hue, 217) 78% 51%)',
}
}
```
### Replace Theme
Return a new object to completely replace:
```ts
extendTheme: (theme) => {
return {
...theme,
colors: {
...theme.colors,
veryCool: '#0000ff',
},
}
}
```
## Theme Differences in Presets
### preset-wind3 vs preset-wind4
| preset-wind3 | preset-wind4 |
|--------------|--------------|
| `fontFamily` | `font` |
| `fontSize` | `text.fontSize` |
| `lineHeight` | `text.lineHeight` or `leading` |
| `letterSpacing` | `text.letterSpacing` or `tracking` |
| `borderRadius` | `radius` |
| `easing` | `ease` |
| `breakpoints` | `breakpoint` |
| `boxShadow` | `shadow` |
| `transitionProperty` | `property` |
## Common Theme Keys
- `colors` - Color palette
- `breakpoints` - Responsive breakpoints
- `fontFamily` - Font stacks
- `fontSize` - Text sizes
- `spacing` - Spacing scale
- `borderRadius` - Border radius values
- `boxShadow` - Shadow definitions
- `animation` - Animation keyframes and timing
<!--
Source references:
- https://unocss.dev/config/theme
-->

View File

@ -0,0 +1,175 @@
---
name: unocss-variants
description: Apply variations like hover:, dark:, responsive to rules
---
# UnoCSS Variants
Variants apply modifications to utility rules, like `hover:`, `dark:`, or responsive prefixes.
## How Variants Work
When matching `hover:m-2`:
1. `hover:m-2` is extracted from source
2. Sent to all variants for matching
3. `hover:` variant matches and returns `m-2`
4. Result `m-2` continues to next variants
5. Finally matches the rule `.m-2 { margin: 0.5rem; }`
6. Variant transformation applied: `.hover\:m-2:hover { margin: 0.5rem; }`
## Creating Custom Variants
```ts
variants: [
// hover: variant
(matcher) => {
if (!matcher.startsWith('hover:'))
return matcher
return {
// Remove prefix, pass to next variants/rules
matcher: matcher.slice(6),
// Modify the selector
selector: s => `${s}:hover`,
}
},
],
rules: [
[/^m-(\d)$/, ([, d]) => ({ margin: `${d / 4}rem` })],
]
```
## Variant Return Object
- `matcher` - The processed class name to pass forward
- `selector` - Function to customize the CSS selector
- `parent` - Wrapper like `@media`, `@supports`
- `layer` - Specify output layer
- `sort` - Control ordering
## Built-in Variants (preset-wind3)
### Pseudo-classes
- `hover:`, `focus:`, `active:`, `visited:`
- `first:`, `last:`, `odd:`, `even:`
- `disabled:`, `checked:`, `required:`
- `focus-within:`, `focus-visible:`
### Pseudo-elements
- `before:`, `after:`
- `placeholder:`, `selection:`
- `marker:`, `file:`
### Responsive
- `sm:`, `md:`, `lg:`, `xl:`, `2xl:`
- `lt-sm:` (less than sm)
- `at-lg:` (at lg only)
### Dark Mode
- `dark:` - Class-based dark mode (default)
- `@dark:` - Media query dark mode
### Group/Peer
- `group-hover:`, `group-focus:`
- `peer-checked:`, `peer-focus:`
### Container Queries
- `@container`, `@sm:`, `@md:`
### Print
- `print:`
### Supports
- `supports-[display:grid]:`
### Aria
- `aria-checked:`, `aria-disabled:`
### Data Attributes
- `data-[state=open]:`
## Dark Mode Configuration
### Class-based (default)
```ts
presetWind3({
dark: 'class'
})
```
```html
<div class="dark:bg-gray-800">
```
Generates: `.dark .dark\:bg-gray-800 { ... }`
### Media Query
```ts
presetWind3({
dark: 'media'
})
```
Generates: `@media (prefers-color-scheme: dark) { ... }`
### Opt-in Media Query
Use `@dark:` regardless of config:
```html
<div class="@dark:bg-gray-800">
```
### Custom Selectors
```ts
presetWind3({
dark: {
light: '.light-mode',
dark: '.dark-mode',
}
})
```
## CSS @layer Variant
Native CSS `@layer` support:
```html
<div class="layer-foo:p-4" />
```
Generates:
```css
@layer foo {
.layer-foo\:p-4 { padding: 1rem; }
}
```
## Breakpoint Differences from Windi CSS
| Windi CSS | UnoCSS |
|-----------|--------|
| `<sm:p-1` | `lt-sm:p-1` |
| `@lg:p-1` | `at-lg:p-1` |
| `>xl:p-1` | `xl:p-1` |
## Media Hover (Experimental)
Addresses sticky hover on touch devices:
```html
<div class="@hover-text-red">
```
Generates:
```css
@media (hover: hover) and (pointer: fine) {
.\@hover-text-red:hover { color: rgb(248 113 113); }
}
```
<!--
Source references:
- https://unocss.dev/config/variants
- https://unocss.dev/presets/wind3
- https://unocss.dev/presets/mini
-->

View File

@ -0,0 +1,199 @@
---
name: unocss-nuxt-integration
description: UnoCSS module for Nuxt applications
---
# UnoCSS Nuxt Integration
The official Nuxt module for UnoCSS.
## Installation
```bash
pnpm add -D unocss @unocss/nuxt
```
Add to Nuxt config:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@unocss/nuxt',
],
})
```
Create config file:
```ts
// uno.config.ts
import { defineConfig, presetWind3 } from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
],
})
```
**Note:** The `uno.css` entry is automatically injected by the module.
## Support Status
| Build Tool | Nuxt 2 | Nuxt Bridge | Nuxt 3 |
|------------|--------|-------------|--------|
| Webpack Dev | ✅ | ✅ | 🚧 |
| Webpack Build | ✅ | ✅ | ✅ |
| Vite Dev | - | ✅ | ✅ |
| Vite Build | - | ✅ | ✅ |
## Configuration
### Using uno.config.ts (Recommended)
Use a dedicated config file for best IDE support:
```ts
// uno.config.ts
import { defineConfig, presetWind3, presetIcons } from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
presetIcons(),
],
shortcuts: {
'btn': 'py-2 px-4 font-semibold rounded-lg',
},
})
```
### Nuxt Layers Support
Enable automatic config merging from Nuxt layers:
```ts
// nuxt.config.ts
export default defineNuxtConfig({
unocss: {
nuxtLayers: true,
},
})
```
Then in your root config:
```ts
// uno.config.ts
import config from './.nuxt/uno.config.mjs'
export default config
```
Or extend the merged config:
```ts
// uno.config.ts
import { mergeConfigs } from '@unocss/core'
import config from './.nuxt/uno.config.mjs'
export default mergeConfigs([config, {
// Your overrides
shortcuts: {
'custom': 'text-red-500',
},
}])
```
## Common Setup Example
```ts
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@unocss/nuxt',
],
})
```
```ts
// uno.config.ts
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetWebFonts,
presetWind3,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
presetAttributify(),
presetIcons({
scale: 1.2,
}),
presetTypography(),
presetWebFonts({
fonts: {
sans: 'DM Sans',
mono: 'DM Mono',
},
}),
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
shortcuts: [
['btn', 'px-4 py-1 rounded inline-block bg-teal-600 text-white cursor-pointer hover:bg-teal-700 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'],
],
})
```
## Usage in Components
```vue
<template>
<div class="p-4 text-center">
<h1 class="text-3xl font-bold text-blue-600">
Hello UnoCSS!
</h1>
<button class="btn mt-4">
Click me
</button>
</div>
</template>
```
With attributify mode:
```vue
<template>
<div p="4" text="center">
<h1 text="3xl blue-600" font="bold">
Hello UnoCSS!
</h1>
</div>
</template>
```
## Inspector
In development, visit `/_nuxt/__unocss` to access the UnoCSS inspector.
## Key Differences from Vite
- No need to import `virtual:uno.css` - automatically injected
- Config file discovery works the same
- All Vite plugin features available
- Nuxt layers config merging available
<!--
Source references:
- https://unocss.dev/integrations/nuxt
-->

View File

@ -0,0 +1,283 @@
---
name: unocss-vite-integration
description: Setting up UnoCSS with Vite and framework-specific tips
---
# UnoCSS Vite Integration
The Vite plugin is the most common way to use UnoCSS.
## Installation
```bash
pnpm add -D unocss
```
```ts
// vite.config.ts
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [
UnoCSS(),
],
})
```
Create config file:
```ts
// uno.config.ts
import { defineConfig, presetWind3 } from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
],
})
```
Add to entry:
```ts
// main.ts
import 'virtual:uno.css'
```
## Modes
### global (default)
Standard mode - generates global CSS injected via `uno.css` import.
```ts
import 'virtual:uno.css'
```
### vue-scoped
Injects generated CSS into Vue SFC `<style scoped>`.
```ts
UnoCSS({
mode: 'vue-scoped',
})
```
### shadow-dom
For Web Components using Shadow DOM:
```ts
UnoCSS({
mode: 'shadow-dom',
})
```
Add placeholder in component styles:
```ts
const template = document.createElement('template')
template.innerHTML = `
<style>
:host { ... }
@unocss-placeholder
</style>
<div class="m-1em">...</div>
`
```
### per-module (experimental)
Generates CSS per module with optional scoping.
### dist-chunk (experimental)
Generates CSS per chunk on build for MPA.
## DevTools Support
Edit classes directly in browser DevTools:
```ts
import 'virtual:uno.css'
import 'virtual:unocss-devtools'
```
**Warning:** Uses MutationObserver to detect changes. Dynamic classes from scripts will also be included.
## Framework-Specific Setup
### React
```ts
// vite.config.ts
import React from '@vitejs/plugin-react'
import UnoCSS from 'unocss/vite'
export default {
plugins: [
UnoCSS(), // Must be before React when using attributify
React(),
],
}
```
**Note:** Remove `tsc` from build script if using `@unocss/preset-attributify`.
### Vue
Works out of the box with `@vitejs/plugin-vue`.
### Svelte
```ts
import { svelte } from '@sveltejs/vite-plugin-svelte'
import extractorSvelte from '@unocss/extractor-svelte'
import UnoCSS from 'unocss/vite'
export default {
plugins: [
UnoCSS({
extractors: [extractorSvelte()],
}),
svelte(),
],
}
```
Supports `class:foo` and `class:foo={bar}` syntax.
### SvelteKit
Same as Svelte, use `sveltekit()` from `@sveltejs/kit/vite`.
### Solid
```ts
import UnoCSS from 'unocss/vite'
import solidPlugin from 'vite-plugin-solid'
export default {
plugins: [
UnoCSS(),
solidPlugin(),
],
}
```
### Preact
```ts
import Preact from '@preact/preset-vite'
import UnoCSS from 'unocss/vite'
export default {
plugins: [
UnoCSS(),
Preact(),
],
}
```
### Elm
```ts
import Elm from 'vite-plugin-elm'
import UnoCSS from 'unocss/vite'
export default {
plugins: [
Elm(),
UnoCSS(),
],
}
```
### Web Components (Lit)
```ts
UnoCSS({
mode: 'shadow-dom',
shortcuts: [
{ 'cool-blue': 'bg-blue-500 text-white' },
],
})
```
```ts
// my-element.ts
@customElement('my-element')
export class MyElement extends LitElement {
static styles = css`
:host { ... }
@unocss-placeholder
`
}
```
Supports `part-[<part-name>]:<utility>` for `::part` styling.
## Inspector
Visit `http://localhost:5173/__unocss` in dev mode to:
- Inspect generated CSS rules
- See applied classes per file
- Test utilities in REPL
## Legacy Browser Support
With `@vitejs/plugin-legacy`:
```ts
import legacy from '@vitejs/plugin-legacy'
import UnoCSS from 'unocss/vite'
export default {
plugins: [
UnoCSS({
legacy: {
renderModernChunks: false,
},
}),
legacy({
targets: ['defaults', 'not IE 11'],
renderModernChunks: false,
}),
],
}
```
## VanillaJS / TypeScript
By default, `.js` and `.ts` files are not extracted. Configure to include:
```ts
// uno.config.ts
export default defineConfig({
content: {
pipeline: {
include: [
/\.(vue|svelte|[jt]sx|html)($|\?)/,
'src/**/*.{js,ts}',
],
},
},
})
```
Or use magic comment in files:
```ts
// @unocss-include
export const classes = {
active: 'bg-primary text-white',
}
```
<!--
Source references:
- https://unocss.dev/integrations/vite
-->

View File

@ -0,0 +1,142 @@
---
name: preset-attributify
description: Group utilities in HTML attributes instead of class
---
# Preset Attributify
Group utilities in HTML attributes for better readability.
## Installation
```ts
import { defineConfig, presetAttributify, presetWind3 } from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
presetAttributify(),
],
})
```
## Basic Usage
Instead of long class strings:
```html
<button class="bg-blue-400 hover:bg-blue-500 text-sm text-white font-mono font-light py-2 px-4 rounded border-2 border-blue-200">
Button
</button>
```
Group by prefix in attributes:
```html
<button
bg="blue-400 hover:blue-500"
text="sm white"
font="mono light"
p="y-2 x-4"
border="2 rounded blue-200"
>
Button
</button>
```
## Prefix Self-Referencing
For utilities matching their prefix (`flex`, `grid`, `border`), use `~`:
```html
<!-- Before -->
<button class="border border-red">Button</button>
<!-- After -->
<button border="~ red">Button</button>
```
## Valueless Attributify
Use utilities as boolean attributes:
```html
<div m-2 rounded text-teal-400 />
```
## Handling Property Conflicts
When attribute names conflict with HTML properties:
```html
<!-- Use un- prefix -->
<a un-text="red">Text color to red</a>
```
### Enforce Prefix
```ts
presetAttributify({
prefix: 'un-',
prefixedOnly: true,
})
```
## Options
```ts
presetAttributify({
strict: false, // Only generate CSS for attributify
prefix: 'un-', // Attribute prefix
prefixedOnly: false, // Require prefix for all
nonValuedAttribute: true, // Support valueless attributes
ignoreAttributes: [], // Attributes to ignore
trueToNonValued: false, // Treat value="true" as valueless
})
```
## TypeScript Support
### Vue 3
```ts
// html.d.ts
declare module '@vue/runtime-dom' {
interface HTMLAttributes { [key: string]: any }
}
declare module '@vue/runtime-core' {
interface AllowedComponentProps { [key: string]: any }
}
export {}
```
### React
```ts
import type { AttributifyAttributes } from '@unocss/preset-attributify'
declare module 'react' {
interface HTMLAttributes<T> extends AttributifyAttributes {}
}
```
## JSX Support
For JSX where `<div foo>` becomes `<div foo={true}>`:
```ts
import { transformerAttributifyJsx } from 'unocss'
export default defineConfig({
transformers: [
transformerAttributifyJsx(),
],
})
```
**Important:** Only use attributify if `uno.config.*` shows `presetAttributify()` is enabled.
<!--
Source references:
- https://unocss.dev/presets/attributify
-->

View File

@ -0,0 +1,184 @@
---
name: preset-icons
description: Pure CSS icons using Iconify with any icon set
---
# Preset Icons
Use any icon as a pure CSS class, powered by Iconify.
## Installation
```bash
pnpm add -D @unocss/preset-icons @iconify-json/[collection-name]
```
Example: `@iconify-json/mdi` for Material Design Icons, `@iconify-json/carbon` for Carbon icons.
```ts
import { defineConfig, presetIcons } from 'unocss'
export default defineConfig({
presets: [
presetIcons(),
],
})
```
## Usage
Two naming conventions:
- `<prefix><collection>-<icon>` → `i-ph-anchor-simple-thin`
- `<prefix><collection>:<icon>` → `i-ph:anchor-simple-thin`
```html
<!-- Phosphor anchor icon -->
<div class="i-ph-anchor-simple-thin" />
<!-- Material Design alarm with color -->
<div class="i-mdi-alarm text-orange-400" />
<!-- Large Vue logo -->
<div class="i-logos-vue text-3xl" />
<!-- Dynamic: Sun in light mode, Moon in dark -->
<button class="i-carbon-sun dark:i-carbon-moon" />
<!-- Hover effect -->
<div class="i-twemoji-grinning-face-with-smiling-eyes hover:i-twemoji-face-with-tears-of-joy" />
```
Browse icons at [icones.js.org](https://icones.js.org/) or [Iconify](https://icon-sets.iconify.design/).
## Icon Modes
Icons automatically choose between `mask` (monochrome) and `background-img` (colorful).
### Force Specific Mode
- `?mask` - Render as mask (colorable with `currentColor`)
- `?bg` - Render as background image (preserves original colors)
```html
<!-- Original with colors -->
<div class="i-vscode-icons:file-type-light-pnpm" />
<!-- Force mask mode, apply custom color -->
<div class="i-vscode-icons:file-type-light-pnpm?mask text-red-300" />
```
## Options
```ts
presetIcons({
scale: 1.2, // Scale relative to font size
prefix: 'i-', // Class prefix (default)
mode: 'auto', // 'auto' | 'mask' | 'bg'
extraProperties: {
'display': 'inline-block',
'vertical-align': 'middle',
},
warn: true, // Warn on missing icons
autoInstall: true, // Auto-install missing icon sets
cdn: 'https://esm.sh/', // CDN for browser usage
})
```
## Custom Icon Collections
### Inline SVGs
```ts
presetIcons({
collections: {
custom: {
circle: '<svg viewBox="0 0 120 120"><circle cx="60" cy="60" r="50"></circle></svg>',
},
}
})
```
Usage: `<span class="i-custom:circle"></span>`
### File System Loader
```ts
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
presetIcons({
collections: {
'my-icons': FileSystemIconLoader(
'./assets/icons',
svg => svg.replace(/#fff/, 'currentColor')
),
}
})
```
### Dynamic Import (Browser)
```ts
import presetIcons from '@unocss/preset-icons/browser'
presetIcons({
collections: {
carbon: () => import('@iconify-json/carbon/icons.json').then(i => i.default),
}
})
```
## Icon Customization
```ts
presetIcons({
customizations: {
// Transform SVG
transform(svg, collection, icon) {
return svg.replace(/#fff/, 'currentColor')
},
// Global sizing
customize(props) {
props.width = '2em'
props.height = '2em'
return props
},
// Per-collection
iconCustomizer(collection, icon, props) {
if (collection === 'mdi') {
props.width = '2em'
}
}
}
})
```
## CSS Directive
Use `icon()` in CSS (requires transformer-directives):
```css
.icon {
background-image: icon('i-carbon-sun');
}
.icon-colored {
background-image: icon('i-carbon-moon', '#fff');
}
```
## Accessibility
```html
<!-- With label -->
<a href="/profile" aria-label="Profile" class="i-ph:user-duotone"></a>
<!-- Decorative -->
<a href="/profile">
<span aria-hidden="true" class="i-ph:user-duotone"></span>
My Profile
</a>
```
<!--
Source references:
- https://unocss.dev/presets/icons
-->

View File

@ -0,0 +1,158 @@
---
name: preset-mini
description: Minimal preset with essential utilities for UnoCSS
---
# Preset Mini
The minimal preset with only essential rules and variants. Good starting point for custom presets.
## Installation
```ts
import { defineConfig, presetMini } from 'unocss'
export default defineConfig({
presets: [
presetMini(),
],
})
```
## What's Included
Subset of `preset-wind3` with essential utilities aligned to CSS properties:
- Basic spacing (margin, padding)
- Display (flex, grid, block, etc.)
- Positioning (absolute, relative, fixed)
- Sizing (width, height)
- Colors (text, background, border)
- Typography basics (font-size, font-weight)
- Borders and border-radius
- Basic transforms and transitions
## What's NOT Included
Opinionated or complex Tailwind utilities:
- `container`
- Complex animations
- Gradients
- Advanced typography
- Prose classes
## Use Cases
1. **Building custom presets** - Start with mini and add only what you need
2. **Minimal bundle size** - When you only need basic utilities
3. **Learning** - Understand UnoCSS core without Tailwind complexity
## Dark Mode
Same as preset-wind3:
```ts
presetMini({
dark: 'class' // or 'media'
})
```
```html
<div class="dark:bg-red:10" />
```
Class-based:
```css
.dark .dark\:bg-red\:10 {
background-color: rgb(248 113 113 / 0.1);
}
```
Media query:
```css
@media (prefers-color-scheme: dark) {
.dark\:bg-red\:10 {
background-color: rgb(248 113 113 / 0.1);
}
}
```
## CSS @layer Variant
Native CSS layer support:
```html
<div class="layer-foo:p4" />
```
```css
@layer foo {
.layer-foo\:p4 {
padding: 1rem;
}
}
```
## Theme Customization
```ts
presetMini({
theme: {
colors: {
veryCool: '#0000ff',
brand: {
primary: 'hsl(var(--hue, 217) 78% 51%)',
}
},
}
})
```
**Note:** `breakpoints` property is overridden, not merged.
## Options
```ts
presetMini({
// Dark mode: 'class' | 'media' | { light: string, dark: string }
dark: 'class',
// Generate [group=""] instead of .group for attributify
attributifyPseudo: false,
// CSS variable prefix (default: 'un-')
variablePrefix: 'un-',
// Utility prefix
prefix: undefined,
// Preflight generation: true | false | 'on-demand'
preflight: true,
})
```
## Building on Mini
Create custom preset extending mini:
```ts
import { presetMini } from 'unocss'
import type { Preset } from 'unocss'
export const myPreset: Preset = {
name: 'my-preset',
presets: [presetMini()],
rules: [
// Add custom rules
['card', { 'border-radius': '8px', 'box-shadow': '0 2px 8px rgba(0,0,0,0.1)' }],
],
shortcuts: {
'btn': 'px-4 py-2 rounded bg-blue-500 text-white',
},
}
```
<!--
Source references:
- https://unocss.dev/presets/mini
-->

View File

@ -0,0 +1,97 @@
---
name: preset-rem-to-px
description: Convert rem units to px for utilities
---
# Preset Rem to Px
Converts `rem` units to `px` in generated utilities.
## Installation
```ts
import { defineConfig, presetRemToPx, presetWind3 } from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
presetRemToPx(),
],
})
```
## What It Does
Transforms all rem values to px:
```html
<div class="p-4">
```
Without preset:
```css
.p-4 { padding: 1rem; }
```
With preset:
```css
.p-4 { padding: 16px; }
```
## Use Cases
- Projects requiring pixel-perfect designs
- Environments where rem doesn't work well
- Consistency with pixel-based design systems
- Email templates (better compatibility)
## Options
```ts
presetRemToPx({
// Base font size for conversion (default: 16)
baseFontSize: 16,
})
```
Custom base:
```ts
presetRemToPx({
baseFontSize: 14, // 1rem = 14px
})
```
## With Preset Wind4
**Note:** `presetRemToPx` is not needed with `preset-wind4`. Use the built-in processor instead:
```ts
import { createRemToPxProcessor } from '@unocss/preset-wind4/utils'
export default defineConfig({
presets: [
presetWind4({
preflights: {
theme: {
process: createRemToPxProcessor(),
}
},
}),
],
// Also apply to utilities
postprocess: [createRemToPxProcessor()],
})
```
## Important Notes
- Order matters: place after the preset that generates rem values
- Affects all utilities with rem units
- Theme values in rem are also converted
<!--
Source references:
- https://unocss.dev/presets/rem-to-px
- https://unocss.dev/presets/wind4
-->

View File

@ -0,0 +1,134 @@
---
name: preset-tagify
description: Use utilities as HTML tag names
---
# Preset Tagify
Use CSS utilities directly as HTML tag names.
## Installation
```ts
import { defineConfig, presetTagify } from 'unocss'
export default defineConfig({
presets: [
presetTagify(),
],
})
```
## Basic Usage
Instead of:
```html
<span class="text-red">red text</span>
<div class="flex">flexbox</div>
<span class="i-line-md-emoji-grin"></span>
```
Use tag names directly:
```html
<text-red>red text</text-red>
<flex>flexbox</flex>
<i-line-md-emoji-grin />
```
Works exactly the same!
## With Prefix
```ts
presetTagify({
prefix: 'un-'
})
```
```html
<!-- Matched -->
<un-flex></un-flex>
<!-- Not matched -->
<flex></flex>
```
## Extra Properties
Add CSS properties to matched tags:
```ts
presetTagify({
// Add display: inline-block to icons
extraProperties: matched => matched.startsWith('i-')
? { display: 'inline-block' }
: {},
})
```
Or apply to all:
```ts
presetTagify({
extraProperties: { display: 'block' }
})
```
## Options
```ts
presetTagify({
// Tag prefix
prefix: '',
// Excluded tags (won't be processed)
excludedTags: ['b', /^h\d+$/, 'table'],
// Extra CSS properties
extraProperties: {},
// Enable default extractor
defaultExtractor: true,
})
```
## Excluded Tags
By default, these tags are excluded:
- `b` (bold)
- `h1` through `h6` (headings)
- `table`
Add your own:
```ts
presetTagify({
excludedTags: [
'b',
/^h\d+$/,
'table',
'article', // Add custom exclusions
/^my-/, // Exclude tags starting with 'my-'
],
})
```
## Use Cases
- Quick prototyping
- Cleaner HTML for simple pages
- Icon embedding: `<i-carbon-sun />`
- Semantic-like styling: `<flex-center>`, `<text-red>`
## Limitations
- Custom element names must contain a hyphen (HTML spec)
- Some frameworks may not support all custom elements
- Utilities without hyphens need the prefix option
<!--
Source references:
- https://unocss.dev/presets/tagify
-->

View File

@ -0,0 +1,95 @@
---
name: preset-typography
description: Prose classes for typographic defaults on vanilla HTML content
---
# Preset Typography
Prose classes for adding typographic defaults to vanilla HTML content.
## Installation
```ts
import { defineConfig, presetTypography, presetWind3 } from 'unocss'
export default defineConfig({
presets: [
presetWind3(), // Required!
presetTypography(),
],
})
```
## Basic Usage
```html
<article class="prose">
<h1>My Article</h1>
<p>This is styled with typographic defaults...</p>
</article>
```
## Sizes
```html
<article class="prose prose-sm">Small</article>
<article class="prose prose-base">Base (default)</article>
<article class="prose prose-lg">Large</article>
<article class="prose prose-xl">Extra large</article>
<article class="prose prose-2xl">2X large</article>
```
Responsive:
```html
<article class="prose prose-sm md:prose-base lg:prose-lg">
Responsive typography
</article>
```
## Colors
```html
<article class="prose prose-gray">Gray (default)</article>
<article class="prose prose-slate">Slate</article>
<article class="prose prose-blue">Blue</article>
```
## Dark Mode
```html
<article class="prose dark:prose-invert">
Dark mode typography
</article>
```
## Excluding Elements
```html
<article class="prose">
<p>Styled</p>
<div class="not-prose">
<p>NOT styled</p>
</div>
</article>
```
**Note:** `not-prose` only works as a class.
## Options
```ts
presetTypography({
selectorName: 'prose', // Custom selector
cssVarPrefix: '--un-prose', // CSS variable prefix
important: false, // Make !important
cssExtend: {
'code': { color: '#8b5cf6' },
'a:hover': { color: '#f43f5e' },
},
})
```
<!--
Source references:
- https://unocss.dev/presets/typography
-->

View File

@ -0,0 +1,91 @@
---
name: preset-web-fonts
description: Easy Google Fonts and other web fonts integration
---
# Preset Web Fonts
Easily use web fonts from Google Fonts and other providers.
## Installation
```ts
import { defineConfig, presetWebFonts, presetWind3 } from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
presetWebFonts({
provider: 'google',
fonts: {
sans: 'Roboto',
mono: 'Fira Code',
},
}),
],
})
```
## Providers
- `google` - Google Fonts (default)
- `bunny` - Privacy-friendly alternative
- `fontshare` - Quality fonts by ITF
- `fontsource` - Self-hosted open source fonts
- `coollabs` - Privacy-friendly drop-in replacement
- `none` - Treat as system font
## Font Configuration
```ts
fonts: {
// Simple
sans: 'Roboto',
// Multiple (fallback)
mono: ['Fira Code', 'Fira Mono:400,700'],
// Detailed
lato: [
{
name: 'Lato',
weights: ['400', '700'],
italic: true,
},
{
name: 'sans-serif',
provider: 'none',
},
],
}
```
## Usage
```html
<p class="font-sans">Roboto</p>
<code class="font-mono">Fira Code</code>
```
## Local Fonts
Self-host fonts:
```ts
import { createLocalFontProcessor } from '@unocss/preset-web-fonts/local'
presetWebFonts({
provider: 'none',
fonts: { sans: 'Roboto' },
processors: createLocalFontProcessor({
cacheDir: 'node_modules/.cache/unocss/fonts',
fontAssetsDir: 'public/assets/fonts',
fontServeBaseUrl: '/assets/fonts',
})
})
```
<!--
Source references:
- https://unocss.dev/presets/web-fonts
-->

View File

@ -0,0 +1,194 @@
---
name: preset-wind3
description: Tailwind CSS / Windi CSS compatible preset for UnoCSS
---
# Preset Wind3
The Tailwind CSS / Windi CSS compatible preset. Most commonly used preset for UnoCSS.
## Installation
```ts
import { defineConfig, presetWind3 } from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
],
})
```
**Note:** `@unocss/preset-uno` and `@unocss/preset-wind` are deprecated and renamed to `@unocss/preset-wind3`.
## Features
- Full Tailwind CSS v3 compatibility
- Dark mode (`dark:`, `@dark:`)
- All responsive variants (`sm:`, `md:`, `lg:`, `xl:`, `2xl:`)
- All standard utilities (flex, grid, spacing, colors, typography, etc.)
- Animation support (includes Animate.css animations)
## Dark Mode
### Class-based (default)
```html
<div class="dark:bg-gray-800">
```
Generates: `.dark .dark\:bg-gray-800 { ... }`
### Media Query Based
```ts
presetWind3({
dark: 'media'
})
```
Generates: `@media (prefers-color-scheme: dark) { ... }`
### Opt-in Media Query
Use `@dark:` regardless of config:
```html
<div class="@dark:bg-gray-800">
```
## Options
```ts
presetWind3({
// Dark mode strategy
dark: 'class', // 'class' | 'media' | { light: '.light', dark: '.dark' }
// Generate pseudo selector as [group=""] instead of .group
attributifyPseudo: false,
// CSS custom properties prefix
variablePrefix: 'un-',
// Utils prefix
prefix: '',
// Generate preflight CSS
preflight: true, // true | false | 'on-demand'
// Mark all utilities as !important
important: false, // boolean | string (selector)
})
```
### Important Option
Make all utilities `!important`:
```ts
presetWind3({
important: true,
})
```
Or scope with selector to increase specificity without `!important`:
```ts
presetWind3({
important: '#app',
})
```
Output: `#app :is(.dark .dark\:bg-blue) { ... }`
## Differences from Tailwind CSS
### Quotes Not Supported
Template quotes don't work due to extractor:
```html
<!-- Won't work -->
<div class="before:content-['']">
<!-- Use shortcut instead -->
<div class="before:content-empty">
```
### Background Position
Use `position:` prefix for custom values:
```html
<!-- Tailwind -->
<div class="bg-[center_top_1rem]">
<!-- UnoCSS -->
<div class="bg-[position:center_top_1rem]">
```
### Animations
UnoCSS integrates Animate.css. Use `-alt` suffix for Animate.css versions when names conflict:
- `animate-bounce` - Tailwind version
- `animate-bounce-alt` - Animate.css version
Custom animations:
```ts
theme: {
animation: {
keyframes: {
custom: '{0%, 100% { opacity: 0; } 50% { opacity: 1; }}',
},
durations: {
custom: '1s',
},
timingFns: {
custom: 'ease-in-out',
},
counts: {
custom: 'infinite',
},
}
}
```
## Differences from Windi CSS
| Windi CSS | UnoCSS |
|-----------|--------|
| `<sm:p-1` | `lt-sm:p-1` |
| `@lg:p-1` | `at-lg:p-1` |
| `>xl:p-1` | `xl:p-1` |
Bracket syntax uses `_` instead of `,`:
```html
<!-- Windi CSS -->
<div class="grid-cols-[1fr,10px,max-content]">
<!-- UnoCSS -->
<div class="grid-cols-[1fr_10px_max-content]">
```
## Experimental: Media Hover
Addresses sticky hover on touch devices:
```html
<div class="@hover-text-red">
```
Generates:
```css
@media (hover: hover) and (pointer: fine) {
.\@hover-text-red:hover { ... }
}
```
<!--
Source references:
- https://unocss.dev/presets/wind3
-->

View File

@ -0,0 +1,247 @@
---
name: preset-wind4
description: Tailwind CSS v4 compatible preset with enhanced features
---
# Preset Wind4
The Tailwind CSS v4 compatible preset. Enhances preset-wind3 with modern CSS features.
## Installation
```ts
import { defineConfig, presetWind4 } from 'unocss'
export default defineConfig({
presets: [
presetWind4(),
],
})
```
## Key Differences from Wind3
### Built-in CSS Reset
No need for `@unocss/reset` - reset is built-in:
```ts
// Remove these imports
import '@unocss/reset/tailwind.css' // ❌ Not needed
import '@unocss/reset/tailwind-compat.css' // ❌ Not needed
// Enable in config
presetWind4({
preflights: {
reset: true,
},
})
```
### OKLCH Color Model
Uses `oklch` for better color perception and contrast. Not compatible with `presetLegacyCompat`.
### Theme CSS Variables
Automatically generates CSS variables from theme:
```css
:root, :host {
--spacing: 0.25rem;
--font-sans: ui-sans-serif, system-ui, sans-serif;
--colors-black: #000;
--colors-white: #fff;
/* ... */
}
```
### @property CSS Rules
Uses `@property` for better browser optimization:
```css
@property --un-text-opacity {
syntax: '<percentage>';
inherits: false;
initial-value: 100%;
}
```
### Theme Key Changes
| preset-wind3 | preset-wind4 |
|--------------|--------------|
| `fontFamily` | `font` |
| `fontSize` | `text.fontSize` |
| `lineHeight` | `text.lineHeight` or `leading` |
| `letterSpacing` | `text.letterSpacing` or `tracking` |
| `borderRadius` | `radius` |
| `easing` | `ease` |
| `breakpoints` | `breakpoint` |
| `verticalBreakpoints` | `verticalBreakpoint` |
| `boxShadow` | `shadow` |
| `transitionProperty` | `property` |
| `container.maxWidth` | `containers.maxWidth` |
| Size properties (`width`, `height`, etc.) | Unified to `spacing` |
## Options
```ts
presetWind4({
preflights: {
// Built-in reset styles
reset: true,
// Theme CSS variables generation
theme: 'on-demand', // true | false | 'on-demand'
// @property CSS rules
property: true,
},
})
```
### Theme Variable Processing
Convert rem to px for theme variables:
```ts
import { createRemToPxProcessor } from '@unocss/preset-wind4/utils'
presetWind4({
preflights: {
theme: {
mode: 'on-demand',
process: createRemToPxProcessor(),
}
},
})
// Also apply to utilities
export default defineConfig({
postprocess: [createRemToPxProcessor()],
})
```
### Property Layer Customization
```ts
presetWind4({
preflights: {
property: {
// Custom parent wrapper
parent: '@layer custom-properties',
// Custom selector
selector: ':where(*, ::before, ::after)',
},
},
})
```
Remove `@supports` wrapper:
```ts
presetWind4({
preflights: {
property: {
parent: false,
},
},
})
```
## Generated Layers
| Layer | Description | Order |
|-------|-------------|-------|
| `properties` | CSS `@property` rules | -200 |
| `theme` | Theme CSS variables | -150 |
| `base` | Reset/preflight styles | -100 |
## Theme.defaults
Global default configuration for reset styles:
```ts
import type { Theme } from '@unocss/preset-wind4/theme'
const defaults: Theme['default'] = {
transition: {
duration: '150ms',
timingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
},
font: {
family: 'var(--font-sans)',
featureSettings: 'var(--font-sans--font-feature-settings)',
variationSettings: 'var(--font-sans--font-variation-settings)',
},
monoFont: {
family: 'var(--font-mono)',
// ...
},
}
```
## Compatibility Notes
### presetRemToPx
Not needed - use built-in processor instead:
```ts
presetWind4({
preflights: {
theme: {
process: createRemToPxProcessor(),
}
},
})
```
### presetLegacyCompat
**Not compatible** with preset-wind4 due to `oklch` color model.
## Migration from Wind3
1. Update theme keys according to the table above
2. Remove `@unocss/reset` imports
3. Enable `preflights.reset: true`
4. Test color outputs (oklch vs rgb)
5. Update any custom theme extensions
```ts
// Before (wind3)
theme: {
fontFamily: { sans: 'Roboto' },
fontSize: { lg: '1.125rem' },
breakpoints: { sm: '640px' },
}
// After (wind4)
theme: {
font: { sans: 'Roboto' },
text: { lg: { fontSize: '1.125rem' } },
breakpoint: { sm: '640px' },
}
```
## When to Use Wind4
Choose **preset-wind4** when:
- Starting a new project
- Targeting modern browsers
- Want built-in reset and CSS variables
- Following Tailwind v4 conventions
Choose **preset-wind3** when:
- Need legacy browser support
- Migrating from Tailwind v3
- Using presetLegacyCompat
- Want stable, proven preset
<!--
Source references:
- https://unocss.dev/presets/wind4
-->

View File

@ -0,0 +1,156 @@
---
name: transformer-attributify-jsx
description: Support valueless attributify in JSX/TSX
---
# Transformer Attributify JSX
Fixes valueless attributify mode in JSX where `<div foo>` becomes `<div foo={true}>`.
## The Problem
In JSX, valueless attributes are transformed:
```jsx
// You write
<div m-2 rounded text-teal-400 />
// JSX compiles to
<div m-2={true} rounded={true} text-teal-400={true} />
```
The `={true}` breaks UnoCSS attributify detection.
## Installation
```ts
import {
defineConfig,
presetAttributify,
transformerAttributifyJsx
} from 'unocss'
export default defineConfig({
presets: [
presetAttributify(),
],
transformers: [
transformerAttributifyJsx(),
],
})
```
## How It Works
The transformer converts JSX boolean attributes back to strings:
```jsx
// Input (after JSX compilation)
<div m-2={true} rounded={true} />
// Output (transformed)
<div m-2="" rounded="" />
```
Now UnoCSS can properly extract the attributify classes.
## Options
```ts
transformerAttributifyJsx({
// Only transform specific attributes
// Default: transforms all that match attributify patterns
blocklist: ['text', 'font'],
})
```
## When to Use
Required when using:
- React
- Preact
- Solid
- Any JSX-based framework
With valueless attributify syntax:
```jsx
// This needs the transformer
<div flex items-center gap-4 />
// This works without transformer (has values)
<div flex="~" items="center" gap="4" />
```
## Framework Setup
### React
```ts
// vite.config.ts
import React from '@vitejs/plugin-react'
import UnoCSS from 'unocss/vite'
export default {
plugins: [
UnoCSS(), // Must be before React
React(),
],
}
```
```ts
// uno.config.ts
import {
defineConfig,
presetAttributify,
presetWind3,
transformerAttributifyJsx
} from 'unocss'
export default defineConfig({
presets: [
presetWind3(),
presetAttributify(),
],
transformers: [
transformerAttributifyJsx(),
],
})
```
### Preact
Same as React, use `@preact/preset-vite` or `@prefresh/vite`.
### Solid
```ts
import UnoCSS from 'unocss/vite'
import solidPlugin from 'vite-plugin-solid'
export default {
plugins: [
UnoCSS(),
solidPlugin(),
],
}
```
## TypeScript Support
Add type declarations:
```ts
// shims.d.ts
import type { AttributifyAttributes } from '@unocss/preset-attributify'
declare module 'react' {
interface HTMLAttributes<T> extends AttributifyAttributes {}
}
```
<!--
Source references:
- https://unocss.dev/transformers/attributify-jsx
-->

View File

@ -0,0 +1,128 @@
---
name: transformer-compile-class
description: Compile multiple classes into one hashed class
---
# Transformer Compile Class
Compiles multiple utility classes into a single hashed class for smaller HTML.
## Installation
```ts
import { defineConfig, transformerCompileClass } from 'unocss'
export default defineConfig({
transformers: [
transformerCompileClass(),
],
})
```
## Usage
Add `:uno:` prefix to mark classes for compilation:
```html
<!-- Before -->
<div class=":uno: text-center sm:text-left">
<div class=":uno: text-sm font-bold hover:text-red" />
</div>
<!-- After -->
<div class="uno-qlmcrp">
<div class="uno-0qw2gr" />
</div>
```
## Generated CSS
```css
.uno-qlmcrp {
text-align: center;
}
.uno-0qw2gr {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 700;
}
.uno-0qw2gr:hover {
--un-text-opacity: 1;
color: rgb(248 113 113 / var(--un-text-opacity));
}
@media (min-width: 640px) {
.uno-qlmcrp {
text-align: left;
}
}
```
## Options
```ts
transformerCompileClass({
// Custom trigger string (default: ':uno:')
trigger: ':uno:',
// Custom class prefix (default: 'uno-')
classPrefix: 'uno-',
// Hash function for class names
hashFn: (str) => /* custom hash */,
// Keep original classes alongside compiled
keepOriginal: false,
})
```
## Use Cases
- **Smaller HTML** - Reduce repetitive class strings
- **Obfuscation** - Hide utility class names in production
- **Performance** - Fewer class attributes to parse
## ESLint Integration
Enforce compile class usage across project:
```json
{
"rules": {
"@unocss/enforce-class-compile": "warn"
}
}
```
This rule:
- Warns when class attribute doesn't start with `:uno:`
- Auto-fixes by adding the prefix
Options:
```json
{
"rules": {
"@unocss/enforce-class-compile": ["warn", {
"prefix": ":uno:",
"enableFix": true
}]
}
}
```
## Combining with Other Transformers
```ts
export default defineConfig({
transformers: [
transformerVariantGroup(), // Process variant groups first
transformerDirectives(), // Then directives
transformerCompileClass(), // Compile last
],
})
```
<!--
Source references:
- https://unocss.dev/transformers/compile-class
-->

View File

@ -0,0 +1,157 @@
---
name: transformer-directives
description: CSS directives @apply, @screen, theme(), and icon()
---
# Transformer Directives
Enables `@apply`, `@screen`, `theme()`, and `icon()` directives in CSS.
## Installation
```ts
import { defineConfig, transformerDirectives } from 'unocss'
export default defineConfig({
transformers: [
transformerDirectives(),
],
})
```
## @apply
Apply utility classes in CSS:
```css
.custom-btn {
@apply py-2 px-4 font-semibold rounded-lg;
}
/* With variants - use quotes */
.custom-btn {
@apply 'hover:bg-blue-600 focus:ring-2';
}
```
### CSS Custom Property Alternative
For vanilla CSS compatibility:
```css
.custom-div {
--at-apply: text-center my-0 font-medium;
}
```
Supported aliases: `--at-apply`, `--uno-apply`, `--uno`
Configure aliases:
```ts
transformerDirectives({
applyVariable: ['--at-apply', '--uno-apply', '--uno'],
// or disable: applyVariable: false
})
```
## @screen
Create breakpoint media queries:
```css
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
@screen sm {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
@screen lg {
.grid {
grid-template-columns: repeat(4, 1fr);
}
}
```
### Breakpoint Variants
```css
/* Less than breakpoint */
@screen lt-sm {
.item { display: none; }
}
/* At specific breakpoint only */
@screen at-md {
.item { width: 50%; }
}
```
## theme()
Access theme values in CSS:
```css
.btn-blue {
background-color: theme('colors.blue.500');
padding: theme('spacing.4');
border-radius: theme('borderRadius.lg');
}
```
Dot notation paths into your theme config.
## icon()
Convert icon utility to SVG (requires preset-icons):
```css
.icon-sun {
background-image: icon('i-carbon-sun');
}
/* With custom color */
.icon-moon {
background-image: icon('i-carbon-moon', '#fff');
}
/* Using theme color */
.icon-alert {
background-image: icon('i-carbon-warning', 'theme("colors.red.500")');
}
```
## Complete Example
```css
.card {
@apply rounded-lg shadow-md p-4;
background-color: theme('colors.white');
}
.card-header {
@apply 'font-bold text-lg border-b';
padding-bottom: theme('spacing.2');
}
@screen md {
.card {
@apply flex gap-4;
}
}
.card-icon {
background-image: icon('i-carbon-document');
@apply w-6 h-6;
}
```
<!--
Source references:
- https://unocss.dev/transformers/directives
-->

View File

@ -0,0 +1,97 @@
---
name: transformer-variant-group
description: Shorthand for grouping utilities with common prefixes
---
# Transformer Variant Group
Enables shorthand syntax for grouping utilities with common prefixes.
## Installation
```ts
import { defineConfig, transformerVariantGroup } from 'unocss'
export default defineConfig({
transformers: [
transformerVariantGroup(),
],
})
```
## Usage
Group multiple utilities under one variant prefix using parentheses:
```html
<!-- Before transformation -->
<div class="hover:(bg-gray-400 font-medium) font-(light mono)" />
<!-- After transformation -->
<div class="hover:bg-gray-400 hover:font-medium font-light font-mono" />
```
## Examples
### Hover States
```html
<button class="hover:(bg-blue-600 text-white scale-105)">
Hover me
</button>
```
Expands to: `hover:bg-blue-600 hover:text-white hover:scale-105`
### Dark Mode
```html
<div class="dark:(bg-gray-800 text-white)">
Dark content
</div>
```
Expands to: `dark:bg-gray-800 dark:text-white`
### Responsive
```html
<div class="md:(flex items-center gap-4)">
Responsive flex
</div>
```
Expands to: `md:flex md:items-center md:gap-4`
### Nested Groups
```html
<div class="lg:hover:(bg-blue-500 text-white)">
Large screen hover
</div>
```
Expands to: `lg:hover:bg-blue-500 lg:hover:text-white`
### Multiple Prefixes
```html
<div class="text-(sm gray-600) font-(medium mono)">
Styled text
</div>
```
Expands to: `text-sm text-gray-600 font-medium font-mono`
## Key Points
- Use parentheses `()` to group utilities
- The prefix applies to all utilities inside the group
- Can be combined with any variant (hover, dark, responsive, etc.)
- Nesting is supported
- Works in class attributes and other extraction sources
<!--
Source references:
- https://unocss.dev/transformers/variant-group
-->

View File

@ -0,0 +1,5 @@
# Generation Info
- **Source:** `sources/vite`
- **Git SHA:** `b40292ce6a7dbbbbac9c6dae5f126b7f44c3e1b7`
- **Generated:** 2026-01-28

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025-PRESENT Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,50 @@
---
name: vite
description: Vite next-generation frontend build tool with fast HMR and optimized builds. Use when configuring Vite, adding plugins, working with dev server, or building for production.
metadata:
author: Anthony Fu
version: "2026.1.28"
source: Generated from https://github.com/vitejs/vite, scripts located at https://github.com/antfu/skills
---
Vite is a modern build tool for frontend development featuring instant server start with native ES modules, lightning-fast HMR, and optimized production builds using Rolldown/Rollup. It supports TypeScript, JSX, CSS pre-processors out of the box and has a rich plugin ecosystem.
> The skill is based on Vite 6.x, generated at 2026-01-28.
## Core
| Topic | Description | Reference |
|-------|-------------|-----------|
| Configuration | Config file setup, defineConfig, conditional and async configs | [core-config](references/core-config.md) |
| CLI Commands | Dev server, build, preview commands and options | [core-cli](references/core-cli.md) |
| Core Features | TypeScript, JSX, CSS, HTML processing, JSON handling | [core-features](references/core-features.md) |
| Using Plugins | Adding, configuring, and ordering plugins | [core-plugins](references/core-plugins.md) |
## Features
| Topic | Description | Reference |
|-------|-------------|-----------|
| CSS Handling | CSS modules, pre-processors, PostCSS, Lightning CSS | [features-css](references/features-css.md) |
| Static Assets | Asset imports, public directory, URL handling | [features-assets](references/features-assets.md) |
| Glob Import | import.meta.glob, dynamic imports, batch loading | [features-glob-import](references/features-glob-import.md) |
| Environment Variables | .env files, modes, import.meta.env constants | [features-env](references/features-env.md) |
| HMR API | Hot Module Replacement client API | [features-hmr](references/features-hmr.md) |
| Web Workers | Worker imports and configuration | [features-workers](references/features-workers.md) |
| Dependency Pre-Bundling | optimizeDeps, caching, monorepo setup | [features-dep-bundling](references/features-dep-bundling.md) |
## Build
| Topic | Description | Reference |
|-------|-------------|-----------|
| Production Build | Build options, browser targets, multi-page apps | [build-production](references/build-production.md) |
| Library Mode | Building libraries with proper package exports | [build-library](references/build-library.md) |
| SSR | Server-side rendering setup and configuration | [build-ssr](references/build-ssr.md) |
## Advanced
| Topic | Description | Reference |
|-------|-------------|-----------|
| JavaScript API | createServer, build, preview programmatic APIs | [advanced-api](references/advanced-api.md) |
| Plugin API | Creating Vite plugins, hooks, virtual modules | [advanced-plugin-api](references/advanced-plugin-api.md) |
| Performance | Optimization tips for dev server and builds | [advanced-performance](references/advanced-performance.md) |
| Backend Integration | Integrating Vite with traditional backends | [advanced-backend](references/advanced-backend.md) |

Some files were not shown because too many files have changed in this diff Show More