This commit is contained in:
2026-08-17 16:45:46 +08:00
parent 4a9438315c
commit d95c90bd1a
48 changed files with 1217 additions and 374 deletions
@@ -66,13 +66,16 @@ const columns: DataTableColumn[] = [
]
const ledgerQueryFields: QueryField[] = [
{ key: 'businessType', type: 'select', label: '流水类型', width: 180, placeholder: '全部类型', options: [
{ label: '费用来源', value: 'FEE_SOURCE' }, { label: '对账单', value: 'RECONCILIATION' }, { label: '结算单', value: 'SETTLEMENT' }, { label: '收付款', value: 'PAYMENT' },
{ label: '纳入结算', value: 'FEE_INCLUDED' }, { label: '排除结算', value: 'FEE_EXCLUDED' },
{ label: '生成对账单', value: 'RECONCILIATION_CREATED' }, { label: '结算单生效', value: 'SETTLEMENT_ISSUED' },
{ label: '收款', value: 'RECEIPT' }, { label: '付款', value: 'PAYMENT' },
{ label: '账务调整', value: 'ADJUSTMENT' }, { label: '作废', value: 'VOID' },
] },
{ key: 'occurredRange', type: 'daterange', label: '发生日期', width: 280 },
]
const ledgerColumns: DataTableColumn[] = [
{ key: 'ledgerId', label: '流水号', minWidth: 160 }, { key: 'occurredAt', label: '发生时间', width: 170, slotName: 'occurredAt' }, { key: 'businessType', label: '业务类型', width: 130, slotName: 'businessType' },
{ key: 'documentNo', label: '关联单据', minWidth: 170 }, { key: 'receivableChange', label: '应收变动(CNY)', width: 145 }, { key: 'payableChange', label: '应付变动(CNY)', width: 145 },
{ key: 'ledgerId', label: '流水号', width: 180, slotName: 'ledgerId' }, { key: 'occurredAt', label: '发生时间', width: 170, slotName: 'occurredAt' }, { key: 'businessType', label: '业务类型', width: 140, slotName: 'businessType' },
{ key: 'documentNo', label: '关联单据', width: 220, slotName: 'documentNo' }, { key: 'receivableChange', label: '应收变动(CNY)', width: 145 }, { key: 'payableChange', label: '应付变动(CNY)', width: 145 },
{ key: 'outstandingReceivableAfter', label: '变动后未收(CNY)', width: 160 }, { key: 'operatorName', label: '操作人', width: 140, slotName: 'operatorName' }, { key: 'remark', label: '备注', minWidth: 180 },
]
const summary = computed(() => rows.value.reduce((result, row) => ({
@@ -153,20 +156,23 @@ function customerName(customerId: string, snapshot?: string) {
}
function ledgerBusinessType(value: unknown) {
const code = String(value || '').trim().toUpperCase()
const known: Record<string, string> = { REC: '收款', RECEIPT: '收款', PAYMENT: '付款', COST: '成本', BILL: '对账单', RECONCILIATION: '对账单', SETTLEMENT: '结算', FEE_SOURCE: '费用来源', ADJUSTMENT: '账务调整' }
const known: Record<string, string> = { REC: '收款', RECONCILIATION: '对账单', FEE_SOURCE: '费用来源' }
return known[code] || financeBusinessTypeLabel(code)
}
function displayOperator(value: unknown) {
const operator = String(value || '').trim()
if (!operator) return '系统'
return /^(?:operator-)?\d+$/i.test(operator) ? '操作人信息缺失' : operator
if (!operator) return '名称数据缺失'
return /^(?:operator-)?\d+$/i.test(operator) ? '名称数据缺失' : operator
}
function formatDateTime(value: unknown) {
const raw = String(value || '').trim()
if (!raw) return '时间暂不可用'
if (!raw) return '时间数据缺失'
const date = new Date(raw)
return Number.isNaN(date.getTime()) ? '时间暂不可用' : date.toLocaleString('zh-CN', { hour12: false })
return Number.isNaN(date.getTime()) ? '时间数据缺失' : date.toLocaleString('zh-CN', { hour12: false })
}
function formatLastChangedAt(value: unknown) { return String(value || '').trim() ? formatDateTime(value) : '暂无变动' }
function displayDocumentNo(value: unknown) { return String(value || '').trim() || '关联单据缺失' }
function displayLedgerId(value: unknown) { return String(value || '').trim() || '流水号缺失' }
function csvCell(value: unknown) { return `"${String(value ?? '').replaceAll('"', '""')}"` }
function downloadCsv(fileName: string, headers: string[], data: unknown[][]) {
const content = `\uFEFF${[headers, ...data].map((line) => line.map(csvCell).join(',')).join('\r\n')}`
@@ -215,7 +221,7 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
<template #toolbar-actions><el-button :disabled="loading || !rows.length" @click="exportAccounts"><Download :size="16" />导出</el-button></template>
<template #actions="{ row }"><div class="row-actions"><el-button link type="primary" @click="openAccount(row, false)"><Eye :size="15" />查看</el-button><el-button v-if="canUpdateTerms" link type="primary" @click="openAccount(row, true)"><Pencil :size="15" />账期</el-button></div></template>
<template #customerName="{ row }">{{ customerName(row.customerId, row.customerName) }}</template>
<template #lastChangedAt="{ row }">{{ formatDateTime(row.lastChangedAt) }}</template>
<template #lastChangedAt="{ row }">{{ formatLastChangedAt(row.lastChangedAt) }}</template>
<template #status="{ row }"><el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'">{{ row.status === 'ACTIVE' ? '正常' : '停用' }}</el-tag></template>
<template #pagination-left><span>汇总:未收 {{ money(summary.receivable) }} · 待出账 {{ money(summary.pending) }} · 本期已收 {{ money(summary.received) }} · 应付 {{ money(summary.payable) }} CNY</span></template>
</DataTable>
@@ -230,7 +236,7 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
<template v-else-if="selected">
<FormSection v-if="!editing" class="ledger-section" title="账户流水" :bordered="false">
<QueryToolbar v-model="ledgerFilters" class="ledger-query" :fields="ledgerQueryFields" ariaLabel="账户流水筛选" preference-key="finance-account-ledger-query" @search="ledgerPage = 1; loadLedger" />
<div class="detail-table-scroll"><DataTable v-model:page="ledgerPage" v-model:page-size="ledgerPageSize" class="ledger-table" :data="ledger" :columns="ledgerColumns" :loading="ledgerLoading" :total="ledgerTotal" row-key="ledgerId" empty-text="暂无账户流水" @update:page="loadLedger()" @update:page-size="ledgerPage = 1; loadLedger()"><template #toolbar-actions><el-button :disabled="!ledger.length" @click="exportLedger"><Download :size="16" />导出流水</el-button></template><template #pagination-left>共 {{ ledgerTotal }} 条</template><template #occurredAt="{ row }">{{ formatDateTime(row.occurredAt) }}</template><template #businessType="{ row }">{{ ledgerBusinessType(row.businessType) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template></DataTable></div>
<div class="detail-table-scroll"><DataTable v-model:page="ledgerPage" v-model:page-size="ledgerPageSize" class="ledger-table" :data="ledger" :columns="ledgerColumns" :loading="ledgerLoading" :total="ledgerTotal" row-key="ledgerId" empty-text="暂无账户流水" @update:page="loadLedger()" @update:page-size="ledgerPage = 1; loadLedger()"><template #toolbar-actions><el-button :disabled="!ledger.length" @click="exportLedger"><Download :size="16" />导出流水</el-button></template><template #pagination-left>共 {{ ledgerTotal }} 条</template><template #ledgerId="{ row }"><span class="ledger-identifier" :title="displayLedgerId(row.ledgerId)">{{ displayLedgerId(row.ledgerId) }}</span></template><template #occurredAt="{ row }">{{ formatDateTime(row.occurredAt) }}</template><template #businessType="{ row }">{{ ledgerBusinessType(row.businessType) }}</template><template #documentNo="{ row }"><span class="ledger-document" :title="displayDocumentNo(row.documentNo)">{{ displayDocumentNo(row.documentNo) }}</span></template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template></DataTable></div>
</FormSection>
<FormSection v-else class="term-section" title="账期设置" :bordered="false"><div class="term-editor"><span class="term-label">账期天数</span><div class="term-control"><el-input-number v-model="creditDays" :min="0" :max="3650" :step="1" /><span class="term-unit">天</span></div></div></FormSection>
</template>
@@ -242,5 +248,264 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
</template>
<style scoped>
.finance-account-page{min-width:0}.finance-account-detail{width:100%;min-width:0}.finance-account-detail__surface{width:min(100%,1600px);margin:0 auto;overflow:hidden}.finance-account-detail__layout{width:100%;margin:0 auto}.finance-account-detail__layout :deep(.detail-page-layout){gap:0}.finance-account-detail__layout :deep(.detail-page-layout__header){min-height:72px;align-items:center;padding:16px 24px;border-bottom:1px solid var(--cc-border,#e3e8ef)}.finance-account-detail__layout :deep(.detail-page-layout__summary){padding:16px 24px 0}.finance-account-detail__layout :deep(.detail-page-layout__content){padding:0 24px 20px}.finance-account-detail__layout :deep(.detail-page-layout__footer){padding:0 24px 24px}.row-actions,.detail-footer,.detail-heading,.term-control{display:flex;align-items:center;gap:8px}.row-actions .el-button{margin-left:0}.detail-heading{width:100%;align-items:center}.detail-title{min-width:0}.detail-heading h2{margin:0;color:var(--cc-text);font-size:20px;line-height:1.4}.detail-heading p{margin:4px 0 0;color:var(--cc-muted);font-size:13px}.detail-back{margin-left:auto}.account-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0;padding:0 0 16px;border-bottom:1px solid var(--cc-border,#e3e8ef)}.account-metric{min-width:0;padding:0 20px}.account-metric:first-child{padding-left:0}.account-metric+.account-metric{border-left:1px solid var(--cc-border,#e3e8ef)}.account-metric::before{display:none}.account-metric span{display:block;color:var(--cc-muted);font-size:12px}.account-metric strong{display:inline-block;margin-top:7px;color:var(--cc-text);font-size:22px;line-height:1;font-variant-numeric:tabular-nums}.account-metric small{margin-left:6px;color:var(--cc-muted);font-size:11px}.ledger-section{padding:16px 0 0}.ledger-query{margin-bottom:12px}:deep(.ledger-query.query-toolbar){padding:0;border:0;background:transparent}.detail-table-scroll{overflow-x:auto}.ledger-table{min-width:0}:deep(.ledger-table .data-table__toolbar.is-compact){min-height:42px;padding:0 0 10px;border-bottom:0}:deep(.ledger-table .data-table__pagination){min-height:48px;padding:8px 12px}.term-section{width:100%;padding:16px 0}.term-editor{display:grid;grid-template-columns:120px minmax(0,220px);justify-content:start;align-items:center;gap:24px;padding:4px 0}.term-label{color:var(--cc-text);font-size:14px;font-weight:600}.term-control :deep(.el-input-number){width:220px}.term-unit{color:var(--cc-muted)}.detail-footer{width:100%;justify-content:flex-end;margin-top:-4px;padding-top:14px;border-top:0}@media(max-width:1080px){.account-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.account-metric:nth-child(3){padding-left:0;border-left:0}.finance-account-detail__surface{width:100%}}@media(max-width:720px){.detail-heading{align-items:stretch;flex-direction:column}.detail-back{margin-left:0}.detail-footer .el-button{min-height:44px}.term-editor{grid-template-columns:1fr}.account-metrics{grid-template-columns:1fr;gap:12px}.account-metric,.account-metric:first-child,.account-metric:nth-child(3){padding-left:0}.account-metric+.account-metric{border-left:0}}@media(max-width:480px){.detail-footer .el-button{flex:1 1 120px}}
.finance-account-page {
min-width: 0;
}
.finance-account-detail {
width: 100%;
min-width: 0;
}
.finance-account-detail__surface {
width: min(100%, 1920px);
margin: 0 auto;
overflow: hidden;
}
.finance-account-detail__layout {
width: 100%;
margin: 0 auto;
}
.finance-account-detail__layout :deep(.detail-page-layout) {
gap: 0;
}
.finance-account-detail__layout :deep(.detail-page-layout__header) {
min-height: 64px;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--cc-border, #e3e8ef);
}
.finance-account-detail__layout :deep(.detail-page-layout__summary) {
padding: 10px 16px 0;
}
.finance-account-detail__layout :deep(.detail-page-layout__content) {
padding: 0 16px 12px;
}
.finance-account-detail__layout :deep(.detail-page-layout__footer) {
padding: 0 16px 12px;
}
.row-actions,
.detail-footer,
.detail-heading,
.term-control {
display: flex;
align-items: center;
gap: 8px;
}
.row-actions .el-button {
margin-left: 0;
}
.detail-heading {
width: 100%;
align-items: center;
}
.detail-title {
min-width: 0;
}
.detail-heading h2 {
margin: 0;
color: var(--cc-text);
font-size: 20px;
line-height: 1.4;
}
.detail-heading p {
margin: 3px 0 0;
color: var(--cc-muted);
font-size: 13px;
}
.detail-back {
margin-left: auto;
}
.account-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0;
padding: 0 0 12px;
border-bottom: 1px solid var(--cc-border, #e3e8ef);
}
.account-metric {
min-width: 0;
padding: 0 12px;
}
.account-metric:first-child {
padding-left: 0;
}
.account-metric + .account-metric {
border-left: 1px solid var(--cc-border, #e3e8ef);
}
.account-metric::before {
display: none;
}
.account-metric span {
display: block;
color: var(--cc-muted);
font-size: 12px;
}
.account-metric strong {
display: inline-block;
margin-top: 6px;
color: var(--cc-text);
font-size: 20px;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.account-metric small {
margin-left: 6px;
color: var(--cc-muted);
font-size: 11px;
}
.ledger-section {
padding: 10px 0 0;
}
.ledger-query {
margin-bottom: 8px;
}
:deep(.ledger-query.query-toolbar) {
padding: 0;
border: 0;
background: transparent;
}
.detail-table-scroll {
overflow-x: auto;
}
.ledger-table {
min-width: 0;
}
.ledger-identifier,
.ledger-document {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
:deep(.ledger-table .data-table__toolbar.is-compact) {
min-height: 36px;
padding: 0 0 6px;
border-bottom: 0;
}
:deep(.ledger-table .data-table__pagination) {
min-height: 40px;
padding: 6px 10px;
}
.term-section {
width: 100%;
padding: 10px 0;
}
.term-editor {
display: grid;
grid-template-columns: 120px minmax(0, 220px);
justify-content: start;
align-items: center;
gap: 14px;
padding: 2px 0;
}
.term-label {
color: var(--cc-text);
font-size: 14px;
font-weight: 600;
}
.term-control :deep(.el-input-number) {
width: 220px;
}
.term-unit {
color: var(--cc-muted);
}
.detail-footer {
width: 100%;
justify-content: flex-end;
margin-top: 0;
padding-top: 8px;
border-top: 0;
}
@media (max-width: 1080px) {
.account-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.account-metric:nth-child(3) {
padding-left: 0;
border-left: 0;
}
.finance-account-detail__surface {
width: 100%;
}
}
@media (max-width: 720px) {
.detail-heading {
align-items: stretch;
flex-direction: column;
}
.detail-back {
margin-left: 0;
}
.detail-footer .el-button {
min-height: 44px;
}
.term-editor {
grid-template-columns: 1fr;
}
.account-metrics {
grid-template-columns: 1fr;
gap: 12px;
}
.account-metric,
.account-metric:first-child,
.account-metric:nth-child(3) {
padding-left: 0;
}
.account-metric + .account-metric {
border-left: 0;
}
}
@media (max-width: 480px) {
.detail-footer .el-button {
flex: 1 1 120px;
}
}
</style>