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:
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(() => {
|
||||
|
||||
Reference in new issue
Block a user