Wire finance transfer list, protocol view v2 readonly payment, and mark design/plan as P0 implemented. Co-authored-by: Cursor <cursoragent@cursor.com>
3312 lines
106 KiB
Vue
3312 lines
106 KiB
Vue
<!-- 采集协议单 — 只读查看(字段与新增/编辑弹窗一致) -->
|
||
<template>
|
||
<el-dialog
|
||
:model-value="modelValue"
|
||
:title="dialogTitle"
|
||
width="1600px"
|
||
class="dialog-form-layout protocol-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="protocol-view-body"
|
||
:class="{ 'is-detail-loading': detailLoading && !bundle }"
|
||
v-loading="detailLoading && !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': detailLoading && !bundle }"
|
||
@click="scrollToSection(tab.key)"
|
||
>
|
||
{{ tab.label }}
|
||
</div>
|
||
</div>
|
||
|
||
<template v-if="bundle">
|
||
<div>
|
||
<!-- 基本信息 -->
|
||
<div id="view-section-basic" class="section-card section-card--white">
|
||
<div class="section-title section-title--banner"><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="protocolStatusTagType(String(protocol?.protocolStatus || ''))"
|
||
size="small"
|
||
>{{
|
||
protocolStatusDisplayLabel(
|
||
String(protocol?.protocolStatus || ''),
|
||
String(protocol?.voidType || '')
|
||
)
|
||
}}</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
|
||
>
|
||
</div>
|
||
<!-- 协议状态变更记录(非储存证查看:仍在基本信息内) -->
|
||
<div v-if="!showStorageCert && hasAnyStatusChange" class="status-change-section">
|
||
<div class="status-change-title">状态变更记录</div>
|
||
<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
|
||
>
|
||
</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">{{
|
||
voidTypeLabel(String(protocol?.voidType || '')) || '—'
|
||
}}</div></div
|
||
>
|
||
<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
|
||
v-if="
|
||
protocolAttachments.length ||
|
||
consentAttachments.length ||
|
||
healthSurveyAttachments.length ||
|
||
(protocol?.remark && String(protocol.remark).trim())
|
||
"
|
||
class="protocol-basic-attachments-block"
|
||
>
|
||
<div class="data-item protocol-attachments-item">
|
||
<div class="data-label">协议附件</div>
|
||
<div class="data-value protocol-attachments-value">
|
||
<ProtocolAttachmentReadonly
|
||
:files="protocolAttachments"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="data-item protocol-attachments-item">
|
||
<div class="data-label">知情同意书</div>
|
||
<div class="data-value protocol-attachments-value">
|
||
<ProtocolAttachmentReadonly
|
||
:files="consentAttachments"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="data-item protocol-attachments-item">
|
||
<div class="data-label">孕妇健康调查表</div>
|
||
<div class="data-value protocol-attachments-value">
|
||
<ProtocolAttachmentReadonly
|
||
:files="healthSurveyAttachments"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div v-if="protocol?.remark && String(protocol.remark).trim()" class="data-item">
|
||
<div class="data-label">备注</div>
|
||
<div class="data-value data-value--muted">{{ dash(protocol?.remark) }}</div>
|
||
</div>
|
||
</div>
|
||
<!-- 储存证:储存证附件、制作状态、制作日期同一行(在协议附件行下方) -->
|
||
<div v-if="showStorageCert" class="data-grid data-grid--3 storage-cert-info-row">
|
||
<div class="data-item storage-cert-attachments-item">
|
||
<div class="data-label">储存证附件</div>
|
||
<div class="data-value storage-cert-attachments-value">
|
||
<ProtocolAttachmentReadonly
|
||
:files="storageCertAttachments"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<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>
|
||
</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">{{
|
||
voidTypeLabel(String(protocol?.voidType || '')) || '—'
|
||
}}</div></div
|
||
>
|
||
<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-collection" class="section-card section-card--white">
|
||
<div class="section-title section-title--banner"><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(collectionInfo?.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(collectionInfo?.sampleCollectTime) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母血采集时间</div
|
||
><div class="data-value">{{
|
||
dash(collectionInfo?.maternalBloodCollectTime)
|
||
}}</div></div
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 样本及运输(字典异步补齐,不阻塞整弹窗) -->
|
||
<div
|
||
id="view-section-sample"
|
||
v-loading="lookupLoading"
|
||
class="section-card section-card--white"
|
||
>
|
||
<div class="section-title section-title--banner"><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">
|
||
<SampleCodeWithSpecialDisplay
|
||
:sample-code="row.sampleCode"
|
||
:is-special="!!row.isSpecial"
|
||
:special-remark="row.specialRemark"
|
||
monospace
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">原价格(元)</div>
|
||
<div class="data-value data-value--price">{{
|
||
row.originalPrice ? `¥${row.originalPrice}` : '—'
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">优惠价格(元)</div>
|
||
<div class="data-value data-value--price">{{
|
||
row.price ? `¥${row.price}` : '—'
|
||
}}</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">
|
||
<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">
|
||
<SampleReceptionDualReadonlyPanel
|
||
:qc-reception="row.sampleReception"
|
||
:production-reception="
|
||
row.productionReception || row.sampleReception?.productionReception
|
||
"
|
||
source-type="cell_storage"
|
||
:sample-type-name="row.sampleTypeName"
|
||
/>
|
||
<div
|
||
v-if="
|
||
row.sampleReception &&
|
||
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>
|
||
</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 section-title--banner"><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">{{
|
||
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
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母亲身份证号</div
|
||
><div class="data-value">{{ dash(partyCustomer?.motherIdCard) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母亲出生年月</div
|
||
><div class="data-value">{{ formatYm(partyCustomer?.motherBirthYm) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">母亲民族</div
|
||
><div class="data-value">{{ dash(partyCustomer?.motherEthnic) }}</div></div
|
||
>
|
||
</div>
|
||
<div class="sub-card-full-row"
|
||
><div class="data-label">母亲邮箱</div
|
||
><div class="data-value">{{ dash(partyCustomer?.motherEmail) }}</div></div
|
||
>
|
||
<div class="sub-card-full-row"
|
||
><div class="data-label">母亲家庭住址</div
|
||
><div class="data-value data-value--muted">{{
|
||
dash(partyCustomer?.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(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
|
||
>
|
||
</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(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?.expectedDeliveryDate)
|
||
}}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿分娩时间</div
|
||
><div class="data-value">{{
|
||
dash(partyCustomer?.newbornDeliveryTime)
|
||
}}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿性别</div
|
||
><div class="data-value">{{
|
||
newbornSexLabel(String(partyCustomer?.newbornSex || ''))
|
||
}}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">分娩方式</div
|
||
><div class="data-value">{{
|
||
deliveryModeLabel(String(partyCustomer?.deliveryMode || ''))
|
||
}}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">胎次</div
|
||
><div class="data-value">{{ dash(partyCustomer?.birthOrder) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿体重(克)</div
|
||
><div class="data-value">{{ dash(partyCustomer?.newbornWeightGram) }}</div></div
|
||
>
|
||
<div class="data-item"
|
||
><div class="data-label">新生儿评分</div
|
||
><div class="data-value">{{ dash(partyCustomer?.apgar1min) }}</div></div
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新生儿保险信息(满月待办查看:客户信息下方独立区域) -->
|
||
<div
|
||
v-if="showFullmoonInsurance"
|
||
id="view-section-fullmoon-insurance"
|
||
class="section-card section-card--white"
|
||
>
|
||
<div class="section-title section-title--banner"><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">{{
|
||
fmtMoney(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-payment" class="section-card">
|
||
<div class="section-title section-title--banner"><h3>缴费信息</h3></div>
|
||
|
||
<!-- v2: 按样本缴费只读 -->
|
||
<template v-if="paymentModelVersion >= 2">
|
||
<el-alert
|
||
v-if="presignDepositHint"
|
||
type="info"
|
||
:closable="false"
|
||
show-icon
|
||
style="margin-bottom: 12px"
|
||
:title="presignDepositHintText"
|
||
/>
|
||
<div class="payment-summary">
|
||
<div class="payment-metric">
|
||
<div class="pm-label">协议总金额(元)</div>
|
||
<div class="pm-value pm-value--amount">{{
|
||
fmtMoney(protocol?.protocolTotalAmount)
|
||
}}</div>
|
||
</div>
|
||
<div class="payment-metric">
|
||
<div class="pm-label">样本合同合计(元)</div>
|
||
<div class="pm-value pm-value--amount">{{ fmtMoney(sampleContractSum) }}</div>
|
||
</div>
|
||
<div class="payment-metric">
|
||
<div class="pm-label">已确认收款(元)</div>
|
||
<div class="pm-value pm-value--amount pm-value--received">{{
|
||
fmtMoney(v2ReceivedAmount)
|
||
}}</div>
|
||
</div>
|
||
</div>
|
||
<div class="section-subtitle">按样本缴费方案</div>
|
||
<div v-if="samplePayments.length" class="sample-pay-readonly">
|
||
<div
|
||
v-for="(sp, sIdx) in samplePayments"
|
||
:key="sp.id || sIdx"
|
||
class="sample-pay-readonly__card"
|
||
>
|
||
<div class="sample-pay-readonly__head">
|
||
<span
|
||
>{{ sp.sampleCode || '未编码' }} ·
|
||
{{ payModeLabel(String(sp.payMode || '')) }}</span
|
||
>
|
||
<span class="sample-pay-readonly__amt">{{ fmtMoney(sp.contractAmount) }}</span>
|
||
</div>
|
||
<div
|
||
v-for="(sch, schIdx) in sp.schedules || []"
|
||
:key="sch.id || schIdx"
|
||
class="sample-pay-readonly__sch"
|
||
>
|
||
<div class="sample-pay-readonly__sch-hd">
|
||
第 {{ sch.scheduleNo || schIdx + 1 }} 期 · 应缴
|
||
{{ fmtMoney(sch.dueAmount) }}
|
||
<template v-if="sch.dueDate"> · {{ formatYmd(sch.dueDate) }}</template>
|
||
· 已缴 {{ fmtMoney(sch.paidAmount) }} · 欠缴
|
||
{{ fmtMoney(sch.arrearsAmount) }}
|
||
</div>
|
||
<div
|
||
v-for="(tr, tIdx) in sch.transfers || []"
|
||
:key="tr.id || tIdx"
|
||
class="sample-pay-readonly__tr"
|
||
>
|
||
<el-tag :type="payStatusTagType(String(tr.payStatus || ''))" size="small">
|
||
{{ payStatusLabel(String(tr.payStatus || '')) }}
|
||
</el-tag>
|
||
<span>{{ transferTypeLabel(String(tr.transferType || '')) || '—' }}</span>
|
||
<span>{{ fmtMoney(tr.transferAmount) }}</span>
|
||
<span>{{ formatDisplayOrDash(tr.transferTime) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无按样本缴费方案" :image-size="40" />
|
||
<div class="section-subtitle">退费登记</div>
|
||
<el-table
|
||
v-if="refundRecords.length"
|
||
:data="refundRecords"
|
||
class="list-table inner-table"
|
||
stripe
|
||
>
|
||
<el-table-column
|
||
prop="refundAmount"
|
||
label="退费金额"
|
||
min-width="120"
|
||
align="center"
|
||
>
|
||
<template #default="{ row }">{{ fmtMoney(row.refundAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="refundTime" label="退费时间" min-width="160" align="center">
|
||
<template #default="{ row }">{{ formatDisplayOrDash(row.refundTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="refundRemark"
|
||
label="备注"
|
||
min-width="180"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
/>
|
||
</el-table>
|
||
<el-empty v-else description="暂无退费登记" :image-size="40" />
|
||
</template>
|
||
|
||
<!-- v1: 旧整单缴费只读 -->
|
||
<template v-else>
|
||
<!-- 缴费主表指标卡 -->
|
||
<div class="payment-summary">
|
||
<div class="payment-metric"
|
||
><div class="pm-label">缴费模式</div
|
||
><div class="pm-value">{{
|
||
payModeLabel(String(payment?.payMode || ''))
|
||
}}</div></div
|
||
>
|
||
<div class="payment-metric"
|
||
><div class="pm-label">合同金额(元)</div
|
||
><div class="pm-value pm-value--amount">{{
|
||
fmtMoney(payment?.contractAmount)
|
||
}}</div></div
|
||
>
|
||
<div class="payment-metric"
|
||
><div class="pm-label">已收金额(元)</div
|
||
><div class="pm-value pm-value--amount pm-value--received">{{
|
||
fmtMoney(receivedAmount)
|
||
}}</div></div
|
||
>
|
||
<template v-if="payment?.payMode === 'dj'">
|
||
<div class="payment-metric"
|
||
><div class="pm-label">趸交截止时间</div
|
||
><div class="pm-value">{{ formatYmd(payment?.lumpSumDeadline) }}</div></div
|
||
>
|
||
</template>
|
||
<template v-if="isInstallmentPayMode(String(payment?.payMode || ''))">
|
||
<div class="payment-metric"
|
||
><div class="pm-label">分期期数</div
|
||
><div class="pm-value">{{ dash(payment?.installmentCount) }}</div></div
|
||
>
|
||
<div class="payment-metric"
|
||
><div class="pm-label">每期间隔(月)</div
|
||
><div class="pm-value">{{ dash(payment?.installmentIntervalMonths) }}</div></div
|
||
>
|
||
<div class="payment-metric"
|
||
><div class="pm-label">分期开始时间</div
|
||
><div class="pm-value">{{ formatYmd(payment?.installmentStartDate) }}</div></div
|
||
>
|
||
<div class="payment-metric"
|
||
><div class="pm-label">应缴费提前(天)</div
|
||
><div class="pm-value">{{
|
||
dash(payment?.installmentAdvanceDays ?? 30)
|
||
}}</div></div
|
||
>
|
||
<div class="payment-metric"
|
||
><div class="pm-label">分期金额(元)</div
|
||
><div class="pm-value">{{
|
||
formatInstallmentAmounts(payment?.installmentRatios)
|
||
}}</div></div
|
||
>
|
||
</template>
|
||
</div>
|
||
<!-- 缴费明细 -->
|
||
<div class="section-subtitle">缴费明细</div>
|
||
<div v-if="paymentDetails.length" class="payment-details">
|
||
<div
|
||
v-for="(detail, dIdx) in paymentDetails"
|
||
:key="detail.id || dIdx"
|
||
class="payment-detail-card"
|
||
:class="{
|
||
'payment-detail-card--refunded': String(detail.payStatus) === 'ytf',
|
||
'payment-detail-card--refund-pending': String(detail.payStatus) === 'tfqrz'
|
||
}"
|
||
>
|
||
<!-- Header -->
|
||
<div class="pdc-header">
|
||
<div
|
||
class="pdc-header-left"
|
||
:class="payDetailColorClass(String(detail.payStatus || ''))"
|
||
>
|
||
缴费明细 #{{ dIdx + 1 }} ·
|
||
<span
|
||
class="pdc-header-amount"
|
||
:class="payDetailColorClass(String(detail.payStatus || ''))"
|
||
>
|
||
{{ fmtMoney(detail.dueAmount) }}
|
||
</span>
|
||
</div>
|
||
<el-tag :type="payStatusTagType(String(detail.payStatus || ''))" size="small">
|
||
{{ payStatusLabel(String(detail.payStatus || '')) }}
|
||
</el-tag>
|
||
</div>
|
||
|
||
<!-- Info row -->
|
||
<div class="pdc-info-row">
|
||
<div class="pdc-info-item">
|
||
<span class="info-label">应缴费金额(元)</span>
|
||
<span class="info-value">{{ fmtMoney(detail.dueAmount) }}</span>
|
||
</div>
|
||
<div class="pdc-info-item">
|
||
<span class="info-label">转账方式</span>
|
||
<span class="info-value">{{
|
||
transferTypeLabel(String(detail.transferType || ''))
|
||
}}</span>
|
||
</div>
|
||
<div class="pdc-info-item">
|
||
<span class="info-label">转账金额(元)</span>
|
||
<span class="info-value">{{ fmtMoney(detail.transferAmount) }}</span>
|
||
</div>
|
||
<div class="pdc-info-item">
|
||
<span class="info-label">转账时间</span>
|
||
<span class="info-value">{{ dash(detail.transferTime) }}</span>
|
||
</div>
|
||
<div
|
||
v-if="isInstallmentPayMode(String(payment?.payMode || ''))"
|
||
class="pdc-info-item"
|
||
>
|
||
<span class="info-label">应缴费时间</span>
|
||
<span class="info-value">{{ formatYmd(detail.installmentDueDate) }}</span>
|
||
</div>
|
||
<div
|
||
v-if="isInstallmentPayMode(String(payment?.payMode || ''))"
|
||
class="pdc-info-item"
|
||
>
|
||
<span class="info-label">分期期序号</span>
|
||
<span class="info-value">{{ dash(detail.installmentNo) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Transfer attachments -->
|
||
<div v-if="detailAttachments(detail).length" class="pdc-attachments-row">
|
||
<div class="data-label">转账凭证</div>
|
||
<ProtocolAttachmentReadonly
|
||
:files="detailAttachments(detail)"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Mini flow: 正向横向进度条 + 下方详情卡片 -->
|
||
<template
|
||
v-for="(flowSteps, flowIdx) in [buildMiniFlowSteps(detail)]"
|
||
:key="`${detail.id || dIdx}-flow-${flowIdx}`"
|
||
>
|
||
<div v-if="flowSteps.length" class="flow-section">
|
||
<div class="mini-flow">
|
||
<template v-for="(step, sIdx) in flowSteps" :key="`c-${step.key}`">
|
||
<div
|
||
v-if="step.connectorToNext"
|
||
class="mini-flow-connector"
|
||
:class="step.connectorToNext"
|
||
:style="miniFlowConnectorStyle(flowSteps.length, sIdx)"
|
||
/>
|
||
</template>
|
||
<div v-for="step in flowSteps" :key="step.key" class="mini-flow-step">
|
||
<div class="mini-flow-dot" :class="step.dotClass">
|
||
{{ step.dotIcon }}
|
||
</div>
|
||
<div class="mini-flow-label" :class="step.labelClass">
|
||
{{ step.label }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="flow-detail-cards">
|
||
<template v-for="step in flowSteps" :key="`d-${step.key}`">
|
||
<div
|
||
v-if="step.hasDetailCard"
|
||
class="flow-detail-card"
|
||
:class="'flow-detail-card--' + step.dotClass"
|
||
>
|
||
<div class="flow-detail-card__header">
|
||
<span class="flow-detail-card__dot" :class="step.dotClass">{{
|
||
step.dotIcon
|
||
}}</span>
|
||
<span class="flow-detail-card__title" :class="step.labelClass">{{
|
||
step.label
|
||
}}</span>
|
||
</div>
|
||
<div class="flow-detail-card__body">
|
||
<template v-if="step.key === 'pay'">
|
||
<div class="flow-detail-card__kv">
|
||
<span class="flow-detail-card__label">确认状态</span>
|
||
<span class="flow-detail-card__value"
|
||
><el-tag type="success" size="small">已缴费</el-tag></span
|
||
>
|
||
</div>
|
||
<div v-if="detail.transferTime" class="flow-detail-card__kv">
|
||
<span class="flow-detail-card__label">缴费时间</span>
|
||
<span class="flow-detail-card__value">{{
|
||
detail.transferTime
|
||
}}</span>
|
||
</div>
|
||
<div
|
||
v-if="detailAttachments(detail).length"
|
||
class="flow-detail-card__kv"
|
||
>
|
||
<span class="flow-detail-card__label">转账凭证</span>
|
||
<ProtocolAttachmentReadonly
|
||
:files="detailAttachments(detail)"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="step.key === 'receipt-finance'">
|
||
<div class="flow-detail-card__kv">
|
||
<span class="flow-detail-card__label">确认状态</span>
|
||
<span class="flow-detail-card__value">
|
||
<el-tag
|
||
v-if="isReceiptFinanceConfirmed(detail)"
|
||
type="success"
|
||
size="small"
|
||
>已确认</el-tag
|
||
>
|
||
<el-tag v-else type="warning" size="small">待确认</el-tag>
|
||
</span>
|
||
</div>
|
||
<div
|
||
v-if="isReceiptFinanceConfirmed(detail) && detail.updateTime"
|
||
class="flow-detail-card__kv"
|
||
>
|
||
<span class="flow-detail-card__label">确认时间</span>
|
||
<span class="flow-detail-card__value">{{
|
||
formatDisplayOrDash(detail.updateTime)
|
||
}}</span>
|
||
</div>
|
||
<div
|
||
v-if="
|
||
isReceiptFinanceConfirmed(detail) &&
|
||
detail.financeRemark &&
|
||
String(detail.financeRemark).trim()
|
||
"
|
||
class="flow-detail-card__kv"
|
||
>
|
||
<span class="flow-detail-card__label">财务备注</span>
|
||
<span class="flow-detail-card__value">{{
|
||
detail.financeRemark
|
||
}}</span>
|
||
</div>
|
||
<div
|
||
v-if="
|
||
isReceiptFinanceConfirmed(detail) &&
|
||
financeConfirmAttachmentFiles(detail).length
|
||
"
|
||
class="flow-detail-card__kv"
|
||
>
|
||
<span class="flow-detail-card__label">财务附件</span>
|
||
<ProtocolAttachmentReadonly
|
||
:files="financeConfirmAttachmentFiles(detail)"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="step.key === 'refund-apply'">
|
||
<div class="flow-detail-card__kv">
|
||
<span class="flow-detail-card__label">退费金额(元)</span>
|
||
<span
|
||
class="flow-detail-card__value"
|
||
:class="payDetailColorClass(String(detail.payStatus || ''))"
|
||
>{{ fmtMoney(detail.refundAmount) }}</span
|
||
>
|
||
</div>
|
||
<div
|
||
v-if="detail.refundRemark && String(detail.refundRemark).trim()"
|
||
class="flow-detail-card__kv"
|
||
>
|
||
<span class="flow-detail-card__label">退费备注</span>
|
||
<span class="flow-detail-card__value">{{
|
||
detail.refundRemark
|
||
}}</span>
|
||
</div>
|
||
<div
|
||
v-if="refundAttachmentFiles(detail).length"
|
||
class="flow-detail-card__kv"
|
||
>
|
||
<span class="flow-detail-card__label">退费附件</span>
|
||
<ProtocolAttachmentReadonly
|
||
:files="refundAttachmentFiles(detail)"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<template v-else-if="step.key === 'refund-finance'">
|
||
<div class="flow-detail-card__kv">
|
||
<span class="flow-detail-card__label">确认状态</span>
|
||
<span class="flow-detail-card__value"
|
||
><el-tag type="danger" size="small">已退费</el-tag></span
|
||
>
|
||
</div>
|
||
<div v-if="detail.updateTime" class="flow-detail-card__kv">
|
||
<span class="flow-detail-card__label">确认时间</span>
|
||
<span class="flow-detail-card__value">{{
|
||
formatDisplayOrDash(detail.updateTime)
|
||
}}</span>
|
||
</div>
|
||
<div
|
||
v-if="
|
||
detail.refundFinanceRemark &&
|
||
String(detail.refundFinanceRemark).trim()
|
||
"
|
||
class="flow-detail-card__kv"
|
||
>
|
||
<span class="flow-detail-card__label">财务备注</span>
|
||
<span class="flow-detail-card__value">{{
|
||
detail.refundFinanceRemark
|
||
}}</span>
|
||
</div>
|
||
<div
|
||
v-if="refundFinanceConfirmAttachmentFiles(detail).length"
|
||
class="flow-detail-card__kv"
|
||
>
|
||
<span class="flow-detail-card__label">财务附件</span>
|
||
<ProtocolAttachmentReadonly
|
||
:files="refundFinanceConfirmAttachmentFiles(detail)"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无缴费明细" :image-size="48" />
|
||
|
||
<!-- 缴费总览 Timeline -->
|
||
<template
|
||
v-for="(overviewSteps, overviewIdx) in [buildOverviewTimelineSteps()]"
|
||
:key="`overview-timeline-${overviewIdx}`"
|
||
>
|
||
<div v-if="overviewSteps.length > 1" class="payment-timeline">
|
||
<div class="timeline-title">缴费总览</div>
|
||
<div class="timeline">
|
||
<!-- Connector lines -->
|
||
<template v-for="(step, sIdx) in overviewSteps" :key="`tc-${sIdx}`">
|
||
<div
|
||
v-if="sIdx < overviewSteps.length - 1"
|
||
class="timeline-connector"
|
||
:class="step.connectorToNext || ''"
|
||
:style="timelineConnectorStyle(overviewSteps.length, sIdx)"
|
||
/>
|
||
</template>
|
||
<div
|
||
v-for="(step, sIdx) in overviewSteps"
|
||
:key="`ts-${sIdx}`"
|
||
class="timeline-step"
|
||
>
|
||
<div class="timeline-dot" :class="step.dotClass">
|
||
{{ step.dotIcon }}
|
||
</div>
|
||
<div class="timeline-label" :class="step.labelClass">
|
||
{{ step.label }}
|
||
<br />
|
||
<span style="font-size: 10px; font-weight: 400">{{ step.subLabel }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 审核记录 -->
|
||
<div
|
||
v-if="showAuditHistory"
|
||
id="view-section-audit"
|
||
class="section-card section-card--white"
|
||
>
|
||
<div class="section-title section-title--banner"><h3>审核记录</h3></div>
|
||
<div class="list-body list-wrap view-inner-table-wrap">
|
||
<el-table
|
||
v-loading="auditLoading"
|
||
class="list-table inner-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"
|
||
>
|
||
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
|
||
</el-table-column>
|
||
<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>
|
||
</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
|
||
append-to-body
|
||
destroy-on-close
|
||
>
|
||
<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 { formatDisplayOrDash } from '@/utils/datetime-display'
|
||
import { computed, nextTick, ref, watch } from 'vue'
|
||
import { Close } from '@element-plus/icons-vue'
|
||
import ProtocolAttachmentReadonly from './ProtocolAttachmentReadonly.vue'
|
||
import SampleQcInfoGroupedPanel from '@/components/business/sample-qc-info-grouped-panel/index.vue'
|
||
import SampleReceptionDualReadonlyPanel from '@/components/business/sample-reception-dual-readonly-panel/index.vue'
|
||
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/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 {
|
||
fetchCollectionProtocolAuditList,
|
||
fetchCollectionProtocolDetail,
|
||
type CollectionProtocolRow
|
||
} from '@/api/collection-protocol'
|
||
import {
|
||
fetchCellSampleTypeList,
|
||
fetchItemUnitList,
|
||
fetchExpressCompanyList,
|
||
fetchSiteList,
|
||
fetchCollectionRegionList,
|
||
fetchHospitalList,
|
||
fetchUserList
|
||
} from '@/api/admin'
|
||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||
import {
|
||
protocolStatusDisplayLabel,
|
||
protocolStatusTagType,
|
||
auditStatusLabel,
|
||
payModeLabel,
|
||
newbornSexLabel,
|
||
deliveryModeLabel,
|
||
sampleStatusLabel,
|
||
sampleStatusTagType,
|
||
transportTypeLabel,
|
||
payStatusLabel,
|
||
payStatusTagType
|
||
} from '@/utils/status-tag-registry'
|
||
import { loadTransferTypeDict, transferTypeLabel } from '@/utils/protocol-payment-labels'
|
||
import { loadVoidTypeDict, voidTypeLabel } from '@/utils/void-type-dict'
|
||
import { formatProtocolDocSubject } from '@/utils/protocol-doc-label'
|
||
import { isInstallmentPayMode } from '@/utils/protocol-payment-presets'
|
||
import { sumSampleContractAmounts } from '@/utils/protocol-sample-payment'
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
modelValue: boolean
|
||
protocolId: string | null
|
||
listRow?: CollectionProtocolRow | null
|
||
/** 展示审核记录 Tab(采集协议单审核页) */
|
||
showAuditHistory?: boolean
|
||
/** 储存证管理查看:展示储存证附件,状态变更记录独立区域 */
|
||
showStorageCert?: boolean
|
||
/** 满月待办查看:客户信息下方独立展示新生儿保险信息 */
|
||
showFullmoonInsurance?: boolean
|
||
}>(),
|
||
{ showAuditHistory: false, showStorageCert: false, showFullmoonInsurance: false }
|
||
)
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:modelValue', val: boolean): void
|
||
}>()
|
||
|
||
const baseFormTabs = [
|
||
{ key: 'basic', label: '基本信息' },
|
||
{ key: 'collection', label: '采集信息' },
|
||
{ key: 'sample', label: '样本及运输信息' },
|
||
{ key: 'party', label: '客户信息' },
|
||
{ key: 'payment', label: '缴费信息' }
|
||
] as const
|
||
|
||
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 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>>>([])
|
||
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 protocol = computed(() => bundle.value?.protocol as Record<string, any> | undefined)
|
||
const partyCustomer = computed(() => bundle.value?.partyCustomer as Record<string, any> | null)
|
||
const collectionInfo = computed(() => bundle.value?.collectionInfo as Record<string, any> | null)
|
||
const payment = computed(() => bundle.value?.payment as Record<string, any> | null)
|
||
const paymentModelVersion = computed(() =>
|
||
Number((bundle.value?.protocol as any)?.paymentModelVersion || 1)
|
||
)
|
||
const samplePayments = computed(
|
||
() => (bundle.value?.samplePayments as Array<Record<string, any>>) || []
|
||
)
|
||
const refundRecords = computed(
|
||
() => (bundle.value?.refundRecords as Array<Record<string, any>>) || []
|
||
)
|
||
const presignDepositHint = computed(
|
||
() => (bundle.value?.presignDepositHint as Record<string, any>) || null
|
||
)
|
||
const sampleContractSum = computed(() => sumSampleContractAmounts(samplePayments.value))
|
||
const presignDepositHintText = computed(() => {
|
||
const h = presignDepositHint.value
|
||
if (!h) return ''
|
||
const amt = Number(h.confirmedDepositAmount || 0)
|
||
const cnt = Number(h.confirmedTransferCount || 0)
|
||
return `来源预签约已确认意向金合计 ¥${formatMoneyDisplay(amt)}(${cnt} 笔),仅提示不自动计入样本缴费`
|
||
})
|
||
const v2ReceivedAmount = computed(() => {
|
||
let sum = 0
|
||
for (const sp of samplePayments.value) {
|
||
for (const sch of sp.schedules || []) {
|
||
for (const tr of sch.transfers || []) {
|
||
if (String(tr.payStatus || '') === 'yjf') {
|
||
sum += Number(tr.transferAmount || 0) || 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return sum
|
||
})
|
||
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 paymentDetails = computed(
|
||
() => (bundle.value?.paymentDetails as Array<Record<string, any>>) || []
|
||
)
|
||
|
||
/** 已收金额 = 已缴费明细的转账金额之和(排除待缴费、退费确认中、已退费) */
|
||
const receivedAmount = computed(() => {
|
||
return paymentDetails.value
|
||
.filter((d) => {
|
||
const s = String(d.payStatus || '')
|
||
return s === 'yjf' || s === 'jfqrz'
|
||
})
|
||
.reduce((sum, d) => {
|
||
const amt = Number(d.transferAmount || 0)
|
||
return sum + (isNaN(amt) ? 0 : amt)
|
||
}, 0)
|
||
})
|
||
const protocolAttachments = computed(() => {
|
||
const list = bundle.value?.attachments
|
||
return Array.isArray(list) ? list : []
|
||
})
|
||
|
||
const consentAttachments = computed(() => {
|
||
const list = bundle.value?.consentAttachmentFiles
|
||
return Array.isArray(list) ? list : []
|
||
})
|
||
|
||
const healthSurveyAttachments = computed(() => {
|
||
const list = bundle.value?.healthSurveyAttachmentFiles
|
||
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'
|
||
}
|
||
|
||
/** 金额只读展示:千分位 + ¥ 前缀,null 显示 — */
|
||
function fmtMoney(v: unknown): string {
|
||
if (v == null || v === '') return '—'
|
||
return '¥' + formatMoneyDisplay(v)
|
||
}
|
||
|
||
function storageCertMakeStatusLabel(makeStatus: string) {
|
||
return makeStatus === '1' ? '已制作' : '未制作'
|
||
}
|
||
|
||
function storageCertMakeStatusTagType(makeStatus: string) {
|
||
return makeStatus === '1' ? 'success' : 'warning'
|
||
}
|
||
|
||
const dialogTitle = computed(() => {
|
||
const name =
|
||
partyCustomer.value?.motherName || props.listRow?.motherName || protocol.value?.motherName
|
||
return `查看 — ${formatProtocolDocSubject('cell_storage', { motherName: name })}`
|
||
})
|
||
|
||
const hasAnyStatusChange = computed(() => {
|
||
if (!protocol.value) return false
|
||
return !!(
|
||
protocol.value.effectiveTime ||
|
||
protocol.value.expireTime ||
|
||
protocol.value.voidTime ||
|
||
protocol.value.toResearchTime
|
||
)
|
||
})
|
||
|
||
const collectorDisplay = computed(() => {
|
||
const fromList = props.listRow?.collectorUserName
|
||
if (fromList) return String(fromList)
|
||
const fromBundle = protocol.value?.collectorUserName
|
||
if (fromBundle) return String(fromBundle)
|
||
const id = protocol.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 = collectionInfo.value?.salesUserName
|
||
if (fromBundle) return String(fromBundle)
|
||
const id = collectionInfo.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 = collectionInfo.value?.regionName
|
||
if (fromBundle) return String(fromBundle)
|
||
const id = collectionInfo.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 = collectionInfo.value?.hospitalName
|
||
if (fromBundle) return String(fromBundle)
|
||
const id = collectionInfo.value?.hospitalId
|
||
const h = hospitalOptions.value.find((x) => x.id === id)
|
||
return h?.hospitalName || dash(id)
|
||
})
|
||
|
||
function dash(val: unknown) {
|
||
return formatDisplayOrDash(val)
|
||
}
|
||
|
||
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 formatInstallmentAmounts(values?: number[] | null) {
|
||
if (!values?.length) return '—'
|
||
return values.map((v) => fmtMoney(v)).join(' / ')
|
||
}
|
||
|
||
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 payDetailColorClass(payStatus: string): string {
|
||
const s = String(payStatus || '')
|
||
if (s === 'ytf') return 'red'
|
||
if (s === 'tfqrz') return 'amber'
|
||
return 'green'
|
||
}
|
||
|
||
/** 缴费明细是否已完成财务收款确认(jfqrz 仅已提交待财务处理) */
|
||
function isReceiptFinanceConfirmed(detail: Record<string, any>) {
|
||
const s = String(detail.payStatus || '')
|
||
return s !== 'jfqrz' && s !== 'dfj'
|
||
}
|
||
|
||
interface MiniFlowStep {
|
||
key: string
|
||
label: string
|
||
dotClass: string
|
||
dotIcon: string
|
||
labelClass: string
|
||
connectorToNext?: string
|
||
hasDetailCard?: boolean
|
||
}
|
||
|
||
function buildMiniFlowSteps(detail: Record<string, any>): MiniFlowStep[] {
|
||
const s = String(detail.payStatus || '')
|
||
if (s === 'dfj') return []
|
||
|
||
const steps: MiniFlowStep[] = []
|
||
|
||
steps.push({
|
||
key: 'pay',
|
||
label: '缴费确认信息',
|
||
dotClass: 'done',
|
||
dotIcon: '✓',
|
||
labelClass: 'done',
|
||
connectorToNext: 'done',
|
||
hasDetailCard: true
|
||
})
|
||
|
||
if (s === 'jfqrz') {
|
||
steps.push({
|
||
key: 'receipt-finance',
|
||
label: '缴费财务确认信息',
|
||
dotClass: 'current-amber',
|
||
dotIcon: '●',
|
||
labelClass: 'current-amber',
|
||
hasDetailCard: true
|
||
})
|
||
} else {
|
||
steps.push({
|
||
key: 'receipt-finance',
|
||
label: '缴费财务确认信息',
|
||
dotClass: 'done',
|
||
dotIcon: '✓',
|
||
labelClass: 'done',
|
||
connectorToNext: s === 'yjf' ? undefined : 'done',
|
||
hasDetailCard: true
|
||
})
|
||
}
|
||
|
||
if (s === 'tfqrz' || s === 'ytf') {
|
||
steps.push({
|
||
key: 'refund-apply',
|
||
label: '退费确认信息',
|
||
dotClass: s === 'tfqrz' ? 'current-amber' : 'done',
|
||
dotIcon: s === 'tfqrz' ? '●' : '✓',
|
||
labelClass: s === 'tfqrz' ? 'current-amber' : 'done',
|
||
connectorToNext: s === 'ytf' ? 'done-red' : undefined,
|
||
hasDetailCard: true
|
||
})
|
||
}
|
||
|
||
if (s === 'ytf') {
|
||
steps.push({
|
||
key: 'refund-finance',
|
||
label: '退费财务确认信息',
|
||
dotClass: 'done-red',
|
||
dotIcon: '✓',
|
||
labelClass: 'done-red',
|
||
hasDetailCard: true
|
||
})
|
||
}
|
||
|
||
if (steps.length) {
|
||
delete steps[steps.length - 1].connectorToNext
|
||
}
|
||
|
||
return steps
|
||
}
|
||
|
||
function miniFlowConnectorStyle(total: number, index: number): string {
|
||
const leftPct = ((index + 0.5) / total) * 100
|
||
const widthPct = 100 / total
|
||
return `left: calc(${leftPct}% + 12px); width: calc(${widthPct}% - 12px);`
|
||
}
|
||
|
||
interface OverviewTimelineStep {
|
||
key: string
|
||
label: string
|
||
subLabel: string
|
||
dotClass: string
|
||
dotIcon: string
|
||
labelClass: string
|
||
connectorToNext?: string
|
||
}
|
||
|
||
/** 缴费总览 — 横向时间线节点(每个缴费明细对应一个节点) */
|
||
function buildOverviewTimelineSteps(): OverviewTimelineStep[] {
|
||
const details = paymentDetails.value
|
||
if (!details.length) return []
|
||
|
||
const steps: OverviewTimelineStep[] = details.map((d, i) => {
|
||
const s = String(d.payStatus || '')
|
||
const name = isInstallmentPayMode(String(payment.value?.payMode || ''))
|
||
? `第${d.installmentNo || i + 1}期`
|
||
: `缴费${i + 1}`
|
||
|
||
if (s === 'ytf') {
|
||
return {
|
||
key: `ov-${i}`,
|
||
label: name,
|
||
subLabel: '已退费',
|
||
dotClass: 'refund',
|
||
dotIcon: '✓',
|
||
labelClass: 'refund'
|
||
}
|
||
}
|
||
if (s === 'tfqrz') {
|
||
return {
|
||
key: `ov-${i}`,
|
||
label: name,
|
||
subLabel: '退费中',
|
||
dotClass: 'refund-current',
|
||
dotIcon: '●',
|
||
labelClass: 'refund-current',
|
||
connectorToNext: 'refund-current'
|
||
}
|
||
}
|
||
if (s === 'yjf') {
|
||
return {
|
||
key: `ov-${i}`,
|
||
label: name,
|
||
subLabel: '已缴费',
|
||
dotClass: 'done',
|
||
dotIcon: '✓',
|
||
labelClass: 'done',
|
||
connectorToNext: 'done'
|
||
}
|
||
}
|
||
if (s === 'jfqrz') {
|
||
return {
|
||
key: `ov-${i}`,
|
||
label: name,
|
||
subLabel: '确认中',
|
||
dotClass: 'current',
|
||
dotIcon: '●',
|
||
labelClass: 'done',
|
||
connectorToNext: 'current'
|
||
}
|
||
}
|
||
return {
|
||
key: `ov-${i}`,
|
||
label: name,
|
||
subLabel: '待缴费',
|
||
dotClass: 'pending',
|
||
dotIcon: '○',
|
||
labelClass: ''
|
||
}
|
||
})
|
||
|
||
// 填充 connectorToNext:下一个节点的连线颜色由本节点的状态决定
|
||
for (let i = 0; i < steps.length - 1; i++) {
|
||
if (!steps[i].connectorToNext) {
|
||
steps[i].connectorToNext = steps[i].dotClass === 'pending' ? '' : 'done'
|
||
}
|
||
}
|
||
|
||
return steps
|
||
}
|
||
|
||
function timelineConnectorStyle(total: number, index: number): string {
|
||
const leftPct = ((index + 0.5) / total) * 100
|
||
const widthPct = 100 / total
|
||
return `left: calc(${leftPct}% + 14px); width: calc(${widthPct}% - 14px);`
|
||
}
|
||
|
||
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 ensureUnitsInOptions(samples: Array<Record<string, any>>) {
|
||
const merged = [...unitOptions.value]
|
||
for (const s of samples) {
|
||
const id = String(s.unitId || '')
|
||
if (!id) continue
|
||
const name = String(s.unitName || '').trim()
|
||
const existing = merged.find((x) => x.id === id)
|
||
if (existing) {
|
||
if (name && (!existing.unitName || existing.unitName === existing.id)) {
|
||
existing.unitName = name
|
||
}
|
||
continue
|
||
}
|
||
merged.push({ id, unitName: name || id })
|
||
}
|
||
unitOptions.value = merged
|
||
}
|
||
|
||
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 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(`¥${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 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 detailAttachments(detail: Record<string, any>) {
|
||
return Array.isArray(detail.attachments) ? detail.attachments : []
|
||
}
|
||
|
||
function financeConfirmAttachmentFiles(detail: Record<string, any>) {
|
||
if (Array.isArray(detail.financeConfirmAttachmentFiles)) {
|
||
return detail.financeConfirmAttachmentFiles
|
||
}
|
||
return urlsToAttachmentFiles(String(detail.financeConfirmAttachments || ''))
|
||
}
|
||
|
||
function refundAttachmentFiles(detail: Record<string, any>) {
|
||
if (Array.isArray(detail.refundAttachmentFiles)) {
|
||
return detail.refundAttachmentFiles
|
||
}
|
||
return urlsToAttachmentFiles(String(detail.refundAttachments || ''))
|
||
}
|
||
|
||
function refundFinanceConfirmAttachmentFiles(detail: Record<string, any>) {
|
||
if (Array.isArray(detail.refundFinanceConfirmAttachmentFiles)) {
|
||
return detail.refundFinanceConfirmAttachmentFiles
|
||
}
|
||
return urlsToAttachmentFiles(String(detail.refundFinanceConfirmAttachments || ''))
|
||
}
|
||
|
||
function urlsToAttachmentFiles(urls: string) {
|
||
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: `url_${i}`, fileUrl: url, fileName: name, fileSize: 0 }
|
||
})
|
||
.filter(Boolean) as Array<Record<string, any>>
|
||
}
|
||
|
||
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 })
|
||
}
|
||
|
||
async function loadBaseLookups() {
|
||
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 || []
|
||
}
|
||
|
||
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>[] = []
|
||
|
||
if (ci?.regionId) {
|
||
tasks.push(
|
||
fetchCollectionRegionList({ page: 1, pageSize: 100 }).then((res) => {
|
||
regionOptions.value = res.list || []
|
||
})
|
||
)
|
||
tasks.push(
|
||
fetchHospitalList(String(ci.regionId), { page: 1, pageSize: 100 }).then((res) => {
|
||
hospitalOptions.value = res.list || []
|
||
})
|
||
)
|
||
}
|
||
if (p?.collectorUserId) {
|
||
tasks.push(
|
||
fetchUserList({ page: 1, pageSize: 50, status: '1' }).then((res) => {
|
||
collectorUserOptions.value = res.list || []
|
||
})
|
||
)
|
||
}
|
||
if (ci?.salesUserId) {
|
||
tasks.push(
|
||
fetchUserList({ page: 1, pageSize: 50, status: '1' }).then((res) => {
|
||
salesUserOptions.value = res.list || []
|
||
})
|
||
)
|
||
}
|
||
if (tasks.length) {
|
||
await Promise.all(tasks)
|
||
}
|
||
}
|
||
|
||
async function finishLookups(data: Record<string, any>, baseLookupsPromise: Promise<void>) {
|
||
lookupLoading.value = true
|
||
try {
|
||
await baseLookupsPromise
|
||
ensureUnitsInOptions((data.samples as Array<Record<string, any>>) || [])
|
||
await loadConditionalLookups(data)
|
||
} finally {
|
||
lookupLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadAuditRecords() {
|
||
if (!props.showAuditHistory || !props.protocolId) {
|
||
auditRecords.value = []
|
||
return
|
||
}
|
||
auditLoading.value = true
|
||
try {
|
||
const res = await fetchCollectionProtocolAuditList({
|
||
page: 1,
|
||
pageSize: 100,
|
||
collectionProtocolId: props.protocolId
|
||
})
|
||
auditRecords.value = res.list || []
|
||
} catch {
|
||
auditRecords.value = []
|
||
} finally {
|
||
auditLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadDetail() {
|
||
if (!props.protocolId) return
|
||
detailLoading.value = true
|
||
lookupLoading.value = true
|
||
bundle.value = null
|
||
auditRecords.value = []
|
||
|
||
const baseLookupsPromise = loadBaseLookups().catch(() => {})
|
||
const auditPromise = props.showAuditHistory ? loadAuditRecords() : null
|
||
|
||
let data: Record<string, any> | null = null
|
||
try {
|
||
data = (await fetchCollectionProtocolDetail(props.protocolId)) as Record<string, any>
|
||
bundle.value = data
|
||
} finally {
|
||
detailLoading.value = false
|
||
}
|
||
|
||
if (!data) {
|
||
lookupLoading.value = false
|
||
return
|
||
}
|
||
|
||
void finishLookups(data, baseLookupsPromise)
|
||
void auditPromise
|
||
}
|
||
|
||
function scrollToSection(key: string) {
|
||
if (detailLoading.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'
|
||
detailLoading.value = false
|
||
lookupLoading.value = false
|
||
auditLoading.value = false
|
||
}
|
||
|
||
watch(
|
||
() => [props.modelValue, props.protocolId] as const,
|
||
([visible, id]) => {
|
||
if (visible) {
|
||
void loadTransferTypeDict()
|
||
void loadVoidTypeDict()
|
||
}
|
||
if (visible && id) loadDetail()
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.protocol-view-body {
|
||
overflow: visible;
|
||
|
||
&.is-detail-loading {
|
||
min-height: min(680px, calc(100vh - 220px));
|
||
}
|
||
}
|
||
|
||
/* ---- 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: var(--mint-text-muted, #334155);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
&--banner {
|
||
margin: -22px -24px 18px;
|
||
padding: 10px 24px;
|
||
border-bottom: 1px solid rgba(16, 185, 129, 0.12);
|
||
border-radius: 12px 12px 0 0;
|
||
background: var(--primary-bg, #ecfdf5);
|
||
|
||
h3 {
|
||
color: var(--primary-dark, #059669);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- 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: var(--mint-text-muted, #334155);
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.data-value {
|
||
font-size: 14px;
|
||
color: #1e293b;
|
||
font-weight: 500;
|
||
word-break: break-word;
|
||
|
||
&--muted {
|
||
color: var(--mint-text-muted, #334155);
|
||
font-weight: 400;
|
||
}
|
||
}
|
||
|
||
/* 储存证:储存证附件 / 制作状态 / 制作日期同一行(在协议附件行下方) */
|
||
.storage-cert-info-row {
|
||
margin-top: 14px;
|
||
padding-top: 14px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
|
||
.storage-cert-attachments-item {
|
||
.storage-cert-attachments-value {
|
||
font-weight: 400;
|
||
}
|
||
|
||
:deep(.protocol-attachment-empty) {
|
||
font-size: 14px;
|
||
color: var(--mint-text-muted, #334155);
|
||
}
|
||
}
|
||
|
||
/* 协议附件 / 知情同意书 / 孕妇健康调查表 / 备注(三行堆叠) */
|
||
.protocol-basic-attachments-block {
|
||
margin-top: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
.protocol-attachments-item {
|
||
.protocol-attachments-value {
|
||
font-weight: 400;
|
||
}
|
||
|
||
:deep(.protocol-attachment-empty) {
|
||
font-size: 14px;
|
||
color: var(--mint-text-muted, #334155);
|
||
}
|
||
}
|
||
|
||
/* ---- 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: var(--mint-text-muted, #334155);
|
||
}
|
||
}
|
||
|
||
.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: var(--mint-text-muted, #334155);
|
||
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;
|
||
}
|
||
|
||
/* ---- 审核记录内嵌列表(对齐 OpenSpec 嵌套列表 14px 标准) ---- */
|
||
.view-inner-table-wrap.list-body.list-wrap {
|
||
margin-top: 4px;
|
||
border: 1px solid var(--mint-border, #e2e8f0);
|
||
border-radius: var(--mint-radius, 10px);
|
||
overflow: hidden;
|
||
background: #fff;
|
||
|
||
:deep(.el-table) {
|
||
--el-table-header-bg-color: var(--mint-table-head, #f8fafc);
|
||
--el-table-border-color: var(--mint-border, #e2e8f0);
|
||
--el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
|
||
--el-table-text-color: var(--mint-text, #0f172a);
|
||
--el-table-header-text-color: var(--mint-text-muted, #64748b);
|
||
font-size: 14px;
|
||
}
|
||
|
||
:deep(.el-table th.el-table__cell) {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--mint-text-muted, #64748b);
|
||
}
|
||
|
||
:deep(.el-table td.el-table__cell) {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--mint-text, #0f172a);
|
||
}
|
||
|
||
:deep(.el-empty) {
|
||
padding: 24px 0;
|
||
}
|
||
}
|
||
|
||
/* ---- 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: var(--mint-text-muted, #334155);
|
||
}
|
||
|
||
.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: var(--mint-text-muted, #334155);
|
||
margin-bottom: 10px;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
.data-value--code {
|
||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', monospace;
|
||
font-size: 13px;
|
||
color: var(--mint-text-muted, #334155);
|
||
}
|
||
|
||
.data-value--price {
|
||
color: #065f46;
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* ---- Payment Summary (Metrics) ---- */
|
||
.payment-summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.payment-metric {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 12px 16px;
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
|
||
.pm-label {
|
||
font-size: 11px;
|
||
color: var(--mint-text-muted, #334155);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.pm-value {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #1e293b;
|
||
|
||
&--amount {
|
||
color: #065f46;
|
||
}
|
||
|
||
&--received {
|
||
color: #047857;
|
||
}
|
||
}
|
||
|
||
/* ---- Payment Detail Cards ---- */
|
||
.payment-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.payment-detail-card {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 14px 16px;
|
||
border: 1px solid #e2e8f0;
|
||
|
||
&--refunded {
|
||
border-left: 3px solid #dc2626;
|
||
border-color: #e2e8f0;
|
||
border-left-color: #dc2626;
|
||
}
|
||
|
||
&--refund-pending {
|
||
border-left: 3px solid #d97706;
|
||
border-color: #e2e8f0;
|
||
border-left-color: #d97706;
|
||
}
|
||
}
|
||
|
||
/* ---- PDC Header ---- */
|
||
.pdc-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
}
|
||
|
||
.pdc-header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
|
||
&::before {
|
||
content: '';
|
||
width: 2px;
|
||
height: 14px;
|
||
border-radius: 1px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&.green::before {
|
||
background: #047857;
|
||
}
|
||
|
||
&.red::before {
|
||
background: #dc2626;
|
||
}
|
||
|
||
&.amber::before {
|
||
background: #d97706;
|
||
}
|
||
}
|
||
|
||
.pdc-header-amount {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
margin-left: 4px;
|
||
|
||
&.green {
|
||
color: #065f46;
|
||
}
|
||
|
||
&.red {
|
||
color: #dc2626;
|
||
}
|
||
|
||
&.amber {
|
||
color: #d97706;
|
||
}
|
||
}
|
||
|
||
/* ---- PDC Info Row ---- */
|
||
.pdc-info-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 14px 20px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.pdc-info-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
|
||
.info-label {
|
||
font-size: 11px;
|
||
color: var(--mint-text-muted, #334155);
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 13px;
|
||
color: #1e293b;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.pdc-attachments-row {
|
||
margin-bottom: 8px;
|
||
padding-top: 8px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
/* ---- Payment Section Subtitle ---- */
|
||
.section-subtitle {
|
||
margin: 0 0 14px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--mint-text-muted, #334155);
|
||
}
|
||
|
||
.sample-pay-readonly {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.sample-pay-readonly__card {
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--mint-border, #e2e8f0);
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
}
|
||
|
||
.sample-pay-readonly__head {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sample-pay-readonly__amt {
|
||
color: #047857;
|
||
}
|
||
|
||
.sample-pay-readonly__sch {
|
||
margin-top: 8px;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
}
|
||
|
||
.sample-pay-readonly__sch-hd {
|
||
margin-bottom: 6px;
|
||
font-size: 12px;
|
||
color: #475569;
|
||
}
|
||
|
||
.sample-pay-readonly__tr {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px 12px;
|
||
align-items: center;
|
||
padding: 4px 0;
|
||
font-size: 12px;
|
||
color: #334155;
|
||
}
|
||
|
||
/* ---- Mini Flow ---- */
|
||
.mini-flow {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
margin: 12px 0;
|
||
padding: 10px 14px;
|
||
background: #f8fafc;
|
||
border-radius: 8px;
|
||
position: relative;
|
||
}
|
||
|
||
.mini-flow-step {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
position: relative;
|
||
flex: 1;
|
||
z-index: 1;
|
||
}
|
||
|
||
.mini-flow-dot {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
z-index: 1;
|
||
|
||
&.done {
|
||
background: #047857;
|
||
color: #fff;
|
||
box-shadow: 0 0 0 3px rgba(4, 120, 87, 0.15);
|
||
}
|
||
|
||
&.done-red {
|
||
background: #dc2626;
|
||
color: #fff;
|
||
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15);
|
||
}
|
||
|
||
&.current-amber {
|
||
background: #fff;
|
||
border: 2px solid #d97706;
|
||
color: #d97706;
|
||
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.15);
|
||
animation: pulseAmber 2s ease-in-out infinite;
|
||
}
|
||
|
||
&.pending-gray {
|
||
background: #f1f5f9;
|
||
color: #94a3b8;
|
||
border: 2px solid #e2e8f0;
|
||
}
|
||
}
|
||
|
||
.mini-flow-label {
|
||
font-size: 10px;
|
||
margin-top: 6px;
|
||
text-align: center;
|
||
max-width: 96px;
|
||
line-height: 1.3;
|
||
|
||
&.done {
|
||
color: #065f46;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&.done-red {
|
||
color: #dc2626;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&.current-amber {
|
||
color: #d97706;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&.pending-gray {
|
||
color: #94a3b8;
|
||
}
|
||
}
|
||
|
||
/* ---- Flow Section: track + detail cards ---- */
|
||
.flow-section {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.flow-detail-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||
gap: 12px;
|
||
margin-top: 14px;
|
||
}
|
||
|
||
.flow-detail-card {
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
|
||
&--current-amber {
|
||
border-color: #fde68a;
|
||
background: #fffbeb;
|
||
}
|
||
|
||
&--done-red {
|
||
border-left: 3px solid #dc2626;
|
||
background: #fff;
|
||
}
|
||
|
||
&--pending-gray {
|
||
border-style: dashed;
|
||
background: #f8fafc;
|
||
}
|
||
}
|
||
|
||
.flow-detail-card__header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 14px;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
background: #f8fafc;
|
||
|
||
.flow-detail-card--done-red & {
|
||
background: #fef2f2;
|
||
border-bottom-color: #fecaca;
|
||
}
|
||
|
||
.flow-detail-card--current-amber & {
|
||
background: #fffbeb;
|
||
border-bottom-color: #fde68a;
|
||
}
|
||
|
||
.flow-detail-card--pending-gray & {
|
||
background: #f1f5f9;
|
||
border-bottom-color: #e2e8f0;
|
||
}
|
||
}
|
||
|
||
.flow-detail-card__dot {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
flex-shrink: 0;
|
||
|
||
&.done {
|
||
background: #047857;
|
||
color: #fff;
|
||
}
|
||
|
||
&.done-red {
|
||
background: #dc2626;
|
||
color: #fff;
|
||
}
|
||
|
||
&.current-amber {
|
||
background: #fff;
|
||
border: 2px solid #d97706;
|
||
color: #d97706;
|
||
}
|
||
|
||
&.pending-gray {
|
||
background: #f1f5f9;
|
||
border: 2px solid #e2e8f0;
|
||
color: #94a3b8;
|
||
}
|
||
}
|
||
|
||
.flow-detail-card__title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #1e293b;
|
||
|
||
&.done {
|
||
color: #065f46;
|
||
}
|
||
|
||
&.done-red {
|
||
color: #dc2626;
|
||
}
|
||
|
||
&.current-amber {
|
||
color: #d97706;
|
||
}
|
||
|
||
&.pending-gray {
|
||
color: #94a3b8;
|
||
}
|
||
}
|
||
|
||
.flow-detail-card__body {
|
||
padding: 12px 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.flow-detail-card__kv {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.flow-detail-card__label {
|
||
font-size: 11px;
|
||
color: var(--mint-text-muted, #334155);
|
||
}
|
||
|
||
.flow-detail-card__value {
|
||
font-size: 13px;
|
||
color: #1e293b;
|
||
font-weight: 500;
|
||
word-break: break-word;
|
||
|
||
&.red {
|
||
color: #dc2626;
|
||
font-weight: 700;
|
||
}
|
||
|
||
&.amber {
|
||
color: #d97706;
|
||
font-weight: 700;
|
||
}
|
||
|
||
&.green {
|
||
color: #065f46;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
.flow-detail-card__empty {
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #334155);
|
||
font-style: italic;
|
||
}
|
||
|
||
.mini-flow-connector {
|
||
position: absolute;
|
||
top: 22px;
|
||
height: 2px;
|
||
z-index: 0;
|
||
|
||
&.done {
|
||
background: #047857;
|
||
}
|
||
|
||
&.done-red {
|
||
background: #dc2626;
|
||
}
|
||
|
||
&.current-amber {
|
||
background: #d97706;
|
||
}
|
||
|
||
&.pending-gray {
|
||
background: #e2e8f0;
|
||
}
|
||
}
|
||
|
||
/* ---- Refund Area ---- */
|
||
.refund-area {
|
||
margin-top: 12px;
|
||
padding: 14px 16px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
|
||
&.red-tint {
|
||
background: #fef2f2;
|
||
border: 1px solid #fecaca;
|
||
}
|
||
|
||
&.amber-tint {
|
||
background: #fffbeb;
|
||
border: 1px solid #fde68a;
|
||
}
|
||
}
|
||
|
||
.refund-area-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-bottom: 2px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
|
||
&.red {
|
||
color: #dc2626;
|
||
}
|
||
|
||
&.amber {
|
||
color: #d97706;
|
||
}
|
||
}
|
||
|
||
.refund-area-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||
gap: 14px 20px;
|
||
|
||
.data-label {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.data-value {
|
||
font-size: 13px;
|
||
|
||
&.red {
|
||
color: #dc2626;
|
||
font-weight: 700;
|
||
}
|
||
|
||
&.amber {
|
||
color: #d97706;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
|
||
.full-col {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
/* ---- Finance Area ---- */
|
||
.finance-area {
|
||
margin-top: 12px;
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
background: #f0fdf4;
|
||
border: 1px solid #bbf7d0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.finance-area-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #065f46;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.finance-area-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px 20px;
|
||
|
||
.data-label {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.data-value {
|
||
font-size: 13px;
|
||
}
|
||
}
|
||
|
||
@keyframes pulseAmber {
|
||
0%,
|
||
100% {
|
||
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.15);
|
||
}
|
||
|
||
50% {
|
||
box-shadow: 0 0 0 6px rgba(217, 119, 6, 0.25);
|
||
}
|
||
}
|
||
|
||
/* ---- Payment Overview Timeline ---- */
|
||
.payment-timeline {
|
||
margin-top: 16px;
|
||
padding-top: 14px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
|
||
.timeline-title {
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #334155);
|
||
font-weight: 600;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.timeline {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0;
|
||
position: relative;
|
||
}
|
||
|
||
.timeline-step {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
position: relative;
|
||
flex: 1;
|
||
}
|
||
|
||
.timeline-dot {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
z-index: 1;
|
||
transition: transform 0.2s;
|
||
|
||
&.done {
|
||
background: #047857;
|
||
color: #fff;
|
||
box-shadow: 0 0 0 4px rgba(4, 120, 87, 0.15);
|
||
}
|
||
|
||
&.current {
|
||
background: #fff;
|
||
border: 2px solid #047857;
|
||
color: #047857;
|
||
box-shadow: 0 0 0 4px rgba(4, 120, 87, 0.1);
|
||
}
|
||
|
||
&.pending {
|
||
background: #f1f5f9;
|
||
color: #94a3b8;
|
||
border: 2px solid #e2e8f0;
|
||
}
|
||
|
||
&.refund {
|
||
background: #dc2626;
|
||
color: #fff;
|
||
box-shadow: 0 0 0 4px rgba(220, 38, 38, 0.15);
|
||
}
|
||
|
||
&.refund-current {
|
||
background: #fff;
|
||
border: 2px solid #d97706;
|
||
color: #d97706;
|
||
box-shadow: 0 0 0 4px rgba(217, 119, 6, 0.15);
|
||
animation: pulseAmber 2s ease-in-out infinite;
|
||
}
|
||
}
|
||
|
||
.timeline-label {
|
||
font-size: 11px;
|
||
color: var(--mint-text-muted, #334155);
|
||
margin-top: 8px;
|
||
text-align: center;
|
||
max-width: 80px;
|
||
line-height: 1.4;
|
||
|
||
&.done {
|
||
color: #065f46;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&.refund {
|
||
color: #dc2626;
|
||
font-weight: 600;
|
||
}
|
||
|
||
&.refund-current {
|
||
color: #d97706;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
|
||
.timeline-connector {
|
||
position: absolute;
|
||
top: 14px;
|
||
height: 2px;
|
||
background: #e2e8f0;
|
||
z-index: 0;
|
||
|
||
&.done {
|
||
background: #047857;
|
||
}
|
||
|
||
&.current {
|
||
background: #047857;
|
||
}
|
||
|
||
&.refund {
|
||
background: #dc2626;
|
||
}
|
||
|
||
&.refund-current {
|
||
background: #d97706;
|
||
}
|
||
}
|
||
|
||
/* ---- 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;
|
||
}
|
||
|
||
.storage-cert-info-row.data-grid--3 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
|
||
.storage-cert-attachments-item {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
|
||
.protocol-attach-remark-row.data-grid--2 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.payment-summary {
|
||
grid-template-columns: repeat(2, 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--4 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.storage-cert-info-row.data-grid--3 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.payment-summary {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.pdc-info-row {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.refund-area-grid {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.finance-area-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|