feat: expose lineage QC API and wire viewer in outbound material dialog

Add GET /cell-products/{id}/lineage-qc, frontend client, readonly dialog,
and a 查看质检 action on outbound cell rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-29 09:55:51 +08:00
1 parent 40d93934a9
commit 33c4408557
4 files changed
+372 -10

No files matched your search

@@ -0,0 +1,264 @@
<!-- 细胞产品 — 来源链质检只读 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<el-dialog
:model-value="modelValue"
title="来源链质检"
width="1040px"
class="dialog-form-layout cell-lineage-qc-dialog"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
style="backdrop-filter: none"
@update:model-value="emit('update:modelValue', $event)"
@closed="resetState"
>
<div v-loading="loading" class="lineage-qc-body">
<div v-if="data" class="lineage-qc-summary">
<span class="lineage-qc-summary__code">{{ data.productCode || '—' }}</span>
<span class="lineage-qc-summary__name">{{ data.productName || '—' }}</span>
</div>
<el-empty
v-if="!loading && (!data?.nodes || !data.nodes.length)"
description="暂无来源链数据"
/>
<el-collapse v-else-if="data?.nodes?.length" v-model="activeNames" class="lineage-qc-collapse">
<el-collapse-item
v-for="(node, idx) in data.nodes"
:key="`${node.nodeType}-${node.id}-${idx}`"
:name="String(idx)"
>
<template #title>
<div class="lineage-qc-node-title">
<el-tag v-if="node.isCurrent" type="success" size="small">当前</el-tag>
<el-tag v-if="node.nodeType === 'sample'" type="info" size="small">样本</el-tag>
<el-tag v-else type="primary" size="small">细胞产品</el-tag>
<span class="lineage-qc-node-title__label">{{ node.label || '—' }}</span>
<span v-if="node.productName" class="lineage-qc-node-title__sub">{{
node.productName
}}</span>
<span v-if="node.passage" class="lineage-qc-node-title__sub"
>代次 {{ node.passage }}</span
>
<span v-if="node.receptionSourceLabel" class="lineage-qc-node-title__sub">{{
node.receptionSourceLabel
}}</span>
</div>
</template>
<div v-if="!(node.workOrders || []).length" class="lineage-qc-empty">暂无质检工单</div>
<div v-for="wo in node.workOrders || []" :key="wo.id" class="lineage-qc-wo">
<div class="lineage-qc-wo__head">
<span class="lineage-qc-wo__no">{{ wo.workOrderNo || '—' }}</span>
<el-tag v-if="wo.workOrderType" type="info" size="small">{{
workOrderTypeLabel(wo.workOrderType)
}}</el-tag>
<el-tag
v-if="wo.workOrderStatus"
:type="workOrderStatusTagType(wo.workOrderStatus)"
size="small"
>
{{ wo.workOrderStatusLabel || wo.workOrderStatus }}
</el-tag>
</div>
<QcWorkOrderTaskResultPanel :tasks="mapTasks(wo.tasks)" />
</div>
</el-collapse-item>
</el-collapse>
</div>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>
关闭
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { Close } from '@element-plus/icons-vue'
import {
fetchCellProductLineageQc,
type CellProductLineageQc,
type CellProductLineageQcTask
} from '@/api/cell-product'
import QcWorkOrderTaskResultPanel from '@/views/quality-inspection/qc-work-order/QcWorkOrderTaskResultPanel.vue'
import type { QcWorkOrderTaskView } from '@/views/quality-inspection/qc-work-order/composables/useQcWorkOrderTaskViewSections'
defineOptions({ name: 'CellProductLineageQcDialog' })
const props = defineProps<{
modelValue: boolean
productId: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
}>()
const loading = ref(false)
const data = ref<CellProductLineageQc | null>(null)
const activeNames = ref<string[]>([])
function workOrderTypeLabel(type?: string) {
const map: Record<string, string> = {
inbound: '入库',
temporary: '临时',
outbound: '出库'
}
return map[type || ''] || type || '—'
}
function workOrderStatusTagType(status?: string): 'success' | 'warning' | 'info' | 'danger' {
if (status === 'ywc') return 'success'
if (status === 'yzf') return 'danger'
if (status === 'ypz') return 'warning'
return 'info'
}
function mapTasks(tasks?: CellProductLineageQcTask[]): QcWorkOrderTaskView[] {
return (tasks || []).map((t) => ({
...(t as any),
results: t.results || [],
attachmentFiles: t.attachmentFiles || [],
materialUsages: []
}))
}
async function loadData() {
const id = (props.productId || '').trim()
if (!id) return
loading.value = true
data.value = null
try {
const res = await fetchCellProductLineageQc(id)
data.value = res || null
activeNames.value = (res?.nodes || []).map((_, i) => String(i))
} catch {
ElMessage.error('加载来源链质检失败')
data.value = null
activeNames.value = []
} finally {
loading.value = false
}
}
function resetState() {
data.value = null
activeNames.value = []
loading.value = false
}
watch(
() => [props.modelValue, props.productId] as const,
([vis, id]) => {
if (vis && id) loadData()
},
{ immediate: true }
)
</script>
<style scoped lang="scss">
.lineage-qc-body {
min-height: 160px;
}
.lineage-qc-summary {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 8px 16px;
margin-bottom: 16px;
padding: 12px 16px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
}
.lineage-qc-summary__code {
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
.lineage-qc-summary__name {
font-size: 13px;
color: var(--mint-text-muted, #64748b);
}
.lineage-qc-collapse {
border: none;
:deep(.el-collapse-item__header) {
height: auto;
min-height: 48px;
line-height: 1.4;
padding: 10px 0;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}
:deep(.el-collapse-item__content) {
padding-bottom: 16px;
}
}
.lineage-qc-node-title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding-right: 12px;
}
.lineage-qc-node-title__label {
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
.lineage-qc-node-title__sub {
font-size: 12px;
color: var(--mint-text-muted, #64748b);
}
.lineage-qc-empty {
padding: 12px 4px;
font-size: 13px;
color: var(--mint-text-muted, #64748b);
}
.lineage-qc-wo {
margin-bottom: 12px;
padding: 12px 14px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
&:last-child {
margin-bottom: 0;
}
}
.lineage-qc-wo__head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.lineage-qc-wo__no {
font-size: 13px;
font-weight: 600;
color: #1e293b;
}
</style>