|
|
@@ -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
|
|
|
+<div v-loading="loading" class="db-qrcode-body">
|
|
|
+ <PoundSlipQrcodeView
|
|
|
+ v-if="captureRow && captureQrObjectUrl"
|
|
|
+ :vehicle-no="captureRow.vehicleNo"
|
|
|
+ :partner-company="captureRow.partnerCompany"
|
|
|
+ :product-line="formatProductLine(captureRow)"
|
|
|
+ :remarks="captureRow.remarks || ''"
|
|
|
+ :date-text="captureRow.date"
|
|
|
+ :business-type="captureRow.businessType"
|
|
|
+ :inventory-id="captureRow.id"
|
|
|
+ :qr-object-url="captureQrObjectUrl"
|
|
|
+ />
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+`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<PublicPoundQr>(`/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
|
|
|
+<div class="btn-row" :class="{ 'btn-row--three': hasPoundQr }">
|
|
|
+ <van-button ... @click="goHistory">运输记录</van-button>
|
|
|
+ <van-button
|
|
|
+ v-if="hasPoundQr"
|
|
|
+ block
|
|
|
+ round
|
|
|
+ plain
|
|
|
+ type="primary"
|
|
|
+ @click="handleOpenPoundQr"
|
|
|
+ >
|
|
|
+ 磅单二维码
|
|
|
+ </van-button>
|
|
|
+ <van-button type="primary" block round :loading="saving" @click="handleSave">
|
|
|
+ 保存回执
|
|
|
+ </van-button>
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+作废/归档分支:若 `hasPoundQr`,在「运输记录」旁或下方仍提供「磅单二维码」按钮(spec D:只读核验)。建议:
|
|
|
+
|
|
|
+```vue
|
|
|
+<div v-else class="btn-row" :class="{ 'btn-row--three': hasPoundQr }">
|
|
|
+ <van-button ... @click="goHistory">运输记录</van-button>
|
|
|
+ <van-button v-if="hasPoundQr" ... @click="handleOpenPoundQr">磅单二维码</van-button>
|
|
|
+</div>
|
|
|
+```
|
|
|
+
|
|
|
+`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 | { /* View props */ }>(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
|
|
|
+<van-popup
|
|
|
+ v-model:show="poundPopupVisible"
|
|
|
+ position="center"
|
|
|
+ round
|
|
|
+ :style="{ width: '92%', background: 'transparent' }"
|
|
|
+ class="pound-qr-popup"
|
|
|
+>
|
|
|
+ <div class="pound-qr-popup__mask">
|
|
|
+ <div class="pound-qr-popup__title">{{ poundTitle }}</div>
|
|
|
+ <PoundSlipQrcodeView v-if="poundView" v-bind="poundView" />
|
|
|
+ </div>
|
|
|
+</van-popup>
|
|
|
+```
|
|
|
+
|
|
|
+遮罩深色:`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`。
|