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:
parent
721df313ee
commit
b553c4e519
672
AGENTS.md
672
AGENTS.md
@ -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>` 上写死
|
||||||
|
|||||||
@ -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/
|
||||||
|
|||||||
@ -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/
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
351
docs/API.md
Normal 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` | 编辑组织 |
|
||||||
@ -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)");
|
|
||||||
});
|
|
||||||
@ -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
548
docs/package-lock.json
generated
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"docx": "^9.7.1",
|
|
||||||
"marked": "^18.0.5",
|
|
||||||
"puppeteer": "^25.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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');
|
|
||||||
})();
|
|
||||||
25
docs/开发文档.md
25
docs/开发文档.md
@ -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. 关门后系统自动检测门磁闭合
|
||||||
|
|||||||
347
docs/开发计划.md
347
docs/开发计划.md
@ -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读取电表数据,上报平台显示正确 |
|
|
||||||
56
software/api-server/Cargo.lock
generated
56
software/api-server/Cargo.lock
generated
@ -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"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "api-server"
|
name = "pms-api"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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"
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 充电控制
|
// 充电控制
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
5
software/h5-uniapp/.agents/skills/pinia/GENERATION.md
Normal file
5
software/h5-uniapp/.agents/skills/pinia/GENERATION.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Generation Info
|
||||||
|
|
||||||
|
- **Source:** `sources/pinia`
|
||||||
|
- **Git SHA:** `55dbfc5c20d4461748996aa74d8c0913e89fb98e`
|
||||||
|
- **Generated:** 2026-01-28
|
||||||
21
software/h5-uniapp/.agents/skills/pinia/LICENSE.md
Normal file
21
software/h5-uniapp/.agents/skills/pinia/LICENSE.md
Normal 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.
|
||||||
59
software/h5-uniapp/.agents/skills/pinia/SKILL.md
Normal file
59
software/h5-uniapp/.agents/skills/pinia/SKILL.md
Normal 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
|
||||||
5
software/h5-uniapp/.agents/skills/pinia/SYNC.md
Normal file
5
software/h5-uniapp/.agents/skills/pinia/SYNC.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Sync Info
|
||||||
|
|
||||||
|
- **Source:** `vendor/antfu/skills/pinia`
|
||||||
|
- **Git SHA:** `4086f59fa12d308fb05592ca8ce1704fbb940aa3`
|
||||||
|
- **Synced:** 2026-01-30
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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/
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
54
software/h5-uniapp/.agents/skills/uni-app/GENERATION.md
Normal file
54
software/h5-uniapp/.agents/skills/uni-app/GENERATION.md
Normal 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
|
||||||
113
software/h5-uniapp/.agents/skills/uni-app/SKILL.md
Normal file
113
software/h5-uniapp/.agents/skills/uni-app/SKILL.md
Normal 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 进行安装。
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
# Generation Info
|
||||||
|
|
||||||
|
- **Source:** `sources/uni-helper`
|
||||||
|
- **Git SHA:** `286d8656dc077a7d9d6a50e647284ff1ea48ac6b`
|
||||||
|
- **Generated:** 2026-01-30
|
||||||
88
software/h5-uniapp/.agents/skills/uni-helper/SKILL.md
Normal file
88
software/h5-uniapp/.agents/skills/uni-helper/SKILL.md
Normal 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
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
5
software/h5-uniapp/.agents/skills/unocss/GENERATION.md
Normal file
5
software/h5-uniapp/.agents/skills/unocss/GENERATION.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Generation Info
|
||||||
|
|
||||||
|
- **Source:** `sources/unocss`
|
||||||
|
- **Git SHA:** `2f7f267d0cc0c43d44357208aabb35b049359a08`
|
||||||
|
- **Generated:** 2026-01-28
|
||||||
21
software/h5-uniapp/.agents/skills/unocss/LICENSE.md
Normal file
21
software/h5-uniapp/.agents/skills/unocss/LICENSE.md
Normal 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.
|
||||||
64
software/h5-uniapp/.agents/skills/unocss/SKILL.md
Normal file
64
software/h5-uniapp/.agents/skills/unocss/SKILL.md
Normal 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) |
|
||||||
5
software/h5-uniapp/.agents/skills/unocss/SYNC.md
Normal file
5
software/h5-uniapp/.agents/skills/unocss/SYNC.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Sync Info
|
||||||
|
|
||||||
|
- **Source:** `vendor/antfu/skills/unocss`
|
||||||
|
- **Git SHA:** `4086f59fa12d308fb05592ca8ce1704fbb940aa3`
|
||||||
|
- **Synced:** 2026-01-30
|
||||||
@ -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/
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
@ -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
|
||||||
|
-->
|
||||||
5
software/h5-uniapp/.agents/skills/vite/GENERATION.md
Normal file
5
software/h5-uniapp/.agents/skills/vite/GENERATION.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Generation Info
|
||||||
|
|
||||||
|
- **Source:** `sources/vite`
|
||||||
|
- **Git SHA:** `b40292ce6a7dbbbbac9c6dae5f126b7f44c3e1b7`
|
||||||
|
- **Generated:** 2026-01-28
|
||||||
21
software/h5-uniapp/.agents/skills/vite/LICENSE.md
Normal file
21
software/h5-uniapp/.agents/skills/vite/LICENSE.md
Normal 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.
|
||||||
50
software/h5-uniapp/.agents/skills/vite/SKILL.md
Normal file
50
software/h5-uniapp/.agents/skills/vite/SKILL.md
Normal 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
Loading…
x
Reference in New Issue
Block a user