2026-08-13 11:57:44 +08:00
< 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 : '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' },
{ key : 'status' , label : '状态' , width : 110 , slotName : 'status' },
]
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 >
2026-08-18 11:33:01 +08:00
< 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 >
2026-08-13 11:57:44 +08:00
< 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 : 16 px ; margin - bottom : 18 px ; padding - bottom : 14 px ; border - bottom : 1 px solid var ( -- cc - border ); }
. panel - title h2 { margin : 0 ; color : # 344054 ; font - size : 16 px ; }. panel - title p { margin : 5 px 0 0 ; color : var ( -- cc - muted ); font - size : 11 px ; }. panel - title > svg { color : var ( -- cc - primary ); }
. module - empty { display : flex ; min - height : 260 px ; align - items : center ; justify - content : center ; flex - direction : column ; gap : 7 px ; color : # 98 a2b3 ; text - align : center ; }. module - empty strong { color : # 475467 ; font - size : 13 px ; }. module - empty p { max - width : 520 px ; margin : 0 ; color : var ( -- cc - muted ); font - size : 12 px ; line - height : 1.7 ; }
: deep (. el - descriptions ) { margin - bottom : 18 px ; } : deep (. el - table + . el - table ) { margin - top : 18 px ; }
</ style >