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,13 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
import { ArrowLeft, CreditCard, Download, Eye, HandCoins, Plus, RotateCcw, XCircle } from '@lucide/vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { createPlatformExportTask } from '@/modules/downloads'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { categoryLabels, listSuppliers, type Supplier } from '@/modules/suppliers'
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 +17,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 {
actionFinanceSettlement,
createFinanceReceipt,
@@ -41,6 +44,7 @@ type SettlementRow = FinanceReceivableSettlement | FinancePayableSettlement
const props = defineProps<{ kind: 'receivable' | 'payable'; settlementId?: string }>()
const emit = defineEmits<{ openDetail: [settlementId: string]; back: [] }>()
const router = useRouter()
const access = useAccessPreviewStore()
const customers = ref<PlatformTenantResource[]>([])
const filters = ref<QueryModel>({ customerId: '', supplierName: '', settlementNo: '', status: '' })
@@ -77,15 +81,18 @@ const receiptValidationError = ref<string | null>(null)
const receiptError = ref<ApiErrorDetails | null>(null)
const receiptForm = ref({ receivedDate: new Date().toISOString().slice(0, 10), amount: 0, paymentMethod: 'BANK_TRANSFER' as 'BANK_TRANSFER' | 'CASH' | 'OTHER', remark: '' })
const actionBusy = ref(false)
const exportSubmitting = ref(false)
const createPermission = computed(() => props.kind === 'receivable' ? 'platform:finance:settlement-ar:create' : 'platform:finance:settlement-ap:create')
const listPermission = computed(() => props.kind === 'receivable' ? 'platform:finance:settlement-ar:list' : 'platform:finance:settlement-ap:list')
const exportPermission = computed(() => props.kind === 'receivable' ? 'platform:finance:settlement-ar:export' : 'platform:finance:settlement-ap:export')
const canView = computed(() => access.can(listPermission.value))
const canListCustomers = computed(() => access.can('platform:customer:list'))
const canCreate = computed(() => access.can(createPermission.value))
const canPay = computed(() => props.kind === 'payable' && access.can('platform:finance:payment:create'))
const canReceive = computed(() => props.kind === 'receivable' && access.can('platform:finance:receipt:create'))
const canVoid = computed(() => access.can('platform:finance:settlement:void'))
const canExport = computed(() => access.can(exportPermission.value) && access.can('platform:download:list'))
const title = computed(() => props.kind === 'receivable' ? '结算单应收' : '结算单应付')
const detailMode = computed(() => Boolean(props.settlementId))
const createLabel = computed(() => props.kind === 'receivable' ? '生成应收结算单' : '生成应付结算单')
@@ -397,37 +404,48 @@ async function performAction(row: SettlementRow, action: 'VOID' | 'REOPEN') {
finally { actionBusy.value = false }
}
function csvCell(value: unknown) { return `"${String(value ?? '').replaceAll('"', '""')}"` }
function exportRows() {
if (!filteredRows.value.length) return ElMessage.warning('当前没有可导出的结算单')
const header = props.kind === 'receivable'
? ['结算单号', '客户', '币种', '应收金额', '已收', '余额', '生成日期', '到期日', '收款状态']
: ['结算单号', '供应商', '费用类型', '币种', '应付金额', '已付', '余额', '生成日期', '付款状态']
const records = filteredRows.value.map((row) => props.kind === 'receivable'
? [row.settlementNo, customerName(row.customerId, (row as FinanceReceivableSettlement).customerName), row.currency, totalAmount(row), paidAmount(row), row.outstandingAmount, (row as FinanceReceivableSettlement).billDate, (row as FinanceReceivableSettlement).dueDate, financeStatusLabel(row.status)]
: [row.settlementNo, (row as FinancePayableSettlement).supplierName, (row as FinancePayableSettlement).feeItemName, row.currency, totalAmount(row), paidAmount(row), row.outstandingAmount, (row as FinancePayableSettlement).createdAt, financeStatusLabel(row.status)])
const blob = new Blob([`\ufeff${[header, ...records].map((record) => record.map(csvCell).join(',')).join('\r\n')}`], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${title.value}-${new Date().toISOString().slice(0, 10)}.csv`
link.click()
URL.revokeObjectURL(url)
async function createSettlementExport(detailId?: string) {
const isDetail = Boolean(detailId)
const taskName = props.kind === 'receivable'
? (isDetail ? '应收结算单详情' : '应收结算单列表')
: (isDetail ? '应付结算单详情' : '应付结算单列表')
const exportType = props.kind === 'receivable'
? (isDetail ? 'FINANCE_RECEIVABLE_SETTLEMENT_DETAIL' : 'FINANCE_RECEIVABLE_SETTLEMENT_LIST')
: (isDetail ? 'FINANCE_PAYABLE_SETTLEMENT_DETAIL' : 'FINANCE_PAYABLE_SETTLEMENT_LIST')
const settlementId = String(detailId || '')
if (isDetail && !/^[1-9]\d*$/.test(settlementId)) return ElMessage.error('结算单业务标识无效,无法创建导出任务')
const parameters: Record<string, string> = isDetail ? { settlementId } : {}
if (!isDetail) {
const customerId = String(filters.value.customerId || '').trim()
if (customerId && !/^[1-9]\d*$/.test(customerId)) return ElMessage.error('客户业务标识无效,无法创建导出任务')
const status = String(filters.value.status || '').trim()
const settlementNo = String(filters.value.settlementNo || '').trim()
const supplierName = String(filters.value.supplierName || '').trim()
if (customerId) parameters.customerId = customerId
if (status) parameters.status = status
if (settlementNo) parameters.settlementNo = settlementNo
if (props.kind === 'payable' && supplierName) parameters.supplierName = supplierName
}
exportSubmitting.value = true
try {
const task = await createPlatformExportTask({
exportType,
format: 'XLSX',
taskName,
parameters,
})
const openDownloadCenter = await showExportTaskCreatedDialog({ taskName: task.taskName, taskNo: task.taskNo })
if (openDownloadCenter) await router.push({ name: 'download-center' })
} catch (cause) {
ElMessage.error(getApiErrorMessage(cause, `${taskName}导出任务创建失败`))
} finally {
exportSubmitting.value = false
}
}
function exportDetail() {
if (!detail.value) return
const current = detail.value
const header = ['结算单号', props.kind === 'receivable' ? '客户' : '供应商', '币种', props.kind === 'receivable' ? '应收金额' : '应付金额', props.kind === 'receivable' ? '已收' : '已付', '余额', '业务期间', '生成日期', '到期日', '状态']
const record = [current.settlementNo, props.kind === 'receivable' ? customerName(current.customerId, current.customerName) : current.supplierName, current.currency, current.amount, current.paidAmount, current.outstandingAmount, current.businessPeriod, current.generatedDate, current.dueDate, financeStatusLabel(current.status)]
const blob = new Blob([`\ufeff${[header, record].map((line) => line.map(csvCell).join(',')).join('\r\n')}`], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${current.settlementNo}.csv`
link.click()
URL.revokeObjectURL(url)
}
function exportRows() { return createSettlementExport() }
function exportDetail() { return detail.value ? createSettlementExport(detail.value.id) : undefined }
onMounted(() => {
if (canListCustomers.value) void loadCustomers()
@@ -448,7 +466,7 @@ onMounted(() => {
<el-tag v-if="detail" :type="financeStatusTone(detail.status)">{{ financeStatusLabel(detail.status) }}</el-tag>
</div>
<div class="settlement-detail-actions">
<el-button v-if="detail" @click="exportDetail"><Download :size="15" />导出</el-button>
<el-button v-if="detail && canExport" :loading="exportSubmitting" :disabled="exportSubmitting" @click="exportDetail"><Download :size="15" />导出</el-button>
<el-button v-if="detail && canReceive && ['ISSUED','PARTIALLY_PAID'].includes(detail.status)" type="primary" :loading="receiptBusy" @click="openReceiptFromDetail"><HandCoins :size="15" />部分收款</el-button>
<el-button v-if="detail && canPay && ['ISSUED','PARTIALLY_PAID'].includes(detail.status)" type="primary" @click="openPaymentFromDetail"><CreditCard :size="15" />部分付款</el-button>
<el-button v-if="detail && canVoid && detail.status === 'ISSUED'" type="danger" :loading="actionBusy" @click="performAction(detailActionRow(), 'VOID')"><XCircle :size="15" />作废</el-button>
@@ -534,7 +552,7 @@ onMounted(() => {
</div>
<div class="settlement-list-actions">
<el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="15" />{{ createLabel }}</el-button>
<el-button @click="exportRows"><Download :size="15" />导出</el-button>
<el-button v-if="canExport" :loading="exportSubmitting" :disabled="loading || exportSubmitting" @click="exportRows"><Download :size="15" />导出</el-button>
</div>
</section>
<section class="surface-panel table-panel">