|
|
@@ -0,0 +1,628 @@
|
|
|
+# 入库/出库单信息完善与现场链接对齐 — 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:** 按 Demo「提案」完善入库/出库现场公共页,并同步管理端列表、编辑弹窗、计划查看回传:展示取送货与完整产品字段(含包装规格、单位中文标签)。
|
|
|
+
|
|
|
+**Architecture:** 后端在 `pl_io_order_service.order_header_to_dict` 读时 enrichment:按 `plan_id` 取 `pl_plan`、按明细 `plan_product_id` 取 `pl_plan_product`,附加物流字段与 `productNo`/`packageSpec`/`unitLabel`/`remark`;公共页 `unit` 直接返回中文标签。前端改共享页 `PublicIoOrderPage`、`IoOrderListPage`、`PlPlanViewTabs`。
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI + SQLAlchemy;Vue 3 + Vant(公共页)+ Element Plus(管理端)
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-07-19-pl-io-order-site-link-enrichment-design.md`
|
|
|
+**Demo:** `docs/superpowers/brainstorm/2026-07-19-pl-outbound-site-link-demo.html`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+| 文件 | 职责 |
|
|
|
+|------|------|
|
|
|
+| `backend/app/services/pl_io_order_service.py` | `enrich_io_order_dict`:计划物流 + 产品扩展 + 单位标签 |
|
|
|
+| `backend/app/schemas/pl/io_order.py` | `IoOrderItemRead` / `IoOrderRead` 增加可选字段 |
|
|
|
+| `backend/app/api/pl/public_io_order.py` | `_build_page` 调用 enrichment;公共页 `unit` 用标签 |
|
|
|
+| `backend/tests/test_pl_io_order_enrichment.py` | enrichment 纯函数/字典断言(无 DB 时可测 helper) |
|
|
|
+| `web-admin/src/api/pl/index.ts` | `PlIoOrder` / `PlIoOrderItem` 类型扩展 |
|
|
|
+| `web-admin/src/api/pl/public-io-order.ts` | 公共页类型扩展 |
|
|
|
+| `web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue` | 按 Demo 重排 UI |
|
|
|
+| `web-admin/src/views/pl/_shared/IoOrderListPage.vue` | 列表列 + 编辑弹窗 |
|
|
|
+| `web-admin/src/components/business/pl-plan-view-tabs/index.vue` | 回传 KV + 产品表 + 快捷编辑表 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 后端 enrichment helper + `order_header_to_dict`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/services/pl_io_order_service.py`
|
|
|
+- Create: `backend/tests/test_pl_io_order_enrichment.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败测试(单位标签与计划字段合并)**
|
|
|
+
|
|
|
+```python
|
|
|
+# backend/tests/test_pl_io_order_enrichment.py
|
|
|
+from app.services.pl_io_order_service import (
|
|
|
+ resolve_unit_label,
|
|
|
+ PLAN_TYPE_BUSINESS_LABEL,
|
|
|
+ merge_plan_logistics_into_order,
|
|
|
+ merge_plan_product_into_item,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def test_resolve_unit_label_maps_dun():
|
|
|
+ assert resolve_unit_label({"dun": "吨"}, "dun") == "吨"
|
|
|
+ assert resolve_unit_label({}, "dun") == "dun"
|
|
|
+ assert resolve_unit_label({}, None) == "吨"
|
|
|
+ assert resolve_unit_label({}, "") == "吨"
|
|
|
+
|
|
|
+
|
|
|
+def test_merge_plan_logistics_into_order():
|
|
|
+ data = {"remark": "order-remark"}
|
|
|
+ plan = type(
|
|
|
+ "P",
|
|
|
+ (),
|
|
|
+ {
|
|
|
+ "plan_type": "outbound",
|
|
|
+ "remark": "plan-remark",
|
|
|
+ "pickup_address": "取货路1号",
|
|
|
+ "pickup_lng": 117.0,
|
|
|
+ "pickup_lat": 30.5,
|
|
|
+ "pickup_time": None,
|
|
|
+ "pickup_notes": "戴帽",
|
|
|
+ "destination_name": "仓库A",
|
|
|
+ "destination_address": "送货路2号",
|
|
|
+ "destination_lng": None,
|
|
|
+ "destination_lat": None,
|
|
|
+ "delivery_time": None,
|
|
|
+ "destination_notes": "北门",
|
|
|
+ },
|
|
|
+ )()
|
|
|
+ merge_plan_logistics_into_order(data, plan)
|
|
|
+ assert data["businessType"] == "出库"
|
|
|
+ assert data["planRemark"] == "plan-remark"
|
|
|
+ assert data["remark"] == "order-remark" # 单据备注不被覆盖
|
|
|
+ assert data["pickupAddress"] == "取货路1号"
|
|
|
+ assert data["destinationAddress"] == "送货路2号"
|
|
|
+ assert data["destinationName"] == "仓库A"
|
|
|
+
|
|
|
+
|
|
|
+def test_merge_plan_product_into_item():
|
|
|
+ item = {"unit": "dun", "productName": "尿素"}
|
|
|
+ product = type(
|
|
|
+ "PP",
|
|
|
+ (),
|
|
|
+ {
|
|
|
+ "product_no": "P001",
|
|
|
+ "package_spec": "50kg/袋",
|
|
|
+ "remark": "易潮",
|
|
|
+ "unit": "dun",
|
|
|
+ "model": "46.4%",
|
|
|
+ "product_name": "工业级尿素",
|
|
|
+ },
|
|
|
+ )()
|
|
|
+ merge_plan_product_into_item(item, product, unit_map={"dun": "吨"})
|
|
|
+ assert item["productNo"] == "P001"
|
|
|
+ assert item["packageSpec"] == "50kg/袋"
|
|
|
+ assert item["remark"] == "易潮"
|
|
|
+ assert item["unitLabel"] == "吨"
|
|
|
+ assert item["unit"] == "dun"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 运行确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_pl_io_order_enrichment.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(函数未定义)
|
|
|
+
|
|
|
+- [ ] **Step 3: 在 `pl_io_order_service.py` 实现 helper(放在 `order_header_to_dict` 之前)**
|
|
|
+
|
|
|
+```python
|
|
|
+CRM_PRODUCT_UNIT = "crm_product_unit"
|
|
|
+
|
|
|
+PLAN_TYPE_BUSINESS_LABEL = {
|
|
|
+ "inbound": "入库",
|
|
|
+ "outbound": "出库",
|
|
|
+ "direct": "直销",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+def resolve_unit_label(unit_map: dict[str, str], unit_code: Optional[str]) -> str:
|
|
|
+ code = (unit_code or "").strip()
|
|
|
+ if not code:
|
|
|
+ return "吨"
|
|
|
+ return unit_map.get(code, code)
|
|
|
+
|
|
|
+
|
|
|
+def merge_plan_logistics_into_order(data: dict, plan: Optional[PlPlan]) -> None:
|
|
|
+ if not plan:
|
|
|
+ data.setdefault("businessType", None)
|
|
|
+ data.setdefault("planRemark", None)
|
|
|
+ data.setdefault("pickupAddress", None)
|
|
|
+ data.setdefault("pickupLng", None)
|
|
|
+ data.setdefault("pickupLat", None)
|
|
|
+ data.setdefault("pickupTime", None)
|
|
|
+ data.setdefault("pickupNotes", None)
|
|
|
+ data.setdefault("destinationName", None)
|
|
|
+ data.setdefault("destinationAddress", None)
|
|
|
+ data.setdefault("destinationLng", None)
|
|
|
+ data.setdefault("destinationLat", None)
|
|
|
+ data.setdefault("deliveryTime", None)
|
|
|
+ data.setdefault("destinationNotes", None)
|
|
|
+ return
|
|
|
+ data["businessType"] = PLAN_TYPE_BUSINESS_LABEL.get(plan.plan_type, plan.plan_type)
|
|
|
+ data["planRemark"] = (plan.remark or "").strip() or None
|
|
|
+ data["pickupAddress"] = plan.pickup_address
|
|
|
+ data["pickupLng"] = plan.pickup_lng
|
|
|
+ data["pickupLat"] = plan.pickup_lat
|
|
|
+ data["pickupTime"] = plan.pickup_time
|
|
|
+ data["pickupNotes"] = plan.pickup_notes
|
|
|
+ data["destinationName"] = plan.destination_name
|
|
|
+ data["destinationAddress"] = plan.destination_address or plan.destination_name
|
|
|
+ data["destinationLng"] = plan.destination_lng
|
|
|
+ data["destinationLat"] = plan.destination_lat
|
|
|
+ data["deliveryTime"] = plan.delivery_time
|
|
|
+ data["destinationNotes"] = plan.destination_notes
|
|
|
+
|
|
|
+
|
|
|
+def merge_plan_product_into_item(
|
|
|
+ item: dict,
|
|
|
+ product: Optional[PlPlanProduct],
|
|
|
+ *,
|
|
|
+ unit_map: dict[str, str],
|
|
|
+) -> None:
|
|
|
+ if not product:
|
|
|
+ item.setdefault("productNo", None)
|
|
|
+ item.setdefault("packageSpec", None)
|
|
|
+ item.setdefault("remark", None)
|
|
|
+ item["unitLabel"] = resolve_unit_label(unit_map, item.get("unit"))
|
|
|
+ return
|
|
|
+ item["productNo"] = product.product_no
|
|
|
+ item["packageSpec"] = product.package_spec
|
|
|
+ item["remark"] = (product.remark or "").strip() or None
|
|
|
+ if not item.get("specification") and product.model:
|
|
|
+ item["specification"] = product.model
|
|
|
+ item["unitLabel"] = resolve_unit_label(unit_map, item.get("unit") or product.unit)
|
|
|
+
|
|
|
+
|
|
|
+def enrich_io_order_dict(
|
|
|
+ db: Session,
|
|
|
+ data: dict,
|
|
|
+ *,
|
|
|
+ tenant_no: str,
|
|
|
+ plan_id: Optional[str],
|
|
|
+ items: list[Any] | None,
|
|
|
+ public_unit_as_label: bool = False,
|
|
|
+) -> dict:
|
|
|
+ """读时附加计划取送货与产品扩展字段。"""
|
|
|
+ from app.utils.excel_export import load_dict_label_maps
|
|
|
+
|
|
|
+ plan = None
|
|
|
+ if plan_id:
|
|
|
+ plan = (
|
|
|
+ db.query(PlPlan)
|
|
|
+ .filter(PlPlan.id == plan_id, PlPlan.del_flag == 0)
|
|
|
+ .first()
|
|
|
+ )
|
|
|
+ merge_plan_logistics_into_order(data, plan)
|
|
|
+
|
|
|
+ unit_map = load_dict_label_maps(
|
|
|
+ db, [CRM_PRODUCT_UNIT], tenant_no=tenant_no
|
|
|
+ ).get(CRM_PRODUCT_UNIT, {})
|
|
|
+
|
|
|
+ raw_items = data.get("items")
|
|
|
+ if raw_items is None:
|
|
|
+ return data
|
|
|
+
|
|
|
+ product_ids = [
|
|
|
+ it.get("planProductId")
|
|
|
+ for it in raw_items
|
|
|
+ if it.get("planProductId")
|
|
|
+ ]
|
|
|
+ products_by_id: dict[str, PlPlanProduct] = {}
|
|
|
+ if product_ids:
|
|
|
+ rows = (
|
|
|
+ db.query(PlPlanProduct)
|
|
|
+ .filter(
|
|
|
+ PlPlanProduct.id.in_(product_ids),
|
|
|
+ PlPlanProduct.del_flag == 0,
|
|
|
+ )
|
|
|
+ .all()
|
|
|
+ )
|
|
|
+ products_by_id = {r.id: r for r in rows}
|
|
|
+
|
|
|
+ for it in raw_items:
|
|
|
+ pp = products_by_id.get(it.get("planProductId") or "")
|
|
|
+ merge_plan_product_into_item(it, pp, unit_map=unit_map)
|
|
|
+ if public_unit_as_label:
|
|
|
+ it["unit"] = it.get("unitLabel") or resolve_unit_label(unit_map, it.get("unit"))
|
|
|
+
|
|
|
+ return data
|
|
|
+```
|
|
|
+
|
|
|
+确保文件顶部已 import:`PlPlan`, `PlPlanProduct`(若尚未导入则补上)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 改造 `order_header_to_dict` 签名与调用 enrichment**
|
|
|
+
|
|
|
+将函数改为接受 `db`(可选,有则 enrichment):
|
|
|
+
|
|
|
+```python
|
|
|
+def order_header_to_dict(
|
|
|
+ kind: str,
|
|
|
+ row: OrderHeader,
|
|
|
+ items: list[Any] | None = None,
|
|
|
+ *,
|
|
|
+ db: Session | None = None,
|
|
|
+ public_unit_as_label: bool = False,
|
|
|
+) -> dict:
|
|
|
+ # ... existing data / items build ...
|
|
|
+ if db is not None:
|
|
|
+ enrich_io_order_dict(
|
|
|
+ db,
|
|
|
+ data,
|
|
|
+ tenant_no=row.tenant_no,
|
|
|
+ plan_id=row.plan_id,
|
|
|
+ items=items,
|
|
|
+ public_unit_as_label=public_unit_as_label,
|
|
|
+ )
|
|
|
+ return data
|
|
|
+```
|
|
|
+
|
|
|
+明细 dict 中保持 `unit` 为库内码;`unitLabel` 由 enrichment 写入。
|
|
|
+
|
|
|
+- [ ] **Step 5: 跑通单元测试**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_pl_io_order_enrichment.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/services/pl_io_order_service.py backend/tests/test_pl_io_order_enrichment.py
|
|
|
+git commit -m "feat(pl): 出入库单读时 enrichment 取送货与产品扩展字段"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: Schema + 管理端 / 公共 API 接入 enrichment
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `backend/app/schemas/pl/io_order.py`
|
|
|
+- Modify: `backend/app/api/pl/io_order.py`
|
|
|
+- Modify: `backend/app/api/pl/public_io_order.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: 扩展 Pydantic 模型**
|
|
|
+
|
|
|
+`IoOrderItemRead` 增加:
|
|
|
+
|
|
|
+```python
|
|
|
+product_no: Optional[str] = Field(default=None, alias="productNo")
|
|
|
+package_spec: Optional[str] = Field(default=None, alias="packageSpec")
|
|
|
+remark: Optional[str] = None
|
|
|
+unit_label: Optional[str] = Field(default=None, alias="unitLabel")
|
|
|
+```
|
|
|
+
|
|
|
+`IoOrderRead` 增加(均为 Optional):
|
|
|
+
|
|
|
+```python
|
|
|
+business_type: Optional[str] = Field(default=None, alias="businessType")
|
|
|
+plan_remark: Optional[str] = Field(default=None, alias="planRemark")
|
|
|
+pickup_address: Optional[str] = Field(default=None, alias="pickupAddress")
|
|
|
+pickup_lng: Optional[float] = Field(default=None, alias="pickupLng")
|
|
|
+pickup_lat: Optional[float] = Field(default=None, alias="pickupLat")
|
|
|
+pickup_time: Optional[datetime] = Field(default=None, alias="pickupTime")
|
|
|
+pickup_notes: Optional[str] = Field(default=None, alias="pickupNotes")
|
|
|
+destination_name: Optional[str] = Field(default=None, alias="destinationName")
|
|
|
+destination_address: Optional[str] = Field(default=None, alias="destinationAddress")
|
|
|
+destination_lng: Optional[float] = Field(default=None, alias="destinationLng")
|
|
|
+destination_lat: Optional[float] = Field(default=None, alias="destinationLat")
|
|
|
+delivery_time: Optional[datetime] = Field(default=None, alias="deliveryTime")
|
|
|
+destination_notes: Optional[str] = Field(default=None, alias="destinationNotes")
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 管理端 `_to_read` 传入 db**
|
|
|
+
|
|
|
+```python
|
|
|
+def _to_read(kind: str, header, items, db: Session) -> IoOrderRead:
|
|
|
+ return IoOrderRead.model_validate(
|
|
|
+ order_header_to_dict(kind, header, items, db=db)
|
|
|
+ )
|
|
|
+```
|
|
|
+
|
|
|
+所有 `_to_read(...)` 调用补上 `db`(list / get / patch / by-plan)。
|
|
|
+
|
|
|
+- [ ] **Step 3: 公共 `_build_page`**
|
|
|
+
|
|
|
+```python
|
|
|
+def _build_page(db: Session, kind: str, header, items) -> dict:
|
|
|
+ plan = db.query(PlPlan).filter(PlPlan.id == header.plan_id, PlPlan.del_flag == 0).first()
|
|
|
+ voided = header.status == "voided" or bool(plan and plan.follow_status == "cancelled")
|
|
|
+ data = order_header_to_dict(
|
|
|
+ kind, header, items, db=db, public_unit_as_label=True
|
|
|
+ )
|
|
|
+ data["voided"] = voided
|
|
|
+ data["title"] = "入库单" if kind == "inbound" else "出库单"
|
|
|
+ # enrichment 已写入 pickup/destination;勿再重复简陋覆盖丢失经纬度
|
|
|
+ return data
|
|
|
+```
|
|
|
+
|
|
|
+删除旧的仅写 `pickupAddress`/`destinationAddress` 两行简陋赋值(由 enrichment 统一提供)。
|
|
|
+
|
|
|
+- [ ] **Step 4: 冒烟 import**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && python -c "
|
|
|
+from app.api.pl.io_order import inbound_router
|
|
|
+from app.api.pl.public_io_order import outbound_public_router
|
|
|
+print('ok')
|
|
|
+"
|
|
|
+```
|
|
|
+
|
|
|
+Expected: `ok`
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/app/schemas/pl/io_order.py backend/app/api/pl/io_order.py backend/app/api/pl/public_io_order.py
|
|
|
+git commit -m "feat(pl): 出入库 API 接入 enrichment 并扩展 schema"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 前端 API 类型
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/api/pl/index.ts`
|
|
|
+- Modify: `web-admin/src/api/pl/public-io-order.ts`
|
|
|
+
|
|
|
+- [ ] **Step 1: `PlIoOrderItem` 扩展**
|
|
|
+
|
|
|
+```typescript
|
|
|
+export interface PlIoOrderItem {
|
|
|
+ id: string
|
|
|
+ orderId: string
|
|
|
+ planProductId: string
|
|
|
+ productId?: string
|
|
|
+ productNo?: string
|
|
|
+ productName?: string
|
|
|
+ specification?: string
|
|
|
+ unit?: string
|
|
|
+ unitLabel?: string
|
|
|
+ planQty?: number
|
|
|
+ actualQty?: number
|
|
|
+ qtySource?: string
|
|
|
+ packageSpec?: string
|
|
|
+ remark?: string
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: `PlIoOrder` 扩展**
|
|
|
+
|
|
|
+```typescript
|
|
|
+ businessType?: string
|
|
|
+ planRemark?: string
|
|
|
+ pickupAddress?: string
|
|
|
+ pickupLng?: number | null
|
|
|
+ pickupLat?: number | null
|
|
|
+ pickupTime?: string
|
|
|
+ pickupNotes?: string
|
|
|
+ destinationName?: string
|
|
|
+ destinationAddress?: string
|
|
|
+ destinationLng?: number | null
|
|
|
+ destinationLat?: number | null
|
|
|
+ deliveryTime?: string
|
|
|
+ destinationNotes?: string
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: `PublicIoOrderPage` / `PublicIoOrderItem` 对齐公共响应**
|
|
|
+
|
|
|
+`PublicIoOrderItem`:`packageSpec?`、`remark?`、`productNo?`(可选)
|
|
|
+`PublicIoOrderPage`:与上列物流字段一致 + `planRemark?` + `businessType?` + `date?`(可用 `planDate`)
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/api/pl/index.ts web-admin/src/api/pl/public-io-order.ts
|
|
|
+git commit -m "feat(web-admin): 扩展出入库单与公共页 TypeScript 类型"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 现场公共页 UI(对齐 Demo)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue`
|
|
|
+- Reference: `web-admin/src/views/public/pl/site/index.vue`(导航、地址 cell)
|
|
|
+- Reference: `docs/superpowers/brainstorm/2026-07-19-pl-outbound-site-link-demo.html`
|
|
|
+
|
|
|
+- [ ] **Step 1: 模板按 Demo 区块重排**
|
|
|
+
|
|
|
+概况 `van-cell-group`:业务类型、日期(`planDate`)、往来单位、车牌、计划号、单号、状态、`planRemark`(有则显示)。
|
|
|
+
|
|
|
+取货 / 送货两组:地址 + `buildAmapNavigationUrl` 导航按钮、时间 `formatDateTime`、注意事项;样式复用 site 页 `address-cell` / `multiline-cell`。
|
|
|
+
|
|
|
+产品区:每项 `.product-card` — 名称、规格、包装规格、计划数量+`unit`(已是标签)、实际数量 `van-field`;样式按 Demo(绿边卡片、大字输入)。
|
|
|
+
|
|
|
+回执、现场备注、上次保存、底部提交保留。
|
|
|
+
|
|
|
+- [ ] **Step 2: script 补导航与格式化**
|
|
|
+
|
|
|
+```typescript
|
|
|
+import { buildAmapNavigationUrl } from '@/utils/amap/navigation'
|
|
|
+
|
|
|
+function formatDateTime(value?: string) {
|
|
|
+ if (!value) return '—'
|
|
|
+ const normalized = String(value).replace('T', ' ')
|
|
|
+ return normalized.length > 16 ? normalized.slice(0, 16) : normalized
|
|
|
+}
|
|
|
+
|
|
|
+const pickupNavUrl = computed(() => {
|
|
|
+ if (!page.value) return null
|
|
|
+ return buildAmapNavigationUrl(
|
|
|
+ page.value.pickupLng,
|
|
|
+ page.value.pickupLat,
|
|
|
+ page.value.pickupAddress || '取货点'
|
|
|
+ )
|
|
|
+})
|
|
|
+// destinationNavUrl 同理
|
|
|
+```
|
|
|
+
|
|
|
+`navTitle` 可用:`出库单 · 现场回执` / `入库单 · 现场回执`。
|
|
|
+
|
|
|
+- [ ] **Step 3: 本地打开任一 token 页目视核对**
|
|
|
+
|
|
|
+对照 Demo:单位为「吨」、有包装规格、有取送货。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue
|
|
|
+git commit -m "feat(web-admin): 出入库现场链接页对齐 Demo 信息密度与样式"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 管理端列表 + 编辑弹窗
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/pl/_shared/IoOrderListPage.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 列表增加两列(放在「产品」前或「车牌」后)**
|
|
|
+
|
|
|
+```vue
|
|
|
+<el-table-column
|
|
|
+ prop="pickupAddress"
|
|
|
+ label="取货地址"
|
|
|
+ min-width="160"
|
|
|
+ show-overflow-tooltip
|
|
|
+/>
|
|
|
+<el-table-column label="送货地址" min-width="160" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.destinationAddress || row.destinationName || '—' }}
|
|
|
+ </template>
|
|
|
+</el-table-column>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 弹窗加宽并补主信息**
|
|
|
+
|
|
|
+`width="1080px"`。在往来单位后增加只读项:
|
|
|
+
|
|
|
+- 取货地址、取货时间、取货注意事项(注意事项 / 地址 `form-col-span-2`)
|
|
|
+- 送货地址、送达时间、送货注意事项
|
|
|
+
|
|
|
+时间用简单展示:`editForm.pickupTime` 字符串即可。
|
|
|
+
|
|
|
+- [ ] **Step 3: 产品表改为 8 列**
|
|
|
+
|
|
|
+```vue
|
|
|
+<el-table-column prop="productNo" label="产品编号" min-width="110" show-overflow-tooltip />
|
|
|
+<el-table-column prop="productName" label="产品名称" min-width="120" show-overflow-tooltip />
|
|
|
+<el-table-column prop="specification" label="型号/规格" min-width="100" show-overflow-tooltip />
|
|
|
+<el-table-column label="单位" min-width="72" align="center">
|
|
|
+ <template #default="{ row }">{{ row.unitLabel || row.unit || '—' }}</template>
|
|
|
+</el-table-column>
|
|
|
+<el-table-column prop="planQty" label="计划数量" min-width="100" align="right" />
|
|
|
+<el-table-column label="实际数量" min-width="120">
|
|
|
+ <!-- 既有 el-input-number -->
|
|
|
+</el-table-column>
|
|
|
+<el-table-column prop="packageSpec" label="包装规格" min-width="110" show-overflow-tooltip />
|
|
|
+<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/pl/_shared/IoOrderListPage.vue
|
|
|
+git commit -m "feat(web-admin): 出入库列表与编辑弹窗展示取送货及完整产品列"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: 计划查看回传对齐
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/components/business/pl-plan-view-tabs/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 扩展 `ioOrderViewFields`**
|
|
|
+
|
|
|
+在既有字段后追加:
|
|
|
+
|
|
|
+```typescript
|
|
|
+{ label: '取货地址', value: ioOrder.value.pickupAddress, span: 'full' },
|
|
|
+{ label: '取货时间', value: formatDateTime(ioOrder.value.pickupTime) || '—' },
|
|
|
+{ label: '取货注意事项', value: ioOrder.value.pickupNotes, span: 'full' },
|
|
|
+{
|
|
|
+ label: '送货地址',
|
|
|
+ value: ioOrder.value.destinationAddress || ioOrder.value.destinationName,
|
|
|
+ span: 'full'
|
|
|
+},
|
|
|
+{ label: '送达时间', value: formatDateTime(ioOrder.value.deliveryTime) || '—' },
|
|
|
+{ label: '送货注意事项', value: ioOrder.value.destinationNotes, span: 'full' },
|
|
|
+```
|
|
|
+
|
|
|
+(若 `ViewField` 无 `span` 则按该组件既有 full-width 写法。)
|
|
|
+
|
|
|
+- [ ] **Step 2: 只读子表列对齐 Task 5 的 8 列**
|
|
|
+
|
|
|
+替换现有仅 `productName` / `specification` / `planQty` / `actualQty` 的列定义。
|
|
|
+
|
|
|
+- [ ] **Step 3: 快捷编辑弹窗 `ioOrderEditForm` 产品表同样 8 列**(实际数量可编辑)
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/components/business/pl-plan-view-tabs/index.vue
|
|
|
+git commit -m "feat(web-admin): 计划查看出入库回传补齐取送货与产品列"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: 图谱更新与手工验收
|
|
|
+
|
|
|
+**Files:** 无新增业务文件时仍更新图谱
|
|
|
+
|
|
|
+- [ ] **Step 1: 更新知识图谱**
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh all
|
|
|
+```
|
|
|
+
|
|
|
+或分别 `backend` / `frontend`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 手工验收对照 Spec §8**
|
|
|
+
|
|
|
+| ID | 操作 |
|
|
|
+|----|------|
|
|
|
+| V1 | 打开出库现场链接:取送货、导航、包装规格、单位「吨」 |
|
|
|
+| V2 | 入库现场链接对称 |
|
|
|
+| V3 | `/#/pl/.../inbound` 与 outbound 列表有取货/送货列 |
|
|
|
+| V4 | 编辑弹窗 8 列产品 + 取送货;改实际数量保存成功 |
|
|
|
+| V5 | 计划查看回传区一致 |
|
|
|
+| V6 | 作废单现场不可提交 |
|
|
|
+
|
|
|
+- [ ] **Step 3: 若有图谱变更则 commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/graphify-out web-admin/graphify-out graphify-out 2>/dev/null || true
|
|
|
+git commit -m "chore: 更新 graphify 图谱(出入库 enrichment)" || true
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec 覆盖自检
|
|
|
+
|
|
|
+| Spec 项 | Task |
|
|
|
+|---------|------|
|
|
|
+| D1 读时 enrichment | Task 1–2 |
|
|
|
+| D2 入库出库对称 | Task 4–6 共享组件 |
|
|
|
+| D3 表面 C | Task 5–6 |
|
|
|
+| D4 单位标签 | Task 1 `unitLabel` / 公共 `public_unit_as_label` |
|
|
|
+| D5 列表不铺满产品列 | Task 5 仅地址列 |
|
|
|
+| D6 列表物流列 | Task 5 |
|
|
|
+| D7 扩展只读 | Task 5–6 |
|
|
|
+| 公共页 Demo | Task 4 |
|
|
|
+| V1–V6 | Task 7 |
|
|
|
+
|
|
|
+无 DDL;不改旧 `/public/pl/site`。
|