refactor(web-admin): extract shared payment receipt confirm dialog
Reuse the same confirm form for flow kanban and per-sample views. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
32c01a4c69
commit
edaddc9da1
2 files changed
+526
-435
No files matched your search
@@ -0,0 +1,513 @@
|
||||
<!-- 缴费确认弹窗(看板 / 按样本复用) -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="缴费确认"
|
||||
width="720px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="receiptFormRef"
|
||||
:model="receiptForm"
|
||||
:rules="receiptFormRules"
|
||||
label-position="top"
|
||||
class="biz-dialog-form"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="客户姓名">
|
||||
<span class="view-field-text">{{ previewRow?.motherName || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="缴费方式">
|
||||
<span class="view-field-text">{{
|
||||
previewRow ? formatPayModeSummary(previewRow) : '—'
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="应缴金额(元)">
|
||||
<span class="view-field-text amount-readonly">{{
|
||||
formatAmount(previewRow?.dueAmount)
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="应缴费时间">
|
||||
<span class="view-field-text">{{
|
||||
formatDate(String(previewRow?.installmentDueDate || ''))
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账方式" prop="transferType">
|
||||
<el-select
|
||||
v-model="receiptForm.transferType"
|
||||
placeholder="请选择转账方式"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in transferTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账金额(元)" prop="transferAmount">
|
||||
<AmountInput
|
||||
v-model="receiptForm.transferAmount"
|
||||
:min="0"
|
||||
placeholder="请输入转账金额"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账时间" prop="transferTime" class="form-col-span-2">
|
||||
<el-date-picker
|
||||
v-model="receiptForm.transferTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择转账时间"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="receiptForm.remark" type="textarea" :rows="3" placeholder="选填" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上传凭证" class="form-col-span-2">
|
||||
<div
|
||||
class="attachment-upload"
|
||||
:class="{ 'is-dragover': isReceiptAttachDragover }"
|
||||
v-bind="receiptAttachDropHandlers"
|
||||
>
|
||||
<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="handleReceiptAttachmentChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="22" color="#047857"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传凭证</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
v-for="(file, fIdx) in receiptForm.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="removeReceiptAttachment(fIdx)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-tip">
|
||||
支持拖拽或点击上传图片、文档等,多个附件将一并保存或提交,单文件不超过 20MB
|
||||
</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="receiptSaveLoading" @click="saveReceiptDraft">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
仅保存
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="receiptSubmitLoading"
|
||||
@click="submitReceiptFinance"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
提交缴费确认
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Plus, Document, DocumentCopy, Delete, Close, Check } from '@element-plus/icons-vue'
|
||||
import AmountInput from '@/components/common/amount-input/index.vue'
|
||||
import {
|
||||
fetchUpdatePaymentDetailDraft,
|
||||
fetchSubmitReceiptFinance,
|
||||
type PaymentDetailDraftPayload
|
||||
} from '@/api/collection-protocol'
|
||||
import {
|
||||
fetchUpdatePaymentTransfer,
|
||||
fetchSubmitPaymentTransferFinance
|
||||
} from '@/api/protocol-sample-payment'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { useAttachmentDropZone } from '@/composables/useAttachmentDropZone'
|
||||
import {
|
||||
payModeLabel,
|
||||
loadTransferTypeDict,
|
||||
getTransferTypeOptions
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
defineOptions({ name: 'PaymentReceiptConfirmDialog' })
|
||||
|
||||
type AttachmentItem = {
|
||||
id: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
_uploading?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
previewRow: Record<string, unknown> | null
|
||||
isTransfer: boolean
|
||||
targetId: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', val: boolean): void
|
||||
(e: 'saved'): void
|
||||
(e: 'submitted'): void
|
||||
}>()
|
||||
|
||||
const dialogVisible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const transferTypeOptions = ref<Api.Dict.DictDataOption[]>([])
|
||||
const receiptSaveLoading = ref(false)
|
||||
const receiptSubmitLoading = ref(false)
|
||||
const receiptFormRef = ref<FormInstance>()
|
||||
const receiptForm = reactive({
|
||||
transferType: '',
|
||||
transferAmount: null as number | null,
|
||||
transferTime: '',
|
||||
remark: '',
|
||||
attachments: [] as AttachmentItem[]
|
||||
})
|
||||
const receiptFormRules: FormRules = {
|
||||
transferType: [{ required: true, message: '请选择转账方式', trigger: 'change' }],
|
||||
transferAmount: [{ required: true, message: '请填写转账金额', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function formatPayModeSummary(row: Record<string, unknown>) {
|
||||
const mode = payModeLabel(String(row.payMode || ''))
|
||||
const no = row.installmentNo
|
||||
if (no != null && no !== '') {
|
||||
return `${mode || '—'} · 第 ${no} 期`
|
||||
}
|
||||
return mode || '—'
|
||||
}
|
||||
|
||||
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): string {
|
||||
if (v == null || v === '') return '—'
|
||||
return '¥' + formatMoneyDisplay(v)
|
||||
}
|
||||
|
||||
function mapRowAttachments(raw: unknown, transferAttachments?: unknown): AttachmentItem[] {
|
||||
if (Array.isArray(raw) && raw.length) {
|
||||
return raw.map((file, idx) => ({
|
||||
id: String((file as Record<string, unknown>).id || `existing_${idx}`),
|
||||
fileName: String((file as Record<string, unknown>).fileName || ''),
|
||||
fileUrl: String((file as Record<string, unknown>).fileUrl || '')
|
||||
}))
|
||||
}
|
||||
const urls =
|
||||
typeof transferAttachments === 'string'
|
||||
? transferAttachments
|
||||
.split(',')
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
return urls.map((url, idx) => ({
|
||||
id: `existing_${idx}`,
|
||||
fileName: url.split('/').pop() || `附件${idx + 1}`,
|
||||
fileUrl: url
|
||||
}))
|
||||
}
|
||||
|
||||
function initializeFormFromPreviewRow() {
|
||||
const row = props.previewRow
|
||||
if (!row) return
|
||||
receiptForm.transferType = String(row.transferType || '')
|
||||
receiptForm.transferAmount =
|
||||
row.transferAmount != null && row.transferAmount !== '' ? Number(row.transferAmount) : null
|
||||
receiptForm.transferTime = row.transferTime ? String(row.transferTime) : ''
|
||||
receiptForm.remark = String(row.remark || '')
|
||||
receiptForm.attachments = mapRowAttachments(row.attachments, row.transferAttachments)
|
||||
receiptFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
function buildReceiptPayload(): PaymentDetailDraftPayload {
|
||||
return {
|
||||
transferType: receiptForm.transferType || undefined,
|
||||
transferAmount: receiptForm.transferAmount ?? undefined,
|
||||
transferTime: receiptForm.transferTime || null,
|
||||
transferAttachments: receiptAttachmentsToUrls(),
|
||||
remark: receiptForm.remark?.trim() || null
|
||||
}
|
||||
}
|
||||
|
||||
function receiptAttachmentsToUrls() {
|
||||
return receiptForm.attachments
|
||||
.filter((f) => f.fileUrl && !f._uploading)
|
||||
.map((f) => f.fileUrl)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
async function handleReceiptAttachmentChange(uploadFile: { raw?: File }) {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
const tempId = `temp_${Date.now()}`
|
||||
receiptForm.attachments.push({
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
fileUrl: '',
|
||||
_uploading: true
|
||||
})
|
||||
const idx = receiptForm.attachments.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'protocol-payment')
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
receiptForm.attachments[idx] = {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
_uploading: false
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
receiptForm.attachments.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function removeReceiptAttachment(index: number) {
|
||||
receiptForm.attachments.splice(index, 1)
|
||||
}
|
||||
|
||||
const { isDragover: isReceiptAttachDragover, dropZoneHandlers: receiptAttachDropHandlers } =
|
||||
useAttachmentDropZone({
|
||||
onFiles: (files) => {
|
||||
for (const file of files) {
|
||||
void handleReceiptAttachmentChange({ raw: file })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveReceiptDraft() {
|
||||
if (!props.targetId) return
|
||||
if (receiptForm.attachments.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
receiptSaveLoading.value = true
|
||||
try {
|
||||
const payload = buildReceiptPayload()
|
||||
if (props.isTransfer) {
|
||||
await fetchUpdatePaymentTransfer(props.targetId, payload)
|
||||
} else {
|
||||
await fetchUpdatePaymentDetailDraft(props.targetId, payload)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('saved')
|
||||
} finally {
|
||||
receiptSaveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitReceiptFinance() {
|
||||
if (!props.targetId) return
|
||||
const valid = await receiptFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (receiptForm.attachments.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确认提交缴费确认?提交后状态变为「缴费确认中」。',
|
||||
'提交缴费确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
receiptSubmitLoading.value = true
|
||||
try {
|
||||
const payload = buildReceiptPayload()
|
||||
if (props.isTransfer) {
|
||||
await fetchUpdatePaymentTransfer(props.targetId, payload)
|
||||
await fetchSubmitPaymentTransferFinance(props.targetId)
|
||||
} else {
|
||||
await fetchSubmitReceiptFinance(props.targetId, payload)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('submitted')
|
||||
} finally {
|
||||
receiptSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initializeFormFromPreviewRow()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTransferTypeDict()
|
||||
transferTypeOptions.value = getTransferTypeOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.view-field-text {
|
||||
display: inline-block;
|
||||
min-height: 36px;
|
||||
line-height: 36px;
|
||||
color: var(--mint-text, #334155);
|
||||
}
|
||||
|
||||
.amount-readonly {
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 120px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a8d4c4;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: #047857;
|
||||
background: #e8f4ee;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #047857;
|
||||
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: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.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: var(--mint-text-muted, #334155);
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -190,147 +190,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
<PaymentReceiptConfirmDialog
|
||||
v-model="receiptDialogVisible"
|
||||
title="缴费确认"
|
||||
width="720px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="receiptFormRef"
|
||||
:model="receiptForm"
|
||||
:rules="receiptFormRules"
|
||||
label-position="top"
|
||||
class="biz-dialog-form"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="客户姓名">
|
||||
<span class="view-field-text">{{ receiptPreviewRow?.motherName || '—' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="缴费方式">
|
||||
<span class="view-field-text">{{
|
||||
receiptPreviewRow ? formatPayModeSummary(receiptPreviewRow) : '—'
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="应缴金额(元)">
|
||||
<span class="view-field-text amount-readonly">{{
|
||||
formatAmount(receiptPreviewRow?.dueAmount)
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="应缴费时间">
|
||||
<span class="view-field-text">{{
|
||||
formatDate(String(receiptPreviewRow?.installmentDueDate || ''))
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账方式" prop="transferType">
|
||||
<el-select
|
||||
v-model="receiptForm.transferType"
|
||||
placeholder="请选择转账方式"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in transferTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
:preview-row="receiptPreviewRow"
|
||||
:is-transfer="receiptIsTransfer"
|
||||
:target-id="receiptTargetId"
|
||||
@saved="loadAllColumns()"
|
||||
@submitted="loadAllColumns()"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账金额(元)" prop="transferAmount">
|
||||
<AmountInput
|
||||
v-model="receiptForm.transferAmount"
|
||||
:min="0"
|
||||
placeholder="请输入转账金额"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账时间" prop="transferTime" class="form-col-span-2">
|
||||
<el-date-picker
|
||||
v-model="receiptForm.transferTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择转账时间"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="receiptForm.remark" type="textarea" :rows="3" placeholder="选填" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上传凭证" class="form-col-span-2">
|
||||
<div
|
||||
class="attachment-upload"
|
||||
:class="{ 'is-dragover': isReceiptAttachDragover }"
|
||||
v-bind="receiptAttachDropHandlers"
|
||||
>
|
||||
<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="handleReceiptAttachmentChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="22" color="#047857"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传凭证</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
v-for="(file, fIdx) in receiptForm.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="removeReceiptAttachment(fIdx)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="form-tip">
|
||||
支持拖拽或点击上传图片、文档等,多个附件将一并保存或提交,单文件不超过 20MB
|
||||
</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="receiptDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="receiptSaveLoading" @click="saveReceiptDraft">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
仅保存
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="receiptSubmitLoading"
|
||||
@click="submitReceiptFinance"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
提交缴费确认
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="paymentDetailVisible"
|
||||
@@ -370,15 +237,11 @@
|
||||
<script setup lang="ts">
|
||||
import { formatDisplayOrDash } from '@/utils/datetime-display'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View,
|
||||
Plus,
|
||||
Document,
|
||||
DocumentCopy,
|
||||
Delete,
|
||||
Close,
|
||||
Check,
|
||||
ArrowDown,
|
||||
@@ -386,21 +249,10 @@
|
||||
} from '@element-plus/icons-vue'
|
||||
import ProtocolViewDialog from '../protocol/ProtocolViewDialog.vue'
|
||||
import ClinicalPaymentConfirmTimeline from '@/components/business/clinical-payment-confirm-timeline/index.vue'
|
||||
import PaymentReceiptConfirmDialog from '@/components/business/payment-receipt-confirm-dialog/index.vue'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import {
|
||||
fetchPaymentDetailList,
|
||||
fetchUpdatePaymentDetailDraft,
|
||||
fetchSubmitReceiptFinance,
|
||||
type CollectionProtocolRow,
|
||||
type PaymentDetailDraftPayload
|
||||
} from '@/api/collection-protocol'
|
||||
import {
|
||||
fetchPaymentTransferList,
|
||||
fetchUpdatePaymentTransfer,
|
||||
fetchSubmitPaymentTransferFinance
|
||||
} from '@/api/protocol-sample-payment'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { useAttachmentDropZone } from '@/composables/useAttachmentDropZone'
|
||||
import { fetchPaymentDetailList, type CollectionProtocolRow } from '@/api/collection-protocol'
|
||||
import { fetchPaymentTransferList } from '@/api/protocol-sample-payment'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import {
|
||||
isPaymentDueWithinWarnWindow,
|
||||
@@ -411,7 +263,6 @@
|
||||
getTransferTypeOptions,
|
||||
transferTypeLabel
|
||||
} from '@/utils/protocol-payment-labels'
|
||||
import AmountInput from '@/components/common/amount-input/index.vue'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const transferTypeOptions = ref<Api.Dict.DictDataOption[]>([])
|
||||
@@ -435,9 +286,9 @@
|
||||
|
||||
/** P0:看板仅保留收款三列;退费列下线(旧 detail 退费态仍可由财务旧接口处理) */
|
||||
const kanbanColumns: KanbanColumnDef[] = [
|
||||
{ key: 'dfj', label: '待缴费' },
|
||||
{ key: 'dfj', label: '已缴费' },
|
||||
{ key: 'jfqrz', label: '缴费确认中' },
|
||||
{ key: 'yjf', label: '已缴费' }
|
||||
{ key: 'yjf', label: '缴费成功' }
|
||||
]
|
||||
|
||||
function createColumnState(): KanbanColumnState {
|
||||
@@ -457,31 +308,11 @@
|
||||
const viewRow = ref<CollectionProtocolRow | null>(null)
|
||||
const paymentDetailVisible = ref(false)
|
||||
const paymentDetailRow = ref<Record<string, unknown> | null>(null)
|
||||
type AttachmentItem = {
|
||||
id: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
_uploading?: boolean
|
||||
}
|
||||
|
||||
const receiptDialogVisible = ref(false)
|
||||
const receiptSaveLoading = ref(false)
|
||||
const receiptSubmitLoading = ref(false)
|
||||
const receiptTargetId = ref('')
|
||||
const receiptIsTransfer = ref(false)
|
||||
const receiptPreviewRow = ref<Record<string, unknown> | null>(null)
|
||||
const receiptFormRef = ref<FormInstance>()
|
||||
const receiptForm = reactive({
|
||||
transferType: '',
|
||||
transferAmount: null as number | null,
|
||||
transferTime: '',
|
||||
remark: '',
|
||||
attachments: [] as AttachmentItem[]
|
||||
})
|
||||
const receiptFormRules: FormRules = {
|
||||
transferType: [{ required: true, message: '请选择转账方式', trigger: 'change' }],
|
||||
transferAmount: [{ required: true, message: '请填写转账金额', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
let loadRequestId = 0
|
||||
|
||||
@@ -631,170 +462,21 @@
|
||||
loadColumn(key)
|
||||
}
|
||||
|
||||
async function openReceiptFinanceDialog(row: Record<string, unknown>) {
|
||||
function openReceiptFinanceDialog(row: Record<string, unknown>) {
|
||||
const id = String(row.id || '')
|
||||
if (!id) return
|
||||
receiptTargetId.value = id
|
||||
receiptIsTransfer.value = isTransferRow(row)
|
||||
receiptPreviewRow.value = row
|
||||
receiptForm.transferType = String(row.transferType || '')
|
||||
receiptForm.transferAmount =
|
||||
row.transferAmount != null && row.transferAmount !== '' ? Number(row.transferAmount) : null
|
||||
receiptForm.transferTime = row.transferTime ? String(row.transferTime) : ''
|
||||
receiptForm.remark = String(row.remark || '')
|
||||
receiptForm.attachments = mapRowAttachments(row.attachments, row.transferAttachments)
|
||||
receiptFormRef.value?.clearValidate()
|
||||
receiptDialogVisible.value = true
|
||||
}
|
||||
|
||||
function mapRowAttachments(raw: unknown, transferAttachments?: unknown): AttachmentItem[] {
|
||||
if (Array.isArray(raw) && raw.length) {
|
||||
return raw.map((file, idx) => ({
|
||||
id: String((file as Record<string, unknown>).id || `existing_${idx}`),
|
||||
fileName: String((file as Record<string, unknown>).fileName || ''),
|
||||
fileUrl: String((file as Record<string, unknown>).fileUrl || '')
|
||||
}))
|
||||
}
|
||||
const urls =
|
||||
typeof transferAttachments === 'string'
|
||||
? transferAttachments
|
||||
.split(',')
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
return urls.map((url, idx) => ({
|
||||
id: `existing_${idx}`,
|
||||
fileName: url.split('/').pop() || `附件${idx + 1}`,
|
||||
fileUrl: url
|
||||
}))
|
||||
}
|
||||
|
||||
function buildReceiptPayload(): PaymentDetailDraftPayload {
|
||||
return {
|
||||
transferType: receiptForm.transferType || undefined,
|
||||
transferAmount: receiptForm.transferAmount ?? undefined,
|
||||
transferTime: receiptForm.transferTime || null,
|
||||
transferAttachments: receiptAttachmentsToUrls(),
|
||||
remark: receiptForm.remark?.trim() || null
|
||||
}
|
||||
}
|
||||
|
||||
function receiptAttachmentsToUrls() {
|
||||
return receiptForm.attachments
|
||||
.filter((f) => f.fileUrl && !f._uploading)
|
||||
.map((f) => f.fileUrl)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
async function handleReceiptAttachmentChange(uploadFile: { raw?: File }) {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
const tempId = `temp_${Date.now()}`
|
||||
receiptForm.attachments.push({
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
fileUrl: '',
|
||||
_uploading: true
|
||||
})
|
||||
const idx = receiptForm.attachments.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', 'protocol-payment')
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
receiptForm.attachments[idx] = {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
_uploading: false
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
receiptForm.attachments.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function removeReceiptAttachment(index: number) {
|
||||
receiptForm.attachments.splice(index, 1)
|
||||
}
|
||||
|
||||
const { isDragover: isReceiptAttachDragover, dropZoneHandlers: receiptAttachDropHandlers } =
|
||||
useAttachmentDropZone({
|
||||
onFiles: (files) => {
|
||||
for (const file of files) {
|
||||
void handleReceiptAttachmentChange({ raw: file })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function saveReceiptDraft() {
|
||||
if (!receiptTargetId.value) return
|
||||
if (receiptForm.attachments.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
receiptSaveLoading.value = true
|
||||
try {
|
||||
const payload = buildReceiptPayload()
|
||||
if (receiptIsTransfer.value) {
|
||||
await fetchUpdatePaymentTransfer(receiptTargetId.value, payload)
|
||||
} else {
|
||||
await fetchUpdatePaymentDetailDraft(receiptTargetId.value, payload)
|
||||
}
|
||||
receiptDialogVisible.value = false
|
||||
loadAllColumns()
|
||||
} finally {
|
||||
receiptSaveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitReceiptFinance() {
|
||||
if (!receiptTargetId.value) return
|
||||
const valid = await receiptFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (receiptForm.attachments.some((f) => f._uploading)) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确认提交缴费确认?提交后状态变为「缴费确认中」。',
|
||||
'提交缴费确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
receiptSubmitLoading.value = true
|
||||
try {
|
||||
const payload = buildReceiptPayload()
|
||||
if (receiptIsTransfer.value) {
|
||||
await fetchUpdatePaymentTransfer(receiptTargetId.value, payload)
|
||||
await fetchSubmitPaymentTransferFinance(receiptTargetId.value)
|
||||
} else {
|
||||
await fetchSubmitReceiptFinance(receiptTargetId.value, payload)
|
||||
}
|
||||
receiptDialogVisible.value = false
|
||||
loadAllColumns()
|
||||
} finally {
|
||||
receiptSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function fullFileUrl(url: string) {
|
||||
if (!url) return ''
|
||||
if (/^https?:\/\//i.test(url)) return url
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||||
}
|
||||
|
||||
function isImageUrl(url: string) {
|
||||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url)
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadAllColumns(true)
|
||||
}
|
||||
@@ -843,108 +525,4 @@
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.view-field-text {
|
||||
display: inline-block;
|
||||
min-height: 36px;
|
||||
line-height: 36px;
|
||||
color: var(--mint-text, #334155);
|
||||
}
|
||||
|
||||
.amount-readonly {
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 120px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a8d4c4;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: #047857;
|
||||
background: #e8f4ee;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #047857;
|
||||
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: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.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: var(--mint-text-muted, #334155);
|
||||
|
||||
&:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user