diff --git a/web-admin/src/components/business/payment-receipt-confirm-dialog/index.vue b/web-admin/src/components/business/payment-receipt-confirm-dialog/index.vue new file mode 100644 index 00000000..d0e4af15 --- /dev/null +++ b/web-admin/src/components/business/payment-receipt-confirm-dialog/index.vue @@ -0,0 +1,513 @@ + + + + + + + {{ previewRow?.motherName || '—' }} + + + {{ + previewRow ? formatPayModeSummary(previewRow) : '—' + }} + + + {{ + formatAmount(previewRow?.dueAmount) + }} + + + {{ + formatDate(String(previewRow?.installmentDueDate || '')) + }} + + + + + + + + + + + + + + + + + + + + + + 上传凭证 + + + + + + + + + + + + + + + + + + 支持拖拽或点击上传图片、文档等,多个附件将一并保存或提交,单文件不超过 20MB + + + + + + + + + 取消 + + + + 仅保存 + + + + 提交缴费确认 + + + + + + + + diff --git a/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue b/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue index 49ff4eb4..2d1cd369 100644 --- a/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue +++ b/web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue @@ -190,147 +190,14 @@ - - - - - {{ receiptPreviewRow?.motherName || '—' }} - - - {{ - receiptPreviewRow ? formatPayModeSummary(receiptPreviewRow) : '—' - }} - - - {{ - formatAmount(receiptPreviewRow?.dueAmount) - }} - - - {{ - formatDate(String(receiptPreviewRow?.installmentDueDate || '')) - }} - - - - - - - - - - - - - - - - - - - - - - 上传凭证 - - - - - - - - - - - - - - - - - - 支持拖拽或点击上传图片、文档等,多个附件将一并保存或提交,单文件不超过 20MB - - - - - - - - - 取消 - - - - 仅保存 - - - - 提交缴费确认 - - - + :preview-row="receiptPreviewRow" + :is-transfer="receiptIsTransfer" + :target-id="receiptTargetId" + @saved="loadAllColumns()" + @submitted="loadAllColumns()" + /> 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([]) @@ -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(null) const paymentDetailVisible = ref(false) const paymentDetailRow = ref | 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 | null>(null) - const receiptFormRef = ref() - 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) { + function openReceiptFinanceDialog(row: Record) { 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).id || `existing_${idx}`), - fileName: String((file as Record).fileName || ''), - fileUrl: String((file as Record).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; - } - }
+ 支持拖拽或点击上传图片、文档等,多个附件将一并保存或提交,单文件不超过 20MB +
- 支持拖拽或点击上传图片、文档等,多个附件将一并保存或提交,单文件不超过 20MB -