diff --git a/docs/superpowers/plans/2026-07-19-pl-io-order-site-link-enrichment.md b/docs/superpowers/plans/2026-07-19-pl-io-order-site-link-enrichment.md
new file mode 100644
index 000000000..15b8225b9
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-19-pl-io-order-site-link-enrichment.md
@@ -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
+
+
+
+ {{ row.destinationAddress || row.destinationName || '—' }}
+
+
+```
+
+- [ ] **Step 2: 弹窗加宽并补主信息**
+
+`width="1080px"`。在往来单位后增加只读项:
+
+- 取货地址、取货时间、取货注意事项(注意事项 / 地址 `form-col-span-2`)
+- 送货地址、送达时间、送货注意事项
+
+时间用简单展示:`editForm.pickupTime` 字符串即可。
+
+- [ ] **Step 3: 产品表改为 8 列**
+
+```vue
+
+
+
+
+ {{ row.unitLabel || row.unit || '—' }}
+
+
+
+
+
+
+
+```
+
+- [ ] **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`。