732 lines
52 KiB
Vue
732 lines
52 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminDensity.css'
|
||
import '../financeDensity.css'
|
||
import { computed, onMounted, ref, watch } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
||
import { ArrowLeft, CreditCard, Download, 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, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
|
||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||
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,
|
||
createFinanceSettlementPayment,
|
||
createPayableSettlement,
|
||
createReceivableSettlement,
|
||
getFinanceSettlement,
|
||
listFinanceFeePool,
|
||
listFinancePayableSettlements,
|
||
listFinanceReceivableSettlements,
|
||
listFinanceStatements,
|
||
uploadFinanceVoucher,
|
||
} from '../api/finance'
|
||
import { financeOperationActionLabel, financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from '../status'
|
||
import type {
|
||
FinanceFeePoolItem,
|
||
FinancePayableSettlement,
|
||
FinanceReceivableSettlement,
|
||
FinanceSettlementDetail,
|
||
FinanceStatementReadModel,
|
||
} from '../types'
|
||
|
||
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: '' })
|
||
const appliedFilters = ref<QueryModel>({ ...filters.value })
|
||
const rows = ref<SettlementRow[]>([])
|
||
const page = ref(1)
|
||
const pageSize = ref(20)
|
||
const loading = ref(false)
|
||
const error = ref<ApiErrorDetails | null>(null)
|
||
const customerError = ref<ApiErrorDetails | null>(null)
|
||
const detail = ref<FinanceSettlementDetail | null>(null)
|
||
const selectedRow = ref<SettlementRow | null>(null)
|
||
const detailLoading = ref(false)
|
||
const detailError = ref<ApiErrorDetails | null>(null)
|
||
const createOpen = ref(false)
|
||
const createBusy = ref(false)
|
||
const statementOptions = ref<FinanceStatementReadModel[]>([])
|
||
const feeOptions = ref<FinanceFeePoolItem[]>([])
|
||
const supplierOptions = ref<Supplier[]>([])
|
||
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)
|
||
const paymentForm = ref({ transactionDate: new Date().toISOString().slice(0, 10), amount: 0, method: 'BANK_TRANSFER' as 'BANK_TRANSFER' | 'CASH' | 'OTHER', referenceNo: '', remark: '' })
|
||
const receiptOpen = ref(false)
|
||
const receiptBusy = ref(false)
|
||
const receiptFile = ref<File | null>(null)
|
||
const receiptUploadKey = ref(0)
|
||
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' ? '生成应收结算单' : '生成应付结算单')
|
||
const activeCustomers = computed(() => customers.value.filter((item) => item.status === 'ACTIVE'))
|
||
const supplierNames = computed(() => [...new Set(rows.value.map((item) => 'supplierName' in item ? item.supplierName : '').filter(Boolean))])
|
||
const statusOptions = computed(() => [...new Set(rows.value.map((item) => item.status).filter(Boolean))])
|
||
const fields = computed<QueryField[]>(() => [
|
||
{ key: 'status', type: 'select', label: props.kind === 'receivable' ? '收款状态' : '付款状态', width: 180, placeholder: '全部状态', props: { clearable: true }, options: statusOptions.value.map((status) => ({ label: financeStatusLabel(status), value: status })) },
|
||
...(props.kind === 'receivable'
|
||
? [{ key: 'customerId', type: 'select' as const, label: '客户名称', width: 260, placeholder: '全部客户', props: { filterable: true, clearable: true, loading: !customers.value.length && !customerError.value }, options: activeCustomers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })) }]
|
||
: [
|
||
{ key: 'supplierName', type: 'select' as const, label: '供应商', width: 240, placeholder: '全部供应商', props: { filterable: true, clearable: true }, options: supplierNames.value.map((name) => ({ label: name, value: name })) },
|
||
{ key: 'customerId', type: 'select' as const, label: '客户名称', width: 260, placeholder: '全部客户', props: { filterable: true, clearable: true, loading: !customers.value.length && !customerError.value }, options: activeCustomers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })) },
|
||
]),
|
||
{ key: 'settlementNo', type: 'input', label: '结算单号', width: 220, placeholder: '请输入结算单号' },
|
||
])
|
||
const filteredRows = computed(() => {
|
||
const status = String(appliedFilters.value.status || '')
|
||
const settlementNo = String(appliedFilters.value.settlementNo || '').trim().toLowerCase()
|
||
const supplierName = String(appliedFilters.value.supplierName || '')
|
||
return rows.value.filter((item) => (!status || item.status === status)
|
||
&& (!settlementNo || item.settlementNo.toLowerCase().includes(settlementNo))
|
||
&& (!supplierName || ('supplierName' in item && item.supplierName === supplierName)))
|
||
})
|
||
const paginatedRows = computed(() => {
|
||
const start = (page.value - 1) * pageSize.value
|
||
return filteredRows.value.slice(start, start + pageSize.value)
|
||
})
|
||
const totals = computed(() => filteredRows.value.reduce((result, row) => ({
|
||
amount: result.amount + totalAmount(row),
|
||
paid: result.paid + paidAmount(row),
|
||
outstanding: result.outstanding + row.outstandingAmount,
|
||
}), { amount: 0, paid: 0, outstanding: 0 }))
|
||
const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
|
||
? [
|
||
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
|
||
{ key: 'settlementNo', label: '结算单号', minWidth: 180 },
|
||
{ key: 'customerId', label: '客户', minWidth: 180, slotName: 'customer' },
|
||
{ key: 'feeItemSummary', label: '费用类型', minWidth: 150, slotName: 'feeItemSummary' },
|
||
{ key: 'currency', label: '币种', width: 80 },
|
||
{ key: 'receivableAmount', label: '应收金额', width: 130, slotName: 'money' },
|
||
{ key: 'receivedAmount', label: '已收', width: 120, slotName: 'paidMoney' },
|
||
{ key: 'outstandingAmount', label: '余额', width: 120, slotName: 'outstandingMoney' },
|
||
{ key: 'reconciliationStatementNo', label: '关联对账单', minWidth: 180, slotName: 'reconciliationStatementNo' },
|
||
{ key: 'businessPeriod', label: '结算周期', width: 120, slotName: 'businessPeriod' },
|
||
{ key: 'generatedDate', label: '生成日期', width: 130, slotName: 'generatedDate' },
|
||
{ key: 'dueDate', label: '到期日', width: 130, slotName: 'dueDate' },
|
||
{ key: 'paymentStatus', label: '收款状态', width: 120, slotName: 'paymentStatus' },
|
||
{ key: 'createdByName', label: '制单人', minWidth: 130, slotName: 'createdByName' },
|
||
{ key: 'createdAt', label: '创建时间', width: 170, slotName: 'createdAt' },
|
||
{ key: 'lastPaymentAt', label: '最后收款时间', width: 170, slotName: 'lastPaymentAt' },
|
||
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus' },
|
||
]
|
||
: [
|
||
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
|
||
{ key: 'settlementNo', label: '结算单号', minWidth: 180 },
|
||
{ key: 'supplierName', label: '供应商', minWidth: 180, slotName: 'supplier' },
|
||
{ key: 'customerId', label: '客户', minWidth: 180, slotName: 'customer' },
|
||
{ key: 'feeItemSummary', label: '费用类型', minWidth: 150, slotName: 'feeItemSummary' },
|
||
{ key: 'currency', label: '币种', width: 80 },
|
||
{ key: 'payableAmount', label: '应付金额', width: 130, slotName: 'money' },
|
||
{ key: 'paidAmount', label: '已付', width: 120, slotName: 'paidMoney' },
|
||
{ key: 'outstandingAmount', label: '余额', width: 120, slotName: 'outstandingMoney' },
|
||
{ key: 'businessPeriod', label: '结算周期', width: 120, slotName: 'businessPeriod' },
|
||
{ key: 'generatedDate', label: '生成日期', width: 130, slotName: 'generatedDate' },
|
||
{ key: 'dueDate', label: '到期日', width: 130, slotName: 'dueDate' },
|
||
{ key: 'paymentStatus', label: '付款状态', width: 120, slotName: 'paymentStatus' },
|
||
{ key: 'createdByName', label: '制单人', minWidth: 130, slotName: 'createdByName' },
|
||
{ key: 'createdAt', label: '创建时间', width: 170, slotName: 'createdAt' },
|
||
{ key: 'lastPaymentAt', label: '最后付款时间', width: 170, slotName: 'lastPaymentAt' },
|
||
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus' },
|
||
])
|
||
|
||
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 settlementPaymentStatusLabel(status?: string) {
|
||
if (status === 'UNPAID') return props.kind === 'receivable' ? '待收款' : '待付款'
|
||
if (status === 'PARTIALLY_PAID') return props.kind === 'receivable' ? '部分收款' : '部分付款'
|
||
if (status === 'PAID') return '已结清'
|
||
return '—'
|
||
}
|
||
function paymentMethodLabel(method?: string) {
|
||
return ({ BANK_TRANSFER: '银行转账', CASH: '现金', OTHER: '其他' } as Record<string, string>)[String(method || '')] || '—'
|
||
}
|
||
function operatorDisplayName(value?: string) {
|
||
const candidate = String(value || '').trim()
|
||
return candidate && !/^(?:operator-)?\d+$/i.test(candidate) ? candidate : '名称数据缺失'
|
||
}
|
||
function settlementRevisionActionLabel(action: string) {
|
||
return ({
|
||
SUBMIT: '提交审核',
|
||
APPROVE: '审核通过',
|
||
REJECT: '审核驳回',
|
||
ISSUE: '出账',
|
||
ADJUST: '调整',
|
||
VOID: '作废',
|
||
REOPEN: '重开',
|
||
RECEIPT: '收款登记',
|
||
PAYMENT: '付款登记',
|
||
} as Record<string, string>)[action] || financeOperationActionLabel(action)
|
||
}
|
||
function rowTotal(value: unknown) { return totalAmount(value as SettlementRow) }
|
||
function rowPaid(value: unknown) { return paidAmount(value as SettlementRow) }
|
||
function rowOutstanding(value: unknown) { return Number((value as SettlementRow).outstandingAmount || 0) }
|
||
function customerName(customerId: string, snapshot?: string) {
|
||
const mappedName = customers.value.find((item) => String(item.id) === String(customerId))?.tenantName
|
||
// Backend snapshots may contain the internal fallback label; never promote that ID to the finance UI.
|
||
if (snapshot && !/^客户\d+$/.test(snapshot.trim())) return snapshot
|
||
return mappedName || '—'
|
||
}
|
||
function totalAmount(row: SettlementRow) { return props.kind === 'receivable' ? (row as FinanceReceivableSettlement).receivableAmount : (row as FinancePayableSettlement).payableAmount }
|
||
function paidAmount(row: SettlementRow) { return props.kind === 'receivable' ? (row as FinanceReceivableSettlement).receivedAmount : (row as FinancePayableSettlement).paidAmount }
|
||
function selectedSupplierName() { return selectedRow.value && 'supplierName' in selectedRow.value ? selectedRow.value.supplierName || '—' : '—' }
|
||
function detailPartyName() { return !detail.value ? '—' : props.kind === 'receivable' ? customerName(detail.value.customerId, detail.value.customerName) : detail.value.supplierName || selectedSupplierName() }
|
||
function detailActionRow(): SettlementRow {
|
||
const current = detail.value!
|
||
return props.kind === 'receivable'
|
||
? { id: current.id, customerId: current.customerId, customerName: current.customerName || '', settlementNo: current.settlementNo, billDate: current.generatedDate || '', dueDate: current.dueDate || '', receivableAmount: current.amount, receivedAmount: current.paidAmount, outstandingAmount: current.outstandingAmount, currency: 'CNY', status: current.status as FinanceReceivableSettlement['status'], version: current.version }
|
||
: { id: current.id, customerId: current.customerId, settlementNo: current.settlementNo, costType: 'SINGLE_ORDER', orderId: null, inboundId: null, containerId: null, supplierName: current.supplierName || '', feeItemName: '', payableAmount: current.amount, paidAmount: current.paidAmount, outstandingAmount: current.outstandingAmount, currency: 'CNY', status: current.status, createdAt: current.generatedDate || '', version: current.version }
|
||
}
|
||
function remainingAfter(amount: number) { return money(Math.max(0, Number(detail.value?.outstandingAmount || 0) - Number(amount || 0))) }
|
||
|
||
async function loadCustomers() {
|
||
if (!canListCustomers.value) return
|
||
customerError.value = null
|
||
try {
|
||
const records: PlatformTenantResource[] = []; let currentPage = 1; let pages = 1
|
||
do { const result = await listPlatformTenants({ page: currentPage, size: 200, status: 'ACTIVE' }); records.push(...result.records); pages = Math.max(1, result.pages); currentPage += 1 } while (currentPage <= pages)
|
||
customers.value = records
|
||
}
|
||
catch (cause) { customerError.value = getApiErrorDetails(cause, '客户列表加载失败') }
|
||
}
|
||
|
||
async function load(model?: QueryModel) {
|
||
if (model) {
|
||
filters.value = { ...model }
|
||
appliedFilters.value = { ...model }
|
||
page.value = 1
|
||
}
|
||
rows.value = []
|
||
error.value = null
|
||
loading.value = true
|
||
try {
|
||
const customerId = String(appliedFilters.value.customerId || '').trim() || undefined
|
||
rows.value = props.kind === 'receivable'
|
||
? await listFinanceReceivableSettlements(customerId)
|
||
: await listFinancePayableSettlements(customerId)
|
||
} catch (cause) { error.value = getApiErrorDetails(cause, `${title.value}加载失败,请重试`) }
|
||
finally { loading.value = false }
|
||
}
|
||
|
||
function openDetail(row: unknown) {
|
||
selectedRow.value = row as SettlementRow
|
||
emit('openDetail', selectedRow.value.id)
|
||
}
|
||
|
||
async function loadDetail(settlementId: string) {
|
||
detail.value = null
|
||
detailError.value = null
|
||
detailLoading.value = true
|
||
try { detail.value = await getFinanceSettlement(settlementId) }
|
||
catch (cause) { detailError.value = getApiErrorDetails(cause, `${title.value}详情加载失败`) }
|
||
finally { detailLoading.value = false }
|
||
}
|
||
|
||
function backToList() { emit('back') }
|
||
|
||
async function openCreate() {
|
||
createForm.value = {
|
||
customerId: '',
|
||
reconciliationStatementId: '',
|
||
supplierId: '',
|
||
feeSourceIds: [],
|
||
dueDate: '',
|
||
}
|
||
createOpen.value = true
|
||
error.value = null
|
||
try {
|
||
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 selectedSupplierId = String(createForm.value.supplierId || '').trim() || undefined
|
||
if (!selectedSupplierId) {
|
||
createForm.value.feeSourceIds = []
|
||
return
|
||
}
|
||
const fees = await listFinanceFeePool(selectedCustomerId, 'PAYABLE', selectedSupplierId)
|
||
feeOptions.value = fees.filter((item) => item.feeSourceId && item.status === 'AVAILABLE' && item.settlementMode?.toUpperCase() !== 'DIRECT_PAID')
|
||
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)
|
||
})
|
||
watch(() => createForm.value.supplierId, () => {
|
||
if (!createOpen.value) return
|
||
createForm.value.feeSourceIds = []
|
||
feeOptions.value = []
|
||
if (createForm.value.customerId) void refreshCreateDependencies(createForm.value.customerId)
|
||
})
|
||
|
||
async function submitCreate() {
|
||
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
|
||
try {
|
||
if (props.kind === 'receivable') await createReceivableSettlement({ reconciliationStatementId: createForm.value.reconciliationStatementId, currency: 'CNY' })
|
||
else await createPayableSettlement({ customerId, supplierId: createForm.value.supplierId, feeSourceIds: createForm.value.feeSourceIds, dueDate: createForm.value.dueDate, currency: 'CNY' })
|
||
createOpen.value = false
|
||
ElMessage.success(`${title.value}已生成`)
|
||
await load()
|
||
} catch (cause) { error.value = getApiErrorDetails(cause, `${title.value}生成失败`) }
|
||
finally { createBusy.value = false }
|
||
}
|
||
|
||
function openPayment(row: unknown) {
|
||
const settlement = row as FinancePayableSettlement
|
||
selectedRow.value = settlement
|
||
detail.value = { id: settlement.id, settlementNo: settlement.settlementNo, customerId: settlement.customerId, direction: 'PAYABLE', amount: settlement.payableAmount, paidAmount: settlement.paidAmount, outstandingAmount: settlement.outstandingAmount, currency: 'CNY', status: settlement.status, version: settlement.version, lines: [], transactions: [], revisions: [] }
|
||
paymentFile.value = null
|
||
paymentForm.value = { transactionDate: new Date().toISOString().slice(0, 10), amount: settlement.outstandingAmount, method: 'BANK_TRANSFER', referenceNo: '', remark: '' }
|
||
paymentOpen.value = true
|
||
}
|
||
|
||
function openPaymentFromDetail() {
|
||
if (!detail.value) return
|
||
openPayment({ id: detail.value.id, settlementNo: detail.value.settlementNo, customerId: detail.value.customerId, supplierName: detail.value.supplierName || '', feeItemName: '', payableAmount: detail.value.amount, paidAmount: detail.value.paidAmount, outstandingAmount: detail.value.outstandingAmount, currency: 'CNY', status: detail.value.status, version: detail.value.version })
|
||
}
|
||
|
||
function selectPaymentFile(file: UploadFile) { paymentFile.value = file.raw ?? null }
|
||
|
||
async function openReceipt(row: unknown) {
|
||
const settlement = row as FinanceReceivableSettlement
|
||
selectedRow.value = settlement
|
||
receiptError.value = null
|
||
receiptValidationError.value = null
|
||
receiptBusy.value = true
|
||
try {
|
||
const current = await getFinanceSettlement(settlement.id)
|
||
if (!current.reconciliationStatementId) throw new Error('应收结算单缺少对账单关联')
|
||
detail.value = current
|
||
receiptFile.value = null
|
||
receiptForm.value = { receivedDate: new Date().toISOString().slice(0, 10), amount: current.outstandingAmount, paymentMethod: 'BANK_TRANSFER', remark: '' }
|
||
receiptOpen.value = true
|
||
} catch (cause) { receiptError.value = getApiErrorDetails(cause, '应收结算单加载失败,无法登记收款') }
|
||
finally { receiptBusy.value = false }
|
||
}
|
||
|
||
async function openReceiptFromDetail() {
|
||
if (!detail.value) return
|
||
await openReceipt({ id: detail.value.id, customerId: detail.value.customerId, settlementNo: detail.value.settlementNo, receivableAmount: detail.value.amount, receivedAmount: detail.value.paidAmount, outstandingAmount: detail.value.outstandingAmount, currency: 'CNY', status: detail.value.status, version: detail.value.version })
|
||
}
|
||
|
||
function selectReceiptFile(file: UploadFile) {
|
||
const raw = file.raw
|
||
if (!raw || !raw.type.startsWith('image/')) {
|
||
receiptFile.value = null
|
||
receiptValidationError.value = '收款凭证仅支持图片文件'
|
||
receiptUploadKey.value += 1
|
||
return
|
||
}
|
||
receiptFile.value = raw
|
||
receiptValidationError.value = null
|
||
receiptError.value = null
|
||
}
|
||
|
||
async function submitReceipt() {
|
||
const current = detail.value
|
||
if (!current?.reconciliationStatementId || !receiptFile.value || receiptForm.value.amount <= 0) {
|
||
receiptValidationError.value = '请填写收款金额并选择收款凭证'
|
||
return
|
||
}
|
||
receiptError.value = null
|
||
receiptBusy.value = true
|
||
try {
|
||
const voucher = await uploadFinanceVoucher(current.customerId, receiptFile.value)
|
||
await createFinanceReceipt({ customerId: current.customerId, receivedDate: receiptForm.value.receivedDate, amount: receiptForm.value.amount, paymentMethod: receiptForm.value.paymentMethod, remark: receiptForm.value.remark || undefined, allocations: [{ billId: current.reconciliationStatementId, amount: receiptForm.value.amount }] }, [voucher.fileId])
|
||
receiptOpen.value = false
|
||
ElMessage.success('收款已登记')
|
||
if (detailMode.value) await loadDetail(current.id)
|
||
else await load()
|
||
} catch (cause) { receiptError.value = getApiErrorDetails(cause, '收款登记失败') }
|
||
finally { receiptBusy.value = false }
|
||
}
|
||
|
||
async function submitPayment() {
|
||
if (!detail.value || !paymentFile.value || paymentForm.value.amount <= 0) return ElMessage.warning('请填写付款金额并选择付款凭证')
|
||
paymentBusy.value = true
|
||
try {
|
||
const voucher = await uploadFinanceVoucher(detail.value.customerId, paymentFile.value)
|
||
await createFinanceSettlementPayment(detail.value.id, { ...paymentForm.value, currency: 'CNY', voucherFileIds: [voucher.fileId], expectedVersion: detail.value.version })
|
||
paymentOpen.value = false
|
||
ElMessage.success('付款已登记')
|
||
if (detailMode.value) await loadDetail(detail.value.id)
|
||
else await load()
|
||
} catch (cause) { error.value = getApiErrorDetails(cause, '付款登记失败') }
|
||
finally { paymentBusy.value = false }
|
||
}
|
||
|
||
async function performAction(row: SettlementRow, action: 'VOID' | 'REOPEN') {
|
||
const reason = await ElMessageBox.prompt('请输入操作原因', action === 'VOID' ? `作废${title.value}` : `重开${title.value}`, {
|
||
customClass: 'admin-density-confirm',
|
||
inputType: 'textarea', inputPattern: /\S+/, inputErrorMessage: '操作原因不能为空', confirmButtonText: '确认', cancelButtonText: '取消',
|
||
}).then((result) => result.value.trim()).catch(() => null)
|
||
if (reason === null) return
|
||
actionBusy.value = true
|
||
try {
|
||
await actionFinanceSettlement(row.id, { action, reason, expectedVersion: row.version })
|
||
ElMessage.success(action === 'VOID' ? '结算单已作废' : '结算单已重开')
|
||
if (detailMode.value) await loadDetail(row.id)
|
||
else await load()
|
||
} catch (cause) { error.value = getApiErrorDetails(cause, `${action === 'VOID' ? '作废' : '重开'}结算单失败`) }
|
||
finally { actionBusy.value = false }
|
||
}
|
||
|
||
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, dense: true })
|
||
if (openDownloadCenter) await router.push({ name: 'download-center' })
|
||
} catch (cause) {
|
||
ElMessage.error(getApiErrorMessage(cause, `${taskName}导出任务创建失败`))
|
||
} finally {
|
||
exportSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
function exportRows() { return createSettlementExport() }
|
||
function exportDetail() { return detail.value ? createSettlementExport(detail.value.id) : undefined }
|
||
|
||
onMounted(() => {
|
||
if (canListCustomers.value) void loadCustomers()
|
||
const settlementId = String(props.settlementId || '')
|
||
if (settlementId) void loadDetail(settlementId)
|
||
else void load()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<AdminPage as="div" class="finance-settlement-page admin-density" spacing="normal">
|
||
<OperationErrorToast :message="receiptValidationError" />
|
||
<OperationErrorToast :details="receiptError" />
|
||
<el-result v-if="!canView" icon="warning" :title="`无权访问${title}`" sub-title="请联系管理员开通结算单查看权限" />
|
||
<DetailPageLayout v-else-if="detailMode" class="settlement-detail-page surface-panel finance-density-detail" :aria-label="`${title}详情`">
|
||
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" aria-label="返回列表" @click="backToList"><ArrowLeft :size="16" />返回列表</el-button><h1 class="density-detail-title">{{ detail?.settlementNo || '—' }}</h1><StatusBadge v-if="detail" :status="financeStatusLabel(detail.status)" /></div><div class="finance-detail-actions">
|
||
<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>
|
||
<el-button v-if="detail && canVoid && detail.status === 'VOIDED'" :loading="actionBusy" @click="performAction(detailActionRow(), 'REOPEN')"><RotateCcw :size="15" />重开</el-button>
|
||
</div></div></template>
|
||
<template v-if="detail" #summary>
|
||
<dl class="density-detail-overview settlement-detail-summary" aria-label="结算金额汇总">
|
||
<div class="summary-item"><dt>{{ kind === 'receivable' ? '应收金额' : '应付金额' }}</dt><dd>{{ money(detail.amount) }}</dd></div>
|
||
<div class="summary-item"><dt>{{ kind === 'receivable' ? '已收金额' : '已付金额' }}</dt><dd class="is-success">{{ money(detail.paidAmount) }}</dd></div>
|
||
<div class="summary-item"><dt>未结余额</dt><dd class="is-danger">{{ money(detail.outstandingAmount) }}</dd></div>
|
||
<div class="summary-item"><dt>到期日</dt><dd class="is-date">{{ displayDate(detail.dueDate) }}</dd></div>
|
||
</dl>
|
||
</template>
|
||
<LoadErrorState v-if="detailError" class="settlement-detail-state" :title="`${title}详情加载失败`" :message="detailError.message" :loading="detailLoading" @retry="loadDetail(String(settlementId || ''))"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||
<el-skeleton v-else-if="detailLoading" class="settlement-detail-state" :rows="10" animated />
|
||
<section v-else-if="detail" class="settlement-detail-content">
|
||
<FormSection class="density-form-section" title="基本信息">
|
||
<el-descriptions border :column="4" class="settlement-detail-info density-descriptions">
|
||
<el-descriptions-item label="结算单号">{{ detail.settlementNo }}</el-descriptions-item>
|
||
<el-descriptions-item :label="kind === 'receivable' ? '客户' : '供应商'">{{ detailPartyName() }}</el-descriptions-item>
|
||
<el-descriptions-item v-if="kind === 'payable'" label="客户">{{ customerName(detail.customerId, detail.customerName) }}</el-descriptions-item>
|
||
<el-descriptions-item label="币种">CNY</el-descriptions-item>
|
||
<el-descriptions-item :label="kind === 'receivable' ? '收款状态' : '付款状态'"><el-tag :type="financeStatusTone(detail.paymentStatus)">{{ settlementPaymentStatusLabel(detail.paymentStatus) }}</el-tag></el-descriptions-item>
|
||
<el-descriptions-item label="结算状态"><el-tag :type="financeStatusTone(detail.status)">{{ financeStatusLabel(detail.status) }}</el-tag></el-descriptions-item>
|
||
<el-descriptions-item label="关联对账单">{{ detail.reconciliationStatementNo || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="结算周期">{{ detail.businessPeriod || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="费用类型">{{ detail.feeItemSummary || '—' }}</el-descriptions-item>
|
||
<el-descriptions-item label="生成日期">{{ displayDate(detail.generatedDate) }}</el-descriptions-item>
|
||
<el-descriptions-item label="到期日">{{ displayDate(detail.dueDate) }}</el-descriptions-item>
|
||
<el-descriptions-item label="制单人">{{ operatorDisplayName(detail.createdByName) }}</el-descriptions-item>
|
||
<el-descriptions-item label="创建时间">{{ displayDateTime(detail.createdAt) }}</el-descriptions-item>
|
||
<el-descriptions-item :label="kind === 'receivable' ? '最后收款时间' : '最后付款时间'">{{ displayDateTime(detail.lastPaymentAt) }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
</FormSection>
|
||
<FormSection class="density-form-section" title="费用明细">
|
||
<div class="detail-table-scroll">
|
||
<el-table :data="detail.lines" size="small" border stripe empty-text="暂无费用明细">
|
||
<el-table-column prop="feeItemName" label="费用类型" min-width="150" />
|
||
<el-table-column prop="sourceType" label="来源类型" min-width="140"><template #default="scope">{{ financeSourceTypeLabel(scope.row.sourceType) }}</template></el-table-column>
|
||
<el-table-column prop="orderNo" label="订单号" min-width="160"><template #default="scope">{{ scope.row.orderNo || '—' }}</template></el-table-column>
|
||
<el-table-column prop="inboundCode" label="入仓码" min-width="130"><template #default="scope">{{ scope.row.inboundCode || '—' }}</template></el-table-column>
|
||
<el-table-column prop="containerNo" label="柜次号" min-width="130"><template #default="scope">{{ scope.row.containerNo || '—' }}</template></el-table-column>
|
||
<el-table-column prop="businessDate" label="业务发生日" width="120"><template #default="scope">{{ displayDate(scope.row.businessDate) }}</template></el-table-column>
|
||
<el-table-column prop="amount" label="金额(CNY)" width="130" align="right"><template #default="scope">{{ money(scope.row.amount) }}</template></el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</FormSection>
|
||
<FormSection class="density-form-section" :title="kind === 'receivable' ? '收款记录' : '付款记录'">
|
||
<div class="detail-table-scroll">
|
||
<el-table :data="detail.transactions" size="small" border stripe :empty-text="kind === 'receivable' ? '暂无收款记录' : '暂无付款记录'">
|
||
<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"><template #default="scope">{{ paymentMethodLabel(scope.row.method) }}</template></el-table-column>
|
||
<el-table-column prop="referenceNo" label="银行流水号/凭证号" min-width="180"><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="130"><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>
|
||
</div>
|
||
</FormSection>
|
||
<FormSection class="density-form-section" title="修订记录" :bordered="false">
|
||
<div class="detail-table-scroll">
|
||
<el-table :data="detail.revisions" size="small" border stripe empty-text="暂无调整记录">
|
||
<el-table-column prop="action" label="动作" min-width="120"><template #default="scope">{{ settlementRevisionActionLabel(scope.row.action) || financeOperationActionLabel(scope.row.action) }}</template></el-table-column>
|
||
<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>
|
||
</FormSection>
|
||
</section>
|
||
</DetailPageLayout>
|
||
<ListPageLayout v-else :aria-label="title">
|
||
|
||
|
||
<section><DataTable :preference-key="`finance-settlement-${kind}-table`" appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!customerError && !error" v-model:page="page" v-model:page-size="pageSize" :data="paginatedRows" :columns="columns" :loading="loading" :total="filteredRows.length" :aria-label="`${title}列表`" row-key="id" empty-text="暂无符合条件的结算单">
|
||
<template #toolbar-start><h1>{{ title }}</h1></template>
|
||
<template #filters>
|
||
<QueryToolbar v-model="filters" :ariaLabel="`${title}筛选`" :fields="fields" :preference-key="`finance-settlement-${kind}-query`" :search-on-clear="false" @search="load" />
|
||
</template>
|
||
<template #notices><template v-if="customerError || error">
|
||
<LoadErrorState :title="`${title}加载失败`" :message="(customerError || error)?.message || ''" :loading="loading" @retry="customerError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customerError || error)!" /></template></LoadErrorState>
|
||
</template></template>
|
||
<template #before-results>
|
||
<dl class="density-detail-overview settlement-summary" aria-label="结算汇总">
|
||
<div class="summary-item"><dt>{{ kind === 'receivable' ? '应收总额' : '应付总额' }}</dt><dd>{{ money(totals.amount) }}</dd></div>
|
||
<div class="summary-item"><dt>{{ kind === 'receivable' ? '已收' : '已付' }}</dt><dd class="is-success">{{ money(totals.paid) }}</dd></div>
|
||
<div class="summary-item"><dt>{{ kind === 'receivable' ? '待收' : '待付' }}</dt><dd class="is-danger">{{ money(totals.outstanding) }}</dd></div>
|
||
</dl>
|
||
</template>
|
||
|
||
|
||
<template #toolbar-actions>
|
||
<div class="settlement-list-actions">
|
||
<el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="15" />{{ createLabel }}</el-button>
|
||
<el-button v-if="canExport" :loading="exportSubmitting" :disabled="loading || exportSubmitting" @click="exportRows"><Download :size="15" />导出</el-button>
|
||
</div>
|
||
</template>
|
||
<template #actions="{ row }"><div class="settlement-row-actions density-actions row-actions admin-text-actions">
|
||
<el-button data-tone="neutral" class="density-action" size="small" @click="openDetail(row)">详情</el-button>
|
||
<el-button data-tone="neutral" class="density-action" v-if="canReceive && ['ISSUED','PARTIALLY_PAID'].includes(String(row.status))" size="small" :loading="receiptBusy" @click="openReceipt(row)">收款</el-button>
|
||
<el-button data-tone="neutral" class="density-action" v-if="canPay && ['ISSUED','PARTIALLY_PAID'].includes(String(row.status))" size="small" @click="openPayment(row)">付款</el-button>
|
||
<el-button data-tone="danger" class="density-action" v-if="canVoid && row.status === 'ISSUED'" size="small" :loading="actionBusy" @click="performAction(row as SettlementRow, 'VOID')">作废</el-button>
|
||
<el-button data-tone="neutral" class="density-action" v-if="canVoid && row.status === 'VOIDED'" size="small" :loading="actionBusy" @click="performAction(row as SettlementRow, 'REOPEN')">重开</el-button>
|
||
</div></template>
|
||
<template #customer="{ row }">{{ customerName(String(row.customerId), row.customerName) }}</template>
|
||
<template #supplier="{ row }">{{ row.supplierName || '—' }}</template>
|
||
<template #feeItemSummary="{ row }">{{ row.feeItemSummary || '—' }}</template>
|
||
<template #money="{ row }">{{ money(rowTotal(row)) }}</template>
|
||
<template #paidMoney="{ row }">{{ money(rowPaid(row)) }}</template>
|
||
<template #outstandingMoney="{ row }">{{ money(rowOutstanding(row)) }}</template>
|
||
<template #reconciliationStatementNo="{ row }">{{ row.reconciliationStatementNo || '—' }}</template>
|
||
<template #businessPeriod="{ row }">{{ row.businessPeriod || '—' }}</template>
|
||
<template #generatedDate="{ row }">{{ displayDate(row.generatedDate) }}</template>
|
||
<template #dueDate="{ row }">{{ displayDate((row as FinanceReceivableSettlement).dueDate) }}</template>
|
||
<template #paymentStatus="{ row }"><el-tag :type="financeStatusTone(row.paymentStatus)">{{ settlementPaymentStatusLabel(row.paymentStatus) }}</el-tag></template>
|
||
<template #createdByName="{ row }">{{ operatorDisplayName(row.createdByName) }}</template>
|
||
<template #createdAt="{ row }">{{ displayDateTime(row.createdAt) }}</template>
|
||
<template #lastPaymentAt="{ row }">{{ displayDateTime(row.lastPaymentAt) }}</template>
|
||
<template #settlementStatus="{ row }"><el-tag :type="financeStatusTone(row.status)">{{ financeStatusLabel(row.status) }}</el-tag></template>
|
||
<template #pagination-left>共 {{ filteredRows.length }} 条 · 币种:CNY</template>
|
||
</DataTable></section>
|
||
</ListPageLayout>
|
||
|
||
<AppDialog class="admin-density-dialog" v-model="createOpen" :title="`生成${title}`" width="min(600px, 94vw)" :confirm-loading="createBusy" @confirm="submitCreate">
|
||
<el-form class="density-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>
|
||
</AppDialog>
|
||
|
||
<AppDialog class="admin-density-dialog" v-model="paymentOpen" title="登记付款" width="min(760px, 94vw)" :confirm-loading="paymentBusy" @confirm="submitPayment">
|
||
<dl v-if="detail" class="density-detail-overview partial-summary"><div><dt>结算单号</dt><dd>{{ detail.settlementNo }}</dd></div><div><dt>供应商</dt><dd>{{ detailPartyName() }}</dd></div><div><dt>应付金额</dt><dd>{{ money(detail.amount) }}</dd></div><div><dt>已付金额</dt><dd class="is-success">{{ money(detail.paidAmount) }}</dd></div><div><dt>当前余额</dt><dd class="is-danger">{{ money(detail.outstandingAmount) }}</dd></div></dl>
|
||
<el-form class="settlement-operation-form density-form" label-position="top"><el-form-item label="付款日期"><el-date-picker v-model="paymentForm.transactionDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="本次付款金额(CNY)"><el-input-number v-model="paymentForm.amount" :min="0.01" :max="detail?.outstandingAmount || 0.01" :precision="2" placeholder="请输入付款金额" /></el-form-item><el-form-item label="付款方式"><el-select v-model="paymentForm.method"><el-option label="银行转账" value="BANK_TRANSFER" /><el-option label="现金" value="CASH" /><el-option label="其他" value="OTHER" /></el-select></el-form-item><el-form-item label="银行流水号/凭证号"><el-input v-model="paymentForm.referenceNo" placeholder="请输入付款流水号" /></el-form-item><el-form-item label="付款凭证"><el-upload :auto-upload="false" :limit="1" accept="image/*" :on-change="selectPaymentFile"><el-button>选择图片</el-button></el-upload></el-form-item><el-form-item label="备注"><el-input v-model="paymentForm.remark" type="textarea" placeholder="请输入付款备注(选填)" /></el-form-item></el-form>
|
||
<div v-if="detail" class="remaining-preview"><span>登记后剩余余额</span><strong>{{ remainingAfter(paymentForm.amount) }}</strong></div>
|
||
</AppDialog>
|
||
|
||
<AppDialog class="admin-density-dialog" v-model="receiptOpen" title="登记收款" width="min(760px, 94vw)" :confirm-loading="receiptBusy" @confirm="submitReceipt">
|
||
<dl v-if="detail" class="density-detail-overview partial-summary"><div><dt>结算单号</dt><dd>{{ detail.settlementNo }}</dd></div><div><dt>客户公司</dt><dd>{{ detailPartyName() }}</dd></div><div><dt>应收金额</dt><dd>{{ money(detail.amount) }}</dd></div><div><dt>已收金额</dt><dd class="is-success">{{ money(detail.paidAmount) }}</dd></div><div><dt>当前余额</dt><dd class="is-danger">{{ money(detail.outstandingAmount) }}</dd></div></dl>
|
||
<el-form class="settlement-operation-form density-form" label-position="top"><el-form-item label="收款日期"><el-date-picker v-model="receiptForm.receivedDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="本次收款金额(CNY)"><el-input-number v-model="receiptForm.amount" :min="0.01" :max="detail?.outstandingAmount || 0.01" :precision="2" placeholder="请输入收款金额" /></el-form-item><el-form-item label="收款方式"><el-select v-model="receiptForm.paymentMethod"><el-option label="银行转账" value="BANK_TRANSFER" /><el-option label="现金" value="CASH" /><el-option label="其他" value="OTHER" /></el-select></el-form-item><el-form-item label="收款凭证"><el-upload :key="receiptUploadKey" :auto-upload="false" :limit="1" accept="image/*" :on-change="selectReceiptFile"><el-button>选择图片</el-button></el-upload></el-form-item><el-form-item label="备注"><el-input v-model="receiptForm.remark" type="textarea" placeholder="请输入收款备注(选填)" /></el-form-item></el-form>
|
||
<div v-if="detail" class="remaining-preview"><span>登记后剩余余额</span><strong>{{ remainingAfter(receiptForm.amount) }}</strong></div>
|
||
</AppDialog>
|
||
</AdminPage>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.finance-settlement-page { min-width: 0; }
|
||
.settlement-summary, .settlement-list-actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; }
|
||
.settlement-summary { gap: var(--admin-space-2) 0; }
|
||
.settlement-list-actions { flex: 0 0 auto; justify-content: flex-end; gap: var(--admin-space-3); }
|
||
.settlement-list-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||
.settlement-detail-page { display: flex; width: min(100%, 1920px); margin: 0 auto; min-width: 0; gap: 0; overflow: hidden; padding: 0; }
|
||
.settlement-detail-actions { display: flex; align-items: center; gap: var(--admin-space-4); min-width: 0; flex-wrap: wrap; }
|
||
.settlement-detail-state { margin: var(--admin-space-5) var(--admin-space-6); }
|
||
.settlement-detail-content { min-width: 0; padding: 0 var(--admin-space-6) var(--admin-space-5); 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; }
|
||
.settlement-operation-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--admin-space-5); 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: var(--admin-space-4); padding: var(--admin-space-3) var(--admin-space-5); border-radius: var(--admin-radius); color: var(--cc-text-secondary, #64748b); background: var(--el-fill-color-light); }
|
||
.remaining-preview strong { color: #b45309; font-size: var(--admin-font-section); }
|
||
.operation-error { margin-bottom: var(--admin-space-5); }
|
||
.operation-error-meta { margin: -var(--admin-space-2) 0 var(--admin-space-5); }
|
||
:deep(.el-select), :deep(.el-date-editor) { width: 100%; }
|
||
|
||
@media (min-width: 1920px) {
|
||
.finance-settlement-page {
|
||
row-gap: var(--admin-space-3);
|
||
}
|
||
|
||
.settlement-detail-page {
|
||
gap: 0;
|
||
}
|
||
|
||
.settlement-detail-state {
|
||
margin: var(--admin-space-4) var(--admin-space-5);
|
||
}
|
||
|
||
.settlement-detail-content {
|
||
padding: 0 var(--admin-space-5) var(--admin-space-4);
|
||
}
|
||
|
||
.detail-table-scroll {
|
||
margin-bottom: var(--admin-space-3);
|
||
}
|
||
|
||
.settlement-operation-form {
|
||
column-gap: var(--admin-space-5);
|
||
}
|
||
}
|
||
@media (max-width: 720px) {
|
||
.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 14px; }
|
||
.settlement-detail-state { margin: 14px; }
|
||
.partial-summary { gap: 10px; padding: 12px 10px; }
|
||
.partial-summary div { padding: 0; border-left: 0; }
|
||
.settlement-summary { align-items: flex-start; flex-direction: column; gap: 6px; }
|
||
.settlement-summary .summary-item + .summary-item { margin-left: 0; padding-left: 0; border-left: 0; }
|
||
.settlement-list-actions { width: 100%; justify-content: flex-start; }
|
||
.summary-item strong { font-size: 21px; }
|
||
.settlement-detail-header { align-items: flex-start; flex-direction: column; padding: 16px; }
|
||
.settlement-detail-actions { width: 100%; }
|
||
.settlement-detail-actions :deep(.el-button) { min-height: 40px; }
|
||
:deep(.settlement-detail-info .el-descriptions__cell) { display: block; width: 100%; }
|
||
}
|
||
</style>
|