This commit is contained in:
zlwu committed 2026-06-30 09:19:06 +08:00
1 parent 1ca2668112
commit 5b3edb1700
278 files changed
+53761 -36931

No files matched your search

@@ -34,7 +34,9 @@
show-overflow-tooltip
/>
<el-table-column prop="productTypeName" label="产品类型" min-width="100" />
<el-table-column prop="specName" label="规格" min-width="90" />
<el-table-column label="规格" min-width="90">
<template #default="{ row }">{{ resolveCellProductSpecDisplay(row) || '—' }}</template>
</el-table-column>
<el-table-column prop="passage" label="代次" min-width="70" align="center" />
<el-table-column
prop="storageSlotName"
@@ -355,7 +357,9 @@
</template>
</el-table-column>
<el-table-column prop="batchNo" label="批次号" min-width="110" show-overflow-tooltip />
<el-table-column prop="specName" label="规格" min-width="90" show-overflow-tooltip />
<el-table-column label="规格" min-width="90" show-overflow-tooltip>
<template #default="{ row }">{{ resolveCellProductSpecDisplay(row) || '—' }}</template>
</el-table-column>
</el-table>
</div>
<template #footer>
@@ -410,6 +414,7 @@
import type { OutboundReleaseCellRow } from '@/api/outbound-release-review'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { cellProductStatusLabel, cellProductStatusTagType } from '@/utils/status-tag-registry'
import { resolveCellProductSpecDisplay } from '@/utils/cell-product-spec'
const props = withDefaults(
defineProps<{
@@ -728,7 +733,7 @@
productCode: row.productCode || '',
productName: row.productName || '',
productTypeName: row.productTypeName || '',
specName: row.specName || '',
specName: resolveCellProductSpecDisplay(row),
passage: row.passage || '',
batchNo: row.batchNo || '',
storageSlotName: row.storageSlotName || '',
@@ -37,7 +37,9 @@
min-width="90"
show-overflow-tooltip
/>
<el-table-column prop="specName" label="规格" min-width="90" show-overflow-tooltip />
<el-table-column label="规格" min-width="90" show-overflow-tooltip>
<template #default="{ row }">{{ resolveCellProductSpecDisplay(row) || '—' }}</template>
</el-table-column>
<el-table-column
prop="storageSlotName"
label="储物格"
@@ -224,6 +226,7 @@
outboundMaterialCellStatusTagType
} from '@/utils/status-tag-registry'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { resolveCellProductSpecDisplay } from '@/utils/cell-product-spec'
defineOptions({ name: 'OutboundMaterialReadonlyPanel' })
@@ -49,9 +49,10 @@
<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>
<template #default="{ row }">{{ resolveCellProductSpecDisplay(row) || '—' }}</template>
</el-table-column>
<el-table-column
v-if="viewOutputsNeedPassage"
prop="passage"
label="代次"
min-width="70"
@@ -70,12 +71,14 @@
<template #default="{ row }">{{ row.storageSlotName || '—' }}</template>
</el-table-column>
<el-table-column
label="预计细胞数量(亿)"
:label="EXPECTED_CELL_COUNT_LABEL"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.expectedCellCount ?? '—' }}</template>
<template #default="{ row }">{{
formatExpectedCellCountDisplay(row.expectedCellCount)
}}</template>
</el-table-column>
<el-table-column label="活率(%)" min-width="80" align="center" header-align="center">
<template #default="{ row }">{{
@@ -120,6 +123,12 @@
import { fetchProductionTaskDetail } from '@/api/production-task'
import { productionConclusionLabel, productionConclusionTagType } from '@/utils/production-labels'
import { cellProductStatusLabel, cellProductStatusTagType } from '@/utils/status-tag-registry'
import {
EXPECTED_CELL_COUNT_LABEL,
formatExpectedCellCountDisplay
} from '@/utils/expected-cell-count'
import { resolveCellProductSpecDisplay } from '@/utils/cell-product-spec'
import { cellProductTypeNeedsPassage } from '@/utils/cell-product-type'
const props = defineProps<{
visible: boolean
@@ -201,10 +210,17 @@
return rows.map((row) => ({
...row,
productCode: row.productCode || row.outputBarcode || '',
productName: row.productName || row.outputName || ''
productName: row.productName || row.outputName || '',
specName: resolveCellProductSpecDisplay(row)
}))
})
const viewOutputsNeedPassage = computed(() =>
viewOutputs.value.some((row) =>
cellProductTypeNeedsPassage(row.productTypeName, row.productTypeCode)
)
)
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
@@ -101,7 +101,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted } from 'vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import { maskIdCard } from '@/utils/mask-sensitive'
import {
@@ -111,6 +111,7 @@
payStatusLabel,
payStatusTagType,
protocolStatusLabel,
loadTransferTypeDict,
transferTypeLabel
} from '@/utils/protocol-payment-labels'
@@ -184,6 +185,10 @@
refundAttachmentFiles.value.length > 0
)
})
onMounted(() => {
void loadTransferTypeDict()
})
</script>
<style scoped lang="scss">
@@ -121,6 +121,12 @@
qcPreviewAttachment,
qcDownloadAttachment
} from '@/views/quality-inspection/qc-work-order/composables/useQcViewAttachmentHelpers'
import { resolveCellProductSpecDisplay } from '@/utils/cell-product-spec'
import {
buildProductionReceptionViewSection,
buildQcReceptionViewFields
} from '@/utils/sample-reception-view-sections'
import type { ProductionReceptionRecord } from '@/utils/sample-production-reception-fields'
const props = defineProps<{
visible: boolean
@@ -338,7 +344,7 @@
{ label: '产品名称', value: d.cellProductName || '—' },
{ label: '产品类型', value: d.cellProductTypeName || '—' },
{ label: '代次', value: d.cellProductPassage || '—' },
{ label: '规格', value: d.cellProductSpecName || '—' }
{ label: '规格', value: resolveCellProductSpecDisplay(d) || '—' }
]
}
]
@@ -366,34 +372,19 @@
? [{ title: '运输信息', fields: transportFields }]
: []),
{
title: '样本接收',
title: '质检接收',
slot: 'reception-extra',
fields: [
{ label: '接收时间', value: r.receptionTime || d.receptionTime || '—' },
{ label: '接收经手人', value: r.handlerName || d.handlerName || '—' },
...(String(r.acceptConclusion || '') === 'js'
? [
{
label: '实验室入库编码',
value: r.labStorageCode || d.labStorageCode || '—'
}
]
: []),
{
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 || ''))
},
{ label: '检查项', checkItems: r.checkResults ?? [], span: true },
{ label: '备注', value: r.remark || '—', span: true }
]
}
fields: buildQcReceptionViewFields(
r as Record<string, unknown>,
d as unknown as Record<string, unknown>
)
},
buildProductionReceptionViewSection(
(r.productionReception as ProductionReceptionRecord) ||
(d as { productionReception?: ProductionReceptionRecord }).productionReception,
String(r.sourceType || d.receptionSource || 'cell_storage'),
String(r.sampleTypeName || d.sampleTypeName || '')
)
]
const taskResultSummaryFields: ViewSection['fields'] = [
{
@@ -0,0 +1,259 @@
<!-- 质检 / 生产样本接收信息只读双区块 -->
<template>
<div class="sample-reception-dual-readonly">
<div v-if="showQc" class="sample-reception-dual-readonly__block">
<h4 v-if="qcTitle" class="detail-section-title">{{ qcTitle }}</h4>
<div v-if="!qcReception" class="sample-reception-dual-readonly__empty">{{ qcEmptyHint }}</div>
<template v-else>
<div class="readonly-grid form-grid-2">
<div class="readonly-item">
<span class="label">接收时间</span>{{ qcReception.receptionTime || '—' }}
</div>
<div class="readonly-item">
<span class="label">经手人</span>{{ qcReception.handlerName || '—' }}
</div>
<div class="readonly-item">
<span class="label">接收温度(℃)</span
>{{
qcReception.receiveTemperature
? `${qcReception.receiveTemperature}℃`
: '—'
}}
</div>
<div class="readonly-item">
<span class="label">结论</span>
<el-tag :type="acceptTagType(qcReception.acceptConclusion)" size="small">
{{ acceptLabel(qcReception) }}
</el-tag>
</div>
<div
v-if="String(qcReception.acceptConclusion || '') === 'js'"
class="readonly-item"
>
<span class="label">实验室入库编码</span>{{ qcReception.labStorageCode || '—' }}
</div>
<div class="readonly-item form-col-span-2 check-items-readonly">
<span class="label">检查项</span>
<div v-if="qcReception.checkResults?.length" class="check-list check-list-view">
<el-checkbox
v-for="chk in qcReception.checkResults"
:key="String(chk.itemCode || chk.itemName)"
:model-value="Boolean(chk.checked)"
disabled
>
{{ chk.itemName }}
</el-checkbox>
</div>
<span v-else>—</span>
</div>
<div class="readonly-item form-col-span-2">
<span class="label">备注</span>{{ qcReception.remark || '—' }}
</div>
<div class="readonly-item form-col-span-2">
<span class="label">接收附件</span>
<ProtocolAttachmentReadonly
:files="qcAttachmentFiles"
@preview="handleAttachmentPreview"
@download="handleAttachmentDownload"
/>
</div>
</div>
</template>
</div>
<div v-if="showProduction" class="sample-reception-dual-readonly__block">
<h4 v-if="productionTitle" class="detail-section-title">{{ productionTitle }}</h4>
<div v-if="!productionReception" class="sample-reception-dual-readonly__empty">
{{ productionEmptyHint }}
</div>
<div
v-else
class="readonly-grid"
:class="productionGridCols === 4 ? 'form-grid-4' : 'form-grid-2'"
>
<div class="readonly-item">
<span class="label">处理时间</span>{{ productionReception.processTime || '—' }}
</div>
<div class="readonly-item">
<span class="label">处理人</span>{{ productionReception.handlerName || '—' }}
</div>
<div
v-for="field in productionFieldDefs"
:key="field.key"
class="readonly-item"
>
<span class="label">{{ field.label }}</span
>{{ formatProductionFieldValue(field, productionReception) }}
</div>
<div class="readonly-item form-col-span-2">
<span class="label">备注</span>{{ productionReception.remark || '—' }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import { acceptConclusionLabel } from '@/utils/qc-labels'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import {
formatProductionFieldValue,
resolveProductionReceptionFields,
type ProductionReceptionRecord,
type QcReceptionRecord
} from '@/utils/sample-production-reception-fields'
defineOptions({ name: 'SampleReceptionDualReadonlyPanel' })
const props = withDefaults(
defineProps<{
qcReception?: QcReceptionRecord | null
productionReception?: ProductionReceptionRecord | null
sourceType?: string
sampleTypeName?: string
showQc?: boolean
showProduction?: boolean
qcTitle?: string
productionTitle?: string
productionGridCols?: 2 | 4
qcEmptyHint?: string
productionEmptyHint?: string
}>(),
{
showQc: true,
showProduction: true,
qcTitle: '质检接收信息',
productionTitle: '生产接收信息',
productionGridCols: 2,
qcEmptyHint: '暂未接收',
productionEmptyHint: '生产尚未登记'
}
)
const productionFieldDefs = computed(() =>
resolveProductionReceptionFields(
props.sourceType || 'cell_storage',
props.sampleTypeName || ''
)
)
function acceptLabel(rec: QcReceptionRecord) {
return (
rec.acceptConclusionLabel ||
acceptConclusionLabel(String(rec.acceptConclusion || '')) ||
'—'
)
}
function acceptTagType(conclusion?: string) {
if (conclusion === 'js') return 'success'
if (conclusion === 'bj') return 'danger'
return 'warning'
}
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const qcAttachmentFiles = computed(() => {
const rec = props.qcReception
if (!rec) return []
if (Array.isArray(rec.receiveAttachmentFiles)) {
return rec.receiveAttachmentFiles as Array<Record<string, unknown>>
}
return []
})
function fullFileUrl(url: string) {
if (!url) return ''
if (/^https?:\/\//i.test(url)) return url
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
}
function handleAttachmentPreview(file: Record<string, unknown>) {
const url = String(file.fileUrl || '')
if (!url) return
window.open(fullFileUrl(url), '_blank')
}
function handleAttachmentDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl })
}
</script>
<style scoped lang="scss">
.sample-reception-dual-readonly__block + .sample-reception-dual-readonly__block {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed var(--el-border-color-lighter, #e2e8f0);
}
.sample-reception-dual-readonly__empty {
color: var(--mint-text-muted, #64748b);
font-size: 13px;
padding: 4px 0 8px;
}
.readonly-item {
font-size: 14px;
line-height: 1.5;
.label {
display: inline-block;
min-width: 72px;
margin-right: 0;
color: var(--mint-text-muted, #64748b);
&::after {
content: ': ';
}
}
}
.check-items-readonly {
display: flex;
flex-direction: column;
gap: 8px;
.label {
margin-right: 0;
}
.check-list {
padding-left: 0;
}
}
.check-list-view {
pointer-events: none;
:deep(.el-checkbox.is-disabled) {
opacity: 1;
cursor: default;
.el-checkbox__label {
color: var(--el-text-color-regular);
}
.el-checkbox__input.is-disabled {
opacity: 1;
cursor: default;
.el-checkbox__inner {
background-color: var(--el-checkbox-bg-color);
border-color: var(--el-checkbox-input-border-color);
cursor: default;
}
&.is-checked .el-checkbox__inner {
background-color: var(--el-checkbox-checked-bg-color);
border-color: var(--el-checkbox-checked-input-border-color);
&::after {
border-color: var(--el-checkbox-checked-icon-color);
}
}
}
}
}
</style>