提交
This commit is contained in:
1 parent
05684ba2a4
commit
7fbf71b6e1
150 files changed
+19834
-12611
No files matched your search
@@ -43,7 +43,7 @@
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="56"
|
||||
min-width="56"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => index + 1"
|
||||
@@ -63,7 +63,7 @@
|
||||
{{ row.qty != null && row.qty !== '' ? row.qty : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="72" align="center" header-align="center">
|
||||
<el-table-column label="单位" min-width="72" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.unit || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" min-width="140" align="center" header-align="center">
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
<!-- 样本查看 — 生产信息卡片(协议单查看弹窗复用) -->
|
||||
<template>
|
||||
<div class="sample-card__section sample-card__section--production">
|
||||
<div class="sample-card__section-label">生产信息</div>
|
||||
|
||||
<div v-if="!productionInfo" class="sample-production-empty">暂无生产信息</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="production-work-order-row">
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">工单号</span>
|
||||
<span class="qc-kv__value qc-kv__value--code">{{
|
||||
productionInfo.workOrderNo || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">工单状态</span>
|
||||
<span class="qc-kv__value">
|
||||
<el-tag
|
||||
:type="productionWorkOrderStatusTagType(productionInfo.workOrderStatus || '')"
|
||||
size="small"
|
||||
>
|
||||
{{
|
||||
productionInfo.workOrderStatusLabel ||
|
||||
productionWorkOrderStatusLabel(productionInfo.workOrderStatus || '')
|
||||
}}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="productionInfo.workOrderStatus === 'dpz' && !productionInfo.tasks?.length"
|
||||
class="sample-production-hint"
|
||||
>
|
||||
工单待配置生产任务
|
||||
</div>
|
||||
|
||||
<div v-if="productionInfo.tasks?.length" class="production-task-cards">
|
||||
<div v-for="task in productionInfo.tasks" :key="task.id" class="production-task-card">
|
||||
<div class="production-task-card__header">
|
||||
<span class="production-task-card__name">{{
|
||||
task.taskName || task.taskNo || '—'
|
||||
}}</span>
|
||||
<el-tag :type="productionTaskStatusTagType(task.taskStatus || '')" size="small">
|
||||
{{ task.taskStatusLabel || productionTaskStatusLabel(task.taskStatus || '') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="production-task-card__body">
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">任务号</span>
|
||||
<span class="qc-kv__value">{{ task.taskNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">开始时间</span>
|
||||
<span class="qc-kv__value">{{ task.startedTime || '—' }}</span>
|
||||
</div>
|
||||
<div class="qc-kv">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div v-else-if="productionInfo.workOrderStatus !== 'dpz'" class="sample-production-hint">
|
||||
暂无生产任务
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
export type ProductionTaskInput = {
|
||||
inputType?: string
|
||||
inputCode?: string
|
||||
inputName?: string
|
||||
}
|
||||
|
||||
export type ProductionTaskOutput = {
|
||||
outputType?: string
|
||||
outputCode?: string
|
||||
outputName?: string
|
||||
}
|
||||
|
||||
export type ProductionTask = {
|
||||
id?: string
|
||||
taskNo?: string
|
||||
taskName?: string
|
||||
taskStatus?: string
|
||||
taskStatusLabel?: string
|
||||
startedTime?: string
|
||||
completedTime?: string
|
||||
inputs?: ProductionTaskInput[]
|
||||
outputs?: ProductionTaskOutput[]
|
||||
}
|
||||
|
||||
export type ProductionInfo = {
|
||||
workOrderNo?: string
|
||||
workOrderStatus?: string
|
||||
workOrderStatusLabel?: string
|
||||
tasks?: ProductionTask[]
|
||||
}
|
||||
|
||||
const PRODUCTION_WORK_ORDER_STATUS_LABELS: Record<string, string> = {
|
||||
dpz: '待配置',
|
||||
jxz: '进行中',
|
||||
ywc: '已完成'
|
||||
}
|
||||
|
||||
const PRODUCTION_TASK_STATUS_LABELS: Record<string, string> = {
|
||||
dpz: '待配置',
|
||||
dzx: '待执行',
|
||||
jxz: '执行中',
|
||||
ywc: '已完成'
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
productionInfo?: ProductionInfo | null
|
||||
}>()
|
||||
|
||||
function productionWorkOrderStatusLabel(status: string) {
|
||||
return PRODUCTION_WORK_ORDER_STATUS_LABELS[status] || status || '—'
|
||||
}
|
||||
|
||||
function productionWorkOrderStatusTagType(status: string) {
|
||||
if (status === 'ywc') return 'success' as const
|
||||
if (status === 'jxz') return 'warning' as const
|
||||
return 'info' as const
|
||||
}
|
||||
|
||||
function productionTaskStatusLabel(status: string) {
|
||||
return PRODUCTION_TASK_STATUS_LABELS[status] || status || '—'
|
||||
}
|
||||
|
||||
function productionTaskStatusTagType(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 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">
|
||||
.sample-card__section--production {
|
||||
margin-top: 4px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.sample-card__section-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.sample-production-empty,
|
||||
.sample-production-hint {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.production-work-order-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 12px 24px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.production-work-order-row .qc-kv {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.production-task-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.production-task-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.production-task-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.production-task-card__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.production-task-card__body {
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.production-task-card__io {
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #f1f5f9;
|
||||
}
|
||||
|
||||
.production-task-card__io-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.qc-kv {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.qc-kv__label {
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.qc-kv__value {
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
|
||||
&--code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
<!-- 样本查看 — 入库放行审核信息(协议单查看弹窗复用) -->
|
||||
<template>
|
||||
<div class="sample-card__section sample-card__section--storage-release">
|
||||
<div class="sample-card__section-label">入库放行审核信息</div>
|
||||
|
||||
<div v-if="!storageReleaseInfo" class="sample-storage-release-empty">
|
||||
暂无入库放行审核信息
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="storage-release-info-grid">
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">审核结论</span>
|
||||
<span class="qc-kv__value">
|
||||
<el-tag
|
||||
v-if="storageReleaseInfo.reviewConclusion"
|
||||
:type="storageReleaseInfo.reviewConclusion === 'tg' ? 'success' : 'danger'"
|
||||
size="small"
|
||||
>
|
||||
{{
|
||||
storageReleaseInfo.reviewConclusionLabel ||
|
||||
conclusionLabel(storageReleaseInfo.reviewConclusion)
|
||||
}}
|
||||
</el-tag>
|
||||
<span v-else class="qc-kv__muted">待审核</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">审核人</span>
|
||||
<span class="qc-kv__value">{{ storageReleaseInfo.reviewByName || '—' }}</span>
|
||||
</div>
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">审核时间</span>
|
||||
<span class="qc-kv__value">{{ storageReleaseInfo.reviewTime || '—' }}</span>
|
||||
</div>
|
||||
<div v-if="storageReleaseInfo.reviewRemark" class="qc-kv qc-kv--full">
|
||||
<span class="qc-kv__label">审核备注</span>
|
||||
<span class="qc-kv__value qc-kv__value--muted">{{
|
||||
storageReleaseInfo.reviewRemark
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="attachmentFiles.length" class="storage-release-attachments">
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">审核附件</span>
|
||||
<span class="qc-kv__value">
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="attachmentFiles"
|
||||
@preview="handlePreview"
|
||||
@download="handleDownload"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
|
||||
export type StorageReleaseInfo = {
|
||||
reviewConclusion?: string
|
||||
reviewConclusionLabel?: string
|
||||
reviewByName?: string
|
||||
reviewTime?: string
|
||||
reviewRemark?: string
|
||||
reviewAttachments?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
storageReleaseInfo?: StorageReleaseInfo | null
|
||||
}>()
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const attachmentFiles = computed(() => {
|
||||
const urls = String(props.storageReleaseInfo?.reviewAttachments || '').trim()
|
||||
if (!urls) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
.map((fileUrl, i) => ({
|
||||
id: `srr_${i}`,
|
||||
fileUrl,
|
||||
fileName: fileUrl.split('/').pop() || `附件${i + 1}`
|
||||
}))
|
||||
})
|
||||
|
||||
function conclusionLabel(conclusion: string) {
|
||||
if (conclusion === 'tg') return '通过'
|
||||
if (conclusion === 'btg') return '不通过'
|
||||
return conclusion
|
||||
}
|
||||
|
||||
function fullFileUrl(suffix: string) {
|
||||
if (!suffix) return ''
|
||||
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/'))
|
||||
return suffix
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${suffix.replace(/^\//, '')}` : suffix
|
||||
}
|
||||
|
||||
function handlePreview(file: Record<string, any>) {
|
||||
const url = fullFileUrl(String(file.fileUrl || ''))
|
||||
if (url) window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function handleDownload(file: Record<string, any>) {
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sample-card__section--storage-release {
|
||||
margin-top: 4px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.sample-card__section-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.sample-storage-release-empty {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.storage-release-info-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 12px 24px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.storage-release-attachments {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #f1f5f9;
|
||||
}
|
||||
|
||||
.qc-kv {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
&--full {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.qc-kv__label {
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.qc-kv__value {
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
|
||||
&--muted {
|
||||
color: #64748b;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.qc-kv__muted {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user