提交
This commit is contained in:
1 parent
48dcf991dc
commit
be2226e4ca
100 files changed
+61340
-40975
No files matched your search
@@ -43,6 +43,12 @@ export function fetchCollectionProtocolDetail(id: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCollectionProtocolStatusChanges(protocolId: string) {
|
||||
return request.get<{ list: Array<Record<string, unknown>> }>({
|
||||
url: `/web/admin/collection-protocols/${protocolId}/status-changes`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCollectionProtocolAuditList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
|
||||
@@ -110,6 +110,12 @@ export function fetchSubmitDonationAudit(
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDonationStatusChanges(donationId: string) {
|
||||
return request.get<{ list: Array<Record<string, unknown>> }>({
|
||||
url: `/web/admin/research-donations/${donationId}/status-changes`
|
||||
})
|
||||
}
|
||||
|
||||
// 状态变更
|
||||
export function fetchDonationStatusChange(
|
||||
donationId: string,
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
<div v-loading="loading" class="sample-pay-detail">
|
||||
<template v-if="detail">
|
||||
<div class="detail-summary form-grid-4">
|
||||
<div class="detail-field">
|
||||
<label>协议号</label>
|
||||
<span>{{ detail.protocolNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<label>客户</label>
|
||||
<span>
|
||||
|
||||
@@ -72,7 +72,13 @@
|
||||
>
|
||||
<template #default="{ row }">{{ fmtMoney(row.price) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="存储年限" min-width="96" align="center" header-align="center">
|
||||
<el-table-column
|
||||
v-if="!hideCollectDetailFields"
|
||||
label="存储年限"
|
||||
min-width="96"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
row.storageYears != null ? row.storageYears + '年' : '—'
|
||||
}}</template>
|
||||
@@ -413,8 +419,9 @@
|
||||
siteOptions?: Api.Site.SiteItem[]
|
||||
expressCompanyOptions?: Api.ExpressCompany.ExpressCompanyItem[]
|
||||
showPrice?: boolean
|
||||
/** 科研捐赠等:列表与样本弹窗均不展示存储年限及相关采集明细 */
|
||||
hideCollectDetailFields?: boolean
|
||||
/** 添加/编辑样本弹窗内隐藏采集量、单位(不影响列表列与存储年限) */
|
||||
/** 添加/编辑样本弹窗内隐藏采集量、单位(不影响列表列) */
|
||||
hideDialogCollectAmountUnit?: boolean
|
||||
autoGenerateSampleCode?: boolean
|
||||
reserveSampleCode?: () => Promise<string>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- OA 审批详情 · 评论输入(列表已并入审批进度时间线) -->
|
||||
<template>
|
||||
<section v-if="instanceStatus === 'running'" class="oa-comment-panel">
|
||||
<section class="oa-comment-panel">
|
||||
<div class="oa-comment-panel__composer">
|
||||
<div class="oa-comment-panel__editor">
|
||||
<el-input
|
||||
|
||||
@@ -147,6 +147,14 @@ export const payStatus: StatusRegistry = {
|
||||
export const payStatusLabel = createLabelGetter(payStatus)
|
||||
export const payStatusTagType = createTagTypeGetter(payStatus)
|
||||
|
||||
/** 采集协议开票申请状态(biz_protocol_invoice_request.status) */
|
||||
export const invoiceRequestStatus: StatusRegistry = {
|
||||
dkp: { label: '待开票', tagType: 'warning' },
|
||||
ykp: { label: '已开票', tagType: 'success' }
|
||||
}
|
||||
export const invoiceRequestStatusLabel = createLabelGetter(invoiceRequestStatus)
|
||||
export const invoiceRequestStatusTagType = createTagTypeGetter(invoiceRequestStatus)
|
||||
|
||||
export const auditStatus: StatusRegistry = {
|
||||
shtg: { label: '审核通过', tagType: 'success' },
|
||||
shth: { label: '审核退回', tagType: 'danger' }
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="协议号/客户/样本/电话"
|
||||
placeholder="客户/样本/电话"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
@@ -69,13 +69,6 @@
|
||||
align="center"
|
||||
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="protocolNo"
|
||||
label="协议号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="客户" min-width="140" align="center">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.motherName || '—' }}</div>
|
||||
@@ -323,12 +316,12 @@
|
||||
|
||||
function formatAmount(v: unknown) {
|
||||
if (v == null || v === '') return '—'
|
||||
return formatMoneyDisplay(v)
|
||||
return formatMoneyDisplay(v as string | number)
|
||||
}
|
||||
|
||||
function formatAmountYuan(v: unknown) {
|
||||
if (v == null || v === '') return '—'
|
||||
return `${formatMoneyDisplay(v)} 元`
|
||||
return `${formatMoneyDisplay(v as string | number)} 元`
|
||||
}
|
||||
|
||||
function hasPaidAmount(row: Record<string, any>) {
|
||||
@@ -414,7 +407,8 @@
|
||||
const scheduleId = String(row.scheduleId || '')
|
||||
const samplePaymentId = String(row.samplePaymentId || '')
|
||||
if (!scheduleId || !samplePaymentId) return
|
||||
periodDialogTitle.value = `${row.motherName || ''} · ${row.protocolNo || ''} · 第 ${row.scheduleNo || '—'} 期转账记录`
|
||||
const titleParts = [row.motherName, row.sampleCode].filter(Boolean)
|
||||
periodDialogTitle.value = `${titleParts.join(' · ')}${titleParts.length ? ' · ' : ''}第 ${row.scheduleNo || '—'} 期转账记录`
|
||||
periodDialogVisible.value = true
|
||||
periodLoading.value = true
|
||||
periodTransfers.value = []
|
||||
|
||||
@@ -55,10 +55,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pending-card__grid">
|
||||
<div class="pending-card__field">
|
||||
<label>协议号</label>
|
||||
<span>{{ row.protocolNo || '-' }}</span>
|
||||
</div>
|
||||
<div class="pending-card__field">
|
||||
<label>包含期数</label>
|
||||
<span>{{ row.installmentSummary || '-' }}</span>
|
||||
@@ -70,7 +66,12 @@
|
||||
</div>
|
||||
<div class="pending-card__foot">
|
||||
<div class="pending-card__meta">
|
||||
<el-tag type="warning" size="small">待开票</el-tag>
|
||||
<el-tag
|
||||
:type="invoiceRequestStatusTagType(String(row.status || 'dkp'))"
|
||||
size="small"
|
||||
>
|
||||
{{ invoiceRequestStatusLabel(String(row.status || 'dkp')) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button class="btn-op-item primary" @click="openProcess(row)">
|
||||
<el-icon><Check /></el-icon>处理开票
|
||||
@@ -199,8 +200,11 @@
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'ykp' ? 'success' : 'warning'" size="small">
|
||||
{{ row.status === 'ykp' ? '已开票' : '待开票' }}
|
||||
<el-tag
|
||||
:type="invoiceRequestStatusTagType(String(row.status || ''))"
|
||||
size="small"
|
||||
>
|
||||
{{ invoiceRequestStatusLabel(String(row.status || '')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -254,9 +258,6 @@
|
||||
<el-form-item label="客户姓名">
|
||||
<span class="view-field-text">{{ processRow.motherName || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="协议号">
|
||||
<span class="view-field-text">{{ processRow.protocolNo || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证号">
|
||||
<span class="view-field-text">{{ maskIdCard(processRow.motherIdCard) || '—' }}</span>
|
||||
</el-form-item>
|
||||
@@ -371,9 +372,6 @@
|
||||
<el-form-item label="客户姓名">
|
||||
<span class="view-field-text">{{ viewRow.motherName || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="协议号">
|
||||
<span class="view-field-text">{{ viewRow.protocolNo || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证号">
|
||||
<span class="view-field-text">{{ maskIdCard(viewRow.motherIdCard) || '—' }}</span>
|
||||
</el-form-item>
|
||||
@@ -394,8 +392,11 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<span class="view-field-tag">
|
||||
<el-tag :type="viewRow.status === 'ykp' ? 'success' : 'warning'" size="small">
|
||||
{{ viewRow.status === 'ykp' ? '已开票' : '待开票' }}
|
||||
<el-tag
|
||||
:type="invoiceRequestStatusTagType(String(viewRow.status || ''))"
|
||||
size="small"
|
||||
>
|
||||
{{ invoiceRequestStatusLabel(String(viewRow.status || '')) }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</el-form-item>
|
||||
@@ -507,6 +508,10 @@
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import { transferTypeLabel, transferTypeTagType, loadTransferTypeDict } from '@/utils/protocol-payment-labels'
|
||||
import {
|
||||
invoiceRequestStatusLabel,
|
||||
invoiceRequestStatusTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
defineOptions({ name: 'CellStorageInvoiceRequestTab' })
|
||||
|
||||
@@ -93,7 +93,12 @@
|
||||
</div>
|
||||
<div class="pending-card__foot">
|
||||
<div class="pending-card__meta">
|
||||
<el-tag type="warning" size="small">退费确认中</el-tag>
|
||||
<el-tag
|
||||
:type="payStatusTagType(String(row.payStatus || 'tfqrz'))"
|
||||
size="small"
|
||||
>
|
||||
{{ payStatusLabel(String(row.payStatus || 'tfqrz')) }}
|
||||
</el-tag>
|
||||
<span>提交 {{ formatDisplayOrDash(row.createTime) }}</span>
|
||||
</div>
|
||||
<el-button class="btn-op-item primary" @click="openConfirm(row)">
|
||||
@@ -332,8 +337,13 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default>
|
||||
<el-tag type="success" size="small">已退费</el-tag>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="payStatusTagType(String(row.payStatus || 'ytf'))"
|
||||
size="small"
|
||||
>
|
||||
{{ payStatusLabel(String(row.payStatus || 'ytf')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -485,6 +495,8 @@
|
||||
import {
|
||||
formatPaymentDueDate,
|
||||
payModeLabel,
|
||||
payStatusLabel,
|
||||
payStatusTagType,
|
||||
protocolStatusLabel,
|
||||
protocolStatusDisplayLabel,
|
||||
protocolStatusTagType,
|
||||
|
||||
@@ -55,9 +55,7 @@
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="pendingSearchForm.keyword"
|
||||
:placeholder="
|
||||
isPresignTab ? '协议号/客户姓名' : '协议号/样本编码/客户姓名/身份证号'
|
||||
"
|
||||
:placeholder="isPresignTab ? '客户姓名' : '样本编码/客户姓名/身份证号'"
|
||||
clearable
|
||||
@keyup.enter="handlePendingSearch"
|
||||
/>
|
||||
@@ -107,15 +105,6 @@
|
||||
(index) => (pendingPagination.page - 1) * pendingPagination.pageSize + index + 1
|
||||
"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="protocolNo"
|
||||
label="协议号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.protocolNo || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="sampleCode"
|
||||
:label="isPresignTab ? '样本类型' : '样本编码'"
|
||||
@@ -267,8 +256,13 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default>
|
||||
<el-tag type="warning" size="small">付款确认中</el-tag>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="payStatusTagType(String(row.payStatus || 'jfqrz'))"
|
||||
size="small"
|
||||
>
|
||||
{{ payStatusLabel(String(row.payStatus || 'jfqrz')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -334,9 +328,7 @@
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="confirmedSearchForm.keyword"
|
||||
:placeholder="
|
||||
isPresignTab ? '协议号/客户姓名' : '协议号/样本编码/客户姓名/身份证号'
|
||||
"
|
||||
:placeholder="isPresignTab ? '客户姓名' : '样本编码/客户姓名/身份证号'"
|
||||
clearable
|
||||
@keyup.enter="handleConfirmedSearch"
|
||||
/>
|
||||
@@ -386,15 +378,6 @@
|
||||
(index) => (confirmedPagination.page - 1) * confirmedPagination.pageSize + index + 1
|
||||
"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="protocolNo"
|
||||
label="协议号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.protocolNo || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="sampleCode"
|
||||
:label="isPresignTab ? '样本类型' : '样本编码'"
|
||||
@@ -546,8 +529,13 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default>
|
||||
<el-tag type="success" size="small">付款完成</el-tag>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="payStatusTagType(String(row.payStatus || 'yjf'))"
|
||||
size="small"
|
||||
>
|
||||
{{ payStatusLabel(String(row.payStatus || 'yjf')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -715,6 +703,8 @@
|
||||
import {
|
||||
formatPaymentDueDate,
|
||||
payModeLabel,
|
||||
payStatusLabel,
|
||||
payStatusTagType,
|
||||
protocolStatusDisplayLabel,
|
||||
protocolStatusTagType,
|
||||
loadTransferTypeDict,
|
||||
@@ -745,7 +735,7 @@
|
||||
|
||||
function formatPaymentAmount(v: unknown): string {
|
||||
if (v == null || v === '') return '-'
|
||||
return '¥' + formatMoneyDisplay(v)
|
||||
return '¥' + formatMoneyDisplay(v as string | number)
|
||||
}
|
||||
|
||||
function isPresignRow(row: Record<string, unknown> | null | undefined) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="协议号/客户/样本编码/电话"
|
||||
placeholder="客户/样本编码/电话"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
@@ -80,14 +80,6 @@
|
||||
header-align="center"
|
||||
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="protocolNo"
|
||||
label="协议号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="客户" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="customer-cell">
|
||||
@@ -220,10 +212,6 @@
|
||||
<div v-loading="detailLoading" class="sample-pay-detail">
|
||||
<template v-if="detail">
|
||||
<div class="detail-summary form-grid-2">
|
||||
<div class="detail-field">
|
||||
<label>协议号</label>
|
||||
<span>{{ detail.protocolNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<label>客户</label>
|
||||
<span>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="协议号/客户/样本编码/电话"
|
||||
placeholder="客户/样本编码/电话"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
@@ -363,12 +363,12 @@
|
||||
|
||||
function formatAmount(v: unknown) {
|
||||
if (v == null || v === '') return '—'
|
||||
return formatMoneyDisplay(v)
|
||||
return formatMoneyDisplay(v as string | number)
|
||||
}
|
||||
|
||||
function formatAmountYuan(v: unknown) {
|
||||
if (v == null || v === '') return '—'
|
||||
return `${formatMoneyDisplay(v)} 元`
|
||||
return `${formatMoneyDisplay(v as string | number)} 元`
|
||||
}
|
||||
|
||||
function transferAttachmentFiles(tr: Record<string, any>) {
|
||||
@@ -491,7 +491,8 @@
|
||||
async function openPeriodDialog(row: Record<string, any>, idx: number) {
|
||||
const cell = periodRaw(row, idx)
|
||||
if (!cell?.scheduleId) return
|
||||
periodDialogTitle.value = `${row.motherName || ''} · ${row.protocolNo || ''} · 第 ${idx + 1} 期转账记录`
|
||||
const titleParts = [row.motherName, row.sampleCode].filter(Boolean)
|
||||
periodDialogTitle.value = `${titleParts.join(' · ')}${titleParts.length ? ' · ' : ''}第 ${idx + 1} 期转账记录`
|
||||
periodDialogVisible.value = true
|
||||
periodLoading.value = true
|
||||
periodTransfers.value = []
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
@closed="resetForm"
|
||||
>
|
||||
<DialogFormBody v-loading="loading">
|
||||
<el-form ref="formRef" :model="form" label-position="top" scroll-to-error>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:disabled="loading || !formReady"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="业务日期">
|
||||
<el-date-picker
|
||||
@@ -371,7 +377,7 @@
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
:disabled="isConverted"
|
||||
:disabled="isConverted || loading || !formReady"
|
||||
@click="handleSave"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
@@ -430,6 +436,7 @@
|
||||
|
||||
const formRef = ref()
|
||||
const loading = ref(false)
|
||||
const formReady = ref(false)
|
||||
const saving = ref(false)
|
||||
const transferTypeOptions = ref<Api.Dict.DictDataOption[]>([])
|
||||
const sampleTypeOptions = ref<Array<Record<string, any>>>([])
|
||||
@@ -457,7 +464,7 @@
|
||||
nickName: String(
|
||||
currentUser?.realName || currentUser?.userName || currentUser?.username || ''
|
||||
),
|
||||
phoneNo: String(currentUser?.phone || currentUser?.phoneNo || ''),
|
||||
phoneNo: String(currentUser?.phone || ''),
|
||||
deptId: '',
|
||||
userNo: '',
|
||||
email: '',
|
||||
@@ -572,13 +579,14 @@
|
||||
fileUrl: '',
|
||||
_uploading: true
|
||||
})
|
||||
const idx = row.attachments.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'presign-transfer')
|
||||
fd.append('file', file)
|
||||
const res: any = await fetchUploadAttachment(fd)
|
||||
const data = res?.data || res
|
||||
const idx = row.attachments.findIndex((f: TransferAttachment) => f.id === tempId)
|
||||
if (idx < 0) return
|
||||
row.attachments[idx] = {
|
||||
id: data?.id || tempId,
|
||||
fileName: data?.fileName || file.name,
|
||||
@@ -587,7 +595,8 @@
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
row.attachments.splice(idx, 1)
|
||||
const idx = row.attachments.findIndex((f: TransferAttachment) => f.id === tempId)
|
||||
if (idx >= 0) row.attachments.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,6 +795,10 @@
|
||||
}
|
||||
|
||||
async function submitTransfer(tr: Record<string, any>) {
|
||||
if ((tr.attachments || []).some((f: TransferAttachment) => f._uploading)) {
|
||||
ElMessage.warning('凭证正在上传,请稍后再提交')
|
||||
return
|
||||
}
|
||||
if (!props.orderId || !tr.id) {
|
||||
ElMessage.warning('请先保存订单后再提交财务确认')
|
||||
return
|
||||
@@ -794,9 +807,9 @@
|
||||
await fetchUpdatePresignTransfer(String(tr.id), {
|
||||
transferType: tr.transferType,
|
||||
transferAmount: tr.transferAmount,
|
||||
transferTime: tr.transferTime || undefined,
|
||||
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
|
||||
remark: tr.remark || undefined
|
||||
transferTime: tr.transferTime || '',
|
||||
transferAttachments: attachmentsToUrls(tr.attachments),
|
||||
remark: tr.remark || ''
|
||||
})
|
||||
}
|
||||
await fetchSubmitPresignTransferFinance(String(tr.id))
|
||||
@@ -817,6 +830,19 @@
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (saving.value) return
|
||||
if (loading.value || !formReady.value) {
|
||||
ElMessage.warning('订单尚未初始化成功,请稍候或重新打开后重试')
|
||||
return
|
||||
}
|
||||
if (
|
||||
form.transfers.some((tr) =>
|
||||
(tr.attachments || []).some((f: TransferAttachment) => f._uploading)
|
||||
)
|
||||
) {
|
||||
ElMessage.warning('凭证正在上传,请稍后再保存')
|
||||
return
|
||||
}
|
||||
if (!form.motherName.trim()) {
|
||||
ElMessage.warning('请填写母亲姓名')
|
||||
return
|
||||
@@ -830,17 +856,17 @@
|
||||
const payload: Record<string, unknown> = {
|
||||
orderDate: form.orderDate || undefined,
|
||||
motherName: form.motherName,
|
||||
expectedDeliveryDate: form.expectedDeliveryDate || undefined,
|
||||
regionId: form.regionId || undefined,
|
||||
hospitalId: form.hospitalId || undefined,
|
||||
sampleCollectTime: form.sampleCollectTime || undefined,
|
||||
collectorUserId: form.collectorUserId || undefined,
|
||||
regionManagerUserId: form.regionManagerUserId || undefined,
|
||||
expectedDeliveryDate: form.expectedDeliveryDate || '',
|
||||
regionId: form.regionId || '',
|
||||
hospitalId: form.hospitalId || '',
|
||||
sampleCollectTime: form.sampleCollectTime || '',
|
||||
collectorUserId: form.collectorUserId || '',
|
||||
regionManagerUserId: form.regionManagerUserId || '',
|
||||
payMode: form.payMode || undefined,
|
||||
contractAmount: form.contractAmount ?? undefined,
|
||||
contractAmount: form.contractAmount ?? null,
|
||||
orderStatus: form.orderStatus || 'active',
|
||||
sampleTypeIds: form.sampleTypeIds,
|
||||
remark: form.remark || undefined
|
||||
remark: form.remark || ''
|
||||
}
|
||||
let transfersForSubmit: Array<Record<string, any>> = []
|
||||
|
||||
@@ -851,18 +877,18 @@
|
||||
const created: any = await fetchCreatePresignTransfer(props.orderId, {
|
||||
transferType: tr.transferType,
|
||||
transferAmount: tr.transferAmount,
|
||||
transferTime: tr.transferTime || undefined,
|
||||
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
|
||||
remark: tr.remark || undefined
|
||||
transferTime: tr.transferTime || '',
|
||||
transferAttachments: attachmentsToUrls(tr.attachments),
|
||||
remark: tr.remark || ''
|
||||
})
|
||||
if (created?.id) tr.id = String(created.id)
|
||||
} else if (tr.id && tr.payStatus === 'dfj') {
|
||||
await fetchUpdatePresignTransfer(String(tr.id), {
|
||||
transferType: tr.transferType,
|
||||
transferAmount: tr.transferAmount,
|
||||
transferTime: tr.transferTime || undefined,
|
||||
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
|
||||
remark: tr.remark || undefined
|
||||
transferTime: tr.transferTime || '',
|
||||
transferAttachments: attachmentsToUrls(tr.attachments),
|
||||
remark: tr.remark || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -873,9 +899,9 @@
|
||||
.map((t) => ({
|
||||
transferType: t.transferType,
|
||||
transferAmount: t.transferAmount,
|
||||
transferTime: t.transferTime || undefined,
|
||||
transferAttachments: attachmentsToUrls(t.attachments) || undefined,
|
||||
remark: t.remark || undefined
|
||||
transferTime: t.transferTime || '',
|
||||
transferAttachments: attachmentsToUrls(t.attachments),
|
||||
remark: t.remark || ''
|
||||
}))
|
||||
const createdOrder: any = await fetchCreatePresignOrder(payload)
|
||||
transfersForSubmit = Array.isArray(createdOrder?.transfers)
|
||||
@@ -901,9 +927,19 @@
|
||||
() => props.modelValue,
|
||||
async (open) => {
|
||||
if (!open) return
|
||||
loading.value = true
|
||||
formReady.value = false
|
||||
resetForm()
|
||||
await loadLookups()
|
||||
if (props.orderId) await loadDetail()
|
||||
try {
|
||||
await loadLookups()
|
||||
if (props.orderId) await loadDetail()
|
||||
formReady.value = true
|
||||
} catch {
|
||||
ElMessage.error('订单初始化失败,请重新打开后重试')
|
||||
emit('update:modelValue', false)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="母亲姓名/协议编号"
|
||||
placeholder="母亲姓名"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
@@ -81,15 +81,6 @@
|
||||
>
|
||||
<template #default="{ row }">{{ formatYmd(row.orderDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="protocolNo"
|
||||
label="协议编号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.protocolNo || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="motherName"
|
||||
label="母亲姓名"
|
||||
@@ -217,7 +208,7 @@
|
||||
|
||||
function fmtMoney(v: unknown) {
|
||||
if (v == null || v === '') return '-'
|
||||
return '¥' + formatMoneyDisplay(v)
|
||||
return '¥' + formatMoneyDisplay(v as string | number)
|
||||
}
|
||||
|
||||
function formatYmd(v: unknown) {
|
||||
@@ -276,9 +267,8 @@
|
||||
'转正式协议',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
const res: any = await fetchConvertPresignOrder(String(row.id))
|
||||
const protocolNo = res?.protocolNo || res?.data?.protocolNo
|
||||
ElMessage.success(protocolNo ? `已生成正式协议 ${protocolNo}` : '已转正式协议')
|
||||
await fetchConvertPresignOrder(String(row.id))
|
||||
ElMessage.success('已转正式协议')
|
||||
loadData()
|
||||
}
|
||||
|
||||
|
||||
@@ -65,16 +65,6 @@
|
||||
header-align="center"
|
||||
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="protocolNo"
|
||||
label="协议号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.protocolNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="客户" min-width="150" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.motherName || '—' }}</div>
|
||||
|
||||
@@ -537,9 +537,7 @@
|
||||
}
|
||||
const row = rows[0]
|
||||
refundProtocolId.value = String(row.id || '')
|
||||
const name = String(row.motherName || '').trim()
|
||||
const no = String(row.protocolNo || '').trim()
|
||||
refundProtocolLabel.value = [name, no].filter(Boolean).join(' / ')
|
||||
refundProtocolLabel.value = String(row.motherName || '').trim()
|
||||
refundDialogVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
<SampleTransportFormSection
|
||||
v-model:samples="formData.samples"
|
||||
sample-source="cell_storage"
|
||||
:protocol-id="formData.id || ''"
|
||||
:protocol-id="editingId || ''"
|
||||
hide-dialog-collect-amount-unit
|
||||
:sample-type-options="sampleTypeOptions"
|
||||
:sample-type-options-loading="sampleTypeLoading"
|
||||
@@ -1232,9 +1232,6 @@
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import {
|
||||
payModeLabel,
|
||||
newbornSexLabel,
|
||||
deliveryModeLabel,
|
||||
sampleStatusLabel,
|
||||
loadTransferTypeDict,
|
||||
getTransferTypeOptions,
|
||||
transferTypeLabel,
|
||||
@@ -1262,7 +1259,7 @@
|
||||
/** 金额只读展示:千分位 + ¥ 前缀 */
|
||||
function fmtMoney(v: unknown): string {
|
||||
if (v == null || v === '') return '—'
|
||||
return '¥' + formatMoneyDisplay(v)
|
||||
return '¥' + formatMoneyDisplay(v as string | number)
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
@@ -1469,6 +1466,7 @@
|
||||
|
||||
/* ====== 新增/编辑弹窗 ====== */
|
||||
const dialogFormLoading = ref(false)
|
||||
const editDetailLoaded = ref(false)
|
||||
const dialogTitle = ref('新增协议单')
|
||||
const isEdit = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
@@ -2078,6 +2076,7 @@
|
||||
|
||||
function handleOpenForAdd() {
|
||||
isEdit.value = false
|
||||
editingId.value = null
|
||||
dialogTitle.value = '新增协议单'
|
||||
resetForm()
|
||||
formData.protocolStatus = 'sxz'
|
||||
@@ -2278,6 +2277,7 @@
|
||||
if (!row || !props.editId) return
|
||||
|
||||
isEdit.value = true
|
||||
editDetailLoaded.value = false
|
||||
dialogTitle.value = '编辑协议单'
|
||||
editingId.value = props.editId
|
||||
resetForm()
|
||||
@@ -2296,11 +2296,11 @@
|
||||
try {
|
||||
const [, bundle] = await Promise.all([
|
||||
loadEthnicOptions(),
|
||||
fetchCollectionProtocolDetail(String(row.id)).catch(() => null)
|
||||
fetchCollectionProtocolDetail(String(row.id))
|
||||
])
|
||||
if (bundle) {
|
||||
applyProtocolBundleToForm(bundle as Record<string, any>, row)
|
||||
}
|
||||
if (!bundle) throw new Error('协议详情为空')
|
||||
applyProtocolBundleToForm(bundle as Record<string, any>, row)
|
||||
editDetailLoaded.value = true
|
||||
|
||||
const ciBundle = (bundle as any)?.collectionInfo as Record<string, any> | null
|
||||
const hydrateTasks: Promise<void>[] = [
|
||||
@@ -2345,7 +2345,10 @@
|
||||
hydrateSampleLookupOptions()
|
||||
}
|
||||
} catch {
|
||||
/* 保留列表行已填基础字段 */
|
||||
if (!editDetailLoaded.value) {
|
||||
ElMessage.error('协议详情加载失败,请重新打开后重试')
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
} finally {
|
||||
dialogFormLoading.value = false
|
||||
}
|
||||
@@ -2353,6 +2356,25 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!formRef.value || submitLoading.value) return
|
||||
if (isEdit.value && !editDetailLoaded.value) {
|
||||
ElMessage.warning('协议详情尚未加载成功,请重新打开后重试')
|
||||
return
|
||||
}
|
||||
const uploadLists = [
|
||||
formData.attachments,
|
||||
formData.consentAttachments,
|
||||
formData.healthSurveyAttachments,
|
||||
...formData.paymentDetails.map((d) => d.attachments || []),
|
||||
...formData.samplePayments.flatMap((p) =>
|
||||
(p.schedules || []).flatMap((sch: any) =>
|
||||
(sch.transfers || []).map((t: any) => t.attachments || [])
|
||||
)
|
||||
)
|
||||
]
|
||||
if (uploadLists.some((list) => list.some((f: any) => f._uploading))) {
|
||||
ElMessage.warning('附件正在上传,请稍后再保存')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
@@ -2438,12 +2460,11 @@
|
||||
.join(',')
|
||||
const payload: Record<string, unknown> = {
|
||||
protocolStatus: formData.protocolStatus,
|
||||
registerDate: formData.registerDate || undefined,
|
||||
collectorUserId: formData.collectorUserId || undefined,
|
||||
protocolAttachments: protocolAttachmentUrls || formData.protocolAttachments || undefined,
|
||||
consentFormUrls: consentAttachmentUrls || formData.consentFormUrls || undefined,
|
||||
healthSurveyFormUrls:
|
||||
healthSurveyAttachmentUrls || formData.healthSurveyFormUrls || undefined,
|
||||
registerDate: formData.registerDate || '',
|
||||
collectorUserId: formData.collectorUserId || '',
|
||||
protocolAttachments: protocolAttachmentUrls,
|
||||
consentFormUrls: consentAttachmentUrls,
|
||||
healthSurveyFormUrls: healthSurveyAttachmentUrls,
|
||||
attachments: formData.attachments.map((a) => ({
|
||||
id: a.id,
|
||||
fileName: a.fileName,
|
||||
@@ -2468,39 +2489,39 @@
|
||||
fileSizeDisplay: a.fileSizeDisplay,
|
||||
fileType: a.fileType
|
||||
})),
|
||||
remark: formData.remark || undefined,
|
||||
remark: formData.remark || '',
|
||||
partyCustomer: {
|
||||
customerId: formData.customerId || undefined,
|
||||
fatherCustomerId: formData.fatherCustomerId || undefined,
|
||||
motherName: formData.motherName || undefined,
|
||||
motherPhone: formData.motherPhone || undefined,
|
||||
motherIdCard: formData.motherIdCard || undefined,
|
||||
motherBirthYm: formData.motherBirthYm || undefined,
|
||||
motherEthnic: formData.motherEthnic || undefined,
|
||||
motherEmail: formData.motherEmail || undefined,
|
||||
motherAddress: formData.motherAddress || undefined,
|
||||
fatherName: formData.fatherName || undefined,
|
||||
fatherPhone: formData.fatherPhone || undefined,
|
||||
fatherIdCard: formData.fatherIdCard || undefined,
|
||||
newbornName: formData.newbornName || undefined,
|
||||
newbornIdCard: formData.newbornIdCard || undefined,
|
||||
expectedDeliveryDate: formData.expectedDeliveryDate || undefined,
|
||||
newbornDeliveryTime: formData.newbornDeliveryTime || undefined,
|
||||
newbornSex: formData.newbornSex || undefined,
|
||||
deliveryMode: formData.deliveryMode || undefined,
|
||||
birthOrder: formData.birthOrder ?? undefined,
|
||||
newbornWeightGram: formData.newbornWeightGram ?? undefined,
|
||||
apgar1min: formData.apgar1min ?? undefined,
|
||||
apgar5min: formData.apgar5min ?? undefined
|
||||
customerId: formData.customerId || '',
|
||||
fatherCustomerId: formData.fatherCustomerId || '',
|
||||
motherName: formData.motherName || '',
|
||||
motherPhone: formData.motherPhone || '',
|
||||
motherIdCard: formData.motherIdCard || '',
|
||||
motherBirthYm: formData.motherBirthYm || '',
|
||||
motherEthnic: formData.motherEthnic || '',
|
||||
motherEmail: formData.motherEmail || '',
|
||||
motherAddress: formData.motherAddress || '',
|
||||
fatherName: formData.fatherName || '',
|
||||
fatherPhone: formData.fatherPhone || '',
|
||||
fatherIdCard: formData.fatherIdCard || '',
|
||||
newbornName: formData.newbornName || '',
|
||||
newbornIdCard: formData.newbornIdCard || '',
|
||||
expectedDeliveryDate: formData.expectedDeliveryDate || '',
|
||||
newbornDeliveryTime: formData.newbornDeliveryTime || '',
|
||||
newbornSex: formData.newbornSex || '',
|
||||
deliveryMode: formData.deliveryMode || '',
|
||||
birthOrder: formData.birthOrder ?? null,
|
||||
newbornWeightGram: formData.newbornWeightGram ?? null,
|
||||
apgar1min: formData.apgar1min ?? null,
|
||||
apgar5min: formData.apgar5min ?? null
|
||||
},
|
||||
collectionInfo: {
|
||||
regionId: formData.regionId || undefined,
|
||||
hospitalId: formData.hospitalId || undefined,
|
||||
medicalStaff: formData.medicalStaff || undefined,
|
||||
salesUserId: formData.salesUserId || undefined,
|
||||
regionManagerUserId: formData.regionManagerUserId || undefined,
|
||||
sampleCollectTime: formData.sampleCollectTime || undefined,
|
||||
maternalBloodCollectTime: formData.maternalBloodCollectTime || undefined
|
||||
regionId: formData.regionId || '',
|
||||
hospitalId: formData.hospitalId || '',
|
||||
medicalStaff: formData.medicalStaff || '',
|
||||
salesUserId: formData.salesUserId || '',
|
||||
regionManagerUserId: formData.regionManagerUserId || '',
|
||||
sampleCollectTime: formData.sampleCollectTime || '',
|
||||
maternalBloodCollectTime: formData.maternalBloodCollectTime || ''
|
||||
},
|
||||
samples: formData.samples.map(({ _rowKey, transport, freight, freightDraft, ...s }) => ({
|
||||
...s,
|
||||
@@ -2560,7 +2581,9 @@
|
||||
transferType: t.transferType,
|
||||
transferAmount: t.transferAmount,
|
||||
transferTime: t.transferTime,
|
||||
transferAttachments: fromFiles || t.transferAttachments || undefined,
|
||||
transferAttachments: Array.isArray(t.attachments)
|
||||
? fromFiles
|
||||
: t.transferAttachments || '',
|
||||
payStatus: t.payStatus || 'dfj',
|
||||
remark: t.remark
|
||||
}
|
||||
@@ -3376,13 +3399,15 @@
|
||||
const q = (query || '').trim()
|
||||
if (!q) {
|
||||
cb([])
|
||||
return
|
||||
return []
|
||||
}
|
||||
try {
|
||||
const res = await fetchCustomerList({ page: 1, pageSize: 20, keyword: q })
|
||||
cb(res.list || [])
|
||||
return res.list || []
|
||||
} catch {
|
||||
cb([])
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3524,13 +3549,13 @@
|
||||
fileType: file.type || '',
|
||||
_uploading: true
|
||||
})
|
||||
const idx = list.length - 1
|
||||
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', folder)
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
const idx = list.findIndex((f) => f.id === tempId)
|
||||
if (idx < 0) return
|
||||
list[idx] = {
|
||||
...list[idx],
|
||||
id: res.id || tempId,
|
||||
@@ -3543,7 +3568,8 @@
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
list.splice(idx, 1)
|
||||
const idx = list.findIndex((f) => f.id === tempId)
|
||||
if (idx >= 0) list.splice(idx, 1)
|
||||
} finally {
|
||||
nextTick(() => uploadRef.value?.clearFiles())
|
||||
}
|
||||
@@ -3668,13 +3694,13 @@
|
||||
fileType: file.type || '',
|
||||
_uploading: true
|
||||
})
|
||||
const aIdx = detail.attachments.length - 1
|
||||
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'protocol-payment')
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
const aIdx = detail.attachments.findIndex((f) => f.id === tempId)
|
||||
if (aIdx < 0) return
|
||||
detail.attachments[aIdx] = {
|
||||
...detail.attachments[aIdx],
|
||||
id: res.id || tempId,
|
||||
@@ -3687,7 +3713,8 @@
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
detail.attachments.splice(aIdx, 1)
|
||||
const aIdx = detail.attachments.findIndex((f) => f.id === tempId)
|
||||
if (aIdx >= 0) detail.attachments.splice(aIdx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
width="960px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@@ -27,10 +27,17 @@
|
||||
<template #default="{ row }">{{ fmtMoney(row.refundAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="refundTime" label="退费时间" min-width="160" />
|
||||
<el-table-column prop="refundRemark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="附件" min-width="100">
|
||||
<el-table-column prop="refundRemark" label="备注" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="附件" min-width="280">
|
||||
<template #default="{ row }">
|
||||
{{ (row.refundAttachmentFiles || []).length || (row.refundAttachments ? 1 : 0) }} 个
|
||||
<ProtocolAttachmentReadonly
|
||||
v-if="rowAttachmentFiles(row).length"
|
||||
:files="rowAttachmentFiles(row)"
|
||||
size="compact"
|
||||
@preview="handleListPreview"
|
||||
@download="handleDownloadAttachment"
|
||||
/>
|
||||
<span v-else class="muted-dash">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="140" fixed="right">
|
||||
@@ -52,7 +59,7 @@
|
||||
<el-dialog
|
||||
v-model="formVisible"
|
||||
:title="editingId ? '编辑退费登记' : '新增退费登记'"
|
||||
width="560px"
|
||||
width="720px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@@ -87,14 +94,15 @@
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
ref="attachmentUploadRef"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.zip,.rar"
|
||||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||||
@change="onFileChange"
|
||||
>
|
||||
<div class="upload-card upload-card--sm">
|
||||
<el-icon :size="22" color="#047857"><Plus /></el-icon>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="28" color="#047857"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
@@ -102,17 +110,57 @@
|
||||
v-for="(file, idx) in form.attachments"
|
||||
:key="file.id || idx"
|
||||
class="attachment-item"
|
||||
:class="{ 'is-uploading': file._uploading }"
|
||||
>
|
||||
<div class="attachment-name" :title="file.fileName">{{ file.fileName }}</div>
|
||||
<el-icon
|
||||
class="action-btn action-btn--danger"
|
||||
@click="form.attachments.splice(idx, 1)"
|
||||
<div
|
||||
class="attachment-preview"
|
||||
@click="
|
||||
isImageFile(file) && file.fileUrl ? handlePreviewAttachment(file) : undefined
|
||||
"
|
||||
>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
<template v-if="isImageFile(file) && file.fileUrl">
|
||||
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||||
</template>
|
||||
<template v-else-if="isImageFile(file) && !file.fileUrl">
|
||||
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
|
||||
</template>
|
||||
<template v-else-if="isVideoFile(file)">
|
||||
<div class="attachment-preview__icon attachment-preview__icon--video">
|
||||
<el-icon :size="28"><VideoPlay /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="attachment-preview__icon attachment-preview__icon--doc">
|
||||
<el-icon :size="28"><Document /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="attachment-footer">
|
||||
<span class="attachment-name" :title="file.fileName">{{
|
||||
file.fileName
|
||||
}}</span>
|
||||
<div class="attachment-actions">
|
||||
<el-icon
|
||||
class="action-btn"
|
||||
title="下载"
|
||||
@click="handleDownloadAttachment(file)"
|
||||
>
|
||||
<Download />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="action-btn action-btn--danger"
|
||||
title="删除"
|
||||
@click="form.attachments.splice(idx, 1)"
|
||||
>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="upload-hint">支持图片与常见文档,单文件不超过 20MB</span>
|
||||
<span class="upload-hint"
|
||||
>支持拖拽或点击上传图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB</span
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -128,14 +176,42 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="文件预览"
|
||||
width="800px"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
>
|
||||
<div v-if="previewType === 'image'" class="preview-container">
|
||||
<img :src="previewUrl" alt="预览" style="max-width: 100%" />
|
||||
</div>
|
||||
<div v-else class="preview-container">
|
||||
<video :src="previewUrl" controls style="max-width: 100%" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { Plus, Edit, Delete, Close, Check } from '@element-plus/icons-vue'
|
||||
import { computed, nextTick, reactive, ref } from 'vue'
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Close,
|
||||
Check,
|
||||
Download,
|
||||
Document,
|
||||
VideoPlay,
|
||||
Loading
|
||||
} from '@element-plus/icons-vue'
|
||||
import AmountInput from '@/components/common/amount-input/index.vue'
|
||||
import ProtocolAttachmentReadonly from './ProtocolAttachmentReadonly.vue'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import {
|
||||
fetchProtocolRefundRecords,
|
||||
@@ -147,7 +223,7 @@
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
protocolId?: string
|
||||
/** 展示在标题中的协议标识,如母亲姓名 / 协议号 */
|
||||
/** 展示在标题中的协议标识,如母亲姓名 */
|
||||
protocolLabel?: string
|
||||
}>()
|
||||
|
||||
@@ -155,26 +231,114 @@
|
||||
'update:modelValue': [boolean]
|
||||
}>()
|
||||
|
||||
type RefundAttachmentItem = {
|
||||
id?: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
fileSize?: number
|
||||
fileSizeDisplay?: string
|
||||
fileType?: string
|
||||
_uploading?: boolean
|
||||
}
|
||||
|
||||
const records = ref<Array<Record<string, any>>>([])
|
||||
const loading = ref(false)
|
||||
const formVisible = ref(false)
|
||||
const editingId = ref('')
|
||||
const saving = ref(false)
|
||||
const attachmentUploadRef = ref()
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewType = ref<'image' | 'video'>('image')
|
||||
const form = reactive({
|
||||
refundAmount: null as number | null,
|
||||
refundTime: '',
|
||||
refundRemark: '',
|
||||
attachments: [] as Array<Record<string, any>>
|
||||
attachments: [] as RefundAttachmentItem[]
|
||||
})
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
const label = String(props.protocolLabel || '').trim()
|
||||
return label ? `退费登记 · ${label}` : '退费登记'
|
||||
})
|
||||
|
||||
function fullFileUrl(suffix: string) {
|
||||
if (!suffix) return ''
|
||||
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/')) {
|
||||
return suffix
|
||||
}
|
||||
return `${minioBaseUrl}/${suffix}`
|
||||
}
|
||||
|
||||
function isImageFile(file: RefundAttachmentItem) {
|
||||
const url = (file.fileUrl || '').toLowerCase()
|
||||
const name = (file.fileName || '').toLowerCase()
|
||||
return (
|
||||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/.test(url) ||
|
||||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(name) ||
|
||||
(file.fileType || '').startsWith('image/')
|
||||
)
|
||||
}
|
||||
|
||||
function isVideoFile(file: RefundAttachmentItem) {
|
||||
const url = (file.fileUrl || '').toLowerCase()
|
||||
const name = (file.fileName || '').toLowerCase()
|
||||
return (
|
||||
/\.(mp4|webm|ogg|mov|avi)(\?|$)/.test(url) ||
|
||||
/\.(mp4|webm|ogg|mov|avi)$/.test(name) ||
|
||||
(file.fileType || '').startsWith('video/')
|
||||
)
|
||||
}
|
||||
|
||||
function rowAttachmentFiles(row: Record<string, any>): RefundAttachmentItem[] {
|
||||
if (Array.isArray(row.refundAttachmentFiles) && row.refundAttachmentFiles.length) {
|
||||
return row.refundAttachmentFiles.map((f: Record<string, any>, index: number) => ({
|
||||
id: f.id || `refund-att-${index}`,
|
||||
fileName: f.fileName || '附件',
|
||||
fileUrl: f.fileUrl || '',
|
||||
fileSize: f.fileSize,
|
||||
fileType: f.fileType
|
||||
}))
|
||||
}
|
||||
const raw = String(row.refundAttachments || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
return raw.map((url, index) => {
|
||||
const name = url.includes('/') ? url.split('/').pop() || `附件${index + 1}` : url
|
||||
return {
|
||||
id: `refund-url-${index}`,
|
||||
fileName: decodeURIComponent(name),
|
||||
fileUrl: url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handlePreviewAttachment(file: RefundAttachmentItem) {
|
||||
if (file._uploading || !file.fileUrl) return
|
||||
previewUrl.value = fullFileUrl(file.fileUrl)
|
||||
previewType.value = isVideoFile(file) ? 'video' : 'image'
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
function handleListPreview(file: Record<string, any>) {
|
||||
handlePreviewAttachment({
|
||||
id: file.id,
|
||||
fileName: file.fileName || '附件',
|
||||
fileUrl: file.fileUrl || '',
|
||||
fileType: file.fileType
|
||||
})
|
||||
}
|
||||
|
||||
function handleDownloadAttachment(file: RefundAttachmentItem | Record<string, any>) {
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
function fmtMoney(v: unknown) {
|
||||
if (v == null || v === '') return '—'
|
||||
return formatMoneyDisplay(v)
|
||||
return formatMoneyDisplay(v as string | number)
|
||||
}
|
||||
|
||||
async function loadRecords() {
|
||||
@@ -203,7 +367,13 @@
|
||||
form.refundTime = String(row.refundTime || '')
|
||||
form.refundRemark = String(row.refundRemark || '')
|
||||
form.attachments = Array.isArray(row.refundAttachmentFiles)
|
||||
? [...row.refundAttachmentFiles]
|
||||
? row.refundAttachmentFiles.map((f: Record<string, any>) => ({
|
||||
id: f.id,
|
||||
fileName: f.fileName || '附件',
|
||||
fileUrl: f.fileUrl || '',
|
||||
fileSize: f.fileSize,
|
||||
fileType: f.fileType
|
||||
}))
|
||||
: []
|
||||
formVisible.value = true
|
||||
}
|
||||
@@ -223,24 +393,47 @@
|
||||
}
|
||||
|
||||
async function onFileChange(uploadFile: any) {
|
||||
const raw = uploadFile?.raw as File | undefined
|
||||
const raw = (uploadFile?.raw || uploadFile) as File | undefined
|
||||
if (!raw) return
|
||||
if (raw.size > 20 * 1024 * 1024) {
|
||||
ElMessage.warning('单文件不超过 20MB')
|
||||
return
|
||||
}
|
||||
const fd = new FormData()
|
||||
fd.append('file', raw)
|
||||
fd.append('folder', 'protocol-refund-record')
|
||||
const res: any = await fetchUploadAttachment(fd)
|
||||
const data = res?.data || res
|
||||
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
form.attachments.push({
|
||||
id: data?.id,
|
||||
fileName: data?.fileName || raw.name,
|
||||
fileUrl: data?.fileUrl || data?.url,
|
||||
fileSize: data?.fileSize,
|
||||
fileType: data?.fileType
|
||||
id: tempId,
|
||||
fileName: raw.name,
|
||||
fileUrl: '',
|
||||
fileSize: raw.size,
|
||||
fileSizeDisplay: `${(raw.size / 1024).toFixed(1)} KB`,
|
||||
fileType: raw.type || '',
|
||||
_uploading: true
|
||||
})
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('file', raw)
|
||||
fd.append('folder', 'protocol-refund-record')
|
||||
const res: any = await fetchUploadAttachment(fd)
|
||||
const data = res?.data || res
|
||||
const idx = form.attachments.findIndex((f) => f.id === tempId)
|
||||
if (idx < 0) return
|
||||
form.attachments[idx] = {
|
||||
...form.attachments[idx],
|
||||
id: data?.id || tempId,
|
||||
fileName: raw.name || data?.fileName,
|
||||
fileUrl: data?.fileUrl || data?.url || '',
|
||||
fileSize: data?.fileSize || raw.size,
|
||||
fileSizeDisplay: data?.fileSizeDisplay || `${(raw.size / 1024).toFixed(1)} KB`,
|
||||
fileType: data?.fileType || raw.type || '',
|
||||
_uploading: false
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
const idx = form.attachments.findIndex((f) => f.id === tempId)
|
||||
if (idx >= 0) form.attachments.splice(idx, 1)
|
||||
} finally {
|
||||
nextTick(() => attachmentUploadRef.value?.clearFiles?.())
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
@@ -249,6 +442,10 @@
|
||||
ElMessage.warning('请填写退费金额')
|
||||
return
|
||||
}
|
||||
if (form.attachments.some((a) => a._uploading)) {
|
||||
ElMessage.warning('附件正在上传,请稍后再保存')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const urls = form.attachments
|
||||
@@ -300,60 +497,175 @@
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
}
|
||||
|
||||
.muted-dash {
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
}
|
||||
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-card--sm {
|
||||
.upload-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 140px;
|
||||
height: 96px;
|
||||
cursor: pointer;
|
||||
border: 1px dashed #a7f3d0;
|
||||
background: #f0fdf4;
|
||||
border: 1px dashed #a8d4c4;
|
||||
border-radius: 8px;
|
||||
background: #ecfdf5;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #e8f4ee;
|
||||
border-color: #047857;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-width: 140px;
|
||||
max-width: 220px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.action-btn--danger {
|
||||
color: #ef4444;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 140px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #047857;
|
||||
box-shadow: 0 2px 8px rgba(4, 120, 87, 0.1);
|
||||
}
|
||||
|
||||
&.is-uploading {
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #f8fafc;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
|
||||
&--video {
|
||||
color: #7c3aed;
|
||||
background: #ede9fe;
|
||||
}
|
||||
|
||||
&--doc {
|
||||
color: #0284c7;
|
||||
background: #e0f2fe;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
display: -webkit-box;
|
||||
min-height: calc(11px * 1.4 * 2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: #334155;
|
||||
word-break: break-all;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
color: var(--mint-text-muted, #334155);
|
||||
cursor: pointer;
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: #047857;
|
||||
}
|
||||
|
||||
&--danger:hover {
|
||||
color: #fff;
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -52,6 +52,10 @@
|
||||
></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
|
||||
@@ -786,6 +790,23 @@
|
||||
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>
|
||||
@@ -1099,62 +1120,47 @@
|
||||
<!-- 状态变更记录(置底) -->
|
||||
<div id="view-section-status-change" class="section-card section-card--white">
|
||||
<div class="section-title section-title--banner"><h3>状态变更记录</h3></div>
|
||||
<el-empty v-if="!hasAnyStatusChange" description="暂无状态变更记录" :image-size="64" />
|
||||
<div v-else class="data-grid data-grid--4">
|
||||
<template v-if="protocol?.effectiveTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议生效时间</div
|
||||
><div class="data-value">{{ dash(protocol.effectiveTime) }}</div></div
|
||||
<div class="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
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议生效描述</div
|
||||
><div class="data-value data-value--muted">{{
|
||||
dash(protocol.effectiveDesc)
|
||||
}}</div></div
|
||||
>
|
||||
</template>
|
||||
<template v-if="protocol?.expireTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议过期时间</div
|
||||
><div class="data-value">{{ dash(protocol.expireTime) }}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议过期描述</div
|
||||
><div class="data-value data-value--muted">{{
|
||||
dash(protocol.expireDesc)
|
||||
}}</div></div
|
||||
>
|
||||
</template>
|
||||
<template v-if="protocol?.voidTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">作废类型</div
|
||||
><div class="data-value">{{
|
||||
voidTypeLabel(String(protocol?.voidType || '')) || '—'
|
||||
}}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议作废时间</div
|
||||
><div class="data-value">{{ dash(protocol.voidTime) }}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议作废描述</div
|
||||
><div class="data-value data-value--muted">{{
|
||||
dash(protocol.voidDesc)
|
||||
}}</div></div
|
||||
>
|
||||
</template>
|
||||
<template v-if="protocol?.toResearchTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议转科研时间</div
|
||||
><div class="data-value">{{ dash(protocol.toResearchTime) }}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">协议转科研描述</div
|
||||
><div class="data-value data-value--muted">{{
|
||||
dash(protocol.toResearchDesc)
|
||||
}}</div></div
|
||||
>
|
||||
</template>
|
||||
<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>
|
||||
</div>
|
||||
@@ -1208,6 +1214,7 @@
|
||||
import {
|
||||
fetchCollectionProtocolAuditList,
|
||||
fetchCollectionProtocolDetail,
|
||||
fetchCollectionProtocolStatusChanges,
|
||||
type CollectionProtocolRow
|
||||
} from '@/api/collection-protocol'
|
||||
import {
|
||||
@@ -1305,8 +1312,52 @@
|
||||
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 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 }>>([])
|
||||
@@ -1520,25 +1571,21 @@
|
||||
/** 金额只读展示:千分位 + ¥ 前缀,null 显示 — */
|
||||
function fmtMoney(v: unknown): string {
|
||||
if (v == null || v === '') return '—'
|
||||
return '¥' + formatMoneyDisplay(v)
|
||||
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 hasAnyStatusChange = computed(() => {
|
||||
if (!protocol.value) return false
|
||||
return !!(
|
||||
protocol.value.effectiveTime ||
|
||||
protocol.value.expireTime ||
|
||||
protocol.value.voidTime ||
|
||||
protocol.value.toResearchTime
|
||||
)
|
||||
})
|
||||
|
||||
const collectorDisplay = computed(() => {
|
||||
const fromList = props.listRow?.collectorUserName
|
||||
if (fromList) return String(fromList)
|
||||
@@ -1892,6 +1939,13 @@
|
||||
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
|
||||
@@ -2007,6 +2061,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
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 loadAuditRecords() {
|
||||
if (!props.showAuditHistory || !props.protocolId) {
|
||||
auditRecords.value = []
|
||||
@@ -2033,9 +2103,11 @@
|
||||
lookupLoading.value = true
|
||||
bundle.value = null
|
||||
auditRecords.value = []
|
||||
statusChangeRecords.value = []
|
||||
|
||||
const baseLookupsPromise = loadBaseLookups().catch(() => {})
|
||||
const auditPromise = props.showAuditHistory ? loadAuditRecords() : null
|
||||
const statusChangePromise = loadStatusChangeRecords()
|
||||
|
||||
let data: Record<string, any> | null = null
|
||||
try {
|
||||
@@ -2052,6 +2124,7 @@
|
||||
|
||||
void finishLookups(data, baseLookupsPromise)
|
||||
void auditPromise
|
||||
void statusChangePromise
|
||||
if (props.highlightSampleCode) {
|
||||
nextTick(() => {
|
||||
document
|
||||
@@ -2074,10 +2147,12 @@
|
||||
function handleClosed() {
|
||||
bundle.value = null
|
||||
auditRecords.value = []
|
||||
statusChangeRecords.value = []
|
||||
activeTab.value = 'basic'
|
||||
detailLoading.value = false
|
||||
lookupLoading.value = false
|
||||
auditLoading.value = false
|
||||
statusChangeLoading.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@@ -444,7 +444,7 @@
|
||||
|
||||
function fmtMoney(v: unknown): string {
|
||||
if (v == null || v === '') return '—'
|
||||
return '¥' + formatMoneyDisplay(v)
|
||||
return '¥' + formatMoneyDisplay(v as string | number)
|
||||
}
|
||||
|
||||
function samplePayKey(pay: Record<string, any>, pIdx: number): string {
|
||||
@@ -896,10 +896,10 @@
|
||||
tr.payStatus = data.payStatus || tr.payStatus || 'dfj'
|
||||
tr.scheduleId = data.scheduleId || tr.scheduleId
|
||||
tr.samplePaymentId = data.samplePaymentId || tr.samplePaymentId
|
||||
if (Array.isArray(data.attachments) && data.attachments.length) {
|
||||
if (Array.isArray(data.attachments)) {
|
||||
tr.attachments = data.attachments.map((a: Record<string, any>) => ({ ...a }))
|
||||
}
|
||||
if (data.transferAttachments) tr.transferAttachments = data.transferAttachments
|
||||
if (data.transferAttachments != null) tr.transferAttachments = data.transferAttachments
|
||||
emitChange()
|
||||
}
|
||||
|
||||
@@ -922,8 +922,8 @@
|
||||
const payload = {
|
||||
transferType: tr.transferType,
|
||||
transferAmount: tr.transferAmount,
|
||||
transferTime: tr.transferTime || undefined,
|
||||
transferAttachments: attachmentsToUrls(tr) || undefined,
|
||||
transferTime: tr.transferTime || '',
|
||||
transferAttachments: attachmentsToUrls(tr),
|
||||
remark: tr.remark
|
||||
}
|
||||
if (tr.id) {
|
||||
@@ -965,13 +965,14 @@
|
||||
_uploading: true
|
||||
})
|
||||
emitChange()
|
||||
const idx = tr.attachments.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'protocol-transfer')
|
||||
fd.append('file', file)
|
||||
const res: any = await fetchUploadAttachment(fd)
|
||||
const data = res?.data || res
|
||||
const idx = tr.attachments.findIndex((f: TransferAttachment) => f.id === tempId)
|
||||
if (idx < 0) return
|
||||
tr.attachments[idx] = {
|
||||
id: data?.id || tempId,
|
||||
fileName: data?.fileName || file.name,
|
||||
@@ -981,7 +982,8 @@
|
||||
emitChange()
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
tr.attachments.splice(idx, 1)
|
||||
const idx = tr.attachments.findIndex((f: TransferAttachment) => f.id === tempId)
|
||||
if (idx >= 0) tr.attachments.splice(idx, 1)
|
||||
emitChange()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@
|
||||
}
|
||||
|
||||
function handleEdit(row: CollectionProtocolRow) {
|
||||
formDialogEditId.value = row.id
|
||||
formDialogEditId.value = String(row.id || '')
|
||||
formDialogRow.value = row
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
@@ -420,9 +420,7 @@
|
||||
}
|
||||
const row = selectedRows.value[0]
|
||||
refundProtocolId.value = String(row.id || '')
|
||||
const name = String(row.motherName || '').trim()
|
||||
const no = String(row.protocolNo || '').trim()
|
||||
refundProtocolLabel.value = [name, no].filter(Boolean).join(' / ')
|
||||
refundProtocolLabel.value = String(row.motherName || '').trim()
|
||||
refundDialogVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,9 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="审批人去重">
|
||||
<el-switch v-model="advanced.approverDedup" />
|
||||
<div class="oa-designer__hint">
|
||||
开启后,同一张审批单里某人已经同意过,后续节点再出现该人时系统自动通过,不再生成待办。或签整节点通过,会签仅完成本人;仅「已同意」会计入,拒绝不触发。默认关闭。
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
@@ -33,44 +33,6 @@
|
||||
|
||||
<template v-if="bundle">
|
||||
<div>
|
||||
<!-- KPI 摘要条 -->
|
||||
<div class="kpi-strip">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">状态</div>
|
||||
<div class="kpi-value">
|
||||
<el-tag
|
||||
:type="donationStatusTagType(String(donation?.donationStatus || ''))"
|
||||
size="small"
|
||||
>
|
||||
{{ donationStatusLabel(String(donation?.donationStatus || '')) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">母亲姓名</div>
|
||||
<div class="kpi-value">{{ dash(party?.motherName) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">捐赠日期</div>
|
||||
<div class="kpi-value">{{ formatYmd(donation?.donationDate) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div id="view-section-basic" class="section-card section-card--white">
|
||||
<div class="section-title section-title--banner"><h3>基本信息</h3></div>
|
||||
@@ -85,6 +47,10 @@
|
||||
></div
|
||||
></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">母亲姓名</div
|
||||
><div class="data-value">{{ dash(party?.motherName) }}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">捐赠日期</div
|
||||
><div class="data-value">{{ formatYmd(donation?.donationDate) }}</div></div
|
||||
@@ -103,62 +69,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态变更记录(独立区域) -->
|
||||
<div id="view-section-status-change" class="section-card section-card--white">
|
||||
<div class="section-title"><h3>状态变更记录</h3></div>
|
||||
<el-empty v-if="!hasAnyStatusChange" description="暂无状态变更记录" :image-size="64" />
|
||||
<div v-else class="data-grid data-grid--4">
|
||||
<template v-if="donation?.effectiveTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">生效时间</div
|
||||
><div class="data-value">{{ dash(donation.effectiveTime) }}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">生效描述</div
|
||||
><div class="data-value data-value--muted">{{
|
||||
dash(donation.effectiveDesc)
|
||||
}}</div></div
|
||||
>
|
||||
</template>
|
||||
<template v-if="donation?.expireTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">过期时间</div
|
||||
><div class="data-value">{{ dash(donation.expireTime) }}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">过期描述</div
|
||||
><div class="data-value data-value--muted">{{
|
||||
dash(donation.expireDesc)
|
||||
}}</div></div
|
||||
>
|
||||
</template>
|
||||
<template v-if="donation?.voidTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">作废时间</div
|
||||
><div class="data-value">{{ dash(donation.voidTime) }}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">作废描述</div
|
||||
><div class="data-value data-value--muted">{{
|
||||
dash(donation.voidDesc)
|
||||
}}</div></div
|
||||
>
|
||||
</template>
|
||||
<template v-if="donation?.convertTime">
|
||||
<div class="data-item"
|
||||
><div class="data-label">转细胞存储时间</div
|
||||
><div class="data-value">{{ dash(donation.convertTime) }}</div></div
|
||||
>
|
||||
<div class="data-item"
|
||||
><div class="data-label">转细胞存储描述</div
|
||||
><div class="data-value data-value--muted">{{
|
||||
dash(donation.convertDesc)
|
||||
}}</div></div
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 采集信息 -->
|
||||
<div id="view-section-collection" class="section-card section-card--white">
|
||||
<div class="section-title section-title--banner"><h3>采集信息</h3></div>
|
||||
@@ -219,7 +129,7 @@
|
||||
</div>
|
||||
<div class="sample-card__body">
|
||||
<div class="sample-card__section">
|
||||
<div class="sample-card__section-label">样本信息</div>
|
||||
<div class="sample-card__section-label">样本及运输</div>
|
||||
<div class="data-grid data-grid--4">
|
||||
<div class="data-item">
|
||||
<div class="data-label">样本编码</div>
|
||||
@@ -232,26 +142,6 @@
|
||||
/>
|
||||
</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">{{
|
||||
@@ -274,6 +164,10 @@
|
||||
<div class="data-label">预计到达</div>
|
||||
<div class="data-value">{{ dash(row.transport?.etaTime) }}</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 class="sample-card__section sample-card__section--reception">
|
||||
@@ -530,6 +424,53 @@
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -571,7 +512,7 @@
|
||||
import { formatDisplayOrDash } from '@/utils/datetime-display'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import { Calendar, CircleCheck, Close, User } from '@element-plus/icons-vue'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import SampleQcInfoGroupedPanel from '@/components/business/sample-qc-info-grouped-panel/index.vue'
|
||||
import SampleReceptionDualReadonlyPanel from '@/components/business/sample-reception-dual-readonly-panel/index.vue'
|
||||
@@ -579,6 +520,7 @@
|
||||
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
|
||||
import SampleStorageReleaseInfo from '@/components/business/sample-storage-release-info/index.vue'
|
||||
import {
|
||||
fetchDonationStatusChanges,
|
||||
fetchResearchDonationAuditList,
|
||||
fetchResearchDonationDetail,
|
||||
type ResearchDonationRow
|
||||
@@ -594,6 +536,7 @@
|
||||
} from '@/api/admin'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import {
|
||||
donationStatusDisplayLabel,
|
||||
donationStatusLabel,
|
||||
donationStatusTagType,
|
||||
auditStatusLabel,
|
||||
@@ -606,6 +549,7 @@
|
||||
acceptConclusionLabel,
|
||||
acceptConclusionTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { loadVoidTypeDict, voidTypeLabel } from '@/utils/void-type-dict'
|
||||
|
||||
defineOptions({ name: 'DonationViewDialog' })
|
||||
|
||||
@@ -627,7 +571,6 @@
|
||||
|
||||
const baseFormTabs = [
|
||||
{ key: 'basic', label: '基本信息' },
|
||||
{ key: 'status-change', label: '状态变更记录' },
|
||||
{ key: 'collection', label: '采集信息' },
|
||||
{ key: 'sample', label: '样本及运输信息' },
|
||||
{ key: 'party', label: '客户信息' }
|
||||
@@ -639,13 +582,58 @@
|
||||
tabs.push({ key: 'audit', label: '审核记录' })
|
||||
}
|
||||
tabs.push({ key: 'attachment', label: '附件信息' })
|
||||
tabs.push({ key: 'status-change', label: '状态变更记录' })
|
||||
return tabs
|
||||
})
|
||||
|
||||
const loading = 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 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 = donationStatusDisplayLabel(to, to === 'yzf' ? voidType : '')
|
||||
if (!from) return toLabel || '—'
|
||||
return `${donationStatusDisplayLabel(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 }>>([])
|
||||
@@ -707,16 +695,6 @@
|
||||
|
||||
const dialogTitle = computed(() => '查看捐赠协议单')
|
||||
|
||||
const hasAnyStatusChange = computed(() => {
|
||||
if (!donation.value) return false
|
||||
return !!(
|
||||
donation.value.effectiveTime ||
|
||||
donation.value.expireTime ||
|
||||
donation.value.voidTime ||
|
||||
donation.value.convertTime
|
||||
)
|
||||
})
|
||||
|
||||
const collectorDisplay = computed(() => {
|
||||
const fromList = props.listRow?.collectorUserName
|
||||
if (fromList) return String(fromList)
|
||||
@@ -954,6 +932,22 @@
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
|
||||
async function loadStatusChangeRecords() {
|
||||
if (!props.donationId) {
|
||||
statusChangeRecords.value = []
|
||||
return
|
||||
}
|
||||
statusChangeLoading.value = true
|
||||
try {
|
||||
const res = await fetchDonationStatusChanges(props.donationId)
|
||||
statusChangeRecords.value = res.list || []
|
||||
} catch {
|
||||
statusChangeRecords.value = []
|
||||
} finally {
|
||||
statusChangeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAuditRecords() {
|
||||
if (!props.showAuditHistory || !props.donationId) {
|
||||
auditRecords.value = []
|
||||
@@ -979,11 +973,13 @@
|
||||
loading.value = true
|
||||
bundle.value = null
|
||||
auditRecords.value = []
|
||||
statusChangeRecords.value = []
|
||||
void loadVoidTypeDict()
|
||||
try {
|
||||
const data = await fetchResearchDonationDetail(props.donationId)
|
||||
bundle.value = data as Record<string, any>
|
||||
await loadLookups(data as Record<string, any>)
|
||||
await loadAuditRecords()
|
||||
await Promise.all([loadAuditRecords(), loadStatusChangeRecords()])
|
||||
if (props.highlightSampleCode) {
|
||||
nextTick(() => {
|
||||
document
|
||||
@@ -1009,9 +1005,11 @@
|
||||
function handleClosed() {
|
||||
bundle.value = null
|
||||
auditRecords.value = []
|
||||
statusChangeRecords.value = []
|
||||
activeTab.value = 'basic'
|
||||
loading.value = false
|
||||
auditLoading.value = false
|
||||
statusChangeLoading.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -1044,75 +1042,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- KPI 摘要条(与渠道/申请单查看弹窗一致:浅绿渐变通栏) ---- */
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0;
|
||||
margin-bottom: 24px;
|
||||
padding: 18px 20px;
|
||||
background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: none;
|
||||
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;
|
||||
@@ -1504,37 +1433,12 @@
|
||||
|
||||
/* ---- Responsive ---- */
|
||||
@media (max-width: 1200px) {
|
||||
.kpi-strip {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.kpi-strip .kpi-card {
|
||||
border-right: none !important;
|
||||
border-bottom: 1px solid rgba(168, 212, 196, 0.75);
|
||||
|
||||
&:nth-last-child(-n + 2) {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.client-sub-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.kpi-strip {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.kpi-strip .kpi-card:nth-last-child(-n + 2) {
|
||||
border-bottom: 1px solid rgba(168, 212, 196, 0.75);
|
||||
}
|
||||
|
||||
.kpi-strip .kpi-card:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.client-sub-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user