优化
This commit is contained in:
@@ -21,5 +21,10 @@ export const financeModule = {
|
||||
meta: { title: '客户账单', description: '查看本企业账单、收款记录和未收余额', permission: 'finance:bill:view' },
|
||||
navigation: { key: 'finance-bills', title: '客户账单', icon: ReceiptText },
|
||||
},
|
||||
{
|
||||
path: 'finance/bills/:billNo/detail', name: 'finance-bill-detail',
|
||||
component: () => import('./views/FinanceBillDetailView.vue'), release: 'active',
|
||||
meta: { title: '账单详情', workspaceTitle: '客户账单 · 详情', description: '查看费用明细、收款记录和客户确认信息', permission: 'finance:bill:view', keepAlive: false },
|
||||
},
|
||||
],
|
||||
} satisfies ClientModuleManifest
|
||||
|
||||
@@ -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>
|
||||
@@ -5,6 +5,7 @@ import ts from 'typescript'
|
||||
|
||||
const routesSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
|
||||
const viewSource = readFileSync(new URL('./FinanceBillsView.vue', import.meta.url), 'utf8')
|
||||
const detailViewSource = readFileSync(new URL('./FinanceBillDetailView.vue', import.meta.url), 'utf8')
|
||||
const apiSource = readFileSync(new URL('../api/finance.ts', import.meta.url), 'utf8')
|
||||
const adapterSource = readFileSync(new URL('../api/adapters.ts', import.meta.url), 'utf8')
|
||||
const permissionSource = readFileSync(new URL('../../tenant-management/contracts.ts', import.meta.url), 'utf8')
|
||||
@@ -22,6 +23,8 @@ const permissionContracts = await importTypeScriptModule(new URL('../../tenant-m
|
||||
|
||||
test('activates the customer finance route with its existing list and detail permission', () => {
|
||||
assert.match(routesSource, /path: 'finance\/bills'/)
|
||||
assert.match(routesSource, /path: 'finance\/bills\/:billNo\/detail'/)
|
||||
assert.match(routesSource, /name: 'finance-bill-detail'/)
|
||||
assert.match(routesSource, /FinanceBillsView\.vue/)
|
||||
assert.match(routesSource, /release: 'active'/)
|
||||
assert.match(routesSource, /permission: 'finance:bill:view'/)
|
||||
@@ -52,8 +55,10 @@ test('adapts the Finance Java DTO contract and filters nested receipt allocation
|
||||
assert.doesNotMatch(adapterSource, /bill_no|customer_name|bill_date|total_amount|outstanding_amount|receipt_no|bill_id|applied_amount/)
|
||||
assert.doesNotMatch(adapterSource, /receipt\.billId|receipt\.appliedAmount/)
|
||||
assert.doesNotMatch(adapterSource, /voucherNo|voucher_no/)
|
||||
assert.match(viewSource, /loadDetail\(row\.id\)/)
|
||||
assert.doesNotMatch(viewSource, /loadDetail\(row\.billNo\)/)
|
||||
assert.match(viewSource, /name: 'finance-bill-detail', params: \{ billNo: row\.billNo \}/)
|
||||
assert.match(detailViewSource, /getMerchantFinanceBill\(summary\.id\)/)
|
||||
assert.match(detailViewSource, /getMerchantFinanceReceipts\(summary\.id\)/)
|
||||
assert.doesNotMatch(detailViewSource, /getMerchantFinanceBill\(billNo\.value\)/)
|
||||
})
|
||||
|
||||
test('does not expose draft or voided bills returned by the server', () => {
|
||||
@@ -117,27 +122,32 @@ test('keeps actions first and exposes list, balance, detail, confirmation, recei
|
||||
const actionIndex = viewSource.indexOf('label="操作"')
|
||||
const billNumberIndex = viewSource.indexOf('label="账单编号"')
|
||||
assert.ok(actionIndex >= 0 && actionIndex < billNumberIndex)
|
||||
for (const expected of ['未收余额', '账单详情', '确认账单', '客户确认信息', '收款记录', '暂无账单', '暂无收款记录', 'state-panel--error']) {
|
||||
for (const expected of ['未收余额', '确认账单', '暂无账单', 'state-panel--error']) {
|
||||
assert.match(viewSource, new RegExp(expected))
|
||||
}
|
||||
assert.match(viewSource, /BANK_TRANSFER: '银行转账'/)
|
||||
assert.match(viewSource, /paymentMethodLabel\(row\.method\)/)
|
||||
assert.match(viewSource, /label="入仓码"/)
|
||||
assert.match(viewSource, /openInboundAssociation\(row\.inboundCode\)/)
|
||||
assert.match(viewSource, /InboundCodeAssociationDialog/)
|
||||
assert.match(viewSource, /:inbound-code="inboundAssociationCode"/)
|
||||
for (const expected of ['账单概览', '费用明细', '收款记录', '暂无收款记录']) assert.match(detailViewSource, new RegExp(expected))
|
||||
assert.match(detailViewSource, /BANK_TRANSFER: '银行转账'/)
|
||||
assert.match(detailViewSource, /paymentMethodLabel\(row\.method\)/)
|
||||
assert.match(detailViewSource, /label: '入仓码'/)
|
||||
assert.match(detailViewSource, /openInboundAssociation\(row\.inboundCode\)/)
|
||||
assert.match(detailViewSource, /InboundCodeAssociationDialog/)
|
||||
assert.match(detailViewSource, /:inbound-code="inboundAssociationCode"/)
|
||||
assert.match(viewSource, /auth\.hasPermission\('finance:bill:confirm'\)/)
|
||||
assert.match(viewSource, /row\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
|
||||
assert.match(viewSource, /size="small" type="success" plain/)
|
||||
assert.match(viewSource, /confirmStatement\(row as FinanceBillSummary\)/)
|
||||
assert.match(viewSource, /detail\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
|
||||
assert.match(viewSource, /confirmStatement\(detail\)/)
|
||||
assert.match(detailViewSource, /detail\.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm/)
|
||||
assert.match(detailViewSource, /confirmStatement\(detail\)/)
|
||||
assert.match(viewSource, /confirmMerchantFinanceStatement\(statementId, statement\.version, key\)/)
|
||||
assert.match(viewSource, /ElMessage\.success\('对账单已确认'\)/)
|
||||
assert.match(viewSource, /ElMessage\.error\(error instanceof ApiError \? error\.message : '对账单确认失败,请重试'\)/)
|
||||
for (const forbidden of ['发起争议', '新增收款', '核销', '修改金额']) {
|
||||
assert.doesNotMatch(viewSource, new RegExp(forbidden))
|
||||
assert.doesNotMatch(detailViewSource, new RegExp(forbidden))
|
||||
}
|
||||
assert.doesNotMatch(viewSource, /<el-drawer|InboundCodeAssociationDialog/)
|
||||
assert.match(detailViewSource, /class="bill-detail-surface"/)
|
||||
assert.match(detailViewSource, /bill-detail-error/)
|
||||
})
|
||||
|
||||
test('keeps the existing customer finance view permission mapping', () => {
|
||||
|
||||
@@ -2,23 +2,17 @@
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { CircleDollarSign, Eye, RefreshCcw, Search } from '@lucide/vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ApiError } from '@/api/http'
|
||||
import { useAuthStore } from '@/modules/auth/public'
|
||||
import { InboundCodeAssociationDialog } from '@/modules/first-leg/public'
|
||||
import { confirmMerchantFinanceStatement, getMerchantFinanceBill, getMerchantFinanceBills, getMerchantFinanceReceipts } from '../api/finance'
|
||||
import type { FinanceBillDetail, FinanceBillQuery, FinanceBillStatus, FinanceBillSummary, FinanceCustomerConfirmationStatus } from '../contracts'
|
||||
import { confirmMerchantFinanceStatement, getMerchantFinanceBills } from '../api/finance'
|
||||
import type { FinanceBillQuery, FinanceBillStatus, FinanceBillSummary, FinanceCustomerConfirmationStatus } from '../contracts'
|
||||
|
||||
const loading = ref(false)
|
||||
const auth = useAuthStore()
|
||||
const detailLoading = ref(false)
|
||||
const router = useRouter()
|
||||
const errorMessage = ref('')
|
||||
const detailError = ref('')
|
||||
const bills = ref<FinanceBillSummary[]>([])
|
||||
const detail = ref<FinanceBillDetail | null>(null)
|
||||
const selectedBillId = ref('')
|
||||
const detailVisible = ref(false)
|
||||
const inboundAssociationVisible = ref(false)
|
||||
const inboundAssociationCode = ref('')
|
||||
const confirmingBillId = ref('')
|
||||
const confirmationKeys = new Map<string, string>()
|
||||
const page = ref(1)
|
||||
@@ -48,15 +42,6 @@ function formatDate(value: string | null | undefined) {
|
||||
return value.slice(0, 10)
|
||||
}
|
||||
|
||||
function formatDateTime(value: string | null | undefined) {
|
||||
return value ? value.replace('T', ' ').replace(/Z$/, '').slice(0, 16) : '-'
|
||||
}
|
||||
|
||||
function paymentMethodLabel(method: string | null | undefined) {
|
||||
if (!method) return '—'
|
||||
return ({ BANK_TRANSFER: '银行转账', CASH: '现金', OTHER: '其他' })[method] ?? '其他'
|
||||
}
|
||||
|
||||
function statusLabel(status: FinanceBillStatus) {
|
||||
return ({ DRAFT: '草稿', ISSUED: '已出账', PARTIALLY_PAID: '部分收款', PAID: '已收清', VOIDED: '作废' })[status] ?? status
|
||||
}
|
||||
@@ -100,25 +85,7 @@ async function loadBills(nextPage = page.value) {
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(row: FinanceBillSummary) {
|
||||
selectedBillId.value = row.id
|
||||
await loadDetail(row.id)
|
||||
}
|
||||
|
||||
async function loadDetail(billId: string) {
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
detail.value = null
|
||||
try {
|
||||
const [bill, receipts] = await Promise.all([getMerchantFinanceBill(billId), getMerchantFinanceReceipts(billId)])
|
||||
detail.value = { ...bill, receipts }
|
||||
} catch (error) {
|
||||
detailError.value = messageOf(error)
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
function openDetail(row: FinanceBillSummary) { void router.push({ name: 'finance-bill-detail', params: { billNo: row.billNo } }) }
|
||||
|
||||
async function confirmStatement(statement: FinanceBillSummary) {
|
||||
if (!canConfirm.value || statement.customerConfirmationStatus !== 'PENDING_CONFIRMATION' || confirmingBillId.value) return
|
||||
@@ -143,9 +110,7 @@ async function confirmStatement(statement: FinanceBillSummary) {
|
||||
await confirmMerchantFinanceStatement(statementId, statement.version, key)
|
||||
confirmationKeys.delete(statementId)
|
||||
ElMessage.success('对账单已确认')
|
||||
const refreshes: Array<Promise<void>> = [loadBills(page.value)]
|
||||
if (detailVisible.value && selectedBillId.value === statementId) refreshes.push(loadDetail(statementId))
|
||||
await Promise.all(refreshes)
|
||||
await loadBills(page.value)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof ApiError ? error.message : '对账单确认失败,请重试')
|
||||
} finally {
|
||||
@@ -163,13 +128,6 @@ function handlePageChange(nextPage: number) {
|
||||
void loadBills(nextPage)
|
||||
}
|
||||
|
||||
function openInboundAssociation(inboundCode: string | null) {
|
||||
const code = inboundCode?.trim()
|
||||
if (!code) return
|
||||
inboundAssociationCode.value = code
|
||||
inboundAssociationVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => { void loadBills(1) })
|
||||
</script>
|
||||
|
||||
@@ -215,19 +173,6 @@ onMounted(() => { void loadBills(1) })
|
||||
<div v-if="!errorMessage && hasData" class="pagination-row"><span>共 {{ total }} 条</span><el-pagination v-model:current-page="page" :page-size="pageSize" layout="prev, pager, next" :total="total" @current-change="handlePageChange" /></div>
|
||||
</section>
|
||||
|
||||
<el-drawer v-model="detailVisible" title="账单详情" size="min(680px, 100%)" destroy-on-close>
|
||||
<div v-loading="detailLoading" class="detail-content">
|
||||
<div v-if="detailError" class="state-panel state-panel--error" role="alert"><p>{{ detailError }}</p><el-button @click="loadDetail(selectedBillId)">重试</el-button></div>
|
||||
<template v-else-if="detail">
|
||||
<div class="detail-heading"><div><h2>{{ detail.billNo }}</h2><p>出账 {{ formatDate(detail.issueDate) }} · 到期 {{ formatDate(detail.dueDate) }} · 未收 <strong class="amount-warning">¥ {{ formatAmount(detail.outstandingAmount) }}</strong></p></div><div class="detail-statuses"><el-tag :type="statusType(detail.status)">{{ statusLabel(detail.status) }}</el-tag><el-tag :type="confirmationType(detail.customerConfirmationStatus)">{{ confirmationLabel(detail.customerConfirmationStatus) }}</el-tag></div></div>
|
||||
<section class="confirmation-panel" aria-label="客户确认信息"><div><strong>{{ confirmationLabel(detail.customerConfirmationStatus) }}</strong><p v-if="detail.customerConfirmationStatus === 'CONFIRMED'">{{ detail.customerConfirmedByName || '当前企业成员' }} · {{ formatDateTime(detail.customerConfirmedAt) }}</p><p v-else>请核对本期金额和费用明细,确认后将记录当前登录成员与确认时间。</p></div><el-button v-if="detail.customerConfirmationStatus === 'PENDING_CONFIRMATION' && canConfirm" type="primary" :loading="confirmingBillId === detail.id" :disabled="Boolean(confirmingBillId)" @click="confirmStatement(detail)">确认账单</el-button></section>
|
||||
<section class="detail-section"><h3>费用明细</h3><el-table :data="detail.lines" size="small"><el-table-column label="入仓码" min-width="170"><template #default="{ row }"><el-button v-if="row.inboundCode" link type="primary" :aria-label="`查看入仓码 ${row.inboundCode} 详情`" @click="openInboundAssociation(row.inboundCode)">{{ row.inboundCode }}</el-button><span v-else>—</span></template></el-table-column><el-table-column prop="feeName" label="费用项目" min-width="180" /><el-table-column label="金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.amount) }}</template></el-table-column></el-table></section>
|
||||
<section class="detail-section"><h3>收款记录</h3><el-table v-if="detail.receipts.length" :data="detail.receipts" size="small"><el-table-column prop="receivedAt" label="收款日期" width="110"><template #default="{ row }">{{ formatDate(row.receivedAt) }}</template></el-table-column><el-table-column prop="receiptNo" label="收款编号" min-width="130" /><el-table-column label="本次金额" width="130" align="right"><template #default="{ row }">¥ {{ formatAmount(row.allocatedAmount) }}</template></el-table-column><el-table-column label="方式" min-width="100"><template #default="{ row }">{{ paymentMethodLabel(row.method) }}</template></el-table-column></el-table><div v-else class="sub-empty">暂无收款记录</div></section>
|
||||
<p class="readonly-notice">如对账单内容有疑问,请先线下联系平台财务,核对无误后再确认。</p>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<InboundCodeAssociationDialog v-model="inboundAssociationVisible" :inbound-code="inboundAssociationCode" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user