diff --git a/docs/superpowers/plans/2026-08-03-mobile-h5-dibang-adapt.md b/docs/superpowers/plans/2026-08-03-mobile-h5-dibang-adapt.md new file mode 100644 index 000000000..c00e5ab68 --- /dev/null +++ b/docs/superpowers/plans/2026-08-03-mobile-h5-dibang-adapt.md @@ -0,0 +1,494 @@ +# mobile-h5 地磅移动端适配一体化后端 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 将 `mobile-h5` 对接一体化 `backend`(门户 RSA 登录 + `db_*` 业务 API + CRM/PL 主数据只读选择),去掉基础资料 CRUD 与手输自动建档。 + +**Architecture:** H5 保留 Vant 页面;登录/CRM 走门户 `{code,data}` 信封;地磅业务走直接 JSON(对齐 `web-admin/src/utils/http/db.ts`)。主数据只选 CRM 客户、CRM 产品、`/api/transport/vehicle`(self+carrier)。现场页继续用出入库 id;免登场景补车辆只读公共接口。 + +**Tech Stack:** Vue 3 / Vant 4 / Vite / Axios / Pinia;`jsencrypt`;FastAPI(仅缺口补丁) + +**Spec:** `docs/superpowers/specs/2026-08-03-mobile-h5-dibang-adapt-design.md` + +--- + +## File map + +| 文件 | 职责 | +|------|------| +| `mobile-h5/vite.config.ts` / `.env*` | 代理到 backend;清理 master-data 斜杠特例 | +| `mobile-h5/src/utils/loginRsa.ts` | RSA 登录体(对齐 web-admin) | +| `mobile-h5/src/utils/http/index.ts` | 门户信封解包 + Bearer;401 处理 | +| `mobile-h5/src/utils/http/db.ts` | 地磅直出 JSON 客户端(尾斜杠、Bearer) | +| `mobile-h5/src/api/authApi.ts` | login / refresh | +| `mobile-h5/src/api/{plans,inventory,weighing,dashboard}Api.ts` | id→string、`productId`、走 db 客户端 | +| `mobile-h5/src/api/crmOptionsApi.ts` | 客户/产品 list | +| `mobile-h5/src/api/vehicleOptionsApi.ts` | 运输车辆 list + 公共车牌 options | +| `mobile-h5/src/components/DbCustomerPicker.vue` 等 | 只选 Picker | +| `mobile-h5/src/views/**` | 表单接选择器;删手输建档 | +| `mobile-h5/src/router/index.ts` | 删 master-data / user 路由 | +| `backend/app/api/db/public_inventory.py`(或新文件) | 免登车牌 options | +| `backend/app/main.py` | 挂载公共路由(若新文件) | + +--- + +### Task 1: 环境与代理 + +**Files:** +- Modify: `mobile-h5/vite.config.ts` +- Modify: `mobile-h5/.env.example` +- Modify: `mobile-h5/.env`(本地,勿提交密钥) +- Modify: `mobile-h5/package.json`(增加 `jsencrypt`) + +- [ ] **Step 1: 安装 jsencrypt** + +```bash +cd mobile-h5 && pnpm add jsencrypt +# 若无 pnpm:npm install jsencrypt +``` + +- [ ] **Step 2: 更新 `.env.example`** + +```env +# 开发代理目标(Vite 用) +VITE_API_BASE_URL=http://127.0.0.1:8000 +# 生产 API 前缀 +VITE_API_URL=/api +``` + +- [ ] **Step 3: 简化 `vite.config.ts` 代理** + +保留 `/api` → `VITE_API_BASE_URL`,**不要** rewrite 去掉 `/api`。 +列表尾斜杠逻辑保留 `plans|inventory|weighing`,**删除** `master-data` / `companies|cargos|vehicles` 特例。 + +```ts +const listResources = ['plans', 'inventory', 'weighing'] +// 删除 pathParts.length === 3 && master-data 整块 +``` + +- [ ] **Step 4: 验证代理** + +```bash +cd mobile-h5 && pnpm dev +# 另开终端,backend 已启动时: +curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5174/api/db/health +``` + +Expected: `200`(或经代理可达的健康检查;若无 `/db/health` 则 `curl` 登录公钥接口): + +```bash +curl -s http://127.0.0.1:5174/api/web/auth/login-rsa-public-key | head -c 200 +``` + +Expected: JSON 含 `publicKeyPem` 或门户信封。 + +- [ ] **Step 5: Commit** + +```bash +git add mobile-h5/package.json mobile-h5/pnpm-lock.yaml mobile-h5/vite.config.ts mobile-h5/.env.example +git commit -m "chore(mobile-h5): point Vite proxy at unified backend and add jsencrypt" +``` + +--- + +### Task 2: 登录 RSA + 双 HTTP 客户端 + +**Files:** +- Create: `mobile-h5/src/utils/loginRsa.ts` +- Create: `mobile-h5/src/utils/http/db.ts` +- Create: `mobile-h5/src/api/authApi.ts` +- Modify: `mobile-h5/src/utils/http/index.ts` +- Modify: `mobile-h5/src/views/login/index.vue` +- Modify: `mobile-h5/src/api/usersApi.ts`(删除或停用旧 `/auth/login`) +- Modify: `mobile-h5/src/store/modules/user.ts`(如需) + +- [ ] **Step 1: 新增 `loginRsa.ts`** + +对齐 `web-admin/src/utils/loginRsa.ts`:拉取 `/web/auth/login-rsa-public-key`,JSEncrypt 加密 `{ username, password, ts }`,返回 `{ encryptedLogin }`。 +公钥请求用**未加密、未强制 Bearer** 的 axios 或现有 client(注意门户信封:若返回 `{code,data}` 则取 `data.publicKeyPem`)。 + +- [ ] **Step 2: 新增 `authApi.ts`** + +```ts +import api from '@/utils/http' // 门户信封客户端 +import { buildWebEncryptedLoginBody } from '@/utils/loginRsa' + +export async function fetchLogin(username: string, password: string) { + const body = await buildWebEncryptedLoginBody(username, password) + // POST /web/auth/login → data: { token, refreshToken, expiresIn, userInfo } + return api.post<{ + token: string + refreshToken: string + expiresIn: number + userInfo: { + id: string + username: string + realName?: string + role?: string + } + }>({ url: '/web/auth/login', data: body }) +} +``` + +保证 `api.post` 对门户响应解包到 `data`(`code===200||code===0`)。 + +- [ ] **Step 3: 改造 `utils/http/index.ts`(门户客户端)** + +- `baseURL`: 开发 `/api`,生产 `VITE_API_URL || '/api'` +- 成功:若有 `code` 字段则解包 `data`;否则原样返回 +- 401:logout → `#/login`(hash 路由用 `router` 或 `/#/login`,勿用无 hash 的 `/login`) +- 错误 toast 优先 `message` / `detail` + +- [ ] **Step 4: 新增 `utils/http/db.ts`(地磅客户端)** + +参考 `web-admin/src/utils/http/db.ts`: + +```ts +const DB_COLLECTION_ROOT = /^\/(plans|inventory|weighing)$/ +function normalizeDbUrl(url?: string) { + if (!url || url.endsWith('/')) return url + return DB_COLLECTION_ROOT.test(url) ? `${url}/` : url +} +``` + +- 响应直接 `res.data`(无 code 信封) +- 请求附加 `Bearer ${accessToken}` +- export `get/post/patch/del` 供业务 API 使用 + +- [ ] **Step 5: 改登录页** + +`login/index.vue` 调用 `fetchLogin`: + +```ts +const data = await fetchLogin(formData.username, formData.password) +userStore.setLoginStatus(true) +userStore.setToken(data.token, data.refreshToken) +userStore.setUserInfo({ + id: data.userInfo.id, + username: data.userInfo.username, + name: data.userInfo.realName || data.userInfo.username, + role: data.userInfo.role +}) +router.push('/dashboard') +``` + +- [ ] **Step 6: 手工验证登录** + +```bash +# backend + mobile-h5 dev 已启动 +# 浏览器打开 http://127.0.0.1:5174/#/login +# 用门户账号(user_no)登录,应进入仪表盘 +``` + +- [ ] **Step 7: Commit** + +```bash +git add mobile-h5/src/utils/loginRsa.ts mobile-h5/src/utils/http/ mobile-h5/src/api/authApi.ts \ + mobile-h5/src/views/login/index.vue mobile-h5/src/api/usersApi.ts mobile-h5/src/store/modules/user.ts +git commit -m "feat(mobile-h5): adapt portal RSA login and split db HTTP client" +``` + +--- + +### Task 3: 业务 API 类型与路径对齐 + +**Files:** +- Modify: `mobile-h5/src/api/plansApi.ts` +- Modify: `mobile-h5/src/api/inventoryApi.ts` +- Modify: `mobile-h5/src/api/weighingApi.ts` +- Modify: `mobile-h5/src/api/dashboardApi.ts` + +对照权威类型:`web-admin/src/api/db/{plans,inventory,weighing}.ts`。 + +- [ ] **Step 1: 将全部业务 id 改为 `string`** + +示例(plans): + +```ts +export interface Plan { + id: string + planNo?: string + // ... + productId?: string + product: string + purchasePlanId?: never // 无此字段 + lastWeighingSlipId?: string + fillLinkInventoryId?: string + // ... +} + +export interface PlanCreate { + // ... + productId: string + plannedQuantity: number + // 不再只传 product 名称创建 +} +``` + +`list/get/create/update/delete/getRelated/getThirdPartyPage` 的 `id` 参数一律 `string`。 +`inventory` / `weighing` 同理:`purchasePlanId`、`inboundOutboundId` → `string`;Create 含 `productId: string`。 + +- [ ] **Step 2: 全部改用 db 客户端** + +```ts +import db from '@/utils/http/db' +// list: +return db.get({ url: '/plans', params }) +// 或 url: '/plans/' — db 客户端会 normalize +``` + +`dashboardApi` 同样走 db 客户端:`/dashboard/inbound/by-cargo` 等。 + +- [ ] **Step 3: 冒烟请求(需已登录 token)** + +浏览器 Network 或临时脚本:登录后 `GET /api/plans/` 应 200 且元素 `id` 为字符串。 + +- [ ] **Step 4: Commit** + +```bash +git add mobile-h5/src/api/plansApi.ts mobile-h5/src/api/inventoryApi.ts \ + mobile-h5/src/api/weighingApi.ts mobile-h5/src/api/dashboardApi.ts +git commit -m "feat(mobile-h5): align db API types to string ids and productId" +``` + +--- + +### Task 4: 主数据 Options API + Picker 组件 + +**Files:** +- Create: `mobile-h5/src/api/crmOptionsApi.ts` +- Create: `mobile-h5/src/api/vehicleOptionsApi.ts` +- Create: `mobile-h5/src/components/pickers/DbCustomerPicker.vue` +- Create: `mobile-h5/src/components/pickers/DbProductPicker.vue` +- Create: `mobile-h5/src/components/pickers/DbVehiclePicker.vue` +- Delete 或停用: `mobile-h5/src/api/masterDataApi.ts` + +- [ ] **Step 1: `crmOptionsApi.ts`(门户信封 client)** + +```ts +import api from '@/utils/http' + +export function fetchCustomerOptions(keyword?: string) { + return api.post<{ list?: Array<{ id: string; customerName: string }> }>({ + url: '/web/crm/customer/list', + data: { page: 1, pageSize: 20, customerName: keyword || undefined } + }) +} + +export function fetchProductOptions(keyword?: string) { + return api.post<{ list?: Array<{ id: string; productName: string; model?: string }> }>({ + url: '/web/crm/products/list', + data: { page: 1, pageSize: 20, productName: keyword || undefined } + }) +} +``` + +按实际后端 list 入参字段名微调(与 `web-admin` `fetchCustomerList` / `fetchProductList` 一致)。解包后取 `list` 或 `rows`。 + +- [ ] **Step 2: `vehicleOptionsApi.ts`** + +登录态: + +```ts +import api from '@/utils/http' // 若 PL 走门户信封;否则用能解 PL 响应的 client + +export function fetchVehicleOptions(q?: string) { + return api.get<{ list?: Array<{ id: string; licensePlate: string; vehicleType: string }> }>({ + url: '/transport/vehicle', + params: { q, page: 1, pageSize: 100 } + }) +} +``` + +响应形状对齐 `VehicleListResponse`(常用 `list`/`rows`/`items` — 以实现时实际 JSON 为准,与 `pickBxList` 同类解包)。 + +免登(Task 6 后端就绪后): + +```ts +export function fetchPublicVehiclePlates(params: { q?: string; inventoryId?: string; planId?: string }) { + return api.get>({ + url: '/public/db/vehicle-plates', + params + }) +} +``` + +- [ ] **Step 3: 三个 Picker 组件** + +共用模式:`van-popup` + `van-search` + 列表点击选中;**无**「手动输入」按钮。 + +- `DbCustomerPicker`:`v-model` = 客户名称字符串;emit 可选 `select(item)` +- `DbProductPicker`:`v-model:productId` + `v-model:productName` + `v-model:specification`(规格可用 `model`) +- `DbVehiclePicker`:`v-model` = `licensePlate`;prop `publicMode?: boolean` 决定调登录态或公共接口 + +空结果文案:`暂无匹配,请到门户维护` + +- [ ] **Step 4: Commit** + +```bash +git add mobile-h5/src/api/crmOptionsApi.ts mobile-h5/src/api/vehicleOptionsApi.ts \ + mobile-h5/src/components/pickers/ mobile-h5/src/api/masterDataApi.ts +git commit -m "feat(mobile-h5): add CRM/PL option APIs and select-only pickers" +``` + +--- + +### Task 5: 业务表单接入选择器 + 去掉自动建档 + +**Files:** +- Modify: `mobile-h5/src/views/plans/detail.vue` +- Modify: `mobile-h5/src/views/inventory/detail.vue` +- Modify: `mobile-h5/src/views/weighing/detail.vue` +- Modify: `mobile-h5/src/views/plans/index.vue`(生成出入库 query 用 string id) +- Modify: `mobile-h5/src/views/inventory/index.vue` +- Modify: `mobile-h5/src/views/weighing/index.vue` +- Modify: `mobile-h5/src/views/dashboard/index.vue`(去掉基础资料入口) +- Modify: `mobile-h5/src/router/index.ts`(删除 master-data、user 路由) +- Delete: `mobile-h5/src/views/master-data/**`(可选,或留文件但无路由) + +- [ ] **Step 1: 计划详情** + +- 往来单位 → `DbCustomerPicker` → `formData.customerName` +- 产品 → `DbProductPicker` → `productId` + `product` + `specification` +- 提交 `planApi.create/update` 必须带 `productId` +- 删除对 `companyApi`/`cargoApi` 的引用 + +- [ ] **Step 2: 出入库详情** + +- 同上客户/产品选择器 +- 车牌 → `DbVehiclePicker`(登录态) +- **删除** `ensureVehicleExists`、`showManualVehicleInput`、手动输入 Dialog +- `purchasePlanId` 保持 string;去掉 `Number(route.query...)` + +- [ ] **Step 3: 地磅单详情** + +- 供货/收货单位用客户选择器(或沿用现有双单位字段 + 客户 Picker) +- 货物 → 产品选择器(`productId` 必填) +- `inboundOutboundId` / `purchasePlanId`:string;加载关联时 `inventoryApi.get(id)` 勿 `Number` + +- [ ] **Step 4: 列表页** + +- 所有 `handleDelete(id: number)` → `string` +- 跳转 query 一律字符串 id +- 仪表盘 `menuList` 去掉 `/master-data` +- 路由删除 `master-data*`;用户管理路由若存在一并删除 + +- [ ] **Step 5: 手工冒烟** + +登录 → 新建计划(选客户+产品)→ 生成出入库(选车牌)→ 生成地磅 → 作废。 + +- [ ] **Step 6: Commit** + +```bash +git add mobile-h5/src/views/ mobile-h5/src/router/index.ts +git commit -m "feat(mobile-h5): wire select-only master data into business forms" +``` + +--- + +### Task 6: 现场免登页 + 公共车牌接口 + +**Files:** +- Modify: `mobile-h5/src/views/inventory/fill.vue` +- Modify: `mobile-h5/src/views/inventory/plan.vue` +- Create or Modify: `backend/app/api/db/public_inventory.py`(或 `public_vehicles.py`) +- Modify: `backend/app/api/__init__.py` / `backend/app/main.py`(如需新 router) +- Create: `backend/tests/test_public_db_vehicle_plates.py`(可选轻量) + +- [ ] **Step 1: 后端公共车牌 options** + +在 `public_inventory` 同模块或新 router `prefix="/public/db")`: + +```python +@router.get("/vehicle-plates") +def list_public_vehicle_plates( + q: Optional[str] = Query(None), + inventory_id: Optional[str] = Query(None, alias="inventoryId"), + plan_id: Optional[str] = Query(None, alias="planId"), + db: Session = Depends(get_db), +): + """免登:按出入库/计划解析 tenant_no,返回 pl_vehicle 车牌(self+carrier)。""" + # 1) 由 inventory_id 或 plan_id 查 db_inbound_outbound / db_purchase_plan 得 tenant_no + # 2) 若都无则 400 + # 3) query PlVehicle: tenant_no, del_flag=0, optional license_plate contains q + # 4) return [{"licensePlate": ..., "vehicleType": ...}, ...] (limit 100) +``` + +只读,禁止 POST。挂到 `API_V1_PREFIX`。 +将路径加入 `API_ENCRYPT_EXCLUDE_PATHS`(若加密开启)。 + +- [ ] **Step 2: 单测或手工 curl** + +```bash +curl -s "http://127.0.0.1:8000/api/public/db/vehicle-plates?inventoryId=<已有出入库id>" +``` + +Expected: JSON 数组含 `licensePlate`。 + +- [ ] **Step 3: fill.vue / plan.vue** + +- 删除 `ensureVehicleExists` 与手输 UI +- 车牌用 `DbVehiclePicker` `publicMode` + 传入当前 `inventoryId` / `planId` +- 保存仍走现有免登 patch/create + +- [ ] **Step 4: 二维码** + +确认 `inventory/index` 二维码内容仍为出入库 id,打开 `#/inventory/fill/{id}`。 + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/api/db/ mobile-h5/src/views/inventory/fill.vue \ + mobile-h5/src/views/inventory/plan.vue backend/tests/test_public_db_vehicle_plates.py +git commit -m "feat: public vehicle plate options for H5 fill pages; select-only plates" +``` + +--- + +### Task 7: 全链路验收与收尾 + +- [ ] **Step 1: 按 spec §10 验收** + +1. 门户账号 RSA 登录 H5 +2. 仪表盘无「基础资料」;图表有数据或空态正常 +3. 计划 → 出入库 → 地磅 → 作废 +4. 客户/产品/车辆无法手输新增 +5. 无登录打开 `#/inventory/fill/{id}`,车牌只能选 +6. Network 中业务 id 均为字符串 + +- [ ] **Step 2: 类型检查** + +```bash +cd mobile-h5 && pnpm exec vue-tsc --noEmit +``` + +Expected: 无因 `number` id 引起的错误(可允许原有无关告警,但新建代码应干净)。 + +- [ ] **Step 3: 最终 commit(若有零散修复)** + +```bash +git add mobile-h5 backend +git commit -m "fix(mobile-h5): polish dibang H5 adaptation after acceptance" +``` + +--- + +## Spec coverage checklist + +| Spec 要求 | Task | +|-----------|------| +| 门户 RSA + JWT 登录 | Task 2 | +| db API 直出 JSON + 尾斜杠 | Task 2–3 | +| id→string、productId | Task 3、5 | +| 删基础资料;客户/产品/车辆只选 | Task 4–5 | +| 禁止手输自动建档(含现场) | Task 5–6 | +| 现场继续用出入库 id | Task 6 | +| H5 无权限 UI 门禁 | 全任务不添加角色判断 | +| 后端仅缺口补丁(公共车牌) | Task 6 | +| 验收清单 | Task 7 | + +## Placeholder / consistency notes + +- CRM list 请求体字段名以实现时 web-admin 实际调用为准(`customerName` / `productName` 等)。 +- PL 车辆列表响应字段统一用 `licensePlate`(camelCase alias)。 +- Hash 路由登出跳转统一 `#/login`。