1520 lines
49 KiB
Vue
1520 lines
49 KiB
Vue
<!-- 捐赠协议单 — 只读查看(字段与新增/编辑弹窗一致) -->
|
||
<template>
|
||
<el-dialog
|
||
:model-value="modelValue"
|
||
:title="dialogTitle"
|
||
width="1600px"
|
||
class="dialog-form-layout donation-view-dialog"
|
||
align-center
|
||
append-to-body
|
||
modal-class="protocol-dialog-mask"
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
@closed="handleClosed"
|
||
>
|
||
<div
|
||
class="donation-view-body"
|
||
:class="{ 'is-detail-loading': loading && !bundle }"
|
||
v-loading="loading && !bundle"
|
||
element-loading-text="加载中…"
|
||
>
|
||
<div class="detail-tabs">
|
||
<div
|
||
v-for="tab in formTabs"
|
||
:key="tab.key"
|
||
class="detail-tab"
|
||
:class="{ active: activeTab === tab.key, 'is-disabled': loading && !bundle }"
|
||
@click="scrollToSection(tab.key)"
|
||
>
|
||
{{ tab.label }}
|
||
</div>
|
||
</div>
|
||
|
||
<template v-if="bundle">
|
||
<div>
|
||
<!-- KPI 摘要条 -->
|
||
<div class="kpi-strip">
|
||
<div class="kpi-card">
|
||
<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="donationStatusTagType(String(donation?.donationStatus || ''))"
|
||
size="small"
|
||
>
|
||
{{ donationStatusLabel(String(donation?.donationStatus || '')) }}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<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(party?.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(donation?.donationDate) }}</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--4">
|
||
<div class="data-item"
|
||
><div class="data-label">状态</div
|
||
><div class="data-value"
|
||
><el-tag
|
||
:type="donationStatusTagType(String(donation?.donationStatus || ''))"
|
||
size="small"
|
||
>{{ donationStatusLabel(String(donation?.donationStatus || '')) }}</el-tag
|
||
></div
|
||
></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">捐赠日期</div
|
||
><div class="data-value">{{ formatYmd(donation?.donationDate) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">来源类型</div
|
||
><div class="data-value">{{
|
||
sourceTypeLabel(String(donation?.sourceType || ''))
|
||
}}</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(donation?.createTime) }}</div></div
|
||
>
|
||
</div>
|
||
<!-- 备注 -->
|
||
<div class="remark-row" v-if="donation?.remark">
|
||
<div class="data-item data-item--full"
|
||
><div class="data-label">备注</div
|
||
><div class="data-value data-value--muted">{{ dash(donation?.remark) }}</div></div
|
||
>
|
||
</div>
|
||
<!-- 知情同意书 -->
|
||
<div v-if="consentAttachmentFiles.length" class="attachment-row">
|
||
<div class="data-label">知情同意书</div>
|
||
<ProtocolAttachmentReadonly
|
||
:files="consentAttachmentFiles"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
<!-- 其他附件 -->
|
||
<div v-if="otherAttachmentFiles.length" class="attachment-row">
|
||
<div class="data-label">其他附件</div>
|
||
<ProtocolAttachmentReadonly
|
||
:files="otherAttachmentFiles"
|
||
@download="downloadFile"
|
||
@preview="openPreview"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 状态变更记录(独立区域) -->
|
||
<div 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="donation?.effectiveTime">
|
||
<div class="data-item"
|
||
><div class="data-label">生效时间</div
|
||
><div class="data-value">{{ dash(donation.effectiveTime) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">生效描述</div
|
||
><div class="data-value data-value--muted">{{
|
||
dash(donation.effectiveDesc)
|
||
}}</div></div
|
||
>
|
||
</template>
|
||
<template v-if="donation?.expireTime">
|
||
<div class="data-item"
|
||
><div class="data-label">过期时间</div
|
||
><div class="data-value">{{ dash(donation.expireTime) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">过期描述</div
|
||
><div class="data-value data-value--muted">{{
|
||
dash(donation.expireDesc)
|
||
}}</div></div
|
||
>
|
||
</template>
|
||
<template v-if="donation?.voidTime">
|
||
<div class="data-item"
|
||
><div class="data-label">作废时间</div
|
||
><div class="data-value">{{ dash(donation.voidTime) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">作废描述</div
|
||
><div class="data-value data-value--muted">{{
|
||
dash(donation.voidDesc)
|
||
}}</div></div
|
||
>
|
||
</template>
|
||
<template v-if="donation?.convertTime">
|
||
<div class="data-item"
|
||
><div class="data-label">转细胞存储时间</div
|
||
><div class="data-value">{{ dash(donation.convertTime) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">转细胞存储描述</div
|
||
><div class="data-value data-value--muted">{{
|
||
dash(donation.convertDesc)
|
||
}}</div></div
|
||
>
|
||
</template>
|
||
</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--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(collection?.medicalStaff) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">销售人员</div
|
||
><div class="data-value">{{ salesDisplay }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">样本采集时间</div
|
||
><div class="data-value">{{ dash(collection?.sampleCollectTime) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母血采集时间</div
|
||
><div class="data-value">{{ dash(collection?.maternalBloodCollectTime) }}</div></div
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 样本及运输 -->
|
||
<div id="view-section-sample" class="section-card section-card--white">
|
||
<div class="section-title"><h3>样本及运输信息</h3></div>
|
||
<div v-if="samples.length" class="sample-cards">
|
||
<div v-for="(row, sIdx) in samples" :key="row.id || sIdx" class="sample-card">
|
||
<div class="sample-card__header">
|
||
<div class="sample-card__title">
|
||
<span class="sample-card__seq">{{ sIdx + 1 }}</span>
|
||
{{ sampleTypeName(row.sampleTypeId) }}
|
||
</div>
|
||
<el-tag :type="sampleStatusTagType(String(row.sampleStatus || ''))" size="small">
|
||
{{ sampleStatusLabel(String(row.sampleStatus || '')) }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="sample-card__body">
|
||
<div class="sample-card__section">
|
||
<div class="sample-card__section-label">样本信息</div>
|
||
<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>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">采集量</div>
|
||
<div class="data-value"
|
||
>{{ dash(row.collectAmount) }} {{ unitName(row.unitId) }}</div
|
||
>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">存储年限(年)</div>
|
||
<div class="data-value">{{ dash(row.storageYears) }}</div>
|
||
</div>
|
||
<div v-if="row.remark" class="data-item data-item--full">
|
||
<div class="data-label">备注</div>
|
||
<div class="data-value data-value--muted">{{ dash(row.remark) }}</div>
|
||
</div>
|
||
</div>
|
||
</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--4">
|
||
<div class="data-item">
|
||
<div class="data-label">运输类型</div>
|
||
<div class="data-value">{{
|
||
transportTypeLabel(String(row.transport?.transportType || ''))
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">目的站点</div>
|
||
<div class="data-value">{{
|
||
siteName(String(row.transport?.destSiteId || ''))
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">预计到达</div>
|
||
<div class="data-value">{{ dash(row.transport?.etaTime) }}</div>
|
||
</div>
|
||
<div class="data-item data-item--full">
|
||
<div class="data-label">快递/携带</div>
|
||
<div class="data-value">{{ sampleTransportExtra(row.transport) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<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--4">
|
||
<div class="data-item">
|
||
<div class="data-label">接收时间</div>
|
||
<div class="data-value">{{
|
||
dash(row.sampleReception.receptionTime)
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">经手人</div>
|
||
<div class="data-value">{{ dash(row.sampleReception.handlerName) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">接收温度(℃)</div>
|
||
<div class="data-value">
|
||
{{
|
||
row.sampleReception.receiveTemperature
|
||
? row.sampleReception.receiveTemperature + '℃'
|
||
: '—'
|
||
}}
|
||
</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">净重(g)</div>
|
||
<div class="data-value">{{
|
||
formatMetric(row.sampleReception.netWeightG)
|
||
}}</div>
|
||
</div>
|
||
<div
|
||
v-if="String(row.sampleReception.acceptConclusion || '') === 'js'"
|
||
class="data-item"
|
||
>
|
||
<div class="data-label">实验室入库编码</div>
|
||
<div class="data-value">{{
|
||
dash(row.sampleReception.labStorageCode)
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">结论</div>
|
||
<div class="data-value">
|
||
<el-tag
|
||
:type="
|
||
acceptConclusionTagType(
|
||
String(row.sampleReception.acceptConclusion || '')
|
||
)
|
||
"
|
||
size="small"
|
||
>
|
||
{{
|
||
acceptConclusionLabel(
|
||
String(row.sampleReception.acceptConclusion || '')
|
||
)
|
||
}}
|
||
</el-tag>
|
||
</div>
|
||
</div>
|
||
<div class="data-item data-item--full">
|
||
<div class="data-label">检查项</div>
|
||
<div
|
||
v-if="sampleReceptionCheckList(row.sampleReception).length"
|
||
class="check-list check-list-view"
|
||
>
|
||
<el-checkbox
|
||
v-for="chk in sampleReceptionCheckList(row.sampleReception)"
|
||
:key="chk.itemCode + String(chk.checkItemId)"
|
||
:model-value="chk.checked"
|
||
disabled
|
||
>
|
||
{{ chk.itemName }}
|
||
</el-checkbox>
|
||
</div>
|
||
<div v-else class="data-value">—</div>
|
||
</div>
|
||
<div v-if="row.sampleReception.remark" class="data-item data-item--full">
|
||
<div class="data-label">备注</div>
|
||
<div class="data-value data-value--muted">{{
|
||
dash(row.sampleReception.remark)
|
||
}}</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
v-if="sampleReceptionAttachmentFiles(row.sampleReception).length"
|
||
class="attachment-row sample-card__attachments"
|
||
>
|
||
<div class="data-label">接收附件</div>
|
||
<ProtocolAttachmentReadonly
|
||
:files="sampleReceptionAttachmentFiles(row.sampleReception)"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<div v-else class="sample-reception-empty">暂未接收</div>
|
||
</div>
|
||
<SampleQcInfoGroupedPanel
|
||
:gate-qc-info="row.qcInfo"
|
||
:process-qc-info-list="row.processQcInfoList"
|
||
:sample-reception="row.sampleReception"
|
||
/>
|
||
<SampleProductionInfoCards :production-info="row.productionInfo" />
|
||
<SampleStorageReleaseInfo :storage-release-info="row.storageReleaseInfo" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无样本" :image-size="48" />
|
||
</div>
|
||
|
||
<!-- 客户信息 -->
|
||
<div id="view-section-party" class="section-card section-card--green">
|
||
<div class="section-title"><h3>客户信息</h3></div>
|
||
<div class="client-sub-cards">
|
||
<!-- 母亲信息 -->
|
||
<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--4">
|
||
<div class="data-item"
|
||
><div class="data-label">关联客户</div
|
||
><div class="data-value">{{
|
||
party?.customerId ? dash(party?.motherName) : '—'
|
||
}}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母亲姓名</div
|
||
><div class="data-value">{{ dash(party?.motherName) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母亲联系电话</div
|
||
><div class="data-value">{{ dash(party?.motherPhone) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母亲身份证号</div
|
||
><div class="data-value">{{ dash(party?.motherIdCard) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母亲出生年月</div
|
||
><div class="data-value">{{ formatYm(party?.motherBirthYm) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母亲民族</div
|
||
><div class="data-value">{{ dash(party?.motherEthnic) }}</div></div
|
||
>
|
||
</div>
|
||
<div class="sub-card-full-row"
|
||
><div class="data-label">母亲邮箱</div
|
||
><div class="data-value">{{ dash(party?.motherEmail) }}</div></div
|
||
>
|
||
<div class="sub-card-full-row"
|
||
><div class="data-label">母亲家庭住址</div
|
||
><div class="data-value data-value--muted">{{
|
||
dash(party?.motherAddress)
|
||
}}</div></div
|
||
>
|
||
</div>
|
||
<!-- 父亲信息 -->
|
||
<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--4">
|
||
<div class="data-item"
|
||
><div class="data-label">父亲姓名</div
|
||
><div class="data-value">{{ dash(party?.fatherName) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">父亲电话</div
|
||
><div class="data-value">{{ dash(party?.fatherPhone) }}</div></div
|
||
>
|
||
<div class="data-item data-item--full"
|
||
><div class="data-label">父亲身份证号</div
|
||
><div class="data-value">{{ dash(party?.fatherIdCard) }}</div></div
|
||
>
|
||
</div>
|
||
</div>
|
||
<!-- 新生儿信息 -->
|
||
<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--4">
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿姓名</div
|
||
><div class="data-value">{{ dash(party?.newbornName) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿身份证</div
|
||
><div class="data-value">{{ dash(party?.newbornIdCard) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">预产期</div
|
||
><div class="data-value">{{ dash(party?.expectedDeliveryDate) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿分娩时间</div
|
||
><div class="data-value">{{ dash(party?.newbornDeliveryTime) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿性别</div
|
||
><div class="data-value">{{
|
||
newbornSexLabel(String(party?.newbornSex || ''))
|
||
}}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">分娩方式</div
|
||
><div class="data-value">{{
|
||
deliveryModeLabel(String(party?.deliveryMode || ''))
|
||
}}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">胎次</div
|
||
><div class="data-value">{{ dash(party?.birthOrder) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿体重(克)</div
|
||
><div class="data-value">{{ dash(party?.newbornWeightGram) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿评分1分钟</div
|
||
><div class="data-value">{{ dash(party?.apgar1min) }}</div></div
|
||
>
|
||
<div class="data-item data-item--full"
|
||
><div class="data-label">新生儿评分5分钟</div
|
||
><div class="data-value">{{ dash(party?.apgar5min) }}</div></div
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 审核记录 -->
|
||
<div
|
||
v-if="showAuditHistory"
|
||
id="view-section-audit"
|
||
class="section-card section-card--white"
|
||
>
|
||
<div class="section-title"><h3>审核记录</h3></div>
|
||
<el-table
|
||
v-loading="auditLoading"
|
||
class="list-table sub-view-table"
|
||
:data="auditRecords"
|
||
stripe
|
||
empty-text="暂无审核记录"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="createTime"
|
||
label="审核时间"
|
||
min-width="170"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="auditStatus"
|
||
label="审核状态"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
:type="String(row.auditStatus) === 'shtg' ? 'success' : 'warning'"
|
||
size="small"
|
||
>
|
||
{{ auditStatusLabel(String(row.auditStatus || '')) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="auditRemark"
|
||
label="审核备注"
|
||
min-width="200"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
<el-table-column
|
||
prop="auditorUserName"
|
||
label="审核人"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{ auditorDisplay(row) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
|
||
<el-icon><Close /></el-icon>
|
||
关闭
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog
|
||
v-model="previewVisible"
|
||
title="文件预览"
|
||
width="800px"
|
||
align-center
|
||
destroy-on-close
|
||
append-to-body
|
||
>
|
||
<div v-if="previewType === 'image'" class="preview-container">
|
||
<img
|
||
:src="previewUrl"
|
||
alt="预览"
|
||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||
/>
|
||
</div>
|
||
<div v-else class="preview-container">
|
||
<video
|
||
:src="previewUrl"
|
||
controls
|
||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||
/>
|
||
</div>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, nextTick, ref, watch } from 'vue'
|
||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||
import { Calendar, CircleCheck, Close, User } from '@element-plus/icons-vue'
|
||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||
import SampleQcInfoGroupedPanel from '@/components/business/sample-qc-info-grouped-panel/index.vue'
|
||
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
|
||
import SampleStorageReleaseInfo from '@/components/business/sample-storage-release-info/index.vue'
|
||
import {
|
||
fetchResearchDonationAuditList,
|
||
fetchResearchDonationDetail,
|
||
type ResearchDonationRow
|
||
} from '@/api/research-donation'
|
||
import {
|
||
fetchCellSampleTypeList,
|
||
fetchItemUnitList,
|
||
fetchExpressCompanyList,
|
||
fetchSiteList,
|
||
fetchCollectionRegionList,
|
||
fetchHospitalList,
|
||
fetchUserList
|
||
} from '@/api/admin'
|
||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||
import {
|
||
donationStatusLabel,
|
||
donationStatusTagType,
|
||
sourceTypeLabel,
|
||
auditStatusLabel,
|
||
auditStatusTagType,
|
||
newbornSexLabel,
|
||
deliveryModeLabel,
|
||
sampleStatusLabel,
|
||
sampleStatusTagType,
|
||
transportTypeLabel,
|
||
acceptConclusionLabel,
|
||
acceptConclusionTagType
|
||
} from '@/utils/status-tag-registry'
|
||
|
||
defineOptions({ name: 'DonationViewDialog' })
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
modelValue: boolean
|
||
donationId: string | null
|
||
listRow?: ResearchDonationRow | null
|
||
/** 展示审核记录 Tab(科研捐赠审核页) */
|
||
showAuditHistory?: boolean
|
||
}>(),
|
||
{ showAuditHistory: false }
|
||
)
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', val: boolean): void
|
||
}>()
|
||
|
||
const baseFormTabs = [
|
||
{ key: 'basic', label: '基本信息' },
|
||
{ key: 'status-change', label: '状态变更记录' },
|
||
{ key: 'collection', label: '采集信息' },
|
||
{ key: 'sample', label: '样本及运输信息' },
|
||
{ key: 'party', label: '客户信息' }
|
||
] as const
|
||
|
||
const formTabs = computed(() => {
|
||
const tabs: Array<{ key: string; label: string }> = [...baseFormTabs]
|
||
if (props.showAuditHistory) {
|
||
tabs.push({ key: 'audit', label: '审核记录' })
|
||
}
|
||
return tabs
|
||
})
|
||
|
||
const loading = ref(false)
|
||
const auditLoading = ref(false)
|
||
const bundle = ref<Record<string, any> | null>(null)
|
||
const auditRecords = ref<Array<Record<string, unknown>>>([])
|
||
const activeTab = ref('basic')
|
||
|
||
const sampleTypeOptions = ref<Array<{ id: string; typeName: string }>>([])
|
||
const unitOptions = ref<Array<{ id: string; unitName: string }>>([])
|
||
const siteOptions = ref<Array<{ id: string; siteName: string }>>([])
|
||
const expressCompanyOptions = ref<Array<{ id: string; expressName: string }>>([])
|
||
const regionOptions = ref<Array<{ id: string; regionName: string }>>([])
|
||
const hospitalOptions = ref<Array<{ id: string; hospitalName: string }>>([])
|
||
const collectorUserOptions = ref<Array<{ id: string; nickName: string }>>([])
|
||
const salesUserOptions = ref<Array<{ id: string; nickName: string }>>([])
|
||
|
||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||
|
||
const donation = computed(() => bundle.value?.donation as Record<string, any> | undefined)
|
||
const party = computed(() => bundle.value?.party as Record<string, any> | null)
|
||
const collection = computed(() => bundle.value?.collection as Record<string, any> | null)
|
||
const samples = computed(() => {
|
||
const rows = (bundle.value?.samples as Array<Record<string, any>>) || []
|
||
return [...rows].sort((a, b) => {
|
||
const oa = Number(a.orderNum ?? 0)
|
||
const ob = Number(b.orderNum ?? 0)
|
||
if (oa !== ob) return oa - ob
|
||
return String(a.createTime || '').localeCompare(String(b.createTime || ''))
|
||
})
|
||
})
|
||
|
||
const consentAttachmentFiles = computed(() => {
|
||
const files = donation.value?.consentAttachmentFiles
|
||
if (Array.isArray(files) && files.length) return files
|
||
const str = String(donation.value?.consentFormUrls || '').trim()
|
||
if (!str) return []
|
||
return str
|
||
.split(',')
|
||
.filter(Boolean)
|
||
.map((url: string) => ({ fileUrl: url, fileName: extractFileName(url) }))
|
||
})
|
||
|
||
const otherAttachmentFiles = computed(() => {
|
||
const files = donation.value?.attachmentFiles
|
||
if (Array.isArray(files) && files.length) return files
|
||
const str = String(donation.value?.attachmentUrls || '').trim()
|
||
if (!str) return []
|
||
return str
|
||
.split(',')
|
||
.filter(Boolean)
|
||
.map((url: string) => ({ fileUrl: url, fileName: extractFileName(url) }))
|
||
})
|
||
|
||
const dialogTitle = computed(() => '查看捐赠协议单')
|
||
|
||
const hasAnyStatusChange = computed(() => {
|
||
if (!donation.value) return false
|
||
return !!(
|
||
donation.value.effectiveTime ||
|
||
donation.value.expireTime ||
|
||
donation.value.voidTime ||
|
||
donation.value.convertTime
|
||
)
|
||
})
|
||
|
||
const collectorDisplay = computed(() => {
|
||
const fromList = props.listRow?.collectorUserName
|
||
if (fromList) return String(fromList)
|
||
const fromBundle = donation.value?.collectorUserName
|
||
if (fromBundle) return String(fromBundle)
|
||
const id = donation.value?.collectorUserId
|
||
const u = collectorUserOptions.value.find((x) => x.id === id)
|
||
return u?.nickName || dash(id)
|
||
})
|
||
|
||
const salesDisplay = computed(() => {
|
||
const fromList = props.listRow?.salesUserName
|
||
if (fromList) return String(fromList)
|
||
const fromBundle = collection.value?.salesUserName
|
||
if (fromBundle) return String(fromBundle)
|
||
const id = collection.value?.salesUserId
|
||
const u = salesUserOptions.value.find((x) => x.id === id)
|
||
return u?.nickName || dash(id)
|
||
})
|
||
|
||
const regionDisplay = computed(() => {
|
||
const fromList = props.listRow?.regionName
|
||
if (fromList) return String(fromList)
|
||
const fromBundle = collection.value?.regionName
|
||
if (fromBundle) return String(fromBundle)
|
||
const id = collection.value?.regionId
|
||
const r = regionOptions.value.find((x) => x.id === id)
|
||
return r?.regionName || dash(id)
|
||
})
|
||
|
||
const hospitalDisplay = computed(() => {
|
||
const fromList = props.listRow?.hospitalName
|
||
if (fromList) return String(fromList)
|
||
const fromBundle = collection.value?.hospitalName
|
||
if (fromBundle) return String(fromBundle)
|
||
const id = collection.value?.hospitalId
|
||
const h = hospitalOptions.value.find((x) => x.id === id)
|
||
return h?.hospitalName || dash(id)
|
||
})
|
||
|
||
function dash(val: unknown) {
|
||
if (val === null || val === undefined || val === '') return '—'
|
||
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)}`
|
||
return dash(val)
|
||
}
|
||
|
||
function formatYm(val: unknown) {
|
||
const s = String(val || '')
|
||
if (s.length === 6) return `${s.slice(0, 4)}-${s.slice(4, 6)}`
|
||
return dash(val)
|
||
}
|
||
|
||
function auditorDisplay(row: Record<string, unknown>) {
|
||
const name = String(row.auditorUserName || '').trim()
|
||
if (name) return name
|
||
return dash(row.auditorUserId)
|
||
}
|
||
|
||
function sampleTypeName(id: string) {
|
||
return sampleTypeOptions.value.find((x) => x.id === id)?.typeName || dash(id)
|
||
}
|
||
|
||
function unitName(id: string) {
|
||
return unitOptions.value.find((x) => x.id === id)?.unitName || dash(id)
|
||
}
|
||
|
||
function siteName(id: string) {
|
||
return siteOptions.value.find((x) => x.id === id)?.siteName || dash(id)
|
||
}
|
||
|
||
function expressName(id: string) {
|
||
return expressCompanyOptions.value.find((x) => x.id === id)?.expressName || dash(id)
|
||
}
|
||
|
||
function sampleReceptionCheckList(reception: Record<string, any>) {
|
||
const list = reception?.checkResults
|
||
return Array.isArray(list) ? list : []
|
||
}
|
||
|
||
function sampleReceptionAttachmentFiles(reception: Record<string, any>) {
|
||
const list = reception?.receiveAttachmentFiles
|
||
if (Array.isArray(list) && list.length) return list
|
||
const urls = String(reception?.receiveAttachments || '')
|
||
if (!urls.trim()) return []
|
||
return urls
|
||
.split(',')
|
||
.map((u, i) => {
|
||
const url = u.trim()
|
||
if (!url) return null
|
||
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
|
||
return { id: `recv_${i}`, fileUrl: url, fileName: name }
|
||
})
|
||
.filter(Boolean) as Array<Record<string, unknown>>
|
||
}
|
||
|
||
function sampleTransportExtra(transport: Record<string, any> | undefined) {
|
||
if (!transport?.transportType) return '—'
|
||
if (transport.transportType === 'kdys') {
|
||
const parts: string[] = []
|
||
const ec = expressName(String(transport.expressCompanyId || ''))
|
||
if (ec !== '—') parts.push(ec)
|
||
if (transport.waybillNo) parts.push(String(transport.waybillNo))
|
||
if (transport.expressFee != null && transport.expressFee !== '') {
|
||
parts.push('¥' + formatMoneyDisplay(transport.expressFee))
|
||
}
|
||
return parts.length ? parts.join(' · ') : '—'
|
||
}
|
||
if (transport.transportType === 'ryxd') {
|
||
const parts: string[] = []
|
||
if (transport.carrierName) parts.push(String(transport.carrierName))
|
||
if (transport.carrierPhone) parts.push(String(transport.carrierPhone))
|
||
return parts.length ? parts.join(' · ') : '—'
|
||
}
|
||
return '—'
|
||
}
|
||
|
||
function extractFileName(url: string): string {
|
||
if (!url) return ''
|
||
const parts = url.split('/')
|
||
return parts[parts.length - 1] || url
|
||
}
|
||
|
||
function fullFileUrl(suffix: string) {
|
||
if (!suffix) return ''
|
||
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/'))
|
||
return suffix
|
||
return `${minioBaseUrl}/${suffix}`
|
||
}
|
||
|
||
function isVideoFile(file: { fileType?: string; fileName?: string }) {
|
||
const t = (file.fileType || '').toLowerCase()
|
||
if (t.startsWith('video/')) return true
|
||
return /\.(mp4|webm|ogg|mov)$/i.test(file.fileName || '')
|
||
}
|
||
|
||
const previewVisible = ref(false)
|
||
const previewUrl = ref('')
|
||
const previewType = ref<'image' | 'video'>('image')
|
||
|
||
function openPreview(file: Record<string, any>) {
|
||
if (!file.fileUrl) return
|
||
previewUrl.value = fullFileUrl(file.fileUrl)
|
||
previewType.value = isVideoFile(file) ? 'video' : 'image'
|
||
previewVisible.value = true
|
||
}
|
||
|
||
function downloadFile(file: Record<string, any>) {
|
||
downloadAttachmentFile(file, { fullFileUrl: fullFileUrl })
|
||
}
|
||
|
||
async function loadLookups(data: Record<string, any>) {
|
||
const [stRes, uRes, ecRes, siteRes] = await Promise.all([
|
||
fetchCellSampleTypeList({ page: 1, pageSize: 100 }),
|
||
fetchItemUnitList({ page: 1, pageSize: 100 }),
|
||
fetchExpressCompanyList({ page: 1, pageSize: 100 }),
|
||
fetchSiteList({ page: 1, pageSize: 100 })
|
||
])
|
||
sampleTypeOptions.value = (stRes.list || []) as any
|
||
unitOptions.value = (uRes.list || []) as any
|
||
expressCompanyOptions.value = ecRes.list || []
|
||
siteOptions.value = siteRes.list || []
|
||
|
||
const coll = data.collection as Record<string, any> | null
|
||
const don = data.donation as Record<string, any> | undefined
|
||
const tasks: Promise<void>[] = []
|
||
|
||
if (coll?.regionId) {
|
||
tasks.push(
|
||
fetchCollectionRegionList({ page: 1, pageSize: 100 }).then((res) => {
|
||
regionOptions.value = res.list || []
|
||
})
|
||
)
|
||
tasks.push(
|
||
fetchHospitalList(String(coll.regionId), { page: 1, pageSize: 100 }).then((res) => {
|
||
hospitalOptions.value = res.list || []
|
||
})
|
||
)
|
||
}
|
||
if (don?.collectorUserId) {
|
||
tasks.push(
|
||
fetchUserList({ page: 1, pageSize: 50, status: '1' }).then((res) => {
|
||
collectorUserOptions.value = res.list || []
|
||
})
|
||
)
|
||
}
|
||
if (coll?.salesUserId) {
|
||
tasks.push(
|
||
fetchUserList({ page: 1, pageSize: 50, status: '1' }).then((res) => {
|
||
salesUserOptions.value = res.list || []
|
||
})
|
||
)
|
||
}
|
||
await Promise.all(tasks)
|
||
}
|
||
|
||
async function loadAuditRecords() {
|
||
if (!props.showAuditHistory || !props.donationId) {
|
||
auditRecords.value = []
|
||
return
|
||
}
|
||
auditLoading.value = true
|
||
try {
|
||
const no =
|
||
String(donation.value?.donationNo || props.listRow?.donationNo || '').trim() || undefined
|
||
const res = await fetchResearchDonationAuditList({
|
||
page: 1,
|
||
pageSize: 100,
|
||
keyword: no
|
||
})
|
||
auditRecords.value = res.list || []
|
||
} catch {
|
||
auditRecords.value = []
|
||
} finally {
|
||
auditLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadDetail() {
|
||
if (!props.donationId) return
|
||
loading.value = true
|
||
bundle.value = null
|
||
auditRecords.value = []
|
||
try {
|
||
const data = await fetchResearchDonationDetail(props.donationId)
|
||
bundle.value = data as Record<string, any>
|
||
await loadLookups(data as Record<string, any>)
|
||
await loadAuditRecords()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function scrollToSection(key: string) {
|
||
if (loading.value && !bundle.value) return
|
||
activeTab.value = key
|
||
nextTick(() => {
|
||
document
|
||
.getElementById(`view-section-${key}`)
|
||
?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||
})
|
||
}
|
||
|
||
function handleClosed() {
|
||
bundle.value = null
|
||
auditRecords.value = []
|
||
activeTab.value = 'basic'
|
||
loading.value = false
|
||
auditLoading.value = false
|
||
}
|
||
|
||
watch(
|
||
() => [props.modelValue, props.donationId] as const,
|
||
([visible, id]) => {
|
||
if (visible && id) loadDetail()
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.donation-view-body {
|
||
overflow: visible;
|
||
|
||
&.is-detail-loading {
|
||
min-height: min(680px, calc(100vh - 220px));
|
||
}
|
||
}
|
||
|
||
/* ---- Sticky Tab 导航 ---- */
|
||
.detail-tabs {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1000;
|
||
display: flex;
|
||
gap: 4px;
|
||
background: #f0fdf4;
|
||
padding: 4px;
|
||
border-radius: 8px;
|
||
margin-bottom: 16px;
|
||
width: fit-content;
|
||
isolation: isolate;
|
||
box-shadow: 0 1px 0 #f0fdf4;
|
||
}
|
||
|
||
.detail-tab {
|
||
padding: 10px 20px;
|
||
cursor: pointer;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
transition: all 0.3s;
|
||
|
||
&.active {
|
||
background: #fff;
|
||
color: #065f46;
|
||
font-weight: 500;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
&:hover:not(.active) {
|
||
color: #065f46;
|
||
}
|
||
}
|
||
|
||
/* ---- KPI 摘要条(与采集协议单查看弹窗一致:一行四列 + 薄荷绿通栏) ---- */
|
||
.kpi-strip {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 0;
|
||
margin-bottom: 24px;
|
||
padding: 6px;
|
||
background: linear-gradient(135deg, #e8f4ee 0%, #f8fafc 52%, #e8f4ee 100%);
|
||
border: 1px solid #a8d4c4;
|
||
border-radius: 12px;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||
}
|
||
|
||
.kpi-card {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
padding: 14px 18px;
|
||
background: transparent;
|
||
border-radius: 8px;
|
||
transition: background 0.2s;
|
||
|
||
&:not(:last-child) {
|
||
border-right: 1px solid rgba(168, 212, 196, 0.75);
|
||
}
|
||
|
||
&:hover {
|
||
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 #a8d4c4;
|
||
color: #065f46;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.kpi-card__main {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.kpi-label {
|
||
margin-bottom: 4px;
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: #64748b;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #0f172a;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
|
||
&--emphasis {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #047857;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
}
|
||
|
||
/* ---- Section Card ---- */
|
||
.section-card {
|
||
border-radius: 12px;
|
||
padding: 22px 24px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||
transition:
|
||
transform 0.2s,
|
||
box-shadow 0.2s;
|
||
position: relative;
|
||
|
||
&:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||
}
|
||
|
||
&--white {
|
||
background: #fff;
|
||
}
|
||
|
||
&--green {
|
||
background: #f0fdf4;
|
||
}
|
||
}
|
||
|
||
.section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 18px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
|
||
&::before {
|
||
content: '';
|
||
width: 3px;
|
||
height: 18px;
|
||
background: #047857;
|
||
border-radius: 2px;
|
||
margin-right: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
h3 {
|
||
margin: 0;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
|
||
/* ---- Data Grid ---- */
|
||
.data-grid {
|
||
display: grid;
|
||
gap: 14px 20px;
|
||
|
||
&--2 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
&--3 {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
|
||
&--4 {
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
.data-item {
|
||
min-width: 0;
|
||
|
||
&--full {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.data-label {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.data-value {
|
||
font-size: 14px;
|
||
color: #1e293b;
|
||
font-weight: 500;
|
||
word-break: break-word;
|
||
|
||
&--muted {
|
||
color: #64748b;
|
||
font-weight: 400;
|
||
}
|
||
}
|
||
|
||
/* ---- Attachment / Remark Row ---- */
|
||
.attachment-row {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
|
||
.data-label {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
}
|
||
}
|
||
|
||
.remark-row {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
|
||
/* ---- Status Change Section ---- */
|
||
.status-change-section {
|
||
margin-top: 14px;
|
||
padding-top: 14px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
|
||
.status-change-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #64748b;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* ---- Client Sub Cards (🤱👨👶) ---- */
|
||
.client-sub-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.client-sub-card {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
padding: 18px 20px;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
border: 1px solid rgba(0, 0, 0, 0.03);
|
||
transition:
|
||
transform 0.2s,
|
||
box-shadow 0.2s;
|
||
|
||
&:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.07);
|
||
}
|
||
}
|
||
|
||
.sub-card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
|
||
h4 {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
}
|
||
}
|
||
|
||
.sub-card-icon {
|
||
font-size: 22px;
|
||
line-height: 1;
|
||
}
|
||
|
||
.sub-card-full-row {
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
/* ---- Sub View Table ---- */
|
||
.sub-view-table {
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* ---- Sample Cards ---- */
|
||
.sample-cards {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
.sample-card {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||
overflow: hidden;
|
||
transition:
|
||
transform 0.2s,
|
||
box-shadow 0.2s;
|
||
|
||
&:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||
}
|
||
}
|
||
|
||
.sample-card__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 18px;
|
||
background: #f8fafc;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.sample-card__title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.sample-card__seq {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: 6px;
|
||
background: #047857;
|
||
color: #fff;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sample-card__body {
|
||
padding: 16px 18px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
.sample-card__section {
|
||
&--transport,
|
||
&--reception {
|
||
margin-top: 2px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
}
|
||
|
||
:deep(.sample-card__section--qc) {
|
||
margin-top: 2px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
|
||
.sample-reception-empty {
|
||
font-size: 13px;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.check-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px 20px;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.sample-card__attachments {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.sample-card__section-label {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #94a3b8;
|
||
margin-bottom: 10px;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
.data-value--code {
|
||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', monospace;
|
||
font-size: 13px;
|
||
color: #475569;
|
||
}
|
||
|
||
/* ---- Preview ---- */
|
||
.preview-container {
|
||
text-align: center;
|
||
}
|
||
|
||
/* ---- Responsive ---- */
|
||
@media (max-width: 1200px) {
|
||
.kpi-strip {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.kpi-strip .kpi-card {
|
||
border-right: none !important;
|
||
border-bottom: 1px solid rgba(168, 212, 196, 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));
|
||
}
|
||
}
|
||
|
||
@media (max-width: 900px) {
|
||
.kpi-strip {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.kpi-strip .kpi-card:nth-last-child(-n + 2) {
|
||
border-bottom: 1px solid rgba(168, 212, 196, 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));
|
||
}
|
||
}
|
||
</style>
|