优化
This commit is contained in:
@@ -49,9 +49,11 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
it('keeps term editing aligned with the account summary and nearby actions', () => {
|
||||
for (const marker of ['class="term-section"', 'class="term-editor"', '账期天数', '#footer', '保存账期']) expect(page).toContain(marker)
|
||||
expect(page).toContain('grid-template-columns:repeat(4,minmax(0,1fr))')
|
||||
expect(page).toContain('grid-template-columns:minmax(180px,1fr) auto')
|
||||
expect(page).toContain('grid-template-columns:120px minmax(0,220px)')
|
||||
expect(page).toContain('justify-content:start')
|
||||
expect(page).toContain('class="surface-panel finance-account-detail__surface"')
|
||||
expect(page).toContain('min-height:88px')
|
||||
expect(page).toContain('min-height:72px')
|
||||
expect(page).toContain('gap:0;padding:0 0 16px')
|
||||
expect(page).toContain('class="detail-table-scroll"')
|
||||
})
|
||||
|
||||
|
||||
@@ -228,11 +228,11 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
|
||||
<template v-if="selected && !ledgerError" #summary><section class="account-metrics" aria-label="客户账户汇总"><div class="account-metric is-receivable"><span>待收金额</span><strong>{{ money(selected.outstandingReceivable) }}</strong><small>CNY</small></div><div class="account-metric is-pending"><span>待开账金额</span><strong>{{ money(selected.pendingBilling) }}</strong><small>CNY</small></div><div class="account-metric is-received"><span>本期已收</span><strong>{{ money(selected.receivedInPeriod) }}</strong><small>CNY</small></div><div class="account-metric is-payable"><span>待付金额</span><strong>{{ money(selected.outstandingPayable) }}</strong><small>CNY</small></div></section></template>
|
||||
<LoadErrorState v-if="ledgerError" :title="editing ? '客户账期加载失败' : '账户流水加载失败'" :message="ledgerError.message" :loading="ledgerLoading" @retry="retryDrawer"><template #details><ApiErrorMeta :details="ledgerError" /></template></LoadErrorState>
|
||||
<template v-else-if="selected">
|
||||
<FormSection v-if="!editing" class="ledger-section" title="账户流水" description="展示应收、应付及每次变动后的未收金额。" :bordered="false">
|
||||
<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>
|
||||
</FormSection>
|
||||
<FormSection v-else class="term-section" title="账期设置" description="到期日按账单日期加客户账期计算;每个客户独立设置,默认 30 天。" :bordered="false"><div class="term-editor"><div><span class="term-label">账期天数</span><p>请输入 0 至 3650 天</p></div><div class="term-control"><el-input-number v-model="creditDays" :min="0" :max="3650" :step="1" /><span class="term-unit">天</span></div></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>
|
||||
<template v-if="editing && selected && !ledgerError" #footer><div class="detail-footer"><el-button @click="closeDetail">关闭</el-button><el-button v-if="canUpdateTerms" type="primary" :loading="saving" @click="saveTerms"><Save :size="16" />保存账期</el-button></div></template>
|
||||
</DetailPageLayout>
|
||||
@@ -242,5 +242,5 @@ 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__header){min-height:88px;align-items:center;padding:20px 24px;border-bottom:1px solid var(--cc-border,#e3e8ef)}.finance-account-detail__layout :deep(.detail-page-layout__summary){padding:20px 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:12px}.account-metric{position:relative;min-width:0;padding:16px 18px;border:1px solid var(--cc-border,#e3e8ef);border-radius:6px;background:#fff;overflow:hidden}.account-metric::before{position:absolute;top:0;right:0;left:0;height:3px;background:#64748b;content:""}.account-metric.is-pending::before{background:#d97706}.account-metric.is-received::before{background:#16a34a}.account-metric.is-payable::before{background:#dc2626}.account-metric span{display:block;color:var(--cc-muted);font-size:12px}.account-metric strong{display:inline-block;margin-top:9px;color:var(--cc-text);font-size:25px;line-height:1;font-variant-numeric:tabular-nums}.account-metric small{margin-left:6px;color:var(--cc-muted);font-size:11px}.ledger-section{padding:18px;border-top:1px solid var(--cc-border,#e3e8ef);border-bottom:1px solid var(--cc-border,#e3e8ef);background:#fff}.ledger-query{margin-bottom:10px}.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:20px;border:1px solid var(--cc-border,#e3e8ef);border-radius:6px;background:#fff}.term-editor{display:grid;grid-template-columns:minmax(180px,1fr) auto;align-items:center;gap:24px;padding:4px 0}.term-label{color:var(--cc-text);font-size:14px;font-weight:600}.term-editor p{margin:5px 0 0;color:var(--cc-muted);font-size:12px}.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))}.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}}@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%,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}}
|
||||
</style>
|
||||
|
||||
@@ -267,7 +267,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
<template #header><div class="detail-heading"><el-button @click="closeDetail"><ArrowLeft :size="16" />返回客户应收应付</el-button><div class="detail-title"><h2>费用来源详情</h2><p>{{ selected?.documentNo || '费用来源' }} · {{ selected ? customerName(selected.customerId, selected.customerName) : '客户名称暂不可用' }}</p></div><div class="detail-actions"><el-button v-if="canReviewSource && selected" @click="openReview(selected as unknown as Record<string, unknown>)"><FileCheck2 :size="16" />审核</el-button><el-button v-if="canSettleSource && selected" @click="openInclusion(selected as unknown as Record<string, unknown>)"><Link2 :size="16" />参与结算</el-button></div></div></template>
|
||||
<template v-if="detail" #summary><section class="arap-metrics" aria-label="费用来源概览"><div class="arap-metric is-amount"><span>费用金额</span><strong>{{ money(detail.amount) }}</strong><small>CNY</small></div><div class="arap-metric"><span>收付方向</span><strong>{{ financeDirectionLabel(detail.direction) }}</strong></div><div class="arap-metric"><span>参与结算</span><strong>{{ detail.settlementIncluded ? '是' : '否' }}</strong></div><div class="arap-metric"><span>审核状态</span><strong>{{ statusLabel(detail.reviewStatus) }}</strong></div></section></template>
|
||||
<LoadErrorState v-if="detailError" title="费用来源详情加载失败" :message="detailError.message" :loading="detailLoading" @retry="selected && loadDetail(selected)"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
<template v-else-if="detail"><div class="arap-detail-grid"><FormSection class="arap-information" title="单据信息" description="费用来源与关联业务摘要" :bordered="false"><el-descriptions :column="2" class="arap-detail"><el-descriptions-item label="客户">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</el-descriptions-item><el-descriptions-item label="费用项目">{{ displayFeeName(detail.feeItemName) }}</el-descriptions-item><el-descriptions-item label="来源类型">{{ sourceTypeDisplay(detail.sourceType) }}</el-descriptions-item><el-descriptions-item label="关联单据">{{ detail.inboundCode || detail.containerNo || '—' }}</el-descriptions-item><el-descriptions-item label="业务日期" :span="2">{{ formatDateTime(detail.businessDate) }}</el-descriptions-item></el-descriptions></FormSection><FormSection class="arap-settlement" title="审核与结算" description="当前审核、参与结算和出账状态" :bordered="false"><el-descriptions :column="1" class="arap-detail"><el-descriptions-item label="费用状态">{{ statusLabel(detail.sourceStatus) }}</el-descriptions-item><el-descriptions-item label="已生成对账单">{{ detail.statementGenerated ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="审核原因">{{ detail.reviewReason || '-' }}</el-descriptions-item><el-descriptions-item label="结算调整原因">{{ detail.settlementReason || '-' }}</el-descriptions-item></el-descriptions></FormSection></div><FormSection class="operation-log" title="操作记录" description="按时间查看审核与结算调整记录" :bordered="false"><el-timeline v-if="detail.operationLogs?.length"><el-timeline-item v-for="(log, index) in detail.operationLogs" :key="`${log.occurredAt}-${index}`" :timestamp="formatDateTime(log.occurredAt)">{{ financeOperationActionLabel(log.action) }} · {{ displayOperator(log.operatorName) }}<small>{{ log.remark || financeOperationResultLabel(log.result) }}</small></el-timeline-item></el-timeline><el-empty v-else description="暂无操作记录" /></FormSection></template>
|
||||
<template v-else-if="detail"><div class="arap-detail-grid"><FormSection class="arap-information" title="单据信息" :bordered="false"><el-descriptions :column="2" class="arap-detail"><el-descriptions-item label="客户">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</el-descriptions-item><el-descriptions-item label="费用项目">{{ displayFeeName(detail.feeItemName) }}</el-descriptions-item><el-descriptions-item label="来源类型">{{ sourceTypeDisplay(detail.sourceType) }}</el-descriptions-item><el-descriptions-item label="关联单据">{{ detail.inboundCode || detail.containerNo || '—' }}</el-descriptions-item><el-descriptions-item label="业务日期" :span="2">{{ formatDateTime(detail.businessDate) }}</el-descriptions-item></el-descriptions></FormSection><FormSection class="arap-settlement" title="审核与结算" :bordered="false"><el-descriptions :column="1" class="arap-detail"><el-descriptions-item label="费用状态">{{ statusLabel(detail.sourceStatus) }}</el-descriptions-item><el-descriptions-item label="已生成对账单">{{ detail.statementGenerated ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="审核原因">{{ detail.reviewReason || '-' }}</el-descriptions-item><el-descriptions-item label="结算调整原因">{{ detail.settlementReason || '-' }}</el-descriptions-item></el-descriptions></FormSection></div><FormSection class="operation-log" title="操作记录" :bordered="false"><el-timeline v-if="detail.operationLogs?.length"><el-timeline-item v-for="(log, index) in detail.operationLogs" :key="`${log.occurredAt}-${index}`" :timestamp="formatDateTime(log.occurredAt)">{{ financeOperationActionLabel(log.action) }} · {{ displayOperator(log.operatorName) }}<small>{{ log.remark || financeOperationResultLabel(log.result) }}</small></el-timeline-item></el-timeline><el-empty v-else description="暂无操作记录" /></FormSection></template>
|
||||
</DetailPageLayout>
|
||||
</section>
|
||||
</AdminPage>
|
||||
@@ -291,8 +291,9 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
.finance-arap-detail { width: 100%; min-width: 0; }
|
||||
.finance-arap-detail__surface { width: min(100%, 1600px); margin: 0 auto; overflow: hidden; }
|
||||
.finance-arap-detail__layout { width: 100%; margin: 0 auto; }
|
||||
.finance-arap-detail__layout :deep(.detail-page-layout__header) { min-height: 88px; align-items: center; padding: 20px 24px; border-bottom: 1px solid var(--cc-border, #e3e8ef); }
|
||||
.finance-arap-detail__layout :deep(.detail-page-layout__summary) { padding: 20px 24px 0; }
|
||||
.finance-arap-detail__layout :deep(.detail-page-layout) { gap: 0; }
|
||||
.finance-arap-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-arap-detail__layout :deep(.detail-page-layout__summary) { padding: 16px 24px 0; }
|
||||
.finance-arap-detail__layout :deep(.detail-page-layout__content) { padding: 0 24px 20px; }
|
||||
.row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
|
||||
.row-actions .el-button { margin-left: 0; }
|
||||
@@ -303,22 +304,24 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
.detail-heading p { overflow: hidden; margin: 4px 0 0; color: var(--cc-muted); text-overflow: ellipsis; white-space: nowrap; }
|
||||
.detail-actions { margin-left: auto; }
|
||||
.muted-action { color: var(--cc-muted); }
|
||||
.arap-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
|
||||
.arap-metric { min-width: 0; padding: 16px 18px; border: 1px solid var(--cc-border, #e3e8ef); border-radius: 6px; background: #fff; }
|
||||
.arap-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); }
|
||||
.arap-metric { min-width: 0; padding: 0 20px; }
|
||||
.arap-metric:first-child { padding-left: 0; }
|
||||
.arap-metric + .arap-metric { border-left: 1px solid var(--cc-border, #e3e8ef); }
|
||||
.arap-metric span { display: block; color: var(--cc-muted); font-size: 12px; }
|
||||
.arap-metric strong { display: inline-block; margin-top: 9px; color: var(--cc-text); font-size: 23px; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
||||
.arap-metric strong { display: inline-block; margin-top: 7px; color: var(--cc-text); font-size: 20px; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
||||
.arap-metric small { margin-left: 6px; color: var(--cc-muted); font-size: 11px; }
|
||||
.arap-metric.is-amount { border-color: #c7d2fe; background: #f7f8ff; }
|
||||
.arap-metric.is-amount strong { color: #4f46e5; }
|
||||
.arap-detail-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(360px, .65fr); gap: 14px; }
|
||||
.arap-information,.arap-settlement,.operation-log { padding: 20px; border: 1px solid var(--cc-border, #e3e8ef); background: #fff; }
|
||||
.arap-detail-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(360px, .65fr); gap: 24px; border-bottom: 1px solid var(--cc-border, #e3e8ef); }
|
||||
.arap-information,.arap-settlement,.operation-log { padding: 16px 0; }
|
||||
.arap-settlement { padding-left: 24px; border-left: 1px solid var(--cc-border, #e3e8ef); }
|
||||
.operation-log { min-height: 150px; }
|
||||
:deep(.arap-detail .el-descriptions__content) { min-width: 0; overflow-wrap: anywhere; }
|
||||
:deep(.arap-detail .el-descriptions__label) { width: 112px; color: var(--cc-muted); }
|
||||
.operation-log :deep(.el-empty) { padding: 12px 0 4px; }
|
||||
.operation-log :deep(.el-empty__image) { display: none; }
|
||||
.operation-log small { display: block; margin-top: 4px; color: var(--cc-muted); }
|
||||
@media (max-width: 1080px) { .finance-arap-detail__surface { width: 100%; } .arap-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } .arap-detail-grid { grid-template-columns: 1fr; } }
|
||||
@media (max-width: 720px) { .detail-heading { align-items: stretch; flex-direction: column; } .detail-actions { margin-left: 0; } .arap-metrics { grid-template-columns: 1fr; } :deep(.arap-detail .el-descriptions__body .el-descriptions__table) { table-layout: fixed; } }
|
||||
@media (max-width: 1080px) { .finance-arap-detail__surface { width: 100%; } .arap-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } .arap-metric:nth-child(3) { padding-left: 0; border-left: 0; } .arap-detail-grid { grid-template-columns: 1fr; gap: 0; } .arap-settlement { padding-left: 0; border-left: 0; border-top: 1px solid var(--cc-border, #e3e8ef); } }
|
||||
@media (max-width: 720px) { .detail-heading { align-items: stretch; flex-direction: column; } .detail-actions { margin-left: 0; } .arap-metrics { grid-template-columns: 1fr; gap: 12px; } .arap-metric,.arap-metric:first-child,.arap-metric:nth-child(3) { padding-left: 0; } .arap-metric + .arap-metric { border-left: 0; } :deep(.arap-detail .el-descriptions__body .el-descriptions__table) { table-layout: fixed; } }
|
||||
@media (max-width: 480px) { :deep(.arap-detail .el-descriptions__cell) { display: block; width: 100%; } }
|
||||
</style>
|
||||
|
||||
@@ -457,7 +457,7 @@ onMounted(() => { if (canListCustomers.value) void loadCustomers(); void load()
|
||||
<div><span>客户对账单</span><h2>{{ detail.billNo }}</h2><p>{{ customerDisplayName(detail.customerId, detail.customerName) }} · {{ detail.periodStart || '—' }} 至 {{ detail.periodEnd || '—' }}</p></div>
|
||||
<el-tag :type="financeStatusTone(detail.status)">{{ financeStatusLabel(detail.status) }}</el-tag>
|
||||
</div>
|
||||
<FormSection class="statement-summary-section" title="对账单概要" description="核对账期、金额和结清状态后查看下方业务明细。" aria-label="所选对账单摘要">
|
||||
<FormSection class="statement-summary-section" title="对账单概要" aria-label="所选对账单摘要">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="总应收金额">{{ detail.totalAmount.toFixed(2) }} CNY</el-descriptions-item>
|
||||
<el-descriptions-item label="已收金额">{{ detail.paidAmount.toFixed(2) }} CNY</el-descriptions-item>
|
||||
@@ -473,7 +473,7 @@ onMounted(() => { if (canListCustomers.value) void loadCustomers(); void load()
|
||||
<el-tab-pane label="账单明细表" name="lines">
|
||||
<LoadErrorState v-if="detailError" title="明细加载失败" :message="detailError.message" :loading="detailLoading" @retry="retrySelectedDetail"><template #details><ApiErrorMeta :details="detailError" /></template></LoadErrorState>
|
||||
<el-skeleton v-else-if="detailLoading" :rows="4" animated />
|
||||
<FormSection v-else-if="detail" title="账单明细" description="展示当前对账单版本中的业务与计费明细。" :bordered="false">
|
||||
<FormSection v-else-if="detail" title="账单明细" :bordered="false">
|
||||
<div class="detail-table-scroll">
|
||||
<DataTable class="statement-detail-table statement-line-table" :data="detail.lines" size="small" border stripe :columns="detailLineColumns" column-settings-mode="hide" row-key="statementLineId" empty-text="该对账单暂无账单明细" aria-label="客户对账单账单明细">
|
||||
<template #inboundCode="{ row }">{{ lineSourceCache.get(row.sourceFeeId)?.inboundCode || row.inboundId || '—' }}</template>
|
||||
@@ -491,7 +491,7 @@ onMounted(() => { if (canListCustomers.value) void loadCustomers(); void load()
|
||||
<el-empty v-else description="请在主表中选择一张对账单" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="账单支付历史表" name="payments">
|
||||
<FormSection v-if="detail" title="支付历史" description="只读展示当前对账单关联的有效收款记录。" :bordered="false">
|
||||
<FormSection v-if="detail" title="支付历史" :bordered="false">
|
||||
<div class="detail-table-scroll">
|
||||
<DataTable class="statement-detail-table statement-payment-table" :data="detail.paymentHistory" size="small" border stripe :columns="paymentColumns" column-settings-mode="hide" row-key="transactionId" empty-text="该对账单暂无有效收款记录" aria-label="客户对账单支付历史">
|
||||
<template #customerName>{{ customerDisplayName(detail.customerId, detail.customerName) }}</template>
|
||||
@@ -523,15 +523,16 @@ onMounted(() => { if (canListCustomers.value) void loadCustomers(); void load()
|
||||
.finance-statements-page { min-width: 0; }
|
||||
:deep(.el-select), :deep(.el-date-editor) { width: 100%; }
|
||||
.statement-detail-panel { min-width: 0; overflow: hidden; }
|
||||
.statement-detail-header { min-height: 56px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 24px; border-bottom: 1px solid var(--cc-border-subtle); }
|
||||
.statement-detail-panel :deep(.detail-page-layout) { gap: 0; }
|
||||
.statement-detail-header { min-height: 48px; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 0 24px; border-bottom: 1px solid var(--cc-border-subtle); }
|
||||
.statement-detail-context { display: grid; min-width: 0; gap: 3px; }
|
||||
.statement-detail-context span { color: var(--cc-text-secondary); font-size: 11px; }
|
||||
.statement-detail-context strong { overflow: hidden; color: var(--cc-text-primary); font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.statement-detail-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 12px; padding: 20px 24px; border-bottom: 1px solid var(--cc-border-subtle); }
|
||||
.statement-detail-head { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 12px; padding: 16px 24px; border-bottom: 1px solid var(--cc-border-subtle); }
|
||||
.statement-detail-head > div > span { color: var(--cc-primary); font-size: 11px; font-weight: 700; }
|
||||
.statement-detail-head h2 { margin: 2px 0 0; font-size: 17px; line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.statement-detail-head h2 { margin: 2px 0 0; font-size: 16px; line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.statement-detail-head p { margin: 5px 0 0; color: var(--cc-text-secondary); font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
|
||||
.statement-summary-section { padding: 20px 24px; border-bottom: 1px solid var(--cc-border-subtle); }
|
||||
.statement-summary-section { padding: 16px 24px; border-bottom: 1px solid var(--cc-border-subtle); }
|
||||
.statement-outstanding { color: var(--el-color-warning-dark-2); }
|
||||
.statement-detail-tabs { min-width: 0; }
|
||||
.statement-detail-table { min-width: 0; }
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceFormalSettlementPage.vue'), 'utf8')
|
||||
|
||||
describe('Finance formal settlement desktop presentation', () => {
|
||||
it('keeps settlement totals and list actions in one compact toolbar', () => {
|
||||
expect(source).toContain('<section class="settlement-list-toolbar" aria-label="结算汇总与列表操作">')
|
||||
expect(source).toContain('<div class="settlement-summary" aria-label="结算汇总">')
|
||||
expect(source).toContain('<div class="settlement-list-actions">')
|
||||
expect(source).toContain('.settlement-list-toolbar { display: flex; min-height: 54px;')
|
||||
expect(source).not.toContain('<template #toolbar-actions>')
|
||||
})
|
||||
|
||||
it('uses a continuous detail layout with locally scrollable wide tables', () => {
|
||||
expect(source).toContain('<DetailPageLayout v-else-if="detailMode" class="settlement-detail-page surface-panel"')
|
||||
expect(source).toContain('class="settlement-detail-content"')
|
||||
expect(source).toContain('class="detail-table-scroll"')
|
||||
expect(source).toContain(':deep(.detail-table-scroll .el-table) { min-width: 920px; }')
|
||||
expect(source).not.toContain('class="settlement-detail-card"')
|
||||
})
|
||||
|
||||
it('presents fee sources and revisions as finance business labels', () => {
|
||||
expect(source).toContain('financeSourceTypeLabel(scope.row.sourceType)')
|
||||
expect(source).not.toContain('prop="sourceVersion" label="来源版本"')
|
||||
expect(source).toContain("VOID: '作废'")
|
||||
expect(source).toContain("REOPEN: '重开'")
|
||||
expect(source).toContain("RECEIPT: '收款登记'")
|
||||
expect(source).toContain("PAYMENT: '付款登记'")
|
||||
expect(source).toContain('settlementRevisionActionLabel(scope.row.action)')
|
||||
})
|
||||
|
||||
it('keeps receipt and payment dialogs readable at desktop width', () => {
|
||||
expect(source.match(/width="min\(760px, 94vw\)"/g)).toHaveLength(2)
|
||||
expect(source).toContain('.partial-summary { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));')
|
||||
expect(source).toContain('.settlement-operation-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));')
|
||||
expect(source).toContain('<el-alert v-if="receiptValidationError"')
|
||||
expect(source).toContain('<el-alert v-if="receiptError"')
|
||||
})
|
||||
})
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
listFinanceStatements,
|
||||
uploadFinanceVoucher,
|
||||
} from '../api/finance'
|
||||
import { financeStatusLabel, financeStatusTone } from '../status'
|
||||
import { financeOperationActionLabel, financeSourceTypeLabel, financeStatusLabel, financeStatusTone } from '../status'
|
||||
import type {
|
||||
FinanceFeePoolItem,
|
||||
FinancePayableSettlement,
|
||||
@@ -136,6 +136,20 @@ const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
|
||||
|
||||
function money(value: number) { return `¥${Number(value || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` }
|
||||
function displayDate(value?: string) { return value || '—' }
|
||||
function displayDateTime(value?: string) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
|
||||
function 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) }
|
||||
@@ -417,11 +431,10 @@ onMounted(() => {
|
||||
<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" />
|
||||
<el-table-column prop="sourceType" label="来源类型" min-width="140"><template #default="scope">{{ financeSourceTypeLabel(scope.row.sourceType) }}</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="sourceVersion" label="来源版本" width="100" />
|
||||
<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>
|
||||
@@ -440,11 +453,11 @@ onMounted(() => {
|
||||
<FormSection 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" />
|
||||
<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 prop="occurredAt" label="时间" min-width="160" />
|
||||
<el-table-column prop="occurredAt" label="时间" min-width="160"><template #default="scope">{{ displayDateTime(scope.row.occurredAt) }}</template></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</FormSection>
|
||||
@@ -458,17 +471,19 @@ onMounted(() => {
|
||||
<LoadErrorState :title="`${title}加载失败`" :message="(customerError || error)?.message || ''" :loading="loading" @retry="customerError ? loadCustomers() : load()"><template #details><ApiErrorMeta :details="(customerError || error)!" /></template></LoadErrorState>
|
||||
</template>
|
||||
<template v-if="!customerError && !error">
|
||||
<section 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>
|
||||
<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>
|
||||
</section>
|
||||
<section class="surface-panel table-panel">
|
||||
<DataTable :data="filteredRows" :columns="columns" :loading="loading" :aria-label="`${title}列表`" row-key="id" empty-text="暂无符合条件的结算单">
|
||||
<template #toolbar-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>
|
||||
</template>
|
||||
<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>
|
||||
@@ -518,8 +533,13 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.finance-settlement-page { min-width: 0; }
|
||||
.settlement-summary { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px 28px; padding: 0 2px; }
|
||||
.settlement-list-toolbar { display: flex; min-height: 54px; align-items: center; justify-content: space-between; gap: 20px; padding: 0 2px 10px; }
|
||||
.settlement-summary, .settlement-list-actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; }
|
||||
.settlement-summary { flex: 1; gap: 8px 0; }
|
||||
.settlement-list-actions { flex: 0 0 auto; justify-content: flex-end; gap: 8px; }
|
||||
.settlement-list-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.summary-item { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
||||
.settlement-summary .summary-item + .summary-item { margin-left: 24px; padding-left: 24px; border-left: 1px solid var(--cc-border, #e3e8ef); }
|
||||
.summary-item span { color: var(--cc-text-secondary, #64748b); font-size: 13px; }
|
||||
.summary-item strong { color: var(--cc-text-primary, #1f2937); font-size: 18px; line-height: 1.2; }
|
||||
.summary-item strong.is-success { color: #16803c; }
|
||||
@@ -564,7 +584,10 @@ onMounted(() => {
|
||||
.settlement-detail-state { margin: 16px; }
|
||||
.partial-summary { gap: 10px; padding: 12px; }
|
||||
.partial-summary div { padding: 0; border-left: 0; }
|
||||
.settlement-list-toolbar { align-items: stretch; flex-direction: column; padding: 0 0 10px; }
|
||||
.settlement-summary { align-items: flex-start; flex-direction: column; gap: 6px; }
|
||||
.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%; }
|
||||
|
||||
@@ -46,6 +46,22 @@ describe('finance presentation governance', () => {
|
||||
expect(settlement).toContain('grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 16px')
|
||||
})
|
||||
|
||||
it('keeps settlement totals and list actions in one compact toolbar', () => {
|
||||
expect(settlement).toContain('class="settlement-list-toolbar"')
|
||||
expect(settlement).toContain('class="settlement-summary" aria-label="结算汇总"')
|
||||
expect(settlement).toContain('class="settlement-list-actions"')
|
||||
expect(settlement).toContain('min-height: 54px; align-items: center; justify-content: space-between')
|
||||
expect(settlement).toContain('.settlement-summary .summary-item + .summary-item')
|
||||
expect(settlement).not.toContain('<template #toolbar-actions>')
|
||||
})
|
||||
|
||||
it('keeps settlement details focused on finance-facing labels and fields', () => {
|
||||
expect(settlement).toContain('financeSourceTypeLabel(scope.row.sourceType)')
|
||||
expect(settlement).toContain('financeOperationActionLabel(scope.row.action)')
|
||||
expect(settlement).toContain('displayDateTime(scope.row.occurredAt)')
|
||||
expect(settlement).not.toContain('label="来源版本"')
|
||||
})
|
||||
|
||||
it('uses optional all-customer filters without a customer-selection blocker', () => {
|
||||
for (const source of [settlement, statements, arap]) {
|
||||
expect(source).toContain("placeholder: '全部客户'")
|
||||
@@ -97,4 +113,33 @@ describe('finance presentation governance', () => {
|
||||
expect(statements).toContain(':data="detail.revisions" size="small" border stripe')
|
||||
expect(statements).toContain(':data="adjustLines" size="small" max-height="420" border stripe')
|
||||
})
|
||||
|
||||
it('keeps the statement detail as a compact, continuous list-linked surface', () => {
|
||||
expect(statements).toContain('class="surface-panel statement-detail-panel"')
|
||||
expect(statements).toContain('@row-click="openDetail"')
|
||||
expect(statements).toContain('.statement-detail-panel :deep(.detail-page-layout) { gap: 0; }')
|
||||
expect(statements).toContain('min-height: 48px')
|
||||
expect(statements).not.toContain('核对账期、金额和结清状态后查看下方业务明细。')
|
||||
expect(statements).not.toContain('展示当前对账单版本中的业务与计费明细。')
|
||||
expect(statements).not.toContain('只读展示当前对账单关联的有效收款记录。')
|
||||
})
|
||||
|
||||
it('uses a compact financial summary row for customer accounts', () => {
|
||||
expect(accounts).toContain('.finance-account-detail__layout :deep(.detail-page-layout){gap:0}')
|
||||
expect(accounts).toContain('.account-metric+.account-metric{border-left:1px solid')
|
||||
expect(accounts).toContain('.account-metric::before{display:none}')
|
||||
expect(accounts).toContain(':deep(.ledger-query.query-toolbar){padding:0;border:0;background:transparent}')
|
||||
expect(accounts).not.toContain('展示应收、应付及每次变动后的未收金额。')
|
||||
expect(accounts).not.toContain('到期日按账单日期加客户账期计算;每个客户独立设置,默认 30 天。')
|
||||
})
|
||||
|
||||
it('keeps fee-source details continuous without nested metric and section cards', () => {
|
||||
expect(arap).toContain('.finance-arap-detail__layout :deep(.detail-page-layout) { gap: 0; }')
|
||||
expect(arap).toContain('.arap-metric + .arap-metric { border-left: 1px solid')
|
||||
expect(arap).toContain('.arap-settlement { padding-left: 24px; border-left: 1px solid')
|
||||
expect(arap).not.toContain('.arap-metric.is-amount { border-color:')
|
||||
expect(arap).not.toContain('description="费用来源与关联业务摘要"')
|
||||
expect(arap).not.toContain('description="当前审核、参与结算和出账状态"')
|
||||
expect(arap).not.toContain('description="按时间查看审核与结算调整记录"')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@ export function financeStatusTone(status: unknown): FinanceStatusTone {
|
||||
}
|
||||
|
||||
const financeSourceTypeLabels: Record<string, string> = {
|
||||
INBOUND_FEE: '入库费用', ORDER_FEE: '订单费用', CONTAINER_COST: '整柜成本', MANUAL_COST: '人工成本', STORAGE_FEE: '仓储费用',
|
||||
INBOUND_FEE: '入库费用', ORDER_FEE: '订单费用', CONTAINER_FEE: '装柜费用', CONTAINER_COST: '整柜成本', MANUAL_COST: '人工成本', STORAGE_FEE: '仓储费用',
|
||||
}
|
||||
const financeBusinessTypeLabels: Record<string, string> = {
|
||||
BILL: '对账单', RECEIPT: '收款', COST: '成本', ADJUSTMENT: '账务调整', SETTLEMENT: '结算',
|
||||
|
||||
Reference in New Issue
Block a user