2026-05-26-outbound-production-work-order.md 29 KB

出库生产工单 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: 在生产工单页面新增出库生产工单 tab,输注计划创建时自动创建出库工单,删除时自动作废,详情展示关联业务信息。

Architecture: 在现有生产工单 index.vue 中扩展第 4 个 tab(outbound),复用通用列表/详情 API。后端通过 work_order_type="outbound" 过滤,通过 infusion_schedule_id 反查关联链。自动创建/作废通过在 pilot_application.py 的 CRUD 函数中挂载钩子实现。

Tech Stack: Python 3.9 / FastAPI / SQLAlchemy / MySQL / Vue 3 / Element Plus / TypeScript

Design Spec: docs/superpowers/specs/2026-05-26-outbound-production-work-order-design.md


File Structure

操作 文件 职责
新增 docs/sql/2026-05-26-outbound-work-order.sql ALTER TABLE DDL
修改 backend/app/models/models.py (BizProductionWorkOrder 类) 新增 biz_type 字段
修改 backend/app/crud/production_work_order.py 状态映射、_wo_to_dict、create_outbound 函数签名、列表增强、详情增强
修改 backend/app/crud/pilot_application.py create_schedule 自动创建钩子、soft_delete_schedule 作废钩子
修改 web-admin/src/views/production/production-work-order/index.vue 新 tab、列配置、操作按钮、详情弹窗、状态映射

Task 1: 数据库 DDL 脚本

Files:

  • Create: docs/sql/2026-05-26-outbound-work-order.sql

  • [ ] Step 1: 创建 SQL 迁移脚本

-- 出库生产工单:新增 biz_type 字段 + 更新 comment
ALTER TABLE biz_production_work_order
  ADD COLUMN biz_type VARCHAR(20) NULL COMMENT '业务类型: pilot_service=中试服务, clinical_application=临床应用' AFTER infusion_schedule_id;

ALTER TABLE biz_production_work_order
  MODIFY COLUMN work_order_type VARCHAR(20) NOT NULL DEFAULT 'inbound' COMMENT '工单类型: inbound=入库, temporary=临时, outbound=出库';

ALTER TABLE biz_production_work_order
  MODIFY COLUMN work_order_status VARCHAR(16) NOT NULL DEFAULT 'dpz' COMMENT '工单状态 dpz待配置 jxz进行中 ywc已完成 yzf已作废';
  • Step 2: 在数据库中执行此脚本

Run: source venv/bin/activate && python -c "from app.core.database import engine; from sqlalchemy import text; conn = engine.connect(); [conn.execute(text(s)) for s in open('docs/sql/2026-05-26-outbound-work-order.sql').read().split(';') if s.strip() and not s.strip().startswith('--')]; conn.commit(); print('OK')"

或手动在 MySQL 客户端中执行。Expected: OK

  • Step 3: Commit
git add docs/sql/2026-05-26-outbound-work-order.sql
git commit -m "feat(production): add biz_type column and yzf status for outbound work orders"

Task 2: 后端模型层 — BizProductionWorkOrder 新增 biz_type

Files:

  • Modify: backend/app/models/models.py (BizProductionWorkOrder 类, 约 line 2430)

  • [ ] Step 1: 在 BizProductionWorkOrder 类中新增 biz_type 字段

在 infusion_schedule_id 字段之后新增:

biz_type = Column(String(20), nullable=True, comment="业务类型: pilot_service=中试服务, clinical_application=临床应用")

同时更新 work_order_type 字段的 comment:

work_order_type = Column(String(20), nullable=False, default="inbound", comment="工单类型: inbound=入库, temporary=临时, outbound=出库")

更新 work_order_status 字段的 comment:

work_order_status = Column(String(16), nullable=False, default="dpz", comment="工单状态 dpz待配置 jxz进行中 ywc已完成 yzf已作废")
  • Step 2: 验证模型无语法错误

Run: cd backend && source venv/bin/activate && python -c "from app.models.models import BizProductionWorkOrder; print('OK')"

Expected: OK

  • Step 3: Commit
git add backend/app/models/models.py
git commit -m "feat(production): add biz_type field to BizProductionWorkOrder model"

Task 3: 后端 CRUD 层 — production_work_order.py

Files:

  • Modify: backend/app/crud/production_work_order.py

  • [ ] Step 1: 扩展状态映射和来源标签

文件顶部 _WORK_ORDER_STATUS_LABELS (约 line 32-36) 修改为:

_WORK_ORDER_STATUS_LABELS = {
    "dpz": "待配置",
    "jxz": "进行中",
    "ywc": "已完成",
    "yzf": "已作废",
}
  • Step 2: _wo_to_dict 增加 bizType 字段

在 _wo_to_dict() 函数 (约 line 87-111) 中,"infusionScheduleId" 之后新增:

"bizType": getattr(wo, "biz_type", None) or None,
"bizTypeLabel": {"pilot_service": "中试服务", "clinical_application": "临床应用"}.get(getattr(wo, "biz_type", None) or "", ""),
  • Step 3: create_outbound_production_work_order 增加 biz_type 参数

函数签名 (约 line 638-646) 修改为:

def create_outbound_production_work_order(
    db: Session,
    *,
    schedule_id: str,
    tenant_no: str,
    user_id: Optional[str] = None,
    create_dept_id: str = "",
    remark: str = "",
    biz_type: str = "pilot_service",
) -> BizProductionWorkOrder:

在创建 BizProductionWorkOrder 对象时 (约 line 658-674) 新增字段:

wo = BizProductionWorkOrder(
    id=generate_uuid(),
    work_order_no=_generate_outbound_work_order_no(db, tenant_no),
    work_order_type="outbound",
    reception_id=None,
    protocol_sample_id=None,
    collection_protocol_id=None,
    protocol_no=None,
    sample_code=None,
    reception_source="outbound",
    infusion_schedule_id=schedule_id,
    biz_type=biz_type,
    work_order_status="dpz",
    tenant_no=tenant_no,
    remark=remark,
    create_by=user_id,
    create_dept_id=create_dept_id,
)
  • Step 4: 列表增强 — outbound 工单附加业务数据

在 _enrich_wo_items() 函数之后新增辅助函数:

_BIZ_TYPE_LABELS = {"pilot_service": "中试服务", "clinical_application": "临床应用"}


def _enrich_outbound_items(db: Session, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """为 outbound 类型工单附加申请单号、细胞类型、规格、输注时间等关联数据。"""
    from app.models.models import (
        BizPilotApplication,
        BizPilotInfusionPlan,
        BizPilotInfusionSchedule,
    )

    outbound_ids = [
        it["infusionScheduleId"]
        for it in items
        if it.get("workOrderType") == "outbound" and it.get("infusionScheduleId")
    ]
    if not outbound_ids:
        return items

    schedules = (
        db.query(BizPilotInfusionSchedule)
        .filter(
            BizPilotInfusionSchedule.id.in_(outbound_ids),
            BizPilotInfusionSchedule.del_flag == 0,
        )
        .all()
    )
    sched_map = {s.id: s for s in schedules}

    plan_ids = list({s.infusion_plan_id for s in schedules if s.infusion_plan_id})
    plan_map = {}
    if plan_ids:
        plans = db.query(BizPilotInfusionPlan).filter(BizPilotInfusionPlan.id.in_(plan_ids)).all()
        plan_map = {p.id: p for p in plans}

    app_ids = list({p.pilot_application_id for p in plan_map.values() if p.pilot_application_id})
    app_map = {}
    if app_ids:
        apps = db.query(BizPilotApplication).filter(BizPilotApplication.id.in_(app_ids)).all()
        app_map = {a.id: a for a in apps}

    for it in items:
        if it.get("workOrderType") != "outbound" or not it.get("infusionScheduleId"):
            continue
        sched = sched_map.get(it["infusionScheduleId"])
        if not sched:
            continue
        plan = plan_map.get(sched.infusion_plan_id) if sched.infusion_plan_id else None
        app = app_map.get(plan.pilot_application_id) if plan and plan.pilot_application_id else None

        it["applicationNo"] = app.application_no if app else ""
        it["cellType"] = sched.cell_type or ""
        it["specText"] = sched.spec_text or ""
        it["infusionTime"] = _fmt_dt(sched.infusion_time) if sched.infusion_time else ""
        it["infusionLocation"] = sched.infusion_location or ""

    return items

然后在 list_production_work_orders() (约 line 343) 的 return 之前调用:

将:

items = _enrich_wo_items(db, [_wo_to_dict(x) for x in rows])
return {"items": items, "total": int(total)}

改为:

items = _enrich_wo_items(db, [_wo_to_dict(x) for x in rows])
items = _enrich_outbound_items(db, items)
return {"items": items, "total": int(total)}
  • Step 5: 详情增强 — outbound 工单详情返回关联链

在 _wo_to_response_dict() 函数 (约 line 160-164) 中,对 outbound 类型附加关联信息:

将:

def _wo_to_response_dict(db: Session, wo: BizProductionWorkOrder) -> Dict[str, Any]:
    """工单响应(含接收记录关联的样本类型、接收人、接收时间及 reception 详情)。"""
    out = _enrich_wo_items(db, [_wo_to_dict(wo)])[0]
    _attach_reception_detail(db, wo, out, wo.tenant_no or "")
    return out

改为:

def _wo_to_response_dict(db: Session, wo: BizProductionWorkOrder) -> Dict[str, Any]:
    """工单响应(含接收记录关联的样本类型、接收人、接收时间及 reception 详情)。"""
    out = _enrich_wo_items(db, [_wo_to_dict(wo)])[0]
    if wo.work_order_type == "outbound" and wo.infusion_schedule_id:
        _attach_outbound_detail(db, wo, out)
    else:
        _attach_reception_detail(db, wo, out, wo.tenant_no or "")
    return out

新增 _attach_outbound_detail 函数:

def _attach_outbound_detail(
    db: Session, wo: BizProductionWorkOrder, out: Dict[str, Any]
) -> None:
    """outbound 工单详情附加申请单 + 输注计划信息。"""
    from app.models.models import (
        BizPilotApplication,
        BizPilotInfusionPlan,
        BizPilotInfusionSchedule,
    )

    _APP_STATUS_LABELS = {"dks": "待开始", "jxz": "进行中", "ywc": "已完成", "yzf": "已作废"}

    sched = (
        db.query(BizPilotInfusionSchedule)
        .filter(BizPilotInfusionSchedule.id == wo.infusion_schedule_id, BizPilotInfusionSchedule.del_flag == 0)
        .first()
    )
    plan = None
    app = None
    if sched and sched.infusion_plan_id:
        plan = db.query(BizPilotInfusionPlan).filter(BizPilotInfusionPlan.id == sched.infusion_plan_id).first()
    if plan and plan.pilot_application_id:
        app = db.query(BizPilotApplication).filter(BizPilotApplication.id == plan.pilot_application_id).first()

    out["reception"] = {}
    out["pilotApplication"] = {
        "applicationNo": app.application_no if app else "",
        "applicationName": app.application_name if app else "",
        "applicationStatus": app.application_status if app else "",
        "applicationStatusLabel": _APP_STATUS_LABELS.get(app.application_status, "") if app else "",
    } if app else {}
    out["infusionSchedule"] = {
        "cellType": sched.cell_type if sched else "",
        "specText": sched.spec_text if sched else "",
        "infusionTime": _fmt_dt(sched.infusion_time) if sched and sched.infusion_time else "",
        "infusionLocation": sched.infusion_location if sched else "",
        "infusionMethod": sched.infusion_method if sched else "",
        "scheduleStatus": sched.schedule_status if sched else "",
    } if sched else {}
  • Step 6: 验证无语法错误

Run: cd backend && source venv/bin/activate && python -c "from app.crud.production_work_order import list_production_work_orders, _wo_to_response_dict, create_outbound_production_work_order; print('OK')"

Expected: OK

  • Step 7: Commit
git add backend/app/crud/production_work_order.py
git commit -m "feat(production): extend CRUD for outbound work orders — biz_type, yzf status, list/detail enrichment"

Task 4: 后端钩子 — 自动创建出库工单

Files:

  • Modify: backend/app/crud/pilot_application.py (create_schedule 函数, 约 line 926)

  • [ ] Step 1: 在 create_schedule 中增加自动创建出库工单

在 create_schedule() 函数 (约 line 966) 的 if commit: 之前插入:

    # 自动创建出库生产工单
    from app.crud import production_work_order as pwo_svc
    pwo_svc.create_outbound_production_work_order(
        db,
        schedule_id=schedule_id,
        tenant_no=tenant_no,
        user_id=user_id,
        create_dept_id=dept_id,
        biz_type="pilot_service",
    )

注意位置:应在 _create_schedule_products 之后、if commit: 之前,确保 schedule_id 和 products 已写入但还未 commit(在同一事务内)。

  • Step 2: 验证无语法错误

Run: cd backend && source venv/bin/activate && python -c "from app.crud.pilot_application import create_schedule; print('OK')"

Expected: OK

  • Step 3: Commit
git add backend/app/crud/pilot_application.py
git commit -m "feat(pilot): auto-create outbound production work order on schedule creation"

Task 5: 后端钩子 — 自动作废出库工单

Files:

  • Modify: backend/app/crud/pilot_application.py (soft_delete_schedule 函数, 约 line 1064)

  • [ ] Step 1: 在 soft_delete_schedule 中增加作废逻辑

在 soft_delete_schedule() 函数中,row.del_flag = 1 (约 line 1077) 之前插入:

    # 关联的出库生产工单标记为已作废
    from app.models.models import BizProductionWorkOrder
    outbound_wo = (
        db.query(BizProductionWorkOrder)
        .filter(
            BizProductionWorkOrder.infusion_schedule_id == schedule_id,
            BizProductionWorkOrder.tenant_no == tenant_no,
            BizProductionWorkOrder.del_flag == 0,
            BizProductionWorkOrder.work_order_type == "outbound",
        )
        .first()
    )
    if outbound_wo:
        outbound_wo.work_order_status = "yzf"
        outbound_wo.update_time = datetime.now()
  • Step 2: 验证无语法错误

Run: cd backend && source venv/bin/activate && python -c "from app.crud.pilot_application import soft_delete_schedule; print('OK')"

Expected: OK

  • Step 3: Commit
git add backend/app/crud/pilot_application.py
git commit -m "feat(pilot): auto-void outbound production work order on schedule deletion"

Task 6: 前端 — 生产工单页面扩展 outbound tab

Files:

  • Modify: web-admin/src/views/production/production-work-order/index.vue

  • [ ] Step 1: 扩展 WorkOrderTypeTab 类型

约 line 432,修改为:

type WorkOrderTypeTab = 'inbound' | 'temporary' | 'outbound' | 'all'
  • Step 2: 新增 outbound tab 面板

约 line 15-19,在 全部生产工单 之前插入 outbound tab:

      <el-tabs v-model="activeTypeTab" class="work-order-type-tabs" @tab-change="handleTypeTabChange">
        <el-tab-pane label="入库生产工单" name="inbound" />
        <el-tab-pane label="临时生产工单" name="temporary" />
        <el-tab-pane label="出库生产工单" name="outbound" />
        <el-tab-pane label="全部生产工单" name="all" />
      </el-tabs>
  • Step 3: 更新搜索框 placeholder

约 line 27,修改 placeholder 为 computed:

将:

:placeholder="activeTypeTab === 'temporary' ? '工单号' : '工单号/协议号/样本编码'"

改为:

:placeholder="searchPlaceholder"

在 script setup 区域新增 computed:

const searchPlaceholder = computed(() => {
  if (activeTypeTab.value === 'temporary') return '工单号'
  if (activeTypeTab.value === 'outbound') return '工单号/申请单号'
  return '工单号/协议号/样本编码'
})
  • Step 4: 更新工具栏 — outbound 和 all tab 不显示新增按钮

约 line 69,修改:

将:

<div v-if="activeTypeTab !== 'all'" class="list-toolbar">

改为:

<div v-if="activeTypeTab !== 'all' && activeTypeTab !== 'outbound'" class="list-toolbar">
  • Step 5: 更新全部 tab 的工单类型 tag 映射

约 line 94-100,修改类型 tag:

将:

<el-table-column v-if="activeTypeTab === 'all'" prop="workOrderType" label="工单类型" min-width="100" align="center">
  <template #default="{ row }">
    <el-tag :type="row.workOrderType === 'temporary' ? 'warning' : 'success'" size="small">
      {{ row.workOrderType === 'temporary' ? '临时' : '入库' }}
    </el-tag>
  </template>
</el-table-column>

改为:

<el-table-column v-if="activeTypeTab === 'all'" prop="workOrderType" label="工单类型" min-width="100" align="center">
  <template #default="{ row }">
    <el-tag :type="woTypeTag(row.workOrderType).type" size="small">
      {{ woTypeTag(row.workOrderType).label }}
    </el-tag>
  </template>
</el-table-column>

在 script 中 sourceTag 函数之后新增:

const woTypeTag = (type: string) => {
  const map: Record<string, { label: string; type: string }> = {
    inbound: { label: '入库', type: 'success' },
    temporary: { label: '临时', type: 'warning' },
    outbound: { label: '出库', type: 'primary' },
  }
  return map[type] || { label: type, type: 'info' }
}
  • Step 6: 新增 outbound 专属列

在 全部工单类型 列之后、协议号 列之前(约 line 100-101 之间),插入 outbound 专属列:

          <!-- 出库工单专属列 -->
          <el-table-column v-if="activeTypeTab === 'outbound'" prop="bizTypeLabel" label="业务类型" min-width="110" align="center">
            <template #default="{ row }">
              <el-tag :type="row.bizType === 'pilot_service' ? 'primary' : 'success'" size="small">
                {{ row.bizTypeLabel || '—' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column v-if="activeTypeTab === 'outbound'" prop="applicationNo" label="申请单号" min-width="150" align="center">
            <template #default="{ row }">{{ row.applicationNo || '—' }}</template>
          </el-table-column>
          <el-table-column v-if="activeTypeTab === 'outbound'" prop="cellType" label="细胞类型" min-width="110" align="center">
            <template #default="{ row }">{{ row.cellType || '—' }}</template>
          </el-table-column>
          <el-table-column v-if="activeTypeTab === 'outbound'" prop="specText" label="规格" min-width="110" align="center">
            <template #default="{ row }">{{ row.specText || '—' }}</template>
          </el-table-column>
          <el-table-column v-if="activeTypeTab === 'outbound'" prop="infusionTime" label="输注时间" min-width="170" align="center">
            <template #default="{ row }">{{ row.infusionTime || '—' }}</template>
          </el-table-column>
  • Step 7: 更新 showReceptionCols 逻辑

约 line 434,修改:

将:

const showReceptionCols = computed(() => activeTypeTab.value !== 'temporary')

改为:

const showReceptionCols = computed(() => activeTypeTab.value !== 'temporary' && activeTypeTab.value !== 'outbound')
  • Step 8: 全部 tab 隐藏操作列

约 line 145-178 的操作列 <el-table-column> 添加 v-if:

将:

          <el-table-column
            label="操作"
            min-width="600"
            fixed="right"
            align="center"
            header-align="center"
          >

改为:

          <el-table-column
            v-if="activeTypeTab !== 'all'"
            label="操作"
            min-width="600"
            fixed="right"
            align="center"
            header-align="center"
          >
  • Step 9: 更新工单状态 Tag 映射

约 line 510-517,修改 woStatusTag:

将:

const woStatusTag = (status: string) => {
  const map: Record<string, { label: string; type: string }> = {
    dpz: { label: '待配置', type: 'info' },
    jxz: { label: '进行中', type: 'warning' },
    ywc: { label: '已完成', type: 'success' }
  }
  return map[status] || { label: status, type: 'info' }
}

改为:

const woStatusTag = (status: string) => {
  const map: Record<string, { label: string; type: string }> = {
    dpz: { label: '待配置', type: 'info' },
    jxz: { label: '进行中', type: 'warning' },
    ywc: { label: '已完成', type: 'success' },
    yzf: { label: '已作废', type: 'danger' }
  }
  return map[status] || { label: status, type: 'info' }
}
  • Step 10: 状态筛选下拉增加已作废选项

约 line 38-41,增加选项:

将:

                <el-option label="待配置" value="dpz" />
                <el-option label="进行中" value="jxz" />
                <el-option label="已完成" value="ywc" />

改为:

                <el-option label="待配置" value="dpz" />
                <el-option label="进行中" value="jxz" />
                <el-option label="已完成" value="ywc" />
                <el-option label="已作废" value="yzf" />
  • Step 11: Commit
git add web-admin/src/views/production/production-work-order/index.vue
git commit -m "feat(production): add outbound work order tab with columns, filters, and status mapping"

Task 7: 前端 — 详情弹窗增强

Files:

  • Modify: web-admin/src/views/production/production-work-order/index.vue (viewSections computed, 约 line 640)

  • [ ] Step 1: 更新 viewSections computed,增加 outbound 分支

约 line 640-733,替换整个 viewSections computed。

核心逻辑:

  • isOutbound = d.workOrderType === 'outbound'
  • outbound 基本信息:工单号、工单类型(出库/primary)、业务类型、工单状态、备注、创建时间
  • outbound 不显示协议号/协议状态/样本编码/来源类型/样本类型
  • outbound 新增"中试申请信息"section:申请单号、申请名称、申请状态
  • outbound 新增"输注计划信息"section:细胞类型、规格、输注时间、输注地点、输注方式
  • outbound 不显示运输信息、样本接收 section
  const viewSections = computed<ViewSection[]>(() => {
    if (!detail.value) return []
    const d = detail.value
    const isTemp = d.workOrderType === 'temporary'
    const isOutbound = d.workOrderType === 'outbound'
    const r = d.reception || {}

    // ── 基本信息 fields ──
    const baseFields: ViewSection['fields'] = [
      { label: '工单号', value: d.workOrderNo },
      {
        label: '工单类型',
        value: isTemp ? '临时' : isOutbound ? '出库' : '入库',
        tagType: isTemp ? 'warning' : isOutbound ? 'primary' : 'success'
      },
    ]

    // outbound 显示业务类型
    if (isOutbound) {
      baseFields.push({
        label: '业务类型',
        value: d.bizTypeLabel || (d.bizType === 'pilot_service' ? '中试服务' : d.bizType === 'clinical_application' ? '临床应用' : '—'),
        tagType: 'primary' as const
      })
    }

    // inbound 显示协议/样本字段
    if (!isTemp && !isOutbound) {
      baseFields.push(
        { label: '协议号', value: d.protocolNo },
        {
          label: '协议状态',
          value: protocolStatusLabelBySource(String(d.protocolStatus || ''), d.receptionSource || 'cell_storage'),
          tagType: protocolStatusTagTypeBySource(String(d.protocolStatus || ''), d.receptionSource || 'cell_storage')
        },
        { label: '样本编码', value: d.sampleCode },
        {
          label: '来源类型',
          value:
            r.sourceTypeLabel ||
            d.receptionSourceLabel ||
            receptionSourceLabel(d.receptionSource || ''),
          tagType: 'primary' as const
        },
        { label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' }
      )
    }

    baseFields.push(
      {
        label: '工单状态',
        value: woStatusTag(d.workOrderStatus).label,
        tagType: woStatusTag(d.workOrderStatus).type as any
      },
      { label: '备注', value: d.remark || '—', span: true },
      { label: '创建时间', value: d.createTime || '—' }
    )

    const sections: ViewSection[] = [
      { title: '基本信息', fields: baseFields }
    ]

    // outbound: 申请单 + 输注计划
    if (isOutbound) {
      const pa = d.pilotApplication || {}
      const sched = d.infusionSchedule || {}
      const appStatusLabel = (s: string) => {
        const m: Record<string, string> = { dks: '待开始', jxz: '进行中', ywc: '已完成', yzf: '已作废' }
        return m[s] || s || '—'
      }
      const appStatusTagType = (s: string) => {
        const m: Record<string, string> = { dks: 'info', jxz: 'warning', ywc: 'success', yzf: 'danger' }
        return m[s] || 'info'
      }
      sections.push({
        title: '中试申请信息',
        fields: [
          { label: '申请单号', value: pa.applicationNo || '—' },
          { label: '申请名称', value: pa.applicationName || '—' },
          {
            label: '申请状态',
            value: appStatusLabel(pa.applicationStatus),
            tagType: appStatusTagType(pa.applicationStatus) as any
          },
        ]
      })
      sections.push({
        title: '输注计划信息',
        fields: [
          { label: '细胞类型', value: sched.cellType || '—' },
          { label: '规格', value: sched.specText || '—' },
          { label: '输注时间', value: sched.infusionTime || '—' },
          { label: '输注地点', value: sched.infusionLocation || '—' },
          { label: '输注方式', value: sched.infusionMethod || '—' },
        ]
      })
      return sections
    }

    // inbound: 运输 + 样本接收
    const transportFields: ViewSection['fields'] = [
      { label: '运输方式', value: transportTypeLabel(String(r.transportType || '')) }
    ]
    if (r.transportType === 'kdys') {
      transportFields.push(
        { label: '快递公司', value: r.expressCompanyName || '—' },
        { label: '快递单号', value: r.waybillNo || '—' }
      )
    } else if (r.transportType === 'ryxd') {
      transportFields.push(
        { label: '携带人', value: r.carrierName || '—' },
        { label: '携带人电话', value: r.carrierPhone || '—' }
      )
    }
    if (r.etaTime) transportFields.push({ label: '预计到达', value: r.etaTime })
    const transportSection = transportFields.length > 1 || r.transportType ? [{ title: '运输信息', fields: transportFields }] : []

    return [
      { title: '基本信息', fields: baseFields },
      ...transportSection,
      ...(!isTemp ? [{
        title: '样本接收',
        slot: 'reception-extra',
        fields: [
          { label: '接收时间', value: r.receptionTime || d.receptionTime || '—' },
          { label: '接收经手人', value: r.handlerName || d.handlerName || '—' },
          { label: '接收温度(℃)', value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—' },
          { label: '体积(ml)', value: formatMetric(r.volumeMl) },
          { label: '净重(g)', value: formatMetric(r.netWeightG) },
          {
            label: '结论',
            value: r.acceptConclusionLabel || acceptConclusionLabel(String(r.acceptConclusion || '')),
            tagType: r.acceptConclusion === 'js' ? 'success' : r.acceptConclusion === 'jj' ? 'danger' : 'info'
          },
          { label: '检查项', value: formatCheckResults(r.checkResults), span: true },
          { label: '备注', value: r.remark || '—', span: true }
        ]
      }] : [])
    ]
  })
  • Step 2: 验证前端无编译错误

Run: cd web-admin && pnpm build 2>&1 | tail -5

Expected: Build 成功(可能有无关的 type warning,但不应有 error)

  • Step 3: Commit
git add web-admin/src/views/production/production-work-order/index.vue
git commit -m "feat(production): add outbound work order detail dialog with pilot application and infusion schedule sections"

Task 8: 端到端验证

Files: 无文件修改

  • Step 1: 启动后端,确认无报错

Run: cd backend && source venv/bin/activate && python -c "from app.main import app; print('App OK')"

Expected: App OK

  • Step 2: 启动前端,确认无编译错误

Run: cd web-admin && pnpm build 2>&1 | tail -5

Expected: Build 成功

  • Step 3: 功能验证清单(手动)
  1. 打开生产工单页面,确认 4 个 tab 都显示
  2. 点击「出库生产工单」tab,确认列表加载(无数据或已有数据)
  3. 确认出库 tab 不显示新增按钮
  4. 确认出库 tab 显示专属列(业务类型、申请单号、细胞类型、规格、输注时间)
  5. 点击「全部生产工单」tab,确认操作列已隐藏
  6. 创建一条输注计划,切换到出库 tab 确认自动生成了工单
  7. 删除该输注计划,确认出库工单状态变为「已作废」
  8. 点击出库工单的查看详情,确认显示申请单信息和输注计划信息
  9. 确认编辑任务、派发任务、完成工单按钮可正常使用
  • Step 4: Final Commit (如有遗漏修复)
git add -A
git commit -m "fix(production): final adjustments for outbound work order feature"