2026-08-19 09:14:35 +08:00
< script setup lang = "ts" >
2026-10-20 23:34:11 +08:00
import '@/styles/adminWorkbench.css'
2026-09-15 09:58:15 +08:00
import '@/styles/adminDensity.css'
import '../financeDensity.css'
2026-08-19 09:14:35 +08:00
import { computed , onMounted , ref } from 'vue'
2026-10-27 16:55:34 +08:00
import { CircleCheck , Clock3 , Download , Printer , RotateCcw , Search , Wallet } from '@lucide/vue'
2026-08-19 09:14:35 +08:00
import { ElMessage } from 'element-plus'
2026-10-30 18:50:13 +08:00
import { useRoute , useRouter } from 'vue-router'
2026-08-19 09:14:35 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { createPlatformExportTask } from '@/modules/downloads'
import { listPlatformTenants , type PlatformTenantResource } from '@/modules/iam'
import { getApiErrorDetails , getApiErrorMessage , type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import DataTable , { type DataTableColumn } from '@/shared/components/DataTable.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import { getReceivableStatistics } from '../api/finance'
import type { ReceivableStatisticsResult , ReceivableStatisticsRow } from '../types'
const now = new Date ()
const defaultMonth = ` ${ now . getFullYear () } - ${ String ( now . getMonth () + 1 ). padStart ( 2 , '0' ) } `
const access = useAccessPreviewStore ()
2026-10-30 18:50:13 +08:00
const route = useRoute ()
2026-08-19 09:14:35 +08:00
const router = useRouter ()
const month = ref ( defaultMonth )
const customerId = ref ( '' )
const customers = ref < PlatformTenantResource [] >([])
const page = ref ( 1 )
const pageSize = ref ( 20 )
const loading = ref ( false )
const exporting = ref ( false )
const error = ref < ApiErrorDetails | null >( null )
const result = ref < ReceivableStatisticsResult | null >( null )
const canView = computed (() => access . can ( 'platform:finance:receivable-statistics:list' ))
2026-10-30 18:50:13 +08:00
const canPrint = computed (() => route . name === 'finance-receivable-statistics' && canView . value && ! loading . value && ! error . value && Boolean ( result . value ))
2026-08-19 09:14:35 +08:00
const canExport = computed (() => access . can ( 'platform:finance:receivable-statistics:export' ) && access . can ( 'platform:download:list' ))
const rows = computed < ReceivableStatisticsRow [] >(() => result . value ? . records ?? [])
2026-09-02 19:42:27 +08:00
const customerNameById = computed (() => new Map ( customers . value . map (( item ) => [ String ( item . id ), item . tenantName ])))
2026-08-19 09:14:35 +08:00
const columns : DataTableColumn [] = [
2026-09-02 19:42:27 +08:00
{ key : 'customerName' , label : '应收单位' , minWidth : 240 , slotName : 'customerName' },
2026-08-19 09:14:35 +08:00
{ key : 'currency' , label : '币种' , width : 100 },
2026-10-27 16:55:34 +08:00
{ key : 'totalReceivable' , label : '应收总额(CNY)' , width : 180 , align : 'right' , slotName : 'totalReceivable' },
{ key : 'totalReceived' , label : '已收合计(CNY)' , width : 180 , align : 'right' , slotName : 'totalReceived' },
{ key : 'totalOutstanding' , label : '余额未收(CNY)' , width : 180 , align : 'right' , slotName : 'totalOutstanding' },
2026-08-19 09:14:35 +08:00
]
async function load () {
if ( ! canView . value || ! month . value ) return
loading . value = true
error . value = null
try {
result . value = await getReceivableStatistics ({ month : month . value , customerId : customerId . value || undefined , page : page . value , pageSize : pageSize . value })
} catch ( reason ) { error . value = getApiErrorDetails ( reason , '应收款统计加载失败,请重试' ) }
finally { loading . value = false }
}
async function loadCustomers () {
try {
const records : PlatformTenantResource [] = []
let current = 1 ; let pages = 1
do { const response = await listPlatformTenants ({ page : current , size : 200 , status : 'ACTIVE' }); records . push (... response . records ); pages = response . pages ; current += 1 } while ( current <= pages )
customers . value = records
} catch { /* 客户筛选失败不阻断统计,仍可查询全部客户。 */ }
}
function search () { page . value = 1 ; void load () }
function reset () { month . value = defaultMonth ; customerId . value = '' ; page . value = 1 ; void load () }
2026-10-30 18:50:13 +08:00
function printCurrent () { if ( canPrint . value ) window . print () }
2026-08-19 09:14:35 +08:00
function statisticsRowKey ( row : ReceivableStatisticsRow ) { return ` ${ row . customerId } : ${ row . customerName } ` }
2026-09-02 19:42:27 +08:00
function displayCustomerName ( value : unknown ) {
const row = value as ReceivableStatisticsRow
const masterName = customerNameById . value . get ( String ( row . customerId )) ? . trim ()
const snapshot = row . customerName ? . trim ()
if ( ! snapshot || /^客户\s*\d+$/ . test ( snapshot ) || snapshot === String ( row . customerId )) return masterName || '—'
return snapshot
}
2026-08-19 09:14:35 +08:00
async function exportCurrent () {
exporting . value = true
try {
const task = await createPlatformExportTask ({ exportType : 'FINANCE_RECEIVABLE_STATISTICS' , format : 'XLSX' , taskName : `应收款统计表- ${ month . value } ` ,
parameters : { month : month . value , ...( customerId . value ? { customerId : customerId . value } : {}) } })
2026-09-15 09:58:15 +08:00
const open = await showExportTaskCreatedDialog ({ taskName : task . taskName , taskNo : task . taskNo , dense : true })
2026-08-19 09:14:35 +08:00
if ( open ) await router . push ({ name : 'download-center' })
} catch ( reason ) { ElMessage . error ( getApiErrorMessage ( reason , '应收款统计导出任务创建失败' )) }
finally { exporting . value = false }
}
function money ( value ?: string ) { return Number ( value || 0 ). toLocaleString ( 'zh-CN' , { minimumFractionDigits : 2 , maximumFractionDigits : 2 }) }
onMounted (() => { void loadCustomers (); void load () })
</ script >
< template >
2026-10-27 16:55:34 +08:00
< AdminPage class = "receivable-statistics-page admin-density admin-workbench" spacing = "compact" >
2026-08-19 09:14:35 +08:00
< el-result v-if = "!canView" icon="warning" title="无权访问应收款统计表" sub-title="请联系管理员开通应收款统计查看权限" />
< template v-else >
2026-10-27 16:55:34 +08:00
< section class = "finance-metric-grid" aria -label =" CNY应收汇总 " v-if = "!error" v-loading="loading" :aria-busy="loading" >
< article class = "workbench-metric workbench-tone is-compact" data -tone =" blue " >
< div class = "workbench-metric-heading" >< span class = "workbench-icon is-compact" >< Wallet aria -hidden =" true " /></ span >< h2 class = "workbench-metric-label" > 应收总额 </ h2 ></ div >
< div class = "workbench-metric-value" >< strong >{{ result ? money ( result . totalReceivable ) : '—' }}</ strong >< span > CNY </ span ></ div >
</ article >
< article class = "workbench-metric workbench-tone is-compact" data -tone =" green " >
< div class = "workbench-metric-heading" >< span class = "workbench-icon is-compact" >< CircleCheck aria -hidden =" true " /></ span >< h2 class = "workbench-metric-label" > 已收合计 </ h2 ></ div >
< div class = "workbench-metric-value" >< strong >{{ result ? money ( result . totalReceived ) : '—' }}</ strong >< span > CNY </ span ></ div >
</ article >
< article class = "workbench-metric workbench-tone is-compact" data -tone =" purple " >
< div class = "workbench-metric-heading" >< span class = "workbench-icon is-compact" >< Clock3 aria -hidden =" true " /></ span >< h2 class = "workbench-metric-label" > 余额未收 </ h2 ></ div >
< div class = "workbench-metric-value" >< strong >{{ result ? money ( result . totalOutstanding ) : '—' }}</ strong >< span > CNY </ span ></ div >
</ article >
</ section >
2026-10-21 13:22:42 +08:00
< div class = "workbench-table is-page-surface" >< DataTable class = "statistics-table" appearance = "integrated" column -settings-mode =" show " :results-visible = "!error" v -model:page = "page" v -model:page-size = "pageSize" :data = "rows" :columns = "columns" :loading = "loading" : total = "result?.totalCustomers ?? 0" :row-key = "statisticsRowKey" preference -key =" finance -receivable-statistics-table " aria -label =" 应收款统计列表 " empty -text =" 当前月份暂无应收数据 " @ update :page = "load()" @ update : page -size =" page = 1 ; load ()" >
2026-09-30 14:01:58 +08:00
< template # toolbar -start >< h1 > 应收款统计表 </ h1 ></ template >
< template # filters >< section class = "query-toolbar statistics-query" aria -label =" 应收款统计筛选 " >
2026-10-27 16:55:34 +08:00
< div class = "query-field" >< el-date-picker v-model = "month" type="month" value-format="YYYY-MM" format="YYYY年MM月" :clearable="false" aria-label="统计月份" /></ div >
< div class = "query-field" >< el-select v-model = "customerId" clearable filterable placeholder="全部应收单位" aria-label="应收单位"><el-option v-for="customer in customers" :key="String(customer.id)" :label="customer.tenantName" :value="String(customer.id)" /></ el-select ></ div >
2026-08-19 09:14:35 +08:00
< div class = "query-actions" >< el-button type = "primary" :loading = "loading" @click ="search" >< Search :size = "16" /> 查询 </ el-button >< el-button @click ="reset" >< RotateCcw :size = "16" /> 重置 </ el-button ></ div >
2026-09-30 14:01:58 +08:00
</ section ></ template >
< template # notices >< LoadErrorState v-if = "error" title="应收款统计加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></ template ></ LoadErrorState ></ template >
2026-10-30 18:50:13 +08:00
< template # toolbar -actions >< div class = "report-actions" >< el-button :disabled = "!canPrint" @click ="printCurrent" >< Printer :size = "16" /> 打印 </ el-button >< el-button v-if = "canExport" type="primary" :loading="exporting" :disabled="loading || exporting" @click="exportCurrent"><Download :size="16" /> 导出Excel </ el-button ></ div ></ template >
2026-09-02 19:42:27 +08:00
< template # customerName = "{ row }" >{{ displayCustomerName ( row ) }}</ template >< template # totalReceivable = "{ row }" >{{ money ( row . totalReceivable ) }}</ template >< template # totalReceived = "{ row }" >{{ money ( row . totalReceived ) }}</ template >< template # totalOutstanding = "{ row }" >< span class = "outstanding-cell" >{{ money ( row . totalOutstanding ) }}</ span ></ template >
2026-08-19 09:14:35 +08:00
< template # pagination -left > 统计月份 : {{ result ? . statisticsMonth }} · 币种 : CNY </ template >
2026-10-20 23:34:11 +08:00
</ DataTable ></ div >
2026-08-19 09:14:35 +08:00
</ template >
</ AdminPage >
2026-10-30 18:50:13 +08:00
< Teleport to = "body" >
< section v-if = "canPrint && result" class="receivable-statistics-print" aria-label="应收款统计打印报表" >
< header >
< h1 > 应收款统计表 </ h1 >
< p > 统计月份 : {{ result . statisticsMonth }} · 币种 : CNY </ p >
</ header >
< table >
< colgroup >< col class = "print-customer-column" />< col class = "print-currency-column" />< col />< col />< col /></ colgroup >
< thead >< tr >< th scope = "col" > 应收单位 </ th >< th scope = "col" > 币种 </ th >< th scope = "col" > 应收总额 ( CNY )</ th >< th scope = "col" > 已收合计 ( CNY )</ th >< th scope = "col" > 余额未收 ( CNY )</ th ></ tr ></ thead >
< tbody >
< tr v-for = "row in rows" :key="statisticsRowKey(row)" >< td >{{ displayCustomerName ( row ) }}</ td >< td >{{ row . currency }}</ td >< td >{{ money ( row . totalReceivable ) }}</ td >< td >{{ money ( row . totalReceived ) }}</ td >< td >{{ money ( row . totalOutstanding ) }}</ td ></ tr >
< tr v-if = "!rows.length"><td colspan="5" > 当前月份暂无应收数据 </ td ></ tr >
</ tbody >
< tfoot >< tr >< th scope = "row" colspan = "2" > 统计汇总 </ th >< td >{{ money ( result . totalReceivable ) }}</ td >< td >{{ money ( result . totalReceived ) }}</ td >< td >{{ money ( result . totalOutstanding ) }}</ td ></ tr ></ tfoot >
</ table >
< p > 数据第 {{ result . page }} 页 · 共 {{ result . totalCustomers }} 家应收单位 </ p >
</ section >
</ Teleport >
2026-08-19 09:14:35 +08:00
</ template >
< style scoped >
2026-09-15 09:58:15 +08:00
. statistics - query { display : flex ; align - items : end ; gap : var ( -- admin - space - 5 ); padding : var ( -- admin - space - 6 ); }
2026-10-27 16:55:34 +08:00
. query - field { min - width : 220 px ; }
2026-08-19 09:14:35 +08:00
. query - field : deep (. el - select ), . query - field : deep (. el - date - editor ) { width : 100 % ; }
2026-09-15 09:58:15 +08:00
. query - actions , . report - actions { display : flex ; gap : var ( -- admin - space - 3 ); }
2026-08-19 09:14:35 +08:00
. report - actions { margin - left : auto ; }
. query - actions : deep (. el - button ), . report - actions : deep (. el - button ) { margin - left : 0 ; }
2026-09-15 09:58:15 +08:00
. outstanding - cell { color : var ( -- el - color - danger ); font - variant - numeric : tabular - nums ; }
2026-08-19 09:14:35 +08:00
. statistics - table { overflow : hidden ; }
2026-10-30 18:50:13 +08:00
. receivable - statistics - print { display : none ; }
2026-08-19 09:14:35 +08:00
@ media print {
2026-10-30 18:50:13 +08:00
: global ( body : has ( > . receivable - statistics - print ) > : not (. receivable - statistics - print )) { display : none ! important ; }
: global ( body : has ( > . receivable - statistics - print )) { min - width : 0 ; height : auto ; margin : 0 ; overflow : visible ; background : # fff ; }
. receivable - statistics - print { display : block ; width : 100 % ; color : # 000 ; font : 10 pt / 1.5 Arial , 'Microsoft YaHei' , sans - serif ; }
. receivable - statistics - print h1 { margin : 0 0 8 pt ; text - align : center ; font - size : 18 pt ; }
. receivable - statistics - print p { margin : 8 pt 0 ; }
. receivable - statistics - print table { width : 100 % ; border - collapse : collapse ; table - layout : fixed ; }
. receivable - statistics - print col { width : 18 % ; }
. receivable - statistics - print . print - customer - column { width : 38 % ; }
. receivable - statistics - print . print - currency - column { width : 8 % ; }
. receivable - statistics - print th , . receivable - statistics - print td { padding : 5 pt ; border : 1 px solid # 999 ; text - align : left ; overflow - wrap : anywhere ; }
. receivable - statistics - print tbody td : nth - child ( n + 3 ), . receivable - statistics - print thead th : nth - child ( n + 3 ), . receivable - statistics - print tfoot td { text - align : right ; font - variant - numeric : tabular - nums ; }
. receivable - statistics - print thead { display : table - header - group ; }
. receivable - statistics - print tfoot { display : table - row - group ; font - weight : 700 ; }
. receivable - statistics - print tr { break - inside : avoid ; }
2026-08-19 09:14:35 +08:00
}
</ style >