charging-cabinet/docs/tech/小程序用户端.md
12451 401f84bda6 chore: 清掉误入库的 agent 工具记忆目录与 Vite 模板 README(内容先按主题归位)
## 起因

用户问"现在确定所有文档都是按分类存放没重复和过期内容了吧" ——
**不能凭感觉答"是"**,于是做了一次针对**已跟踪文件**的体检(重复/断链/过期陈述三维度)。
第一轮体检结果:重复 0 组、断链 0 处,但**又扫出 3 处该处理的东西**。

## 一、`.workbuddy/memory/` —— 另一个 agent 工具的记忆目录被误入库

3 个文件(`MEMORY.md` / `2026-07-02.md` / `2026-07-07.md`),**全仓零引用**,
最后一次"提交"是搭车进 `feat(protocol): TCP v1.1 协议对齐` 这类无关提交 ——
与之前那几张误提交的图片同一模式。

- ⚠️ `2026-07-02.md` 里含一处**生图 API Key 片段**(`sk-ib7i...M2z`);
- `.gitignore` 里 `.codebuddy/` 早有,但 `.workbuddy/` **漏了** ⇒ 才被顺手提交进来。

### 处理:内容先归位,再删目录(不是一删了之)

按 `docs/README.md` 已确立的原则"**过时就删;仍有价值的归到正确的类**",
把 `MEMORY.md` 里仍有价值的三类内容分别落位:

| 内容 | 归到哪 | 为什么 |
|---|---|---|
| UI 颜色系统 / 背景容器 / 36 仓通道卡布局 | **`docs/tech/小程序用户端.md`** 新增「附:UI 设计规范(视觉口径,改动界面前必读)」 | 这是页面视觉口径,属小程序文档 |
| Ardot `batch_edit` 的 `fills`/`strokes`/`cornerRadius` 写法坑 + 设计稿 7 屏 ID | **`docs/compound-notes/2026-07-ardot-batch-edit-属性写法.md`** | 工具特有的单点教训,正是 compound-notes 的定义 |
| "开发前必须确认需求"、"生产环境须授权"、落地页明亮风格偏好 | **`AGENTS.md` 新增「操作规则」小节** | 项目级规则,须常驻加载 |

然后把 `.workbuddy/` 从版本控制移除并**加入 `.gitignore`**(连同 `.opencode/`、`.codegraph/`),
堵住同一个坑再犯。

## 二、`software/web/README.md` —— 建站模板残留

23 行里 9 行是 Vite/plugin-react/Oxlint 的模板话术(英文、与项目无关)。
`api-server` / `device-server` / `h5-uniapp` 三个工程**都没有** README —— 构建方式统一在
`AGENTS.md` 与 `docs/tech/部署手册.md` 里。⇒ **删除**。

## 三、一个我自己的误判(已纠正,记录以免后人重复)

我一度怀疑 `docs/tech/API.md` 接口表"少 12 行"(我数出 68 行,而 `mod.rs` 有 80 个 `.route(`)。
复核发现 **`API.md` 自己写明了计数约定**:"一行 = 一次 `.route(...)` 注册,共 80 行;
展开后 93 对" —— 而我的正则 `^\|\s*(GET|POST)\s*\|` **漏掉了 `| GET / POST |` 这种链式写法**。
用能匹配链式的正则重数 = **正好 80** ⇒ **`API.md` 是准确的,我的怀疑是错的**。

## 校验(只扫 git 已跟踪文件)

- 已跟踪文件 **217**(原 221 → 删 3 个工具记忆 + 1 个模板 README);
- 文档内容重复:**0 组**;
- 已跟踪 md 的本地链接:**0 断链**;
- `.workbuddy` 已跟踪文件数 = 0、工作区已不存在。

## 仍**刻意保留**的(不是遗漏)

- `生产环境.txt`:**唯一凭据清单**且被 `docs/tech/小程序用户端.md` 引用为密钥记录处 ——
  上一提交已把"不入代码仓库"这句与事实矛盾的表述改准;是否改走 `.gitignore`+密码管理器由你定。
- `docs/README.md` 的「旧名→现名映射」表 / 处置记录 / 自证方法:**必须**出现旧名,属预期。
- `docs/compound-notes/TEMPLATE.md`(94 B):新建笔记的模板,给该目录提供格式约束。
- `tasks/002-未结待办-*.md`:两项未结(`energy` 等硬件答复、长函数重构),
  `tasks/` 本身已 gitignore、不入库。
2026-09-20 05:04:53 +08:00

199 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 微信小程序用户端(AGENTS.md 的详细参考篇)
> 本文件由 `AGENTS.md` 的「微信小程序用户端」整段搬迁而来(**内容逐字保留,未删改**)。
> 页面清单、构建/上传流程、状态位与 RSSI 的分层展示口径都在这里。
> 门禁命令(`vue-tsc` 类型检查 + `build:mp-weixin`)与「产物不上传服务器」已留在 `AGENTS.md`。
---
## 微信小程序用户端
### 小程序配置
| 参数 | 值 |
|------|-----|
| AppID | `wx226061c550c52e85` |
| AppSecret | `f11dfee9dc73265b75bf54277ebb1047` |
| 账号 | `13951516272@163.com` |
| 消息加解密方式 | 明文模式 |
| 数据格式 | XML |
| 服务器地址(URL) | `https://app.anzhizhichong.com/api/wechat/callback` |
| Token(令牌) | `7GW16XdnFyak0mE32rc8ZPij` |
| EncodingAESKey | `kRijggSqeCFWi3JhfGyK0nzc6VmdygWWkoqyKPl0bYu` |
> ⚠️ 以上密钥只记录在**仓库根目录的 `生产环境.txt`**(内网 Gitea,明文密码见
> `AGENTS.md`「代码审查豁免」)—— **绝不写进任何源码、`.env` 或配置文件**。
> 该文件是**唯一凭据清单**(阿里云/小程序 AppSecret/腾讯地图/SSL AccessKey 都在其中),
> **不要删除**;需要新凭据时追加到那里,不要散落在别处。
### 构建命令
```bash
cd software/h5-uniapp
npm run build:mp-weixin
# 产物在 dist/build/mp-weixin/,用微信开发者工具导入上传
```
### API 域名配置
小程序请求后端 API 必须配置合法域名(HTTPS):
- 后台「开发管理 → 开发设置 → 服务器域名」
- request 合法域名:`https://app.anzhizhichong.com`
- 开发阶段可在 `manifest.json` 设 `urlCheck: false` 跳过校验
### API base URL
小程序用全 URL(`api/index.ts` 条件编译):
```typescript
// #ifdef MP-WEIXIN
const BASE_URL = 'https://app.anzhizhichong.com/api/pms'
// #endif
// #ifndef MP-WEIXIN
const BASE_URL = '/api/pms'
// #endif
```
### 信号强度显示(RSSI)
设备卡片右上角显示 RSSI 信号强度。
> ✅ **2026-09 已收敛为「两端同源同名、统一 5 档」**(此前两端不一致,见下方"行为变化")。
> **唯一权威实现**:`web/src/utils/deviceStatus.ts` 与 `h5-uniapp/src/utils/deviceStatus.ts`
> 的 `rssiSignal()` / `rssiBarColors()` / `RSSI_TIERS`(**两端逐字一致,改一边必须同步另一边**)。
> 一致性用例:`node test/rssi_signal.test.mjs`(遍历 0–99 逐值断言两端 `level`/`bars`/`color` 相同)。
> ⛔ **页面里不得再自己写一套阈值**(历史上两端各写一套,就是不一致的根因)。
**统一分档表(协议口径:`rssi` 是 0–99 的信号等级,不是 dBm)**
| 阈值 | `level` | 颜色 | 5 格制格数 | PC 图标亮的 `<path>` |
|------|---------|------|-----------|---------------------|
| ≥ 70 | 4 `strong` | `#16a34a` 绿 | 5 | 4 条全亮 |
| ≥ 55 | 3 `good` | `#2563eb` 蓝 | 4 | 3 条 |
| ≥ 40 | 2 `fair` | `#f59e0b` 黄 | 3 | 2 条 |
| ≥ 30 | 1 `weak` | `#f97316` 橙 | 2 | 1 条 |
| < 30 或 null | 0 `none` | `#9ca3af` 灰 | 1(底格) | 0 条(只剩底部圆点 `M2 20h.01`) |
**两端如何消费同一份纯函数:**
- **PC**(`web/src/pages/devices/CabinetGrid.tsx::SignalIconComponent`):`rssiSignal(rssi)` 取 `color` 与 `level`,
4 条 `<path>` 按 `level > i` 决定亮几条;卡片头部数值颜色用同一个 `color`。
- **小程序**(`home/index.vue`、`device-detail/index.vue`):
`v-for="(color, i) in rssiBarColors(cab.rssi)"` 渲染 `bars` 根 `<view class="signal-bar">`,
颜色**内联** `:style="{ background: color }"`;**格数真的随档位变化**。
- 颜色只在 TS 里定义一次(不再有 `.signal.good/.mid/.low/.off` 这类 CSS 颜色规则;
页面 CSS 只负责尺寸/间距)。
**后端(两端共用):** 设备列表接口用 pipeline 批量 `HGET device:{imei} rssi`
(`routes/cabinet_tree.rs::attach_realtime_status`,字段下标见 `DEVICE_LIST_FIELDS`);
小程序侧 `routes/h5.rs::get_cabinets` 同样带 Redis。
**相关文件:**
- 纯函数(权威): `software/web/src/utils/deviceStatus.ts`、`software/h5-uniapp/src/utils/deviceStatus.ts`
- PC 消费点: `software/web/src/pages/devices/CabinetGrid.tsx`(`SignalIconComponent` + 卡片头部)
- 小程序消费点: `software/h5-uniapp/src/pages/home/index.vue`、`pages/device-detail/index.vue`
- 类型: `software/h5-uniapp/src/api/index.ts` — `CabinetItem.rssi`
- 一致性用例: `test/rssi_signal.test.mjs`
> ⚠️ **行为变化(2026-09,小程序端显示会变 —— 这是有意为之)**
> 收敛前:PC 5 档(≥70 绿/≥55 蓝/≥40 黄/≥30 橙/<30 灰,格数随档位增删);
> 小程序只有 **4 档(无 ≥70)**、**30–39 用红色**、且**恒渲染 5 格只改颜色**。
> 收敛后小程序:**新增 ≥70 档(绿/满格)**、**30–39 由红改橙**、**≥55 由绿改蓝**、
> **离线灰由 `#94a3b8` 改为 `#9ca3af`**,且**格数随档位变化**(不再恒 5 格)。
> 影响页面:小程序首页设备卡片、小程序设备详情页头部。
### 近期 UI 改动
1. **设备卡片头部**:**PC 管理端**(`web/src/pages/devices/CabinetGrid.tsx`)去掉"在线/离线"文字 badge,
改为信号图标 + RSSI 数值。
✅ **小程序保留"在线/离线"文字 badge —— 这是有意差异,不是缺陷,不要当 bug 改掉**:
`h5-uniapp/src/pages/home/index.vue:43`(设备卡片,`cab.status===1 ? '在线' : '离线'`)与
`pages/device-detail/index.vue:18`(设备详情;同文件 `:74` 还有**板级**的在线/离线 badge)。
理由(产品决定):PC 用"信号图标 + RSSI 数值"表达;小程序屏幕小、文字 badge 更直观。
> PC 卡片:**图标 + 数值**;小程序:**文字 badge**(有意保留)。
另外 **PC 设备详情页**(`web/src/pages/devices/cabinet.tsx:503`)也仍用 `Tag` 渲染
`CABINET_STATUS_MAP`(`0 离线 / 1 在线 / 2 禁用`)—— 只是**卡片**去掉了,别理解成"全平台都去掉了"。
2. **通道统计**:图标+数字一行排列,图标用 `<svg>` 内联 Lucide(不加容器,flex 居中)
3. **右上角扫码**:scan-line 图标(Lucide),灰色 #6b7280,居中垂直对齐
### 状态位展示约定(**状态位按归属层级展示**)
> 以下三条是**协议 §3.8 / §4.2 / §5.7 的既定结论**(产品已确认),后续不要再反复确认。
1. **柜级状态位 → 显示在"柜子"层面**
- 数据:**⚠️ 这两个名字不是 DB 列**(实测 `cabinets` 表**没有** `status1`/`status2` 列,
它的列是 `id/project_id/abstract_id/imei/iccid/auth_str/name/address/status/created_at/updated_at/
latitude/longitude/work_mode/auth_str_issued/cell`)。它们是:
**Redis 字段** `device:{imei}` 的 `cabinet_status1` / `cabinet_status2`(设备上报原样落,2 位 hex)
→ 接口**原样透出**为响应字段 `status1` / `status2`。
写:`report_worker.rs::write_device_state`;读:`cabinet_tree.rs::attach_realtime_status`(列表)、
`cabinets.rs::fetch_cabinet_realtime`(详情)。
- 位定义(§3.8):`status1` bit0 断电|bit1 烟雾|bit2 水位|bit3 倾倒|**bit4 柜门打开**|
bit5 加热风扇|bit6 接触器断开|bit7 喷淋;`status2` bit0 声光报警|**bit1 柜门打开超时**
(阈值硬件定义,**平台只呈现、不自行判定**);`status2` bit2–7 为保留位,**不展示**。
- 页面:PC `web/src/pages/devices/cabinet.tsx`、小程序 `h5-uniapp/src/pages/device-detail/index.vue`。
- **取不到返回 `null`(未知),绝不落 `"00"`** —— `"00"` 是"全部正常"的有效值。
2. **仓体级状态位 → 显示在"仓体"层面**
- 数据:通道 18 字符 hex 的 `batt_status`(byte0) / `pow_status`(byte1);保留位
(`batt_status` bit2–3、`pow_status` bit3–6)**不展示**。
- 页面:PC 柜子详情里的通道卡片、小程序 `pages/compartment-detail/index.vue`。
- **仓体层面不显示任何"门"状态**:协议**没有**"按仓体(舱门)开门"的指令(§4.2 L403)。
3. **柜门是柜级独立动作,不与业务联动,全平台只有一个开门按钮**
- `open` 是**柜级**指令(**不带 `id`**),语义是**「柜门解锁,下发后由人拉开,门不会自动开」**;
没有"关门"指令(关门也是人为动作)。**全平台只有一扇柜门**,不按仓体区分。
- **柜门状态永远来自 `status_post`(约 60 秒一次),不是实时量** —— 界面上必须标注
"来自最近一次上报(约 60 秒一次),非实时"(§4.2 L428)。
- 开门是**独立流程**,与电池插入、充电开始/结束**没有因果关系也没有先后要求**(§5.7、§7.2):
不得出现在"开始充电"流程里、不得改任何订单状态、不得要求存在订单。
- 入口只有一个:PC 详情页按钮(`<Permission code="charge:open_door">` 包裹)+
小程序设备详情页按钮;**仓体(通道)详情页不放开门按钮**。
- 权限码 `charge:open_door`(后端 `check_permission` 硬校验;`device:door:unlock` 是历史误用的
不存在权限码,**禁止再用**)。
4. **解析函数两端必须同源同名**:`web/src/utils/deviceStatus.ts` 与
`h5-uniapp/src/utils/deviceStatus.ts` 各有一组 `parseCabinetStatus1` / `parseCabinetStatus2` /
`cabinetDoorState` / `cabinetStatusTags`(注释互相指认对方文件,改一边务必同步另一边)。
端到端一致性用例见 `test/device_status_parse.test.mjs`(`node test/device_status_parse.test.mjs`,
逐位遍历 0x00–0xFF 断言两端一致)。
5. **`cabinets.status` 是「启用/禁用」配置态,不表示在线** —— `1` 启用 / `2` 禁用(`0` 是建表默认值
产生的取值,代码不显式写它,语义等同启用);实时在线一律以 Redis `device:{imei}.online` 为准
(唯一权威判据 `routes/cabinet_tree.rs::device_online_flag`,接口字段 `is_online`),
`energy_stats` 的 `online_count`、H5 首页 `online_cabinets`、PC/小程序的「在线/离线」全部消费它
(`CABINET_STATUS_MAP` 只保留启用/禁用两档,**不要再出现"离线"档**)。
### 开发约束(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>` 上写死
---
## 附:UI 设计规范(视觉口径,改动界面前必读)
> 来源:H5 设计稿(Ardot 文件 ID `701384244279693`「安知充H5应用设计稿」,7 屏:
> 登录 / 首页 / 设备详情 / 设备列表 / 仓体详情 / 订单 / 我的)落地时沉淀的口径。
> **改动任何页面配色或布局前先对照本节**,避免同一系统内出现两套视觉。
### 颜色系统(状态色 + 背景色成对使用)
| 语义 | 主色 | 浅底 |
|------|------|------|
| 主色(品牌蓝) | `#2563eb` | `#eff6ff` |
| 在线 | `#16a34a` | `#f0fdf4` |
| 离线 | `#9ca3af` | `#f9fafb` |
| 故障 | `#dc2626` | `#fef2f2` |
| 停止 | `#f59e0b` | `#fffbeb` |
| 充电中 | `#2563eb` | `#eff6ff` |
| 满电 | `#7c3aed` | — |
### 背景与容器
- 页面底色统一 `#f5f5f5`(**含 tab 页与子页面**:设备详情 / 设备列表 / 仓体详情);
- 登录页用渐变 `linear-gradient(135deg, #f0f4ff, #e0e7ff, #f0f9ff)`;
- **所有卡片/容器为白底 `#FFFFFF` + 圆角**。
### 设备详情(36 仓)
- 布局为 **6 板 × 6 通道 = 36**;通道卡片白底 + **顶部 3px 彩色状态条**(色条取上表状态色)。