优化
This commit is contained in:
@@ -21,7 +21,7 @@ describe('finance api', () => {
|
||||
})
|
||||
|
||||
it('uses only the formal settlement customer summary and keeps money as decimal strings', async () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', data: { customerId: '9007199254740993', currency: 'CNY', totalReceivable: '123456.780000', receivedAmount: '100000.000000', outstandingReceivable: '23456.780000', pendingBilling: '0.000000', outstandingPayable: '0.000000', asOf: '2026-08-13T04:00:00Z', lastChangedAt: null } })
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', data: { customerId: '9007199254740993', currency: 'CNY', totalReceivable: '123456.780000', receivedAmount: '100000.000000', outstandingReceivable: '23456.780000', pendingBilling: '0.000000', totalCost: '300.000000', paidCost: '200.000000', outstandingPayable: '100.000000', asOf: '2026-08-13T04:00:00Z', lastChangedAt: null } })
|
||||
|
||||
const result = await getFinanceCustomerAccountSummary('9007199254740993')
|
||||
|
||||
@@ -57,12 +57,12 @@ describe('finance api', () => {
|
||||
})
|
||||
|
||||
it('uses the formal G1 account list path and fixed page DTO', async () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', requestId: 'req-g1', traceId: 'trace-g1', data: [{ customerId: 10001, customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00' }], page: { page: 2, pageSize: 20, total: 21 } })
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'success', requestId: 'req-g1', traceId: 'trace-g1', data: [{ customerId: 10001, customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, totalCost: 260, paidCost: 180, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00' }], page: { page: 2, pageSize: 20, total: 21 } })
|
||||
|
||||
const result = await listFinanceAccounts({ page: 2, size: 20, customerId: '10001', status: 'ACTIVE' })
|
||||
|
||||
expect(get).toHaveBeenCalledWith('/platform/finance/v1/accounts', { params: { page: 2, pageSize: 20, customerId: '10001', status: 'ACTIVE' } })
|
||||
expect(result).toEqual({ records: [{ customerId: '10001', customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00' }], total: 21, pages: 2 })
|
||||
expect(result).toEqual({ records: [{ customerId: '10001', customerCode: 'C-10001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 300, pendingBilling: 50, receivedInPeriod: 100, totalCost: 260, paidCost: 180, outstandingPayable: 80, status: 'ACTIVE', lastChangedAt: '2026-08-11T10:00:00' }], total: 21, pages: 2 })
|
||||
})
|
||||
|
||||
it('uses the formal G1 account ledger path and fixed page DTO', async () => {
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface FinanceCustomerAccountSummaryResource {
|
||||
receivedAmount: number | string
|
||||
outstandingReceivable: number | string
|
||||
pendingBilling: number | string
|
||||
totalCost: number | string
|
||||
paidCost: number | string
|
||||
outstandingPayable: number | string
|
||||
asOf: string
|
||||
lastChangedAt: string | null
|
||||
@@ -103,6 +105,7 @@ export async function getFinanceCustomerAccountSummary(customerId: string): Prom
|
||||
customerId: stringValue(item.customerId), currency: 'CNY',
|
||||
totalReceivable: stringValue(item.totalReceivable), receivedAmount: stringValue(item.receivedAmount),
|
||||
outstandingReceivable: stringValue(item.outstandingReceivable), pendingBilling: stringValue(item.pendingBilling),
|
||||
totalCost: stringValue(item.totalCost), paidCost: stringValue(item.paidCost),
|
||||
outstandingPayable: stringValue(item.outstandingPayable), asOf: stringValue(item.asOf),
|
||||
lastChangedAt: item.lastChangedAt == null ? null : stringValue(item.lastChangedAt),
|
||||
}
|
||||
@@ -121,7 +124,7 @@ export async function listFinanceAccounts(params: { page?: number; size?: number
|
||||
const query = { page: params.page ?? 1, pageSize: params.size ?? 20, customerId: params.customerId || undefined, status: params.status || undefined }
|
||||
const response = await http.get<ApiEnvelope<RawResource[]> & { page?: RawResource }, ApiEnvelope<RawResource[]> & { page?: RawResource }>(`${basePath}/accounts`, { params: query })
|
||||
const data = unwrapApiResponse(response)
|
||||
const records = data.map((item) => ({ customerId: stringValue(item.customerId), customerCode: stringValue(item.customerCode), customerName: stringValue(item.customerName), contactName: stringValue(item.contactName), contactPhone: stringValue(item.contactPhone), outstandingReceivable: numberValue(item.outstandingReceivable), pendingBilling: numberValue(item.pendingBilling), receivedInPeriod: numberValue(item.receivedInPeriod), outstandingPayable: numberValue(item.outstandingPayable), status: stringValue(item.status), lastChangedAt: stringValue(item.lastChangedAt) }))
|
||||
const records = data.map((item) => ({ customerId: stringValue(item.customerId), customerCode: stringValue(item.customerCode), customerName: stringValue(item.customerName), contactName: stringValue(item.contactName), contactPhone: stringValue(item.contactPhone), outstandingReceivable: numberValue(item.outstandingReceivable), pendingBilling: numberValue(item.pendingBilling), receivedInPeriod: numberValue(item.receivedInPeriod), totalCost: numberValue(item.totalCost), paidCost: numberValue(item.paidCost), outstandingPayable: numberValue(item.outstandingPayable), status: stringValue(item.status), lastChangedAt: stringValue(item.lastChangedAt) }))
|
||||
const total = numberValue(response.page?.total ?? records.length)
|
||||
const pageSize = numberValue(response.page?.pageSize ?? query.pageSize) || query.pageSize
|
||||
return { records, total, pages: Math.max(1, Math.ceil(total / pageSize)) }
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('finance six-page delivery contract', () => {
|
||||
const columns = page.slice(page.indexOf('const columns:'))
|
||||
expect(columns.indexOf("key: 'actions'")).toBeLessThan(columns.indexOf("key: 'direction'"))
|
||||
expect(page).toContain('<ApiErrorMeta :details="detailError"')
|
||||
expect(page).toContain('<OperationErrorToast :details="reviewError || mutationError || generationError" />')
|
||||
expect(page).toContain('<OperationErrorToast :details="reviewError || mutationError" />')
|
||||
expect(page).not.toContain('<ApiErrorMeta :details="reviewError"')
|
||||
expect(page).not.toContain('<ApiErrorMeta :details="mutationError"')
|
||||
expect(page).not.toContain('接口尚未提供')
|
||||
|
||||
@@ -126,7 +126,7 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
|
||||
vi.stubGlobal('localStorage', memoryStorage())
|
||||
vi.stubGlobal('sessionStorage', memoryStorage())
|
||||
vi.mocked(listFinanceAccounts).mockReset().mockResolvedValue({ records: [{ customerId: '10001', customerCode: 'C-001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 100, pendingBilling: 20, receivedInPeriod: 30, outstandingPayable: 40, status: 'ACTIVE', lastChangedAt: '2026-08-13 10:00:00' }], total: 1, pages: 1 })
|
||||
vi.mocked(listFinanceAccounts).mockReset().mockResolvedValue({ records: [{ customerId: '10001', customerCode: 'C-001', customerName: '客户A', contactName: '张三', contactPhone: '13800000000', outstandingReceivable: 100, pendingBilling: 20, receivedInPeriod: 30, totalCost: 90, paidCost: 50, outstandingPayable: 40, status: 'ACTIVE', lastChangedAt: '2026-08-13 10:00:00' }], total: 1, pages: 1 })
|
||||
vi.mocked(listFinanceAccountLedger).mockReset().mockResolvedValue({ records: [{ ledgerId: '1', occurredAt: '2026-08-13T10:00:00', businessType: 'REC', documentType: 'RECEIPT', documentId: '2', documentNo: 'REC-2', receivableChange: -10, payableChange: 0, outstandingReceivableAfter: 90, operatorName: 'operator-1000001', remark: '收款' }], total: 1, pages: 1 })
|
||||
vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '客户A', status: 'ACTIVE' } as PlatformTenantResource], page: 1, size: 200, total: 1, pages: 1 })
|
||||
vi.mocked(createPlatformExportTask).mockReset().mockResolvedValue({ id: 'exp-1', taskNo: 'EXP-1', taskName: '客户账户', status: 'PENDING' } as never)
|
||||
@@ -155,7 +155,7 @@ describe('FinanceCustomerAccountsView delivery', () => {
|
||||
})
|
||||
|
||||
it('shows explicit master-data empty states without exposing the customer id', async () => {
|
||||
vi.mocked(listFinanceAccounts).mockResolvedValue({ records: [{ customerId: '10001', customerCode: '', customerName: '客户A', contactName: '', contactPhone: '', outstandingReceivable: 0, pendingBilling: 0, receivedInPeriod: 0, outstandingPayable: 0, status: 'ACTIVE', lastChangedAt: '' }], total: 1, pages: 1 })
|
||||
vi.mocked(listFinanceAccounts).mockResolvedValue({ records: [{ customerId: '10001', customerCode: '', customerName: '客户A', contactName: '', contactPhone: '', outstandingReceivable: 0, pendingBilling: 0, receivedInPeriod: 0, totalCost: 0, paidCost: 0, outstandingPayable: 0, status: 'ACTIVE', lastChangedAt: '' }], total: 1, pages: 1 })
|
||||
const wrapper = await mountAccountsView(['platform:finance:customer-account:list'])
|
||||
expect(wrapper.text()).toContain('客户编号')
|
||||
expect(wrapper.text()).toContain('联系人')
|
||||
|
||||
@@ -68,6 +68,8 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'outstandingReceivable', label: '待收(CNY)', width: 130 },
|
||||
{ key: 'pendingBilling', label: '待开账(CNY)', width: 140 },
|
||||
{ key: 'receivedInPeriod', label: '本期已收(CNY)', width: 140 },
|
||||
{ key: 'totalCost', label: '总成本(CNY)', width: 140 },
|
||||
{ key: 'paidCost', label: '已付成本(CNY)', width: 150 },
|
||||
{ key: 'outstandingPayable', label: '待付(CNY)', width: 130 },
|
||||
{ key: 'status', label: '客户状态', width: 110, slotName: 'status' },
|
||||
{ key: 'lastChangedAt', label: '最近变动', width: 170, slotName: 'lastChangedAt' },
|
||||
@@ -89,8 +91,10 @@ const summary = computed(() => rows.value.reduce((result, row) => ({
|
||||
receivable: result.receivable + row.outstandingReceivable,
|
||||
pending: result.pending + row.pendingBilling,
|
||||
received: result.received + row.receivedInPeriod,
|
||||
totalCost: result.totalCost + row.totalCost,
|
||||
paidCost: result.paidCost + row.paidCost,
|
||||
payable: result.payable + row.outstandingPayable,
|
||||
}), { receivable: 0, pending: 0, received: 0, payable: 0 }))
|
||||
}), { receivable: 0, pending: 0, received: 0, totalCost: 0, paidCost: 0, payable: 0 }))
|
||||
|
||||
async function load(model: QueryModel = filters.value) {
|
||||
Object.assign(filters.value, model)
|
||||
@@ -263,7 +267,7 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
|
||||
<template #customerCode="{ row }">{{ customerField(row.customerCode) }}</template><template #contactName="{ row }">{{ customerField(row.contactName) }}</template><template #contactPhone="{ row }">{{ customerField(row.contactPhone) }}</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>
|
||||
<template #pagination-left><span>汇总:未收 {{ money(summary.receivable) }} · 待出账 {{ money(summary.pending) }} · 本期已收 {{ money(summary.received) }} · 总成本 {{ money(summary.totalCost) }} · 已付成本 {{ money(summary.paidCost) }} · 待付 {{ money(summary.payable) }} CNY</span></template>
|
||||
</DataTable>
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
@@ -271,7 +275,7 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
|
||||
<section class="surface-panel finance-account-detail__surface">
|
||||
<DetailPageLayout class="finance-account-detail__layout" :aria-label="editing ? '维护客户账期' : '客户账户详情'">
|
||||
<template #header><div class="detail-heading"><div class="detail-title"><h2>{{ editing ? '维护客户账期' : '客户账户详情' }}</h2><p v-if="selected">{{ customerName(selected.customerId, selected.customerName) }} · 客户财务往来总览</p></div><el-button class="detail-back" @click="closeDetail"><ArrowLeft :size="16" />返回客户账户</el-button></div></template>
|
||||
<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>
|
||||
<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"><span>总成本</span><strong>{{ money(selected.totalCost) }}</strong><small>CNY</small></div><div class="account-metric is-received"><span>已付成本</span><strong>{{ money(selected.paidCost) }}</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 class="customer-profile-section" title="客户信息" :bordered="false"><el-descriptions :column="3"><el-descriptions-item label="客户编号">{{ customerField(selected.customerCode) }}</el-descriptions-item><el-descriptions-item label="联系人">{{ customerField(selected.contactName) }}</el-descriptions-item><el-descriptions-item label="联系电话">{{ customerField(selected.contactPhone) }}</el-descriptions-item></el-descriptions></FormSection>
|
||||
@@ -377,7 +381,7 @@ watch(() => [route.name, String(route.params.customerId || '')], ([name, custome
|
||||
|
||||
.account-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
padding: 0 0 12px;
|
||||
border-bottom: 1px solid var(--cc-border, #e3e8ef);
|
||||
|
||||
@@ -183,6 +183,8 @@ export interface FinanceAccountSummary {
|
||||
outstandingReceivable: number
|
||||
pendingBilling: number
|
||||
receivedInPeriod: number
|
||||
totalCost: number
|
||||
paidCost: number
|
||||
outstandingPayable: number
|
||||
status: string
|
||||
lastChangedAt: string
|
||||
|
||||
@@ -13,10 +13,6 @@ import { adjustContainerFee, approveContainerFee, createContainerFee, listApplic
|
||||
|
||||
const props = defineProps<{ planId: string; containerBookingId: string; customers: Array<{ accountId: string; customerName: string | null }> }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const settlementModeModel = computed<string>({
|
||||
get: () => form.settlementMode ?? '',
|
||||
set: (value) => { form.settlementMode = value === 'ON_ACCOUNT' || value === 'DIRECT_PAID' ? value : null },
|
||||
})
|
||||
const accountId = ref('')
|
||||
const fees = ref<ContainerFeeResource[]>([])
|
||||
const feeItems = ref<ApplicableFeeItem[]>([])
|
||||
@@ -36,6 +32,10 @@ const saving = ref(false)
|
||||
const target = ref<ContainerFeeResource | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive<ContainerFeeSaveRequest>({ feeItemId: '', direction: 'RECEIVABLE', supplierId: null, settlementMode: null, paidAt: null, paymentMethod: null, paymentReferenceNo: null, paymentVoucherFileIds: null, businessDate: new Date().toISOString().slice(0, 10), amount: 0, currency: 'CNY', remark: null })
|
||||
const settlementModeModel = computed<string>({
|
||||
get: () => form.settlementMode ?? '',
|
||||
set: (value) => { form.settlementMode = value === 'ON_ACCOUNT' || value === 'DIRECT_PAID' ? value : null },
|
||||
})
|
||||
const voucherFiles = ref<Array<{ fileId: string; fileName: string }>>([])
|
||||
const voucherUploading = ref(false)
|
||||
const voucherError = ref<ApiErrorDetails | null>(null)
|
||||
|
||||
@@ -52,6 +52,10 @@ const emptyForm = (): SaveInboundFeeDetailRequest => ({
|
||||
paymentReferenceNo: null, paymentVoucherFileIds: null, remark: null,
|
||||
})
|
||||
const form = reactive<SaveInboundFeeDetailRequest>(emptyForm())
|
||||
const settlementModeModel = computed<string>({
|
||||
get: () => form.settlementMode ?? '',
|
||||
set: (value) => { form.settlementMode = value === 'ON_ACCOUNT' || value === 'DIRECT_PAID' ? value : null },
|
||||
})
|
||||
const rules: FormRules = {
|
||||
feeItemId: [{ required: true, message: '请选择费用项目', trigger: 'change' }],
|
||||
businessDate: [{ required: true, message: '请选择业务发生日', trigger: 'change' }],
|
||||
@@ -356,7 +360,3 @@ onMounted(() => void load())
|
||||
@keyframes fee-refresh-spin { to { transform: rotate(360deg); } }
|
||||
@media (max-width: 720px) { .fee-toolbar { align-items: flex-start; flex-direction: column; } .fee-form { grid-template-columns: 1fr; } .fee-form :deep(.el-form-item:last-child) { grid-column: auto; } }
|
||||
</style>
|
||||
const settlementModeModel = computed<string>({
|
||||
get: () => form.settlementMode ?? '',
|
||||
set: (value) => { form.settlementMode = value === 'ON_ACCOUNT' || value === 'DIRECT_PAID' ? value : null },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user