提交
This commit is contained in:
1 parent
93f5801aa9
commit
9dd7eb56ba
28 files changed
+3310
-2858
No files matched your search
@@ -0,0 +1,251 @@
|
||||
<!-- 生产任务 — 只读查看(任务信息 + 产出细胞产品) -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<SystemViewDialog
|
||||
v-model:visible="innerVisible"
|
||||
title="生产任务详情"
|
||||
width="960px"
|
||||
:grid-columns="3"
|
||||
:icon="Operation"
|
||||
:meta="viewDetail?.taskNo || ''"
|
||||
:sections="viewSections"
|
||||
:loading="loading"
|
||||
append-to-body
|
||||
compact
|
||||
modal-class="production-task-view-dialog-mask"
|
||||
>
|
||||
<template #task-outputs>
|
||||
<div v-if="viewOutputs.length" class="view-subtable-wrap">
|
||||
<el-table :data="viewOutputs" stripe class="list-table view-subtable">
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="56"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
label="产品编码"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.productCode || row.outputBarcode || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="产品名称"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.productName || row.outputName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品类型" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.productTypeName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格" min-width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.specName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="passage"
|
||||
label="代次"
|
||||
min-width="70"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="batchNo"
|
||||
label="批次号"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="储物格位置" min-width="160" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.storageSlotName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预计细胞数量(亿)" min-width="130" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.expectedCellCount ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活性" min-width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.viability != null ? row.viability : '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="冻存日期" min-width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatDateOnly(row.freezeDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期" min-width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatDateOnly(row.expiryDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" min-width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.quantity ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unit" label="单位" min-width="70" align="center" header-align="center" />
|
||||
<el-table-column label="状态" min-width="90" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="productStatusTagType(row.productStatus)" size="small">
|
||||
{{ productStatusLabel(row.productStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<p v-else class="view-empty-hint">暂无产出产品</p>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Operation } from '@element-plus/icons-vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { fetchProductionTaskDetail } from '@/api/production-task'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
taskId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void
|
||||
}>()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const viewDetail = ref<Record<string, any> | null>(null)
|
||||
|
||||
const TASK_STATUS_LABELS: Record<string, string> = {
|
||||
dpz: '待配置',
|
||||
dzx: '待执行',
|
||||
jxz: '执行中',
|
||||
ywc: '已完成'
|
||||
}
|
||||
|
||||
const TASK_TYPE_LABELS: Record<string, string> = {
|
||||
cell_storage: '入库生产单',
|
||||
outbound: '出库生产单'
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.taskId] as const,
|
||||
async ([vis, id]) => {
|
||||
if (!vis || !id) {
|
||||
viewDetail.value = null
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
viewDetail.value = await fetchProductionTaskDetail(id)
|
||||
} catch {
|
||||
viewDetail.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function taskStatusLabel(status?: string) {
|
||||
return TASK_STATUS_LABELS[status || ''] || status || '—'
|
||||
}
|
||||
|
||||
function taskStatusTagType(status?: string) {
|
||||
if (status === 'ywc') return 'success' as const
|
||||
if (status === 'jxz') return 'warning' as const
|
||||
if (status === 'dzx') return 'info' as const
|
||||
return 'info' as const
|
||||
}
|
||||
|
||||
function taskTypeLabel(taskType?: string) {
|
||||
return TASK_TYPE_LABELS[taskType || ''] || taskType || '—'
|
||||
}
|
||||
|
||||
function taskTypeTagType(taskType?: string) {
|
||||
if (taskType === 'cell_storage') return 'primary' as const
|
||||
if (taskType === 'outbound') return 'warning' as const
|
||||
return 'info' as const
|
||||
}
|
||||
|
||||
function productStatusTagType(status?: string) {
|
||||
const map: Record<string, 'success' | 'warning' | 'danger' | 'info'> = {
|
||||
frozen: 'success',
|
||||
in_production: 'warning',
|
||||
voided: 'danger',
|
||||
thawed: 'info'
|
||||
}
|
||||
return map[status || ''] || 'info'
|
||||
}
|
||||
|
||||
function productStatusLabel(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
frozen: '已冻存',
|
||||
in_production: '生产中',
|
||||
voided: '已作废',
|
||||
thawed: '已复苏'
|
||||
}
|
||||
return map[status || ''] || status || '—'
|
||||
}
|
||||
|
||||
function formatDateOnly(val: unknown) {
|
||||
if (!val) return '—'
|
||||
const s = String(val)
|
||||
return s.length >= 10 ? s.slice(0, 10) : s
|
||||
}
|
||||
|
||||
const viewOutputs = computed(() => {
|
||||
const rows = (viewDetail.value?.outputs as Array<Record<string, any>>) || []
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
productCode: row.productCode || row.outputBarcode || '',
|
||||
productName: row.productName || row.outputName || ''
|
||||
}))
|
||||
})
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
return [
|
||||
{
|
||||
title: '任务信息',
|
||||
fields: [
|
||||
{ label: '任务号', value: d.taskNo || '—' },
|
||||
{ label: '任务名称', value: d.taskName || '—' },
|
||||
{ label: '工单号', value: d.workOrderNo || '—' },
|
||||
{
|
||||
label: '任务类型',
|
||||
value: taskTypeLabel(d.taskType),
|
||||
tagType: taskTypeTagType(d.taskType)
|
||||
},
|
||||
{
|
||||
label: '任务状态',
|
||||
value: taskStatusLabel(d.taskStatus),
|
||||
tagType: taskStatusTagType(d.taskStatus)
|
||||
},
|
||||
{ label: '协议号', value: d.protocolNo || '—' },
|
||||
{ label: '样本编码', value: d.sampleCode || '—' },
|
||||
{ label: '开始时间', value: d.startedTime || '—' },
|
||||
{ label: '完成时间', value: d.completedTime || '—' },
|
||||
{ label: '备注', value: d.remark || '—', span: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '产出(细胞产品)',
|
||||
fields: [],
|
||||
slot: 'task-outputs'
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.production-task-view-dialog-mask {
|
||||
z-index: 4000 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -59,30 +59,11 @@
|
||||
<span class="qc-kv__label">完成时间</span>
|
||||
<span class="qc-kv__value">{{ task.completedTime || '—' }}</span>
|
||||
</div>
|
||||
<!-- 输入信息 -->
|
||||
<div v-if="task.inputs?.length" class="production-task-card__io">
|
||||
<div class="production-task-card__io-label">输入</div>
|
||||
<div class="production-task-card__io-items">
|
||||
<div v-for="(inp, idx) in task.inputs" :key="'in-' + idx" class="io-item">
|
||||
<el-tag type="info" size="small" class="io-item__type">{{
|
||||
inputTypeLabel(inp.inputType)
|
||||
}}</el-tag>
|
||||
<span class="io-item__name">{{ inp.inputName || inp.inputCode || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 输出信息 -->
|
||||
<div v-if="task.outputs?.length" class="production-task-card__io">
|
||||
<div class="production-task-card__io-label">输出</div>
|
||||
<div class="production-task-card__io-items">
|
||||
<div v-for="(out, idx) in task.outputs" :key="'out-' + idx" class="io-item">
|
||||
<el-tag type="success" size="small" class="io-item__type">{{
|
||||
outputTypeLabel(out.outputType)
|
||||
}}</el-tag>
|
||||
<span class="io-item__name">{{ out.outputName || out.outputCode || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="production-task-card__foot">
|
||||
<button type="button" class="production-task-card__link" @click.stop="openTaskView(task)">
|
||||
点击查看详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,10 +72,17 @@
|
||||
暂无生产任务
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Teleport to="body">
|
||||
<ProductionTaskViewDialog v-model:visible="taskViewVisible" :task-id="taskViewId" />
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import ProductionTaskViewDialog from '@/components/business/production-task-view-dialog/index.vue'
|
||||
|
||||
export type ProductionTaskInput = {
|
||||
inputType?: string
|
||||
inputCode?: string
|
||||
@@ -143,6 +131,15 @@
|
||||
productionInfo?: ProductionInfo | null
|
||||
}>()
|
||||
|
||||
const taskViewVisible = ref(false)
|
||||
const taskViewId = ref('')
|
||||
|
||||
function openTaskView(task: ProductionTask) {
|
||||
if (!task.id) return
|
||||
taskViewId.value = task.id
|
||||
taskViewVisible.value = true
|
||||
}
|
||||
|
||||
function productionWorkOrderStatusLabel(status: string) {
|
||||
return PRODUCTION_WORK_ORDER_STATUS_LABELS[status] || status || '—'
|
||||
}
|
||||
@@ -163,20 +160,6 @@
|
||||
if (status === 'dzx') return 'info' as const
|
||||
return 'info' as const
|
||||
}
|
||||
|
||||
function inputTypeLabel(type?: string) {
|
||||
if (type === 'sample') return '样本'
|
||||
if (type === 'reagent') return '试剂'
|
||||
if (type === 'consumable') return '耗材'
|
||||
return type || '—'
|
||||
}
|
||||
|
||||
function outputTypeLabel(type?: string) {
|
||||
if (type === 'sample') return '样本'
|
||||
if (type === 'cell_product') return '细胞产品'
|
||||
if (type === 'other') return '其他'
|
||||
return type || '—'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -253,41 +236,24 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.production-task-card__io {
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
.production-task-card__foot {
|
||||
padding: 2px 12px 8px;
|
||||
text-align: right;
|
||||
border-top: 1px dashed #f1f5f9;
|
||||
}
|
||||
|
||||
.production-task-card__io-label {
|
||||
.production-task-card__link {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
line-height: 1.4;
|
||||
color: #10b981;
|
||||
cursor: pointer;
|
||||
|
||||
.production-task-card__io-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.io-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
|
||||
.io-item__type {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.io-item__name {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
&:hover {
|
||||
color: #059669;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user