提交
This commit is contained in:
1 parent
15ed25e33b
commit
dc31968204
143 files changed
+15823
-9989
No files matched your search
@@ -11,7 +11,7 @@
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<div v-loading="loading" class="protocol-view-body">
|
||||
<div v-loading="detailLoading" class="protocol-view-body">
|
||||
<div class="detail-tabs">
|
||||
<div
|
||||
v-for="tab in formTabs"
|
||||
@@ -28,41 +28,77 @@
|
||||
<!-- KPI 摘要条 -->
|
||||
<div class="kpi-strip">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">协议编号</div>
|
||||
<div class="kpi-value">{{ dash(protocol?.protocolNo) }}</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">协议状态</div>
|
||||
<div class="kpi-value">
|
||||
<el-tag :type="protocolStatusTagType(String(protocol?.protocolStatus || ''))" size="small">
|
||||
{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}
|
||||
</el-tag>
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><Document /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">协议编号</div>
|
||||
<div class="kpi-value kpi-value--emphasis">{{ dash(protocol?.protocolNo) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">母亲姓名</div>
|
||||
<div class="kpi-value">{{ dash(partyCustomer?.motherName) }}</div>
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">协议状态</div>
|
||||
<div class="kpi-value">
|
||||
<el-tag :type="protocolStatusTagType(String(protocol?.protocolStatus || ''))" size="small">
|
||||
{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-label">登记日期</div>
|
||||
<div class="kpi-value">{{ formatYmd(protocol?.registerDate) }}</div>
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">母亲姓名</div>
|
||||
<div class="kpi-value">{{ dash(partyCustomer?.motherName) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">登记日期</div>
|
||||
<div class="kpi-value">{{ formatYmd(protocol?.registerDate) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div id="view-section-basic" class="section-card section-card--white">
|
||||
<div class="section-title"><h3>基本信息</h3></div>
|
||||
<div class="data-grid data-grid--3">
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item"><div class="data-label">协议号</div><div class="data-value">{{ dash(protocol?.protocolNo) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">协议状态</div><div class="data-value"><el-tag :type="protocolStatusTagType(String(protocol?.protocolStatus || ''))" size="small">{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}</el-tag></div></div>
|
||||
<div class="data-item"><div class="data-label">登记日期</div><div class="data-value">{{ formatYmd(protocol?.registerDate) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">采集专员</div><div class="data-value">{{ collectorDisplay }}</div></div>
|
||||
<div class="data-item"><div class="data-label">创建时间</div><div class="data-value">{{ dash(protocol?.createTime) }}</div></div>
|
||||
<template v-if="showStorageCert">
|
||||
<div class="data-item">
|
||||
<div class="data-label">制作状态</div>
|
||||
<div class="data-value">
|
||||
<el-tag
|
||||
:type="storageCertMakeStatusTagType(String(storageCertInfo?.makeStatus || '0'))"
|
||||
size="small"
|
||||
>
|
||||
{{ storageCertMakeStatusLabel(String(storageCertInfo?.makeStatus || '0')) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">制作日期</div>
|
||||
<div class="data-value">{{ formatYmd(storageCertInfo?.makeDate) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 协议状态变更记录 -->
|
||||
<div v-if="hasAnyStatusChange" class="status-change-section">
|
||||
<!-- 协议状态变更记录(非储存证查看:仍在基本信息内) -->
|
||||
<div v-if="!showStorageCert && hasAnyStatusChange" class="status-change-section">
|
||||
<div class="status-change-title">状态变更记录</div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div class="data-grid data-grid--4">
|
||||
<template v-if="protocol?.effectiveTime">
|
||||
<div class="data-item"><div class="data-label">协议生效时间</div><div class="data-value">{{ dash(protocol.effectiveTime) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">协议生效描述</div><div class="data-value data-value--muted">{{ dash(protocol.effectiveDesc) }}</div></div>
|
||||
@@ -81,7 +117,16 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 附件行 -->
|
||||
<!-- 储存证附件(储存证查看:在协议附件上方) -->
|
||||
<div v-if="showStorageCert" class="attachment-row">
|
||||
<div class="data-label">储存证附件</div>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="storageCertAttachments"
|
||||
@preview="openPreview"
|
||||
@download="downloadFile"
|
||||
/>
|
||||
</div>
|
||||
<!-- 协议附件 -->
|
||||
<div class="attachment-row" v-if="protocolAttachments.length">
|
||||
<div class="data-label">协议附件</div>
|
||||
<ProtocolAttachmentReadonly :files="protocolAttachments" @preview="openPreview" @download="downloadFile" />
|
||||
@@ -92,6 +137,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态变更记录(储存证查看:独立区域) -->
|
||||
<div v-if="showStorageCert" id="view-section-status-change" class="section-card section-card--white">
|
||||
<div class="section-title"><h3>状态变更记录</h3></div>
|
||||
<el-empty v-if="!hasAnyStatusChange" description="暂无状态变更记录" :image-size="64" />
|
||||
<div v-else class="data-grid data-grid--4">
|
||||
<template v-if="protocol?.effectiveTime">
|
||||
<div class="data-item"><div class="data-label">协议生效时间</div><div class="data-value">{{ dash(protocol.effectiveTime) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">协议生效描述</div><div class="data-value data-value--muted">{{ dash(protocol.effectiveDesc) }}</div></div>
|
||||
</template>
|
||||
<template v-if="protocol?.expireTime">
|
||||
<div class="data-item"><div class="data-label">协议过期时间</div><div class="data-value">{{ dash(protocol.expireTime) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">协议过期描述</div><div class="data-value data-value--muted">{{ dash(protocol.expireDesc) }}</div></div>
|
||||
</template>
|
||||
<template v-if="protocol?.voidTime">
|
||||
<div class="data-item"><div class="data-label">协议作废时间</div><div class="data-value">{{ dash(protocol.voidTime) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">协议作废描述</div><div class="data-value data-value--muted">{{ dash(protocol.voidDesc) }}</div></div>
|
||||
</template>
|
||||
<template v-if="protocol?.toResearchTime">
|
||||
<div class="data-item"><div class="data-label">协议转科研时间</div><div class="data-value">{{ dash(protocol.toResearchTime) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">协议转科研描述</div><div class="data-value data-value--muted">{{ dash(protocol.toResearchDesc) }}</div></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户信息 -->
|
||||
<div id="view-section-party" class="section-card section-card--green">
|
||||
<div class="section-title"><h3>客户信息</h3></div>
|
||||
@@ -99,7 +168,7 @@
|
||||
<!-- 母亲信息 -->
|
||||
<div class="client-sub-card">
|
||||
<div class="sub-card-header"><span class="sub-card-icon">🤱</span><h4>母亲信息</h4></div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item"><div class="data-label">关联客户</div><div class="data-value">{{ partyCustomer?.customerId ? dash(partyCustomer?.motherName) : '—' }}</div></div>
|
||||
<div class="data-item"><div class="data-label">母亲姓名</div><div class="data-value">{{ dash(partyCustomer?.motherName) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">母亲联系电话</div><div class="data-value">{{ dash(partyCustomer?.motherPhone) }}</div></div>
|
||||
@@ -112,7 +181,7 @@
|
||||
<!-- 父亲信息 -->
|
||||
<div class="client-sub-card">
|
||||
<div class="sub-card-header"><span class="sub-card-icon">👨</span><h4>父亲信息</h4></div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item"><div class="data-label">父亲姓名</div><div class="data-value">{{ dash(partyCustomer?.fatherName) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">父亲电话</div><div class="data-value">{{ dash(partyCustomer?.fatherPhone) }}</div></div>
|
||||
<div class="data-item data-item--full"><div class="data-label">父亲身份证号</div><div class="data-value">{{ dash(partyCustomer?.fatherIdCard) }}</div></div>
|
||||
@@ -121,7 +190,7 @@
|
||||
<!-- 新生儿信息 -->
|
||||
<div class="client-sub-card">
|
||||
<div class="sub-card-header"><span class="sub-card-icon">👶</span><h4>新生儿信息</h4></div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item"><div class="data-label">新生儿姓名</div><div class="data-value">{{ dash(partyCustomer?.newbornName) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">新生儿身份证</div><div class="data-value">{{ dash(partyCustomer?.newbornIdCard) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">新生儿分娩时间</div><div class="data-value">{{ dash(partyCustomer?.newbornDeliveryTime) }}</div></div>
|
||||
@@ -136,10 +205,67 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新生儿保险信息(满月待办查看:客户信息下方独立区域) -->
|
||||
<div
|
||||
v-if="showFullmoonInsurance"
|
||||
id="view-section-fullmoon-insurance"
|
||||
class="section-card section-card--white"
|
||||
>
|
||||
<div class="section-title"><h3>新生儿保险信息</h3></div>
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item"><div class="data-label">满月日期</div><div class="data-value">{{ dash(fullmoonInsuranceInfo?.fullmoonDate) }}</div></div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">待办状态</div>
|
||||
<div class="data-value">
|
||||
<el-tag
|
||||
:type="fullmoonTodoStatusTagType(String(fullmoonInsuranceInfo?.todoStatus || '0'))"
|
||||
size="small"
|
||||
>
|
||||
{{ fullmoonTodoStatusLabel(String(fullmoonInsuranceInfo?.todoStatus || '0')) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">是否已订购保险</div>
|
||||
<div class="data-value">
|
||||
<el-tag
|
||||
:type="insuranceSubscribedTagType(String(fullmoonInsuranceInfo?.insuranceSubscribed || '0'))"
|
||||
size="small"
|
||||
>
|
||||
{{ insuranceSubscribedLabel(String(fullmoonInsuranceInfo?.insuranceSubscribed || '0')) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item"><div class="data-label">保险公司</div><div class="data-value">{{ dash(fullmoonInsuranceInfo?.insuranceCompanyName) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">保险险种</div><div class="data-value">{{ dash(fullmoonInsuranceInfo?.insuranceType) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">保险单号</div><div class="data-value">{{ dash(fullmoonInsuranceInfo?.insurancePolicyNo) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">保单费用(元)</div><div class="data-value">{{ formatMoney(fullmoonInsuranceInfo?.insuranceFee) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">保险单生效时间</div><div class="data-value">{{ formatYmd(fullmoonInsuranceInfo?.insuranceEffectiveDate) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">保险单失效时间</div><div class="data-value">{{ formatYmd(fullmoonInsuranceInfo?.insuranceExpiryDate) }}</div></div>
|
||||
<div class="data-item"><div class="data-label">办理日期</div><div class="data-value">{{ formatYmd(fullmoonInsuranceInfo?.handleDate) }}</div></div>
|
||||
</div>
|
||||
<div v-if="fullmoonInsuranceInfo?.handleRemark" class="remark-row">
|
||||
<div class="data-item data-item--full">
|
||||
<div class="data-label">办理备注</div>
|
||||
<div class="data-value data-value--muted">{{ dash(fullmoonInsuranceInfo?.handleRemark) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachment-row">
|
||||
<div class="data-label">保险单附件</div>
|
||||
<ProtocolAttachmentReadonly
|
||||
v-if="fullmoonInsuranceAttachments.length"
|
||||
:files="fullmoonInsuranceAttachments"
|
||||
@preview="openPreview"
|
||||
@download="downloadFile"
|
||||
/>
|
||||
<div v-else class="data-value data-value--muted">—</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 采集信息 -->
|
||||
<div id="view-section-collection" class="section-card section-card--white">
|
||||
<div class="section-title"><h3>采集信息</h3></div>
|
||||
<div class="data-grid data-grid--3">
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item"><div class="data-label">采集地区</div><div class="data-value">{{ regionDisplay }}</div></div>
|
||||
<div class="data-item"><div class="data-label">采集医院</div><div class="data-value">{{ hospitalDisplay }}</div></div>
|
||||
<div class="data-item"><div class="data-label">采集医护</div><div class="data-value">{{ dash(collectionInfo?.medicalStaff) }}</div></div>
|
||||
@@ -149,8 +275,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样本及运输 -->
|
||||
<div id="view-section-sample" class="section-card section-card--white">
|
||||
<!-- 样本及运输(字典异步补齐,不阻塞整弹窗) -->
|
||||
<div
|
||||
id="view-section-sample"
|
||||
v-loading="lookupLoading"
|
||||
class="section-card section-card--white"
|
||||
>
|
||||
<div class="section-title"><h3>样本及运输信息</h3></div>
|
||||
<div v-if="samples.length" class="sample-cards">
|
||||
<div
|
||||
@@ -170,7 +300,7 @@
|
||||
<div class="sample-card__body">
|
||||
<div class="sample-card__section">
|
||||
<div class="sample-card__section-label">样本信息</div>
|
||||
<div class="data-grid data-grid--3">
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item">
|
||||
<div class="data-label">样本编码</div>
|
||||
<div class="data-value data-value--code">{{ dash(row.sampleCode) }}</div>
|
||||
@@ -195,7 +325,7 @@
|
||||
</div>
|
||||
<div v-if="row.transport?.transportType" class="sample-card__section sample-card__section--transport">
|
||||
<div class="sample-card__section-label">运输信息</div>
|
||||
<div class="data-grid data-grid--3">
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item">
|
||||
<div class="data-label">运输类型</div>
|
||||
<div class="data-value">{{ transportTypeLabel(String(row.transport?.transportType || '')) }}</div>
|
||||
@@ -217,7 +347,7 @@
|
||||
<div class="sample-card__section sample-card__section--reception">
|
||||
<div class="sample-card__section-label">样本接收信息</div>
|
||||
<template v-if="row.sampleReception">
|
||||
<div class="data-grid data-grid--3">
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item">
|
||||
<div class="data-label">接收时间</div>
|
||||
<div class="data-value">{{ dash(row.sampleReception.receptionTime) }}</div>
|
||||
@@ -236,6 +366,10 @@
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">净重(g)</div>
|
||||
<div class="data-value">{{ formatMetric(row.sampleReception.netWeightG) }}</div>
|
||||
</div>
|
||||
<div class="data-item">
|
||||
<div class="data-label">结论</div>
|
||||
<div class="data-value">
|
||||
@@ -642,7 +776,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import { Calendar, CircleCheck, Close, Document, User } from '@element-plus/icons-vue'
|
||||
import ProtocolAttachmentReadonly from './ProtocolAttachmentReadonly.vue'
|
||||
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
|
||||
import {
|
||||
@@ -684,8 +818,12 @@
|
||||
listRow?: CollectionProtocolRow | null
|
||||
/** 展示审核记录 Tab(采集协议单审核页) */
|
||||
showAuditHistory?: boolean
|
||||
/** 储存证管理查看:展示储存证附件,状态变更记录独立区域 */
|
||||
showStorageCert?: boolean
|
||||
/** 满月待办查看:客户信息下方独立展示新生儿保险信息 */
|
||||
showFullmoonInsurance?: boolean
|
||||
}>(),
|
||||
{ showAuditHistory: false }
|
||||
{ showAuditHistory: false, showStorageCert: false, showFullmoonInsurance: false }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -702,13 +840,18 @@
|
||||
|
||||
const formTabs = computed(() => {
|
||||
const tabs: Array<{ key: string; label: string }> = [...baseFormTabs]
|
||||
if (props.showFullmoonInsurance) {
|
||||
const partyIdx = tabs.findIndex((t) => t.key === 'party')
|
||||
tabs.splice(partyIdx + 1, 0, { key: 'fullmoon-insurance', label: '新生儿保险' })
|
||||
}
|
||||
if (props.showAuditHistory) {
|
||||
tabs.push({ key: 'audit', label: '审核记录' })
|
||||
}
|
||||
return tabs
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const lookupLoading = ref(false)
|
||||
const auditLoading = ref(false)
|
||||
const bundle = ref<Record<string, any> | null>(null)
|
||||
const auditRecords = ref<Array<Record<string, unknown>>>([])
|
||||
@@ -760,6 +903,98 @@
|
||||
return Array.isArray(list) ? list : []
|
||||
})
|
||||
|
||||
const storageCertInfo = computed(() => {
|
||||
const sc = bundle.value?.storageCert as Record<string, any> | undefined | null
|
||||
if (sc) return sc
|
||||
if (!props.showStorageCert || !props.listRow) return null
|
||||
return {
|
||||
makeStatus: props.listRow.makeStatus ?? '0',
|
||||
certImageUrls: props.listRow.certImageUrls ?? '',
|
||||
makeDate: props.listRow.makeDate ?? '',
|
||||
remark: props.listRow.remark ?? ''
|
||||
}
|
||||
})
|
||||
|
||||
const storageCertAttachments = computed(() => {
|
||||
const urls = String(storageCertInfo.value?.certImageUrls || '').trim()
|
||||
if (!urls) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
.map((url, index) => {
|
||||
const rawName = url.split('/').pop() || ''
|
||||
const fileName = decodeURIComponent(rawName) || `附件${index + 1}`
|
||||
return { id: `cert_${index}`, fileName, fileUrl: url, fileType: '' }
|
||||
})
|
||||
})
|
||||
|
||||
const fullmoonInsuranceInfo = computed(() => {
|
||||
const fm = bundle.value?.fullmoonTodo as Record<string, any> | undefined
|
||||
if (fm) return fm
|
||||
if (!props.showFullmoonInsurance || !props.listRow) return null
|
||||
const row = props.listRow as Record<string, any>
|
||||
return {
|
||||
fullmoonDate: row.fullmoonDate ?? '',
|
||||
todoStatus: row.todoStatus ?? '0',
|
||||
insuranceSubscribed: row.insuranceSubscribed ?? '0',
|
||||
insuranceCompanyName: row.insuranceCompanyName ?? '',
|
||||
insuranceType: row.insuranceType ?? '',
|
||||
insurancePolicyNo: row.insurancePolicyNo ?? '',
|
||||
insuranceEffectiveDate: row.insuranceEffectiveDate ?? row.policyEffectiveDate ?? '',
|
||||
insuranceExpiryDate: row.insuranceExpiryDate ?? row.policyExpiryDate ?? '',
|
||||
insuranceFee: row.insuranceFee,
|
||||
handleDate: row.handleDate ?? row.processDate ?? '',
|
||||
handleRemark: row.handleRemark ?? row.remark ?? '',
|
||||
insuranceAttachments: row.insuranceAttachments ?? ''
|
||||
}
|
||||
})
|
||||
|
||||
const fullmoonInsuranceAttachments = computed(() => {
|
||||
const urls = String(fullmoonInsuranceInfo.value?.insuranceAttachments || '').trim()
|
||||
if (!urls) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
.map((url, index) => {
|
||||
const rawName = url.split('/').pop() || ''
|
||||
const fileName = decodeURIComponent(rawName) || `附件${index + 1}`
|
||||
return { id: `ins_${index}`, fileName, fileUrl: url, fileType: '' }
|
||||
})
|
||||
})
|
||||
|
||||
function fullmoonTodoStatusLabel(status: string) {
|
||||
return status === '1' ? '已办保险' : '未办保险'
|
||||
}
|
||||
|
||||
function fullmoonTodoStatusTagType(status: string) {
|
||||
return status === '1' ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
function insuranceSubscribedLabel(val: string) {
|
||||
return val === '1' ? '是' : '否'
|
||||
}
|
||||
|
||||
function insuranceSubscribedTagType(val: string) {
|
||||
return val === '1' ? 'success' : 'info'
|
||||
}
|
||||
|
||||
function formatMoney(val: unknown) {
|
||||
if (val === null || val === undefined || val === '') return '—'
|
||||
const n = Number(val)
|
||||
if (Number.isNaN(n)) return String(val)
|
||||
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||
}
|
||||
|
||||
function storageCertMakeStatusLabel(makeStatus: string) {
|
||||
return makeStatus === '1' ? '已制作' : '未制作'
|
||||
}
|
||||
|
||||
function storageCertMakeStatusTagType(makeStatus: string) {
|
||||
return makeStatus === '1' ? 'success' : 'warning'
|
||||
}
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
const no = protocol.value?.protocolNo || props.listRow?.protocolNo
|
||||
return no ? `查看协议单 — ${no}` : '查看协议单'
|
||||
@@ -820,6 +1055,12 @@
|
||||
return String(val)
|
||||
}
|
||||
|
||||
function formatMetric(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? String(n) : '—'
|
||||
}
|
||||
|
||||
function formatYmd(val: unknown) {
|
||||
const s = String(val || '').replace(/-/g, '')
|
||||
if (s.length === 8) return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`
|
||||
@@ -1249,7 +1490,7 @@
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
async function loadLookups(data: Record<string, any>) {
|
||||
async function loadBaseLookups() {
|
||||
const [stRes, uRes, ecRes, siteRes] = await Promise.all([
|
||||
fetchCellSampleTypeList({ page: 1, pageSize: 100 }),
|
||||
fetchItemUnitList({ page: 1, pageSize: 100 }),
|
||||
@@ -1260,7 +1501,9 @@
|
||||
unitOptions.value = (uRes.list || []) as any
|
||||
expressCompanyOptions.value = ecRes.list || []
|
||||
siteOptions.value = siteRes.list || []
|
||||
}
|
||||
|
||||
async function loadConditionalLookups(data: Record<string, any>) {
|
||||
const ci = data.collectionInfo as Record<string, any> | null
|
||||
const p = data.protocol as Record<string, any> | undefined
|
||||
const tasks: Promise<void>[] = []
|
||||
@@ -1291,10 +1534,22 @@
|
||||
})
|
||||
)
|
||||
}
|
||||
await Promise.all(tasks)
|
||||
if (tasks.length) {
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAuditRecords() {
|
||||
async function finishLookups(data: Record<string, any>, baseLookupsPromise: Promise<void>) {
|
||||
lookupLoading.value = true
|
||||
try {
|
||||
await baseLookupsPromise
|
||||
await loadConditionalLookups(data)
|
||||
} finally {
|
||||
lookupLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAuditRecords(protocolNoHint?: string) {
|
||||
if (!props.showAuditHistory || !props.protocolId) {
|
||||
auditRecords.value = []
|
||||
return
|
||||
@@ -1302,7 +1557,12 @@
|
||||
auditLoading.value = true
|
||||
try {
|
||||
const pno =
|
||||
String(protocol.value?.protocolNo || props.listRow?.protocolNo || '').trim() || undefined
|
||||
String(
|
||||
protocolNoHint ||
|
||||
protocol.value?.protocolNo ||
|
||||
props.listRow?.protocolNo ||
|
||||
''
|
||||
).trim() || undefined
|
||||
const res = await fetchCollectionProtocolAuditList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
@@ -1319,16 +1579,35 @@
|
||||
|
||||
async function loadDetail() {
|
||||
if (!props.protocolId) return
|
||||
loading.value = true
|
||||
detailLoading.value = true
|
||||
lookupLoading.value = true
|
||||
bundle.value = null
|
||||
auditRecords.value = []
|
||||
|
||||
const protocolNoHint = String(props.listRow?.protocolNo || '').trim()
|
||||
const baseLookupsPromise = loadBaseLookups().catch(() => {})
|
||||
const auditEarlyPromise =
|
||||
props.showAuditHistory && protocolNoHint
|
||||
? loadAuditRecords(protocolNoHint)
|
||||
: null
|
||||
|
||||
let data: Record<string, any> | null = null
|
||||
try {
|
||||
const data = await fetchCollectionProtocolDetail(props.protocolId)
|
||||
bundle.value = data as Record<string, any>
|
||||
await loadLookups(data as Record<string, any>)
|
||||
await loadAuditRecords()
|
||||
data = (await fetchCollectionProtocolDetail(props.protocolId)) as Record<string, any>
|
||||
bundle.value = data
|
||||
} finally {
|
||||
loading.value = false
|
||||
detailLoading.value = false
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
lookupLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
void finishLookups(data, baseLookupsPromise)
|
||||
|
||||
if (props.showAuditHistory && !auditEarlyPromise) {
|
||||
void loadAuditRecords()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1345,6 +1624,9 @@
|
||||
bundle.value = null
|
||||
auditRecords.value = []
|
||||
activeTab.value = 'basic'
|
||||
detailLoading.value = false
|
||||
lookupLoading.value = false
|
||||
auditLoading.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -1396,59 +1678,77 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- KPI 摘要条 ---- */
|
||||
/* ---- KPI 摘要条(统一薄荷绿通栏 + 图标徽章) ---- */
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
gap: 0;
|
||||
margin-bottom: 24px;
|
||||
padding: 6px;
|
||||
background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 52%, #ecfdf5 100%);
|
||||
border: 1px solid #a7f3d0;
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 18px 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
border-left: 3px solid #10b981;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 18px;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: radial-gradient(
|
||||
circle at top right,
|
||||
rgba(16, 185, 129, 0.06),
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid rgba(167, 243, 208, 0.75);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.1);
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #059669;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.kpi-card__main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
color: #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&--emphasis {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #047857;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Section Card ---- */
|
||||
@@ -1703,6 +2003,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.sample-card__section--qc) {
|
||||
margin-top: 2px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.sample-reception-empty {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
@@ -1890,10 +2196,10 @@
|
||||
|
||||
/* ---- PDC Info Row ---- */
|
||||
.pdc-info-row {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px 20px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pdc-info-item {
|
||||
@@ -2237,8 +2543,8 @@
|
||||
|
||||
.refund-area-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px 20px;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px 20px;
|
||||
|
||||
.data-label {
|
||||
font-size: 11px;
|
||||
@@ -2441,11 +2747,19 @@
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.kpi-strip .kpi-card {
|
||||
border-right: none !important;
|
||||
border-bottom: 1px solid rgba(167, 243, 208, 0.75);
|
||||
|
||||
&:nth-last-child(-n + 2) {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.client-sub-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.data-grid--3,
|
||||
.data-grid--4 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -2460,11 +2774,18 @@
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.kpi-strip .kpi-card:nth-last-child(-n + 2) {
|
||||
border-bottom: 1px solid rgba(167, 243, 208, 0.75);
|
||||
}
|
||||
|
||||
.kpi-strip .kpi-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.client-sub-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.data-grid--3,
|
||||
.data-grid--4 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -2474,12 +2795,11 @@
|
||||
}
|
||||
|
||||
.pdc-info-row {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.refund-area-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.finance-area-grid {
|
||||
|
||||
Reference in new issue
Block a user