This commit is contained in:
zlwu committed 2026-05-20 14:32:07 +08:00
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 {