优化
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ApiError } from '@/api/http'
|
||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { InboundCodeAssociationDialog } from '@/modules/first-leg/public'
|
||||
import { confirmMerchantFinanceStatement, getMerchantFinanceBill, getMerchantFinanceBills, getMerchantFinanceReceipts } from '../api/finance'
|
||||
import type { FinanceBillDetail, FinanceBillSummary, FinanceBillStatus, FinanceCustomerConfirmationStatus } from '../contracts'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const billNo = computed(() => String(route.params.billNo || ''))
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detail = ref<FinanceBillDetail | null>(null)
|
||||
const confirming = ref(false)
|
||||
const inboundAssociationVisible = ref(false)
|
||||
const inboundAssociationCode = ref('')
|
||||
const canConfirm = computed(() => auth.hasPermission('finance:bill:confirm'))
|
||||
const lineColumns: DataTableColumn[] = [
|
||||
{ key: 'inboundCode', label: '入仓码', slotName: 'inboundCode', minWidth: 180 },
|
||||
{ prop: 'feeName', label: '费用项目', minWidth: 220 },
|
||||
{ key: 'amount', label: '金额', slotName: 'amount', width: 150, align: 'right', headerAlign: 'right' },
|
||||
]
|
||||
const receiptColumns: DataTableColumn[] = [
|
||||
{ key: 'receivedAt', label: '收款日期', slotName: 'receivedAt', width: 130 },
|
||||
{ prop: 'receiptNo', label: '收款编号', minWidth: 180 },
|
||||
{ key: 'allocatedAmount', label: '本次金额', slotName: 'allocatedAmount', width: 150, align: 'right', headerAlign: 'right' },
|
||||
{ key: 'method', label: '方式', slotName: 'method', minWidth: 120 },
|
||||
]
|
||||
|
||||
function formatAmount(value?: string | null) { const number = Number(value); return Number.isFinite(number) ? number.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : value || '0.00' }
|
||||
function formatDate(value?: string | null) { return value ? value.slice(0, 10) : '—' }
|
||||
function formatDateTime(value?: string | null) { return value ? value.replace('T', ' ').replace(/Z$/, '').slice(0, 16) : '—' }
|
||||
function statusLabel(status: FinanceBillStatus) { return ({ DRAFT: '草稿', ISSUED: '已出账', PARTIALLY_PAID: '部分收款', PAID: '已收清', VOIDED: '作废' } as Record<string, string>)[status] || status }
|
||||
function statusType(status: FinanceBillStatus) { return ({ ISSUED: 'info', PARTIALLY_PAID: 'warning', PAID: 'success', VOIDED: 'danger', DRAFT: 'info' } as const)[status] }
|
||||
function confirmationLabel(status: FinanceCustomerConfirmationStatus) { return ({ NOT_REQUIRED: '无需确认', PENDING_CONFIRMATION: '待确认', CONFIRMED: '已确认' })[status] }
|
||||
function paymentMethodLabel(method?: string | null) { return ({ BANK_TRANSFER: '银行转账', CASH: '现金', OTHER: '其他' } as Record<string, string>)[method || ''] || '其他' }
|
||||
function messageOf(error: unknown) { return error instanceof ApiError ? error.message : '账单详情加载失败,请稍后重试' }
|
||||
|
||||
async function load() {
|
||||
if (!billNo.value) return
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
try {
|
||||
const result = await getMerchantFinanceBills({ keyword: billNo.value, page: 1, size: 100 })
|
||||
const summary = result.records.find(item => item.billNo === billNo.value)
|
||||
if (!summary) throw new Error('未找到该账单')
|
||||
const [bill, receipts] = await Promise.all([getMerchantFinanceBill(summary.id), getMerchantFinanceReceipts(summary.id)])
|
||||
detail.value = { ...bill, receipts }
|
||||
} catch (error) { detail.value = null; errorMessage.value = error instanceof Error ? error.message : messageOf(error) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
async function confirmStatement(statement: FinanceBillSummary) {
|
||||
if (!canConfirm.value || statement.customerConfirmationStatus !== 'PENDING_CONFIRMATION' || confirming.value) return
|
||||
try { await ElMessageBox.confirm(`确认账单 ${statement.billNo}(¥ ${formatAmount(statement.totalAmount)})金额与费用明细无误?`, '确认客户账单', { type: 'warning', confirmButtonText: '确认无误', cancelButtonText: '取消' }) } catch { return }
|
||||
confirming.value = true
|
||||
try { await confirmMerchantFinanceStatement(statement.id, statement.version); ElMessage.success('账单已确认'); await load() }
|
||||
catch (error) { ElMessage.error(messageOf(error)) }
|
||||
finally { confirming.value = false }
|
||||
}
|
||||
function openInboundAssociation(value?: string | null) { if (!value) return; inboundAssociationCode.value = value; inboundAssociationVisible.value = true }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article v-loading="loading" class="bill-detail-surface">
|
||||
<template v-if="detail">
|
||||
<header class="bill-detail-header"><button type="button" class="bill-detail-back" @click="router.push({ name: 'finance-bills' })"><ArrowLeft :size="16" />返回列表</button><div class="bill-detail-title"><strong>{{ detail.billNo }}</strong><span>出账 {{ formatDate(detail.issueDate) }} · 到期 {{ formatDate(detail.dueDate) }}</span></div><div class="bill-detail-actions"><el-tag :type="statusType(detail.status)" effect="light">{{ statusLabel(detail.status) }}</el-tag><el-tag effect="light">{{ confirmationLabel(detail.customerConfirmationStatus) }}</el-tag><el-button v-if="detail.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm" type="primary" :loading="confirming" @click="confirmStatement(detail)">确认账单</el-button></div></header>
|
||||
<section class="bill-detail-section"><h3>账单概览</h3><dl class="bill-detail-grid"><div><dt>账单金额</dt><dd>¥ {{ formatAmount(detail.totalAmount) }}</dd></div><div><dt>已收金额</dt><dd>¥ {{ formatAmount(detail.receivedAmount) }}</dd></div><div><dt>未收余额</dt><dd class="amount-warning">¥ {{ formatAmount(detail.outstandingAmount) }}</dd></div><div><dt>客户确认</dt><dd>{{ confirmationLabel(detail.customerConfirmationStatus) }}</dd></div><div><dt>确认成员</dt><dd>{{ detail.customerConfirmedByName || '—' }}</dd></div><div><dt>确认时间</dt><dd>{{ formatDateTime(detail.customerConfirmedAt) }}</dd></div></dl></section>
|
||||
<section class="bill-detail-section"><h3>费用明细</h3><DataTable :data="detail.lines" :columns="lineColumns" row-key="id" empty-text="暂无费用明细"><template #inboundCode="{ row }"><el-button v-if="row.inboundCode" link type="primary" @click="openInboundAssociation(row.inboundCode)">{{ row.inboundCode }}</el-button><span v-else>—</span></template><template #amount="{ row }">¥ {{ formatAmount(row.amount) }}</template></DataTable></section>
|
||||
<section class="bill-detail-section"><h3>收款记录</h3><DataTable :data="detail.receipts" :columns="receiptColumns" row-key="id" empty-text="暂无收款记录"><template #receivedAt="{ row }">{{ formatDate(row.receivedAt) }}</template><template #allocatedAmount="{ row }">¥ {{ formatAmount(row.allocatedAmount) }}</template><template #method="{ row }">{{ paymentMethodLabel(row.method) }}</template></DataTable></section>
|
||||
<p class="bill-detail-notice">如对账单内容有疑问,请先联系平台财务,核对无误后再确认。</p>
|
||||
</template>
|
||||
<div v-else-if="errorMessage" class="bill-detail-error" role="alert"><strong>{{ errorMessage }}</strong><el-button @click="load">重新加载</el-button></div>
|
||||
</article>
|
||||
<InboundCodeAssociationDialog v-model="inboundAssociationVisible" :inbound-code="inboundAssociationCode" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bill-detail-surface{overflow:hidden;min-height:240px;border:1px solid var(--client-border);border-radius:var(--client-radius);background:#fff}.bill-detail-header{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:14px;min-height:64px;padding:10px 16px;border-bottom:1px solid var(--client-border)}.bill-detail-back{display:inline-flex;align-items:center;gap:5px;padding:5px 0;border:0;background:transparent;color:var(--el-color-primary);cursor:pointer}.bill-detail-title{display:flex;min-width:0;flex-direction:column;gap:3px}.bill-detail-title strong{overflow:hidden;color:#1f2d45;font-size:18px;text-overflow:ellipsis;white-space:nowrap}.bill-detail-title span{color:var(--client-muted);font-size:12px}.bill-detail-actions{display:flex;align-items:center;gap:8px}.bill-detail-section+.bill-detail-section{border-top:1px solid var(--client-border)}.bill-detail-section>h3{display:flex;min-height:40px;align-items:center;margin:0;padding:0 16px;border-bottom:1px solid var(--client-border);background:var(--client-header-bg);font-size:14px}.bill-detail-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin:0}.bill-detail-grid div{display:grid;min-height:42px;grid-template-columns:88px minmax(0,1fr);align-items:center;gap:8px;padding:5px 12px;border-right:1px solid var(--client-border);border-bottom:1px solid var(--client-border)}.bill-detail-grid dt{color:var(--client-muted);font-size:12px}.bill-detail-grid dd{margin:0;overflow:hidden;color:var(--client-text);font-size:13px;font-weight:600;text-overflow:ellipsis;white-space:nowrap}.amount-warning{color:#b45309!important}.bill-detail-notice{margin:0;padding:10px 14px;border-top:1px solid var(--client-border);background:#f5faf8;color:#45615d;font-size:12px}.bill-detail-error{display:flex;min-height:220px;align-items:center;justify-content:center;gap:12px;color:#b42318}@media(max-width:1100px){.bill-detail-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:720px){.bill-detail-header{grid-template-columns:1fr auto}.bill-detail-back{grid-column:1/-1}.bill-detail-grid{grid-template-columns:1fr}}
|
||||
</style>
|
||||
Reference in New Issue
Block a user