fix(customers): wait for billing defaults before loading

This commit is contained in:
2026-09-10 22:12:27 +08:00
parent 781d25de08
commit b6b91297ae
4 changed files with 62 additions and 12 deletions
@@ -93,6 +93,24 @@ describe('CustomerBillingRulesPanel', () => {
expect(getCustomerDimensionConfig).not.toHaveBeenCalled() expect(getCustomerDimensionConfig).not.toHaveBeenCalled()
}) })
it('does not request billing rules before customer defaults are ready', async () => {
mount(CustomerBillingRulesPanel, {
attachTo: document.body,
props: {
customerId: '8',
customerName: '柏悦物流',
dimensionInitializationStatus: 'PENDING',
},
global: { plugins: [ElementPlus] },
})
await flushPromises()
expect(document.body.textContent).toContain('客户计费规则正在初始化')
expect(document.body.textContent).not.toContain('箱规尺寸规则加载失败')
expect(getCustomerDimensionConfig).not.toHaveBeenCalled()
expect(listCustomerChargeRules).not.toHaveBeenCalled()
})
it('submits all four rows in one update request', async () => { it('submits all four rows in one update request', async () => {
const wrapper = mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } }) const wrapper = mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } })
await flushPromises() await flushPromises()
@@ -19,12 +19,21 @@ import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue' import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import { createCustomerChargeRule, listCustomerChargeRules, setCustomerChargeRuleDefault, type CustomerChargeRule, type CreateCustomerChargeRuleRequest } from '@/modules/fba-pricing' import { createCustomerChargeRule, listCustomerChargeRules, setCustomerChargeRuleDefault, type CustomerChargeRule, type CreateCustomerChargeRuleRequest } from '@/modules/fba-pricing'
const props = defineProps<{ customerId: string; customerName: string; customerCode?: string }>() const props = defineProps<{
customerId: string
customerName: string
customerCode?: string
dimensionInitializationStatus?: string
}>()
const access = useAccessPreviewStore() const access = useAccessPreviewStore()
const canView = computed(() => access.can('firstleg:dimension-config:view')) const canView = computed(() => access.can('firstleg:dimension-config:view'))
const canEdit = computed(() => canView.value && access.can('firstleg:dimension-config:edit')) const canEdit = computed(() => canView.value && access.can('firstleg:dimension-config:edit'))
const canChargeView = computed(() => access.can('firstleg:fba-pricing:rule:view')) const canChargeView = computed(() => access.can('firstleg:fba-pricing:rule:view'))
const canChargeEdit = computed(() => canChargeView.value && access.can('firstleg:fba-pricing:rule:edit')) const canChargeEdit = computed(() => canChargeView.value && access.can('firstleg:fba-pricing:rule:edit'))
const initializationStatus = computed(() => props.dimensionInitializationStatus?.trim().toUpperCase() || 'READY')
const initializationPending = computed(() => ['PENDING', 'SENDING'].includes(initializationStatus.value))
const initializationFailed = computed(() => initializationStatus.value === 'FAILED')
const initializationReady = computed(() => !initializationPending.value && !initializationFailed.value)
const config = ref<CustomerDimensionConfig | null>(null) const config = ref<CustomerDimensionConfig | null>(null)
const loading = ref(false) const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null) const loadError = ref<ApiErrorDetails | null>(null)
@@ -92,7 +101,17 @@ watch(form, () => {
watch(() => props.customerId, () => { watch(() => props.customerId, () => {
editorVisible.value = false editorVisible.value = false
defaultCalculationMode.value = 'ACTUAL' defaultCalculationMode.value = 'ACTUAL'
if (initializationReady.value) {
void loadConfig() void loadConfig()
void loadChargeRules()
}
})
watch(initializationReady, (ready, previous) => {
if (ready && !previous) {
void loadConfig()
void loadChargeRules()
}
}) })
function sortRules(rules: CustomerDimensionRule[]) { function sortRules(rules: CustomerDimensionRule[]) {
@@ -101,7 +120,7 @@ function sortRules(rules: CustomerDimensionRule[]) {
} }
async function loadConfig() { async function loadConfig() {
if (!canView.value) return if (!canView.value || !initializationReady.value) return
loading.value = true loading.value = true
loadError.value = null loadError.value = null
try { try {
@@ -266,7 +285,7 @@ function formatDate(value: string | null) {
} }
async function loadChargeRules() { async function loadChargeRules() {
if (!canChargeView.value) return if (!canChargeView.value || !initializationReady.value) return
chargeLoading.value = true chargeLoading.value = true
try { try {
chargeRules.value = await listCustomerChargeRules(props.customerId) chargeRules.value = await listCustomerChargeRules(props.customerId)
@@ -287,17 +306,29 @@ async function loadChargeRules() {
finally { chargeLoading.value = false } finally { chargeLoading.value = false }
} }
onMounted(() => { void loadConfig(); void loadChargeRules() }) onMounted(() => {
if (!initializationReady.value) return
void loadConfig()
void loadChargeRules()
})
</script> </script>
<template> <template>
<div class="dimension-rule-panel"> <div class="dimension-rule-panel">
<div v-if="!canView || loadError" class="panel-title rule-heading"> <div v-if="initializationPending" class="rule-state initialization-state" role="status">
<Ruler :size="30" />
<strong>客户计费规则正在初始化</strong>
<p>默认箱规和权重系数正在后台准备,请稍后重新加载客户详情。</p>
</div>
<div v-else-if="initializationFailed" class="rule-error initialization-state" role="alert">
<div><strong>客户计费规则初始化失败</strong><p>请在客户管理中重试初始化,成功后再查看计费规则。</p></div>
</div>
<div v-if="initializationReady && (!canView || loadError)" class="panel-title rule-heading">
<div><h2>按箱规尺寸计算</h2><p>{{ customerName }}{{ customerCode ? `(${customerCode})` : '' }}的尺寸计算规则</p></div> <div><h2>按箱规尺寸计算</h2><p>{{ customerName }}{{ customerCode ? `(${customerCode})` : '' }}的尺寸计算规则</p></div>
</div> </div>
<div v-if="!canView" class="rule-state"><Ruler :size="30" /><strong>暂无尺寸计算规则查看权限</strong><p>请联系管理员申请尺寸计算规则查看权限。</p></div> <div v-if="initializationReady && !canView" class="rule-state"><Ruler :size="30" /><strong>暂无尺寸计算规则查看权限</strong><p>请联系管理员申请尺寸计算规则查看权限。</p></div>
<div v-else-if="loadError" class="rule-error" role="alert"><div><strong>箱规尺寸规则加载失败</strong><p>{{ loadError.message }}</p><ApiErrorMeta :details="loadError" /></div><el-button type="primary" plain :loading="loading" @click="loadConfig">重新加载</el-button></div> <div v-else-if="initializationReady && loadError" class="rule-error" role="alert"><div><strong>箱规尺寸规则加载失败</strong><p>{{ loadError.message }}</p><ApiErrorMeta :details="loadError" /></div><el-button type="primary" plain :loading="loading" @click="loadConfig">重新加载</el-button></div>
<section v-else-if="config" class="rule-card"> <section v-else-if="initializationReady && config" class="rule-card">
<header class="rule-card-heading"> <header class="rule-card-heading">
<div class="rule-title-row"> <div class="rule-title-row">
<h2>按箱规尺寸计算</h2> <h2>按箱规尺寸计算</h2>
@@ -315,9 +346,9 @@ onMounted(() => { void loadConfig(); void loadChargeRules() })
<template #empty><div class="rule-state"><Ruler :size="28" /><strong>当前客户暂无箱规尺寸规则</strong></div></template> <template #empty><div class="rule-state"><Ruler :size="28" /><strong>当前客户暂无箱规尺寸规则</strong></div></template>
</DataTable> </DataTable>
</section> </section>
<div v-else v-loading="loading" class="loading-state" /> <div v-else-if="initializationReady" v-loading="loading" class="loading-state" />
<section v-if="canChargeView" class="rule-card charge-rule-card"> <section v-if="initializationReady && canChargeView" class="rule-card charge-rule-card">
<header class="rule-card-heading"><div><div class="rule-title-row"><h2>箱规按权重系数计算</h2><el-checkbox :model-value="defaultCalculationMode === 'WEIGHT'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('WEIGHT', Boolean($event))">设为默认规则</el-checkbox></div><p class="section-hint">勾选后客户下单数据才会按该规则调整;未勾选时默认使用实测原尺寸(不调整)。</p></div></header> <header class="rule-card-heading"><div><div class="rule-title-row"><h2>箱规按权重系数计算</h2><el-checkbox :model-value="defaultCalculationMode === 'WEIGHT'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('WEIGHT', Boolean($event))">设为默认规则</el-checkbox></div><p class="section-hint">勾选后客户下单数据才会按该规则调整;未勾选时默认使用实测原尺寸(不调整)。</p></div></header>
<el-table v-loading="chargeLoading" :data="chargeDisplayRows" border stripe empty-text="暂无系数配置"> <el-table v-loading="chargeLoading" :data="chargeDisplayRows" border stripe empty-text="暂无系数配置">
<el-table-column prop="boxTypeName" label="箱规类型" min-width="110" /> <el-table-column prop="boxTypeName" label="箱规类型" min-width="110" />
@@ -412,7 +412,7 @@ watch(orderPage, (page, previousPage) => {
<section v-else-if="activeTab === 'statements'" class="detail-panel module-panel"><CustomerStatementsPanel :customer-id="tenantId" :customer-name="customer.tenantName" /></section> <section v-else-if="activeTab === 'statements'" class="detail-panel module-panel"><CustomerStatementsPanel :customer-id="tenantId" :customer-name="customer.tenantName" /></section>
<section v-else-if="activeTab === 'logs'" class="detail-panel module-panel"><CustomerAuditLogsPanel :customer-id="tenantId" :customer-name="customer.tenantName" /></section> <section v-else-if="activeTab === 'logs'" class="detail-panel module-panel"><CustomerAuditLogsPanel :customer-id="tenantId" :customer-name="customer.tenantName" /></section>
<section v-else class="detail-panel module-panel"><CustomerBillingRulesPanel :customer-id="tenantId" :customer-name="customer.tenantName" :customer-code="customer.tenantCode" /><CustomerOverseasStorageFeePanel :customer-id="tenantId" :customer-name="customer.tenantName" /></section> <section v-else class="detail-panel module-panel"><CustomerBillingRulesPanel :customer-id="tenantId" :customer-name="customer.tenantName" :customer-code="customer.tenantCode" :dimension-initialization-status="customer.dimensionInitializationStatus" /><CustomerOverseasStorageFeePanel :customer-id="tenantId" :customer-name="customer.tenantName" /></section>
</div> </div>
</DetailPageLayout> </DetailPageLayout>
+1
View File
@@ -29,6 +29,7 @@ export interface PlatformTenantResource {
tenantCode: string tenantCode: string
tenantName: string tenantName: string
status: PlatformTenantStatus status: PlatformTenantStatus
dimensionInitializationStatus?: 'PENDING' | 'SENDING' | 'READY' | 'FAILED' | string
legalName: string | null legalName: string | null
legalRepresentative: string | null legalRepresentative: string | null
unifiedSocialCreditCode: string | null unifiedSocialCreditCode: string | null