This commit is contained in:
2026-08-08 14:41:17 +08:00
parent a3a09d1dec
commit 315c370ec9
3 changed files with 24 additions and 6 deletions
@@ -28,4 +28,10 @@ describe('customer overseas storage fee api', () => {
expect(http.put).toHaveBeenCalledWith(`/platform/v1/iam/tenants/${config.customerId}/overseas-storage-fees`, request)
expect(result.version).toBe(1)
})
it('normalizes an omitted end day as an open-ended tier', async () => {
vi.mocked(http.get).mockResolvedValue({ data: { ...config, tiers: [{ id: 1, startDay: 91, unitPrice: 2, sortNo: 5, feeType: 'STORAGE', billingUnit: 'CBM' }] } })
const result = await getCustomerOverseasStorageFees(config.customerId)
expect(result.tiers[0]?.endDay).toBeNull()
})
})
+1 -1
View File
@@ -45,7 +45,7 @@ function normalize(data: RawConfig): OverseasStorageFeeConfig {
return {
...data,
id: String(data.id), customerId: String(data.customerId), version: Number(data.version),
tiers: data.tiers.map((tier) => ({ ...tier, id: String(tier.id), unitPrice: Number(tier.unitPrice), feeType: tier.feeType as OverseasFeeType, billingUnit: tier.billingUnit as OverseasBillingUnit })),
tiers: data.tiers.map((tier) => ({ ...tier, id: String(tier.id), endDay: tier.endDay == null ? null : Number(tier.endDay), unitPrice: Number(tier.unitPrice), feeType: tier.feeType as OverseasFeeType, billingUnit: tier.billingUnit as OverseasBillingUnit })),
}
}
@@ -32,13 +32,13 @@ const columns = [
{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 180 },
{ key: 'customerId', prop: 'customerId', label: '客户ID', minWidth: 180 },
{ key: 'currency', prop: 'currency', label: '币种', width: 90 },
{ key: 'storage', slotName: 'storage', label: '仓租', minWidth: 260 },
{ key: 'returnStorage', slotName: 'returnStorage', label: '退货仓储费', minWidth: 260 },
{ key: 'storage', slotName: 'storage', label: '仓租(CBM/天)', minWidth: 300 },
{ key: 'returnStorage', slotName: 'returnStorage', label: '退货仓储费(托/天)', minWidth: 300 },
]
function clone(tiers: OverseasStorageFeeTier[]) { return tiers.map((tier) => ({ ...tier })) }
function label(tier: OverseasStorageFeeTier) { return tier.endDay === null ? `${tier.startDay}天以上` : `${tier.startDay}-${tier.endDay}天` }
function summary(row: OverseasStorageFeeConfig, feeType: OverseasStorageFeeTier['feeType']) { return row.tiers.filter((tier) => tier.feeType === feeType).map((tier) => `${label(tier)} ${tier.unitPrice}`).join(' / ') }
function tiersFor(row: OverseasStorageFeeConfig, feeType: OverseasStorageFeeTier['feeType']) { return row.tiers.filter((tier) => tier.feeType === feeType).sort((a, b) => a.sortNo - b.sortNo) }
async function load() {
loading.value = true; error.value = null
try { const result = await listOverseasStorageFees(page.value, size.value, keyword.value.trim() || undefined); rows.value = result.records; total.value = Number(result.total) }
@@ -62,7 +62,12 @@ onMounted(() => { void load() })
<section v-if="error" class="surface-panel error-state" role="alert"><div><strong>{{ error.message }}</strong><ApiErrorMeta :details="error" /></div><el-button type="primary" @click="load">重新加载</el-button></section>
<QueryToolbar v-else v-model="filters" class="surface-panel" :fields="queryFields" ariaLabel="海外仓阶梯费用筛选" preference-key="overseas-storage-fees-query" @search="search" @reset="() => { filters.keyword = ''; search() }" />
<section v-if="!error" class="surface-panel table-panel">
<DataTable v-model:page="page" v-model:page-size="size" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-storage-fees-table" row-key="id" empty-text="暂无海外仓阶梯费用配置" @update:page="load" @update:page-size="load"><template #toolbar-start><div class="table-heading"><span>共 {{ total }} 个客户</span></div></template><template #actions="{ row }"><el-button v-if="access.can('platform:customer:edit')" link type="primary" @click="openEdit(row as OverseasStorageFeeConfig)"><Edit :size="15" />编辑</el-button></template><template #storage="{ row }">{{ summary(row as OverseasStorageFeeConfig, 'STORAGE') }}</template><template #returnStorage="{ row }">{{ summary(row as OverseasStorageFeeConfig, 'RETURN_STORAGE') }}</template></DataTable>
<DataTable v-model:page="page" v-model:page-size="size" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-storage-fees-table" row-key="id" empty-text="暂无海外仓阶梯费用配置" @update:page="load" @update:page-size="load">
<template #toolbar-start><div class="table-heading"><span>共 {{ total }} 个客户</span></div></template>
<template #actions="{ row }"><el-button v-if="access.can('platform:customer:edit')" link type="primary" @click="openEdit(row as OverseasStorageFeeConfig)"><Edit :size="15" />编辑</el-button></template>
<template #storage="{ row }"><div class="tier-list"><div v-for="tier in tiersFor(row as OverseasStorageFeeConfig, 'STORAGE')" :key="tier.id" class="tier-item"><span>{{ label(tier) }}</span><strong>{{ tier.unitPrice }} {{ (row as OverseasStorageFeeConfig).currency }}</strong></div></div></template>
<template #returnStorage="{ row }"><div class="tier-list"><div v-for="tier in tiersFor(row as OverseasStorageFeeConfig, 'RETURN_STORAGE')" :key="tier.id" class="tier-item"><span>{{ label(tier) }}</span><strong>{{ tier.unitPrice }} {{ (row as OverseasStorageFeeConfig).currency }}</strong></div></div></template>
</DataTable>
</section>
<AppDialog v-model="dialogOpen" title="编辑海外仓阶梯费用" width="min(1120px, calc(100vw - 28px))" :confirm-loading="saving" @confirm="save">
<el-form label-position="top"><el-form-item label="币种"><el-select v-model="form.currency" style="width:180px"><el-option label="USD" value="USD" /><el-option label="CNY" value="CNY" /><el-option label="EUR" value="EUR" /></el-select></el-form-item><div class="groups"><div v-for="(group, index) in [storageTiers, returnTiers]" :key="index"><h3>{{ index === 0 ? '仓租(CBM/天)' : '退货仓储费(托/天)' }}</h3><el-table :data="group" size="small"><el-table-column label="开始天数"><template #default="{ row }"><el-input-number v-model="row.startDay" :min="1" :controls="false" /></template></el-table-column><el-table-column label="结束天数"><template #default="{ row }"><el-input-number v-if="row.endDay !== null" v-model="row.endDay" :min="1" :controls="false" /><span v-else>以上</span></template></el-table-column><el-table-column label="价格"><template #default="{ row }"><el-input-number v-model="row.unitPrice" :min="0" :precision="6" :controls="false" /></template></el-table-column></el-table></div></div></el-form>
@@ -71,5 +76,12 @@ onMounted(() => { void load() })
</template>
<style scoped>
.page-header,.toolbar,.pagination { display:flex; align-items:center; justify-content:space-between; gap:12px; }.page-header { margin-bottom:16px; }.page-header h1 { margin:0; font-size:22px; }.page-header p { margin:6px 0 0; color:var(--cc-muted); font-size:13px; }.toolbar { justify-content:flex-start; padding:16px; border-bottom:1px solid var(--cc-border); }.toolbar :deep(.el-input) { width:min(320px,100%); }.pagination { justify-content:flex-end; padding:16px; }.error-state { display:flex; justify-content:space-between; padding:20px; color:#b42318; }.groups { display:grid; grid-template-columns:1fr 1fr; gap:20px; }.groups h3 { margin:0 0 10px; font-size:15px; } @media (max-width:720px) { .groups { grid-template-columns:1fr; }.page-header { align-items:flex-start; flex-direction:column; } }
.error-state { display:flex; justify-content:space-between; padding:20px; color:#b42318; }
.tier-list { display:grid; grid-template-columns:repeat(2,minmax(130px,1fr)); gap:4px 8px; padding:2px 0; }
.tier-item { display:flex; align-items:center; justify-content:space-between; gap:6px; min-height:24px; padding:2px 6px; border-left:2px solid #409eff; background:#f6f9fd; }
.tier-item span { color:var(--cc-muted); font-size:12px; white-space:nowrap; }
.tier-item strong { color:var(--cc-text); font-size:12px; font-weight:650; white-space:nowrap; }
.groups { display:grid; grid-template-columns:1fr 1fr; gap:20px; }
.groups h3 { margin:0 0 10px; font-size:15px; }
@media (max-width:720px) { .groups,.tier-list { grid-template-columns:1fr; } }
</style>