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 <cursoragent@cursor.com>
This commit is contained in:
1 parent
157ad5e29a
commit
8994849292
1 file changed
+865
@@ -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`。
|
||||
Reference in new issue
Block a user