提交
This commit is contained in:
1 parent
e5ba8f097a
commit
62cfb8fbbf
24 files changed
+3323
-249
No files matched your search
@@ -56,6 +56,8 @@
|
||||
const buttons = document.querySelectorAll<HTMLElement>(selectors.join(','))
|
||||
buttons.forEach((button) => {
|
||||
autoIconClassList.forEach((className) => button.classList.remove(className))
|
||||
// 区域/状态 Tab 仅展示文案,不参与按钮自动图标
|
||||
if (button.classList.contains('region-tab-btn')) return
|
||||
const text = (button.textContent || '').replace(/\s+/g, '')
|
||||
const matched = iconClassByText.find(([rule]) => rule.test(text))
|
||||
if (matched) button.classList.add(matched[1])
|
||||
@@ -78,7 +80,7 @@
|
||||
const panelTitleClassList = panelTitleIconByText.map(([, cls]) => cls)
|
||||
const panelTitleBarClass = 'title-bar-heading'
|
||||
const panelTitleBarTextRules: RegExp[] = [
|
||||
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|收款确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表)/
|
||||
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|缴费确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表|缴费信息列表)/
|
||||
]
|
||||
|
||||
const refreshPanelTitleIcons = () => {
|
||||
|
||||
@@ -68,9 +68,16 @@ export function fetchReceiptConfirmList(params?: { page?: number; pageSize?: num
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchReceiptConfirmedList(params?: { page?: number; pageSize?: number }) {
|
||||
return request.get<PageList<Record<string, unknown>>>({
|
||||
url: '/web/admin/protocol-payment-details/receipt-confirmed',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchReceiptConfirmSubmit(
|
||||
detailId: string,
|
||||
data?: { financeConfirmAttachments?: string }
|
||||
data?: { financeConfirmAttachments?: string; financeRemark?: string }
|
||||
) {
|
||||
return request.post({
|
||||
url: `/web/admin/protocol-payment-details/${detailId}/receipt-confirm`,
|
||||
@@ -86,9 +93,16 @@ export function fetchRefundConfirmList(params?: { page?: number; pageSize?: numb
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchRefundConfirmedList(params?: { page?: number; pageSize?: number }) {
|
||||
return request.get<PageList<Record<string, unknown>>>({
|
||||
url: '/web/admin/protocol-payment-details/refund-confirmed',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchRefundConfirmSubmit(
|
||||
detailId: string,
|
||||
data?: { financeConfirmAttachments?: string }
|
||||
data?: { refundFinanceConfirmAttachments?: string; refundFinanceRemark?: string }
|
||||
) {
|
||||
return request.post({
|
||||
url: `/web/admin/protocol-payment-details/${detailId}/refund-confirm`,
|
||||
@@ -97,13 +111,58 @@ export function fetchRefundConfirmSubmit(
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchInstallmentWarningList(params?: {
|
||||
export function fetchSubmitReceiptFinance(detailId: string) {
|
||||
return request.post({
|
||||
url: `/web/admin/protocol-payment-details/${detailId}/submit-receipt-finance`,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSubmitReceiptFinanceBatch(detailIds: string[]) {
|
||||
return request.post({
|
||||
url: '/web/admin/protocol-payment-details/submit-receipt-finance-batch',
|
||||
data: { detailIds },
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSubmitRefundFinance(
|
||||
detailId: string,
|
||||
data: {
|
||||
refundAmount: number
|
||||
refundRemark?: string
|
||||
refundAttachments?: string
|
||||
}
|
||||
) {
|
||||
return request.post({
|
||||
url: `/web/admin/protocol-payment-details/${detailId}/submit-refund-finance`,
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSubmitRefundFinanceBatch(data: {
|
||||
detailIds: string[]
|
||||
refundAmount: number
|
||||
refundRemark?: string
|
||||
refundAttachments?: string
|
||||
}) {
|
||||
return request.post({
|
||||
url: '/web/admin/protocol-payment-details/submit-refund-finance-batch',
|
||||
data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchPaymentDetailList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
warningType?: string
|
||||
payStatus?: string
|
||||
keyword?: string
|
||||
transferType?: string
|
||||
}) {
|
||||
return request.get<PageList<Record<string, unknown>>>({
|
||||
url: '/web/admin/protocol-payment-details/installment-warning',
|
||||
url: '/web/admin/protocol-payment-details',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export type FinanceAttachmentItem = {
|
||||
id: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
fileType?: string
|
||||
_uploading?: boolean
|
||||
}
|
||||
|
||||
export function financeAttachmentsToUrlString(items: FinanceAttachmentItem[]) {
|
||||
return items
|
||||
.filter((f) => f.fileUrl && !f._uploading)
|
||||
.map((f) => f.fileUrl)
|
||||
.join(',')
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
<!-- 财务确认弹窗:附件上传 + 财务备注(对齐采集协议单附件 UI) -->
|
||||
<template>
|
||||
<div class="protocol-finance-confirm-fields">
|
||||
<el-form-item label="财务备注" class="form-col-span-2">
|
||||
<el-input
|
||||
:model-value="financeRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="选填"
|
||||
@update:model-value="emit('update:financeRemark', $event)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务确认附件" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||||
@change="handleAttachmentChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="22" color="#10b981"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
v-for="(file, fIdx) in attachments"
|
||||
:key="file.id || fIdx"
|
||||
class="attachment-item"
|
||||
:class="{ 'is-uploading': file._uploading }"
|
||||
>
|
||||
<div
|
||||
class="attachment-preview"
|
||||
@click="isImageFile(file) && file.fileUrl ? handlePreview(file) : undefined"
|
||||
>
|
||||
<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
|
||||
v-if="file.fileUrl"
|
||||
class="action-btn"
|
||||
title="下载"
|
||||
@click.stop="downloadFile(file)"
|
||||
>
|
||||
<Download />
|
||||
</el-icon>
|
||||
<el-icon class="action-btn" title="删除" @click.stop="removeAttachment(fIdx)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="upload-hint">支持图片、视频、文档等,单文件不超过 20MB。</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Document,
|
||||
Delete,
|
||||
Download,
|
||||
Loading,
|
||||
VideoPlay
|
||||
} from '@element-plus/icons-vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import type { FinanceAttachmentItem } from './finance-attachments'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
financeRemark: string
|
||||
attachments: FinanceAttachmentItem[]
|
||||
uploadFolder?: string
|
||||
}>(),
|
||||
{ uploadFolder: 'protocol-finance-confirm' }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:financeRemark', val: string): void
|
||||
(e: 'update:attachments', val: FinanceAttachmentItem[]): void
|
||||
}>()
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function fullFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||||
}
|
||||
|
||||
function isImageFile(file: FinanceAttachmentItem) {
|
||||
const t = file.fileType || ''
|
||||
const n = file.fileName || ''
|
||||
return t.startsWith('image/') || /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(n)
|
||||
}
|
||||
|
||||
function isVideoFile(file: FinanceAttachmentItem) {
|
||||
const t = file.fileType || ''
|
||||
const n = file.fileName || ''
|
||||
return t.startsWith('video/') || /\.(mp4|webm|mov|avi|mkv)$/i.test(n)
|
||||
}
|
||||
|
||||
function handlePreview(file: FinanceAttachmentItem) {
|
||||
if (!file.fileUrl) return
|
||||
window.open(fullFileUrl(file.fileUrl), '_blank')
|
||||
}
|
||||
|
||||
function downloadFile(file: FinanceAttachmentItem) {
|
||||
if (!file.fileUrl) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(file.fileUrl)
|
||||
a.download = file.fileName || 'download'
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
}
|
||||
|
||||
function removeAttachment(index: number) {
|
||||
const next = [...props.attachments]
|
||||
next.splice(index, 1)
|
||||
emit('update:attachments', next)
|
||||
}
|
||||
|
||||
async function handleAttachmentChange(uploadFile: { raw?: File }) {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
const maxSizeMb = 20
|
||||
if (file.size / 1024 / 1024 > maxSizeMb) {
|
||||
ElMessage.error(`文件大小不能超过 ${maxSizeMb}MB`)
|
||||
return
|
||||
}
|
||||
const tempId = `temp_${Date.now()}`
|
||||
const uploading: FinanceAttachmentItem = {
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
fileUrl: '',
|
||||
fileType: file.type || '',
|
||||
_uploading: true
|
||||
}
|
||||
const listWithTemp = [...props.attachments, uploading]
|
||||
emit('update:attachments', listWithTemp)
|
||||
const idx = listWithTemp.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', props.uploadFolder)
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
const updated = [...listWithTemp]
|
||||
updated[idx] = {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
fileType: res.fileType || file.type || '',
|
||||
_uploading: false
|
||||
}
|
||||
emit('update:attachments', updated)
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
emit(
|
||||
'update:attachments',
|
||||
props.attachments.filter((a) => a.id !== tempId)
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.protocol-finance-confirm-fields {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 120px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
border-style: solid;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #10b981;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.4;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&.is-uploading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #64748b;
|
||||
|
||||
&--video {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,253 @@
|
||||
<!-- 缴费明细只读信息(核对用) -->
|
||||
<template>
|
||||
<div class="protocol-payment-detail-readonly">
|
||||
<h4 v-if="title" class="detail-block-title">{{ title }}</h4>
|
||||
<dl class="view-kv-grid view-kv-grid--4">
|
||||
<div class="view-kv">
|
||||
<dt>协议号</dt>
|
||||
<dd>{{ dash(detail?.protocolNo) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>协议状态</dt>
|
||||
<dd>{{ protocolStatusLabel(String(detail?.protocolStatus || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>缴费方式</dt>
|
||||
<dd>{{ payModeLabel(String(detail?.payMode || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>缴费状态</dt>
|
||||
<dd>{{ payStatusLabel(String(detail?.payStatus || '')) }}</dd>
|
||||
</div>
|
||||
<template v-if="showCustomer">
|
||||
<div class="view-kv">
|
||||
<dt>客户姓名</dt>
|
||||
<dd>{{ dash(detail?.motherName) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>身份证号</dt>
|
||||
<dd>{{ maskIdCard(String(detail?.motherIdCard || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>新生儿姓名</dt>
|
||||
<dd>{{ dash(detail?.newbornName) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>新生儿身份证号</dt>
|
||||
<dd>{{ maskIdCard(String(detail?.newbornIdCard || '')) }}</dd>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="detail?.payMode === 'fq'" class="view-kv">
|
||||
<dt>分期期序号</dt>
|
||||
<dd>{{ dash(detail?.installmentNo) }}</dd>
|
||||
</div>
|
||||
<div v-if="detail?.payMode === 'fq'" class="view-kv">
|
||||
<dt>应缴费时间</dt>
|
||||
<dd>{{ formatPaymentDueDate(detail?.installmentDueDate) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>应缴费金额</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.dueAmount) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>转账方式</dt>
|
||||
<dd>{{ transferTypeLabel(String(detail?.transferType || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>转账金额</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.transferAmount) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>转账时间</dt>
|
||||
<dd>{{ dash(detail?.transferTime) }}</dd>
|
||||
</div>
|
||||
<div v-if="detail?.remark" class="view-kv view-kv-span-2">
|
||||
<dt>备注</dt>
|
||||
<dd>{{ dash(detail?.remark) }}</dd>
|
||||
</div>
|
||||
<div class="view-field-block view-kv-span-2 payment-detail-readonly__attachments">
|
||||
<div class="view-field-block__label">转账凭证</div>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="transferAttachmentFiles"
|
||||
@preview="(f) => emit('preview', f)"
|
||||
@download="(f) => emit('download', f)"
|
||||
/>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<template v-if="showRefundApply && hasRefundApplyInfo">
|
||||
<div class="form-section-divider form-section-divider--inner" />
|
||||
<h4 v-if="refundApplyTitle" class="detail-block-title">{{ refundApplyTitle }}</h4>
|
||||
<dl class="view-kv-grid view-kv-grid--4">
|
||||
<div class="view-kv">
|
||||
<dt>退费金额</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.refundAmount) }}</dd>
|
||||
</div>
|
||||
<div v-if="detail?.refundRemark" class="view-kv view-kv-span-2">
|
||||
<dt>退费备注</dt>
|
||||
<dd>{{ dash(detail?.refundRemark) }}</dd>
|
||||
</div>
|
||||
<div class="view-field-block view-kv-span-2 payment-detail-readonly__attachments">
|
||||
<div class="view-field-block__label">退费附件</div>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="refundAttachmentFiles"
|
||||
@preview="(f) => emit('preview', f)"
|
||||
@download="(f) => emit('download', f)"
|
||||
/>
|
||||
</div>
|
||||
</dl>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import {
|
||||
formatPaymentAmount,
|
||||
formatPaymentDueDate,
|
||||
payModeLabel,
|
||||
payStatusLabel,
|
||||
protocolStatusLabel,
|
||||
transferTypeLabel
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
detail?: Record<string, unknown> | null
|
||||
title?: string
|
||||
showCustomer?: boolean
|
||||
showRefundApply?: boolean
|
||||
refundApplyTitle?: string
|
||||
}>(),
|
||||
{ showCustomer: true, showRefundApply: false, refundApplyTitle: '退费申请(请核对)' }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'preview', file: Record<string, unknown>): void
|
||||
(e: 'download', file: Record<string, unknown>): void
|
||||
}>()
|
||||
|
||||
function dash(v: unknown) {
|
||||
if (v == null || v === '') return '—'
|
||||
return String(v)
|
||||
}
|
||||
|
||||
const transferAttachmentFiles = computed(() => {
|
||||
const row = props.detail
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.attachments)) {
|
||||
return row.attachments as Array<Record<string, unknown>>
|
||||
}
|
||||
const urls = String(row.transferAttachments || '')
|
||||
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 }
|
||||
})
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
const refundAttachmentFiles = computed(() => {
|
||||
const row = props.detail
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.refundAttachmentFiles)) {
|
||||
return row.refundAttachmentFiles as Array<Record<string, unknown>>
|
||||
}
|
||||
const urls = String(row.refundAttachments || '')
|
||||
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 }
|
||||
})
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
const hasRefundApplyInfo = computed(() => {
|
||||
const row = props.detail
|
||||
if (!row) return false
|
||||
const amount = row.refundAmount
|
||||
const hasAmount = amount != null && amount !== '' && Number(amount) > 0
|
||||
return (
|
||||
hasAmount ||
|
||||
String(row.refundRemark || '').trim() !== '' ||
|
||||
refundAttachmentFiles.value.length > 0
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.protocol-payment-detail-readonly {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detail-block-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.view-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.view-kv-grid--4 {
|
||||
@media (min-width: 720px) {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.view-kv {
|
||||
min-width: 0;
|
||||
|
||||
dt {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.view-kv-span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.view-field-block__label {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.payment-detail-readonly__attachments {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.form-section-divider--inner {
|
||||
height: 1px;
|
||||
margin: 16px 0;
|
||||
background: var(--mint-border, #e2e8f0);
|
||||
}
|
||||
</style>
|
||||
@@ -129,6 +129,13 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
component: '/cell-storage/protocol/index',
|
||||
meta: { title: '采集协议单', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000007',
|
||||
name: 'CellStoragePaymentInfo',
|
||||
path: 'payment-info',
|
||||
component: '/cell-storage/payment-info/index',
|
||||
meta: { title: '缴费信息', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000003',
|
||||
name: 'CellStorageProtocolAudit',
|
||||
@@ -141,7 +148,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
name: 'CellStoragePaymentConfirm',
|
||||
path: 'payment-confirm',
|
||||
component: '/cell-storage/payment-confirm/index',
|
||||
meta: { title: '收款确认(财务)', keepAlive: true }
|
||||
meta: { title: '缴费确认(财务)', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000005',
|
||||
@@ -149,13 +156,6 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
path: 'refund-confirm',
|
||||
component: '/cell-storage/refund-confirm/index',
|
||||
meta: { title: '退费确认(财务)', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000006',
|
||||
name: 'CellStorageInstallmentWarning',
|
||||
path: 'installment-warning',
|
||||
component: '/cell-storage/installment-warning/index',
|
||||
meta: { title: '分期缴费预警', keepAlive: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/** 身份证号脱敏:18 位保留前 6 后 4,15 位保留前 6 后 3 */
|
||||
export function maskIdCard(value: unknown): string {
|
||||
const raw = String(value ?? '').trim()
|
||||
if (!raw) return '-'
|
||||
const len = raw.length
|
||||
if (len === 18) {
|
||||
return `${raw.slice(0, 6)}${'*'.repeat(8)}${raw.slice(14)}`
|
||||
}
|
||||
if (len === 15) {
|
||||
return `${raw.slice(0, 6)}${'*'.repeat(6)}${raw.slice(12)}`
|
||||
}
|
||||
if (len <= 4) {
|
||||
return '*'.repeat(len)
|
||||
}
|
||||
const head = Math.min(3, Math.floor(len / 3))
|
||||
const tail = Math.min(4, len - head - 1)
|
||||
return `${raw.slice(0, head)}${'*'.repeat(len - head - tail)}${raw.slice(len - tail)}`
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export function protocolStatusLabel(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dsx: '待生效',
|
||||
sxz: '生效中',
|
||||
ygq: '已过期',
|
||||
yzf: '已作废',
|
||||
zky: '转科研'
|
||||
}
|
||||
return map[s] || s || '-'
|
||||
}
|
||||
|
||||
export function payModeLabel(s: string) {
|
||||
if (s === 'dj') return '趸交'
|
||||
if (s === 'fq') return '分期'
|
||||
return s || '-'
|
||||
}
|
||||
|
||||
export function payStatusLabel(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dfj: '待缴费',
|
||||
jfqrz: '缴费确认中',
|
||||
yjf: '已缴费',
|
||||
tfqrz: '退费确认中',
|
||||
ytf: '已退费'
|
||||
}
|
||||
return map[s] || '未知'
|
||||
}
|
||||
|
||||
export function transferTypeLabel(t: string) {
|
||||
const map: Record<string, string> = {
|
||||
wxzz: '微信转账',
|
||||
zzbzz: '支付宝转账',
|
||||
yhkzz: '银行卡转账',
|
||||
xj: '现金'
|
||||
}
|
||||
return map[t] || t || '-'
|
||||
}
|
||||
|
||||
export function formatPaymentDueDate(d: unknown) {
|
||||
const s = String(d || '')
|
||||
if (!s) return '-'
|
||||
if (s.length === 8) return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`
|
||||
return s
|
||||
}
|
||||
|
||||
export function formatPaymentAmount(v: unknown) {
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
<!-- 分期缴费预警 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>分期缴费预警</h1>
|
||||
<p class="page-desc">仅展示待缴费分期;预警窗口为未来 15 天内应缴;逾期为应缴日早于今日。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">分期预警列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="范围">
|
||||
<el-select v-model="warningType" style="width: 100%" @change="handleSearch">
|
||||
<el-option label="全部" value="all" />
|
||||
<el-option label="预警(15天内)" value="warn" />
|
||||
<el-option label="逾期" value="overdue" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id">
|
||||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||||
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentDueDate" label="应缴日" width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="warningType" label="分级" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="warningTagType(String(row.warningType || ''))" effect="plain">{{
|
||||
warningTypeLabel(String(row.warningType || ''))
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferAmount" label="金额" width="120" align="center" header-align="center" />
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { fetchInstallmentWarningList } from '@/api/collection-protocol'
|
||||
|
||||
defineOptions({ name: 'CellStorageInstallmentWarning' })
|
||||
|
||||
function warningTypeLabel(w: string) {
|
||||
if (w === 'overdue') return '逾期'
|
||||
if (w === 'warn') return '预警'
|
||||
if (w === 'upcoming') return '未到期'
|
||||
return '其他'
|
||||
}
|
||||
|
||||
function warningTagType(w: string) {
|
||||
if (w === 'overdue') return 'danger'
|
||||
if (w === 'warn') return 'warning'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const warningType = ref('all')
|
||||
const tableData = ref<Record<string, unknown>[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchInstallmentWarningList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
warningType: warningType.value
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
@@ -1,31 +1,132 @@
|
||||
<!-- 收款确认(财务) -->
|
||||
<!-- 缴费确认(财务) -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>收款确认</h1>
|
||||
<p class="page-desc">仅处理「缴费确认中」明细,确认后变为已缴费。</p>
|
||||
<h1>缴费确认</h1>
|
||||
<p class="page-desc">
|
||||
待确认:处理「缴费确认中」明细;已确认:查看财务确认收款记录及当时填写的备注与附件。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="region-tabs region-tab-strip">
|
||||
<div class="tab-button-group region-tab-group">
|
||||
<el-button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="region-tab-btn"
|
||||
:type="activeTab === tab.key ? 'primary' : 'default'"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
<span class="region-tab-inner">
|
||||
<span class="region-tab-name">{{ tab.label }}</span>
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">收款确认列表</h2>
|
||||
<h2 class="title-bar-heading">缴费确认列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="list-body list-wrap payment-confirm-table-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id">
|
||||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentDueDate" label="应缴日" width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="transferAmount" label="金额" width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="payStatus" label="状态" width="120" align="center" header-align="center">
|
||||
<template #default>缴费确认中</template>
|
||||
<el-table-column prop="motherName" label="客户姓名" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.motherName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="180" align="center" header-align="center" />
|
||||
<el-table-column prop="motherIdCard" label="身份证号" min-width="170" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ maskIdCard(row.motherIdCard) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="newbornName" label="新生儿姓名" min-width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.newbornName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornIdCard"
|
||||
label="新生儿身份证号"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ maskIdCard(row.newbornIdCard) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="protocolStatus" label="协议状态" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ protocolStatusLabel(String(row.protocolStatus || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payMode" label="缴费方式" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ payModeLabel(String(row.payMode || '')) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.installmentNo ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="installmentDueDate" label="应缴费时间" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentDueDate(row.installmentDueDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dueAmount" label="应缴金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.dueAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferAmount" label="实缴金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.transferAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferType" label="转账方式" width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ transferTypeLabel(String(row.transferType || '')) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferTime" label="转账时间" min-width="170" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.transferTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payStatus" label="状态" width="110" align="center" header-align="center">
|
||||
<template #default>
|
||||
{{ activeTab === 'pending' ? '缴费确认中' : '已缴费' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'confirmed'"
|
||||
prop="updateTime"
|
||||
label="确认时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'confirmed'"
|
||||
prop="financeRemark"
|
||||
label="财务备注"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.financeRemark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'pending'"
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="140" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="openConfirm(row)">确认收款</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'pending'"
|
||||
class="btn-op-item primary"
|
||||
@click="openConfirm(row)"
|
||||
>
|
||||
确认收款
|
||||
</el-button>
|
||||
<el-button v-else class="btn-op-item" @click="openViewConfirm(row)">查看确认信息</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -45,15 +146,26 @@
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="确认收款"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
width="900px"
|
||||
class="dialog-form-layout payment-confirm-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ProtocolPaymentDetailReadonly
|
||||
:detail="confirmRow"
|
||||
title="缴费明细(请核对)"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
<div class="form-section-divider" />
|
||||
<h4 class="detail-section-title">财务确认</h4>
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
<el-form-item label="财务确认附件(MinIO 地址,逗号分隔,可选)">
|
||||
<el-input v-model="confirmAttachments" type="textarea" :rows="3" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<ProtocolFinanceConfirmFields
|
||||
v-model:finance-remark="financeRemark"
|
||||
v-model:attachments="attachments"
|
||||
upload-folder="protocol-finance-receipt"
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||||
@@ -66,32 +178,150 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="viewDialogVisible"
|
||||
title="确认收款信息"
|
||||
width="900px"
|
||||
class="dialog-form-layout payment-confirm-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
>
|
||||
<ProtocolPaymentDetailReadonly
|
||||
:detail="viewRow"
|
||||
title="缴费明细"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
<div class="form-section-divider" />
|
||||
<h4 class="detail-section-title">财务确认信息</h4>
|
||||
<el-form label-position="top" class="biz-dialog-form view-confirm-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="确认时间" class="form-col-span-2">
|
||||
<span class="view-field-text">{{ viewRow?.updateTime || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务备注" class="form-col-span-2">
|
||||
<span class="view-field-text view-field-text--multiline">{{
|
||||
viewRow?.financeRemark || '—'
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务确认附件" class="form-col-span-2">
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="viewFinanceAttachmentFiles"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="viewDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Check } from '@element-plus/icons-vue'
|
||||
import { fetchReceiptConfirmList, fetchReceiptConfirmSubmit } from '@/api/collection-protocol'
|
||||
import {
|
||||
fetchReceiptConfirmList,
|
||||
fetchReceiptConfirmedList,
|
||||
fetchReceiptConfirmSubmit
|
||||
} from '@/api/collection-protocol'
|
||||
import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
|
||||
import ProtocolPaymentDetailReadonly from '@/components/business/protocol-payment-detail-readonly/index.vue'
|
||||
import ProtocolAttachmentReadonly from '../protocol/ProtocolAttachmentReadonly.vue'
|
||||
import {
|
||||
financeAttachmentsToUrlString,
|
||||
type FinanceAttachmentItem
|
||||
} from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import {
|
||||
formatPaymentAmount,
|
||||
formatPaymentDueDate,
|
||||
payModeLabel,
|
||||
protocolStatusLabel,
|
||||
transferTypeLabel
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
|
||||
defineOptions({ name: 'CellStoragePaymentConfirm' })
|
||||
|
||||
const tabs = [
|
||||
{ key: 'pending', label: '待确认' },
|
||||
{ key: 'confirmed', label: '已确认' }
|
||||
]
|
||||
|
||||
const activeTab = ref('pending')
|
||||
const tableData = ref<Record<string, unknown>[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const viewDialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const confirmAttachments = ref('')
|
||||
const financeRemark = ref('')
|
||||
const attachments = ref<FinanceAttachmentItem[]>([])
|
||||
const currentDetailId = ref<string | null>(null)
|
||||
const confirmRow = ref<Record<string, unknown> | null>(null)
|
||||
const viewRow = ref<Record<string, unknown> | null>(null)
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const viewFinanceAttachmentFiles = computed(() => {
|
||||
const row = viewRow.value
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.financeConfirmAttachmentFiles)) {
|
||||
return row.financeConfirmAttachmentFiles as Array<Record<string, unknown>>
|
||||
}
|
||||
const urls = String(row.financeConfirmAttachments || '')
|
||||
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 }
|
||||
})
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
function fullFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||||
}
|
||||
|
||||
function handleAttachmentPreview(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
window.open(fullFileUrl(url), '_blank')
|
||||
}
|
||||
|
||||
function handleAttachmentDownload(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(url)
|
||||
a.download = String(file.fileName || 'download')
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchReceiptConfirmList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const params = { page: pagination.page, pageSize: pagination.pageSize }
|
||||
const res =
|
||||
activeTab.value === 'pending'
|
||||
? await fetchReceiptConfirmList(params)
|
||||
: await fetchReceiptConfirmedList(params)
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
@@ -99,18 +329,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(key: string) {
|
||||
if (activeTab.value === key) return
|
||||
activeTab.value = key
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openConfirm(row: Record<string, unknown>) {
|
||||
currentDetailId.value = String(row.id)
|
||||
confirmAttachments.value = ''
|
||||
confirmRow.value = row
|
||||
financeRemark.value = ''
|
||||
attachments.value = []
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openViewConfirm(row: Record<string, unknown>) {
|
||||
viewRow.value = row
|
||||
viewDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitConfirm() {
|
||||
if (!currentDetailId.value) return
|
||||
if (attachments.value.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await fetchReceiptConfirmSubmit(currentDetailId.value, {
|
||||
financeConfirmAttachments: confirmAttachments.value || undefined
|
||||
financeConfirmAttachments: financeAttachmentsToUrlString(attachments.value) || undefined,
|
||||
financeRemark: financeRemark.value.trim() || undefined
|
||||
})
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
@@ -123,3 +372,73 @@
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.region-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.region-tab-btn {
|
||||
position: relative;
|
||||
min-width: 108px;
|
||||
padding: 0 12px;
|
||||
|
||||
:deep(.el-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.region-tab-btn.el-button > :deep(span) {
|
||||
position: static !important;
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.region-tab-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.payment-confirm-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.payment-confirm-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section-divider {
|
||||
height: 1px;
|
||||
margin: 20px 0;
|
||||
background: var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.view-confirm-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.view-field-text {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
|
||||
&--multiline {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,888 @@
|
||||
<!-- 缴费信息 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>缴费信息</h1>
|
||||
<p class="page-desc">查看采集协议单缴费明细,按状态分为待缴费、缴费确认中、已缴费、退费确认中、已退费。</p>
|
||||
</header>
|
||||
|
||||
<div class="region-tabs region-tab-strip">
|
||||
<div class="tab-button-group region-tab-group">
|
||||
<el-button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="region-tab-btn"
|
||||
:type="activeTab === tab.key ? 'primary' : 'default'"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
<span class="region-tab-inner">
|
||||
<span class="region-tab-name">{{ tab.label }}</span>
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">缴费信息列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" label-position="top" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="协议号"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账方式">
|
||||
<el-select v-model="searchForm.transferType" placeholder="全部" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="微信转账" value="wxzz" />
|
||||
<el-option label="支付宝转账" value="zzbzz" />
|
||||
<el-option label="银行卡转账" value="yhkzz" />
|
||||
<el-option label="现金" value="xj" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">支持按协议号模糊检索。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="activeTab === 'dfj' || activeTab === 'yjf'"
|
||||
class="list-toolbar"
|
||||
>
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="handleBatchSubmitFinance"
|
||||
>
|
||||
{{ activeTab === 'dfj' ? '批量缴费确认' : '批量退费确认' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'dfj' || activeTab === 'yjf'"
|
||||
type="selection"
|
||||
width="55"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="protocolNo"
|
||||
label="协议号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="motherName"
|
||||
label="客户姓名"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.motherName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="motherIdCard"
|
||||
label="身份证号"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ maskIdCard(row.motherIdCard) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornName"
|
||||
label="新生儿姓名"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.newbornName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornIdCard"
|
||||
label="新生儿身份证号"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ maskIdCard(row.newbornIdCard) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="protocolStatus"
|
||||
label="协议状态"
|
||||
width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ protocolStatusLabel(String(row.protocolStatus || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="payMode"
|
||||
label="缴费方式"
|
||||
width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ payModeLabel(String(row.payMode || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="installmentNo"
|
||||
label="期数"
|
||||
width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="installmentDueDate"
|
||||
label="应缴费时间"
|
||||
width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(String(row.installmentDueDate || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="dueAmount"
|
||||
label="应缴金额(元)"
|
||||
width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="amount-text">{{ formatAmount(row.dueAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="transferAmount"
|
||||
label="实缴金额(元)"
|
||||
width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="amount-text">{{ formatAmount(row.transferAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="transferType"
|
||||
label="转账方式"
|
||||
width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ transferTypeLabel(String(row.transferType || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="transferTime"
|
||||
label="转账时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.transferTime || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="payStatus"
|
||||
label="状态"
|
||||
width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="payStatusTagType(String(row.payStatus || ''))" size="small">
|
||||
{{ payStatusLabel(String(row.payStatus || '')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
:min-width="activeTab === 'dfj' || activeTab === 'yjf' ? 280 : 120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'dfj'"
|
||||
class="btn-op-item primary"
|
||||
@click="handleSubmitReceiptFinance(row)"
|
||||
>
|
||||
缴费确认
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'yjf'"
|
||||
class="btn-op-item primary"
|
||||
@click="openRefundFinanceDialog(row)"
|
||||
>
|
||||
退费确认
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog
|
||||
v-model="refundDialogVisible"
|
||||
title="退费确认"
|
||||
width="640px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="refundFormRef"
|
||||
:model="refundForm"
|
||||
:rules="refundFormRules"
|
||||
label-position="top"
|
||||
class="biz-dialog-form"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="退费金额(元)" prop="refundAmount" class="form-col-span-2">
|
||||
<el-input-number
|
||||
v-model="refundForm.refundAmount"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
:step="100"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="退费备注" prop="refundRemark" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="refundForm.refundRemark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="选填"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="退费附件" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||||
@change="handleRefundAttachmentChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="22" color="#10b981"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
v-for="(file, fIdx) in refundForm.attachments"
|
||||
: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 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>
|
||||
<el-icon class="action-btn" title="删除" @click="removeRefundAttachment(fIdx)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-tip">支持图片、文档等,多个附件将一并提交。</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="refundDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="refundSubmitLoading"
|
||||
@click="submitRefundFinance"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
确认提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<ProtocolViewDialog
|
||||
v-model="viewVisible"
|
||||
:protocol-id="viewProtocolId"
|
||||
:list-row="viewRow"
|
||||
show-audit-history
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import {
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View,
|
||||
Plus,
|
||||
Document,
|
||||
Delete,
|
||||
Close,
|
||||
Check
|
||||
} from '@element-plus/icons-vue'
|
||||
import ProtocolViewDialog from '../protocol/ProtocolViewDialog.vue'
|
||||
import {
|
||||
fetchPaymentDetailList,
|
||||
fetchSubmitReceiptFinance,
|
||||
fetchSubmitReceiptFinanceBatch,
|
||||
fetchSubmitRefundFinance,
|
||||
fetchSubmitRefundFinanceBatch,
|
||||
type CollectionProtocolRow
|
||||
} from '@/api/collection-protocol'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
|
||||
defineOptions({ name: 'CellStoragePaymentInfo' })
|
||||
|
||||
const tabs = [
|
||||
{ key: 'dfj', label: '待缴费' },
|
||||
{ key: 'jfqrz', label: '缴费确认中' },
|
||||
{ key: 'yjf', label: '已缴费' },
|
||||
{ key: 'tfqrz', label: '退费确认中' },
|
||||
{ key: 'ytf', label: '已退费' }
|
||||
]
|
||||
|
||||
const activeTab = ref('dfj')
|
||||
const searchForm = reactive({ keyword: '', transferType: '' })
|
||||
const tableData = ref<Record<string, unknown>[]>([])
|
||||
const selectedRows = ref<Record<string, unknown>[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const viewVisible = ref(false)
|
||||
const viewProtocolId = ref<string | null>(null)
|
||||
const viewRow = ref<CollectionProtocolRow | null>(null)
|
||||
const refundDialogVisible = ref(false)
|
||||
const refundSubmitLoading = ref(false)
|
||||
const refundTargetIds = ref<string[]>([])
|
||||
const refundFormRef = ref<FormInstance>()
|
||||
const refundForm = reactive({
|
||||
refundAmount: null as number | null,
|
||||
refundRemark: '',
|
||||
attachments: [] as Array<{
|
||||
id: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
_uploading?: boolean
|
||||
}>
|
||||
})
|
||||
const refundFormRules: FormRules = {
|
||||
refundAmount: [{ required: true, message: '请填写退费金额', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function handleSelectionChange(rows: Record<string, unknown>[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function handleView(row: Record<string, unknown>) {
|
||||
const pid = String(row.collectionProtocolId || '')
|
||||
if (!pid) {
|
||||
ElMessage.warning('缺少关联协议单,无法查看')
|
||||
return
|
||||
}
|
||||
viewProtocolId.value = pid
|
||||
viewRow.value = {
|
||||
id: pid,
|
||||
protocolNo: row.protocolNo,
|
||||
protocolStatus: row.protocolStatus,
|
||||
motherName: row.motherName,
|
||||
motherIdCard: row.motherIdCard,
|
||||
newbornName: row.newbornName,
|
||||
newbornIdCard: row.newbornIdCard
|
||||
}
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function protocolStatusLabel(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dsx: '待生效',
|
||||
sxz: '生效中',
|
||||
ygq: '已过期',
|
||||
yzf: '已作废',
|
||||
zky: '转科研'
|
||||
}
|
||||
return map[s] || s || '-'
|
||||
}
|
||||
|
||||
function payModeLabel(s: string) {
|
||||
if (s === 'dj') return '趸交'
|
||||
if (s === 'fq') return '分期'
|
||||
return s || '-'
|
||||
}
|
||||
|
||||
function payStatusLabel(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dfj: '待缴费',
|
||||
jfqrz: '缴费确认中',
|
||||
yjf: '已缴费',
|
||||
tfqrz: '退费确认中',
|
||||
ytf: '已退费'
|
||||
}
|
||||
return map[s] || '未知'
|
||||
}
|
||||
|
||||
function payStatusTagType(s: string) {
|
||||
const map: Record<string, string> = {
|
||||
dfj: 'info',
|
||||
jfqrz: 'warning',
|
||||
yjf: 'success',
|
||||
tfqrz: 'warning',
|
||||
ytf: 'danger'
|
||||
}
|
||||
return (map[s] || 'info') as 'info' | 'warning' | 'success' | 'danger'
|
||||
}
|
||||
|
||||
function transferTypeLabel(t: string) {
|
||||
const map: Record<string, string> = {
|
||||
wxzz: '微信转账',
|
||||
zzbzz: '支付宝转账',
|
||||
yhkzz: '银行卡转账',
|
||||
xj: '现金'
|
||||
}
|
||||
return map[t] || t || '-'
|
||||
}
|
||||
|
||||
function formatDate(d: string) {
|
||||
if (!d || d.length !== 8) return d || '-'
|
||||
return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`
|
||||
}
|
||||
|
||||
function formatAmount(v: unknown) {
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchPaymentDetailList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
payStatus: activeTab.value,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
transferType: searchForm.transferType || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(key: string) {
|
||||
if (activeTab.value === key) return
|
||||
activeTab.value = key
|
||||
selectedRows.value = []
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleSubmitReceiptFinance(row: Record<string, unknown>) {
|
||||
const id = String(row.id || '')
|
||||
if (!id) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认对该缴费明细进行缴费确认?提交后状态变为「缴费确认中」。', '缴费确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await fetchSubmitReceiptFinance(id)
|
||||
loadData()
|
||||
} catch {
|
||||
/* cancel */
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchSubmitFinance() {
|
||||
if (!selectedRows.value.length) return
|
||||
const ids = selectedRows.value.map((r) => String(r.id || '')).filter(Boolean)
|
||||
if (!ids.length) return
|
||||
|
||||
if (activeTab.value === 'dfj') {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将选中的 ${ids.length} 条待缴费明细批量缴费确认?`,
|
||||
'批量缴费确认',
|
||||
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
await fetchSubmitReceiptFinanceBatch(ids)
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
} catch {
|
||||
/* cancel */
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (activeTab.value === 'yjf') {
|
||||
openRefundFinanceDialog(undefined, ids)
|
||||
}
|
||||
}
|
||||
|
||||
function resetRefundForm(defaultAmount?: number) {
|
||||
refundForm.refundAmount = defaultAmount ?? null
|
||||
refundForm.refundRemark = ''
|
||||
refundForm.attachments = []
|
||||
refundFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function openRefundFinanceDialog(
|
||||
row?: Record<string, unknown>,
|
||||
batchIds?: string[]
|
||||
) {
|
||||
const ids = batchIds?.length
|
||||
? batchIds
|
||||
: row?.id
|
||||
? [String(row.id)]
|
||||
: []
|
||||
if (!ids.length) return
|
||||
refundTargetIds.value = ids
|
||||
const defaultAmount =
|
||||
!batchIds?.length && row?.transferAmount != null
|
||||
? Number(row.transferAmount)
|
||||
: undefined
|
||||
resetRefundForm(
|
||||
defaultAmount != null && !Number.isNaN(defaultAmount) ? defaultAmount : undefined
|
||||
)
|
||||
refundDialogVisible.value = true
|
||||
}
|
||||
|
||||
function refundAttachmentsToUrls() {
|
||||
return refundForm.attachments
|
||||
.filter((f) => f.fileUrl && !f._uploading)
|
||||
.map((f) => f.fileUrl)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
async function handleRefundAttachmentChange(uploadFile: { raw?: File }) {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
const maxSizeMb = 20
|
||||
if (file.size / 1024 / 1024 > maxSizeMb) {
|
||||
ElMessage.error(`文件大小不能超过 ${maxSizeMb}MB`)
|
||||
return
|
||||
}
|
||||
const tempId = `temp_${Date.now()}`
|
||||
refundForm.attachments.push({
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
fileUrl: '',
|
||||
_uploading: true
|
||||
})
|
||||
const idx = refundForm.attachments.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'protocol-payment-refund')
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
refundForm.attachments[idx] = {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
_uploading: false
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
refundForm.attachments.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function removeRefundAttachment(index: number) {
|
||||
refundForm.attachments.splice(index, 1)
|
||||
}
|
||||
|
||||
async function submitRefundFinance() {
|
||||
if (!refundTargetIds.value.length) return
|
||||
const valid = await refundFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (refundForm.attachments.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
const payload = {
|
||||
refundAmount: Number(refundForm.refundAmount),
|
||||
refundRemark: refundForm.refundRemark?.trim() || undefined,
|
||||
refundAttachments: refundAttachmentsToUrls() || undefined
|
||||
}
|
||||
refundSubmitLoading.value = true
|
||||
try {
|
||||
if (refundTargetIds.value.length === 1) {
|
||||
await fetchSubmitRefundFinance(refundTargetIds.value[0], payload)
|
||||
} else {
|
||||
await fetchSubmitRefundFinanceBatch({
|
||||
detailIds: refundTargetIds.value,
|
||||
...payload
|
||||
})
|
||||
}
|
||||
refundDialogVisible.value = false
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
} finally {
|
||||
refundSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.transferType = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.region-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.region-tab-btn {
|
||||
position: relative;
|
||||
min-width: 108px;
|
||||
padding: 0 12px;
|
||||
|
||||
:deep(.el-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.region-tab-btn.el-button > :deep(span) {
|
||||
position: static !important;
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.region-tab-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-form :deep(.el-form-item__label) {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 120px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #10b981;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 120px;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
&.is-uploading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
height: 96px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview__icon--doc {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -4,27 +4,142 @@
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>退费确认</h1>
|
||||
<p class="page-desc">仅处理「退费确认中」明细,确认后变为已退费。</p>
|
||||
<p class="page-desc">
|
||||
待确认:处理「退费确认中」明细;已确认:查看财务确认退费记录及当时填写的备注与附件。
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="region-tabs region-tab-strip">
|
||||
<div class="tab-button-group region-tab-group">
|
||||
<el-button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="region-tab-btn"
|
||||
:type="activeTab === tab.key ? 'primary' : 'default'"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
<span class="region-tab-inner">
|
||||
<span class="region-tab-name">{{ tab.label }}</span>
|
||||
</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">退费确认列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="list-body list-wrap refund-confirm-table-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id">
|
||||
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="transferAmount" label="金额" width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="payStatus" label="状态" width="120" align="center" header-align="center">
|
||||
<template #default>退费确认中</template>
|
||||
<el-table-column prop="motherName" label="客户姓名" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.motherName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="180" align="center" header-align="center" />
|
||||
<el-table-column prop="motherIdCard" label="身份证号" min-width="170" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ maskIdCard(row.motherIdCard) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="newbornName" label="新生儿姓名" min-width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.newbornName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornIdCard"
|
||||
label="新生儿身份证号"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ maskIdCard(row.newbornIdCard) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="protocolStatus" label="协议状态" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ protocolStatusLabel(String(row.protocolStatus || '')) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payMode" label="缴费方式" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ payModeLabel(String(row.payMode || '')) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="installmentNo" label="期数" width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.installmentNo ?? '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="installmentDueDate" label="应缴费时间" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentDueDate(row.installmentDueDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="dueAmount" label="应缴金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.dueAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferAmount" label="实缴金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.transferAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferType" label="转账方式" width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ transferTypeLabel(String(row.transferType || '')) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="transferTime" label="转账时间" min-width="170" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.transferTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="refundAmount" label="退费金额(元)" width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ formatPaymentAmount(row.refundAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="refundRemark"
|
||||
label="退费备注"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.refundRemark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="payStatus" label="状态" width="110" align="center" header-align="center">
|
||||
<template #default>
|
||||
{{ activeTab === 'pending' ? '退费确认中' : '已退费' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'confirmed'"
|
||||
prop="updateTime"
|
||||
label="确认时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'confirmed'"
|
||||
prop="refundFinanceRemark"
|
||||
label="财务备注"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.refundFinanceRemark || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="activeTab === 'pending'"
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="140" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="openConfirm(row)">确认退费</el-button>
|
||||
<el-button
|
||||
v-if="activeTab === 'pending'"
|
||||
class="btn-op-item primary"
|
||||
@click="openConfirm(row)"
|
||||
>
|
||||
确认退费
|
||||
</el-button>
|
||||
<el-button v-else class="btn-op-item" @click="openViewConfirm(row)">查看确认信息</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -44,15 +159,28 @@
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="确认退费"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
width="900px"
|
||||
class="dialog-form-layout refund-confirm-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<ProtocolPaymentDetailReadonly
|
||||
:detail="confirmRow"
|
||||
title="缴费明细(请核对)"
|
||||
show-refund-apply
|
||||
refund-apply-title="退费申请(请核对)"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
<div class="form-section-divider" />
|
||||
<h4 class="detail-section-title">财务确认</h4>
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
<el-form-item label="财务确认附件(MinIO 地址,逗号分隔,可选)">
|
||||
<el-input v-model="confirmAttachments" type="textarea" :rows="3" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<ProtocolFinanceConfirmFields
|
||||
v-model:finance-remark="financeRemark"
|
||||
v-model:attachments="attachments"
|
||||
upload-folder="protocol-finance-refund"
|
||||
/>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||||
@@ -65,32 +193,152 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="viewDialogVisible"
|
||||
title="确认退费信息"
|
||||
width="900px"
|
||||
class="dialog-form-layout refund-confirm-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
>
|
||||
<ProtocolPaymentDetailReadonly
|
||||
:detail="viewRow"
|
||||
title="缴费明细"
|
||||
show-refund-apply
|
||||
refund-apply-title="退费申请"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
<div class="form-section-divider" />
|
||||
<h4 class="detail-section-title">财务确认信息</h4>
|
||||
<el-form label-position="top" class="biz-dialog-form view-confirm-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="确认时间" class="form-col-span-2">
|
||||
<span class="view-field-text">{{ viewRow?.updateTime || '-' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务备注" class="form-col-span-2">
|
||||
<span class="view-field-text view-field-text--multiline">{{
|
||||
viewRow?.refundFinanceRemark || '—'
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务确认附件" class="form-col-span-2">
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="viewFinanceAttachmentFiles"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="viewDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Check } from '@element-plus/icons-vue'
|
||||
import { fetchRefundConfirmList, fetchRefundConfirmSubmit } from '@/api/collection-protocol'
|
||||
import {
|
||||
fetchRefundConfirmList,
|
||||
fetchRefundConfirmedList,
|
||||
fetchRefundConfirmSubmit
|
||||
} from '@/api/collection-protocol'
|
||||
import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
|
||||
import ProtocolPaymentDetailReadonly from '@/components/business/protocol-payment-detail-readonly/index.vue'
|
||||
import ProtocolAttachmentReadonly from '../protocol/ProtocolAttachmentReadonly.vue'
|
||||
import {
|
||||
financeAttachmentsToUrlString,
|
||||
type FinanceAttachmentItem
|
||||
} from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import {
|
||||
formatPaymentAmount,
|
||||
formatPaymentDueDate,
|
||||
payModeLabel,
|
||||
protocolStatusLabel,
|
||||
transferTypeLabel
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
|
||||
defineOptions({ name: 'CellStorageRefundConfirm' })
|
||||
|
||||
const tabs = [
|
||||
{ key: 'pending', label: '待确认' },
|
||||
{ key: 'confirmed', label: '已确认' }
|
||||
]
|
||||
|
||||
const activeTab = ref('pending')
|
||||
const tableData = ref<Record<string, unknown>[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const viewDialogVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const confirmAttachments = ref('')
|
||||
const financeRemark = ref('')
|
||||
const attachments = ref<FinanceAttachmentItem[]>([])
|
||||
const currentDetailId = ref<string | null>(null)
|
||||
const confirmRow = ref<Record<string, unknown> | null>(null)
|
||||
const viewRow = ref<Record<string, unknown> | null>(null)
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const viewFinanceAttachmentFiles = computed(() => {
|
||||
const row = viewRow.value
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.refundFinanceConfirmAttachmentFiles)) {
|
||||
return row.refundFinanceConfirmAttachmentFiles as Array<Record<string, unknown>>
|
||||
}
|
||||
const urls = String(row.refundFinanceConfirmAttachments || '')
|
||||
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 }
|
||||
})
|
||||
.filter(Boolean) as Array<Record<string, unknown>>
|
||||
})
|
||||
|
||||
function fullFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||||
}
|
||||
|
||||
function handleAttachmentPreview(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
window.open(fullFileUrl(url), '_blank')
|
||||
}
|
||||
|
||||
function handleAttachmentDownload(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(url)
|
||||
a.download = String(file.fileName || 'download')
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchRefundConfirmList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const params = { page: pagination.page, pageSize: pagination.pageSize }
|
||||
const res =
|
||||
activeTab.value === 'pending'
|
||||
? await fetchRefundConfirmList(params)
|
||||
: await fetchRefundConfirmedList(params)
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
@@ -98,18 +346,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(key: string) {
|
||||
if (activeTab.value === key) return
|
||||
activeTab.value = key
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openConfirm(row: Record<string, unknown>) {
|
||||
currentDetailId.value = String(row.id)
|
||||
confirmAttachments.value = ''
|
||||
confirmRow.value = row
|
||||
financeRemark.value = ''
|
||||
attachments.value = []
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openViewConfirm(row: Record<string, unknown>) {
|
||||
viewRow.value = row
|
||||
viewDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitConfirm() {
|
||||
if (!currentDetailId.value) return
|
||||
if (attachments.value.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await fetchRefundConfirmSubmit(currentDetailId.value, {
|
||||
financeConfirmAttachments: confirmAttachments.value || undefined
|
||||
refundFinanceConfirmAttachments: financeAttachmentsToUrlString(attachments.value) || undefined,
|
||||
refundFinanceRemark: financeRemark.value.trim() || undefined
|
||||
})
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
@@ -122,3 +389,73 @@
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.region-tabs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.region-tab-btn {
|
||||
position: relative;
|
||||
min-width: 108px;
|
||||
padding: 0 12px;
|
||||
|
||||
:deep(.el-icon) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.region-tab-btn.el-button > :deep(span) {
|
||||
position: static !important;
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.region-tab-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.refund-confirm-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.refund-confirm-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section-divider {
|
||||
height: 1px;
|
||||
margin: 20px 0;
|
||||
background: var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.view-confirm-form :deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.view-field-text {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
|
||||
&--multiline {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user