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:
zlwuandCursor committed 2026-10-05 13:04:24 +08:00
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>