优化
This commit is contained in:
@@ -80,7 +80,8 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
const wrapper = mount(FinanceCustomerAccountsView, { global: { plugins: [pinia, ElementPlus, router] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('客户A · C-001')
|
||||
expect(wrapper.text()).toContain('客户A')
|
||||
expect(wrapper.text()).not.toContain('C-001')
|
||||
expect(listFinanceAccounts).toHaveBeenCalledWith({ page: 1, size: 200 })
|
||||
expect(listFinanceAccountLedger).toHaveBeenCalledWith('10001', expect.objectContaining({ page: 1, size: 20 }))
|
||||
wrapper.unmount()
|
||||
|
||||
@@ -56,9 +56,6 @@ const queryFields = computed<QueryField[]>(() => [
|
||||
const columns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 150, fixed: 'left', slotName: 'actions' },
|
||||
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' },
|
||||
{ key: 'customerCode', label: '客户编号', width: 150 },
|
||||
{ key: 'contactName', label: '联系人', width: 120 },
|
||||
{ key: 'contactPhone', label: '联系电话', width: 140 },
|
||||
{ key: 'outstandingReceivable', label: '待收(CNY)', width: 130 },
|
||||
{ key: 'pendingBilling', label: '待开账(CNY)', width: 140 },
|
||||
{ key: 'receivedInPeriod', label: '本期已收(CNY)', width: 140 },
|
||||
@@ -175,7 +172,7 @@ function downloadCsv(fileName: string, headers: string[], data: unknown[][]) {
|
||||
const url = URL.createObjectURL(new Blob([content], { type: 'text/csv;charset=utf-8' }))
|
||||
const link = document.createElement('a'); link.href = url; link.download = fileName; link.click(); URL.revokeObjectURL(url)
|
||||
}
|
||||
function exportAccounts() { downloadCsv('客户账户.csv', columns.slice(1).map((item) => String(item.label)), rows.value.map((row) => [customerName(row.customerId, row.customerName), row.customerCode, row.contactName, row.contactPhone, row.outstandingReceivable, row.pendingBilling, row.receivedInPeriod, row.outstandingPayable, row.status, row.lastChangedAt])) }
|
||||
function exportAccounts() { downloadCsv('客户账户.csv', columns.slice(1).map((item) => String(item.label)), rows.value.map((row) => [customerName(row.customerId, row.customerName), row.outstandingReceivable, row.pendingBilling, row.receivedInPeriod, row.outstandingPayable, row.status, row.lastChangedAt])) }
|
||||
function exportLedger() { downloadCsv(`${selected.value ? customerName(selected.value.customerId, selected.value.customerName) : '客户'}-账户流水.csv`, ledgerColumns.map((item) => String(item.label)), ledger.value.map((row) => [row.ledgerId, formatDateTime(row.occurredAt), ledgerBusinessType(row.businessType), row.documentNo, row.receivableChange, row.payableChange, row.outstandingReceivableAfter, displayOperator(row.operatorName), row.remark])) }
|
||||
async function closeDetail() {
|
||||
detailOpen.value = false; selected.value = null; editing.value = false; ledger.value = []
|
||||
@@ -223,10 +220,10 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
<AdminPage v-else as="div" class="finance-account-detail" spacing="normal">
|
||||
<div class="detail-heading"><el-button @click="closeDetail"><ArrowLeft :size="16" />返回客户账户</el-button><div><h2>{{ editing ? '维护客户账期' : '客户账户详情' }}</h2><p v-if="selected">{{ customerName(selected.customerId, selected.customerName) }} · {{ selected.customerCode }}</p></div></div>
|
||||
<div class="detail-heading"><el-button @click="closeDetail"><ArrowLeft :size="16" />返回客户账户</el-button><div><h2>{{ editing ? '维护客户账期' : '客户账户详情' }}</h2><p v-if="selected">{{ customerName(selected.customerId, selected.customerName) }}</p></div></div>
|
||||
<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">
|
||||
<el-descriptions :column="2" border class="account-detail"><el-descriptions-item label="客户">{{ customerName(selected.customerId, selected.customerName) }}</el-descriptions-item><el-descriptions-item label="客户编号">{{ selected.customerCode }}</el-descriptions-item><el-descriptions-item label="待收">{{ money(selected.outstandingReceivable) }} CNY</el-descriptions-item><el-descriptions-item label="待开账">{{ money(selected.pendingBilling) }} CNY</el-descriptions-item><el-descriptions-item label="本期已收">{{ money(selected.receivedInPeriod) }} CNY</el-descriptions-item><el-descriptions-item label="待付">{{ money(selected.outstandingPayable) }} CNY</el-descriptions-item></el-descriptions>
|
||||
<el-descriptions :column="2" class="account-detail"><el-descriptions-item label="客户">{{ customerName(selected.customerId, selected.customerName) }}</el-descriptions-item><el-descriptions-item label="待收">{{ money(selected.outstandingReceivable) }} CNY</el-descriptions-item><el-descriptions-item label="待开账">{{ money(selected.pendingBilling) }} CNY</el-descriptions-item><el-descriptions-item label="本期已收">{{ money(selected.receivedInPeriod) }} CNY</el-descriptions-item><el-descriptions-item label="待付">{{ money(selected.outstandingPayable) }} CNY</el-descriptions-item></el-descriptions>
|
||||
<FormSection v-if="!editing" title="账户流水" description="展示应收、应付及每次变动后的未收金额。"><QueryToolbar v-model="ledgerFilters" :fields="ledgerQueryFields" ariaLabel="账户流水筛选" preference-key="finance-account-ledger-query" @search="ledgerPage = 1; loadLedger" /><div class="detail-toolbar"><span>共 {{ ledgerTotal }} 条</span><el-button :disabled="!ledger.length" @click="exportLedger"><Download :size="16" />导出流水</el-button></div><DataTable v-model:page="ledgerPage" v-model:page-size="ledgerPageSize" :data="ledger" :columns="ledgerColumns" :loading="ledgerLoading" :total="ledgerTotal" row-key="ledgerId" empty-text="暂无账户流水" @update:page="loadLedger()" @update:page-size="ledgerPage = 1; loadLedger()"><template #occurredAt="{ row }">{{ formatDateTime(row.occurredAt) }}</template><template #businessType="{ row }">{{ ledgerBusinessType(row.businessType) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template></DataTable></FormSection>
|
||||
<FormSection v-else title="账期设置" description="每个客户独立设置,默认 30 天。"><el-input-number v-model="creditDays" :min="0" :max="3650" :step="1" /><span class="term-unit">天</span></FormSection>
|
||||
</template>
|
||||
|
||||
@@ -243,7 +243,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
|
||||
<div class="detail-heading"><el-button @click="closeDetail"><ArrowLeft :size="16" />返回客户应收应付</el-button><div><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>
|
||||
<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">
|
||||
<el-descriptions :column="2" border class="arap-detail"><el-descriptions-item label="收付方向">{{ financeDirectionLabel(detail.direction) }}</el-descriptions-item><el-descriptions-item label="审核状态">{{ statusLabel(detail.reviewStatus) }}</el-descriptions-item><el-descriptions-item label="客户">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</el-descriptions-item><el-descriptions-item label="来源状态">{{ statusLabel(detail.sourceStatus) }}</el-descriptions-item><el-descriptions-item label="来源类型">{{ sourceTypeDisplay(detail.sourceType) }}</el-descriptions-item><el-descriptions-item label="来源版本">v{{ detail.sourceVersion }}</el-descriptions-item><el-descriptions-item label="入库单">{{ detail.inboundCode || detail.inboundId || '-' }}</el-descriptions-item><el-descriptions-item label="柜次">{{ detail.containerNo || detail.containerId || '-' }}</el-descriptions-item><el-descriptions-item label="费用项目">{{ detail.feeItemName }}</el-descriptions-item><el-descriptions-item label="金额">{{ money(detail.amount) }} CNY</el-descriptions-item><el-descriptions-item label="参与结算">{{ detail.settlementIncluded ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="已生成对账单">{{ detail.statementGenerated ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="审核原因" :span="2">{{ detail.reviewReason || '-' }}</el-descriptions-item><el-descriptions-item label="结算调整原因" :span="2">{{ detail.settlementReason || '-' }}</el-descriptions-item></el-descriptions>
|
||||
<el-descriptions :column="2" class="arap-detail"><el-descriptions-item label="收付方向">{{ financeDirectionLabel(detail.direction) }}</el-descriptions-item><el-descriptions-item label="审核状态">{{ statusLabel(detail.reviewStatus) }}</el-descriptions-item><el-descriptions-item label="客户">{{ customerName(detail.customerId, detail.customerName || selected?.customerName) }}</el-descriptions-item><el-descriptions-item label="费用状态">{{ statusLabel(detail.sourceStatus) }}</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="费用项目">{{ detail.feeItemName }}</el-descriptions-item><el-descriptions-item label="金额">{{ money(detail.amount) }} CNY</el-descriptions-item><el-descriptions-item label="参与结算">{{ detail.settlementIncluded ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="已生成对账单">{{ detail.statementGenerated ? '是' : '否' }}</el-descriptions-item><el-descriptions-item label="审核原因" :span="2">{{ detail.reviewReason || '-' }}</el-descriptions-item><el-descriptions-item label="结算调整原因" :span="2">{{ detail.settlementReason || '-' }}</el-descriptions-item></el-descriptions>
|
||||
<section class="operation-log"><h3>操作记录</h3><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="暂无操作记录" /></section>
|
||||
</template>
|
||||
</AdminPage>
|
||||
|
||||
@@ -74,8 +74,8 @@ describe('FinanceCustomerStatementsView', () => {
|
||||
expect(document.body.textContent).toContain('REC-501')
|
||||
expect(document.body.textContent).toContain('CONT-31')
|
||||
expect(document.body.textContent).toContain('IN-21')
|
||||
expect(document.body.textContent).toContain('来源版本')
|
||||
expect(document.body.textContent).toContain('hash-81')
|
||||
expect(document.body.textContent).not.toContain('来源版本')
|
||||
expect(document.body.textContent).not.toContain('hash-81')
|
||||
expect(document.body.textContent).toContain('优惠调整')
|
||||
expect(document.body.textContent).toContain('创建人A')
|
||||
expect(document.body.textContent).toContain('2026-08-20 11:00')
|
||||
@@ -150,13 +150,13 @@ describe('FinanceCustomerStatementsView', () => {
|
||||
expect(listPlatformTenants).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses statement list permission for fee-source details and keeps frozen trace fields visible', async () => {
|
||||
it('uses statement list permission for fee-source details without exposing technical trace fields', async () => {
|
||||
const wrapper = await mountPage(['platform:finance:customer-statement:list'])
|
||||
await (wrapper.vm as unknown as { openDetail(row: FinanceStatementReadModel): Promise<void> }).openDetail(statement)
|
||||
await flushPromises()
|
||||
expect(getFinanceFeeSource).toHaveBeenCalledWith('81')
|
||||
expect(document.body.textContent).toContain('CONT-31')
|
||||
expect(document.body.textContent).toContain('hash-81')
|
||||
expect(document.body.textContent).not.toContain('hash-81')
|
||||
})
|
||||
|
||||
it('surfaces detail hydration failures instead of silently returning an empty filtered list', async () => {
|
||||
|
||||
@@ -429,20 +429,14 @@ onMounted(() => { if (canListCustomers.value) void loadCustomers(); void load()
|
||||
<el-skeleton v-else-if="detailLoading" :rows="4" animated />
|
||||
<template v-else-if="detail">
|
||||
<el-table :data="detail.lines" size="small" border stripe empty-text="该对账单暂无账单明细">
|
||||
<el-table-column label="应收/应付" width="110"><template #default>应收</template></el-table-column>
|
||||
<el-table-column label="客户" min-width="170"><template #default>{{ customerDisplayName(detail.customerId, detail.customerName) }}</template></el-table-column>
|
||||
<el-table-column label="入仓号" min-width="140"><template #default="{ row }">{{ lineSourceCache.get(row.sourceFeeId)?.inboundCode || row.inboundId || '—' }}</template></el-table-column>
|
||||
<el-table-column label="柜次号" min-width="140"><template #default="{ row }">{{ lineSourceCache.get(row.sourceFeeId)?.containerNo || (row.containerId ? `柜次ID ${row.containerId}` : '—') }}</template></el-table-column>
|
||||
<el-table-column label="业务发生日" width="130"><template #default="{ row }">{{ lineSourceCache.get(row.sourceFeeId)?.businessDate || '—' }}</template></el-table-column>
|
||||
<el-table-column label="来源类型" min-width="140"><template #default="{ row }">{{ financeSourceTypeLabel(lineSourceCache.get(row.sourceFeeId)?.sourceType) }}</template></el-table-column>
|
||||
<el-table-column prop="feeItemName" label="费用项目" min-width="150" />
|
||||
<el-table-column label="计费量" width="110"><template #default="{ row }">{{ lineSourceCache.get(row.sourceFeeId)?.quantity ?? '—' }}</template></el-table-column>
|
||||
<el-table-column label="单价(CNY)" width="120"><template #default="{ row }">{{ lineSourceCache.get(row.sourceFeeId)?.unitPrice ?? '—' }}</template></el-table-column>
|
||||
<el-table-column prop="sourceAmount" label="来源金额(CNY)" width="150" />
|
||||
<el-table-column prop="amount" label="账单金额(CNY)" width="150" />
|
||||
<el-table-column prop="sourceFeeId" label="来源费用ID" min-width="140" />
|
||||
<el-table-column prop="sourceFeeVersion" label="来源版本" width="110" />
|
||||
<el-table-column prop="sourceHash" label="来源快照Hash" min-width="190" />
|
||||
<el-table-column prop="adjustmentReason" label="调整说明" min-width="180" />
|
||||
<el-table-column label="对账单号" min-width="180"><template #default>{{ detail.billNo }}</template></el-table-column>
|
||||
<el-table-column label="结算起始日" width="130"><template #default>{{ detail.periodStart || '—' }}</template></el-table-column>
|
||||
|
||||
@@ -394,7 +394,7 @@ onMounted(() => {
|
||||
</section>
|
||||
<section class="settlement-detail-info surface-panel">
|
||||
<h3>基本信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions :column="2">
|
||||
<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>
|
||||
@@ -429,8 +429,7 @@ onMounted(() => {
|
||||
</section>
|
||||
<section class="detail-section surface-panel">
|
||||
<h3>修订记录</h3>
|
||||
<el-table :data="detail.revisions" size="small" border stripe empty-text="暂无修订记录">
|
||||
<el-table-column prop="revisionNo" label="版本" width="90" />
|
||||
<el-table :data="detail.revisions" size="small" border stripe empty-text="暂无调整记录">
|
||||
<el-table-column prop="action" label="动作" min-width="120" />
|
||||
<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>
|
||||
@@ -508,10 +507,10 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.finance-settlement-page { min-width: 0; }
|
||||
.settlement-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
||||
.summary-item { min-width: 0; padding: 16px 18px; border: 1px solid var(--cc-border, #e5e7eb); border-radius: 8px; background: var(--cc-surface, #fff); }
|
||||
.summary-item span { display: block; color: var(--cc-text-secondary, #64748b); font-size: 13px; }
|
||||
.summary-item strong { display: block; margin-top: 8px; color: var(--cc-text-primary, #1f2937); font-size: 24px; line-height: 1.2; }
|
||||
.settlement-summary { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px 28px; padding: 0 2px; }
|
||||
.summary-item { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
|
||||
.summary-item span { color: var(--cc-text-secondary, #64748b); font-size: 13px; }
|
||||
.summary-item strong { color: var(--cc-text-primary, #1f2937); font-size: 18px; line-height: 1.2; }
|
||||
.summary-item strong.is-success { color: #16803c; }
|
||||
.summary-item strong.is-danger { color: #c2413a; }
|
||||
.settlement-detail-page { display: grid; min-width: 0; gap: 14px; }
|
||||
@@ -536,7 +535,8 @@ onMounted(() => {
|
||||
.operation-error-meta { margin: -6px 0 12px; }
|
||||
:deep(.el-select), :deep(.el-date-editor) { width: 100%; }
|
||||
@media (max-width: 720px) {
|
||||
.settlement-summary, .settlement-detail-summary, .partial-summary { grid-template-columns: 1fr; }
|
||||
.settlement-detail-summary, .partial-summary { grid-template-columns: 1fr; }
|
||||
.settlement-summary { align-items: flex-start; flex-direction: column; gap: 6px; }
|
||||
.summary-item strong { font-size: 21px; }
|
||||
.settlement-detail-header { align-items: flex-start; flex-direction: column; }
|
||||
.settlement-detail-actions { width: 100%; }
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('finance presentation governance', () => {
|
||||
})
|
||||
|
||||
it('translates source codes and provides reason placeholders', () => {
|
||||
expect(arap).toContain('financeSourceTypeLabel(detail.sourceType)')
|
||||
expect(arap).toContain('sourceTypeDisplay(detail.sourceType)')
|
||||
expect(arap).toContain('financeOperationActionLabel(log.action)')
|
||||
expect(arap).toContain('financeOperationResultLabel(log.result)')
|
||||
expect(arap).toContain('financeDirectionLabel(row.direction)')
|
||||
|
||||
Reference in New Issue
Block a user