修复 DonationViewDialog 缺失的操作日志状态与加载调用,清理误插入的重置语句,并避免显式 audit 动作被状态映射覆盖。 Co-authored-by: Cursor <cursoragent@cursor.com>
3764 lines
118 KiB
Vue
3764 lines
118 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">{{ protocol?.projectName || '未归属' }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">项目组</div>
|
||
<div class="data-value">{{ protocol?.teamName || '—' }}</div>
|
||
</div>
|
||
<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">{{ basicMotherNameDisplay }}</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">{{ dash(protocol?.createTime) }}</div></div
|
||
>
|
||
</div>
|
||
<div v-if="protocol?.remark && String(protocol.remark).trim()" class="remark-row">
|
||
<div class="data-item data-item--full">
|
||
<div class="data-label">备注</div>
|
||
<div class="data-value data-value--muted">{{ dash(protocol?.remark) }}</div>
|
||
</div>
|
||
</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--7 data-grid--collection-row">
|
||
<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">{{ regionManagerDisplay }}</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"
|
||
:class="{
|
||
'is-highlight':
|
||
!!highlightSampleCode && String(row.sampleCode || '') === highlightSampleCode
|
||
}"
|
||
:id="row.sampleCode ? `view-sample-${row.sampleCode}` : undefined"
|
||
>
|
||
<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.freight?.id ||
|
||
row.freight?.transportMode ||
|
||
row.transport?.etaTime
|
||
"
|
||
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">{{
|
||
dash(row.freight?.transportModeLabel || row.freight?.transportMode)
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">运费(元)</div>
|
||
<div class="data-value"
|
||
>{{
|
||
String(row.freight?.transportMode || '').trim() === 'sfkd' &&
|
||
(row.freight?.freightAmount == null || row.freight?.freightAmount === '')
|
||
? '月结'
|
||
: row.freight?.freightAmount != null
|
||
? '¥' + formatMoneyDisplay(row.freight.freightAmount)
|
||
: '—'
|
||
}}{{
|
||
Number(row.freight?.linkedSampleCount || 0) > 1 ? '(共用)' : ''
|
||
}}</div
|
||
>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">预计到达</div>
|
||
<div class="data-value">{{ dash(row.transport?.etaTime) }}</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"
|
||
:show-section-label="false"
|
||
/>
|
||
<SampleProductionInfoCards :production-info="row.productionInfo" />
|
||
<SampleStorageReleaseInfo :storage-release-info="row.storageReleaseInfo" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无样本" :image-size="48" />
|
||
</div>
|
||
|
||
<div
|
||
v-if="showHandlePanels"
|
||
id="view-section-storage-cert-handle"
|
||
class="section-card section-card--white"
|
||
>
|
||
<div class="section-title section-title--banner"><h3>储存证办理</h3></div>
|
||
<ProtocolHandlePanel
|
||
v-if="protocolId"
|
||
mode="storage-cert"
|
||
:protocol-id="protocolId"
|
||
:sample-code="highlightSampleCode || undefined"
|
||
:initial="storageCertInfo"
|
||
@saved="loadDetail"
|
||
/>
|
||
</div>
|
||
<div
|
||
v-if="showHandlePanels"
|
||
id="view-section-insurance-handle"
|
||
class="section-card section-card--white"
|
||
>
|
||
<div class="section-title section-title--banner"><h3>保险办理</h3></div>
|
||
<ProtocolHandlePanel
|
||
v-if="protocolId"
|
||
mode="insurance"
|
||
:protocol-id="protocolId"
|
||
:sample-code="highlightSampleCode || undefined"
|
||
:initial="fullmoonInsuranceInfo"
|
||
@saved="loadDetail"
|
||
/>
|
||
</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 class="data-item"
|
||
><div class="data-label">母亲邮箱</div
|
||
><div class="data-value">{{ dash(partyCustomer?.motherEmail) }}</div></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="showHandleSummary"
|
||
id="view-section-storage-insurance"
|
||
class="section-card section-card--white"
|
||
>
|
||
<div class="section-title section-title--banner"><h3>储存证与新生儿保险</h3></div>
|
||
<div class="handle-summary-cards">
|
||
<div v-if="showStorageCert" class="handle-summary-card">
|
||
<div class="sub-card-header"><h4>储存证</h4></div>
|
||
<div class="data-grid data-grid--2">
|
||
<div class="data-item">
|
||
<div class="data-label">办理时间</div>
|
||
<div class="data-value">{{ dash(storageCertInfo?.handleTime) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">办理人员</div>
|
||
<div class="data-value">{{ dash(storageCertInfo?.handlerUserName) }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="attachment-row">
|
||
<div class="data-label">储存证附件</div>
|
||
<ProtocolAttachmentReadonly
|
||
:files="storageCertAttachments"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
<div class="remark-row">
|
||
<div class="data-item data-item--full">
|
||
<div class="data-label">备注</div>
|
||
<div class="data-value data-value--muted">{{
|
||
dash(storageCertInfo?.remark)
|
||
}}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="showFullmoonInsurance" class="handle-summary-card">
|
||
<div class="sub-card-header"><h4>新生儿保险</h4></div>
|
||
<div class="data-grid data-grid--2">
|
||
<div class="data-item">
|
||
<div class="data-label">办理时间</div>
|
||
<div class="data-value">{{ dash(fullmoonInsuranceInfo?.handleTime) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">办理人员</div>
|
||
<div class="data-value">{{ dash(fullmoonInsuranceInfo?.handlerUserName) }}</div>
|
||
</div>
|
||
</div>
|
||
<div class="attachment-row">
|
||
<div class="data-label">保险附件</div>
|
||
<ProtocolAttachmentReadonly
|
||
:files="fullmoonInsuranceAttachments"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
</div>
|
||
<div 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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 缴费信息 -->
|
||
<div id="view-section-payment" class="section-card section-card--white">
|
||
<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
|
||
v-if="depositTransfers.length"
|
||
class="sample-pay-readonly__batch-title"
|
||
style="margin-bottom: 8px"
|
||
>
|
||
意向金转账(关联预签约订单)
|
||
</div>
|
||
<el-table
|
||
v-if="depositTransfers.length"
|
||
:data="depositTransfers"
|
||
class="list-table inner-table"
|
||
stripe
|
||
style="margin-bottom: 16px"
|
||
>
|
||
<el-table-column type="index" label="#" min-width="50" align="center" />
|
||
<el-table-column label="转账方式" min-width="120" align="center">
|
||
<template #default="{ row }">
|
||
{{ transferTypeLabel(String(row.transferType || '')) || '—' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="转账金额(元)" min-width="130" align="center">
|
||
<template #default="{ row }">{{ fmtMoney(row.transferAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="转账时间" min-width="170" align="center">
|
||
<template #default="{ row }">
|
||
{{ formatDisplayOrDash(row.transferTime) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="转账凭证" min-width="220" align="left">
|
||
<template #default="{ row }">
|
||
<ProtocolAttachmentReadonly
|
||
v-if="(row.attachments || []).length"
|
||
:files="row.attachments || []"
|
||
size="compact"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
<span v-else class="muted-dash">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="备注"
|
||
min-width="140"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ row.remark || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="财务备注"
|
||
min-width="140"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ row.financeRemark || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" min-width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="payStatusTagType(String(row.payStatus || ''))" size="small">
|
||
{{ payStatusLabel(String(row.payStatus || '')) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="payment-summary payment-summary--3">
|
||
<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 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">
|
||
<div class="sample-pay-readonly__title">
|
||
<span>{{ samplePayTitle(sp) }}</span>
|
||
<el-tag v-if="sp.sampleCode" size="small" type="info">{{
|
||
sp.sampleCode
|
||
}}</el-tag>
|
||
<el-tag :type="payModeTagType(String(sp.payMode || ''))" size="small">
|
||
{{ payModeLabel(String(sp.payMode || '')) }}
|
||
</el-tag>
|
||
</div>
|
||
<span class="sample-pay-readonly__amt">{{ fmtMoney(sp.contractAmount) }}</span>
|
||
</div>
|
||
|
||
<div
|
||
class="data-grid"
|
||
:class="
|
||
String(sp.payMode) === 'fq' ? 'data-grid--6' : 'data-grid--3'
|
||
"
|
||
>
|
||
<div class="data-item">
|
||
<div class="data-label">缴费模式</div>
|
||
<div class="data-value">{{ payModeLabel(String(sp.payMode || '')) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">合同金额(元)</div>
|
||
<div class="data-value data-value--price">{{
|
||
fmtMoney(sp.contractAmount)
|
||
}}</div>
|
||
</div>
|
||
<template v-if="String(sp.payMode) === 'dj'">
|
||
<div class="data-item">
|
||
<div class="data-label">趸交截止时间</div>
|
||
<div class="data-value">{{ formatYmd(sp.lumpSumDeadline) }}</div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="data-item">
|
||
<div class="data-label">分期期数</div>
|
||
<div class="data-value">{{ dash(sp.installmentCount) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">每期间隔(月)</div>
|
||
<div class="data-value">{{ dash(sp.installmentIntervalMonths) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">分期开始时间</div>
|
||
<div class="data-value">{{ formatYmd(sp.installmentStartDate) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">提前催缴预警(天)</div>
|
||
<div class="data-value">{{ dash(sp.installmentAdvanceDays ?? 30) }}</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="sample-pay-readonly__batch-title">缴费明细</div>
|
||
<div v-if="!(sp.schedules || []).length" class="sample-pay-readonly__empty">
|
||
暂无缴费明细
|
||
</div>
|
||
<div
|
||
v-for="(sch, schIdx) in sp.schedules || []"
|
||
:key="sch.id || schIdx"
|
||
class="sample-pay-readonly__sch"
|
||
>
|
||
<div class="sample-pay-readonly__sch-title">
|
||
第 {{ sch.scheduleNo || schIdx + 1 }} 期
|
||
</div>
|
||
<div class="data-grid data-grid--3">
|
||
<div class="data-item">
|
||
<div class="data-label">应缴费金额(元)</div>
|
||
<div class="data-value data-value--price">{{
|
||
fmtMoney(sch.dueAmount)
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">应缴费时间</div>
|
||
<div class="data-value">{{ formatYmd(sch.dueDate) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">已缴费金额(元)</div>
|
||
<div class="data-value data-value--price">{{
|
||
fmtMoney(sch.paidAmount)
|
||
}}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sample-pay-readonly__tr-title">
|
||
转账记录
|
||
<span v-if="(sch.transfers || []).length" class="sample-pay-readonly__tr-count"
|
||
>({{ (sch.transfers || []).length }} 笔)</span
|
||
>
|
||
</div>
|
||
<div class="list-body list-wrap view-inner-table-wrap">
|
||
<el-table
|
||
:data="sch.transfers || []"
|
||
class="list-table inner-table"
|
||
stripe
|
||
empty-text="暂无转账记录"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
label="转账方式"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
{{ transferTypeLabel(String(row.transferType || '')) || '—' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="转账金额(元)"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{ fmtMoney(row.transferAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="转账时间"
|
||
min-width="190"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatDisplayOrDash(row.transferTime) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="转账凭证"
|
||
min-width="280"
|
||
align="left"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<ProtocolAttachmentReadonly
|
||
v-if="transferAttachments(row).length"
|
||
:files="transferAttachments(row)"
|
||
size="compact"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
<span v-else class="muted-dash">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="备注"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ row.remark || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="财务备注"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ row.financeRemark || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="状态"
|
||
min-width="110"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
:type="payStatusTagType(String(row.payStatus || ''))"
|
||
size="small"
|
||
>
|
||
{{ payStatusLabel(String(row.payStatus || '')) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无按样本缴费方案" :image-size="40" />
|
||
|
||
<div class="sample-pay-readonly__batch-title sample-pay-readonly__batch-title--mt"
|
||
>退费登记</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-column
|
||
label="附件"
|
||
min-width="280"
|
||
align="left"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<ProtocolAttachmentReadonly
|
||
v-if="refundRecordAttachments(row).length"
|
||
:files="refundRecordAttachments(row)"
|
||
size="compact"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
<span v-else class="muted-dash">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-empty v-else description="暂无退费登记" :image-size="40" />
|
||
</template>
|
||
|
||
<!-- v1: 旧整单缴费只读(样式对齐 v2:期汇总 + 转账记录) -->
|
||
<template v-else>
|
||
<div class="payment-summary payment-summary--3">
|
||
<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>
|
||
<div class="payment-metric">
|
||
<div class="pm-label">缴费模式</div>
|
||
<div class="pm-value">{{ payModeLabel(String(payment?.payMode || '')) }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sample-pay-readonly">
|
||
<div class="sample-pay-readonly__card">
|
||
<div class="sample-pay-readonly__head">
|
||
<div class="sample-pay-readonly__title">
|
||
<span>协议缴费</span>
|
||
<el-tag :type="payModeTagType(String(payment?.payMode || ''))" size="small">
|
||
{{ payModeLabel(String(payment?.payMode || '')) }}
|
||
</el-tag>
|
||
</div>
|
||
<span class="sample-pay-readonly__amt">{{
|
||
fmtMoney(payment?.contractAmount)
|
||
}}</span>
|
||
</div>
|
||
|
||
<div
|
||
class="data-grid"
|
||
:class="
|
||
isInstallmentPayMode(String(payment?.payMode || ''))
|
||
? 'data-grid--6'
|
||
: 'data-grid--3'
|
||
"
|
||
>
|
||
<div class="data-item">
|
||
<div class="data-label">缴费模式</div>
|
||
<div class="data-value">{{
|
||
payModeLabel(String(payment?.payMode || ''))
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">合同金额(元)</div>
|
||
<div class="data-value data-value--price">{{
|
||
fmtMoney(payment?.contractAmount)
|
||
}}</div>
|
||
</div>
|
||
<template v-if="String(payment?.payMode) === 'dj'">
|
||
<div class="data-item">
|
||
<div class="data-label">趸交截止时间</div>
|
||
<div class="data-value">{{ formatYmd(payment?.lumpSumDeadline) }}</div>
|
||
</div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="data-item">
|
||
<div class="data-label">分期期数</div>
|
||
<div class="data-value">{{ dash(payment?.installmentCount) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">每期间隔(月)</div>
|
||
<div class="data-value">{{
|
||
dash(payment?.installmentIntervalMonths)
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">分期开始时间</div>
|
||
<div class="data-value">{{ formatYmd(payment?.installmentStartDate) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">提前催缴预警(天)</div>
|
||
<div class="data-value">{{
|
||
dash(payment?.installmentAdvanceDays ?? 30)
|
||
}}</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="sample-pay-readonly__batch-title">缴费明细</div>
|
||
<div v-if="!v1PayPeriods.length" class="sample-pay-readonly__empty">
|
||
暂无缴费明细
|
||
</div>
|
||
<div
|
||
v-for="(sch, schIdx) in v1PayPeriods"
|
||
:key="sch.scheduleNo || schIdx"
|
||
class="sample-pay-readonly__sch"
|
||
>
|
||
<div class="sample-pay-readonly__sch-title">
|
||
第 {{ sch.scheduleNo || schIdx + 1 }} 期
|
||
</div>
|
||
<div class="data-grid data-grid--3">
|
||
<div class="data-item">
|
||
<div class="data-label">应缴费金额(元)</div>
|
||
<div class="data-value data-value--price">{{
|
||
fmtMoney(sch.dueAmount)
|
||
}}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">应缴费时间</div>
|
||
<div class="data-value">{{ formatYmd(sch.dueDate) }}</div>
|
||
</div>
|
||
<div class="data-item">
|
||
<div class="data-label">已缴费金额(元)</div>
|
||
<div class="data-value data-value--price">{{
|
||
fmtMoney(sch.paidAmount)
|
||
}}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sample-pay-readonly__tr-title">
|
||
转账记录
|
||
<span v-if="sch.transfers.length" class="sample-pay-readonly__tr-count"
|
||
>({{ sch.transfers.length }} 笔)</span
|
||
>
|
||
</div>
|
||
<div class="list-body list-wrap view-inner-table-wrap">
|
||
<el-table
|
||
:data="sch.transfers"
|
||
class="list-table inner-table"
|
||
stripe
|
||
empty-text="暂无转账记录"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
label="转账方式"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
{{ transferTypeLabel(String(row.transferType || '')) || '—' }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="转账金额(元)"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">{{ fmtMoney(row.transferAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="转账时间"
|
||
min-width="190"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
{{ formatDisplayOrDash(row.transferTime) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="转账凭证"
|
||
min-width="280"
|
||
align="left"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<ProtocolAttachmentReadonly
|
||
v-if="detailAttachments(row).length"
|
||
:files="detailAttachments(row)"
|
||
size="compact"
|
||
@preview="openPreview"
|
||
@download="downloadFile"
|
||
/>
|
||
<span v-else class="muted-dash">—</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="状态"
|
||
min-width="110"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tag
|
||
:type="payStatusTagType(String(row.payStatus || ''))"
|
||
size="small"
|
||
>
|
||
{{ payStatusLabel(String(row.payStatus || '')) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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="auditStatusTagType(String(row.auditStatus || ''))" 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 id="view-section-attachment" class="section-card section-card--white">
|
||
<div class="section-title section-title--banner"><h3>附件信息</h3></div>
|
||
<div class="protocol-basic-attachments-block protocol-basic-attachments-block--flush">
|
||
<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>
|
||
</div>
|
||
|
||
<!-- 状态变更记录(置底) -->
|
||
<div id="view-section-status-change" 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="statusChangeLoading"
|
||
class="list-table inner-table"
|
||
:data="statusChangeRecords"
|
||
stripe
|
||
empty-text="暂无状态变更记录"
|
||
>
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column label="变更类型" min-width="110" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="statusChangeTagType(String(row.changeType || ''))" size="small">
|
||
{{ statusChangeTypeLabel(String(row.changeType || '')) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="变更时间" min-width="170" align="center" header-align="center">
|
||
<template #default="{ row }">{{ dash(row.changeTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态流转" min-width="160" align="center" header-align="center">
|
||
<template #default="{ row }">{{ statusChangeFlow(row) }}</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="变更说明"
|
||
min-width="200"
|
||
align="center"
|
||
header-align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ statusChangeDesc(row) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作人" min-width="120" align="center" header-align="center">
|
||
<template #default="{ row }">{{ dash(row.operatorUserName) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作日志(Issue #24) -->
|
||
<div id="view-section-operation-log" 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="operationLogLoading"
|
||
class="list-table inner-table"
|
||
:data="operationLogRecords"
|
||
stripe
|
||
empty-text="暂无操作日志"
|
||
>
|
||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||
<el-table-column label="动作" min-width="100" align="center" header-align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="operationActionTagType(String(row.action || ''))" size="small">
|
||
{{ operationActionLabel(String(row.action || '')) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="来源端" min-width="90" align="center" header-align="center">
|
||
<template #default="{ row }">{{ operationChannelLabel(String(row.clientChannel || '')) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作时间" min-width="170" align="center" header-align="center">
|
||
<template #default="{ row }">{{ dash(row.operateTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作人" min-width="120" align="center" header-align="center">
|
||
<template #default="{ row }">{{ dash(row.operatorUserName) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="变更摘要" min-width="260" align="left" header-align="center" show-overflow-tooltip>
|
||
<template #default="{ row }">{{ operationSummary(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 ProtocolHandlePanel from '@/components/business/protocol-handle-panel/index.vue'
|
||
import {
|
||
fetchCollectionProtocolAuditList,
|
||
fetchCollectionProtocolDetail,
|
||
fetchCollectionProtocolStatusChanges,
|
||
fetchCollectionProtocolOperationLogs,
|
||
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,
|
||
auditStatusTagType,
|
||
payModeLabel,
|
||
payModeTagType,
|
||
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
|
||
/** 样本信息管理:储存证/保险轻量办理(可编辑,与只读摘要互斥) */
|
||
showHandlePanels?: boolean
|
||
highlightSampleCode?: string
|
||
}>(),
|
||
{
|
||
showAuditHistory: false,
|
||
showStorageCert: false,
|
||
showFullmoonInsurance: false,
|
||
showHandlePanels: false,
|
||
highlightSampleCode: ''
|
||
}
|
||
)
|
||
|
||
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
|
||
|
||
/** 协议审核等页:只读展示储存证/保险(与样本信息录入字段对齐);办理编辑用 showHandlePanels */
|
||
const showHandleSummary = computed(
|
||
() => !props.showHandlePanels && (props.showStorageCert || props.showFullmoonInsurance)
|
||
)
|
||
|
||
const formTabs = computed(() => {
|
||
const tabs: Array<{ key: string; label: string }> = [...baseFormTabs]
|
||
if (props.showHandlePanels) {
|
||
const sampleIdx = tabs.findIndex((t) => t.key === 'sample')
|
||
tabs.splice(sampleIdx + 1, 0, { key: 'storage-cert-handle', label: '储存证办理' })
|
||
tabs.splice(sampleIdx + 2, 0, { key: 'insurance-handle', label: '保险办理' })
|
||
}
|
||
if (showHandleSummary.value) {
|
||
const partyIdx = tabs.findIndex((t) => t.key === 'party')
|
||
tabs.splice(partyIdx + 1, 0, { key: 'storage-insurance', label: '储存证与保险' })
|
||
}
|
||
if (props.showAuditHistory) {
|
||
tabs.push({ key: 'audit', label: '审核记录' })
|
||
}
|
||
tabs.push({ key: 'attachment', label: '附件信息' })
|
||
tabs.push({ key: 'status-change', label: '状态变更记录' })
|
||
tabs.push({ key: 'operation-log', label: '操作日志' })
|
||
return tabs
|
||
})
|
||
|
||
const detailLoading = ref(false)
|
||
const lookupLoading = ref(false)
|
||
const auditLoading = ref(false)
|
||
const statusChangeLoading = ref(false)
|
||
const bundle = ref<Record<string, any> | null>(null)
|
||
const auditRecords = ref<Array<Record<string, unknown>>>([])
|
||
const statusChangeRecords = ref<Array<Record<string, unknown>>>([])
|
||
const operationLogLoading = ref(false)
|
||
const operationLogRecords = ref<Array<Record<string, unknown>>>([])
|
||
|
||
const OPERATION_ACTION_LABEL: Record<string, string> = {
|
||
create: '创建',
|
||
update: '修改',
|
||
delete: '删除',
|
||
void: '作废',
|
||
activate: '生效',
|
||
expire: '过期',
|
||
to_research: '转科研',
|
||
to_storage: '转细胞存储',
|
||
audit: '审核'
|
||
}
|
||
const OPERATION_ACTION_TAG: Record<string, 'success' | 'warning' | 'danger' | 'primary' | 'info'> = {
|
||
create: 'success',
|
||
update: 'primary',
|
||
delete: 'danger',
|
||
void: 'danger',
|
||
activate: 'success',
|
||
expire: 'warning',
|
||
to_research: 'primary',
|
||
to_storage: 'primary',
|
||
audit: 'info'
|
||
}
|
||
|
||
function operationActionLabel(action: string) {
|
||
return OPERATION_ACTION_LABEL[action] || action || '—'
|
||
}
|
||
function operationActionTagType(action: string) {
|
||
return OPERATION_ACTION_TAG[action] || 'info'
|
||
}
|
||
function operationChannelLabel(channel: string) {
|
||
if (channel === 'mini') return '小程序'
|
||
if (channel === 'web') return 'Web'
|
||
return channel || '—'
|
||
}
|
||
function operationSummary(row: Record<string, unknown>) {
|
||
const before = (row.before || {}) as Record<string, any>
|
||
const after = (row.after || {}) as Record<string, any>
|
||
const beforeProto = (before.protocol || before.donation || {}) as Record<string, any>
|
||
const afterProto = (after.protocol || after.donation || {}) as Record<string, any>
|
||
const beforeParty = (before.partyCustomer || before.party || {}) as Record<string, any>
|
||
const afterParty = (after.partyCustomer || after.party || {}) as Record<string, any>
|
||
const parts: string[] = []
|
||
const beforeStatus = String(beforeProto.protocolStatus || beforeProto.donationStatus || '')
|
||
const afterStatus = String(afterProto.protocolStatus || afterProto.donationStatus || '')
|
||
if (beforeStatus || afterStatus) {
|
||
if (beforeStatus && afterStatus && beforeStatus !== afterStatus) {
|
||
parts.push(`状态 ${beforeStatus}→${afterStatus}`)
|
||
} else if (afterStatus) {
|
||
parts.push(`状态 ${afterStatus}`)
|
||
}
|
||
}
|
||
for (const key of ['motherName', 'customerName', 'donorName', 'name']) {
|
||
const b = String(beforeParty[key] || '').trim()
|
||
const a = String(afterParty[key] || '').trim()
|
||
if (b || a) {
|
||
if (b && a && b !== a) parts.push(`姓名 ${b}→${a}`)
|
||
else if (a && !b) parts.push(`姓名 ${a}`)
|
||
break
|
||
}
|
||
}
|
||
const beforeSamples = Array.isArray(before.samples) ? before.samples : []
|
||
const afterSamples = Array.isArray(after.samples) ? after.samples : []
|
||
if (beforeSamples.length || afterSamples.length) {
|
||
const pick = (list: any[]) =>
|
||
list
|
||
.map((s) => `${s?.sampleCode || ''}:${s?.sampleStatus || s?.status || ''}`)
|
||
.filter((x) => x !== ':')
|
||
.join(',')
|
||
const bs = pick(beforeSamples)
|
||
const as = pick(afterSamples)
|
||
if (bs || as) {
|
||
if (bs && as && bs !== as) parts.push(`样本 ${bs}→${as}`)
|
||
else if (as) parts.push(`样本 ${as}`)
|
||
}
|
||
}
|
||
if (!parts.length) {
|
||
const remark = String(row.remark || '').trim()
|
||
return remark || '见前后快照'
|
||
}
|
||
return parts.join(';')
|
||
}
|
||
|
||
const STATUS_CHANGE_TYPE_LABEL: Record<string, string> = {
|
||
activate: '生效',
|
||
expire: '过期',
|
||
void: '作废',
|
||
to_research: '转科研',
|
||
to_storage: '转细胞存储'
|
||
}
|
||
const STATUS_CHANGE_TAG: Record<string, 'success' | 'warning' | 'danger' | 'primary' | 'info'> = {
|
||
activate: 'success',
|
||
expire: 'warning',
|
||
void: 'danger',
|
||
to_research: 'primary',
|
||
to_storage: 'primary'
|
||
}
|
||
|
||
function statusChangeTypeLabel(changeType: string) {
|
||
return STATUS_CHANGE_TYPE_LABEL[changeType] || changeType || '—'
|
||
}
|
||
|
||
function statusChangeTagType(changeType: string) {
|
||
return STATUS_CHANGE_TAG[changeType] || 'info'
|
||
}
|
||
|
||
function statusChangeFlow(row: Record<string, unknown>) {
|
||
const from = String(row.fromStatus || '')
|
||
const to = String(row.toStatus || '')
|
||
const voidType = String(row.voidType || '')
|
||
const toLabel = protocolStatusDisplayLabel(to, to === 'yzf' ? voidType : '')
|
||
if (!from) return toLabel || '—'
|
||
return `${protocolStatusDisplayLabel(from)} → ${toLabel}`
|
||
}
|
||
|
||
function statusChangeDesc(row: Record<string, unknown>) {
|
||
const desc = String(row.changeDesc || '').trim()
|
||
if (String(row.changeType || '') === 'void') {
|
||
const vt = voidTypeLabel(String(row.voidType || ''))
|
||
if (vt && desc) return `作废类型:${vt};${desc}`
|
||
if (vt) return `作废类型:${vt}`
|
||
}
|
||
return desc || '—'
|
||
}
|
||
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 depositTransfers = computed(
|
||
() => (bundle.value?.depositTransfers 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>>) || []
|
||
)
|
||
|
||
/**
|
||
* v1 明细按期归组,结构对齐 v2 schedules(期汇总 + 多笔转账)
|
||
* - 趸交:整单视为第 1 期
|
||
* - 分期:按 installmentNo 归组
|
||
*/
|
||
const v1PayPeriods = computed(() => {
|
||
const details = paymentDetails.value
|
||
if (!details.length) return [] as Array<{
|
||
scheduleNo: number
|
||
dueAmount: number | null
|
||
dueDate: string
|
||
paidAmount: number
|
||
transfers: Array<Record<string, any>>
|
||
}>
|
||
|
||
const paidOf = (rows: Array<Record<string, any>>) =>
|
||
rows
|
||
.filter((d) => String(d.payStatus || '') === 'yjf')
|
||
.reduce((sum, d) => sum + (Number(d.transferAmount) || 0), 0)
|
||
|
||
const isFq = isInstallmentPayMode(String(payment.value?.payMode || ''))
|
||
if (!isFq) {
|
||
return [
|
||
{
|
||
scheduleNo: 1,
|
||
dueAmount:
|
||
details[0]?.dueAmount != null
|
||
? Number(details[0].dueAmount)
|
||
: payment.value?.contractAmount != null
|
||
? Number(payment.value.contractAmount)
|
||
: null,
|
||
dueDate: String(
|
||
payment.value?.lumpSumDeadline || details[0]?.installmentDueDate || ''
|
||
),
|
||
paidAmount: paidOf(details),
|
||
transfers: details
|
||
}
|
||
]
|
||
}
|
||
|
||
const groups = new Map<number, Array<Record<string, any>>>()
|
||
for (const d of details) {
|
||
const no = Number(d.installmentNo) || 0
|
||
const list = groups.get(no) || []
|
||
list.push(d)
|
||
groups.set(no, list)
|
||
}
|
||
return [...groups.entries()]
|
||
.sort((a, b) => a[0] - b[0])
|
||
.map(([no, rows]) => ({
|
||
scheduleNo: no || 1,
|
||
dueAmount: rows[0]?.dueAmount != null ? Number(rows[0].dueAmount) : null,
|
||
dueDate: String(rows[0]?.installmentDueDate || ''),
|
||
paidAmount: paidOf(rows),
|
||
transfers: rows
|
||
}))
|
||
})
|
||
|
||
/** 已收金额 = 已缴费明细的转账金额之和(排除待缴费、退费确认中、已退费) */
|
||
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
|
||
const row = props.listRow as Record<string, any>
|
||
return {
|
||
makeStatus: row.makeStatus ?? '0',
|
||
certImageUrls: row.certImageUrls ?? '',
|
||
makeDate: row.makeDate ?? '',
|
||
handleTime: row.handleTime ?? '',
|
||
handlerUserId: row.handlerUserId ?? '',
|
||
handlerUserName: row.handlerUserName ?? '',
|
||
remark: row.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 {
|
||
handleTime: row.handleTime ?? '',
|
||
handlerUserId: row.handlerUserId ?? '',
|
||
handlerUserName: row.handlerUserName ?? '',
|
||
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: '' }
|
||
})
|
||
})
|
||
|
||
/** 金额只读展示:千分位 + ¥ 前缀,null 显示 — */
|
||
function fmtMoney(v: unknown): string {
|
||
if (v == null || v === '') return '—'
|
||
return '¥' + formatMoneyDisplay(v as string | number)
|
||
}
|
||
|
||
const basicMotherNameDisplay = computed(() =>
|
||
dash(
|
||
partyCustomer.value?.motherName || props.listRow?.motherName || protocol.value?.motherName
|
||
)
|
||
)
|
||
|
||
const dialogTitle = computed(() => {
|
||
const name =
|
||
partyCustomer.value?.motherName || props.listRow?.motherName || protocol.value?.motherName
|
||
return `查看 — ${formatProtocolDocSubject('cell_storage', { motherName: name })}`
|
||
})
|
||
|
||
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 regionManagerDisplay = computed(() => {
|
||
const fromList = (props.listRow as any)?.regionManagerUserName
|
||
if (fromList) return String(fromList)
|
||
const fromBundle = (collectionInfo.value as any)?.regionManagerUserName
|
||
if (fromBundle) return String(fromBundle)
|
||
const id = (collectionInfo.value as any)?.regionManagerUserId
|
||
return 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 samplePayTitle(sp: Record<string, any>) {
|
||
const code = String(sp.sampleCode || '').trim()
|
||
const sample = samples.value.find((s) => String(s.sampleCode || '') === code)
|
||
if (sample?.sampleTypeName) return String(sample.sampleTypeName)
|
||
if (sample?.sampleTypeId) return sampleTypeName(String(sample.sampleTypeId))
|
||
return code || '样本'
|
||
}
|
||
|
||
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 transferAttachments(tr: Record<string, any>) {
|
||
if (Array.isArray(tr.attachments) && tr.attachments.length) return tr.attachments
|
||
return urlsToAttachmentFiles(String(tr.transferAttachments || ''))
|
||
}
|
||
|
||
function financeConfirmAttachmentFiles(detail: Record<string, any>) {
|
||
if (Array.isArray(detail.financeConfirmAttachmentFiles)) {
|
||
return detail.financeConfirmAttachmentFiles
|
||
}
|
||
return urlsToAttachmentFiles(String(detail.financeConfirmAttachments || ''))
|
||
}
|
||
|
||
function refundRecordAttachments(row: Record<string, any>) {
|
||
if (Array.isArray(row.refundAttachmentFiles) && row.refundAttachmentFiles.length) {
|
||
return row.refundAttachmentFiles
|
||
}
|
||
return urlsToAttachmentFiles(String(row.refundAttachments || ''))
|
||
}
|
||
|
||
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 loadStatusChangeRecords() {
|
||
if (!props.protocolId) {
|
||
statusChangeRecords.value = []
|
||
return
|
||
}
|
||
statusChangeLoading.value = true
|
||
try {
|
||
const res = await fetchCollectionProtocolStatusChanges(props.protocolId)
|
||
statusChangeRecords.value = res.list || []
|
||
} catch {
|
||
statusChangeRecords.value = []
|
||
} finally {
|
||
statusChangeLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function loadOperationLogRecords() {
|
||
if (!props.protocolId) {
|
||
operationLogRecords.value = []
|
||
return
|
||
}
|
||
operationLogLoading.value = true
|
||
try {
|
||
const res = await fetchCollectionProtocolOperationLogs(props.protocolId)
|
||
operationLogRecords.value = res.list || []
|
||
} catch {
|
||
operationLogRecords.value = []
|
||
} finally {
|
||
operationLogLoading.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 = []
|
||
statusChangeRecords.value = []
|
||
operationLogRecords.value = []
|
||
|
||
const baseLookupsPromise = loadBaseLookups().catch(() => {})
|
||
const auditPromise = props.showAuditHistory ? loadAuditRecords() : null
|
||
const statusChangePromise = loadStatusChangeRecords()
|
||
const operationLogPromise = loadOperationLogRecords()
|
||
|
||
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
|
||
void statusChangePromise
|
||
void operationLogPromise
|
||
if (props.highlightSampleCode) {
|
||
nextTick(() => {
|
||
document
|
||
.getElementById(`view-sample-${props.highlightSampleCode}`)
|
||
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||
})
|
||
}
|
||
}
|
||
|
||
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 = []
|
||
statusChangeRecords.value = []
|
||
operationLogRecords.value = []
|
||
activeTab.value = 'basic'
|
||
detailLoading.value = false
|
||
lookupLoading.value = false
|
||
auditLoading.value = false
|
||
statusChangeLoading.value = false
|
||
}
|
||
|
||
watch(
|
||
() => [props.modelValue, props.protocolId] as const,
|
||
([visible, id]) => {
|
||
if (visible) {
|
||
void loadTransferTypeDict()
|
||
void loadVoidTypeDict()
|
||
}
|
||
if (visible && id) loadDetail()
|
||
}
|
||
)
|
||
|
||
watch(
|
||
() => [props.modelValue, props.highlightSampleCode, bundle.value] as const,
|
||
([visible, code]) => {
|
||
if (!visible || !code || !bundle.value) return
|
||
nextTick(() => {
|
||
document.getElementById(`view-sample-${code}`)?.scrollIntoView({
|
||
behavior: 'smooth',
|
||
block: 'center'
|
||
})
|
||
})
|
||
}
|
||
)
|
||
</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));
|
||
}
|
||
|
||
&--6 {
|
||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||
}
|
||
|
||
&--7 {
|
||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
.data-grid--collection-row {
|
||
gap: 12px 14px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
|
||
/* 协议附件 / 知情同意书 / 孕妇健康调查表(三行堆叠) */
|
||
.protocol-basic-attachments-block {
|
||
margin-top: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
|
||
&--flush {
|
||
margin-top: 0;
|
||
}
|
||
}
|
||
|
||
.handle-summary-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.handle-summary-card {
|
||
background: #fff;
|
||
border-radius: 10px;
|
||
padding: 18px 20px;
|
||
border: 1px solid rgba(0, 0, 0, 0.03);
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||
|
||
.attachment-row,
|
||
.remark-row {
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
}
|
||
|
||
.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);
|
||
}
|
||
|
||
&.is-highlight {
|
||
border-color: var(--primary, #10b981);
|
||
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.18);
|
||
}
|
||
}
|
||
|
||
.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 {
|
||
margin: 0 0 10px;
|
||
padding-left: 10px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
color: #334155;
|
||
border-left: 3px solid #10b981;
|
||
}
|
||
|
||
/* 样本卡内子组件小节标题:与 sample-card__section-label 统一 */
|
||
:deep(.sample-card__section-label),
|
||
:deep(.sample-qc-grouped-panel__subtitle),
|
||
:deep(.detail-section-title) {
|
||
margin: 0 0 10px;
|
||
padding-left: 10px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
color: #334155;
|
||
border-left: 3px solid #10b981;
|
||
}
|
||
|
||
.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;
|
||
|
||
&--3 {
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
}
|
||
|
||
.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;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sample-pay-readonly__title {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: center;
|
||
}
|
||
|
||
.sample-pay-readonly__amt {
|
||
flex-shrink: 0;
|
||
color: #047857;
|
||
}
|
||
|
||
.sample-pay-readonly__batch-title {
|
||
margin: 14px 0 8px;
|
||
padding-left: 10px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
line-height: 1.4;
|
||
color: #334155;
|
||
border-left: 3px solid #10b981;
|
||
|
||
&--mt {
|
||
margin-top: 20px;
|
||
}
|
||
}
|
||
|
||
.sample-pay-readonly__empty {
|
||
margin: 4px 0 8px;
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
|
||
&--sm {
|
||
margin: 0;
|
||
padding: 2px 0 0;
|
||
}
|
||
}
|
||
|
||
.sample-pay-readonly__sch {
|
||
margin-top: 10px;
|
||
padding: 12px 12px 10px;
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
border: 1px solid #eef2f7;
|
||
}
|
||
|
||
.sample-pay-readonly__sch-title {
|
||
margin-bottom: 10px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.sample-pay-readonly__tr-title {
|
||
margin: 12px 0 8px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #475569;
|
||
}
|
||
|
||
.sample-pay-readonly__tr-count {
|
||
font-weight: 400;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.sample-pay-readonly__sch .view-inner-table-wrap {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.muted-dash {
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
}
|
||
|
||
/* ---- v1 缴费明细(简洁平铺) ---- */
|
||
.pay-detail-simple {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.pay-detail-simple__card {
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--mint-border, #e2e8f0);
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
}
|
||
|
||
.pay-detail-simple__head {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.pay-detail-simple__title {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: center;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #0f172a;
|
||
}
|
||
|
||
.pay-detail-simple__amt {
|
||
flex-shrink: 0;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: #047857;
|
||
}
|
||
|
||
.pay-detail-simple__attach {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: flex-start;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
|
||
.pay-detail-simple__attach-label {
|
||
flex-shrink: 0;
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
color: #64748b;
|
||
}
|
||
|
||
.pay-detail-simple__extra {
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px dashed #e2e8f0;
|
||
}
|
||
|
||
/* ---- 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,
|
||
.handle-summary-cards {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.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,
|
||
.handle-summary-cards {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.data-grid--4,
|
||
.data-grid--6,
|
||
.data-grid--7 {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
|
||
.payment-summary,
|
||
.payment-summary--3 {
|
||
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>
|