|
|
@@ -0,0 +1,590 @@
|
|
|
+# 司机链接 · 运输记录 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:** 司机免登录页增加「运输记录」:按主车车牌聚合同租户计划,按取货/送货单日筛选(默认今天 + 今/明/后天快捷),点击进入对应司机详情;填写/回执 UI 与现状完全一致。
|
|
|
+
|
|
|
+**Architecture:** 方案 1 — 用入口 `driver_access_token` 鉴权,解析主车 `license_plate` 后查询同车牌历史;列表项返回各单自有 token,前端跳转现有详情页;不改 feedback/attachments 契约。
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2;Vue 3 + Vant(公开 H5);pytest
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-08-21-pl-driver-transport-history-design.md`
|
|
|
+**Demo:** `docs/demo-driver-transport-history.html`
|
|
|
+
|
|
|
+**约束:** 填写界面(产品回传数量、品牌芯片、附件、备注、保存回执)**不得改版**,仅在详情底部增加「运输记录」入口;归档/作废空状态文案保持现状。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+| 文件 | 职责 |
|
|
|
+|------|------|
|
|
|
+| `backend/app/schemas/pl/transport.py` | `PublicTransportHistoryRecordRead` / `PublicTransportHistoryPageRead` |
|
|
|
+| `backend/app/api/pl/public_transport.py` | `GET /{token}/history`;车牌归一、日期命中查询、组装列表 |
|
|
|
+| `backend/tests/test_pl_public_transport_history.py` | 纯函数/标志/日期解析单测 |
|
|
|
+| `web-admin/src/api/pl/public-transport.ts` | `getHistory` + 类型 |
|
|
|
+| `web-admin/src/router/routes/staticRoutes.ts` | 注册 `/public/pl/transport/:token/history` |
|
|
|
+| `web-admin/src/views/public/pl/transport/index.vue` | 底部「运输记录」;voided/archived 也显示;error 不显示 |
|
|
|
+| `web-admin/src/views/public/pl/transport/history.vue` | 新列表页(筛选 + 列表);点击跳转详情 |
|
|
|
+
|
|
|
+加密排除:`/api/public/pl/transport` 已在 `API_ENCRYPT_EXCLUDE_PATHS` 前缀排除,无需改配置。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: Schema + 日期/车牌辅助函数(TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/schemas/pl/transport.py`
|
|
|
+- Modify: `backend/app/api/pl/public_transport.py`(先加纯函数)
|
|
|
+- Create: `backend/tests/test_pl_public_transport_history.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败单测**
|
|
|
+
|
|
|
+```python
|
|
|
+# backend/tests/test_pl_public_transport_history.py
|
|
|
+from datetime import date, datetime
|
|
|
+
|
|
|
+from app.api.pl.public_transport import (
|
|
|
+ _history_matches_date,
|
|
|
+ _normalize_main_plate,
|
|
|
+ _parse_history_date,
|
|
|
+ _history_status_label,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def test_normalize_main_plate_trims():
|
|
|
+ assert _normalize_main_plate(" 苏AD12345 ") == "苏AD12345"
|
|
|
+ assert _normalize_main_plate(None) == ""
|
|
|
+ assert _normalize_main_plate("") == ""
|
|
|
+
|
|
|
+
|
|
|
+def test_parse_history_date_ok_and_bad():
|
|
|
+ assert _parse_history_date("2026-08-21") == date(2026, 8, 21)
|
|
|
+ assert _parse_history_date("2026/08/21") is None
|
|
|
+ assert _parse_history_date("") is None
|
|
|
+
|
|
|
+
|
|
|
+def test_history_matches_date_or_logic():
|
|
|
+ day = date(2026, 8, 21)
|
|
|
+ assert _history_matches_date(datetime(2026, 8, 21, 9, 0), None, day) is True
|
|
|
+ assert _history_matches_date(None, datetime(2026, 8, 21, 18, 0), day) is True
|
|
|
+ assert _history_matches_date(
|
|
|
+ datetime(2026, 8, 20, 9, 0), datetime(2026, 8, 22, 18, 0), day
|
|
|
+ ) is False
|
|
|
+ assert _history_matches_date(None, None, day) is False
|
|
|
+
|
|
|
+
|
|
|
+def test_history_status_label():
|
|
|
+ assert _history_status_label(True, False) == "已作废"
|
|
|
+ assert _history_status_label(True, True) == "已作废"
|
|
|
+ assert _history_status_label(False, True) == "已归档"
|
|
|
+ assert _history_status_label(False, False) == "进行中"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑测确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_pl_public_transport_history.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(函数未定义)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现辅助函数 + Schema**
|
|
|
+
|
|
|
+在 `public_transport.py` 增加:
|
|
|
+
|
|
|
+```python
|
|
|
+from datetime import date as date_cls
|
|
|
+
|
|
|
+def _normalize_main_plate(value: Optional[str]) -> str:
|
|
|
+ return (value or "").strip()
|
|
|
+
|
|
|
+
|
|
|
+def _parse_history_date(raw: Optional[str]) -> Optional[date_cls]:
|
|
|
+ text = (raw or "").strip()
|
|
|
+ if not text:
|
|
|
+ return None
|
|
|
+ try:
|
|
|
+ return date_cls.fromisoformat(text)
|
|
|
+ except ValueError:
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _as_date(value) -> Optional[date_cls]:
|
|
|
+ if value is None:
|
|
|
+ return None
|
|
|
+ if isinstance(value, datetime):
|
|
|
+ return value.date()
|
|
|
+ if isinstance(value, date_cls):
|
|
|
+ return value
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
+def _history_matches_date(pickup_time, delivery_time, day: date_cls) -> bool:
|
|
|
+ return _as_date(pickup_time) == day or _as_date(delivery_time) == day
|
|
|
+
|
|
|
+
|
|
|
+def _history_status_label(plan_voided: bool, plan_archived: bool) -> str:
|
|
|
+ if plan_voided:
|
|
|
+ return "已作废"
|
|
|
+ if plan_archived:
|
|
|
+ return "已归档"
|
|
|
+ return "进行中"
|
|
|
+```
|
|
|
+
|
|
|
+在 `schemas/pl/transport.py` 的 `PublicTransportPageRead` 后追加:
|
|
|
+
|
|
|
+```python
|
|
|
+class PublicTransportHistoryRecordRead(BaseModel):
|
|
|
+ plan_id: str = Field(alias="planId")
|
|
|
+ plan_no: str = Field(alias="planNo")
|
|
|
+ transport_id: str = Field(alias="transportId")
|
|
|
+ driver_access_token: Optional[str] = Field(default=None, alias="driverAccessToken")
|
|
|
+ driver_url: Optional[str] = Field(default=None, alias="driverUrl")
|
|
|
+ license_plate: Optional[str] = Field(default=None, alias="licensePlate")
|
|
|
+ pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime")
|
|
|
+ delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime")
|
|
|
+ pickup_address: Optional[str] = Field(default=None, alias="pickupAddress")
|
|
|
+ destination_address: Optional[str] = Field(default=None, alias="destinationAddress")
|
|
|
+ plan_voided: bool = Field(default=False, alias="planVoided")
|
|
|
+ plan_archived: bool = Field(default=False, alias="planArchived")
|
|
|
+ status_label: str = Field(default="进行中", alias="statusLabel")
|
|
|
+ model_config = {"populate_by_name": True}
|
|
|
+
|
|
|
+
|
|
|
+class PublicTransportHistoryPageRead(BaseModel):
|
|
|
+ license_plate: Optional[str] = Field(default=None, alias="licensePlate")
|
|
|
+ date: str
|
|
|
+ records: list[PublicTransportHistoryRecordRead] = Field(default_factory=list)
|
|
|
+ model_config = {"populate_by_name": True}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 再跑测**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_pl_public_transport_history.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/tests/test_pl_public_transport_history.py backend/app/api/pl/public_transport.py backend/app/schemas/pl/transport.py
|
|
|
+git commit -m "feat(pl): add transport history helpers and schemas"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 实现 `GET /{token}/history`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/api/pl/public_transport.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 增加 `_build_driver_url` 与 history 端点**
|
|
|
+
|
|
|
+在 `public_transport.py`(复用 config,与 `plan.py` 一致):
|
|
|
+
|
|
|
+```python
|
|
|
+from fastapi import Query
|
|
|
+from sqlalchemy import or_, and_
|
|
|
+from app.core.config import settings
|
|
|
+from app.schemas.pl.transport import (
|
|
|
+ PublicTransportHistoryPageRead,
|
|
|
+ PublicTransportHistoryRecordRead,
|
|
|
+ # ...existing imports
|
|
|
+)
|
|
|
+
|
|
|
+def _build_driver_url(token: str) -> str:
|
|
|
+ origin = (settings.PUBLIC_WEB_ORIGIN or "").rstrip("/")
|
|
|
+ return f"{origin}/#/public/pl/transport/{token}"
|
|
|
+
|
|
|
+
|
|
|
+@router.get("/{token}/history")
|
|
|
+def get_public_transport_history(
|
|
|
+ token: str,
|
|
|
+ date: str = Query(..., description="筛选日 YYYY-MM-DD"),
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+):
|
|
|
+ transport = _get_transport_by_token(db, token)
|
|
|
+ day = _parse_history_date(date)
|
|
|
+ if day is None:
|
|
|
+ return error_response(400, "日期格式无效,请使用 YYYY-MM-DD")
|
|
|
+
|
|
|
+ plate = _normalize_main_plate(transport.license_plate)
|
|
|
+ if not plate:
|
|
|
+ page = PublicTransportHistoryPageRead(license_plate=None, date=day.isoformat(), records=[])
|
|
|
+ return success_response(page.model_dump(by_alias=True))
|
|
|
+
|
|
|
+ # 同租户、同主车车牌、已安排:经 pl_plan_transport 关联
|
|
|
+ rows = (
|
|
|
+ db.query(PlPlan, PlTransport)
|
|
|
+ .join(
|
|
|
+ PlPlanTransport,
|
|
|
+ and_(
|
|
|
+ PlPlanTransport.plan_id == PlPlan.id,
|
|
|
+ PlPlanTransport.del_flag == 0,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .join(
|
|
|
+ PlTransport,
|
|
|
+ and_(
|
|
|
+ PlTransport.id == PlPlanTransport.transport_id,
|
|
|
+ PlTransport.del_flag == 0,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .filter(
|
|
|
+ PlPlan.tenant_no == transport.tenant_no,
|
|
|
+ PlPlan.del_flag == 0,
|
|
|
+ PlPlan.transport_status == "arranged",
|
|
|
+ PlTransport.tenant_no == transport.tenant_no,
|
|
|
+ PlTransport.license_plate == plate,
|
|
|
+ or_(
|
|
|
+ and_(PlPlan.pickup_time.isnot(None), func.date(PlPlan.pickup_time) == day),
|
|
|
+ and_(PlPlan.delivery_time.isnot(None), func.date(PlPlan.delivery_time) == day),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ .order_by(PlPlan.pickup_time.desc(), PlPlan.plan_date.desc(), PlPlan.create_time.desc())
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+
|
|
|
+ # 去重:同一 plan 可能多条 link 时保留最新 transport(按查询顺序首次)
|
|
|
+ seen_plan: set[str] = set()
|
|
|
+ records: list[PublicTransportHistoryRecordRead] = []
|
|
|
+ for plan, tr in rows:
|
|
|
+ if plan.id in seen_plan:
|
|
|
+ continue
|
|
|
+ seen_plan.add(plan.id)
|
|
|
+ voided, archived = _resolve_driver_link_flags(plan, tr)
|
|
|
+ tok = (tr.driver_access_token or "").strip()
|
|
|
+ records.append(
|
|
|
+ PublicTransportHistoryRecordRead(
|
|
|
+ plan_id=plan.id,
|
|
|
+ plan_no=plan.plan_no or "",
|
|
|
+ transport_id=tr.id,
|
|
|
+ driver_access_token=tok or None,
|
|
|
+ driver_url=_build_driver_url(tok) if tok else None,
|
|
|
+ license_plate=tr.license_plate or plate,
|
|
|
+ pickup_time=plan.pickup_time,
|
|
|
+ delivery_time=plan.delivery_time,
|
|
|
+ pickup_address=plan.pickup_address,
|
|
|
+ destination_address=plan.destination_address or plan.destination_name,
|
|
|
+ plan_voided=voided,
|
|
|
+ plan_archived=archived,
|
|
|
+ status_label=_history_status_label(voided, archived),
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ page = PublicTransportHistoryPageRead(
|
|
|
+ license_plate=plate,
|
|
|
+ date=day.isoformat(),
|
|
|
+ records=records,
|
|
|
+ )
|
|
|
+ return success_response(page.model_dump(by_alias=True))
|
|
|
+```
|
|
|
+
|
|
|
+注意:文件顶部需 `from sqlalchemy import func`(若尚未导入)。路由须注册在 `/{token}/feedback` 等同级;FastAPI 中 `/{token}/history` 静态后缀与 `/{token}` 不冲突。
|
|
|
+
|
|
|
+若 `PlTransport.license_plate` 偶发空但计划上有车牌:实现时在 filter 使用
|
|
|
+`(PlTransport.license_plate == plate) | ((PlTransport.license_plate.is_(None) | (PlTransport.license_plate == "")) & (PlPlan.license_plate == plate))`
|
|
|
+——仅当联调发现漏数时再加,首版以运输单车牌为准(与 Spec 一致)。
|
|
|
+
|
|
|
+- [ ] **Step 2: 手工冒烟(有本地库时)**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 用已知有效司机 token
|
|
|
+curl -s "http://127.0.0.1:8000/api/public/pl/transport/<TOKEN>/history?date=$(date +%F)" | head
|
|
|
+```
|
|
|
+
|
|
|
+Expected: `code=0`,`data.records` 为数组。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/api/pl/public_transport.py
|
|
|
+git commit -m "feat(pl): add public driver transport history API"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 前端 API 类型与 `getHistory`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/api/pl/public-transport.ts`
|
|
|
+
|
|
|
+- [ ] **Step 1: 追加类型与方法**
|
|
|
+
|
|
|
+```typescript
|
|
|
+export interface PublicTransportHistoryRecord {
|
|
|
+ planId: string
|
|
|
+ planNo: string
|
|
|
+ transportId: string
|
|
|
+ driverAccessToken?: string
|
|
|
+ driverUrl?: string
|
|
|
+ licensePlate?: string
|
|
|
+ pickupTime?: string
|
|
|
+ deliveryTime?: string
|
|
|
+ pickupAddress?: string
|
|
|
+ destinationAddress?: string
|
|
|
+ planVoided?: boolean
|
|
|
+ planArchived?: boolean
|
|
|
+ statusLabel?: string
|
|
|
+}
|
|
|
+
|
|
|
+export interface PublicTransportHistoryPage {
|
|
|
+ licensePlate?: string
|
|
|
+ date: string
|
|
|
+ records: PublicTransportHistoryRecord[]
|
|
|
+}
|
|
|
+
|
|
|
+export const publicTransportApi = {
|
|
|
+ getPage: (token: string) => publicPlGet<PublicTransportPage>(`/public/pl/transport/${token}`),
|
|
|
+ getHistory: (token: string, date: string) =>
|
|
|
+ publicPlGet<PublicTransportHistoryPage>(`/public/pl/transport/${token}/history`, {
|
|
|
+ params: { date }
|
|
|
+ }),
|
|
|
+ // ...saveFeedback / uploadAttachment 保持不变
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+确认 `publicPlGet` 支持第二参 `params`(与项目其他 public API 一致);若不支持,改为
|
|
|
+`` `/public/pl/transport/${token}/history?date=${encodeURIComponent(date)}` ``。
|
|
|
+
|
|
|
+- [ ] **Step 2: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/api/pl/public-transport.ts
|
|
|
+git commit -m "feat(pl): add public transport history API client"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 路由 + 详情页底部「运输记录」
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/router/routes/staticRoutes.ts`
|
|
|
+- Modify: `web-admin/src/views/public/pl/transport/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 注册 history 路由(放在详情路由旁)**
|
|
|
+
|
|
|
+```typescript
|
|
|
+{
|
|
|
+ path: '/public/pl/transport/:token/history',
|
|
|
+ name: 'PublicPlTransportHistory',
|
|
|
+ component: () => import('@views/public/pl/transport/history.vue'),
|
|
|
+ meta: { title: '运输记录', isHideTab: true, isPublicMobile: true }
|
|
|
+},
|
|
|
+{
|
|
|
+ path: '/public/pl/transport/:token',
|
|
|
+ name: 'PublicPlTransportDriver',
|
|
|
+ component: () => import('@views/public/pl/transport/index.vue'),
|
|
|
+ meta: { title: '运输计划', isHideTab: true, isPublicMobile: true }
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+先写 history 再写 `:token`,避免个别匹配器歧义(hash 模式下通常无妨,仍建议 history 在前)。
|
|
|
+
|
|
|
+- [ ] **Step 2: 改详情页 footer(填写区逻辑不动)**
|
|
|
+
|
|
|
+模板:
|
|
|
+
|
|
|
+- `v-else-if="page?.planVoided"` / `planArchived` 空状态外层包一层,**空状态下方仍渲染 footer**:
|
|
|
+
|
|
|
+```vue
|
|
|
+<van-empty v-else-if="page?.planVoided" description="运输计划已作废" />
|
|
|
+<van-empty v-else-if="page?.planArchived" description="运输计划已归档完成" />
|
|
|
+<template v-else-if="page">
|
|
|
+ <!-- 现有取送货/产品/回执/备注 全部保持不动 -->
|
|
|
+ ...
|
|
|
+</template>
|
|
|
+
|
|
|
+<footer
|
|
|
+ v-if="page && !errorMessage"
|
|
|
+ class="page-footer"
|
|
|
+>
|
|
|
+ <div v-if="!page.planVoided && !page.planArchived" class="btn-row">
|
|
|
+ <van-button block round plain type="primary" @click="goHistory">运输记录</van-button>
|
|
|
+ <van-button type="primary" block round :loading="saving" @click="handleSave">
|
|
|
+ 保存回执
|
|
|
+ </van-button>
|
|
|
+ </div>
|
|
|
+ <van-button
|
|
|
+ v-else
|
|
|
+ block
|
|
|
+ round
|
|
|
+ plain
|
|
|
+ type="primary"
|
|
|
+ @click="goHistory"
|
|
|
+ >
|
|
|
+ 运输记录
|
|
|
+ </van-button>
|
|
|
+</footer>
|
|
|
+```
|
|
|
+
|
|
|
+注意:原先 footer 在 `v-else-if="page"` 内部且 voided/archived 时不渲染;需把 footer **提到** 与 empty 同级,条件用 `page && !errorMessage`。`loading` 时不显示 footer。
|
|
|
+
|
|
|
+```ts
|
|
|
+import { useRouter, useRoute } from 'vue-router'
|
|
|
+
|
|
|
+const router = useRouter()
|
|
|
+const route = useRoute()
|
|
|
+const token = computed(() => String(route.params.token || ''))
|
|
|
+
|
|
|
+function goHistory() {
|
|
|
+ router.push({ name: 'PublicPlTransportHistory', params: { token: token.value } })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+样式:`.btn-row { display:grid; grid-template-columns:1fr 1fr; gap:8px; }`(若尚无)。
|
|
|
+
|
|
|
+**禁止**改动产品回传、BrandQtyChipsField、uploader、备注、`handleSave` 校验逻辑。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/router/routes/staticRoutes.ts web-admin/src/views/public/pl/transport/index.vue
|
|
|
+git commit -m "feat(pl): add transport history entry on driver public page"
|
|
|
+```
|
|
|
+
|
|
|
+此时 `history.vue` 尚未创建会导致路由懒加载失败——若先提交,Task 5 必须紧接着完成;或本 Task 先放一个最小占位 `history.vue`(仅 nav + empty),Task 5 再补全。
|
|
|
+
|
|
|
+**推荐:** Task 4 Step 2 同时创建占位:
|
|
|
+
|
|
|
+```vue
|
|
|
+<!-- web-admin/src/views/public/pl/transport/history.vue 占位 -->
|
|
|
+<template>
|
|
|
+ <PublicMobileShell>
|
|
|
+ <van-nav-bar title="运输记录" left-arrow @click-left="$router.back()" />
|
|
|
+ <van-empty description="加载中…" />
|
|
|
+ </PublicMobileShell>
|
|
|
+</template>
|
|
|
+<script setup lang="ts">
|
|
|
+ import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
|
|
|
+</script>
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 实现 `history.vue` 列表页
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/public/pl/transport/history.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 完整实现列表(对齐 Demo 筛选行为)**
|
|
|
+
|
|
|
+要点:
|
|
|
+
|
|
|
+```ts
|
|
|
+function formatYmd(d: Date) {
|
|
|
+ const y = d.getFullYear()
|
|
|
+ const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
+ const day = String(d.getDate()).padStart(2, '0')
|
|
|
+ return `${y}-${m}-${day}`
|
|
|
+}
|
|
|
+
|
|
|
+function addDays(base: Date, n: number) {
|
|
|
+ const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + n)
|
|
|
+ return d
|
|
|
+}
|
|
|
+
|
|
|
+const today = formatYmd(new Date())
|
|
|
+const pickDate = ref(today)
|
|
|
+
|
|
|
+async function load() {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const data = await publicTransportApi.getHistory(token.value, pickDate.value)
|
|
|
+ page.value = data
|
|
|
+ } catch (e: any) {
|
|
|
+ errorMessage.value = e?.message || '加载失败'
|
|
|
+ page.value = null
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function setQuick(offset: 0 | 1 | 2) {
|
|
|
+ pickDate.value = formatYmd(addDays(new Date(), offset))
|
|
|
+ load()
|
|
|
+}
|
|
|
+
|
|
|
+function openRecord(row: PublicTransportHistoryRecord) {
|
|
|
+ const t = (row.driverAccessToken || '').trim()
|
|
|
+ if (!t) {
|
|
|
+ showFailToast('该记录暂无司机链接')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ router.push({ name: 'PublicPlTransportDriver', params: { token: t } })
|
|
|
+}
|
|
|
+
|
|
|
+function goBack() {
|
|
|
+ router.push({ name: 'PublicPlTransportDriver', params: { token: token.value } })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+UI:
|
|
|
+
|
|
|
+- 顶栏:返回 → `goBack()`(回到进入列表时的 token 详情,不是 browser back,避免从 B 详情进列表再 back 到错误页)
|
|
|
+- 快捷 chip:今天/明天/后天;与 `pickDate` 相等时高亮
|
|
|
+- `<input type="date" v-model="pickDate" @change="load" />` 或 Vant 等价
|
|
|
+- 卡片列表:`planNo`、`statusLabel` Tag、取货/送货日、线路(`pickupAddress → destinationAddress`)
|
|
|
+- 无数据:`van-empty`「暂无运输记录」
|
|
|
+
|
|
|
+样式参考 Demo / 现有 `transport/index.vue`(绿顶栏、inset card)。状态色:已作废红、已归档灰、进行中绿。
|
|
|
+
|
|
|
+- [ ] **Step 2: 本地手动验收清单**
|
|
|
+
|
|
|
+1. 打开有效司机链接 → 底部有「运输记录」+「保存回执」;填写区与改前一致。
|
|
|
+2. 进列表默认今天;点明天/后天/改日期会刷新。
|
|
|
+3. 点进行中记录 → 进入该单详情,可保存回执。
|
|
|
+4. 归档/作废详情 → 仅空状态 +「运输记录」。
|
|
|
+5. 无效 token → 无「运输记录」按钮。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/public/pl/transport/history.vue
|
|
|
+git commit -m "feat(pl): implement public driver transport history list page"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: 图谱更新(若本会话改了代码)
|
|
|
+
|
|
|
+**Files:** 无业务文件
|
|
|
+
|
|
|
+- [ ] **Step 1:** 若实现阶段改了 backend/web-admin 代码,执行:
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh backend
|
|
|
+# 或 frontend / all,按实际改动
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2:** 不必强制 commit 图谱;与项目惯例一致即可。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec 覆盖自检
|
|
|
+
|
|
|
+| Spec 项 | Task |
|
|
|
+|---------|------|
|
|
|
+| 主车车牌聚合 | Task 2 |
|
|
|
+| 取货日 OR 送货日 = 单日 | Task 1–2 |
|
|
|
+| 默认今天 + 今/明/后快捷 | Task 5 |
|
|
|
+| 底部运输记录(含归档/作废) | Task 4 |
|
|
|
+| 无「返回列表」 | Task 4–5 |
|
|
|
+| 互通填写(跳转目标 token) | Task 5 `openRecord` |
|
|
|
+| 填写 UI 与现状一致 | Task 4 明确禁止改表单 |
|
|
|
+| 现场链接不改 | 无对应 Task(非目标) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 执行交接
|
|
|
+
|
|
|
+Plan complete and saved to `docs/superpowers/plans/2026-08-21-pl-driver-transport-history.md`. Two execution options:
|
|
|
+
|
|
|
+**1. Subagent-Driven (recommended)** — 每个 Task 开新子代理,Task 间复查
|
|
|
+
|
|
|
+**2. Inline Execution** — 本会话按 executing-plans 连续做完
|
|
|
+
|
|
|
+Which approach?
|