统一后台状态列顺序、详情布局与测量计费展示

This commit is contained in:
2026-10-28 10:32:33 +08:00
parent 8e97fd6a9f
commit e41e9e6d3f
120 changed files with 436 additions and 238 deletions
@@ -97,8 +97,8 @@ describe('FinanceCustomerAccountsView delivery', () => {
expect(page).not.toContain('text-overflow: ellipsis')
})
it('keeps account detail and term editing readable across the 1920 workspace', () => {
expect(page).toContain('width: min(100%, 1920px)')
it('keeps account detail and term editing readable across the full workspace width', () => {
expect(page).not.toContain('width: min(100%, 1920px)')
expect(page).toContain('@media (min-width: 1920px)')
expect(page).toContain('finance-density-detail')
expect(page).toContain('../financeDensity.css')
@@ -98,6 +98,7 @@ const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 290, fixed: 'left', slotName: 'actions' },
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' },
{ key: 'customerCode', label: '客户编号', width: 150, slotName: 'customerCode' },
{ key: 'status', label: '客户状态', width: 110, slotName: 'status', afterColumn: 'customerCode' },
{ key: 'contactName', label: '联系人', width: 130, slotName: 'contactName' },
{ key: 'contactPhone', label: '联系电话', width: 150, slotName: 'contactPhone' },
{ key: 'actualBalance', label: '实际余额(CNY)', align: 'right', width: 155, slotName: 'actualBalance' },
@@ -107,7 +108,6 @@ const columns: DataTableColumn[] = [
{ key: 'creditExpiresOn', label: '授信有效期', width: 140, slotName: 'creditExpiresOn' },
{ key: 'creditUsed', label: '已用授信(CNY)', align: 'right', width: 150, slotName: 'creditUsed' },
{ key: 'pendingRechargeCount', label: '待审核充值', align: 'right', width: 120, slotName: 'pendingRechargeCount' },
{ key: 'status', label: '客户状态', width: 110, slotName: 'status' },
{ key: 'lastChangedAt', label: '最近变动', width: 170, slotName: 'lastChangedAt' },
]
const ledgerQueryFields: QueryField[] = [
@@ -133,10 +133,10 @@ const ledgerColumns: DataTableColumn[] = [
]
const rechargeColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' },
{ key: 'requestNo', label: '申请单号', minWidth: 190 }, { key: 'createdAt', label: '提交时间', width: 170, slotName: 'createdAt' },
{ key: 'requestNo', label: '申请单号', minWidth: 190 },
{ key: 'status', label: '状态', width: 110, slotName: 'status', afterColumn: 'requestNo' }, { key: 'createdAt', label: '提交时间', width: 170, slotName: 'createdAt' },
{ key: 'amount', label: '充值金额(CNY)', width: 150, align: 'right', slotName: 'amount' },
{ key: 'voucherFileName', label: '付款凭证', minWidth: 170, slotName: 'voucherFileName' }, { key: 'submittedByName', label: '提交人', width: 130 },
{ key: 'status', label: '状态', width: 110, slotName: 'status' }, { key: 'reviewedByName', label: '审核人', width: 130 },
{ key: 'voucherFileName', label: '付款凭证', minWidth: 170, slotName: 'voucherFileName' }, { key: 'submittedByName', label: '提交人', width: 130 }, { key: 'reviewedByName', label: '审核人', width: 130 },
{ key: 'reviewRemark', label: '审核备注', minWidth: 180 }, { key: 'reviewedAt', label: '审核时间', width: 170, slotName: 'reviewedAt' },
]
const sortedRechargeRequests = computed(() => [...rechargeRequests.value].sort((left, right) => Number(right.status === 'PENDING') - Number(left.status === 'PENDING')))
@@ -560,7 +560,7 @@ onBeforeUnmount(clearVoucherPreview)
</DataTable></div>
</FormSection>
<FormSection v-if="!editing" class="ledger-section density-form-section" title="账户流水" :bordered="false">
<div class="detail-table-scroll workbench-table is-page-surface"><DataTable v-model:page="ledgerPage" v-model:page-size="ledgerPageSize" class="ledger-table" appearance="integrated" :data="ledger" :columns="ledgerColumns" :loading="ledgerLoading" :total="ledgerTotal" row-key="ledgerId" empty-text="暂无账户流水" @update:page="loadLedger()" @update:page-size="ledgerPage = 1; loadLedger()"><template #filters><QueryToolbar v-model="ledgerFilters" class="ledger-query workbench-query" :fields="ledgerQueryFields" ariaLabel="账户流水筛选" preference-key="finance-account-ledger-query" @search="ledgerPage = 1; void loadLedger($event)" /></template><template #actions="{ row }"><el-button v-if="canRefund(row as FinanceLedgerEntry)" link type="primary" @click="openRefund(row as FinanceLedgerEntry)">退款</el-button><span v-else>—</span></template><template #pagination-left>共 {{ ledgerTotal }} 条</template><template #ledgerId="{ row }"><span class="ledger-identifier" :title="displayLedgerId(row.businessNo || row.ledgerId)">{{ displayLedgerId(row.businessNo || row.ledgerId) }}</span></template><template #occurredAt="{ row }">{{ row.occurredDate || formatDateTime(row.createdAt || row.occurredAt) }}</template><template #businessType="{ row }">{{ walletBusinessType(row.changeType || row.businessType) }}</template><template #documentNo="{ row }"><span v-if="row.changeType === 'REFUND'">原消费:</span><span class="ledger-document" :title="displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo)">{{ displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo) }}</span></template><template #amount="{ row }">{{ money(row.amount) }}</template><template #ledgerPaymentMethod="{ row }">{{ row.paymentMethod ? paymentMethodLabel(row.paymentMethod) : '—' }}</template><template #actualBalanceAfter="{ row }">{{ money(row.actualBalanceAfter) }}</template><template #frozenBalanceAfter="{ row }">{{ money(row.frozenBalanceAfter) }}</template><template #availableBalanceAfter="{ row }">{{ money(row.availableBalanceAfter) }}</template><template #creditUsedAfter="{ row }">{{ money(row.creditUsedAfter) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template></DataTable></div>
<div class="detail-table-scroll workbench-table is-page-surface"><DataTable v-model:page="ledgerPage" v-model:page-size="ledgerPageSize" class="ledger-table" appearance="integrated" :data="ledger" :columns="ledgerColumns" :loading="ledgerLoading" :total="ledgerTotal" row-key="ledgerId" empty-text="暂无账户流水" @update:page="loadLedger()" @update:page-size="ledgerPage = 1; loadLedger()"><template #filters><QueryToolbar v-model="ledgerFilters" class="ledger-query workbench-query" :fields="ledgerQueryFields" ariaLabel="账户流水筛选" preference-key="finance-account-ledger-query" @search="ledgerPage = 1; void loadLedger($event)" /></template><template #actions="{ row }"><div class="density-actions row-actions admin-text-actions"><el-button v-if="canRefund(row as FinanceLedgerEntry)" class="density-action" link type="primary" @click="openRefund(row as FinanceLedgerEntry)">退款</el-button><span v-else>—</span></div></template><template #pagination-left>共 {{ ledgerTotal }} 条</template><template #ledgerId="{ row }"><span class="ledger-identifier" :title="displayLedgerId(row.businessNo || row.ledgerId)">{{ displayLedgerId(row.businessNo || row.ledgerId) }}</span></template><template #occurredAt="{ row }">{{ row.occurredDate || formatDateTime(row.createdAt || row.occurredAt) }}</template><template #businessType="{ row }">{{ walletBusinessType(row.changeType || row.businessType) }}</template><template #documentNo="{ row }"><span v-if="row.changeType === 'REFUND'">原消费:</span><span class="ledger-document" :title="displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo)">{{ displayDocumentNo(row.referenceNo || row.sourceNo || row.documentNo) }}</span></template><template #amount="{ row }">{{ money(row.amount) }}</template><template #ledgerPaymentMethod="{ row }">{{ row.paymentMethod ? paymentMethodLabel(row.paymentMethod) : '—' }}</template><template #actualBalanceAfter="{ row }">{{ money(row.actualBalanceAfter) }}</template><template #frozenBalanceAfter="{ row }">{{ money(row.frozenBalanceAfter) }}</template><template #availableBalanceAfter="{ row }">{{ money(row.availableBalanceAfter) }}</template><template #creditUsedAfter="{ row }">{{ money(row.creditUsedAfter) }}</template><template #operatorName="{ row }">{{ displayOperator(row.operatorName) }}</template></DataTable></div>
</FormSection>
<FormSection v-else class="term-section density-form-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" placeholder="请输入账期天数" /><span class="term-unit">天</span></div></div></FormSection>
</template>
@@ -646,7 +646,7 @@ onBeforeUnmount(clearVoucherPreview)
}
.finance-account-detail__surface {
width: min(100%, 1920px);
width: 100%;
margin: 0 auto;
overflow: hidden;
}
@@ -281,9 +281,9 @@ describe('FinanceCustomerArapView G2 operations', () => {
expect(wrapper.find('.arap-information').text()).not.toContain('T00:00:00')
})
it('keeps the fee-source detail readable across the 1920 workspace', () => {
it('keeps the fee-source detail readable across the full workspace width', () => {
const page = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceCustomerArapView.vue'), 'utf8')
expect(page).toContain('width: min(100%, 1920px)')
expect(page).not.toContain('width: min(100%, 1920px)')
expect(page).toContain('<el-descriptions border :column="3" class="arap-detail density-descriptions">')
expect(page).not.toContain('arap-detail-grid')
expect(page).toContain('finance-density-detail')
@@ -149,11 +149,11 @@ const settlementStatusSpan = computed(() => {
})
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 260, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: isSelectableReceivable }, { key: 'direction', label: '收付方向', width: 100, slotName: 'direction' },
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' }, { key: 'businessReference', label: '关联业务', minWidth: 220, slotName: 'businessReference' }, { key: 'sourceType', label: '来源类型', width: 120, slotName: 'sourceType' },
{ key: 'feeName', label: '费用项目', minWidth: 150 }, { key: 'amount', label: '金额(CNY)', align: 'right', width: 130 }, { key: 'supplierName', label: '供应商', minWidth: 170, slotName: 'supplierName' },
{ key: 'status', label: '来源状态', width: 130, slotName: 'sourceStatus' },
{ key: 'reviewStatus', label: '审核状态', width: 120, slotName: 'status' }, { key: 'statementGenerated', label: '已生成对账单', width: 140, slotName: 'generated' }, { key: 'businessDate', label: '业务发生日', width: 170, slotName: 'businessDate' },
{ key: 'actions', label: '操作', width: 260, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left', selectable: isSelectableReceivable }, { key: 'businessReference', label: '关联业务', minWidth: 220, slotName: 'businessReference' },
{ key: 'status', label: '来源状态', width: 130, slotName: 'sourceStatus', afterColumn: 'businessReference' },
{ key: 'reviewStatus', label: '审核状态', width: 120, slotName: 'status', afterColumn: 'businessReference' }, { key: 'statementGenerated', label: '已生成对账单', width: 140, slotName: 'generated', afterColumn: 'businessReference' }, { key: 'direction', label: '收付方向', width: 100, slotName: 'direction' },
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' }, { key: 'sourceType', label: '来源类型', width: 120, slotName: 'sourceType' },
{ key: 'feeName', label: '费用项目', minWidth: 150 }, { key: 'amount', label: '金额(CNY)', align: 'right', width: 130 }, { key: 'supplierName', label: '供应商', minWidth: 170, slotName: 'supplierName' }, { key: 'businessDate', label: '业务发生日', width: 170, slotName: 'businessDate' },
]
function customerName(customerId: string, snapshot?: string) {
const masterName = customers.value.find((item) => String(item.id) === String(customerId))?.tenantName?.trim()
@@ -509,7 +509,7 @@ watch(() => [route.name, String(route.params.feeSourceId || '')], ([name, feeSou
}
.finance-arap-detail__surface {
width: min(100%, 1920px);
width: 100%;
margin: 0 auto;
overflow: hidden;
}
@@ -302,7 +302,7 @@ describe('FinanceCustomerStatementsView', () => {
{ label: '已结清', value: 'PAID' },
{ label: '已作废', value: 'VOIDED' },
])
expect(statusColumn).toEqual({ key: 'status', label: '对账单状态', width: 120, slotName: 'status' })
expect(statusColumn).toEqual({ key: 'status', label: '对账单状态', width: 120, slotName: 'status', afterColumn: 'billNo' })
})
it('creates a formal statement export task with only non-empty filters', async () => {
@@ -128,9 +128,10 @@ function resetFilters() {
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 280, fixed: 'left', slotName: 'actions' },
{ key: 'status', label: '对账单状态', width: 120, slotName: 'status' },
{ key: 'customerConfirmationStatus', label: '客户确认', width: 130, slotName: 'customerConfirmationStatus' },
{ key: 'billNo', label: '对账单号', minWidth: 180 },
{ key: 'status', label: '对账单状态', width: 120, slotName: 'status', afterColumn: 'billNo' },
{ key: 'customerConfirmationStatus', label: '客户确认', width: 130, slotName: 'customerConfirmationStatus', afterColumn: 'billNo' },
{ key: 'settled', label: '是否结清', width: 100, slotName: 'settled', afterColumn: 'billNo' },
{ key: 'customerName', label: '客户', minWidth: 180, slotName: 'customerName' },
{ key: 'periodStart', label: '结算起始日', width: 130 },
{ key: 'periodEnd', label: '结算结束日', width: 130 },
@@ -144,7 +145,6 @@ const columns: DataTableColumn[] = [
{ key: 'receivedAmount', label: '已收金额(CNY)', align: 'right', width: 145 },
{ key: 'outstandingAmount', label: '未收金额(CNY)', align: 'right', width: 145 },
{ key: 'lastPaymentAt', label: '最后收款时间', minWidth: 170, slotName: 'lastPaymentAt' },
{ key: 'settled', label: '是否结清', width: 100, slotName: 'settled' },
{ key: 'remark', label: '备注', minWidth: 180 },
{ key: 'createdByName', label: '创建人', width: 130, slotName: 'createdByName' },
{ key: 'createdAt', label: '创建时间', minWidth: 170, slotName: 'createdAt' },
@@ -765,7 +765,7 @@ watch(detailRouteId, (statementId) => {
.statement-create-table td:not([colspan]) { width: 50%; }
.statement-dialog-facts th { width: var(--admin-label-width); }
.statement-adjust-editor { display: grid; gap: var(--admin-space-5); }
.statement-detail-page { min-width: 0; width: min(100%, 1920px); margin: 0 auto; overflow: hidden; }
.statement-detail-page { min-width: 0; width: 100%; margin: 0 auto; overflow: hidden; }
.statement-summary-section { padding: var(--admin-space-4) var(--admin-space-6); border-bottom: 1px solid var(--workbench-border); }
.statement-outstanding { color: var(--el-color-warning-dark-2); }
.statement-detail-table { min-width: 0; }
@@ -139,6 +139,8 @@ const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
? [
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
{ key: 'settlementNo', label: '结算单号', minWidth: 180 },
{ key: 'paymentStatus', label: '收款状态', width: 120, slotName: 'paymentStatus', afterColumn: 'settlementNo' },
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus', afterColumn: 'settlementNo' },
{ key: 'customerId', label: '客户', minWidth: 180, slotName: 'customer' },
{ key: 'feeItemSummary', label: '费用类型', minWidth: 150, slotName: 'feeItemSummary' },
{ key: 'currency', label: '币种', width: 80 },
@@ -149,15 +151,15 @@ const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
{ key: 'businessPeriod', label: '结算周期', width: 120, slotName: 'businessPeriod' },
{ key: 'generatedDate', label: '生成日期', width: 130, slotName: 'generatedDate' },
{ key: 'dueDate', label: '到期日', width: 130, slotName: 'dueDate' },
{ key: 'paymentStatus', label: '收款状态', width: 120, slotName: 'paymentStatus' },
{ key: 'createdByName', label: '制单人', minWidth: 130, slotName: 'createdByName' },
{ key: 'createdAt', label: '创建时间', width: 170, slotName: 'createdAt' },
{ key: 'lastPaymentAt', label: '最后收款时间', width: 170, slotName: 'lastPaymentAt' },
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus' },
]
: [
{ key: 'actions', label: '操作', width: 250, fixed: 'left', slotName: 'actions' },
{ key: 'settlementNo', label: '结算单号', minWidth: 180 },
{ key: 'paymentStatus', label: '付款状态', width: 120, slotName: 'paymentStatus', afterColumn: 'settlementNo' },
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus', afterColumn: 'settlementNo' },
{ key: 'supplierName', label: '供应商', minWidth: 180, slotName: 'supplier' },
{ key: 'customerId', label: '客户', minWidth: 180, slotName: 'customer' },
{ key: 'feeItemSummary', label: '费用类型', minWidth: 150, slotName: 'feeItemSummary' },
@@ -168,11 +170,9 @@ const columns = computed<DataTableColumn[]>(() => props.kind === 'receivable'
{ key: 'businessPeriod', label: '结算周期', width: 120, slotName: 'businessPeriod' },
{ key: 'generatedDate', label: '生成日期', width: 130, slotName: 'generatedDate' },
{ key: 'dueDate', label: '到期日', width: 130, slotName: 'dueDate' },
{ key: 'paymentStatus', label: '付款状态', width: 120, slotName: 'paymentStatus' },
{ key: 'createdByName', label: '制单人', minWidth: 130, slotName: 'createdByName' },
{ key: 'createdAt', label: '创建时间', width: 170, slotName: 'createdAt' },
{ key: 'lastPaymentAt', label: '最后付款时间', width: 170, slotName: 'lastPaymentAt' },
{ key: 'status', label: '结算状态', width: 120, slotName: 'settlementStatus' },
])
function money(value: number) { return `¥${Number(value || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` }
@@ -738,7 +738,7 @@ onMounted(() => {
.settlement-list-actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; }
.settlement-list-actions { flex: 0 0 auto; justify-content: flex-end; gap: var(--admin-space-3); }
.settlement-list-actions :deep(.el-button + .el-button) { margin-left: 0; }
.settlement-detail-page { display: flex; width: min(100%, 1920px); margin: 0 auto; min-width: 0; gap: 0; overflow: hidden; padding: 0; }
.settlement-detail-page { display: flex; width: 100%; margin: 0 auto; min-width: 0; gap: 0; overflow: hidden; padding: 0; }
.settlement-detail-actions { display: flex; align-items: center; gap: var(--admin-space-4); min-width: 0; flex-wrap: wrap; }
.settlement-detail-state { margin: var(--admin-space-5) var(--admin-space-6); }
.settlement-detail-content { min-width: 0; padding: 0 var(--admin-space-6) var(--admin-space-5); overflow: hidden; }
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ElMessageBox } from 'element-plus'
import { ApiResponseError } from '@/shared/api/http'
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
import { preferenceStorageKey } from '@/shared/utils/uiLayoutPreference'
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
import { listSuppliers, type Supplier } from '@/modules/suppliers'
import * as api from '../api/finance'
@@ -110,6 +111,7 @@ function errorDetails(wrapper: VueWrapper) {
}
enableAutoUnmount(afterEach)
beforeEach(() => {
localStorage.clear()
vi.resetAllMocks()
can.mockReturnValue(true)
vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} })
@@ -129,6 +131,19 @@ beforeEach(() => {
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); document.body.innerHTML = '' })
describe('formal settlement native dialogs', () => {
it.each(['receivable', 'payable'] as const)('keeps %s statuses after the settlement number with saved old column order', async (kind) => {
localStorage.setItem(preferenceStorageKey('table', 'anonymous', `finance-settlement-${kind}-table`), JSON.stringify({
version: 1,
order: ['actions', 'settlementNo', 'customerId', 'feeItemSummary', 'currency', 'paymentStatus', 'status'],
hidden: ['currency'],
widths: { settlementNo: 240 },
}))
const wrapper = await mountPage(kind)
const headers = wrapper.findAll('.el-table__header-wrapper th .cell').map(header => header.text())
expect(headers.slice(0, 4)).toEqual(['操作', '结算单号', kind === 'receivable' ? '收款状态' : '付款状态', '结算状态'])
expect(headers).not.toContain('币种')
})
it('creates receivables from issued statements with original long IDs and CNY', async () => {
const { dialog } = await openCreate('receivable')
expect(dialog.props('width')).toContain('600px')
@@ -19,5 +19,6 @@ describe('Finance settlement route 1920px layout', () => {
const shared = readFileSync(resolve(process.cwd(), 'src/modules/finance/pages/FinanceFormalSettlementPage.vue'), 'utf8')
expect(shared).toContain(':column="4" class="settlement-detail-info density-descriptions"')
expect(shared).toContain('finance-density-detail')
expect(shared).not.toContain('width: min(100%, 1920px)')
})
})