2026-05-26-outbound-qc-work-order.md 30 KB

出库质检工单功能实施计划

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: 前端在 qc-work-order/index.vue 中新增第 4 个 Tab outbound,复用列表骨架、配置检测、编辑备注逻辑,新建独立查看对话框组件 OutboundQcViewDialog.vue。后端新增聚合查询 API,扩展现有列表 API 返回字段,并在输注计划取消时自动作废关联质检工单。

Tech Stack: Vue 3 + Element Plus (前端) / FastAPI + SQLAlchemy + Pydantic v2 (后端) / MySQL 5.7

设计文档: docs/superpowers/specs/2026-05-26-outbound-qc-work-order-design.md


Task 1: 数据库 — 新增 biz_type 字段

Files:

  • Create: docs/sql/2026-05-26-qc-work-order-add-biz-type.sql
  • Modify: backend/app/models/models.py:1239 (在 infusion_schedule_id 后新增字段)

  • [ ] Step 1: 创建 SQL 迁移文件

-- 出库质检工单:新增 biz_type 字段
ALTER TABLE biz_qc_work_order
  ADD COLUMN biz_type VARCHAR(20) NULL COMMENT '业务类型: pilot_service=中试服务, clinical_application=临床应用' AFTER infusion_schedule_id;
  • Step 2: 在 SQLAlchemy 模型中新增 biz_type 字段

在 backend/app/models/models.py 的 BizQcWorkOrder 类中,在 infusion_schedule_id 字段之后添加:

    biz_type = Column(String(20), nullable=True, comment="业务类型: pilot_service=中试服务, clinical_application=临床应用")
  • Step 3: 在数据库执行 SQL

在 MySQL 客户端中执行迁移 SQL。

  • Step 4: Commit
git add docs/sql/2026-05-26-qc-work-order-add-biz-type.sql backend/app/models/models.py
git commit -m "feat(qc): add biz_type column to qc_work_order table for outbound business type"

Task 2: 后端 — 扩展列表 API 和 _row_to_dict

Files:

  • Modify: backend/app/crud/qc_work_order.py (扩展列表查询和 _row_to_dict)
  • Modify: backend/app/schemas/qc_work_order.py (无改动,schema 跟随 _row_to_dict)

参考文件(只读):

  • backend/app/crud/qc_work_order.py — 搜索 _row_to_dict 函数和 list_work_orders 函数
  • backend/app/models/models.py — 搜索 BizPilotInfusionSchedule、BizPilotInfusionPlan、BizPilotApplication 模型

  • [ ] Step 1: 修改 list_work_orders() 函数,支持 outbound 类型的 JOIN 查询和 bizType 筛选参数

在 backend/app/crud/qc_work_order.py 中:

  1. 找到 list_work_orders() 函数(查询质检工单列表)
  2. 新增可选参数 biz_type: Optional[str] = None
  3. 当 work_order_type == 'outbound' 时,LEFT JOIN 以下链路获取关联数据:
    • biz_qc_work_order.infusion_schedule_id → biz_pilot_infusion_schedule
    • biz_pilot_infusion_schedule.infusion_plan_id → biz_pilot_infusion_plan
    • biz_pilot_infusion_plan.pilot_application_id → biz_pilot_application
  4. 添加 WHERE 条件 biz_type 筛选(当参数非空时)
  • Step 2: 扩展 _row_to_dict() 函数,为 outbound 类型返回额外字段

在 _row_to_dict() 函数中,当工单的 work_order_type == 'outbound' 时,额外返回:

# outbound 工单额外字段
'bizType': row.biz_type,                                    # 业务类型
'bizTypeLabel': '中试服务' if row.biz_type == 'pilot_service' else ('临床应用' if row.biz_type == 'clinical_application' else ''),
'applicationNo': application_no if outbound else None,      # 申请单号
'applicationName': application_name if outbound else None,  # 申请单名称
'channelName': channel_name if outbound else None,           # 渠道名称
'infusionTime': infusion_time if outbound else None,         # 输注时间

这些字段从 JOIN 查询结果中获取。

  • Step 3: 修改列表 API 路由,接收 bizType 查询参数

在 backend/app/api/qc.py 的工单列表端点中,从 request.args 或 Pydantic 查询模型中接收 bizType 参数,传递给 list_work_orders()。

  • Step 4: 修改 create_outbound_qc_work_order() 写入 biz_type

在 create_outbound_qc_work_order() 函数中,创建工单时设置 biz_type = 'pilot_service'(当前场景均为中试服务)。

  • Step 5: Commit
git add backend/app/crud/qc_work_order.py backend/app/api/qc.py
git commit -m "feat(qc): extend outbound work order list API with application/infusion fields and bizType filter"

Task 3: 后端 — 新增出库质检查看聚合 API

Files:

  • Modify: backend/app/api/qc.py (新增路由)
  • Modify: backend/app/crud/qc_work_order.py (新增查询函数)
  • Modify: backend/app/schemas/qc_work_order.py (新增返回 Schema)

参考文件(只读):

  • backend/app/crud/qc_work_order.py — 搜索 get_work_order_detail 了解现有详情查询模式
  • backend/app/crud/pilot_application.py — 搜索申请单/方案/计划的查询模式
  • backend/app/models/models.py — 搜索 BizPilotPatient、BizPilotInfusionScheduleProduct 模型

  • [ ] Step 1: 在 schemas/qc_work_order.py 中新增出库详情返回 Schema

class OutboundQcDetailApplication(BaseModel):
    """出库质检查看 - 申请单信息"""
    model_config = ConfigDict(populate_by_name=True)
    application_no: Optional[str] = Field(None, alias="applicationNo")
    application_name: Optional[str] = Field(None, alias="applicationName")
    application_status: Optional[str] = Field(None, alias="applicationStatus")
    channel_name: Optional[str] = Field(None, alias="channelName")


class OutboundQcDetailPatient(BaseModel):
    """出库质检查看 - 患者信息"""
    model_config = ConfigDict(populate_by_name=True)
    patient_name: Optional[str] = Field(None, alias="patientName")
    sex: Optional[str] = Field(None)
    age: Optional[str] = Field(None)
    phone_no: Optional[str] = Field(None, alias="phoneNo")
    id_card_no: Optional[str] = Field(None, alias="idCardNo")
    emergency_contact: Optional[str] = Field(None, alias="emergencyContact")
    emergency_phone: Optional[str] = Field(None, alias="emergencyPhone")
    clinical_diagnosis: Optional[str] = Field(None, alias="clinicalDiagnosis")
    diagnosis_attachments: Optional[str] = Field(None, alias="diagnosisAttachments")


class OutboundQcDetailInfusionPlan(BaseModel):
    """出库质检查看 - 输注方案信息"""
    model_config = ConfigDict(populate_by_name=True)
    cell_type: Optional[str] = Field(None, alias="cellType")
    spec_text: Optional[str] = Field(None, alias="specText")
    total_course_count: Optional[int] = Field(None, alias="totalCourseCount")
    course_interval: Optional[str] = Field(None, alias="courseInterval")
    infusion_route: Optional[str] = Field(None, alias="infusionRoute")
    infusion_start_date: Optional[str] = Field(None, alias="infusionStartDate")


class OutboundQcDetailInfusionSchedule(BaseModel):
    """出库质检查看 - 输注计划信息"""
    model_config = ConfigDict(populate_by_name=True)
    infusion_time: Optional[str] = Field(None, alias="infusionTime")
    infusion_location: Optional[str] = Field(None, alias="infusionLocation")
    infusion_method: Optional[str] = Field(None, alias="infusionMethod")
    schedule_status: Optional[str] = Field(None, alias="scheduleStatus")
    infusion_status: Optional[str] = Field(None, alias="infusionStatus")
    qc_status: Optional[str] = Field(None, alias="qcStatus")
    cancel_reason: Optional[str] = Field(None, alias="cancelReason")


class OutboundQcDetailProduct(BaseModel):
    """出库质检查看 - 产品明细"""
    model_config = ConfigDict(populate_by_name=True)
    project_code: Optional[str] = Field(None, alias="projectCode")
    project_name: Optional[str] = Field(None, alias="projectName")
    cell_type_name: Optional[str] = Field(None, alias="cellTypeName")
    spec_text: Optional[str] = Field(None, alias="specText")
    agreement_price: Optional[float] = Field(None, alias="agreementPrice")
    actual_price: Optional[float] = Field(None, alias="actualPrice")
    quantity: Optional[int] = Field(None)
    product_status: Optional[str] = Field(None, alias="productStatus")


class OutboundQcDetailResponse(BaseModel):
    """出库质检聚合详情"""
    model_config = ConfigDict(populate_by_name=True)
    work_order: Optional[dict] = Field(None, alias="workOrder")
    application: Optional[OutboundQcDetailApplication] = Field(None, alias="application")
    patient: Optional[OutboundQcDetailPatient] = Field(None, alias="patient")
    infusion_plan: Optional[OutboundQcDetailInfusionPlan] = Field(None, alias="infusionPlan")
    infusion_schedule: Optional[OutboundQcDetailInfusionSchedule] = Field(None, alias="infusionSchedule")
    products: Optional[list[OutboundQcDetailProduct]] = Field(None, alias="products")
    tasks: Optional[list[dict]] = Field(None, alias="tasks")
  • Step 2: 在 crud/qc_work_order.py 中新增 get_outbound_qc_detail() 函数
def get_outbound_qc_detail(db: Session, work_order_id: str, tenant_no: str) -> dict:
    """获取出库质检工单聚合详情(含申请单、患者、方案、计划、产品、任务)"""
    # 1. 获取工单基本信息(复用现有 get_work_order_detail 或直接查询)
    # 2. 通过 infusion_schedule_id JOIN 链路获取:
    #    - biz_pilot_infusion_schedule
    #    - biz_pilot_infusion_plan → biz_pilot_application → biz_pilot_patient
    #    - biz_pilot_infusion_schedule_product
    # 3. 获取关联的质检任务(复用现有任务查询逻辑)
    # 4. 组装返回 dict

实现时参考现有 get_work_order_detail() 的模式,通过 SQLAlchemy session 查询并组装字典返回。

  • Step 3: 在 api/qc.py 中新增路由
@router.get("/qc-work-orders/{work_order_id}/outbound-detail")
def api_get_outbound_qc_detail(work_order_id: str, db: Session = Depends(get_db), current_user=Depends(require_auth)):
    """获取出库质检工单聚合详情"""
    tenant_no = current_user["tenant_no"]
    result = get_outbound_qc_detail(db, work_order_id, tenant_no)
    return success(result)
  • Step 4: 验证 API 可用

启动后端服务,访问 /docs 确认新端点出现在 Swagger 文档中。

  • Step 5: Commit
git add backend/app/schemas/qc_work_order.py backend/app/crud/qc_work_order.py backend/app/api/qc.py
git commit -m "feat(qc): add outbound QC work order aggregated detail API endpoint"

Task 4: 后端 — 输注计划取消时自动作废关联质检工单

Files:

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

参考文件(只读):

  • backend/app/crud/pilot_infusion_schedule_mgmt.py — 搜索状态更新逻辑、update_status、取消相关代码
  • backend/app/models/models.py — 搜索 BizQcWorkOrder 模型

  • [ ] Step 1: 找到输注计划取消/删除的状态更新入口

在 pilot_infusion_schedule_mgmt.py 中搜索输注计划状态变更为 yqx 的逻辑入口(通常在 update_status 或 cancel_schedule 函数中)。

  • Step 2: 在取消逻辑中新增自动作废关联质检工单的代码

在状态变更为 yqx 的逻辑之后,新增:

from app.models.models import BizQcWorkOrder

# 当输注计划被取消时,自动作废关联的未完成出库质检工单
pending_qc_orders = db.query(BizQcWorkOrder).filter(
    BizQcWorkOrder.infusion_schedule_id == schedule_id,
    BizQcWorkOrder.work_order_status.notin_(['ywc', 'yzf']),
    BizQcWorkOrder.del_flag == 0
).all()
for order in pending_qc_orders:
    order.work_order_status = 'yzf'
  • Step 3: Commit
git add backend/app/crud/pilot_infusion_schedule_mgmt.py
git commit -m "feat(infusion): auto-void outbound QC work orders when infusion schedule is cancelled"

Task 5: 前端 — 新增 API 函数和类型

Files:

  • Modify: web-admin/src/api/qc-work-order.ts (新增类型和 API 函数)

  • [ ] Step 1: 在 QcWorkOrder 类型中新增 outbound 相关字段

在 QcWorkOrder 类型定义中新增:

  bizType?: string
  bizTypeLabel?: string
  applicationNo?: string
  applicationName?: string
  channelName?: string
  infusionTime?: string

同时,fetchQcWorkOrderList 的参数类型新增 bizType?: string:

export function fetchQcWorkOrderList(params?: {
  page?: number
  pageSize?: number
  keyword?: string
  workOrderStatus?: string
  workOrderType?: string
  bizType?: string
  createTimeStart?: string
  createTimeEnd?: string
}) {
  • Step 2: 新增出库质检查看聚合 API 类型定义和调用函数
export type OutboundQcDetailApplication = {
  applicationNo?: string
  applicationName?: string
  applicationStatus?: string
  channelName?: string
}

export type OutboundQcDetailPatient = {
  patientName?: string
  sex?: string
  age?: string
  phoneNo?: string
  idCardNo?: string
  emergencyContact?: string
  emergencyPhone?: string
  clinicalDiagnosis?: string
  diagnosisAttachments?: string
}

export type OutboundQcDetailInfusionPlan = {
  cellType?: string
  specText?: string
  totalCourseCount?: number
  courseInterval?: string
  infusionRoute?: string
  infusionStartDate?: string
}

export type OutboundQcDetailInfusionSchedule = {
  infusionTime?: string
  infusionLocation?: string
  infusionMethod?: string
  scheduleStatus?: string
  infusionStatus?: string
  qcStatus?: string
  cancelReason?: string
}

export type OutboundQcDetailProduct = {
  projectCode?: string
  projectName?: string
  cellTypeName?: string
  specText?: string
  agreementPrice?: number
  actualPrice?: number
  quantity?: number
  productStatus?: string
}

export type OutboundQcDetail = {
  workOrder: QcWorkOrderDetail
  application?: OutboundQcDetailApplication
  patient?: OutboundQcDetailPatient
  infusionPlan?: OutboundQcDetailInfusionPlan
  infusionSchedule?: OutboundQcDetailInfusionSchedule
  products?: OutboundQcDetailProduct[]
  tasks?: QcWorkOrderDetail['tasks']
}

export function fetchOutboundQcDetail(id: string) {
  return request.get<OutboundQcDetail>({
    url: `/web/admin/qc-work-orders/${id}/outbound-detail`
  })
}
  • Step 3: Commit
git add web-admin/src/api/qc-work-order.ts
git commit -m "feat(qc): add outbound QC detail API types and fetch function"

Task 6: 前端 — 新增出库质检 Tab 和列表

Files:

  • Modify: web-admin/src/views/quality-inspection/qc-work-order/index.vue

  • [ ] Step 1: 扩展 Tab 类型定义和 el-tabs

在 <script setup> 中修改 WorkOrderTypeTab 类型:

type WorkOrderTypeTab = 'inbound' | 'temporary' | 'outbound' | 'all'

在模板的 <el-tabs> 中,在"临时质检工单"和"全部质检工单"之间新增:

<el-tab-pane label="出库质检工单" name="outbound" />
  • Step 2: 新增出库列的 computed 控制

在 <script setup> 中新增:

const showOutboundCols = computed(() => activeTypeTab.value === 'outbound')
  • Step 3: 修改筛选区域 — 出库 Tab 显示业务类型筛选

在筛选区域的 <div class="filter-grid"> 中,关键词输入框的 placeholder 需要根据 Tab 调整:

<el-input
  v-model="searchForm.keyword"
  :placeholder="
    activeTypeTab === 'temporary'
      ? '工单号'
      : activeTypeTab === 'outbound'
        ? '工单号/申请单号/申请单名称'
        : '工单号/协议号/样本编码'
  "
  clearable
/>

在工单状态 el-select 之后,新增业务类型筛选(仅 outbound Tab 显示):

<el-form-item v-if="activeTypeTab === 'outbound'" label="业务类型">
  <el-select
    v-model="searchForm.bizType"
    placeholder="全部"
    clearable
    style="width: 100%"
  >
    <el-option label="中试服务" value="pilot_service" />
    <el-option label="临床应用" value="clinical_application" />
  </el-select>
</el-form-item>

同时在 searchForm reactive 中新增 bizType: ''。

  • Step 4: 修改工具栏 — 出库 Tab 不显示新增按钮

在 <div v-if="activeTypeTab !== 'all'" class="list-toolbar"> 中,新增 outbound 的排除:

<div v-if="activeTypeTab !== 'all' && activeTypeTab !== 'outbound'" class="list-toolbar">
  • Step 5: 新增出库质检专属表格列

在表格中,在"工单号"列之后、"业务类型"列之前(或复用业务类型列位置),新增出库专属列。在 <el-table-column prop="workOrderNo"...> 后面添加:

<!-- 出库质检专属列 -->
<el-table-column
  v-if="showOutboundCols"
  prop="bizTypeLabel"
  label="业务类型"
  min-width="100"
  align="center"
>
  <template #default="{ row }">
    <el-tag
      :type="row.bizType === 'pilot_service' ? 'primary' : 'warning'"
      size="small"
    >
      {{ row.bizTypeLabel || (row.bizType === 'pilot_service' ? '中试服务' : '临床应用') }}
    </el-tag>
  </template>
</el-table-column>
<el-table-column
  v-if="showOutboundCols"
  prop="applicationNo"
  label="申请单号"
  min-width="160"
  align="center"
  show-overflow-tooltip
/>
<el-table-column
  v-if="showOutboundCols"
  prop="applicationName"
  label="申请单名称"
  min-width="200"
  align="center"
  show-overflow-tooltip
/>
<el-table-column
  v-if="showOutboundCols"
  prop="channelName"
  label="渠道名称"
  min-width="140"
  align="center"
  show-overflow-tooltip
/>
<el-table-column
  v-if="showOutboundCols"
  prop="infusionTime"
  label="输注时间"
  min-width="160"
  align="center"
/>
  • Step 6: 修改状态列支持 yzf(已作废)

在状态列的 el-tag 中,tagType 计算需新增 yzf 分支:

<el-tag
  :type="
    row.workOrderStatus === 'ywc'
      ? 'success'
      : row.workOrderStatus === 'ypz'
        ? 'warning'
        : row.workOrderStatus === 'yzf'
          ? 'danger'
          : 'info'
  "
  size="small"
>

在 workOrderStatusLabel 映射函数中确认已支持 yzf → 已作废(需检查 utils/qc-labels.ts 或 utils/status-tag-registry.ts,如缺失则新增)。

  • Step 7: 修改操作列 — 出库 Tab 按钮逻辑

在操作列的 <template #default="{ row }"> 中:

查看按钮 — 所有 Tab 都显示(保持不变)

编辑按钮 — 出库 Tab 显示"编辑备注"按钮(与入库的编辑备注行为一致):

<!-- 出库质检编辑备注 -->
<el-button
  v-if="activeTypeTab === 'outbound' && row.workOrderStatus !== 'ywc' && row.workOrderStatus !== 'yzf'"
  class="btn-op-item"
  @click="handleEditRemark(row)"
>
  <el-icon><EditPen /></el-icon>编辑
</el-button>

配置检测按钮 — 保持现有逻辑(dpz/ypz 时显示),对 outbound 同样适用

删除按钮 — outbound Tab 不显示。将删除按钮的条件修改为:

<el-button
  v-if="activeTypeTab !== 'outbound' && activeTypeTab !== 'all' && row.workOrderStatus !== 'ywc'"
  class="btn-op-item"
  @click="handleDelete(row)"
>删除</el-button>
  • Step 8: 修改 loadData() 传递 bizType 参数

在 loadData() 函数中,调用 fetchQcWorkOrderList 时新增 bizType 参数:

const res = await fetchQcWorkOrderList({
  page: pagination.page,
  pageSize: pagination.pageSize,
  keyword: searchForm.keyword || undefined,
  workOrderStatus: searchForm.workOrderStatus || undefined,
  workOrderType: activeTypeTab.value === 'all' ? undefined : activeTypeTab.value,
  bizType: searchForm.bizType || undefined,
  createTimeStart: createTimeRange.value?.[0] || undefined,
  createTimeEnd: createTimeRange.value?.[1] || undefined
})
  • Step 9: Commit
git add web-admin/src/views/quality-inspection/qc-work-order/index.vue
git commit -m "feat(qc): add outbound QC work order tab with list, filters and action buttons"

Task 7: 前端 — 新建 OutboundQcViewDialog.vue 查看对话框

Files:

  • Create: web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue
  • Modify: web-admin/src/views/quality-inspection/qc-work-order/index.vue (引入并使用新组件)

参考文件(只读,必须先读取理解模式):

  • web-admin/src/components/business/system-view-dialog/index.vue — SystemViewDialog 组件接口
  • web-admin/src/views/quality-inspection/qc-work-order/index.vue:425-460 — 现有查看对话框的使用方式
  • web-admin/src/views/quality-inspection/qc-work-order/index.vue:964-1020 — buildTaskTypeSections() 函数
  • web-admin/src/components/business/protocol-attachment/ProtocolAttachmentReadonly.vue — 附件预览组件
  • web-admin/src/utils/status-tag-registry.ts — 状态标签映射函数

  • [ ] Step 1: 创建 OutboundQcViewDialog.vue 基础骨架

<!-- 出库质检工单查看对话框 -->
<template>
  <SystemViewDialog
    v-model:visible="dialogVisible"
    title="出库质检工单详情"
    width="1200px"
    :grid-columns="4"
    compact
    :loading="loading"
    :icon="Document"
    :meta="detail?.workOrder?.workOrderNo || ''"
    :sections="sections"
  >
    <!-- 诊断附件 slot -->
    <template #diagnosis-attachments>
      <ProtocolAttachmentReadonly
        :files="diagnosisAttachmentFiles"
        @preview="handlePreview"
        @download="handleDownload"
      />
    </template>
    <!-- 任务附件 slots -->
    <template v-for="task in tasksWithAttachments" #[taskAttachmentSlotName(task.id)]>
      <div :key="task.id" class="outbound-qc-task-attachments">
        <ProtocolAttachmentReadonly
          :files="task.attachmentFiles || []"
          @preview="handleTaskPreview"
          @download="handleTaskDownload"
        />
      </div>
    </template>
  </SystemViewDialog>
</template>

<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Document } from '@element-plus/icons-vue'
import { fetchOutboundQcDetail, type OutboundQcDetail } from '@/api/qc-work-order'
import { fetchQcTaskResults, type QcTaskResultItem } from '@/api/qc-task-result'
import {
  workOrderStatusLabel,
  taskStatusLabel,
  qcTaskConclusionLabel
} from '@/utils/qc-labels'
import {
  scheduleStatusLabel,
  scheduleStatusTagType,
  infusionStatusLabel,
  infusionStatusTagType,
  qcStatusLabel,
  qcStatusTagType,
  applicationStatusLabel,
  applicationStatusTagType,
  bizTypeLabel,
  bizTypeTagType
} from '@/utils/status-tag-registry'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { formatMoneyDisplay } from '@/utils/money-format'

const props = defineProps<{
  visible: boolean
  workOrderId: string
}>()

const emit = defineEmits<{
  (e: 'update:visible', v: boolean): void
}>()

const dialogVisible = computed({
  get: () => props.visible,
  set: (v) => emit('update:visible', v)
})

const loading = ref(false)
const detail = ref<OutboundQcDetail | null>(null)
type TaskWithResults = { id: string; taskNo: string; taskType: string; itemCode?: string; itemName?: string; taskStatus: string; conclusion?: string; conclusionLabel?: string; remark?: string; completedTime?: string; attachmentFiles?: Array<{ id?: string; fileUrl?: string; fileName?: string }>; results?: QcTaskResultItem[]; companyName?: string; feeAmount?: number }
const tasksWithResults = ref<TaskWithResults[]>([])

// ... 后续步骤中补充 sections 计算逻辑和附件处理
  • Step 2: 实现 sections computed 属性

在 <script setup> 中实现完整的 sections computed,包含 7 个 Section 组:

  1. 工单基本信息 — 工单号、工单类型(出库)、业务类型(el-tag)、状态(el-tag)、检测方式、配置时间、配置人、备注、创建时间
  2. 中试服务申请单信息 — 申请单号、名称、状态(el-tag)、渠道名称
  3. 患者信息 — 姓名、性别、年龄、电话、证件号、紧急联系人、紧急电话、临床诊断(span)、诊断附件(slot)
  4. 输注方案信息 — 细胞类型、规格、总疗程数、疗程间隔、输注途径、输注开始日期
  5. 本次输注计划信息 — 输注时间、地点、方式、生产状态(el-tag)、输注状态(el-tag)、质检状态(el-tag)、取消原因(仅已取消)
  6. 产品明细 — 内嵌表格展示 products 数组(项目编码、名称、细胞类型、规格、协议单价、实际单价、份数、产品状态)
  7. 检测结果 — 动态 Section,复用 buildTaskTypeSections 的逻辑生成实验室和第三方项目的详情

注意:状态标签映射函数需确认 status-tag-registry.ts 中是否存在。如 scheduleStatusLabel、infusionStatusLabel、qcStatusLabel、applicationStatusLabel、bizTypeLabel 等不存在,需在该文件中新增。

  • Step 3: 实现 handleView 数据加载
watch(() => props.visible, async (v) => {
  if (!v || !props.workOrderId) return
  loading.value = true
  tasksWithResults.value = []
  try {
    const d = await fetchOutboundQcDetail(props.workOrderId)
    detail.value = d
    // 加载任务结果
    const rawTasks = (d.workOrder?.tasks || []) as TaskWithResults[]
    const enriched = await Promise.all(
      rawTasks.map(async (task) => {
        if (task.taskType !== 'sysz' || task.taskStatus !== 'ywc') {
          return { ...task, results: [] as QcTaskResultItem[] }
        }
        try {
          const results = await fetchQcTaskResults(task.id)
          return { ...task, results: results || [] }
        } catch {
          return { ...task, results: [] as QcTaskResultItem[] }
        }
      })
    )
    tasksWithResults.value = enriched
  } finally {
    loading.value = false
  }
})
  • Step 4: 补充附件处理逻辑

实现 diagnosisAttachmentFiles computed(解析 patient.diagnosisAttachments 为文件数组)、handlePreview、handleDownload、handleTaskPreview、handleTaskDownload、taskAttachmentSlotName 等函数。参照现有 index.vue 中同名函数的模式。

  • Step 5: Commit
git add web-admin/src/views/quality-inspection/qc-work-order/OutboundQcViewDialog.vue
git commit -m "feat(qc): create OutboundQcViewDialog component with full detail sections"

Task 8: 前端 — 在 index.vue 中集成 OutboundQcViewDialog

Files:

  • Modify: web-admin/src/views/quality-inspection/qc-work-order/index.vue

  • [ ] Step 1: 导入 OutboundQcViewDialog 组件

在 <script setup> 的 import 区域添加:

import OutboundQcViewDialog from './OutboundQcViewDialog.vue'
  • Step 2: 新增出库查看对话框的状态变量
const outboundViewVisible = ref(false)
const outboundViewId = ref('')
  • Step 3: 新增出库查看处理函数
function handleOutboundView(row: QcWorkOrder) {
  outboundViewId.value = row.id
  outboundViewVisible.value = true
}
  • Step 4: 修改 handleView 函数区分出库类型

修改现有的 handleView 函数,当 activeTypeTab === 'outbound' 时调用 handleOutboundView:

async function handleView(row: QcWorkOrder) {
  if (activeTypeTab.value === 'outbound') {
    handleOutboundView(row)
    return
  }
  // ... 原有逻辑不变
}
  • Step 5: 在模板中放置 OutboundQcViewDialog

在 </section> 结束标签之前(与 WorkOrderRemarkDialog 同级)添加:

<OutboundQcViewDialog
  v-model:visible="outboundViewVisible"
  :work-order-id="outboundViewId"
/>
  • Step 6: Commit
git add web-admin/src/views/quality-inspection/qc-work-order/index.vue
git commit -m "feat(qc): integrate OutboundQcViewDialog into QC work order page"

Task 9: 前端 — 补充状态标签映射

Files:

  • Modify: web-admin/src/utils/status-tag-registry.ts (如需新增标签函数)

  • [ ] Step 1: 检查 status-tag-registry.ts 中是否已有以下映射函数

需要确认的函数:

  • scheduleStatusLabel / scheduleStatusTagType — 输注计划生产状态 (dsc/ysc/yjf/yqx)
  • infusionStatusLabel / infusionStatusTagType — 输注状态 (dtz/yrqr/yqx/ywc)
  • qcStatusLabel / qcStatusTagType — 质检状态 (dzc/yzj/yqx)
  • applicationStatusLabel / applicationStatusTagType — 申请单状态 (dks/jxz/ywc/yzf)
  • bizTypeLabel / bizTypeTagType — 业务类型 (pilot_service/clinical_application)
  • productStatusLabel / productStatusTagType — 产品状态 (dtz/yrqr/yqx/ywc)

如果已存在,跳过此 Task。如果不存在,新增对应的映射函数。

  • Step 2: 新增缺失的标签映射函数

遵循文件中已有的映射函数模式(label 函数返回中文名,tagType 函数返回 TagType),为每个缺失的状态类型新增。

  • Step 3: Commit
git add web-admin/src/utils/status-tag-registry.ts
git commit -m "feat(qc): add missing status label/tag mappings for outbound QC view dialog"

Task 10: 验证与修复

  • Step 1: 运行前端 lint 检查
cd web-admin && pnpm lint

修复所有 lint 错误。

  • Step 2: 运行前端 Prettier 格式化
cd web-admin && pnpm lint:prettier
  • Step 3: 运行前端类型检查
cd web-admin && pnpm build:typecheck

确保无类型错误。

  • Step 4: 前端构建验证
cd web-admin && pnpm build

确保构建成功。

  • Step 5: 后端 LSP 诊断

对修改过的后端文件运行 lsp_diagnostics 确认无错误。

  • Step 6: 端到端手动验证
  1. 访问质检工单页面,确认 4 个 Tab 正常切换
  2. 在出库质检 Tab 确认列表加载、筛选、分页正常
  3. 确认查看对话框显示完整的申请单+输注计划+患者+产品信息
  4. 确认编辑备注功能正常
  5. 确认配置检测功能正常
  6. 确认删除按钮不显示、新增按钮不显示
  7. 在输注计划页面取消一个计划,返回出库质检 Tab 确认关联工单状态变为"已作废"