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
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft, Eye, FileCheck2, FilePlus2, Link2 } from '@lucide/vue'
import { ArrowLeft, Download, Eye, FileCheck2, FilePlus2, Link2 } from '@lucide/vue'
import { ElMessage } from 'element-plus'
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 { createReconciliationStatement, getFinanceFeeSource, listFinanceFeePool, reviewFinanceFeeSource, updateFinanceFeeSourceSettlementInclusion } from '../api/finance'
import { financeArapStatusOptions, financeDirectionLabel, financeOperationActionLabel, financeOperationResultLabel, financeSourceTypeLabel, financeStatusLabel } from '../status'
import type { FinanceFeeSourceDetail } from '../types'
@@ -71,6 +73,7 @@ const generationForm = reactive<{ mode: 'BY_CUTOFF' | 'SELECTED'; customerId: st
const selectedRows = ref<ArapRow[]>([])
const page = ref(1)
const pageSize = ref(20)
const exportBusy = ref(false)
const canView = computed(() => access.can('platform:finance:customer-arap:list'))
const canListCustomers = computed(() => canView.value || access.can('platform:customer:list'))
@@ -78,6 +81,7 @@ const canViewSource = computed(() => access.can('platform:finance:customer-arap:
const canReviewSource = computed(() => access.can('platform:finance:fee-source:review'))
const canSettleSource = computed(() => access.can('platform:finance:fee-source:settlement'))
const canCreateStatement = computed(() => access.can('platform:finance:reconciliation:create'))
const canExport = computed(() => access.can('platform:finance:customer-arap:export') && access.can('platform:download:list'))
const stateError = computed(() => customersError.value || loadError.value)
const activeCustomers = computed(() => customers.value.filter((item) => item.status === 'ACTIVE'))
const queryFields = computed<QueryField[]>(() => [
@@ -226,6 +230,34 @@ async function submitGeneration() {
generationSaving.value = true; generationError.value = null
try { await createReconciliationStatement({ customerId: generationForm.customerId, currency: 'CNY', generationMode: generationForm.mode, feeSourceIds: generationForm.mode === 'SELECTED' ? eligible.map((row) => row.feeSourceId as string) : undefined, periodStart: period[0], periodEnd: period[1], remark: generationForm.remark.trim() }); generationOpen.value = false; selectedRows.value = []; ElMessage.success('对账单草稿已生成'); await load() } catch (error) { generationError.value = getApiErrorDetails(error, '对账单生成失败,请重试') } finally { generationSaving.value = false }
}
async function exportFeeSources() {
const range = Array.isArray(filters.value.businessRange) ? filters.value.businessRange.map(String) : []
const customerId = String(filters.value.customerId || '').trim()
const direction = String(filters.value.direction || '').trim()
const reviewStatus = String(filters.value.reviewStatus || '').trim()
const included = String(filters.value.settlementIncluded || '')
const generated = String(filters.value.statementGenerated || '')
const keyword = String(filters.value.keyword || '').trim()
exportBusy.value = true
try {
const task = await createPlatformExportTask({
exportType: 'FINANCE_FEE_SOURCE_LIST', format: 'XLSX', taskName: '客户应收应付费用来源',
parameters: {
...(customerId ? { customerId } : {}),
...(direction ? { direction } : {}),
...(reviewStatus ? { reviewStatus } : {}),
...(included ? { settlementIncluded: included === 'true' } : {}),
...(generated ? { statementGenerated: generated === 'true' } : {}),
...(range[0] ? { businessDateFrom: range[0] } : {}),
...(range[1] ? { businessDateTo: range[1] } : {}),
...(keyword ? { keyword } : {}),
},
})
const openDownloadCenter = await showExportTaskCreatedDialog({ taskName: task.taskName, taskNo: task.taskNo })
if (openDownloadCenter) await router.push({ name: 'download-center' })
} catch (error) { ElMessage.error(getApiErrorMessage(error, '客户应收应付导出任务创建失败')) }
finally { exportBusy.value = false }
}
async function closeDetail() { detailOpen.value = false; selected.value = null; detail.value = null; await router.push({ name: 'finance-customer-arap' }) }
async function restoreFeeSourceDetail(feeSourceId: string) {
detailOpen.value = true; detailLoading.value = true; detailError.value = null; detail.value = null
@@ -257,7 +289,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
<template v-if="stateError" #state><LoadErrorState title="客户应收应付加载失败" :message="stateError.message" :loading="customersLoading || loading" @retry="customersError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="stateError" /></template></LoadErrorState></template>
<section v-if="!customersError && !loadError" class="surface-panel table-panel">
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="filteredRows.length" preference-key="finance-customer-arap-table" aria-label="客户应收应付列表" row-key="id" empty-text="暂无客户应收应付数据" @selection-change="onSelectionChange">
<template #toolbar-actions><div v-if="canCreateStatement" class="generation-actions"><el-button @click="openGeneration('BY_CUTOFF')"><FilePlus2 :size="16" />按截止日期生成</el-button><el-button type="primary" :disabled="!selectedRows.length" @click="openGeneration('SELECTED')"><FilePlus2 :size="16" />按勾选生成</el-button></div></template>
<template #toolbar-actions><div class="generation-actions"><el-button v-if="canExport" :loading="exportBusy" :disabled="loading || exportBusy" @click="exportFeeSources"><Download :size="16" />导出Excel</el-button><template v-if="canCreateStatement"><el-button @click="openGeneration('BY_CUTOFF')"><FilePlus2 :size="16" />按截止日期生成</el-button><el-button type="primary" :disabled="!selectedRows.length" @click="openGeneration('SELECTED')"><FilePlus2 :size="16" />按勾选生成</el-button></template></div></template>
<template #actions="{ row }"><div class="row-actions"><template v-if="row.feeSourceId"><el-button v-if="canViewSource" link type="primary" @click="openDetail(row)"><Eye :size="15" />详情</el-button><el-button v-if="canReviewSource" link type="primary" @click="openReview(row)"><FileCheck2 :size="15" />审核</el-button><el-button v-if="canSettleSource" link type="primary" @click="openInclusion(row)"><Link2 :size="15" />结算</el-button></template><span v-else class="muted-action">手工成本</span></div></template>
<template #customerName="{ row }">{{ customerName(row.customerId, row.customerName) }}</template><template #direction="{ row }"><el-tag :type="row.direction === 'RECEIVABLE' ? 'success' : row.direction === 'PAYABLE' ? 'warning' : 'info'">{{ financeDirectionLabel(row.direction) }}</el-tag></template>
<template #documentNo="{ row }">{{ feeDocumentLabel(row.direction, row.feeName) }}</template><template #sourceType="{ row }">{{ sourceTypeDisplay(row.sourceType) }}</template><template #sourceId="{ row }">{{ row.sourceId }}</template>