diff --git a/docs/superpowers/plans/2026-07-19-pl-site-io-order-status-select.md b/docs/superpowers/plans/2026-07-19-pl-site-io-order-status-select.md
new file mode 100644
index 000000000..712d920c8
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-19-pl-site-io-order-status-select.md
@@ -0,0 +1,504 @@
+# 出入库现场链接 · 单据状态可选 — 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:** 现场出入库链接页可选手动单据状态(进行中 / 已完成 / 已作废),提交时按所选写入;作废后整页只读可查看。
+
+**Architecture:** `PublicIoOrderFeedback` 增加必填 `status`;`apply_site_feedback` 显式写入状态并删除「首次提交自动已完成」。前端 `PublicIoOrderPage` 增加 `van-radio-group`,作废确认后提交;`voided` 时改只读展示而非空白 empty。
+
+**Tech Stack:** FastAPI + Pydantic v2 + SQLAlchemy;Vue 3 + Vant(公共页)
+
+**Spec:** `docs/superpowers/specs/2026-07-19-pl-site-io-order-status-select-design.md`
+
+---
+
+## 文件结构
+
+| 文件 | 职责 |
+|------|------|
+| `backend/app/schemas/pl/io_order.py` | `PublicIoOrderFeedback.status` |
+| `backend/app/services/pl_io_order_service.py` | `apply_site_feedback` 接收并写入 `status` |
+| `backend/app/api/pl/public_io_order.py` | `save_feedback` 传入 `payload.status` |
+| `backend/tests/test_pl_io_order_site_feedback_status.py` | 状态写入 / 非法值 / 已作废拒绝 / 不再隐式完成 |
+| `web-admin/src/api/pl/public-io-order.ts` | `saveFeedback` 参数含 `status` |
+| `web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue` | 状态选择、作废确认、只读态 |
+| `docs/superpowers/specs/2026-07-19-pl-inbound-outbound-order-design.md` | D5 旁注被本设计覆盖(可选收尾) |
+
+---
+
+### Task 1: 后端 `apply_site_feedback` 显式 status(TDD)
+
+**Files:**
+- Create: `backend/tests/test_pl_io_order_site_feedback_status.py`
+- Modify: `backend/app/services/pl_io_order_service.py`(`apply_site_feedback`)
+- Modify: `backend/app/schemas/pl/io_order.py`(`PublicIoOrderFeedback`)
+- Modify: `backend/app/api/pl/public_io_order.py`(传入 `status`)
+
+- [ ] **Step 1: 写失败测试**
+
+在 `backend/tests/test_pl_io_order_site_feedback_status.py` 写入:
+
+```python
+from datetime import datetime
+from decimal import Decimal
+from types import SimpleNamespace
+from unittest.mock import MagicMock
+
+import pytest
+
+from app.services.pl_io_order_service import apply_site_feedback
+
+
+def _header(**kwargs):
+ defaults = dict(
+ status="in_progress",
+ site_feedback_remark=None,
+ site_feedback_images=None,
+ site_feedback_time=None,
+ site_first_submitted_at=None,
+ update_by=None,
+ )
+ defaults.update(kwargs)
+ return SimpleNamespace(**defaults)
+
+
+def _item(item_id="item-1"):
+ return SimpleNamespace(
+ id=item_id,
+ actual_qty=None,
+ qty_source=None,
+ update_by=None,
+ )
+
+
+def test_apply_site_feedback_sets_status_completed():
+ header = _header()
+ item = _item()
+ db = MagicMock()
+ apply_site_feedback(
+ db,
+ header=header,
+ items=[item],
+ item_qty={"item-1": Decimal("12.5")},
+ remark="ok",
+ images=["a.jpg"],
+ status="completed",
+ )
+ assert header.status == "completed"
+ assert item.actual_qty == Decimal("12.5")
+ assert header.site_first_submitted_at is not None
+ assert header.site_feedback_remark == "ok"
+ db.flush.assert_called_once()
+
+
+def test_apply_site_feedback_sets_status_voided():
+ header = _header()
+ apply_site_feedback(
+ MagicMock(),
+ header=header,
+ items=[_item()],
+ item_qty={"item-1": Decimal("1")},
+ remark=None,
+ images=[],
+ status="voided",
+ )
+ assert header.status == "voided"
+
+
+def test_apply_site_feedback_keeps_in_progress_when_requested():
+ """首次提交选进行中时,不得隐式改成已完成。"""
+ header = _header(status="in_progress")
+ apply_site_feedback(
+ MagicMock(),
+ header=header,
+ items=[_item()],
+ item_qty={"item-1": Decimal("1")},
+ remark=None,
+ images=[],
+ status="in_progress",
+ )
+ assert header.status == "in_progress"
+ assert header.site_first_submitted_at is not None
+
+
+def test_apply_site_feedback_rejects_invalid_status():
+ header = _header()
+ with pytest.raises(ValueError, match="无效状态"):
+ apply_site_feedback(
+ MagicMock(),
+ header=header,
+ items=[_item()],
+ item_qty={},
+ remark=None,
+ images=[],
+ status="done",
+ )
+
+
+def test_apply_site_feedback_rejects_already_voided():
+ header = _header(status="voided")
+ with pytest.raises(ValueError, match="单据已作废"):
+ apply_site_feedback(
+ MagicMock(),
+ header=header,
+ items=[_item()],
+ item_qty={},
+ remark=None,
+ images=[],
+ status="completed",
+ )
+
+
+def test_apply_site_feedback_does_not_reset_first_submitted_at():
+ first = datetime(2026, 1, 1, 12, 0, 0)
+ header = _header(
+ status="completed",
+ site_first_submitted_at=first,
+ )
+ apply_site_feedback(
+ MagicMock(),
+ header=header,
+ items=[_item()],
+ item_qty={"item-1": Decimal("2")},
+ remark="二改",
+ images=[],
+ status="in_progress",
+ )
+ assert header.status == "in_progress"
+ assert header.site_first_submitted_at == first
+```
+
+- [ ] **Step 2: 跑测试确认失败**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_pl_io_order_site_feedback_status.py -v
+```
+
+Expected: FAIL(`apply_site_feedback()` 缺少 `status` 参数,或行为仍为隐式 `completed`)
+
+- [ ] **Step 3: Schema 增加 `status`**
+
+修改 `backend/app/schemas/pl/io_order.py` 中 `PublicIoOrderFeedback`:
+
+```python
+class PublicIoOrderFeedback(BaseModel):
+ status: str
+ remark: Optional[str] = None
+ images: list[str] = Field(default_factory=list)
+ items: list[PublicIoOrderItemFeedback] = Field(default_factory=list)
+ model_config = {"populate_by_name": True}
+```
+
+- [ ] **Step 4: 改 `apply_site_feedback`**
+
+将 `backend/app/services/pl_io_order_service.py` 中函数改为:
+
+```python
+_IO_ORDER_STATUS_OK = frozenset({"in_progress", "completed", "voided"})
+
+
+def apply_site_feedback(
+ db: Session,
+ *,
+ header: OrderHeader,
+ items: list[OrderItem],
+ item_qty: dict[str, Decimal],
+ remark: Optional[str],
+ images: list[str],
+ status: str,
+ user_id: Optional[str] = None,
+) -> None:
+ if header.status == "voided":
+ raise ValueError("单据已作废,无法提交")
+ if status not in _IO_ORDER_STATUS_OK:
+ raise ValueError("无效状态")
+ now = datetime.now()
+ for item in items:
+ if item.id not in item_qty:
+ continue
+ item.actual_qty = item_qty[item.id]
+ item.qty_source = "manual"
+ item.update_by = user_id
+ header.site_feedback_remark = (remark or "").strip() or None
+ header.site_feedback_images = dump_feedback_images(images) if images else header.site_feedback_images
+ header.site_feedback_time = now
+ header.status = status
+ if not header.site_first_submitted_at:
+ header.site_first_submitted_at = now
+ header.update_by = user_id
+ db.flush()
+```
+
+要点:删除 `if header.status == "in_progress": header.status = "completed"`。
+
+- [ ] **Step 5: API 传入 `status`**
+
+修改 `backend/app/api/pl/public_io_order.py` 中 `apply_site_feedback(...)` 调用,增加:
+
+```python
+status=payload.status,
+```
+
+- [ ] **Step 6: 跑测试确认通过**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_pl_io_order_site_feedback_status.py -v
+```
+
+Expected: 全部 PASS
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add backend/tests/test_pl_io_order_site_feedback_status.py \
+ backend/app/services/pl_io_order_service.py \
+ backend/app/schemas/pl/io_order.py \
+ backend/app/api/pl/public_io_order.py
+git commit -m "$(cat <<'EOF'
+feat(pl): 现场回执提交显式写入出入库单状态
+
+删除首次提交自动改为已完成的隐式逻辑,改为由客户端传入 status。
+EOF
+)"
+```
+
+---
+
+### Task 2: 前端 API 类型增加 `status`
+
+**Files:**
+- Modify: `web-admin/src/api/pl/public-io-order.ts`
+
+- [ ] **Step 1: 更新 `saveFeedback` 参数类型**
+
+```typescript
+saveFeedback: (
+ token: string,
+ data: {
+ status: 'in_progress' | 'completed' | 'voided'
+ remark?: string
+ images?: string[]
+ items: { id: string; actualQty: number }[]
+ }
+) => publicPlPost<{ success: boolean }>(`${base}/${token}/feedback`, data),
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add web-admin/src/api/pl/public-io-order.ts
+git commit -m "$(cat <<'EOF'
+feat(web-admin): 出入库现场回执 API 增加 status 字段
+EOF
+)"
+```
+
+---
+
+### Task 3: 公共页状态选择 + 作废确认 + 只读态
+
+**Files:**
+- Modify: `web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue`
+
+- [ ] **Step 1: script — 增加状态与只读计算**
+
+在现有 `ref` 旁增加:
+
+```typescript
+import { showConfirmDialog, showFailToast, showSuccessToast } from 'vant'
+import 'vant/es/dialog/style'
+
+const selectedStatus = ref<'in_progress' | 'completed' | 'voided'>('in_progress')
+const isReadOnly = computed(() => Boolean(page.value?.voided))
+```
+
+在 `loadPage` 成功赋值后:
+
+```typescript
+selectedStatus.value = (['in_progress', 'completed', 'voided'].includes(data.status || '')
+ ? data.status
+ : 'in_progress') as 'in_progress' | 'completed' | 'voided'
+```
+
+- [ ] **Step 2: template — 去掉 voided 整页 empty,改为可展示 + 提示条**
+
+将:
+
+```vue
+
+
+```
+
+改为:
+
+```vue
+
+
+```
+
+(保留链接无效时的 `errorMessage` empty。)
+
+- [ ] **Step 3: template — 状态选择器(可编辑时)**
+
+在「现场备注」`van-cell-group` **之前**插入:
+
+```vue
+
+
+
+ 进行中
+ 已完成
+ 已作废
+
+
+
+```
+
+样式(scoped):
+
+```scss
+.status-radio-wrap {
+ padding: 12px 16px 16px;
+}
+```
+
+- [ ] **Step 4: template — 只读禁用数量 / 上传 / 备注;隐藏提交**
+
+- 实际数量 `van-field`:加 `:readonly="isReadOnly"`
+- `van-uploader`:加 `v-if="!isReadOnly"`;只读时用图片列表展示 `feedbackImages`(可用简单 `
` 行或保留 uploader 且 `:deletable="false"` `:show-upload="false"`)
+- 备注 `van-field`:`:readonly="isReadOnly"`
+- footer:`v-if="!isReadOnly"` 包住提交按钮
+
+推荐 uploader 只读写法:
+
+```vue
+
+```
+
+- [ ] **Step 5: `handleSave` 带 status + 作废确认**
+
+```typescript
+async function handleSave() {
+ if (isReadOnly.value) return
+ if (selectedStatus.value === 'voided') {
+ try {
+ await showConfirmDialog({
+ title: '确认作废',
+ message: '确认作废本出入库单?作废后不可再提交。'
+ })
+ } catch {
+ return
+ }
+ }
+ saving.value = true
+ try {
+ await api.saveFeedback(token.value, {
+ status: selectedStatus.value,
+ remark: feedbackRemark.value,
+ images: feedbackImages.value,
+ items: editableItems.value.map(it => ({
+ id: it.id,
+ actualQty: Number(it.actualQtyText || 0)
+ }))
+ })
+ showSuccessToast('提交成功')
+ await loadPage()
+ } catch (e: any) {
+ showFailToast(e?.message || '提交失败')
+ } finally {
+ saving.value = false
+ }
+}
+```
+
+- [ ] **Step 6: 手工验收(开发服务器)**
+
+1. 打开已安排的出入库现场链接 → 状态默认「进行中」
+2. 选「已完成」提交 → 刷新后顶部状态为已完成,选择器仍为已完成
+3. 再选「进行中」提交 → 回到进行中
+4. 选「已作废」→ 确认 → 提交成功 → 出现提示条,无提交按钮,内容可查看
+5. 管理端确认:仅本单作废,计划跟进状态未变
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue
+git commit -m "$(cat <<'EOF'
+feat(web-admin): 出入库现场页支持选择单据状态并作废只读
+
+提交携带 status;作废需确认;作废后整页只读可查看。
+EOF
+)"
+```
+
+---
+
+### Task 4: 关联设计旁注 + 图谱更新
+
+**Files:**
+- Modify: `docs/superpowers/specs/2026-07-19-pl-inbound-outbound-order-design.md`(D5 行)
+- Run: `./scripts/update-graph.sh all`(或分别 backend / frontend)
+
+- [ ] **Step 1: D5 旁注**
+
+在原 D5 表格单元格末尾追加:
+
+```text
+(现场自动完成规则已被 `2026-07-19-pl-site-io-order-status-select-design.md` 覆盖:现场提交须显式传 status)
+```
+
+- [ ] **Step 2: 更新知识图谱**
+
+```bash
+./scripts/update-graph.sh all
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add docs/superpowers/specs/2026-07-19-pl-inbound-outbound-order-design.md \
+ backend/graphify-out web-admin/graphify-out 2>/dev/null || true
+git add docs/superpowers/specs/2026-07-19-pl-inbound-outbound-order-design.md
+git commit -m "$(cat <<'EOF'
+docs(pl): 标注出入库单 D5 现场状态规则已被显式选择设计覆盖
+EOF
+)"
+```
+
+(若 graphify 输出有变更再一并 add;无变更则只提交 docs。)
+
+---
+
+## 自检(对照 Spec)
+
+| Spec 项 | Task |
+|---------|------|
+| D1 提交体带 status | Task 1 + 2 + 3 |
+| D2 三态可选 | Task 3 radio |
+| D3 默认当前真实状态 | Task 3 `loadPage` |
+| D4 作废后只读可查看 | Task 3 notice + disable |
+| D5 仅作废本单 | Task 1 只写 `header.status` |
+| D6 删除隐式完成 | Task 1 测试 + 实现 |
+| D7 入库出库对称 | 共用 `PublicIoOrderPage` / `apply_site_feedback` |
+| 作废二次确认 | Task 3 `showConfirmDialog` |
+| 无 DDL | 全计划未改 SQL |
+
+---
+
+## 执行说明
+
+完成计划后可选执行方式见下方对话确认。
diff --git a/docs/superpowers/specs/2026-07-19-pl-inbound-outbound-order-design.md b/docs/superpowers/specs/2026-07-19-pl-inbound-outbound-order-design.md
new file mode 100644
index 000000000..a0af3e571
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-19-pl-inbound-outbound-order-design.md
@@ -0,0 +1,240 @@
+# 运输计划 · 入库单 / 出库单设计
+
+**日期:** 2026-07-19
+**状态:** 已确认并实现中
+**范围:** web-admin 运输管理 + backend PL API + 免登录现场公共页
+**界面模式:** 列表对齐标准列表(P01 / `system/unit`);编辑弹窗对齐 Fresh Mint `dialog-form-layout`
+
+---
+
+## 1. 背景与问题
+
+现状为双轨:
+
+- 安排运输后按产品生成 `pl_transport_site_link`(现场仅备注+附件,数量只读)
+- 可选勾选生成地磅 `db_inbound_outbound`(合一列表 `/#/db/inventory`)
+
+客户需要运输计划域下**独立入库单 / 出库单**界面:安排后落单、现场按产品填实际数量、整单附件备注、归档可见;并覆盖取消与「只能作废重建」的约束。
+
+---
+
+## 2. 已确认决策
+
+| # | 决策 | 选择 |
+|---|------|------|
+| D1 | 单据域 | 新建 PL 主从表:入库一套 + 出库一套(对称),不复用地磅表作主单据 |
+| D2 | 地磅关系 | 有关联地磅单时,用其数量**预填** `actual_qty`,仍可改;不做双向自动覆盖 |
+| D3 | 现场链接 | 免登录公共页;**一单一链**(多产品同页) |
+| D4 | 多产品 | 明细各自填实际数量;附件+备注在主表整单共用 |
+| D5 | 状态 | `in_progress` / `completed` / `voided`;新建默认进行中;**现场首次提交 → 自动已完成**;管理端可手动选择进行中/已完成;**已作废**主要由计划作废写入(管理端也可手动置作废,作废后禁止现场提交)(现场自动完成规则已被 `2026-07-19-pl-site-io-order-status-select-design.md` 覆盖:现场提交须显式传 status) |
+| D6 | 已完成可改 | 已完成仍可继续完善数量/附件/备注 |
+| D7 | 归档 | 以入库/出库单为权威,归档**只读展示**;可选同步到 `pl_plan_product` |
+| D8 | 快照 | **最小快照**(兼容作废单历史展示)+ 保留 `plan_id` / `transport_id` |
+| D9 | 计划冻结 | 安排后**完全冻结**:不可编辑计划、**不提供调整运输**;只可作废 → 新建计划再安排 |
+| D10 | 菜单 | 一级「运输管理」→ 二级「库存管理」→「入库单」「出库单」 |
+| D11 | 直销单 | 不生成入库/出库单 |
+
+---
+
+## 3. 菜单与路由
+
+```
+运输管理(PL_MENU_ROOT,既有)
+└── 库存管理(新建 M,建议 id: PL_GROUP_STOCK)
+ ├── 入库单(C,perms: pl:inbound:list)
+ └── 出库单(C,perms: pl:outbound:list)
+```
+
+| 菜单 | 建议 path | component | 路由 Hash(示意) |
+|------|-----------|-----------|-------------------|
+| 库存管理 | `stock-module` | — | `/#/pl/stock-module/...` |
+| 入库单 | `inbound` | `pl/inbound/index` | `/#/pl/stock-module/inbound` |
+| 出库单 | `outbound` | `pl/outbound/index` | `/#/pl/stock-module/outbound` |
+
+说明:
+
+- 与既有「地磅出入库」`/#/db/inventory`、证件提醒路径 `pl/inventory/...` **分离**,故二级目录用 `stock-module`,避免与 `pl_inventory` 库存台账语义混淆。
+- 权限按钮:列表查看、编辑、改状态、复制现场链接(细项实现阶段落库 `sys_menu`)。
+- 角色授权:实现时挂到既有运输相关角色;`super_admin` 默认可见。
+
+---
+
+## 4. 数据模型
+
+### 4.1 入库主表 `pl_inbound_order`
+
+| 字段 | 说明 |
+|------|------|
+| `id` | VARCHAR(64) UUID |
+| `order_no` | 业务单号(如 RK + 日期序号) |
+| `plan_id` | → `pl_plan` |
+| `transport_id` | → `pl_transport` |
+| `status` | `in_progress` \| `completed` \| `voided` |
+| `partner_company` | 最小快照:往来单位 |
+| `vehicle_no` | 最小快照:车牌 |
+| `plan_no` | 最小快照:计划号 |
+| `plan_date` | 最小快照:计划日期 |
+| `site_access_token` | 一单一链,唯一 |
+| `site_feedback_remark` | 整单现场备注 |
+| `site_feedback_images` | 回执图(逗号分隔 URL 或 JSON,与现现场链路一致) |
+| `site_feedback_time` | 最近提交时间 |
+| `site_first_submitted_at` | 首次提交时间;用于自动置 `completed` |
+| 公共字段 | `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag` |
+
+### 4.2 入库明细 `pl_inbound_order_item`
+
+| 字段 | 说明 |
+|------|------|
+| `id` | UUID |
+| `order_id` | → `pl_inbound_order` |
+| `plan_product_id` | → `pl_plan_product` |
+| `product_id` / `product_name` / `specification` / `unit` | 产品快照 |
+| `plan_qty` | 安排时计划数量 |
+| `actual_qty` | 实际数量(可改) |
+| `qty_source` | `plan` \| `dibang` \| `manual` |
+| 公共字段 | 同规范 |
+
+### 4.3 出库
+
+`pl_outbound_order` / `pl_outbound_order_item` 与上表**字段对称**(单号前缀 CK)。
+
+### 4.4 索引建议
+
+- `(tenant_no, plan_id)`、`(tenant_no, transport_id)`、`(tenant_no, status)`、`site_access_token` UNIQUE
+- 明细 `(order_id)`、`(plan_product_id)`
+
+### 4.5 与旧对象
+
+| 对象 | 关系 |
+|------|------|
+| `pl_transport_site_link` | **新安排不再生成**;历史数据只读兼容或后续迁移脚本(实现计划中单列任务) |
+| `db_inbound_outbound` | 仍可选「生成地磅」;有关联时预填 `actual_qty`,不互相覆盖写死 |
+| `pl_plan` / `pl_transport` | 关联键 + 最小快照;列表可跳转计划 |
+
+---
+
+## 5. 生命周期
+
+```
+安排运输(入库/出库计划)
+ → 创建 1 主表 + N 明细,status=in_progress,生成 token
+ → actual_qty 默认 = plan_qty
+ → 若有地磅关联产品行 → 预填 actual_qty,qty_source=dibang
+
+现场首次成功提交
+ → 写明细 actual_qty + 主表备注/附件
+ → site_first_submitted_at 有值则 status → completed(若仍为 in_progress)
+
+管理端
+ → 可手动改 status(含 completed ↔ in_progress)
+ → 已完成仍可改数量/附件/备注
+
+计划作废
+ → 关联未作废的入库/出库单 → voided
+ → token 失效(公共页只读提示已作废,禁止提交)
+
+已安排后
+ → 禁止编辑计划正文
+ → 禁止「调整运输」
+ → 纠错路径:作废计划 → 新建计划 → 再安排(新单 + 新链接)
+```
+
+直销单:安排运输不创建入库/出库单(可继续仅司机链接,若现网有)。
+
+---
+
+## 6. 接口(示意)
+
+### 6.1 管理端
+
+- `GET /api/.../pl/inbound` 列表
+- `GET /api/.../pl/inbound/{id}` 详情(含 items)
+- `PATCH /api/.../pl/inbound/{id}` 编辑(数量、备注、附件、status)
+- `GET /api/.../pl/inbound/{id}/site-url` 复制现场链接
+- 出库对称:`/pl/outbound/...`
+
+安排运输成功响应中返回:`inboundOrder` / `outboundOrder`(含 `siteUrl`),供安排成功弹窗复制。
+
+作废计划:在现有 cancel 流程中追加作废关联入库/出库单。
+
+### 6.2 公共页
+
+- `GET /api/public/pl/inbound/{token}` 摘要 + 明细
+- `POST /api/public/pl/inbound/{token}/feedback` 提交实际数量(按 item)+ 备注 + 附件
+- 出库对称
+
+URL 形态:`{PUBLIC_WEB_ORIGIN}/#/public/pl/inbound/{token}`(出库 `/public/pl/outbound/{token}`)。
+
+---
+
+## 7. 前端界面
+
+### 7.1 列表
+
+- 筛选:单号、计划号、状态、日期、往来单位、车牌等
+- 列:单号(主标识链接打开查看/编辑)、状态 Tag、计划号、往来单位、车牌、产品摘要、实际量合计、首次/最近提交时间
+- 工具栏:复制现场链接(选中一行)等
+- 状态 Tag:`进行中=warning`,`已完成=success`,`已作废=info`(或 danger,实现时对齐 UI 规范枚举)
+
+### 7.2 编辑弹窗
+
+- 主表:状态(可选手动改)、快照信息只读、整单备注与附件
+- 明细表:计划数量只读、实际数量可编辑
+- `label-position="top"`,无背景模糊
+
+### 7.3 现场公共页
+
+- 免登录;布局对齐「一张单编辑」:产品行实际数量 + 底部备注/附件 + 提交
+- `voided`:不可提交;`completed`:仍可再次提交完善
+
+### 7.4 归档
+
+- `PlPlanViewTabs` / 归档弹窗:以关联入库/出库单只读展示实际数量、附件、备注
+- 可选:归档保存时同步 `pl_plan_product.actual_qty`(及 inbound/outbound 数量字段)
+
+### 7.5 运输计划
+
+- `arranged` 后:隐藏/禁用编辑与「调整运输」;作废入口保留
+- 安排成功:复制现场链接改为入库/出库单单链(替代按产品多条旧现场 link)
+
+---
+
+## 8. 非目标(本期不做)
+
+- 入库/出库与地磅单双向自动覆盖
+- 拼车多计划共用一张出入库单
+- 强制迁移全部历史 `pl_transport_site_link`(可后续脚本)
+- 小程序端入库/出库列表(若未单列需求)
+
+---
+
+## 9. 验收要点
+
+| ID | 场景 |
+|----|------|
+| V1 | 入库计划安排 → 库存管理-入库单可见,状态进行中,有现场链接 |
+| V2 | 多产品:一链打开,各产品实际数量独立,备注附件整单 |
+| V3 | 有地磅时 actual 预填地磅量,可改 |
+| V4 | 现场首次提交 → 已完成;管理端可改回进行中/再改完成 |
+| V5 | 已完成后仍可改数量与附件 |
+| V6 | 作废计划 → 单作废,链接不可再提交 |
+| V7 | 已安排计划不可编辑、无调整运输;须作废重建 |
+| V8 | 归档只读看到单上实际数据 |
+| V9 | 菜单:运输管理 → 库存管理 → 入库单/出库单 |
+| V10 | 直销单不产生入库/出库单 |
+
+---
+
+## 10. 实现备注
+
+- DDL 同步 `docs/sql/`;遵守 database-conventions 公共字段与软删
+- 租户隔离、数据权限与现有 PL API 一致
+- 字段一致性:`_row_to_dict` / CreateUpdateBody / 前端 form 对齐
+- 替代旧现场 link 时,安排运输 API 与前端复制链接 UX 一并切换
+
+---
+
+## 11. 决策记录(brainstorm)
+
+- 可视化会话:`.superpowers/brainstorm/` 下 HTML 选项页
+- 方案选型:主从表分离 A;现场公共页 A;一单一链 A;归档权威 A;最小快照 A;计划完全冻结 B
diff --git a/docs/superpowers/specs/2026-07-19-pl-site-io-order-status-select-design.md b/docs/superpowers/specs/2026-07-19-pl-site-io-order-status-select-design.md
new file mode 100644
index 000000000..9c7fc660b
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-19-pl-site-io-order-status-select-design.md
@@ -0,0 +1,172 @@
+# 出入库现场链接 · 单据状态可选设计
+
+**日期:** 2026-07-19
+**状态:** 已确认
+**范围:** web-admin 公共现场页 `PublicIoOrderPage` + backend `public_io_order` / `apply_site_feedback`
+**关联:** 修正 `2026-07-19-pl-inbound-outbound-order-design.md` 中 D5「现场首次提交 → 自动已完成」规则
+**界面模式:** 公共页对齐既有 `public/pl/inbound|outbound`(Vant)
+
+---
+
+## 1. 背景与问题
+
+安排运输后生成出入库单现场链接。当前「提交」只保存实际数量、备注、附件;状态变更依赖服务端隐式规则:
+
+- 首次提交且 `status == in_progress` → 自动改为 `completed`
+- 再次提交不改状态
+- 页面状态只读展示,无法选手动状态(含作废)
+
+现场需要明确选择单据状态(含作废),且默认展示当前真实状态。
+
+---
+
+## 2. 已确认决策
+
+| # | 决策 | 选择 |
+|---|------|------|
+| D1 | 实现方案 | 提交体携带 `status`,服务端按所选写入(方案 1) |
+| D2 | 可选状态 | `in_progress` / `completed` / `voided`(进行中 / 已完成 / 已作废) |
+| D3 | 选择器默认值 | 进入页时展示 **当前单据真实状态**(多为进行中) |
+| D4 | 作废后链接 | 仍可打开 **只读查看**,不可再提交 / 上传 |
+| D5 | 作废影响范围 | **仅本出入库单** `status = voided`;不联动作废运输计划、运输单、地磅、司机链接 |
+| D6 | 隐式完成 | **删除**「首次提交自动已完成」 |
+| D7 | 入库 / 出库 | 对称;共用 `PublicIoOrderPage` 与 `apply_site_feedback` |
+
+---
+
+## 3. 前端(公共页)
+
+文件:`web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue`
+
+### 3.1 状态选择
+
+- 在「现场备注」区块上方增加「单据状态」选择,使用 **`van-radio-group` 横向三项**(与现有 Vant 公共页一致)
+- 选项文案:`进行中` / `已完成` / `已作废`,值分别为 `in_progress` / `completed` / `voided`
+- `loadPage` 后:`selectedStatus = page.status`
+- 可编辑态:顶部信息卡「状态」改为只读展示当前已保存状态;下方选择器为本次拟提交状态
+- 只读态:仅顶部信息卡展示状态标签,不展示选择器
+
+### 3.2 提交
+
+- `saveFeedback` body 增加 `status: selectedStatus`(必填)
+- 当用户选择 `voided` 并点提交:先 `showConfirmDialog`(或等价)确认文案大致为「确认作废本出入库单?作废后不可再提交。」;取消则不请求
+- 提交成功后 `loadPage()` 刷新
+
+### 3.3 只读态(`page.voided === true`)
+
+判定保持后端现有:`header.status == "voided"` **或** 计划 `follow_status == "cancelled"`。
+
+- **不再**仅用 `van-empty`「单据已作废」遮住整页
+- 展示与可编辑态相同的信息结构(业务信息、取送货、产品数量、回执、备注、状态)
+- 数量输入、上传、备注、状态选择、提交按钮全部禁用或隐藏
+- 顶部增加只读提示条:「单据已作废,仅可查看」
+
+### 3.4 类型
+
+- `web-admin/src/api/pl/public-io-order.ts`:`saveFeedback` 参数增加 `status`
+
+---
+
+## 4. 后端
+
+### 4.1 Schema
+
+`PublicIoOrderFeedback`(`backend/app/schemas/pl/io_order.py`)增加:
+
+```text
+status: str # 必填;alias 可选;合法值 in_progress | completed | voided
+```
+
+非法值 → 业务 400「无效状态」。
+
+### 4.2 `apply_site_feedback`
+
+文件:`backend/app/services/pl_io_order_service.py`
+
+1. 若 `header.status == "voided"` → 仍抛 `ValueError("单据已作废,无法提交")`(调用方在 commit 前已用 `page["voided"]` 拦计划取消场景)
+2. 校验 `status ∈ {in_progress, completed, voided}`
+3. 写入 `header.status = status`(显式,不再隐式升级)
+4. 照旧写明细 `actual_qty`、`site_feedback_*`、`site_feedback_time`
+5. `site_first_submitted_at`:若为空则写入当前时间(**与本次所选状态无关**)
+6. **删除**以下逻辑:
+
+```text
+if not header.site_first_submitted_at:
+ ...
+ if header.status == "in_progress":
+ header.status = "completed"
+```
+
+(首次提交时间与状态写入顺序:先写 feedback 字段与 status,再按需补 `site_first_submitted_at`。)
+
+### 4.3 公共 API
+
+`backend/app/api/pl/public_io_order.py` 的 `save_feedback`:把 `payload.status` 传入 `apply_site_feedback`。
+`page["voided"]` 时拒绝提交 / 上传的既有逻辑不变。
+
+### 4.4 非目标
+
+- 不修改管理端 `PATCH` 出入库单改状态逻辑
+- 不调用计划作废(`void_plan`)及其级联(运输取消、地磅、现场旧链 revoke 等)
+- 不改司机公共页、旧 `pl_transport_site_link` 现场页
+- 无 DDL 变更
+
+---
+
+## 5. 数据流
+
+```text
+现场页 load → GET /public/pl/{inbound|outbound}/{token}
+ → selectedStatus = page.status
+
+用户编辑数量/备注/附件/状态 → 提交
+ →(若 status=voided)确认弹窗
+ → POST .../feedback { status, items, remark, images }
+ → apply_site_feedback 写 status + 回执字段
+ → 若写为 voided:下次 GET voided=true → 只读
+```
+
+---
+
+## 6. 错误处理
+
+| 情况 | 行为 |
+|------|------|
+| 链接无效 | 404 / 前端空态(既有) |
+| 已 voided 或计划已取消仍提交 | 400「单据已作废,无法提交」 |
+| `status` 缺失或非法 | 400「无效状态」 |
+| 作废确认取消 | 不发请求 |
+
+---
+
+## 7. 验收
+
+| # | 场景 | 期望 |
+|---|------|------|
+| 1 | 安排后首次打开现场链接 | 状态选择器为「进行中」 |
+| 2 | 选「已完成」提交 | 单据 `completed`;管理端列表一致;可再次进入继续改数量/状态 |
+| 3 | 选「进行中」提交(曾已完成) | 单据回到 `in_progress` |
+| 4 | 选「已作废」并确认提交 | 仅本单 `voided`;运输计划跟进状态不变 |
+| 5 | 作废后再打开 | 可查看历史回执与数量;不可提交/上传 |
+| 6 | 管理端作废计划后打开现场链接 | 只读(既有 `voided`) |
+| 7 | 回归 | 无 `status` 的旧客户端若仍存在会失败;本期仅本仓库公共页,同步改前端即可 |
+
+---
+
+## 8. 改动文件清单
+
+| 层 | 文件 |
+|----|------|
+| Schema | `backend/app/schemas/pl/io_order.py` |
+| Service | `backend/app/services/pl_io_order_service.py` |
+| API | `backend/app/api/pl/public_io_order.py` |
+| 前端 API | `web-admin/src/api/pl/public-io-order.ts` |
+| 公共页 | `web-admin/src/views/public/pl/_shared/PublicIoOrderPage.vue` |
+| 文档 | 本文件;实现后可在关联设计 D5 旁注「已被本设计覆盖」 |
+
+---
+
+## 9. 测试建议
+
+- 后端:对 `apply_site_feedback` 单测或手工:首次提交 `completed` / `voided` / `in_progress`;已 voided 再调应失败
+- 前端:入库与出库各走一遍上述验收表 1–5