This commit is contained in:
2026-08-17 17:49:10 +08:00
parent 911a3620e1
commit c7334b2cf8
30 changed files with 1043 additions and 249 deletions
@@ -4,8 +4,9 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Download, Eye, MoreHorizontal, Plus, RotateCcw, Send, ShieldCheck, XCircle } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { createPlatformExportTask } from '@/modules/downloads'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
@@ -15,6 +16,7 @@ import FormSection from '@/shared/components/FormSection.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import { actionReconciliationStatement, createReconciliationStatement, getFinanceFeeSource, getReconciliationStatement, listFinanceStatements } from '../api/finance'
import { financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from '../status'
import type { FinanceReconciliationDetail, FinanceStatementReadModel, ReconciliationActionPayload } from '../types'
@@ -90,6 +92,7 @@ const canReview = computed(() => access.can('platform:finance:reconciliation:rev
const canIssue = computed(() => access.can('platform:finance:reconciliation:issue'))
const canAdjust = computed(() => access.can('platform:finance:reconciliation:adjust'))
const canVoid = computed(() => access.can('platform:finance:reconciliation:void'))
const canExport = computed(() => access.can('platform:finance:customer-statement:export') && access.can('platform:download:list'))
const canGenerate = computed(() => canCreate.value && canListCustomers.value)
const detailRouteId = computed(() => route.name === 'finance-customer-statement-detail' ? String(route.params.statementId || '') : '')
const isDetailRoute = computed(() => Boolean(detailRouteId.value))
@@ -318,31 +321,46 @@ function revisionActionLabel(action: string) {
return ({ SUBMIT: '提交审核', APPROVE: '审核通过', REJECT: '审核驳回', ISSUE: '出账', ADJUST: '调整', VOID: '作废', REOPEN: '重开' } as Record<string, string>)[action] || action || '—'
}
function csvCell(value: unknown) { return `"${String(value ?? '').replaceAll('"', '""')}"` }
async function exportStatements() {
const customerId = String(filters.value.customerId || '').trim()
if (customerId && !/^[1-9]\d*$/.test(customerId)) return ElMessage.error('客户业务标识无效,无法创建导出任务')
const parameters: Record<string, string | boolean> = {}
const putText = (key: string) => {
const value = String(filters.value[key] || '').trim()
if (value) parameters[key] = value
}
const putRange = (sourceKey: string, fromKey: string, toKey: string) => {
const value = filters.value[sourceKey]
if (!Array.isArray(value)) return
if (value[0]) parameters[fromKey] = String(value[0])
if (value[1]) parameters[toKey] = String(value[1])
}
if (customerId) parameters.customerId = customerId
putText('status')
putText('billNo')
putText('containerNo')
putRange('periodStartRange', 'periodStartFrom', 'periodStartTo')
putRange('periodEndRange', 'periodEndFrom', 'periodEndTo')
putRange('lastPaymentRange', 'lastPaymentFrom', 'lastPaymentTo')
putRange('createdRange', 'createdFrom', 'createdTo')
const settled = String(filters.value.settled || '')
if (settled) parameters.settled = settled === 'true'
exportBusy.value = true
try {
const hydrated = await Promise.all(rows.value.map(async (row) => {
const current = await fetchDetail(row.id)
return detailToRow(row, current)
}))
rows.value = hydrated
const task = await createPlatformExportTask({
exportType: 'FINANCE_RECONCILIATION_STATEMENT_LIST',
format: 'XLSX',
taskName: '客户对账单',
parameters,
})
const openDownloadCenter = await showExportTaskCreatedDialog({ taskName: task.taskName, taskNo: task.taskNo })
if (openDownloadCenter) await router.push({ name: 'download-center' })
} catch (cause) {
error.value = getApiErrorDetails(cause, '导出前加载完整对账单字段失败')
ElMessage.error(getApiErrorMessage(cause, '客户对账单导出任务创建失败'))
} finally {
exportBusy.value = false
return
}
const header = ['审核状态', '对账单号', '客户', '结算起始日', '结算结束日', '账期(天)', '货币', '总应收金额', '已收金额', '未收金额', '最后收款时间', '是否结清', '备注', '创建人', '创建时间', '更新人', '更新时间']
const body = filteredRows.value.map((row) => [financeStatusLabel(row.status), row.billNo, customerDisplayName(row.customerId, row.customerName), row.periodStart, row.periodEnd, row.creditDays ?? '', row.currency, row.totalAmount, row.receivedAmount, row.outstandingAmount, formatDateTime(row.lastPaymentAt), row.settled === null ? '' : (row.settled ? '是' : '否'), row.remark, operatorDisplayName(row.createdByName), formatDateTime(row.createdAt), operatorDisplayName(row.updatedByName), formatDateTime(row.updatedAt)])
const blob = new Blob([`\uFEFF${[header, ...body].map((line) => line.map(csvCell).join(',')).join('\r\n')}`], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = `客户对账单-${new Date().toISOString().slice(0, 10)}.csv`
anchor.click()
URL.revokeObjectURL(url)
exportBusy.value = false
ElMessage.success(`已导出 ${body.length} 条客户对账单`)
}
function openCreate() {
@@ -495,7 +513,7 @@ watch(detailRouteId, (statementId) => {
<section class="surface-panel table-panel">
<DataTable :data="filteredRows" :columns="columns" :loading="loading" aria-label="客户对账单列表" row-key="id" empty-text="暂无客户对账单" preference-key="finance-customer-statements" @row-click="openDetail">
<template #toolbar-actions>
<el-button :loading="exportBusy" :disabled="loading || exportBusy || !filteredRows.length" @click="exportStatements"><Download :size="15" />导出</el-button>
<el-button v-if="canExport" :loading="exportBusy" :disabled="loading || exportBusy" @click="exportStatements"><Download :size="15" />导出</el-button>
<el-button v-if="canCreate" type="primary" :disabled="!canGenerate" :title="canGenerate ? '生成对账单' : '缺少客户列表权限,无法选择客户'" @click="openCreate"><Plus :size="15" />生成对账单</el-button>
</template>
<template #actions="{ row }">