Files
admin-web/src/modules/finance/components/CustomerStatementsPanel.vue
T

102 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Eye, FileText } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { getReconciliationStatement, listFinanceStatements } from '../api/finance'
import { financeStatusLabel, financeStatusTone } from '../status'
import type { FinanceReconciliationDetail, FinanceStatementReadModel } from '../types'
const props = defineProps<{ customerId: string; customerName: string }>()
const access = useAccessPreviewStore()
const rows = ref<FinanceStatementReadModel[]>([])
const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const detailOpen = ref(false)
const detailLoading = ref(false)
const detail = ref<FinanceReconciliationDetail | null>(null)
const detailError = ref<ApiErrorDetails | null>(null)
let requestSerial = 0
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' },
{ key: 'billNo', prop: 'billNo', label: '对账单号', minWidth: 180 },
{ key: 'status', label: '状态', width: 110, slotName: 'status', afterColumn: 'billNo' },
{ key: 'billDate', prop: 'billDate', label: '账单日期', width: 120 },
{ key: 'dueDate', prop: 'dueDate', label: '到期日', width: 120 },
{ key: 'totalAmount', label: '应收(CNY)', width: 130, align: 'right', slotName: 'totalAmount' },
{ key: 'receivedAmount', label: '已收(CNY)', width: 130, align: 'right', slotName: 'receivedAmount' },
{ key: 'outstandingAmount', label: '未收(CNY)', width: 130, align: 'right', slotName: 'outstandingAmount' },
]
async function load() {
const customerId = props.customerId.trim()
const serial = ++requestSerial
rows.value = []
loadError.value = null
if (!customerId || !access.can('platform:finance:customer-statement:list')) return
loading.value = true
try {
const result = await listFinanceStatements(customerId)
if (serial === requestSerial && customerId === props.customerId.trim()) rows.value = result
} catch (error) {
if (serial === requestSerial) loadError.value = getApiErrorDetails(error, '客户财务账单加载失败,请稍后重试')
} finally {
if (serial === requestSerial) loading.value = false
}
}
async function openDetail(row: FinanceStatementReadModel) {
detailOpen.value = true
detailLoading.value = true
detail.value = null
detailError.value = null
try { detail.value = await getReconciliationStatement(row.id) }
catch (error) { detailError.value = getApiErrorDetails(error, '对账单详情加载失败,请稍后重试') }
finally { detailLoading.value = false }
}
function formatAmount(value: number) {
return value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
watch(() => props.customerId, () => { void load() }, { immediate: true })
</script>
<template>
<section class="customer-statements-panel">
<div class="panel-title"><div><h2>财务账单</h2><p>{{ customerName }}的客户对账单</p></div><FileText :size="19" /></div>
<div v-if="!access.can('platform:finance:customer-statement:list')" class="module-empty"><FileText :size="30" /><strong>暂无财务账单查看权限</strong><p>请联系管理员开通客户对账单查看权限。</p></div>
<LoadErrorState v-else-if="loadError" title="客户财务账单加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
<DataTable v-else :data="rows" :columns="columns" :loading="loading" row-key="id" aria-label="客户财务账单列表" empty-text="当前客户暂无财务账单" border stripe>
<template #actions="{ row }"><el-button size="small" plain type="primary" :disabled="loading" :aria-label="`查看对账单${(row as FinanceStatementReadModel).billNo}详情`" @click="openDetail(row as FinanceStatementReadModel)"><Eye :size="15" />详情</el-button></template>
<template #totalAmount="{ row }">{{ formatAmount((row as FinanceStatementReadModel).totalAmount) }}</template>
<template #receivedAmount="{ row }">{{ formatAmount((row as FinanceStatementReadModel).receivedAmount) }}</template>
<template #outstandingAmount="{ row }">{{ formatAmount((row as FinanceStatementReadModel).outstandingAmount) }}</template>
<template #status="{ row }"><el-tag :type="financeStatusTone((row as FinanceStatementReadModel).status)">{{ financeStatusLabel((row as FinanceStatementReadModel).status) }}</el-tag></template>
<template #pagination-left>币种:CNY</template>
</DataTable>
<AppDrawer v-model="detailOpen" title="客户对账单详情" size="min(820px, 94vw)" :loading="detailLoading">
<LoadErrorState v-if="detailError" title="详情加载失败" :message="detailError.message" :loading="detailLoading"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
<template v-else-if="detail">
<el-descriptions :column="2" border><el-descriptions-item label="对账单号">{{ detail.billNo }}</el-descriptions-item><el-descriptions-item label="客户">{{ detail.customerName }}</el-descriptions-item><el-descriptions-item label="状态">{{ financeStatusLabel(detail.status) }}</el-descriptions-item><el-descriptions-item label="金额">{{ detail.totalAmount }} CNY</el-descriptions-item><el-descriptions-item label="账期">{{ detail.periodStart }} 至 {{ detail.periodEnd }}</el-descriptions-item><el-descriptions-item label="未收金额">{{ detail.outstandingAmount }} CNY</el-descriptions-item></el-descriptions>
<el-table :data="detail.lines" size="small" border stripe><el-table-column prop="feeItemName" label="费用项目" /><el-table-column prop="amount" label="账单金额(CNY)" /></el-table>
<el-table v-if="detail.revisions.length" :data="detail.revisions" size="small" border stripe><el-table-column prop="revisionNo" label="修订" width="72" /><el-table-column prop="action" label="动作" width="130" /><el-table-column prop="beforeAmount" label="调整前" /><el-table-column prop="afterAmount" label="调整后" /><el-table-column prop="reason" label="原因" /></el-table>
</template>
</AppDrawer>
</section>
</template>
<style scoped>
.customer-statements-panel { min-width:0; }
.panel-title { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; margin-bottom:18px; padding-bottom:14px; border-bottom:1px solid var(--cc-border); }
.panel-title h2 { margin:0; color:#344054; font-size:16px; }.panel-title p { margin:5px 0 0; color:var(--cc-muted); font-size:11px; }.panel-title > svg { color:var(--cc-primary); }
.module-empty { display:flex; min-height:260px; align-items:center; justify-content:center; flex-direction:column; gap:7px; color:#98a2b3; text-align:center; }.module-empty strong { color:#475467; font-size:13px; }.module-empty p { max-width:520px; margin:0; color:var(--cc-muted); font-size:12px; line-height:1.7; }
:deep(.el-descriptions) { margin-bottom:18px; }:deep(.el-table + .el-table) { margin-top:18px; }
</style>