feat(web-admin): payment-info 缴费明细 dialog with list-mode transfers

Open sample-level detail from the list; manage transfers like
SamplePaymentPanel; remove direct PaymentReceiptConfirmDialog path.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-10 15:15:56 +08:00
1 parent ea5fba9dc2
commit 842aa0847f
2 files changed
+846 -102

No files matched your search

@@ -0,0 +1,826 @@
<!-- 样本缴费详情:应缴只读 + 列表模式转账(对齐 SamplePaymentPanel) -->
<template>
<el-dialog
v-model="dialogVisible"
title="样本缴费详情"
width="1080px"
class="dialog-form-layout"
align-center
append-to-body
modal-class="protocol-dialog-mask"
destroy-on-close
:close-on-click-modal="false"
@closed="onClosed"
>
<div v-loading="loading" 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>
{{ detail.motherName || '—' }}
<span v-if="detail.motherPhone" class="detail-muted">
· {{ detail.motherPhone }}
</span>
</span>
</div>
<div class="detail-field">
<label>样本编码</label>
<span>{{ detail.sampleCode || '—' }}</span>
</div>
<div class="detail-field">
<label>缴费方式</label>
<el-tag :type="payModeTagType(String(detail.payMode || ''))" size="small">
{{ payModeLabel(String(detail.payMode || '')) || '—' }}
</el-tag>
</div>
<div class="detail-field">
<label>合同额</label>
<span class="amount-text">{{ formatAmount(detail.contractAmount) }}</span>
</div>
<div class="detail-field">
<label>已缴 / 待缴</label>
<span class="amount-text">
{{ formatAmount(detail.paidAmount) }} /
{{ formatAmount(detail.dueRemainAmount) }}
</span>
</div>
<div class="detail-field">
<label>整体进度</label>
<span>
已缴清 {{ Number(detail.clearedScheduleCount) || 0 }}/{{
Number(detail.activeScheduleCount) || 0
}}
期
</span>
</div>
<div class="detail-field">
<label>缴费进度</label>
<el-tag
:type="samplePaymentProgressTagType(String(detail.progressStatus || ''))"
size="small"
>
{{ samplePaymentProgressLabel(String(detail.progressStatus || '')) || '—' }}
</el-tag>
</div>
</div>
<div class="section-subtitle">应缴批次</div>
<div v-if="!schedules.length" class="muted-hint">暂无应缴批次</div>
<div
v-for="(sch, sIdx) in schedules"
:key="String(sch.id || sIdx)"
class="schedule-block"
>
<div class="schedule-block__head">
<span
>第 {{ sch.scheduleNo || sIdx + 1 }} 期 · 应缴
{{ formatAmount(sch.dueAmount) }}
<template v-if="sch.dueDate"> · {{ formatDueDate(String(sch.dueDate)) }}</template>
· 已缴 {{ formatAmount(sch.paidAmount) }} · 欠缴
{{ formatAmount(sch.arrearsAmount) }}
</span>
<el-tag
:type="paymentScheduleStatusTagType(String(sch.scheduleStatus || ''))"
size="small"
>
{{ paymentScheduleStatusLabel(String(sch.scheduleStatus || '')) }}
</el-tag>
<el-button
v-if="scheduleCanAddTransfer(sch)"
type="primary"
size="small"
@click="addTransfer(sIdx)"
>
<el-icon><Plus /></el-icon>
添加转账
</el-button>
</div>
<el-table
:data="sch.transfers || []"
class="list-table transfer-table"
stripe
empty-text="暂无转账记录,请点击上方按钮添加"
>
<el-table-column type="index" label="#" min-width="50" align="center" />
<el-table-column label="转账方式" min-width="140" align="center">
<template #default="{ row }">
<el-select
v-model="row.transferType"
placeholder="请选择"
style="width: 100%"
:disabled="isTransferFieldsLocked(row)"
>
<el-option
v-for="opt in transferTypeOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="转账金额(元)" min-width="140" align="center">
<template #default="{ row }">
<AmountInput
v-model="row.transferAmount"
:min="0"
:disabled="isTransferFieldsLocked(row)"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column label="转账时间" min-width="190" align="center">
<template #default="{ row }">
<el-date-picker
v-model="row.transferTime"
type="datetime"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DD HH:mm:ss"
clearable
placeholder="选择时间"
:disabled="isTransferFieldsLocked(row)"
style="width: 100%"
/>
</template>
</el-table-column>
<el-table-column label="转账凭证" min-width="280" align="left">
<template #default="{ row, $index }">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
v-if="!isTransferFieldsLocked(row)"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
@change="(f: any) => handleTransferAttachmentChange(sIdx, $index, f)"
>
<div class="upload-card">
<el-icon :size="22" color="#047857"><Plus /></el-icon>
<span class="upload-card-text">上传凭证</span>
</div>
</el-upload>
<div
v-for="(file, fIdx) in ensureTransferAttachments(row)"
:key="file.id || fIdx"
class="attachment-item"
:class="{ 'is-uploading': file._uploading }"
>
<div class="attachment-preview">
<template v-if="file.fileUrl && isImageUrl(file.fileUrl)">
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
</template>
<template v-else>
<div class="attachment-preview__icon">
<el-icon :size="22"><Document /></el-icon>
</div>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{
file.fileName || '附件'
}}</span>
<el-icon
v-if="!isTransferFieldsLocked(row)"
class="action-btn"
title="删除"
@click="removeTransferAttachment(sIdx, $index, fIdx)"
>
<Delete />
</el-icon>
</div>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="备注" min-width="160" align="center">
<template #default="{ row }">
<el-input
v-if="!isTransferFieldsLocked(row)"
v-model="row.remark"
maxlength="512"
clearable
placeholder="选填"
/>
<span v-else>{{ row.remark || '—' }}</span>
</template>
</el-table-column>
<el-table-column
label="财务备注"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.financeRemark || '—' }}</template>
</el-table-column>
<el-table-column label="状态" min-width="110" align="center">
<template #default="{ row }">
<el-tag :type="payStatusTagType(String(row.payStatus || 'dfj'))" size="small">
{{ payStatusLabel(String(row.payStatus || 'dfj')) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="300" align="center" fixed="right">
<template #default="{ row, $index }">
<el-button
v-if="!isTransferFieldsLocked(row)"
link
type="primary"
size="small"
:loading="savingKey === transferRowKey(sIdx, $index, row)"
@click="saveTransferRow(sIdx, $index)"
>
<el-icon><Select /></el-icon>
保存
</el-button>
<el-button
v-if="row.id && row.payStatus === 'dfj'"
link
type="primary"
size="small"
:loading="submittingId === row.id"
@click="submitTransfer(row)"
>
<el-icon><Check /></el-icon>
提交财务确认
</el-button>
<el-button
v-if="row.id && row.payStatus === 'jfqrz'"
link
type="warning"
size="small"
:loading="submittingId === row.id"
@click="withdrawTransfer(row)"
>
<el-icon><RefreshLeft /></el-icon>
撤回
</el-button>
<el-button
v-if="!isTransferFieldsLocked(row)"
link
type="danger"
size="small"
:loading="deletingId === String(row.id || '')"
@click="removeTransfer(sIdx, $index)"
>
<el-icon><Delete /></el-icon>
删除
</el-button>
<span
v-if="isTransferFieldsLocked(row) && row.payStatus !== 'jfqrz'"
class="muted-dash"
>—</span
>
</template>
</el-table-column>
</el-table>
</div>
</template>
<el-empty v-else-if="!loading" description="暂无详情" :image-size="64" />
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>
关闭
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Check, Delete, Document, Select, RefreshLeft, Close } from '@element-plus/icons-vue'
import AmountInput from '@/components/common/amount-input/index.vue'
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatDisplayOrDash } from '@/utils/datetime-display'
import {
payStatusLabel,
payStatusTagType,
loadTransferTypeDict,
getTransferTypeOptions
} from '@/utils/protocol-payment-labels'
import {
payModeLabel,
payModeTagType,
samplePaymentProgressLabel,
samplePaymentProgressTagType
} from '@/utils/status-tag-registry'
import {
fetchSamplePaymentDetail,
fetchCreatePaymentTransfer,
fetchUpdatePaymentTransfer,
fetchSubmitPaymentTransferFinance,
fetchWithdrawPaymentTransfer,
fetchDeletePaymentTransfer
} from '@/api/protocol-sample-payment'
import { fetchUploadAttachment } from '@/api/business'
defineOptions({ name: 'SamplePaymentDetailDialog' })
type TransferAttachment = {
id?: string
fileName?: string
fileUrl?: string
_uploading?: boolean
}
type TransferRow = Record<string, any>
type ScheduleRow = Record<string, any>
const props = defineProps<{
modelValue: boolean
samplePaymentId: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', val: boolean): void
(e: 'changed'): void
}>()
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const loading = ref(false)
const detail = ref<Record<string, any> | null>(null)
const schedules = ref<ScheduleRow[]>([])
const transferTypeOptions = ref<Api.Dict.DictDataOption[]>([])
const savingKey = ref('')
const submittingId = ref('')
const deletingId = ref('')
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
function formatAmount(v: unknown): string {
if (v == null || v === '') return '—'
const text = formatMoneyDisplay(v as string | number)
return text ? `¥${text}` : '—'
}
function formatDueDate(d: string) {
if (!d) return '—'
if (d.length === 8 && /^\d{8}$/.test(d)) {
return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`
}
return formatDisplayOrDash(d)
}
function paymentScheduleStatusLabel(status: string) {
const map: Record<string, string> = {
open: '待缴清',
cleared: '已缴清',
void: '已作废'
}
return map[status] || status || '—'
}
function paymentScheduleStatusTagType(
status: string
): 'success' | 'warning' | 'danger' | 'info' | 'primary' {
const map: Record<string, 'success' | 'warning' | 'danger' | 'info' | 'primary'> = {
open: 'warning',
cleared: 'success',
void: 'info'
}
return map[status] || 'info'
}
/** yjf / jfqrz 字段只读;jfqrz 仍可撤回 */
function isTransferFieldsLocked(tr: TransferRow) {
const st = String(tr.payStatus || '')
return st === 'yjf' || st === 'jfqrz'
}
function scheduleCanAddTransfer(sch: ScheduleRow) {
if (String(sch.scheduleStatus || '') === 'void') return false
return Number(sch.arrearsAmount) > 0
}
function fullFileUrl(url: string) {
if (!url) return ''
if (/^https?:\/\//i.test(url)) return url
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
}
function isImageUrl(url: string) {
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url)
}
function ensureTransferAttachments(tr: TransferRow): TransferAttachment[] {
if (Array.isArray(tr.attachments)) return tr.attachments
const urls = String(tr.transferAttachments || '')
.split(',')
.map((s: string) => s.trim())
.filter(Boolean)
tr.attachments = urls.map((fileUrl: string, i: number) => ({
id: `att_${i}`,
fileName: fileUrl.includes('/') ? fileUrl.split('/').pop() || '附件' : '附件',
fileUrl
}))
return tr.attachments
}
function transferRowKey(sIdx: number, tIdx: number, row: TransferRow) {
return String(row.id || `${sIdx}-${tIdx}`)
}
function attachmentsToUrls(tr: TransferRow) {
ensureTransferAttachments(tr)
return (tr.attachments || [])
.filter((f: TransferAttachment) => f?.fileUrl && !f._uploading)
.map((f: TransferAttachment) => String(f.fileUrl))
.join(',')
}
function validateTransferRow(tr: TransferRow) {
if (!(tr.transferType || '').trim()) {
ElMessage.warning('请选择转账方式')
return false
}
const amt = Number(tr.transferAmount)
if (!Number.isFinite(amt) || amt <= 0) {
ElMessage.warning('请填写转账金额')
return false
}
if ((tr.attachments || []).some((f: TransferAttachment) => f._uploading)) {
ElMessage.warning('凭证正在上传,请稍后再保存')
return false
}
return true
}
async function ensureTransferTypes() {
await loadTransferTypeDict()
transferTypeOptions.value = getTransferTypeOptions()
}
async function loadDetail() {
const id = String(props.samplePaymentId || '')
if (!id) {
detail.value = null
schedules.value = []
return
}
loading.value = true
try {
const data = (await fetchSamplePaymentDetail(id)) as Record<string, any>
detail.value = data || null
const list = ((data?.schedules as ScheduleRow[]) || []).slice()
list.sort((a, b) => Number(a.scheduleNo || 0) - Number(b.scheduleNo || 0))
for (const sch of list) {
for (const tr of sch.transfers || []) {
ensureTransferAttachments(tr)
}
}
schedules.value = list
} finally {
loading.value = false
}
}
function notifyChanged() {
emit('changed')
}
function addTransfer(sIdx: number) {
const sch = schedules.value[sIdx]
if (!sch || !scheduleCanAddTransfer(sch)) return
const arrears = Number(sch.arrearsAmount)
const due = Number(sch.dueAmount)
const hint = arrears > 0 ? arrears : due > 0 ? due : null
if (!Array.isArray(sch.transfers)) sch.transfers = []
sch.transfers.push({
transferType: '',
transferAmount: hint,
transferTime: '',
payStatus: 'dfj',
remark: '',
financeRemark: '',
transferAttachments: '',
attachments: [] as TransferAttachment[]
})
}
async function saveTransferRow(sIdx: number, tIdx: number) {
const sch = schedules.value[sIdx]
const tr = sch?.transfers?.[tIdx]
if (!tr || isTransferFieldsLocked(tr)) return
if (!sch?.id) {
ElMessage.warning('缺少应缴批次,无法保存转账')
return
}
if (!validateTransferRow(tr)) return
const key = transferRowKey(sIdx, tIdx, tr)
savingKey.value = key
try {
const payload = {
transferType: tr.transferType,
transferAmount: tr.transferAmount,
transferTime: tr.transferTime || undefined,
transferAttachments: attachmentsToUrls(tr) || undefined,
remark: tr.remark
}
if (tr.id) {
await fetchUpdatePaymentTransfer(String(tr.id), payload)
ElMessage.success('转账记录已保存')
} else {
await fetchCreatePaymentTransfer({
scheduleId: String(sch.id),
...payload
})
}
await loadDetail()
notifyChanged()
} finally {
savingKey.value = ''
}
}
async function submitTransfer(tr: TransferRow) {
if (!tr.id) {
ElMessage.warning('请先保存该转账记录后再提交财务确认')
return
}
submittingId.value = String(tr.id)
try {
await fetchSubmitPaymentTransferFinance(String(tr.id))
await loadDetail()
notifyChanged()
} finally {
submittingId.value = ''
}
}
async function withdrawTransfer(tr: TransferRow) {
if (!tr.id) return
await ElMessageBox.confirm(
'确认撤回该笔转账的财务确认申请?撤回后可修改并重新提交。',
'撤回',
{ type: 'warning' }
)
submittingId.value = String(tr.id)
try {
await fetchWithdrawPaymentTransfer(String(tr.id))
ElMessage.success('已撤回')
await loadDetail()
notifyChanged()
} finally {
submittingId.value = ''
}
}
async function removeTransfer(sIdx: number, tIdx: number) {
const sch = schedules.value[sIdx]
const tr = sch?.transfers?.[tIdx]
if (!tr || isTransferFieldsLocked(tr)) return
if (!tr.id) {
sch.transfers.splice(tIdx, 1)
return
}
if (String(tr.payStatus || '') !== 'dfj') {
ElMessage.warning('仅草稿转账笔可删除')
return
}
await ElMessageBox.confirm('确认删除该转账草稿?', '删除', { type: 'warning' })
deletingId.value = String(tr.id)
try {
await fetchDeletePaymentTransfer(String(tr.id))
await loadDetail()
notifyChanged()
} finally {
deletingId.value = ''
}
}
async function handleTransferAttachmentChange(
sIdx: number,
tIdx: number,
uploadFile: { raw?: File }
) {
const file = uploadFile.raw
if (!file) return
if (file.size > 20 * 1024 * 1024) {
ElMessage.warning('单文件不超过 20MB')
return
}
const tr = schedules.value[sIdx]?.transfers?.[tIdx]
if (!tr || isTransferFieldsLocked(tr)) return
ensureTransferAttachments(tr)
const tempId = `temp_${Date.now()}_${Math.random().toString(16).slice(2)}`
tr.attachments.push({
id: tempId,
fileName: file.name,
fileUrl: '',
_uploading: true
})
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
tr.attachments[idx] = {
id: data?.id || tempId,
fileName: data?.fileName || file.name,
fileUrl: data?.fileUrl || data?.url || '',
_uploading: false
}
} catch {
ElMessage.error('文件上传失败')
tr.attachments.splice(idx, 1)
}
}
function removeTransferAttachment(sIdx: number, tIdx: number, fIdx: number) {
const tr = schedules.value[sIdx]?.transfers?.[tIdx]
if (!tr?.attachments || isTransferFieldsLocked(tr)) return
tr.attachments.splice(fIdx, 1)
}
function onClosed() {
detail.value = null
schedules.value = []
}
watch(
() => [props.modelValue, props.samplePaymentId] as const,
([open, id]) => {
if (open && id) {
ensureTransferTypes()
loadDetail()
}
}
)
</script>
<style scoped lang="scss">
.sample-pay-detail {
min-height: 120px;
}
.detail-summary {
margin-bottom: 16px;
}
.detail-field {
display: flex;
flex-direction: column;
gap: 4px;
label {
font-size: 11px;
color: var(--mint-text-muted, #64748b);
}
span {
font-size: 14px;
color: var(--mint-text, #0f172a);
}
}
.detail-muted {
color: var(--mint-text-muted, #64748b);
font-weight: 400;
}
.amount-text {
font-weight: 600;
color: var(--primary-dark, #059669);
}
.section-subtitle {
margin: 0 0 10px;
font-size: 13px;
font-weight: 600;
color: var(--mint-text-secondary, #475569);
}
.muted-hint {
font-size: 13px;
color: var(--mint-text-muted, #94a3b8);
}
.schedule-block {
margin-bottom: 12px;
padding: 10px 12px;
border-radius: 8px;
background: var(--mint-surface-muted, #f8fafc);
}
.schedule-block__head {
display: flex;
flex-wrap: wrap;
gap: 8px 12px;
align-items: center;
margin-bottom: 10px;
font-size: 13px;
}
.transfer-table {
width: 100%;
:deep(.el-date-editor) {
width: 100%;
}
}
.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;
}
.upload-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 88px;
height: 72px;
cursor: pointer;
border: 1px dashed #a7f3d0;
border-radius: 8px;
background: #ecfdf5;
}
.upload-card-text {
margin-top: 4px;
font-size: 11px;
color: #047857;
}
.attachment-item {
width: 88px;
overflow: hidden;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
&.is-uploading {
opacity: 0.6;
}
}
.attachment-preview {
display: flex;
align-items: center;
justify-content: center;
width: 88px;
height: 52px;
background: #f8fafc;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.attachment-preview__icon {
color: #64748b;
}
.attachment-footer {
display: flex;
gap: 4px;
align-items: center;
padding: 4px 6px;
}
.attachment-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
}
.action-btn {
flex-shrink: 0;
cursor: pointer;
color: #94a3b8;
&:hover {
color: #dc2626;
}
}
</style>
@@ -169,11 +169,11 @@
<el-icon><Document /></el-icon>协议单
</el-button>
<el-button
v-if="row.canConfirm"
class="btn-op-item primary"
@click="handleConfirm(row)"
:disabled="!row.id"
@click="handleOpenDetail(row)"
>
<el-icon><Check /></el-icon>缴费确认
<el-icon><List /></el-icon>缴费明细
</el-button>
</template>
</el-table-column>
@@ -253,25 +253,13 @@
<el-button class="btn-ghost" @click="periodDialogVisible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
<el-button
v-if="periodCanConfirm"
class="btn-main"
type="primary"
:loading="confirming"
@click="openConfirmFromPeriod"
>
<el-icon><Check /></el-icon>缴费确认
</el-button>
</template>
</el-dialog>
<PaymentReceiptConfirmDialog
v-model="receiptDialogVisible"
:preview-row="receiptPreviewRow"
:is-transfer="true"
:target-id="receiptTargetId"
@saved="onReceiptRefresh"
@submitted="onReceiptRefresh"
<SamplePaymentDetailDialog
v-model="detailVisible"
:sample-payment-id="detailSamplePaymentId"
@changed="loadData"
/>
<el-dialog
@@ -303,16 +291,12 @@
</template>
<script setup lang="ts">
import { Search, RefreshLeft, Check, Document, Close } from '@element-plus/icons-vue'
import { Search, RefreshLeft, Document, Close, List } from '@element-plus/icons-vue'
import ProtocolViewDialog from '../protocol/ProtocolViewDialog.vue'
import ProtocolAttachmentReadonly from '../protocol/ProtocolAttachmentReadonly.vue'
import PaymentReceiptConfirmDialog from '@/components/business/payment-receipt-confirm-dialog/index.vue'
import SamplePaymentDetailDialog from '@/components/business/sample-payment-detail-dialog/index.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import {
fetchSamplePaymentList,
fetchSamplePaymentDetail,
fetchCreatePaymentTransfer
} from '@/api/protocol-sample-payment'
import { fetchSamplePaymentList, fetchSamplePaymentDetail } from '@/api/protocol-sample-payment'
import { type CollectionProtocolRow } from '@/api/collection-protocol'
import { formatDisplayOrDash } from '@/utils/datetime-display'
import { formatMoneyDisplay } from '@/utils/money-format'
@@ -366,14 +350,9 @@
arrearsAmount: null as number | null
})
const periodTransfers = ref<Record<string, any>[]>([])
const periodCanConfirm = ref(false)
const periodSchedule = ref<Record<string, any> | null>(null)
const periodSamplePaymentId = ref('')
const confirming = ref(false)
const receiptDialogVisible = ref(false)
const receiptPreviewRow = ref<Record<string, any> | null>(null)
const receiptTargetId = ref('')
const detailVisible = ref(false)
const detailSamplePaymentId = ref('')
const protocolVisible = ref(false)
const protocolViewId = ref('')
@@ -513,93 +492,32 @@
async function openPeriodDialog(row: Record<string, any>, idx: number) {
const cell = periodRaw(row, idx)
if (!cell?.scheduleId) return
periodSamplePaymentId.value = String(row.id || '')
periodDialogTitle.value = `${row.motherName || ''} · ${row.protocolNo || ''} · 第 ${idx + 1} 期转账记录`
periodDialogVisible.value = true
periodLoading.value = true
periodTransfers.value = []
periodCanConfirm.value = false
periodSchedule.value = null
try {
const detail = await fetchSamplePaymentDetail(String(row.id))
const schedules = ((detail.schedules as any[]) || []) as Record<string, any>[]
const sch = schedules.find((s) => String(s.id) === String(cell.scheduleId))
periodSchedule.value = sch || null
periodSummary.dueAmount = sch?.dueAmount ?? cell.dueAmount ?? null
// 列表 displayPaidAmount / arrearsAmount 与详情 paidAmount 已对齐(含 dfj+jfqrz+yjf)
periodSummary.paidAmount =
sch?.paidAmount ?? cell.displayPaidAmount ?? null
periodSummary.arrearsAmount =
sch?.arrearsAmount ?? cell.arrearsAmount ?? null
periodSummary.paidAmount = sch?.paidAmount ?? cell.displayPaidAmount ?? null
periodSummary.arrearsAmount = sch?.arrearsAmount ?? cell.arrearsAmount ?? null
periodTransfers.value = (sch?.transfers || []) as Record<string, any>[]
const hasJfqrz = periodTransfers.value.some((t) => String(t.payStatus) === 'jfqrz')
const arrears = Number(periodSummary.arrearsAmount)
periodCanConfirm.value = !hasJfqrz && Number.isFinite(arrears) && arrears > 0
} finally {
periodLoading.value = false
}
}
async function openConfirmFromPeriod() {
const schedule = periodSchedule.value
if (!schedule?.id) return
confirming.value = true
try {
const transfers = (schedule.transfers || []) as Record<string, any>[]
const dfj = transfers.find((t) => String(t.payStatus) === 'dfj')
let transferId = dfj?.id ? String(dfj.id) : ''
if (!transferId) {
const arrears = Number(schedule.arrearsAmount)
const due = Number(schedule.dueAmount)
const amount = arrears > 0 ? arrears : due
const created = await fetchCreatePaymentTransfer({
scheduleId: String(schedule.id),
transferAmount: amount
})
transferId = String((created as any)?.id || (created as any)?.data?.id || '')
if (!transferId) {
ElMessage.error('创建转账笔失败')
return
}
}
receiptTargetId.value = transferId
receiptPreviewRow.value = {
motherName: periodDialogTitle.value,
installmentNo: schedule.scheduleNo,
dueAmount: schedule.dueAmount,
installmentDueDate: schedule.dueDate,
scheduleId: schedule.id,
transferAmount: schedule.arrearsAmount || schedule.dueAmount
}
receiptDialogVisible.value = true
periodDialogVisible.value = false
} finally {
confirming.value = false
}
}
async function handleConfirm(row: Record<string, any>) {
const detail = await fetchSamplePaymentDetail(String(row.id))
const schedules = ((detail.schedules as any[]) || []) as Record<string, any>[]
const target = [...schedules]
.sort((a, b) => Number(a.scheduleNo || 0) - Number(b.scheduleNo || 0))
.find((s) => {
const transfers = (s.transfers || []) as any[]
if (transfers.some((t) => String(t.payStatus) === 'jfqrz')) return false
return Number(s.arrearsAmount) > 0
})
if (!target) {
ElMessage.warning('暂无可确认的期次')
function handleOpenDetail(row: Record<string, any>) {
const id = String(row.id || '')
if (!id) {
ElMessage.warning('缺少缴费方案,无法查看明细')
return
}
periodSamplePaymentId.value = String(row.id)
periodSchedule.value = target
periodDialogTitle.value = `${row.motherName || ''} · ${row.protocolNo || ''} · 第 ${target.scheduleNo} 期`
await openConfirmFromPeriod()
}
function onReceiptRefresh() {
loadData()
detailSamplePaymentId.value = id
detailVisible.value = true
}
onMounted(() => {