This commit is contained in:
zlwu committed 2026-05-17 12:31:11 +08:00
1 parent e5ba8f097a
commit 62cfb8fbbf
24 files changed
+3323 -249

No files matched your search

@@ -908,7 +908,7 @@
<el-option label="已退费" value="ytf" />
</el-select>
</el-form-item>
<el-form-item v-if="formData.payMode === 'fq'" label="应缴费时间">
<el-form-item label="应缴费时间">
<el-date-picker
v-model="detail.installmentDueDate"
type="date"
@@ -1426,7 +1426,7 @@
if (detail.payStatus) {
parts.push(payStatusLabel(String(detail.payStatus)))
}
if (formData.payMode === 'fq' && detail.installmentDueDate) {
if (detail.installmentDueDate) {
parts.push(`应缴日 ${formatPaymentDateYmd(String(detail.installmentDueDate))}`)
}
const attCount = Array.isArray(detail.attachments) ? detail.attachments.length : 0
@@ -1682,13 +1682,15 @@
const pad = (n: number) => String(n).padStart(2, '0')
const defaultTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
const isFirst = formData.paymentDetails.length === 0
// 趸交模式下应缴费时间默认等于趸交截止时间
const defaultDueDate = formData.payMode === 'dj' ? formData.lumpSumDeadline : ''
formData.paymentDetails.push({
transferType: '',
dueAmount: null,
transferAmount: null,
transferTime: defaultTime,
payStatus: 'dfj',
installmentDueDate: '',
installmentDueDate: defaultDueDate,
_expanded: isFirst,
attachments: [] as Array<{
id?: string
@@ -1754,10 +1756,21 @@
if (val === 'fq') {
tryGenerateInstallmentDetails()
} else {
// 趸交模式:将趸交截止时间同步到所有明细的应缴费时间
syncPaymentDueAmounts()
syncLumpSumDueDate()
}
}
/** 趸交模式:将趸交截止时间同步到所有缴费明细的应缴费时间 */
function syncLumpSumDueDate() {
if (formData.payMode !== 'dj') return
const deadline = formData.lumpSumDeadline || ''
formData.paymentDetails.forEach((d) => {
d.installmentDueDate = deadline
})
}
function handleOpenForAdd() {
isEdit.value = false
dialogTitle.value = '新增协议单'
@@ -2621,6 +2634,16 @@
() => calcBirthFromIdCard()
)
/* ====== 趸交截止时间联动 ====== */
watch(
() => formData.lumpSumDeadline,
() => {
if (formData.payMode === 'dj') {
syncLumpSumDueDate()
}
}
)
/* ====== 附件上传 ====== */
const attachmentUploadRef = ref()
@@ -313,6 +313,56 @@
/>
</div>
</dl>
<template
v-for="flowCtx in [{ nodes: buildPaymentFlowNodes(detail) }]"
:key="`${detail.id || dIdx}-flow`"
>
<div v-if="flowCtx.nodes.length" class="payment-flow">
<div class="payment-flow-scroll">
<div class="payment-flow-track">
<div
v-for="(node, nIdx) in flowCtx.nodes"
:key="node.key"
class="payment-flow-item"
>
<div class="payment-flow-rail">
<span class="payment-flow-node" />
<span
v-if="nIdx < flowCtx.nodes.length - 1"
class="payment-flow-line"
/>
</div>
<div class="payment-flow-body">
<div class="payment-flow-title">{{ node.title }}</div>
<div
v-if="node.amount != null && node.amount !== ''"
class="payment-flow-kv"
>
<span class="payment-flow-kv__label">退费金额(元)</span>
<span class="payment-flow-kv__value">{{ dash(node.amount) }}</span>
</div>
<div
v-if="node.remark != null && String(node.remark).trim() !== ''"
class="payment-flow-kv payment-flow-kv--remark"
>
<span class="payment-flow-kv__label">{{ node.remarkLabel }}</span>
<span class="payment-flow-kv__value">{{ dash(node.remark) }}</span>
</div>
<div v-if="node.attachments.length" class="payment-flow-attachments">
<span class="payment-flow-kv__label">{{ node.attachmentLabel }}</span>
<ProtocolAttachmentReadonly
:files="node.attachments"
@preview="openPreview"
@download="downloadFile"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
@@ -494,6 +544,8 @@
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)
@@ -502,6 +554,8 @@
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)
@@ -510,6 +564,8 @@
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)
@@ -518,6 +574,8 @@
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)
@@ -649,6 +707,131 @@
return Array.isArray(detail.attachments) ? detail.attachments : []
}
function financeConfirmAttachmentFiles(detail: Record<string, any>) {
if (Array.isArray(detail.financeConfirmAttachmentFiles)) {
return detail.financeConfirmAttachmentFiles
}
return urlsToAttachmentFiles(String(detail.financeConfirmAttachments || ''))
}
function refundAttachmentFiles(detail: Record<string, any>) {
if (Array.isArray(detail.refundAttachmentFiles)) {
return detail.refundAttachmentFiles
}
return urlsToAttachmentFiles(String(detail.refundAttachments || ''))
}
function refundFinanceConfirmAttachmentFiles(detail: Record<string, any>) {
if (Array.isArray(detail.refundFinanceConfirmAttachmentFiles)) {
return detail.refundFinanceConfirmAttachmentFiles
}
return urlsToAttachmentFiles(String(detail.refundFinanceConfirmAttachments || ''))
}
function urlsToAttachmentFiles(urls: string) {
if (!urls.trim()) return []
return urls.split(',').map((u, i) => {
const url = u.trim()
if (!url) return null
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
return { id: `url_${i}`, fileUrl: url, fileName: name, fileSize: 0 }
}).filter(Boolean) as Array<Record<string, any>>
}
function hasFinanceRemark(detail: Record<string, any>) {
return String(detail.financeRemark || '').trim() !== ''
}
function hasFinanceConfirmAttachments(detail: Record<string, any>) {
return financeConfirmAttachmentFiles(detail).length > 0
}
function hasRefundApplyInfo(detail: Record<string, any>) {
const amount = detail.refundAmount
const hasAmount = amount != null && amount !== '' && Number(amount) > 0
return (
hasAmount ||
String(detail.refundRemark || '').trim() !== '' ||
refundAttachmentFiles(detail).length > 0
)
}
/** 缴费财务确认:已缴费或退费确认中,且已有财务确认字段 */
function showPaymentFinanceConfirm(detail: Record<string, any>) {
const status = String(detail.payStatus || '')
if (status === 'ytf') return false
if (status !== 'yjf' && status !== 'tfqrz') return false
return hasFinanceRemark(detail) || hasFinanceConfirmAttachments(detail)
}
/** 退费确认申请:发起退费时填写 */
function showRefundApply(detail: Record<string, any>) {
const status = String(detail.payStatus || '')
if (status !== 'tfqrz' && status !== 'ytf') return false
return hasRefundApplyInfo(detail)
}
function hasRefundFinanceRemark(detail: Record<string, any>) {
return String(detail.refundFinanceRemark || '').trim() !== ''
}
function hasRefundFinanceConfirmAttachments(detail: Record<string, any>) {
return refundFinanceConfirmAttachmentFiles(detail).length > 0
}
/** 财务退费确认:已退费时展示的财务确认信息 */
function showRefundFinanceConfirm(detail: Record<string, any>) {
if (String(detail.payStatus || '') !== 'ytf') return false
return hasRefundFinanceRemark(detail) || hasRefundFinanceConfirmAttachments(detail)
}
interface PaymentFlowNode {
key: string
title: string
remarkLabel: string
attachmentLabel: string
remark?: unknown
amount?: unknown
attachments: Array<Record<string, any>>
}
/** 缴费明细 — 财务/退费流程横向节点(按业务顺序) */
function buildPaymentFlowNodes(detail: Record<string, any>): PaymentFlowNode[] {
const nodes: PaymentFlowNode[] = []
if (showPaymentFinanceConfirm(detail)) {
nodes.push({
key: 'receipt-finance',
title: '缴费财务确认',
remarkLabel: '财务备注',
attachmentLabel: '财务附件',
remark: detail.financeRemark,
attachments: financeConfirmAttachmentFiles(detail)
})
}
if (showRefundApply(detail)) {
nodes.push({
key: 'refund-apply',
title: '退费确认申请',
remarkLabel: '退费备注',
attachmentLabel: '退费附件',
remark: detail.refundRemark,
amount: detail.refundAmount,
attachments: refundAttachmentFiles(detail)
})
}
if (showRefundFinanceConfirm(detail)) {
nodes.push({
key: 'refund-finance',
title: '财务退费确认',
remarkLabel: '财务备注',
attachmentLabel: '财务附件',
remark: detail.refundFinanceRemark,
attachments: refundFinanceConfirmAttachmentFiles(detail)
})
}
return nodes
}
function fullFileUrl(suffix: string) {
if (!suffix) return ''
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/'))
@@ -947,11 +1130,133 @@
}
.payment-detail-card__attachments {
margin-top: 8px;
padding-top: 8px;
margin-top: 4px;
padding-top: 12px;
border-top: 1px dashed #e2e8f0;
}
.payment-flow {
margin-top: 14px;
padding-top: 14px;
border-top: 1px solid #e2e8f0;
}
.payment-flow-scroll {
overflow-x: auto;
padding: 0 0 6px 4px;
&::-webkit-scrollbar {
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 2px;
}
}
.payment-flow-track {
display: inline-flex;
min-width: 100%;
align-items: flex-start;
}
.payment-flow-item {
display: flex;
flex-direction: column;
min-width: 200px;
flex: 1 0 200px;
max-width: 280px;
}
.payment-flow-rail {
display: flex;
align-items: center;
height: 22px;
}
.payment-flow-node {
position: relative;
z-index: 1;
flex-shrink: 0;
width: 12px;
height: 12px;
border: 2px solid #fff;
border-radius: 50%;
background: var(--primary, #10b981);
box-shadow: 0 0 0 1px var(--primary, #10b981);
}
.payment-flow-line {
flex: 1;
height: 2px;
margin-left: 0;
background: #d1d5db;
}
.payment-flow-body {
padding: 8px 12px 0 0;
font-size: 13px;
line-height: 1.45;
}
.payment-flow-title {
margin-bottom: 8px;
font-size: 13px;
font-weight: 600;
color: #334155;
}
.payment-flow-kv {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 8px;
}
.payment-flow-kv--remark .payment-flow-kv__value {
word-break: break-word;
}
.payment-flow-kv__label {
font-size: 11px;
font-weight: 500;
color: #94a3b8;
}
.payment-flow-kv__value {
color: #1e293b;
}
.payment-flow-attachments {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
:deep(.attachment-list) {
flex-wrap: nowrap;
gap: 8px;
}
:deep(.attachment-item) {
width: 88px;
flex-shrink: 0;
}
:deep(.attachment-preview) {
height: 64px;
}
:deep(.attachment-footer) {
padding: 4px 6px;
}
:deep(.attachment-name) {
font-size: 11px;
}
}
.view-field-block {
display: flex;
flex-direction: column;
@@ -248,7 +248,12 @@
/>
<!-- 查看弹窗 -->
<ProtocolViewDialog v-model="viewVisible" :protocol-id="viewProtocolId" :list-row="viewRow" />
<ProtocolViewDialog
v-model="viewVisible"
:protocol-id="viewProtocolId"
:list-row="viewRow"
show-audit-history
/>
</section>
</div>
</template>