Files
admin-web/src/modules/finance/pages/FinanceFormalSettlementPage.vue
T

654 lines
48 KiB
Vue
Raw Normal View History

2026-08-11 18:43:56 +08:00
<script setup lang="ts">
2026-08-17 16:45:46 +08:00
import { computed, onMounted, ref, watch } from 'vue'
2026-08-13 11:57:44 +08:00
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
import { ArrowLeft, CreditCard, Download, Eye, HandCoins, Plus, RotateCcw, XCircle } from '@lucide/vue'
2026-08-11 18:43:56 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
2026-08-15 11:06:04 +08:00
import { categoryLabels, listSuppliers, type Supplier } from '@/modules/suppliers'
2026-08-11 18:43:56 +08:00
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import AdminPage from '@/shared/components/AdminPage.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
2026-08-12 09:25:55 +08:00
import AppDialog from '@/shared/components/AppDialog.vue'
2026-08-11 18:43:56 +08:00
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
2026-08-17 12:31:27 +08:00
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
import FormSection from '@/shared/components/FormSection.vue'
2026-08-11 18:43:56 +08:00
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'
2026-08-12 09:25:55 +08:00
import {
2026-08-13 11:57:44 +08:00
actionFinanceSettlement,
2026-08-12 09:25:55 +08:00
createFinanceReceipt,
2026-08-13 11:57:44 +08:00
createFinanceSettlementPayment,
2026-08-12 09:25:55 +08:00
createPayableSettlement,
createReceivableSettlement,
getFinanceSettlement,
listFinanceFeePool,
listFinancePayableSettlements,
listFinanceReceivableSettlements,
listFinanceStatements,
uploadFinanceVoucher,
} from '../api/finance'
2026-08-17 13:24:55 +08:00
import { financeOperationActionLabel, financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from '../status'
2026-08-12 09:25:55 +08:00
import type {
FinanceFeePoolItem,
FinancePayableSettlement,
FinanceReceivableSettlement,
FinanceSettlementDetail,
FinanceStatementReadModel,
} from '../types'
2026-08-11 18:43:56 +08:00
2026-08-13 11:57:44 +08:00
type SettlementRow = FinanceReceivableSettlement | FinancePayableSettlement
const props = defineProps<{ kind: 'receivable' | 'payable'; settlementId?: string }>()
const emit = defineEmits<{ openDetail: [settlementId: string]; back: [] }>()
2026-08-11 18:43:56 +08:00
const access = useAccessPreviewStore()
const customers = ref<PlatformTenantResource[]>([])
2026-08-13 11:57:44 +08:00
const filters = ref<QueryModel>({ customerId: '', supplierName: '', settlementNo: '', status: '' })
const rows = ref<SettlementRow[]>([])
2026-08-11 18:43:56 +08:00
const loading = ref(false)
const error = ref<ApiErrorDetails | null>(null)
const customerError = ref<ApiErrorDetails | null>(null)
2026-08-12 09:25:55 +08:00
const detail = ref<FinanceSettlementDetail | null>(null)
2026-08-13 11:57:44 +08:00
const selectedRow = ref<SettlementRow | null>(null)
2026-08-12 09:25:55 +08:00
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[]>([])
2026-08-15 11:06:04 +08:00
const supplierOptions = ref<Supplier[]>([])
2026-08-17 16:45:46 +08:00
const suppliersLoaded = ref(false)
const createForm = ref({
customerId: '',
reconciliationStatementId: '',
supplierId: '',
feeSourceIds: [] as string[],
dueDate: '',
})
2026-08-12 09:25:55 +08:00
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)
2026-08-14 14:27:03 +08:00
const receiptUploadKey = ref(0)
const receiptValidationError = ref<string | null>(null)
const receiptError = ref<ApiErrorDetails | null>(null)
2026-08-12 09:25:55 +08:00
const receiptForm = ref({ receivedDate: new Date().toISOString().slice(0, 10), amount: 0, paymentMethod: 'BANK_TRANSFER' as 'BANK_TRANSFER' | 'CASH' | 'OTHER', remark: '' })
2026-08-13 11:57:44 +08:00
const actionBusy = ref(false)
2026-08-12 09:25:55 +08:00
const createPermission = computed(() => props.kind === 'receivable' ? 'platform:finance:settlement-ar:create' : 'platform:finance:settlement-ap:create')
2026-08-13 11:57:44 +08:00
const listPermission = computed(() => props.kind === 'receivable' ? 'platform:finance:settlement-ar:list' : 'platform:finance:settlement-ap:list')
2026-08-13 14:37:59 +08:00
const canView = computed(() => access.can(listPermission.value))
2026-08-13 11:57:44 +08:00
const canListCustomers = computed(() => access.can('platform:customer:list'))
2026-08-12 09:25:55 +08:00
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'))
2026-08-13 11:57:44 +08:00
const canVoid = computed(() => access.can('platform:finance:settlement:void'))
2026-08-11 18:43:56 +08:00
const title = computed(() => props.kind === 'receivable' ? '结算单应收' : '结算单应付')
2026-08-13 11:57:44 +08:00
const detailMode = computed(() => Boolean(props.settlementId))
2026-08-12 09:25:55 +08:00
const createLabel = computed(() => props.kind === 'receivable' ? '生成应收结算单' : '生成应付结算单')
2026-08-11 19:35:28 +08:00
const activeCustomers = computed(() => customers.value.filter((item) => item.status === 'ACTIVE'))
2026-08-13 11:57:44 +08:00
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'
2026-08-14 12:50:42 +08:00
? [{ 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) })) }]
2026-08-13 11:57:44 +08:00
: [
{ key: 'supplierName', type: 'select' as const, label: '供应商', width: 240, placeholder: '全部供应商', props: { filterable: true, clearable: true }, options: supplierNames.value.map((name) => ({ label: name, value: name })) },
2026-08-14 12:50:42 +08:00
{ 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) })) },
2026-08-13 11:57:44 +08:00
]),
{ key: 'settlementNo', type: 'input', label: '结算单号', width: 220, placeholder: '请输入结算单号' },
])
const filteredRows = computed(() => {
const status = String(filters.value.status || '')
const settlementNo = String(filters.value.settlementNo || '').trim().toLowerCase()
const supplierName = String(filters.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 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 }))
2026-08-11 18:43:56 +08:00
const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
2026-08-13 11:57:44 +08:00
? [
{ key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' },
{ key: 'settlementNo', label: '结算单号', minWidth: 180 },
{ key: 'customerId', label: '客户', minWidth: 180, slotName: 'customer' },
{ 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: 'billDate', label: '生成日期', width: 130, slotName: 'billDate' },
{ key: 'dueDate', label: '到期日', width: 130, slotName: 'dueDate' },
{ key: 'status', label: '收款状态', width: 120, slotName: 'status' },
]
: [
{ key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' },
{ key: 'settlementNo', label: '结算单号', minWidth: 180 },
{ key: 'supplierName', label: '供应商', minWidth: 180, slotName: 'supplier' },
{ key: 'feeItemName', label: '费用类型', minWidth: 150, slotName: 'feeType' },
{ 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: 'createdAt', label: '生成日期', width: 130, slotName: 'createdAt' },
{ key: 'status', label: '付款状态', width: 120, slotName: 'status' },
])
function money(value: number) { return `¥${Number(value || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` }
function displayDate(value?: string) { return value || '—' }
2026-08-17 13:24:55 +08:00
function displayDateTime(value?: string) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
2026-08-17 16:45:46 +08:00
function operatorDisplayName(value?: string) {
const candidate = String(value || '').trim()
return candidate && !/^(?:operator-)?\d+$/i.test(candidate) ? candidate : '名称数据缺失'
}
2026-08-17 13:24:55 +08:00
function settlementRevisionActionLabel(action: string) {
return ({
SUBMIT: '提交审核',
APPROVE: '审核通过',
REJECT: '审核驳回',
ISSUE: '出账',
ADJUST: '调整',
VOID: '作废',
REOPEN: '重开',
RECEIPT: '收款登记',
PAYMENT: '付款登记',
} as Record<string, string>)[action] || financeOperationActionLabel(action)
}
2026-08-13 11:57:44 +08:00
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) }
2026-08-15 09:57:49 +08:00
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 || '—'
}
2026-08-13 11:57:44 +08:00
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))) }
2026-08-12 09:25:55 +08:00
async function loadCustomers() {
2026-08-13 11:57:44 +08:00
if (!canListCustomers.value) return
2026-08-12 09:25:55 +08:00
customerError.value = null
2026-08-14 12:50:42 +08:00
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
}
2026-08-12 09:25:55 +08:00
catch (cause) { customerError.value = getApiErrorDetails(cause, '客户列表加载失败') }
}
async function load(model: QueryModel = filters.value) {
filters.value = { ...model }
rows.value = []
error.value = null
loading.value = true
try {
2026-08-12 17:29:35 +08:00
const customerId = String(filters.value.customerId || '').trim() || undefined
2026-08-12 09:25:55 +08:00
rows.value = props.kind === 'receivable'
2026-08-12 17:29:35 +08:00
? await listFinanceReceivableSettlements(customerId)
: await listFinancePayableSettlements(customerId)
2026-08-12 09:25:55 +08:00
} catch (cause) { error.value = getApiErrorDetails(cause, `${title.value}加载失败,请重试`) }
finally { loading.value = false }
}
2026-08-14 14:27:03 +08:00
function openDetail(row: unknown) {
2026-08-13 11:57:44 +08:00
selectedRow.value = row as SettlementRow
2026-08-15 10:27:18 +08:00
emit('openDetail', selectedRow.value.id)
2026-08-13 11:57:44 +08:00
}
async function loadDetail(settlementId: string) {
2026-08-12 09:25:55 +08:00
detail.value = null
detailError.value = null
detailLoading.value = true
2026-08-13 11:57:44 +08:00
try { detail.value = await getFinanceSettlement(settlementId) }
2026-08-12 09:25:55 +08:00
catch (cause) { detailError.value = getApiErrorDetails(cause, `${title.value}详情加载失败`) }
finally { detailLoading.value = false }
}
2026-08-13 11:57:44 +08:00
function backToList() { emit('back') }
2026-08-12 09:25:55 +08:00
async function openCreate() {
2026-08-17 16:45:46 +08:00
createForm.value = {
customerId: '',
reconciliationStatementId: '',
supplierId: '',
feeSourceIds: [],
dueDate: '',
}
2026-08-12 09:25:55 +08:00
createOpen.value = true
error.value = null
try {
2026-08-17 16:45:46 +08:00
await refreshCreateDependencies()
if (props.kind === 'payable' && !suppliersLoaded.value) {
const suppliers = await listSuppliers({ page: 1, size: 200, status: 'ACTIVE' })
2026-08-15 11:06:04 +08:00
supplierOptions.value = suppliers.records.filter((item) => item.status === 'ACTIVE')
2026-08-17 16:45:46 +08:00
suppliersLoaded.value = true
2026-08-12 09:25:55 +08:00
}
} catch (cause) { error.value = getApiErrorDetails(cause, '结算来源加载失败') }
}
2026-08-17 16:45:46 +08:00
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)
})
2026-08-12 09:25:55 +08:00
async function submitCreate() {
2026-08-17 16:45:46 +08:00
const customerId = String(createForm.value.customerId || '')
if (!customerId) return ElMessage.warning('请先选择客户')
2026-08-12 09:25:55 +08:00
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 {
2026-08-13 11:57:44 +08:00
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' })
2026-08-12 09:25:55 +08:00
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
2026-08-13 11:57:44 +08:00
selectedRow.value = settlement
2026-08-12 09:25:55 +08:00
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
}
2026-08-13 11:57:44 +08:00
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 })
}
2026-08-12 09:25:55 +08:00
function selectPaymentFile(file: UploadFile) { paymentFile.value = file.raw ?? null }
async function openReceipt(row: unknown) {
const settlement = row as FinanceReceivableSettlement
2026-08-13 11:57:44 +08:00
selectedRow.value = settlement
2026-08-14 14:27:03 +08:00
receiptError.value = null
receiptValidationError.value = null
2026-08-12 09:25:55 +08:00
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
2026-08-14 14:27:03 +08:00
} catch (cause) { receiptError.value = getApiErrorDetails(cause, '应收结算单加载失败,无法登记收款') }
2026-08-12 09:25:55 +08:00
finally { receiptBusy.value = false }
}
2026-08-13 11:57:44 +08:00
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 })
}
2026-08-14 14:27:03 +08:00
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
}
2026-08-12 09:25:55 +08:00
async function submitReceipt() {
const current = detail.value
2026-08-14 14:27:03 +08:00
if (!current?.reconciliationStatementId || !receiptFile.value || receiptForm.value.amount <= 0) {
receiptValidationError.value = '请填写收款金额并选择收款凭证'
return
}
receiptError.value = null
2026-08-12 09:25:55 +08:00
receiptBusy.value = true
try {
const voucher = await uploadFinanceVoucher(current.customerId, receiptFile.value)
2026-08-13 11:57:44 +08:00
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])
2026-08-12 09:25:55 +08:00
receiptOpen.value = false
ElMessage.success('收款已登记')
2026-08-13 11:57:44 +08:00
if (detailMode.value) await loadDetail(current.id)
else await load()
2026-08-14 14:27:03 +08:00
} catch (cause) { receiptError.value = getApiErrorDetails(cause, '收款登记失败') }
2026-08-12 09:25:55 +08:00
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('付款已登记')
2026-08-13 11:57:44 +08:00
if (detailMode.value) await loadDetail(detail.value.id)
else await load()
2026-08-12 09:25:55 +08:00
} catch (cause) { error.value = getApiErrorDetails(cause, '付款登记失败') }
finally { paymentBusy.value = false }
}
2026-08-13 11:57:44 +08:00
async function performAction(row: SettlementRow, action: 'VOID' | 'REOPEN') {
const reason = await ElMessageBox.prompt('请输入操作原因', action === 'VOID' ? `作废${title.value}` : `重开${title.value}`, {
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 }
}
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)
}
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)
}
onMounted(() => {
if (canListCustomers.value) void loadCustomers()
const settlementId = String(props.settlementId || '')
if (settlementId) void loadDetail(settlementId)
else void load()
})
2026-08-11 18:43:56 +08:00
</script>
2026-08-12 09:25:55 +08:00
2026-08-11 18:43:56 +08:00
<template>
<AdminPage as="div" class="finance-settlement-page" spacing="normal">
<el-result v-if="!canView" icon="warning" :title="`无权访问${title}`" sub-title="请联系管理员开通结算单查看权限" />
2026-08-17 12:31:27 +08:00
<DetailPageLayout v-else-if="detailMode" class="settlement-detail-page surface-panel" :aria-label="`${title}详情`">
<template #header><header class="settlement-detail-header">
2026-08-13 12:22:41 +08:00
<div class="settlement-detail-heading">
<el-button aria-label="返回列表" title="返回列表" @click="backToList"><ArrowLeft :size="17" /></el-button>
2026-08-17 12:31:27 +08:00
<div><div class="settlement-detail-breadcrumb">财务管理 / {{ title }} / {{ detail?.settlementNo || settlementId }}</div><h1>{{ title }}详情</h1></div>
2026-08-13 12:22:41 +08:00
<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 && 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>
2026-08-17 12:31:27 +08:00
</header></template>
<template v-if="detail" #summary>
2026-08-13 12:22:41 +08:00
<section class="settlement-detail-summary" aria-label="结算金额汇总">
<div class="summary-item"><span>{{ kind === 'receivable' ? '应收金额' : '应付金额' }}</span><strong>{{ money(detail.amount) }}</strong></div>
<div class="summary-item"><span>{{ kind === 'receivable' ? '已收金额' : '已付金额' }}</span><strong class="is-success">{{ money(detail.paidAmount) }}</strong></div>
<div class="summary-item"><span>未结余额</span><strong class="is-danger">{{ money(detail.outstandingAmount) }}</strong></div>
<div class="summary-item"><span>到期日</span><strong class="is-date">{{ displayDate(detail.dueDate) }}</strong></div>
</section>
2026-08-17 12:31:27 +08:00
</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 title="基本信息">
<el-descriptions :column="4" class="settlement-detail-info">
2026-08-13 12:22:41 +08:00
<el-descriptions-item label="结算单号">{{ detail.settlementNo }}</el-descriptions-item>
<el-descriptions-item :label="kind === 'receivable' ? '客户' : '供应商'">{{ detailPartyName() }}</el-descriptions-item>
<el-descriptions-item label="币种">CNY</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.reconciliationStatementId || '—' }}</el-descriptions-item>
<el-descriptions-item label="业务期间">{{ detail.businessPeriod || '—' }}</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>
2026-08-17 12:31:27 +08:00
</FormSection>
<FormSection title="费用明细">
<div class="detail-table-scroll">
2026-08-13 12:22:41 +08:00
<el-table :data="detail.lines" size="small" border stripe empty-text="暂无费用明细">
<el-table-column prop="feeItemName" label="费用类型" min-width="150" />
2026-08-17 13:24:55 +08:00
<el-table-column prop="sourceType" label="来源类型" min-width="140"><template #default="scope">{{ financeSourceTypeLabel(scope.row.sourceType) }}</template></el-table-column>
2026-08-13 12:22:41 +08:00
<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>
2026-08-17 12:31:27 +08:00
</div>
</FormSection>
<FormSection :title="kind === 'receivable' ? '收款记录' : '付款记录'">
<div class="detail-table-scroll">
2026-08-13 12:22:41 +08:00
<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" />
2026-08-17 16:45:46 +08:00
<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>
2026-08-13 12:22:41 +08:00
<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>
2026-08-17 12:31:27 +08:00
</div>
</FormSection>
<FormSection title="修订记录" :bordered="false">
<div class="detail-table-scroll">
2026-08-15 09:47:23 +08:00
<el-table :data="detail.revisions" size="small" border stripe empty-text="暂无调整记录">
2026-08-17 13:24:55 +08:00
<el-table-column prop="action" label="动作" min-width="120"><template #default="scope">{{ settlementRevisionActionLabel(scope.row.action) || financeOperationActionLabel(scope.row.action) }}</template></el-table-column>
2026-08-13 12:22:41 +08:00
<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>
2026-08-17 16:45:46 +08:00
<el-table-column label="操作人" min-width="140"><template #default="scope">{{ operatorDisplayName(scope.row.operatorName) }}</template></el-table-column>
2026-08-17 13:24:55 +08:00
<el-table-column prop="occurredAt" label="时间" min-width="160"><template #default="scope">{{ displayDateTime(scope.row.occurredAt) }}</template></el-table-column>
2026-08-13 12:22:41 +08:00
</el-table>
2026-08-17 12:31:27 +08:00
</div>
</FormSection>
</section>
</DetailPageLayout>
2026-08-11 18:43:56 +08:00
<ListPageLayout v-else :aria-label="title">
2026-08-12 09:25:55 +08:00
<template #toolbar>
2026-08-13 11:57:44 +08:00
<QueryToolbar v-model="filters" class="surface-panel" :ariaLabel="`${title}筛选`" :fields="fields" :preference-key="`finance-settlement-${kind}-query`" @search="load" @reset="load" />
2026-08-12 09:25:55 +08:00
</template>
<template v-if="customerError || error" #state>
<LoadErrorState :title="`${title}加载失败`" :message="(customerError || error)?.message || ''" :loading="loading" @retry="customerError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customerError || error)!" /></template></LoadErrorState>
</template>
2026-08-13 11:57:44 +08:00
<template v-if="!customerError && !error">
2026-08-17 13:24:55 +08:00
<section class="settlement-list-toolbar" aria-label="结算汇总与列表操作">
<div class="settlement-summary" aria-label="结算汇总">
<div class="summary-item"><span>{{ kind === 'receivable' ? '应收总额' : '应付总额' }}</span><strong>{{ money(totals.amount) }}</strong></div>
<div class="summary-item"><span>{{ kind === 'receivable' ? '已收' : '已付' }}</span><strong class="is-success">{{ money(totals.paid) }}</strong></div>
<div class="summary-item"><span>{{ kind === 'receivable' ? '待收' : '待付' }}</span><strong class="is-danger">{{ money(totals.outstanding) }}</strong></div>
</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>
</div>
2026-08-13 11:57:44 +08:00
</section>
<section class="surface-panel table-panel">
<DataTable :data="filteredRows" :columns="columns" :loading="loading" :aria-label="`${title}列表`" row-key="id" empty-text="暂无符合条件的结算单">
<template #actions="{ row }">
<el-button link type="primary" @click="openDetail(row)"><Eye :size="15" />详情</el-button>
<el-button v-if="canReceive && ['ISSUED','PARTIALLY_PAID'].includes(String(row.status))" link :loading="receiptBusy" @click="openReceipt(row)"><HandCoins :size="15" />收款</el-button>
<el-button v-if="canPay && ['ISSUED','PARTIALLY_PAID'].includes(String(row.status))" link @click="openPayment(row)"><CreditCard :size="15" />付款</el-button>
<el-button v-if="canVoid && row.status === 'ISSUED'" link type="danger" :loading="actionBusy" @click="performAction(row as SettlementRow, 'VOID')"><XCircle :size="15" />作废</el-button>
<el-button v-if="canVoid && row.status === 'VOIDED'" link :loading="actionBusy" @click="performAction(row as SettlementRow, 'REOPEN')"><RotateCcw :size="15" />重开</el-button>
</template>
<template #customer="{ row }">{{ customerName(String(row.customerId), row.customerName) }}</template>
<template #supplier="{ row }">{{ row.supplierName || '—' }}</template>
<template #feeType="{ row }">{{ row.feeItemName || '详见费用明细' }}</template>
<template #money="{ row }">{{ money(rowTotal(row)) }}</template>
<template #paidMoney="{ row }">{{ money(rowPaid(row)) }}</template>
<template #outstandingMoney="{ row }">{{ money(rowOutstanding(row)) }}</template>
<template #billDate="{ row }">{{ displayDate((row as FinanceReceivableSettlement).billDate) }}</template>
<template #dueDate="{ row }">{{ displayDate((row as FinanceReceivableSettlement).dueDate) }}</template>
<template #createdAt="{ row }">{{ displayDate((row as FinancePayableSettlement).createdAt) }}</template>
<template #status="{ row }"><el-tag :type="financeStatusTone(row.status)">{{ financeStatusLabel(row.status) }}</el-tag></template>
<template #pagination-left>共 {{ filteredRows.length }} 条 · 币种:CNY</template>
</DataTable>
</section>
</template>
2026-08-11 18:43:56 +08:00
</ListPageLayout>
2026-08-12 09:25:55 +08:00
2026-08-17 16:45:46 +08:00
<AppDialog v-model="createOpen" :title="`生成${title}`" width="min(600px, 94vw)" :confirm-loading="createBusy" @confirm="submitCreate">
2026-08-12 09:25:55 +08:00
<el-form label-position="top">
2026-08-17 16:45:46 +08:00
<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>
2026-08-13 11:57:44 +08:00
<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>
2026-08-15 11:06:04 +08:00
<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>
2026-08-12 09:25:55 +08:00
</el-form>
</AppDialog>
2026-08-17 12:31:27 +08:00
<AppDialog v-model="paymentOpen" title="登记付款" width="min(760px, 94vw)" :confirm-loading="paymentBusy" @confirm="submitPayment">
2026-08-13 12:22:41 +08:00
<div v-if="detail" class="partial-summary"><div><span>结算单号</span><strong>{{ detail.settlementNo }}</strong></div><div><span>供应商</span><strong>{{ detailPartyName() }}</strong></div><div><span>应付金额</span><strong>{{ money(detail.amount) }}</strong></div><div><span>已付金额</span><strong class="is-success">{{ money(detail.paidAmount) }}</strong></div><div><span>当前余额</span><strong class="is-danger">{{ money(detail.outstandingAmount) }}</strong></div></div>
2026-08-17 12:31:27 +08:00
<el-form class="settlement-operation-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" /></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" /></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" /></el-form-item></el-form>
2026-08-13 12:22:41 +08:00
<div v-if="detail" class="remaining-preview"><span>登记后剩余余额</span><strong>{{ remainingAfter(paymentForm.amount) }}</strong></div>
2026-08-12 09:25:55 +08:00
</AppDialog>
2026-08-17 12:31:27 +08:00
<AppDialog v-model="receiptOpen" title="登记收款" width="min(760px, 94vw)" :confirm-loading="receiptBusy" @confirm="submitReceipt">
2026-08-13 12:22:41 +08:00
<div v-if="detail" class="partial-summary"><div><span>结算单号</span><strong>{{ detail.settlementNo }}</strong></div><div><span>客户公司</span><strong>{{ detailPartyName() }}</strong></div><div><span>应收金额</span><strong>{{ money(detail.amount) }}</strong></div><div><span>已收金额</span><strong class="is-success">{{ money(detail.paidAmount) }}</strong></div><div><span>当前余额</span><strong class="is-danger">{{ money(detail.outstandingAmount) }}</strong></div></div>
2026-08-14 14:27:03 +08:00
<el-alert v-if="receiptValidationError" type="error" :title="receiptValidationError" :closable="false" show-icon class="operation-error" />
<el-alert v-if="receiptError" type="error" :title="receiptError.message" :closable="false" show-icon class="operation-error" />
<ApiErrorMeta v-if="receiptError" :details="receiptError" class="operation-error-meta" />
2026-08-17 12:31:27 +08:00
<el-form class="settlement-operation-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" /></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" /></el-form-item></el-form>
2026-08-13 12:22:41 +08:00
<div v-if="detail" class="remaining-preview"><span>登记后剩余余额</span><strong>{{ remainingAfter(receiptForm.amount) }}</strong></div>
2026-08-12 09:25:55 +08:00
</AppDialog>
2026-08-11 18:43:56 +08:00
</AdminPage>
</template>
2026-08-12 09:25:55 +08:00
<style scoped>
.finance-settlement-page { min-width: 0; }
2026-08-17 13:24:55 +08:00
.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; }
2026-08-17 16:45:46 +08:00
.settlement-summary { flex: 1; gap: 6px 0; }
2026-08-17 13:24:55 +08:00
.settlement-list-actions { flex: 0 0 auto; justify-content: flex-end; gap: 8px; }
.settlement-list-actions :deep(.el-button + .el-button) { margin-left: 0; }
2026-08-15 09:47:23 +08:00
.summary-item { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
2026-08-17 16:45:46 +08:00
.settlement-summary .summary-item + .summary-item { margin-left: 18px; padding-left: 18px; border-left: 1px solid var(--cc-border, #e3e8ef); }
2026-08-15 09:47:23 +08:00
.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; }
2026-08-13 11:57:44 +08:00
.summary-item strong.is-success { color: #16803c; }
.summary-item strong.is-danger { color: #c2413a; }
2026-08-17 16:45:46 +08:00
.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); }
2026-08-13 12:22:41 +08:00
.settlement-detail-heading, .settlement-detail-actions { display: flex; align-items: center; gap: 10px; min-width: 0; flex-wrap: wrap; }
2026-08-17 12:31:27 +08:00
.settlement-detail-heading h1 { margin: 0; font-size: 20px; line-height: 1.35; letter-spacing: 0; }
2026-08-13 12:22:41 +08:00
.settlement-detail-breadcrumb { color: var(--cc-text-secondary, #64748b); font-size: 12px; }
2026-08-17 16:45:46 +08:00
.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); }
2026-08-17 12:31:27 +08:00
.settlement-detail-summary .summary-item:first-child { padding-left: 0; border-left: 0; }
2026-08-13 12:22:41 +08:00
.summary-item strong.is-date { font-size: 20px; }
2026-08-17 16:45:46 +08:00
.settlement-detail-state { margin: 14px 16px; }
.settlement-detail-content { min-width: 0; padding: 0 16px 12px; overflow: hidden; }
2026-08-17 12:31:27 +08:00
.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; }
2026-08-17 16:45:46 +08:00
.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); }
2026-08-17 12:31:27 +08:00
.partial-summary div:first-child { border-left: 0; }
2026-08-13 12:22:41 +08:00
.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; }
2026-08-17 16:45:46 +08:00
.settlement-operation-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 14px; row-gap: 0; }
2026-08-17 12:31:27 +08:00
.settlement-operation-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
.settlement-operation-form :deep(.el-input-number) { width: 100%; }
2026-08-17 16:45:46 +08:00
.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); }
2026-08-13 12:22:41 +08:00
.remaining-preview strong { color: #b45309; font-size: 16px; }
2026-08-14 14:27:03 +08:00
.operation-error { margin-bottom: 12px; }
.operation-error-meta { margin: -6px 0 12px; }
2026-08-12 09:25:55 +08:00
:deep(.el-select), :deep(.el-date-editor) { width: 100%; }
2026-08-13 11:57:44 +08:00
@media (max-width: 720px) {
2026-08-17 12:31:27 +08:00
.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; }
2026-08-17 16:45:46 +08:00
.settlement-detail-content { padding: 0 16px 14px; }
.settlement-detail-state { margin: 14px; }
.partial-summary { gap: 10px; padding: 12px 10px; }
2026-08-17 12:31:27 +08:00
.partial-summary div { padding: 0; border-left: 0; }
2026-08-17 13:24:55 +08:00
.settlement-list-toolbar { align-items: stretch; flex-direction: column; padding: 0 0 10px; }
2026-08-15 09:47:23 +08:00
.settlement-summary { align-items: flex-start; flex-direction: column; gap: 6px; }
2026-08-17 13:24:55 +08:00
.settlement-summary .summary-item + .summary-item { margin-left: 0; padding-left: 0; border-left: 0; }
.settlement-list-actions { width: 100%; justify-content: flex-start; }
2026-08-13 11:57:44 +08:00
.summary-item strong { font-size: 21px; }
2026-08-17 12:31:27 +08:00
.settlement-detail-header { align-items: flex-start; flex-direction: column; padding: 16px; }
2026-08-13 12:22:41 +08:00
.settlement-detail-actions { width: 100%; }
.settlement-detail-actions :deep(.el-button) { min-height: 40px; }
2026-08-17 12:31:27 +08:00
:deep(.settlement-detail-info .el-descriptions__cell) { display: block; width: 100%; }
2026-08-13 11:57:44 +08:00
}
2026-08-12 09:25:55 +08:00
</style>