Просмотр исходного кода

feat(production): add outbound work order tab, columns, and detail dialog with pilot application and infusion schedule sections

zlwu 5 месяцев назад
Родитель
Сommit
6bdcd0a565
1 измененных файлов с 322 добавлено и 87 удалено
  1. 322 87
      web-admin/src/views/production/production-work-order/index.vue

+ 322 - 87
web-admin/src/views/production/production-work-order/index.vue

@@ -4,7 +4,9 @@
   <div class="page-container">
     <header class="page-head">
       <h1>生产工单</h1>
-      <p class="page-desc">管理入库与临时生产工单;入库工单关联样本接收,临时工单可独立创建并派发任务。</p>
+      <p class="page-desc"
+        >管理入库与临时生产工单;入库工单关联样本接收,临时工单可独立创建并派发任务。</p
+      >
     </header>
 
     <section class="panel">
@@ -12,9 +14,14 @@
         <h2>工单列表</h2>
       </div>
 
-      <el-tabs v-model="activeTypeTab" class="work-order-type-tabs" @tab-change="handleTypeTabChange">
+      <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>
 
@@ -22,11 +29,7 @@
         <el-form :model="searchForm" class="filter-form" @submit.prevent>
           <div class="filter-grid">
             <el-form-item label="关键词">
-              <el-input
-                v-model="searchForm.keyword"
-                :placeholder="activeTypeTab === 'temporary' ? '工单号' : '工单号/协议号/样本编码'"
-                clearable
-              />
+              <el-input v-model="searchForm.keyword" :placeholder="searchPlaceholder" clearable />
             </el-form-item>
             <el-form-item label="工单状态">
               <el-select
@@ -38,6 +41,7 @@
                 <el-option label="待配置" value="dpz" />
                 <el-option label="进行中" value="jxz" />
                 <el-option label="已完成" value="ywc" />
+                <el-option label="已作废" value="yzf" />
               </el-select>
             </el-form-item>
             <el-form-item label="创建时间">
@@ -66,10 +70,11 @@
         </el-form>
       </div>
 
-      <div v-if="activeTypeTab !== 'all'" class="list-toolbar">
+      <div v-if="activeTypeTab !== 'all' && activeTypeTab !== 'outbound'" class="list-toolbar">
         <div class="list-toolbar-row">
           <el-button class="btn-main" type="primary" @click="handleAdd">
-            <el-icon><Plus /></el-icon>{{ activeTypeTab === 'temporary' ? '新增临时工单' : '新增生产工单' }}
+            <el-icon><Plus /></el-icon
+            >{{ activeTypeTab === 'temporary' ? '新增临时工单' : '新增生产工单' }}
           </el-button>
         </div>
       </div>
@@ -91,30 +96,121 @@
             :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
           />
           <el-table-column prop="workOrderNo" label="工单号" min-width="150" align="center" />
-          <el-table-column v-if="activeTypeTab === 'all'" prop="workOrderType" label="工单类型" min-width="100" align="center">
+          <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>
+          <!-- 出库工单专属列 -->
+          <el-table-column
+            v-if="activeTypeTab === 'outbound'"
+            prop="bizTypeLabel"
+            label="业务类型"
+            min-width="110"
+            align="center"
+          >
             <template #default="{ row }">
-              <el-tag :type="row.workOrderType === 'temporary' ? 'warning' : 'success'" size="small">
-                {{ row.workOrderType === 'temporary' ? '临时' : '入库' }}
+              <el-tag :type="row.bizType === 'pilot_service' ? 'primary' : 'success'" size="small">
+                {{ row.bizTypeLabel || '—' }}
               </el-tag>
             </template>
           </el-table-column>
-          <el-table-column v-if="showReceptionCols" prop="protocolNo" label="协议号" min-width="140" align="center">
+          <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>
+          <el-table-column
+            v-if="showReceptionCols"
+            prop="protocolNo"
+            label="协议号"
+            min-width="140"
+            align="center"
+          >
             <template #default="{ row }">{{ formatTempEmptyField(row, row.protocolNo) }}</template>
           </el-table-column>
-          <el-table-column v-if="showReceptionCols" prop="protocolStatus" label="协议状态" min-width="100" align="center">
+          <el-table-column
+            v-if="showReceptionCols"
+            prop="protocolStatus"
+            label="协议状态"
+            min-width="100"
+            align="center"
+          >
             <template #default="{ row }">
               <span v-if="shouldShowTempEmptyField(row, row.protocolStatus)">—</span>
               <el-tag
                 v-else
-                :type="protocolStatusTagTypeBySource(String(row.protocolStatus || ''), row.receptionSource || 'cell_storage')"
+                :type="
+                  protocolStatusTagTypeBySource(
+                    String(row.protocolStatus || ''),
+                    row.receptionSource || 'cell_storage'
+                  )
+                "
                 size="small"
-              >{{ protocolStatusLabelBySource(String(row.protocolStatus || ''), row.receptionSource || 'cell_storage') }}</el-tag>
+                >{{
+                  protocolStatusLabelBySource(
+                    String(row.protocolStatus || ''),
+                    row.receptionSource || 'cell_storage'
+                  )
+                }}</el-tag
+              >
             </template>
           </el-table-column>
-          <el-table-column v-if="showReceptionCols" prop="sampleCode" label="样本编码" min-width="120" align="center">
+          <el-table-column
+            v-if="showReceptionCols"
+            prop="sampleCode"
+            label="样本编码"
+            min-width="120"
+            align="center"
+          >
             <template #default="{ row }">{{ formatTempEmptyField(row, row.sampleCode) }}</template>
           </el-table-column>
-          <el-table-column v-if="showReceptionCols" prop="receptionSource" label="来源类型" min-width="110" align="center">
+          <el-table-column
+            v-if="showReceptionCols"
+            prop="receptionSource"
+            label="来源类型"
+            min-width="110"
+            align="center"
+          >
             <template #default="{ row }">
               <span v-if="shouldShowTempEmptyField(row, row.receptionSource)">—</span>
               <el-tag v-else :type="sourceTag(row.receptionSource).type" size="small">
@@ -122,13 +218,31 @@
               </el-tag>
             </template>
           </el-table-column>
-          <el-table-column v-if="showReceptionCols" prop="sampleTypeName" label="样本类型" min-width="110" align="center">
+          <el-table-column
+            v-if="showReceptionCols"
+            prop="sampleTypeName"
+            label="样本类型"
+            min-width="110"
+            align="center"
+          >
             <template #default="{ row }">{{ row.sampleTypeName || '—' }}</template>
           </el-table-column>
-          <el-table-column v-if="showReceptionCols" prop="handlerName" label="接收人" min-width="100" align="center">
+          <el-table-column
+            v-if="showReceptionCols"
+            prop="handlerName"
+            label="接收人"
+            min-width="100"
+            align="center"
+          >
             <template #default="{ row }">{{ row.handlerName || '—' }}</template>
           </el-table-column>
-          <el-table-column v-if="showReceptionCols" prop="receptionTime" label="接收时间" min-width="170" align="center">
+          <el-table-column
+            v-if="showReceptionCols"
+            prop="receptionTime"
+            label="接收时间"
+            min-width="170"
+            align="center"
+          >
             <template #default="{ row }">{{ row.receptionTime || '—' }}</template>
           </el-table-column>
           <el-table-column prop="remark" label="备注" min-width="140" align="center">
@@ -143,6 +257,7 @@
           </el-table-column>
           <el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
           <el-table-column
+            v-if="activeTypeTab !== 'all'"
             label="操作"
             min-width="600"
             fixed="right"
@@ -173,7 +288,12 @@
               >
                 <el-icon><Check /></el-icon>完成工单
               </el-button>
-              <el-button v-if="row.workOrderStatus !== 'ywc'" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
+              <el-button
+                v-if="row.workOrderStatus !== 'ywc'"
+                class="btn-op-item"
+                @click="handleDelete(row)"
+                >删除</el-button
+              >
             </template>
           </el-table-column>
         </el-table>
@@ -302,7 +422,6 @@
         </div>
       </el-dialog>
 
-
       <!-- 新增生产工单弹窗 -->
       <el-dialog
         v-model="addVisible"
@@ -314,10 +433,18 @@
         :close-on-click-modal="false"
       >
         <el-form ref="addFormRef" :model="addForm" :rules="addRules" label-position="top">
-          <p v-if="addMode === 'temporary'" class="hint-muted">临时工单不关联样本与协议,创建后请派发任务。</p>
+          <p v-if="addMode === 'temporary'" class="hint-muted"
+            >临时工单不关联样本与协议,创建后请派发任务。</p
+          >
           <div v-if="addMode === 'temporary'" class="form-grid-2">
             <el-form-item label="备注" class="form-col-span-2">
-              <el-input v-model="addForm.remark" type="textarea" :rows="3" placeholder="选填" maxlength="512" />
+              <el-input
+                v-model="addForm.remark"
+                type="textarea"
+                :rows="3"
+                placeholder="选填"
+                maxlength="512"
+              />
             </el-form-item>
           </div>
           <div v-else class="form-grid-2">
@@ -325,7 +452,11 @@
               <el-input v-model="addForm.protocolNo" placeholder="请输入协议号(可选)" clearable />
             </el-form-item>
             <el-form-item label="样本编码">
-              <el-input v-model="addForm.sampleCode" placeholder="请输入样本编码(可选)" clearable />
+              <el-input
+                v-model="addForm.sampleCode"
+                placeholder="请输入样本编码(可选)"
+                clearable
+              />
             </el-form-item>
             <el-form-item label="来源类型" prop="receptionSource">
               <el-select v-model="addForm.receptionSource" style="width: 100%">
@@ -334,7 +465,13 @@
               </el-select>
             </el-form-item>
             <el-form-item label="备注" class="form-col-span-2">
-              <el-input v-model="addForm.remark" type="textarea" :rows="3" placeholder="选填" maxlength="512" />
+              <el-input
+                v-model="addForm.remark"
+                type="textarea"
+                :rows="3"
+                placeholder="选填"
+                maxlength="512"
+              />
             </el-form-item>
           </div>
         </el-form>
@@ -392,16 +529,20 @@
     protocolStatusLabel,
     protocolStatusTagType,
     donationStatusLabel,
-    donationStatusTagType,
+    donationStatusTagType
   } from '@/utils/status-tag-registry'
   import type { TagType } from '@/utils/status-tag-registry'
 
   /** 根据来源类型返回协议状态标签 */
   function protocolStatusLabelBySource(status: string, source: string): string {
-    return source === 'research_donation' ? donationStatusLabel(status) : protocolStatusLabel(status)
+    return source === 'research_donation'
+      ? donationStatusLabel(status)
+      : protocolStatusLabel(status)
   }
   function protocolStatusTagTypeBySource(status: string, source: string): TagType {
-    return source === 'research_donation' ? donationStatusTagType(status) : protocolStatusTagType(status)
+    return source === 'research_donation'
+      ? donationStatusTagType(status)
+      : protocolStatusTagType(status)
   }
 
   /** 临时工单无关联协议/样本时,空字段展示为 — */
@@ -418,20 +559,21 @@
   import DispatchTaskDialog from './DispatchTaskDialog.vue'
   import EditTaskInfoDialog from './EditTaskInfoDialog.vue'
   import WorkOrderRemarkDialog from '@/components/business/work-order-remark-dialog/index.vue'
-  import {
-    productionConclusionLabel,
-    productionConclusionTagType
-  } from '@/utils/production-labels'
-  import {
-    fetchProductionTaskList,
-    fetchDeleteProductionTask
-  } from '@/api/production-task'
+  import { productionConclusionLabel, productionConclusionTagType } from '@/utils/production-labels'
+  import { fetchProductionTaskList, fetchDeleteProductionTask } from '@/api/production-task'
 
   defineOptions({ name: 'ProductionProductionWorkOrder' })
 
-  type WorkOrderTypeTab = 'inbound' | 'temporary' | 'all'
+  type WorkOrderTypeTab = 'inbound' | 'temporary' | 'outbound' | 'all'
   const activeTypeTab = ref<WorkOrderTypeTab>('inbound')
-  const showReceptionCols = computed(() => activeTypeTab.value !== 'temporary')
+  const showReceptionCols = computed(
+    () => activeTypeTab.value !== 'temporary' && activeTypeTab.value !== 'outbound'
+  )
+  const searchPlaceholder = computed(() => {
+    if (activeTypeTab.value === 'temporary') return '工单号'
+    if (activeTypeTab.value === 'outbound') return '工单号/申请单号'
+    return '工单号/协议号/样本编码'
+  })
   const addMode = ref<'inbound' | 'temporary'>('inbound')
 
   // ── 新增工单 ──────────────────────────────────────────
@@ -485,7 +627,8 @@
   const remarkEditLoading = ref(false)
   const remarkEditRow = ref<{ id: string; workOrderNo?: string; remark?: string } | null>(null)
   const remarkEditTitle = computed(
-    () => `编辑备注${remarkEditRow.value?.workOrderNo ? ` — ${remarkEditRow.value.workOrderNo}` : ''}`
+    () =>
+      `编辑备注${remarkEditRow.value?.workOrderNo ? ` — ${remarkEditRow.value.workOrderNo}` : ''}`
   )
   const remarkEditInitialRemark = computed(() => remarkEditRow.value?.remark || '')
 
@@ -511,7 +654,8 @@
     const map: Record<string, { label: string; type: string }> = {
       dpz: { label: '待配置', type: 'info' },
       jxz: { label: '进行中', type: 'warning' },
-      ywc: { label: '已完成', type: 'success' }
+      ywc: { label: '已完成', type: 'success' },
+      yzf: { label: '已作废', type: 'danger' }
     }
     return map[status] || { label: status, type: 'info' }
   }
@@ -530,6 +674,14 @@
     }
     return map[source] || { label: source, type: 'info' }
   }
+  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' }
+  }
 
   function formatLocalDate(d: Date): string {
     const y = d.getFullYear()
@@ -641,7 +793,122 @@
     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 || '')) }
     ]
@@ -661,62 +928,29 @@
       transportFields.length > 1 || r.transportType
         ? [{ title: '运输信息', fields: transportFields }]
         : []
+
     return [
-      {
-        title: '基本信息',
-        fields: [
-          { label: '工单号', value: d.workOrderNo },
-          {
-            label: '工单类型',
-            value: isTemp ? '临时' : '入库',
-            tagType: isTemp ? 'warning' : 'success'
-          },
-          ...(isTemp
-            ? []
-            : [
-                { 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 || '—' }
-              ]),
-          {
-            label: '工单状态',
-            value: woStatusTag(d.workOrderStatus).label,
-            tagType: woStatusTag(d.workOrderStatus).type as any
-          },
-          { label: '备注', value: d.remark || '—', span: true },
-          { label: '创建时间', value: d.createTime || '—' }
-        ]
-      },
-      ...(isTemp ? [] : transportSection),
-      ...(isTemp
-        ? []
-        : [
+      { 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: '接收温度(℃)',
+                  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 || '')),
+                    r.acceptConclusionLabel ||
+                    acceptConclusionLabel(String(r.acceptConclusion || '')),
                   tagType:
                     r.acceptConclusion === 'js'
                       ? 'success'
@@ -728,7 +962,8 @@
                 { label: '备注', value: r.remark || '—', span: true }
               ]
             }
-          ])
+          ]
+        : [])
     ]
   })