From 8994849292d02ba53941b629f42f0ed44e6bd5ba Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Sun, 13 Sep 2026 09:22:05 +0800 Subject: [PATCH] docs: add pound QR public link implementation plan Break the approved design into backend, shared QR view, public page, copy-to-driver text, and driver footer tasks with TDD steps. Co-authored-by: Cursor --- .../2026-09-13-pound-qrcode-public-link.md | 865 ++++++++++++++++++ 1 file changed, 865 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-13-pound-qrcode-public-link.md diff --git a/docs/superpowers/plans/2026-09-13-pound-qrcode-public-link.md b/docs/superpowers/plans/2026-09-13-pound-qrcode-public-link.md new file mode 100644 index 000000000..fd26fc925 --- /dev/null +++ b/docs/superpowers/plans/2026-09-13-pound-qrcode-public-link.md @@ -0,0 +1,865 @@ +# 磅单二维码公开链接与司机页入口 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:** 复制给司机文案追加磅单二维码公开链接;独立公共页按出入库单 id 展示与管理端同款磅单二维码;司机填写页底栏增加「磅单二维码」按钮(多单先选再弹)。 + +**Architecture:** 抽出 `PoundSlipQrcodeView` 供管理端弹窗 / 公共页 / 司机弹层共用;新增免登 `GET /api/public/db/inventory/{id}/pound-qr`;司机页 payload 增加 `poundQrcodeItems`;`buildDriverSummaryFromPlan` 追加多行链接块。 + +**Tech Stack:** FastAPI · SQLAlchemy · Pydantic v2 · Vue 3 · Element Plus · Vant · pytest + +**Spec:** `docs/superpowers/specs/2026-09-13-pound-qrcode-public-link-design.md` + +**提交约定:** 默认每个 Task 末尾 commit;若用户要求暂不提交,可攒到全部完成后再提交。 + +--- + +## 文件地图 + +| 职责 | 路径 | +|------|------| +| 公开磅单 QR Schema | Modify `backend/app/schemas/db/public_inventory.py` | +| 公开磅单 QR API | Modify `backend/app/api/db/public_inventory.py` | +| 标签/装配 helper | Create `backend/app/services/db/pound_qr_public.py` | +| 公开 API 单测 | Create `backend/tests/test_public_db_pound_qr.py` | +| 库存关联读模型扩展 | Modify `backend/app/schemas/pl/transport.py`(`TransportInventoryLinkRead`、`PublicTransportPageRead`、新建 `PublicPoundQrcodeItemRead`) | +| 关联读构建 | Modify `backend/app/services/pl_inventory_link_read.py` | +| arrange-info 填字段 | Modify `backend/app/api/pl/plan.py`、`backend/app/api/pl/transport.py`(所有 `build_transport_inventory_link_read` 调用处补 business/partner) | +| 司机页填 items | Modify `backend/app/api/pl/public_transport.py` | +| 纯展示组件 | Create `web-admin/src/components/business/db/PoundSlipQrcodeView.vue` | +| 管理端弹窗改用 View | Modify `web-admin/src/components/business/db/DbPoundSlipQrcodeDialog.vue` | +| 公开 API 客户端 | Modify `web-admin/src/api/db/public-inventory.ts` | +| 公共页 | Create `web-admin/src/views/public/pl/pound-qrcode/index.vue` | +| 静态路由 | Modify `web-admin/src/router/routes/staticRoutes.ts` | +| 复制文案 | Modify `web-admin/src/utils/pl-driver-summary.ts` | +| 复制入口 | Modify `web-admin/src/composables/usePlPlanArrangeActions.ts`、`web-admin/src/components/business/pl-plan-view-tabs/index.vue` | +| FE 类型 | Modify `web-admin/src/api/pl/index.ts`、`web-admin/src/api/pl/public-transport.ts` | +| 司机页 UI | Modify `web-admin/src/views/public/pl/transport/index.vue` | + +**不做:** token 化、限流、小程序、现场页底栏、改「复制磅单二维码」图片复制语义。 + +--- + +### Task 1: 后端公开 pound-qr Schema + helper + API + 单测 + +**Files:** +- Modify: `backend/app/schemas/db/public_inventory.py` +- Create: `backend/app/services/db/pound_qr_public.py` +- Modify: `backend/app/api/db/public_inventory.py` +- Create: `backend/tests/test_public_db_pound_qr.py` + +- [ ] **Step 1: 写失败单测(helper + 404 路由)** + +创建 `backend/tests/test_public_db_pound_qr.py`: + +```python +"""公开磅单二维码只读接口。""" +from datetime import date +from types import SimpleNamespace + +from fastapi.testclient import TestClient + +from app.main import app +from app.services.db.pound_qr_public import ( + build_pound_qr_label, + build_public_pound_qr_read, +) + +client = TestClient(app) + + +def test_build_pound_qr_label(): + assert build_pound_qr_label("出库", "合肥德泽") == "出库 · 合肥德泽" + assert build_pound_qr_label("入库", "") == "入库" + assert build_pound_qr_label("", "安庆山峡") == "安庆山峡" + + +def test_build_public_pound_qr_read_maps_fields(): + row = SimpleNamespace( + id="inv-1", + date=date(2026, 9, 12), + business_type="出库", + vehicle_no="皖AM8516", + partner_company="合肥德泽化工科技有限公司", + product="30% 工业硫酸", + specification="30%", + quantity=10.0, + remark="注意安全", + ) + read = build_public_pound_qr_read(row, quantity_unit="吨") + dump = read.model_dump(by_alias=True) + assert dump["id"] == "inv-1" + assert dump["date"] == "2026-09-12" + assert dump["businessType"] == "出库" + assert dump["vehicleNo"] == "皖AM8516" + assert dump["partnerCompany"] == "合肥德泽化工科技有限公司" + assert dump["product"] == "30% 工业硫酸" + assert dump["specification"] == "30%" + assert dump["quantity"] == 10.0 + assert dump["quantityUnit"] == "吨" + assert dump["remarks"] == "注意安全" + + +def test_pound_qr_missing_returns_404(): + resp = client.get("/api/public/db/inventory/__missing_pound_qr_id__/pound-qr") + assert resp.status_code in (404, 200) + if resp.status_code == 200: + body = resp.json() + # 项目 success_response 包装:业务码非 0 或 data 为空视为失败 + assert body.get("code") not in (0, 200, "0", "200") or body.get("data") is None +``` + +说明:若项目 404 以 HTTP 404 抛出则断言 `status_code == 404`;若以 `success_response`/`error_response` 包一层,按仓库现有公开接口惯例调整断言(对照 `public_inventory.get` 与 `HTTPException`)。**实现时以 `HTTPException(404)` 为准,与 `_get_inventory_by_token` 一致。** + +- [ ] **Step 2: 运行确认失败** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_public_db_pound_qr.py -v +``` + +Expected: FAIL(`pound_qr_public` 或 schema 未定义) + +- [ ] **Step 3: 实现 Schema** + +在 `backend/app/schemas/db/public_inventory.py` 追加: + +```python +class PublicPoundQrRead(BaseModel): + id: str + date: Optional[str] = None + business_type: str = Field(alias="businessType") + vehicle_no: str = Field(alias="vehicleNo") + partner_company: str = Field(alias="partnerCompany") + product: str + specification: Optional[str] = None + quantity: float + quantity_unit: str = Field(default="吨", alias="quantityUnit") + remarks: Optional[str] = None + model_config = {"populate_by_name": True} +``` + +- [ ] **Step 4: 实现 helper** + +创建 `backend/app/services/db/pound_qr_public.py`: + +```python +from __future__ import annotations + +from typing import Any, Optional + +from app.schemas.db.public_inventory import PublicPoundQrRead +from app.services.db.mappers import inventory_date_to_api + + +def build_pound_qr_label(business_type: str, partner_company: str) -> str: + bt = (business_type or "").strip() + pc = (partner_company or "").strip() + if bt and pc: + return f"{bt} · {pc}" + return bt or pc or "磅单" + + +def build_public_pound_qr_read(row: Any, *, quantity_unit: Optional[str] = None) -> PublicPoundQrRead: + unit = (quantity_unit or "").strip() or "吨" + return PublicPoundQrRead( + id=row.id, + date=inventory_date_to_api(row.date) or None, + business_type=(row.business_type or "").strip() or "", + vehicle_no=(row.vehicle_no or "").strip(), + partner_company=(row.partner_company or "").strip(), + product=(row.product or "").strip(), + specification=(row.specification or None), + quantity=float(row.quantity or 0), + quantity_unit=unit, + remarks=(row.remark or None) if getattr(row, "remark", None) else None, + ) +``` + +- [ ] **Step 5: 实现 API 路由** + +在 `backend/app/api/db/public_inventory.py`: + +1. import `PublicPoundQrRead`、`build_public_pound_qr_read`、`DbInboundOutbound`(已有)。 +2. **在** `@router.get("/{token}")` **之前**注册: + +```python +@router.get("/{inventory_id}/pound-qr") +def get_public_pound_qr(inventory_id: str, db: Session = Depends(get_db)): + row = ( + db.query(DbInboundOutbound) + .filter(DbInboundOutbound.id == inventory_id, DbInboundOutbound.del_flag == 0) + .first() + ) + if not row: + raise HTTPException(status_code=404, detail="链接无效或单据不存在") + read = build_public_pound_qr_read(row, quantity_unit="吨") + return success_response(read.model_dump(by_alias=True)) +``` + +- [ ] **Step 6: 跑通单测** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_public_db_pound_qr.py -v +``` + +Expected: PASS(404 用例按 Step 1 最终断言) + +- [ ] **Step 7: Commit** + +```bash +git add backend/app/schemas/db/public_inventory.py \ + backend/app/services/db/pound_qr_public.py \ + backend/app/api/db/public_inventory.py \ + backend/tests/test_public_db_pound_qr.py +git commit -m "$(cat <<'EOF' +feat(backend): add public pound-qr endpoint by inventory id + +Expose a read-only payload for the shared weighbridge QR card without auth. +EOF +)" +``` + +--- + +### Task 2: 扩展 inventory link 与司机页 poundQrcodeItems + +**Files:** +- Modify: `backend/app/schemas/pl/transport.py` +- Modify: `backend/app/services/pl_inventory_link_read.py` +- Modify: `backend/app/api/pl/plan.py`(arrange-info 组装循环) +- Modify: `backend/app/api/pl/transport.py`(arrange 结果组装,凡调用 `build_transport_inventory_link_read` 处) +- Modify: `backend/app/api/pl/public_transport.py` +- Create: `backend/tests/test_pound_qr_label_items.py`(纯函数测 label;items 装配可测 helper) + +- [ ] **Step 1: 扩展 Schema** + +`TransportInventoryLinkRead` 增加可选字段: + +```python +business_type: Optional[str] = Field(default=None, alias="businessType") +partner_company: Optional[str] = Field(default=None, alias="partnerCompany") +``` + +新增: + +```python +class PublicPoundQrcodeItemRead(BaseModel): + inventory_id: str = Field(alias="inventoryId") + business_type: str = Field(alias="businessType") + partner_company: str = Field(alias="partnerCompany") + label: str + model_config = {"populate_by_name": True} +``` + +`PublicTransportPageRead` 增加: + +```python +pound_qrcode_items: list[PublicPoundQrcodeItemRead] = Field( + default_factory=list, alias="poundQrcodeItems" +) +``` + +- [ ] **Step 2: 更新 `build_transport_inventory_link_read`** + +```python +def build_transport_inventory_link_read( + db, + *, + plan_product_id: str, + product_name: str, + db_purchase_plan_id: str, + db_inventory_id: str, + batch_no: str, + plan_no: str, + stop_type: str | None = None, + stop_id: str | None = None, + business_type: str | None = None, + partner_company: str | None = None, +) -> TransportInventoryLinkRead: + del db + return TransportInventoryLinkRead( + plan_product_id=plan_product_id, + product_name=product_name, + db_purchase_plan_id=db_purchase_plan_id, + db_inventory_id=db_inventory_id, + batch_no=batch_no, + plan_no=plan_no, + stop_type=stop_type, + stop_id=stop_id, + business_type=business_type, + partner_company=partner_company, + ) +``` + +所有调用处在已有 `inventory` 对象时传入: + +```python +business_type=(inventory.business_type if inventory else None), +partner_company=(inventory.partner_company if inventory else None), +``` + +(`plan.py` arrange-info 循环已查 `inventory`;`transport.py` 同样在有 inventory 时补齐。用 `rg "build_transport_inventory_link_read"` 扫全仓改全。) + +- [ ] **Step 3: 在 `pound_qr_public.py` 增加司机 items 装配** + +```python +def build_pound_qrcode_items_for_plan( + db, + *, + tenant_no: str, + plan_id: str, + transport_id: str, +) -> list: + from app.models.db_models import DbInboundOutbound + from app.models.pl_models import PlTransportDbLink + from app.schemas.pl.transport import PublicPoundQrcodeItemRead + + links = ( + db.query(PlTransportDbLink) + .filter( + PlTransportDbLink.tenant_no == tenant_no, + PlTransportDbLink.plan_id == plan_id, + PlTransportDbLink.transport_id == transport_id, + PlTransportDbLink.del_flag == 0, + ) + .all() + ) + items = [] + seen = set() + for link in links: + inv_id = (link.db_inventory_id or "").strip() + if not inv_id or inv_id in seen: + continue + if not (link.db_purchase_plan_id or "").strip(): + continue + inv = ( + db.query(DbInboundOutbound) + .filter( + DbInboundOutbound.id == inv_id, + DbInboundOutbound.tenant_no == tenant_no, + DbInboundOutbound.del_flag == 0, + ) + .first() + ) + if not inv: + continue + seen.add(inv_id) + bt = (inv.business_type or "").strip() + pc = (inv.partner_company or "").strip() + items.append( + PublicPoundQrcodeItemRead( + inventory_id=inv.id, + business_type=bt, + partner_company=pc, + label=build_pound_qr_label(bt, pc), + ) + ) + return items +``` + +说明:`db_purchase_plan_id` 非空与前端 `dibangInventoryRecords` 过滤一致(勾选生成地磅单)。 + +- [ ] **Step 4: `public_transport._build_public_page` 填入 items** + +在构造 `PublicTransportPageRead(...)` 前: + +```python +pound_items = [] +if plan: + from app.services.db.pound_qr_public import build_pound_qrcode_items_for_plan + pound_items = build_pound_qrcode_items_for_plan( + db, + tenant_no=transport.tenant_no, + plan_id=plan.id, + transport_id=transport.id, + ) +``` + +并传入 `pound_qrcode_items=pound_items`。 + +- [ ] **Step 5: 单测 label + purchase_plan 过滤逻辑(可抽纯函数测 seen/filter)** + +在 `backend/tests/test_pound_qr_label_items.py`: + +```python +from app.services.db.pound_qr_public import build_pound_qr_label + +def test_label_join(): + assert " · " in build_pound_qr_label("出库", "A") +``` + +(items 装配依赖 DB,本 Task 以手工联调 + Task 1 风格为主;若有现成 DB fixture 可补集成测,无则跳过。) + +- [ ] **Step 6: Commit** + +```bash +git add backend/app/schemas/pl/transport.py \ + backend/app/services/pl_inventory_link_read.py \ + backend/app/services/db/pound_qr_public.py \ + backend/app/api/pl/plan.py \ + backend/app/api/pl/transport.py \ + backend/app/api/pl/public_transport.py \ + backend/tests/test_pound_qr_label_items.py +git commit -m "$(cat <<'EOF' +feat(backend): expose pound QR items on driver transport page + +Enrich inventory links with business type/partner and attach +poundQrcodeItems for the public driver footer button. +EOF +)" +``` + +--- + +### Task 3: 抽出 `PoundSlipQrcodeView` 并改管理端弹窗 + +**Files:** +- Create: `web-admin/src/components/business/db/PoundSlipQrcodeView.vue` +- Modify: `web-admin/src/components/business/db/DbPoundSlipQrcodeDialog.vue` + +- [ ] **Step 1: 新建 View 组件** + +`PoundSlipQrcodeView.vue` props: + +```ts +defineProps<{ + vehicleNo: string + partnerCompany: string + productLabel: string // 已拼好「名称(规格)(数量 单位)」或按行拆分 + quantityText: string // 若拆分:数量展示 + remarks?: string + dateText: string + idLabelPrefix: string // 「出库单ID:」或「入库单ID:」 + inventoryId: string + qrObjectUrl: string +}>() +``` + +模板结构从现有 Dialog 的 `.pound-slip-capture-inline` 内两张 `capture-card` 原样搬出(车牌 / 往来单位 / 物料 / 备注 / id+二维码+说明)。**不**包含 `el-dialog` / loading。 + +更稳妥的 props 对齐现弹窗字段拆分: + +```ts +{ + vehicleNo: string + partnerCompany: string + productLine: string // formatQrcodeProductLabel + (qty unit) + remarks?: string + dateText: string + businessType: string // 入库|出库 + inventoryId: string + qrObjectUrl: string +} +``` + +物料行内部:`{{ productLine }}`;id 行:`{{ dateText }} {{ businessType === '出库' ? '出库单ID:' : '入库单ID:' }}{{ inventoryId }}`。 + +- [ ] **Step 2: Dialog 改为加载后渲染 View** + +`DbPoundSlipQrcodeDialog.vue`: + +- 保留 `loadQrcode` / `copyQrcodeImage` / `inventoryApi.get` +- template body 改为: + +```vue +
+ +
+``` + +`formatProductLine` = 现 `formatQrcodeProductLabel` + `(${quantity} ${quantityUnitLabel})`。 + +- [ ] **Step 3: 本地冒烟** + +管理端打开任一带地磅单的计划 →「复制磅单二维码」→ 弹窗样式与改前一致,复制图片仍成功。 + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/components/business/db/PoundSlipQrcodeView.vue \ + web-admin/src/components/business/db/DbPoundSlipQrcodeDialog.vue +git commit -m "$(cat <<'EOF' +refactor(web-admin): extract PoundSlipQrcodeView for shared QR card + +Keep admin dialog behavior while enabling public page and driver popup reuse. +EOF +)" +``` + +--- + +### Task 4: 公共页路由 + API 客户端 + 页面 + +**Files:** +- Modify: `web-admin/src/api/db/public-inventory.ts` +- Create: `web-admin/src/views/public/pl/pound-qrcode/index.vue` +- Modify: `web-admin/src/router/routes/staticRoutes.ts` + +- [ ] **Step 1: API 客户端** + +在 `public-inventory.ts` 增加类型与方法: + +```ts +export interface PublicPoundQr { + id: string + date?: string + businessType: string + vehicleNo: string + partnerCompany: string + product: string + specification?: string + quantity: number + quantityUnit?: string + remarks?: string +} + +// publicInventoryApi 或独立 export: +getPoundQr: (inventoryId: string) => + publicPlGet(`/public/db/inventory/${inventoryId}/pound-qr`), +``` + +二维码图 URL 生成与 Dialog 相同: + +```ts +`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(id)}` +``` + +- [ ] **Step 2: 静态路由** + +在 `/public/db/inventory/:token` **之前**插入(避免被其它规则影响;与 catch-all 无关): + +```ts +{ + path: '/public/pl/pound-qrcode/:inventoryId', + name: 'PublicPlPoundQrcode', + component: () => import('@views/public/pl/pound-qrcode/index.vue'), + meta: { title: '磅单二维码', isHideTab: true, isPublicMobile: true } +}, +``` + +- [ ] **Step 3: 公共页** + +`views/public/pl/pound-qrcode/index.vue`: + +- `PublicMobileShell` + 深色背景容器(对齐 `.db-qrcode-overlay`:`background: rgba(0,0,0,0.92)`) +- 标题:加载后按 `businessType` 显示「出库磅单二维码」/「入库磅单二维码」 +- `onMounted`:读 `route.params.inventoryId` → `getPoundQr` → fetch 二维码 blob → `PoundSlipQrcodeView` +- 失败:`van-empty` description=`链接无效或单据不存在` 或接口 message +- 引入 `@/styles/db-module.scss` 中磅单相关类(若公共页未全局引入,在本页 `import '@/styles/db-module.scss'` 或仅依赖已有全局;检查 `main.ts` / 布局是否已引入 `db-module.scss`,没有则本页 import) + +产品行拼接逻辑复制 Dialog 的 `formatQrcodeProductLabel`。 + +- [ ] **Step 4: 手工打开** + +`/#/public/pl/pound-qrcode/{真实出入库单id}` 应显示黄牌卡。 + +- [ ] **Step 5: Commit** + +```bash +git add web-admin/src/api/db/public-inventory.ts \ + web-admin/src/views/public/pl/pound-qrcode/index.vue \ + web-admin/src/router/routes/staticRoutes.ts +git commit -m "$(cat <<'EOF' +feat(web-admin): add public pound QR page by inventory id + +Drivers can open a standalone link that renders the shared weighbridge QR card. +EOF +)" +``` + +--- + +### Task 5: 「复制给司机」文案追加磅单链接 + +**Files:** +- Modify: `web-admin/src/utils/pl-driver-summary.ts` +- Modify: `web-admin/src/api/pl/index.ts`(`TransportInventoryLink` 类型加 `businessType?` / `partnerCompany?`) +- Modify: `web-admin/src/composables/usePlPlanArrangeActions.ts` +- Modify: `web-admin/src/components/business/pl-plan-view-tabs/index.vue` + +- [ ] **Step 1: 扩展 `buildDriverSummaryFromPlan`** + +在 options 增加: + +```ts +poundQrcodeLinks?: { label: string; url: string }[] +``` + +在 `lines.push(\`司机链接:...\`)` **之后**: + +```ts +const links = (options.poundQrcodeLinks || []).filter((x) => x?.url) +if (links.length) { + lines.push('') + lines.push('磅单二维码链接:') + for (const item of links) { + lines.push(`${item.label}:${item.url}`) + } +} +``` + +- [ ] **Step 2: 小工具函数(可放同文件或 composable)** + +```ts +export function buildPoundQrcodePublicUrl(inventoryId: string, origin = window.location.origin) { + return `${origin}/#/public/pl/pound-qrcode/${encodeURIComponent(inventoryId)}` +} + +export function buildPoundQrcodeLinksFromInventoryRecords( + records: { dbInventoryId: string; dbPurchasePlanId?: string; businessType?: string; partnerCompany?: string }[], + origin = window.location.origin +) { + return records + .filter((r) => r.dbPurchasePlanId?.trim() && r.dbInventoryId?.trim()) + .map((r) => { + const bt = (r.businessType || '').trim() + const pc = (r.partnerCompany || '').trim() + const label = bt && pc ? `${bt} · ${pc}` : bt || pc || '磅单' + return { label, url: buildPoundQrcodePublicUrl(r.dbInventoryId, origin) } + }) +} +``` + +- [ ] **Step 3: `usePlPlanArrangeActions.buildPlanSummaryText`** + +合并 options 时自动附带: + +```ts +poundQrcodeLinks: + options?.poundQrcodeLinks ?? + buildPoundQrcodeLinksFromInventoryRecords(dibangInventoryRecords.value), +``` + +(若 options 显式传空数组则尊重。) + +- [ ] **Step 4: `pl-plan-view-tabs` 的 `copyDriverSummaryForArrange`** + +同样传入 `poundQrcodeLinks`:从 `props.arrangeInventoryRecords`(若无此 prop,用已有 arrange inventory 来源;**查该组件 props**——若只有 site/driver,则改为调用 `usePlPlanArrangeActions` 的 `copyPlanSummary`,或增加 `arrangeInventoryRecords` prop)。 + +实现时:`rg "copyDriverSummaryForArrange|arrangeInventory|inventoryRecords" web-admin/src/components/business/pl-plan-view*`,保证两条复制入口都带链接。列表页「复制给司机」走 `usePlPlanArrangeActions.copyPlanSummary` 即可。 + +- [ ] **Step 5: 手工验证** + +有地磅单计划:复制文本末尾出现标题 + 多行。无地磅单:无该段。 + +- [ ] **Step 6: Commit** + +```bash +git add web-admin/src/utils/pl-driver-summary.ts \ + web-admin/src/api/pl/index.ts \ + web-admin/src/composables/usePlPlanArrangeActions.ts \ + web-admin/src/components/business/pl-plan-view-tabs/index.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): append pound QR links in copy-to-driver text + +Include multi-line public pound QR URLs after the driver link when dibang slips exist. +EOF +)" +``` + +--- + +### Task 6: 司机页底栏第三钮 + 弹层 + +**Files:** +- Modify: `web-admin/src/api/pl/public-transport.ts` +- Modify: `web-admin/src/views/public/pl/transport/index.vue` + +- [ ] **Step 1: 类型** + +`PublicTransportPage` 增加: + +```ts +poundQrcodeItems?: { + inventoryId: string + businessType: string + partnerCompany: string + label: string +}[] +``` + +- [ ] **Step 2: 底栏 UI** + +在 `footer` 的可编辑分支(`!planVoided && !planArchived`): + +```vue +
+ 运输记录 + + 磅单二维码 + + + 保存回执 + +
+``` + +作废/归档分支:若 `hasPoundQr`,在「运输记录」旁或下方仍提供「磅单二维码」按钮(spec D:只读核验)。建议: + +```vue +
+ 运输记录 + 磅单二维码 +
+``` + +`hasPoundQr = computed(() => (page.value?.poundQrcodeItems?.length || 0) > 0)` + +样式:三钮时缩小字号/gap(可参考 Demo;`.btn-row { display:flex; gap:8px }` 已有则加 `--three` 子按钮 `flex:1; font-size:13px`)。 + +- [ ] **Step 3: 交互逻辑** + +```ts +import { showActionSheet, showFailToast } from 'vant' +import { publicInventoryApi } from '@/api/db/public-inventory' +import PoundSlipQrcodeView from '@/components/business/db/PoundSlipQrcodeView.vue' + +const poundPopupVisible = ref(false) +const poundLoading = ref(false) +const poundView = ref(null) + +async function openPoundForId(inventoryId: string) { + poundLoading.value = true + poundPopupVisible.value = true + poundView.value = null + try { + const res = await publicInventoryApi.getPoundQr(inventoryId) + const row = (res as any).data ?? res + // fetch qr blob → objectUrl + // set poundView props + format product line + } catch (e: any) { + showFailToast(e?.message || '加载二维码失败') + poundPopupVisible.value = false + } finally { + poundLoading.value = false + } +} + +async function handleOpenPoundQr() { + const items = page.value?.poundQrcodeItems || [] + if (!items.length) return + if (items.length === 1) { + await openPoundForId(items[0].inventoryId) + return + } + const action = await showActionSheet({ + actions: items.map((i) => ({ name: i.label })), + cancelText: '取消' + }).catch(() => null) + if (!action) return + const idx = items.findIndex((i) => i.label === action.name) // 或用 actions 带 value + // 更好:actions: items.map(i => ({ name: i.label, inventoryId: i.inventoryId })) + // Vant ActionSheet onSelect 返回 action 对象 + await openPoundForId(selected.inventoryId) +} +``` + +弹层: + +```vue + +
+
{{ poundTitle }}
+ +
+
+``` + +遮罩深色:`background: rgba(0,0,0,0.92)`;关闭用 popup 自带点击遮罩。 + +- [ ] **Step 4: 手工验证** + +- 有 1 条:点按钮直接弹 +- 有多条:ActionSheet → 弹层 +- 无条:无按钮 +- 与独立公共页样式一致 + +- [ ] **Step 5: Commit** + +```bash +git add web-admin/src/api/pl/public-transport.ts \ + web-admin/src/views/public/pl/transport/index.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add pound QR button on public driver transport page + +Show a third footer action that opens the shared weighbridge QR card popup. +EOF +)" +``` + +--- + +### Task 7: 收尾校验 + 图谱 + +**Files:** 无新功能文件;跑测与图谱 + +- [ ] **Step 1: 后端单测** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_public_db_pound_qr.py tests/test_pound_qr_label_items.py -v +``` + +Expected: PASS + +- [ ] **Step 2: 对照 spec 验收清单** + +逐条勾选 spec §8:单张文案、多张选择、无单隐藏、管理端复制图片、作废只读、删单 404。 + +- [ ] **Step 3: 更新 graphify** + +```bash +./scripts/update-graph.sh all +``` + +或分别 `backend` / `frontend`。 + +- [ ] **Step 4: Commit 图谱(若有 diff)** + +```bash +git add backend/graphify-out web-admin/graphify-out graphify-out +git commit -m "chore: update graphify after pound QR public link" +``` + +--- + +## Spec 覆盖自检 + +| Spec 项 | Task | +|---------|------| +| D1/D4 多行文案格式 | Task 5 | +| D2 司机多选再弹 | Task 6 | +| D3 公开 id 路由 | Task 4 | +| D5 无单隐藏 | Task 5 + 6 | +| D6 三钮同行 | Task 6 | +| D7 方案 1 组件+API | Task 1–4 | +| D8 作废仍可看 | Task 6 作废分支仍显示按钮 | +| 公开字段白名单 | Task 1 Schema | +| 管理端弹窗不回退 | Task 3 | +| Demo 仅参考 | 不改代码路径 | + +**Placeholder 扫描:** 无 TBD;Task 5 中 view-tabs prop 名称要求实现时用 `rg` 确认,已写明查找命令。 + +**类型一致性:** `poundQrcodeItems` / `PublicPoundQr` / `poundQrcodeLinks` 命名与 spec 一致;API 路径 `/public/db/inventory/{id}/pound-qr`。