This commit is contained in:
2026-08-17 16:45:46 +08:00
parent 4a9438315c
commit d95c90bd1a
48 changed files with 1217 additions and 374 deletions
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
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'
@@ -57,7 +57,14 @@ const createBusy = ref(false)
const statementOptions = ref<FinanceStatementReadModel[]>([])
const feeOptions = ref<FinanceFeePoolItem[]>([])
const supplierOptions = ref<Supplier[]>([])
const createForm = ref({ reconciliationStatementId: '', supplierId: '', feeSourceIds: [] as string[], dueDate: '' })
const suppliersLoaded = ref(false)
const createForm = ref({
customerId: '',
reconciliationStatementId: '',
supplierId: '',
feeSourceIds: [] as string[],
dueDate: '',
})
const paymentOpen = ref(false)
const paymentBusy = ref(false)
const paymentFile = ref<File | null>(null)
@@ -137,6 +144,10 @@ const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
function money(value: number) { return `¥${Number(value || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` }
function displayDate(value?: string) { return value || '—' }
function displayDateTime(value?: string) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
function operatorDisplayName(value?: string) {
const candidate = String(value || '').trim()
return candidate && !/^(?:operator-)?\d+$/i.test(candidate) ? candidate : '名称数据缺失'
}
function settlementRevisionActionLabel(action: string) {
return ({
SUBMIT: '提交审核',
@@ -213,27 +224,67 @@ async function loadDetail(settlementId: string) {
function backToList() { emit('back') }
async function openCreate() {
const customerId = String(filters.value.customerId || '')
if (!customerId) return ElMessage.warning('请先选择客户')
createForm.value = { reconciliationStatementId: '', supplierId: '', feeSourceIds: [], dueDate: '' }
createForm.value = {
customerId: '',
reconciliationStatementId: '',
supplierId: '',
feeSourceIds: [],
dueDate: '',
}
createOpen.value = true
error.value = null
try {
if (props.kind === 'receivable') {
statementOptions.value = (await listFinanceStatements(customerId)).filter((item) => item.status === 'ISSUED')
} else {
const [fees, suppliers] = await Promise.all([
listFinanceFeePool(customerId, 'PAYABLE'),
listSuppliers({ page: 1, size: 200, status: 'ACTIVE' }),
])
feeOptions.value = fees.filter((item) => item.feeSourceId && item.status === 'AVAILABLE')
await refreshCreateDependencies()
if (props.kind === 'payable' && !suppliersLoaded.value) {
const suppliers = await listSuppliers({ page: 1, size: 200, status: 'ACTIVE' })
supplierOptions.value = suppliers.records.filter((item) => item.status === 'ACTIVE')
suppliersLoaded.value = true
}
} catch (cause) { error.value = getApiErrorDetails(cause, '结算来源加载失败') }
}
function resetCreateDependentFields() {
createForm.value.reconciliationStatementId = ''
createForm.value.supplierId = ''
createForm.value.feeSourceIds = []
createForm.value.dueDate = ''
}
async function refreshCreateDependencies(customerId?: string) {
const selectedCustomerId = String((customerId ?? createForm.value.customerId) || '').trim()
statementOptions.value = []
feeOptions.value = []
if (!selectedCustomerId) {
resetCreateDependentFields()
return
}
if (props.kind === 'receivable') {
statementOptions.value = (await listFinanceStatements(selectedCustomerId)).filter((item) => item.status === 'ISSUED')
return
}
const fees = await listFinanceFeePool(selectedCustomerId, 'PAYABLE')
feeOptions.value = fees.filter((item) => item.feeSourceId && item.status === 'AVAILABLE')
if (createForm.value.supplierId && !supplierOptions.value.find((supplier) => String(supplier.id) === String(createForm.value.supplierId))) {
createForm.value.supplierId = ''
}
if (createForm.value.feeSourceIds.length) {
const available = new Set(feeOptions.value.map((item) => item.feeSourceId).filter(Boolean) as string[])
createForm.value.feeSourceIds = createForm.value.feeSourceIds.filter((id) => available.has(id))
if (!createForm.value.feeSourceIds.length && feeOptions.value.length) {
createForm.value.feeSourceIds = []
}
}
}
watch(() => createForm.value.customerId, (customerId) => {
if (createOpen.value) void refreshCreateDependencies(customerId)
})
async function submitCreate() {
const customerId = String(filters.value.customerId || '')
const customerId = String(createForm.value.customerId || '')
if (!customerId) return ElMessage.warning('请先选择客户')
if (props.kind === 'receivable' && !createForm.value.reconciliationStatementId) return ElMessage.warning('请选择已出账对账单')
if (props.kind === 'payable' && (!createForm.value.supplierId || !createForm.value.dueDate || !createForm.value.feeSourceIds.length)) return ElMessage.warning('请完整填写供应商、到期日和费用来源')
createBusy.value = true
@@ -445,6 +496,9 @@ onMounted(() => {
<el-table-column prop="transactionDate" :label="kind === 'receivable' ? '收款日期' : '付款日期'" width="130" />
<el-table-column prop="amount" label="金额(CNY)" width="130" align="right"><template #default="scope">{{ money(scope.row.amount) }}</template></el-table-column>
<el-table-column prop="method" :label="kind === 'receivable' ? '收款方式' : '付款方式'" min-width="130" />
<el-table-column prop="referenceNo" label="支付流水号" min-width="160"><template #default="scope">{{ scope.row.referenceNo || '—' }}</template></el-table-column>
<el-table-column label="登记人" min-width="140"><template #default="scope">{{ operatorDisplayName(scope.row.createdByName) }}</template></el-table-column>
<el-table-column label="登记时间" min-width="170"><template #default="scope">{{ displayDateTime(scope.row.createdAt) }}</template></el-table-column>
<el-table-column label="凭证" width="100"><template #default="scope">{{ scope.row.voucherFileIds.length ? `${scope.row.voucherFileIds.length} 个` : '—' }}</template></el-table-column>
<el-table-column prop="remark" label="备注" min-width="180"><template #default="scope">{{ scope.row.remark || '—' }}</template></el-table-column>
</el-table>
@@ -457,6 +511,7 @@ onMounted(() => {
<el-table-column prop="beforeAmount" label="调整前(CNY)" width="140" align="right"><template #default="scope">{{ money(scope.row.beforeAmount) }}</template></el-table-column>
<el-table-column prop="afterAmount" label="调整后(CNY)" width="140" align="right"><template #default="scope">{{ money(scope.row.afterAmount) }}</template></el-table-column>
<el-table-column prop="reason" label="原因" min-width="180"><template #default="scope">{{ scope.row.reason || '—' }}</template></el-table-column>
<el-table-column label="操作人" min-width="140"><template #default="scope">{{ operatorDisplayName(scope.row.operatorName) }}</template></el-table-column>
<el-table-column prop="occurredAt" label="时间" min-width="160"><template #default="scope">{{ displayDateTime(scope.row.occurredAt) }}</template></el-table-column>
</el-table>
</div>
@@ -507,8 +562,9 @@ onMounted(() => {
</template>
</ListPageLayout>
<AppDialog v-model="createOpen" :title="`生成${title}`" width="min(560px, 94vw)" :confirm-loading="createBusy" @confirm="submitCreate">
<AppDialog v-model="createOpen" :title="`生成${title}`" width="min(600px, 94vw)" :confirm-loading="createBusy" @confirm="submitCreate">
<el-form label-position="top">
<el-form-item label="客户"><el-select v-model="createForm.customerId" filterable placeholder="请选择客户"><el-option v-for="item in activeCustomers" :key="String(item.id)" :label="item.tenantName" :value="String(item.id)" /></el-select></el-form-item>
<el-form-item v-if="kind === 'receivable'" label="已出账对账单"><el-select v-model="createForm.reconciliationStatementId" filterable><el-option v-for="item in statementOptions" :key="item.id" :label="`${item.billNo} · ${item.outstandingAmount} CNY`" :value="item.id" /></el-select></el-form-item>
<template v-else><el-form-item label="供应商"><el-select v-model="createForm.supplierId" filterable placeholder="请选择供应商"><el-option v-for="item in supplierOptions" :key="String(item.id)" :label="`${item.name} · ${categoryLabels[item.category]}`" :value="String(item.id)" /></el-select></el-form-item><el-form-item label="到期日"><el-date-picker v-model="createForm.dueDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="应付费用来源"><el-select v-model="createForm.feeSourceIds" multiple filterable><el-option v-for="item in feeOptions" :key="item.feeSourceId!" :label="`${item.feeItemName} · ${item.amount} CNY`" :value="item.feeSourceId!" /></el-select></el-form-item></template>
</el-form>
@@ -535,43 +591,43 @@ onMounted(() => {
.finance-settlement-page { min-width: 0; }
.settlement-list-toolbar { display: flex; min-height: 54px; align-items: center; justify-content: space-between; gap: 20px; padding: 0 2px 10px; }
.settlement-summary, .settlement-list-actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; }
.settlement-summary { flex: 1; gap: 8px 0; }
.settlement-summary { flex: 1; gap: 6px 0; }
.settlement-list-actions { flex: 0 0 auto; justify-content: flex-end; gap: 8px; }
.settlement-list-actions :deep(.el-button + .el-button) { margin-left: 0; }
.summary-item { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.settlement-summary .summary-item + .summary-item { margin-left: 24px; padding-left: 24px; border-left: 1px solid var(--cc-border, #e3e8ef); }
.settlement-summary .summary-item + .summary-item { margin-left: 18px; padding-left: 18px; border-left: 1px solid var(--cc-border, #e3e8ef); }
.summary-item span { color: var(--cc-text-secondary, #64748b); font-size: 13px; }
.summary-item strong { color: var(--cc-text-primary, #1f2937); font-size: 18px; line-height: 1.2; }
.summary-item strong.is-success { color: #16803c; }
.summary-item strong.is-danger { color: #c2413a; }
.settlement-detail-page { display: flex; min-width: 0; gap: 0; overflow: hidden; padding: 0; }
.settlement-detail-header { display: flex; min-height: 88px; align-items: center; justify-content: space-between; gap: 18px; padding: 20px 24px; border-bottom: 1px solid var(--cc-border, #e3e8ef); }
.settlement-detail-page { display: flex; width: min(100%, 1920px); margin: 0 auto; min-width: 0; gap: 0; overflow: hidden; padding: 0; }
.settlement-detail-header { display: flex; min-height: 68px; align-items: center; justify-content: space-between; gap: 14px; padding: 14px 16px; border-bottom: 1px solid var(--cc-border, #e3e8ef); }
.settlement-detail-heading, .settlement-detail-actions { display: flex; align-items: center; gap: 10px; min-width: 0; flex-wrap: wrap; }
.settlement-detail-heading h1 { margin: 0; font-size: 20px; line-height: 1.35; letter-spacing: 0; }
.settlement-detail-breadcrumb { color: var(--cc-text-secondary, #64748b); font-size: 12px; }
.settlement-detail-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); padding: 14px 24px; border-bottom: 1px solid var(--cc-border, #e3e8ef); }
.settlement-detail-summary .summary-item { align-items: flex-start; flex-direction: column; gap: 5px; padding: 0 20px; border-left: 1px solid var(--cc-border, #e3e8ef); }
.settlement-detail-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); padding: 10px 16px; border-bottom: 1px solid var(--cc-border, #e3e8ef); }
.settlement-detail-summary .summary-item { align-items: flex-start; flex-direction: column; gap: 4px; padding: 0 12px; border-left: 1px solid var(--cc-border, #e3e8ef); }
.settlement-detail-summary .summary-item:first-child { padding-left: 0; border-left: 0; }
.summary-item strong.is-date { font-size: 20px; }
.settlement-detail-state { margin: 20px 24px; }
.settlement-detail-content { min-width: 0; padding: 0 24px 20px; overflow: hidden; }
.settlement-detail-state { margin: 14px 16px; }
.settlement-detail-content { min-width: 0; padding: 0 16px 12px; overflow: hidden; }
.settlement-detail-info { min-width: 0; }
.detail-table-scroll { min-width: 0; overflow-x: auto; }
:deep(.settlement-detail-info .el-descriptions__table) { table-layout: fixed; }
:deep(.settlement-detail-info .el-descriptions__content) { min-width: 0; overflow-wrap: anywhere; }
:deep(.detail-table-scroll .el-table) { min-width: 920px; }
.partial-summary { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); margin-bottom: 18px; padding: 14px 0; border: 1px solid var(--el-color-primary-light-7); border-radius: 6px; background: var(--el-color-primary-light-9); }
.partial-summary div { min-width: 0; padding: 0 12px; border-left: 1px solid var(--el-color-primary-light-7); }
.partial-summary { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); margin-bottom: 12px; padding: 10px 0; border: 1px solid var(--el-color-primary-light-7); border-radius: 6px; background: var(--el-color-primary-light-9); }
.partial-summary div { min-width: 0; padding: 0 10px; border-left: 1px solid var(--el-color-primary-light-7); }
.partial-summary div:first-child { border-left: 0; }
.partial-summary span, .partial-summary strong { display: block; overflow-wrap: anywhere; }
.partial-summary span { color: var(--cc-text-secondary, #64748b); font-size: 12px; }
.partial-summary strong { margin-top: 4px; color: var(--cc-text-primary, #1f2937); font-size: 15px; }
.partial-summary strong.is-success { color: #16803c; }
.partial-summary strong.is-danger { color: #c2413a; }
.settlement-operation-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 16px; }
.settlement-operation-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 14px; row-gap: 0; }
.settlement-operation-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
.settlement-operation-form :deep(.el-input-number) { width: 100%; }
.remaining-preview { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 14px; border-radius: 8px; color: var(--cc-text-secondary, #64748b); background: var(--el-fill-color-light); }
.remaining-preview { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 12px; border-radius: 8px; color: var(--cc-text-secondary, #64748b); background: var(--el-fill-color-light); }
.remaining-preview strong { color: #b45309; font-size: 16px; }
.operation-error { margin-bottom: 12px; }
.operation-error-meta { margin: -6px 0 12px; }
@@ -580,9 +636,9 @@ onMounted(() => {
.settlement-detail-summary, .partial-summary, .settlement-operation-form { grid-template-columns: 1fr; }
.settlement-detail-summary { gap: 10px; padding: 12px 16px; }
.settlement-detail-summary .summary-item { padding: 0; border-left: 0; }
.settlement-detail-content { padding: 0 16px 16px; }
.settlement-detail-state { margin: 16px; }
.partial-summary { gap: 10px; padding: 12px; }
.settlement-detail-content { padding: 0 16px 14px; }
.settlement-detail-state { margin: 14px; }
.partial-summary { gap: 10px; padding: 12px 10px; }
.partial-summary div { padding: 0; border-left: 0; }
.settlement-list-toolbar { align-items: stretch; flex-direction: column; padding: 0 0 10px; }
.settlement-summary { align-items: flex-start; flex-direction: column; gap: 6px; }