优化
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance } from 'element-plus'
|
||||
import { Pencil, RefreshCw } from '@lucide/vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import { getCustomerOverseasStorageFees, updateCustomerOverseasStorageFees, type OverseasStorageFeeConfig, type OverseasStorageFeeTier } from './overseasStorageFee'
|
||||
|
||||
const props = defineProps<{ customerId: string; customerName: string }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const canView = computed(() => access.can('platform:customer:view'))
|
||||
const canEdit = computed(() => access.can('platform:customer:edit'))
|
||||
const config = ref<OverseasStorageFeeConfig | null>(null)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref<ApiErrorDetails | null>(null)
|
||||
const visible = ref(false)
|
||||
const dirty = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive<{ currency: string; tiers: OverseasStorageFeeTier[]; version: number }>({ currency: 'USD', tiers: [], version: 0 })
|
||||
const currencies = ['USD', 'CNY', 'HKD', 'EUR', 'GBP']
|
||||
const storageTiers = computed(() => form.tiers.filter((tier) => tier.feeType === 'STORAGE'))
|
||||
const returnTiers = computed(() => form.tiers.filter((tier) => tier.feeType === 'RETURN_STORAGE'))
|
||||
|
||||
const { captureBaseline } = useUnsavedWorkRegistration({ key: `customer-overseas-storage-fee-${props.customerId}`, label: computed(() => `${props.customerName}的海外仓费用`), isOpen: visible, model: form, requiredPermission: computed(() => 'platform:customer:edit') })
|
||||
watch(form, () => { if (visible.value) dirty.value = true }, { deep: true })
|
||||
watch(() => props.customerId, () => { visible.value = false; void load() })
|
||||
|
||||
async function load() {
|
||||
if (!canView.value) return
|
||||
loading.value = true; error.value = null
|
||||
try { config.value = await getCustomerOverseasStorageFees(props.customerId) }
|
||||
catch (cause) { config.value = null; error.value = getApiErrorDetails(cause, '海外仓费用加载失败,请稍后重试') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function cloneTiers(tiers: OverseasStorageFeeTier[]) { return tiers.map((tier) => ({ ...tier })) }
|
||||
async function openEditor() {
|
||||
if (!config.value) return
|
||||
form.currency = config.value.currency || 'USD'; form.tiers = cloneTiers(config.value.tiers); form.version = config.value.version
|
||||
await nextTick(); dirty.value = false; visible.value = true; await nextTick(); captureBaseline(); formRef.value?.clearValidate()
|
||||
}
|
||||
async function closeEditor() {
|
||||
if (saving.value) return
|
||||
if (dirty.value) { try { await ElMessageBox.confirm('海外仓费用尚未保存,确定放弃修改吗?', '放弃编辑', { confirmButtonText: '放弃', cancelButtonText: '继续编辑', type: 'warning' }) } catch { return } }
|
||||
visible.value = false
|
||||
}
|
||||
function validate() {
|
||||
if (storageTiers.value.length !== 5 || returnTiers.value.length !== 4) { ElMessage.error('仓租必须为5档,退货仓储费必须为4档'); return false }
|
||||
for (const [label, tiers] of [['仓租', storageTiers.value], ['退货仓储费', returnTiers.value] ] as const) {
|
||||
let expected = 1
|
||||
for (const [index, tier] of tiers.entries()) {
|
||||
if (!Number.isInteger(tier.startDay) || tier.startDay !== expected || tier.startDay < 1) { ElMessage.error(`${label}第${index + 1}档天数必须连续且从第1天开始`); return false }
|
||||
if (!Number.isFinite(tier.unitPrice) || tier.unitPrice < 0) { ElMessage.error(`${label}第${index + 1}档价格不能小于0`); return false }
|
||||
if (index === tiers.length - 1) { if (tier.endDay !== null) { ElMessage.error(`${label}最后一档必须为“以上”`); return false } }
|
||||
else if (!Number.isInteger(tier.endDay) || tier.endDay! < tier.startDay) { ElMessage.error(`${label}第${index + 1}档结束天数无效`); return false }
|
||||
if (tier.endDay !== null) expected = tier.endDay + 1
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
async function save() {
|
||||
if (!config.value || !validate()) return
|
||||
saving.value = true
|
||||
try {
|
||||
await updateCustomerOverseasStorageFees(props.customerId, { customerId: props.customerId, customerName: props.customerName, currency: form.currency, version: form.version, tiers: form.tiers.map(({ feeType, billingUnit, startDay, endDay, unitPrice }) => ({ feeType, billingUnit, startDay, endDay, unitPrice })) })
|
||||
await load(); dirty.value = false; visible.value = false; ElMessage.success('海外仓费用已保存')
|
||||
} catch (cause) { ElMessage.error(getApiErrorDetails(cause, '海外仓费用保存失败,请刷新后重试').message) }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
function dayLabel(tier: OverseasStorageFeeTier) { return tier.endDay === null ? `第${tier.startDay}天以上` : `第${tier.startDay}-${tier.endDay}天` }
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="overseas-fee-panel">
|
||||
<header class="panel-heading"><div><h2>海外仓费用</h2><p>客户专属仓租和退货仓储费阶梯</p></div><el-button v-if="canEdit && config" type="primary" :loading="loading" @click="openEditor"><Pencil :size="16" />编辑费用</el-button></header>
|
||||
<div v-if="error" class="fee-error" role="alert"><div><strong>海外仓费用加载失败</strong><p>{{ error.message }}</p><ApiErrorMeta :details="error" /></div><el-button plain type="primary" :loading="loading" @click="load"><RefreshCw :size="16" />重新加载</el-button></div>
|
||||
<div v-else-if="loading && !config" v-loading="true" class="fee-loading" />
|
||||
<template v-else-if="config">
|
||||
<div class="fee-meta"><span>币种:<strong>{{ config.currency }}</strong></span><span>按整组保存,版本 {{ config.version }}</span></div>
|
||||
<div class="fee-groups"><div><h3>仓租({{ config.currency }}/CBM/天)</h3><table><thead><tr><th>天数</th><th>价格</th></tr></thead><tbody><tr v-for="tier in config.tiers.filter((item) => item.feeType === 'STORAGE')" :key="tier.id"><td>{{ dayLabel(tier) }}</td><td>{{ tier.unitPrice }}</td></tr></tbody></table></div><div><h3>退货仓储费({{ config.currency }}/托/天)</h3><table><thead><tr><th>天数</th><th>价格</th></tr></thead><tbody><tr v-for="tier in config.tiers.filter((item) => item.feeType === 'RETURN_STORAGE')" :key="tier.id"><td>{{ dayLabel(tier) }}</td><td>{{ tier.unitPrice }}</td></tr></tbody></table></div></div>
|
||||
</template>
|
||||
<div v-else class="fee-empty">当前客户暂无海外仓费用配置</div>
|
||||
<AppDialog v-model="visible" title="编辑海外仓费用" width="min(1120px, calc(100vw - 28px))" :confirm-loading="saving" :show-cancel="false" @confirm="save">
|
||||
<el-form ref="formRef" :model="form" label-position="top"><el-form-item label="币种"><el-select v-model="form.currency" style="width:180px"><el-option v-for="currency in currencies" :key="currency" :label="currency" :value="currency" /></el-select></el-form-item><div class="edit-groups"><div v-for="(group, groupIndex) in [storageTiers, returnTiers]" :key="groupIndex"><h3>{{ groupIndex === 0 ? '仓租(CBM/天)' : '退货仓储费(托/天)' }}</h3><table><thead><tr><th>开始天数</th><th>结束天数</th><th>价格</th></tr></thead><tbody><tr v-for="tier in group" :key="tier.id"><td><el-input-number v-model="tier.startDay" :min="1" :max="36500" :controls="false" /></td><td><el-input-number v-if="tier.endDay !== null" v-model="tier.endDay" :min="1" :max="36500" :controls="false" /><span v-else>以上</span></td><td><el-input-number v-model="tier.unitPrice" :min="0" :precision="6" :controls="false" /></td></tr></tbody></table></div></div></el-form>
|
||||
<template #footer><div class="dialog-actions"><el-button :disabled="saving" @click="closeEditor">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存费用</el-button></div></template>
|
||||
</AppDialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.overseas-fee-panel { margin-top: 18px; border: 1px solid var(--cc-border); background: #fff; }.panel-heading { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:18px; border-bottom:1px solid var(--cc-border); }.panel-heading h2,.fee-groups h3,.edit-groups h3 { margin:0; color:var(--cc-text); font-size:16px; }.panel-heading p { margin:5px 0 0; color:var(--cc-muted); font-size:12px; }.fee-meta { display:flex; flex-wrap:wrap; gap:24px; padding:12px 18px; color:var(--cc-muted); font-size:12px; background:#f8fafc; }.fee-meta strong { color:var(--cc-text); }.fee-groups,.edit-groups { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:18px; padding:18px; }.fee-groups table,.edit-groups table { width:100%; border-collapse:collapse; font-size:12px; }.fee-groups th,.fee-groups td,.edit-groups th,.edit-groups td { padding:9px 10px; border-bottom:1px solid var(--cc-border); text-align:left; }.fee-groups th,.edit-groups th { color:var(--cc-muted); font-weight:650; }.edit-groups :deep(.el-input-number) { width:130px; }.fee-error { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:20px; background:#fff4f3; }.fee-error p { margin:5px 0; color:#b42318; font-size:12px; }.fee-loading { min-height:220px; }.fee-empty { min-height:160px; display:grid; place-items:center; color:var(--cc-muted); }.dialog-actions { display:flex; justify-content:flex-end; gap:8px; }.dialog-actions :deep(.el-button + .el-button) { margin-left:0; }
|
||||
@media (max-width:760px) { .panel-heading,.fee-error { align-items:stretch; flex-direction:column; }.fee-groups,.edit-groups { grid-template-columns:1fr; padding:12px; }.edit-groups { overflow:auto; }.edit-groups > div { min-width:620px; }.panel-heading :deep(.el-button) { min-height:44px; } }
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getCustomerOverseasStorageFees, updateCustomerOverseasStorageFees } from './overseasStorageFee'
|
||||
import { http } from '@/shared/api/http'
|
||||
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { get: vi.fn(), put: vi.fn() },
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
|
||||
const config = { id: 12, customerId: 9007199254740993n.toString(), customerName: '客户', currency: 'USD', tiers: [], version: 0, createdAt: null, updatedAt: null }
|
||||
|
||||
describe('customer overseas storage fee api', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('keeps customer ids as strings and uses the customer detail path', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({ data: config })
|
||||
const result = await getCustomerOverseasStorageFees(config.customerId)
|
||||
expect(http.get).toHaveBeenCalledWith(`/platform/v1/iam/tenants/${config.customerId}/overseas-storage-fees`)
|
||||
expect(result.id).toBe('12')
|
||||
expect(result.customerId).toBe(config.customerId)
|
||||
})
|
||||
|
||||
it('submits the complete group and reloads the new version', async () => {
|
||||
vi.mocked(http.put).mockResolvedValue({ data: null })
|
||||
vi.mocked(http.get).mockResolvedValue({ data: { ...config, version: 1 } })
|
||||
const request = { customerId: config.customerId, customerName: '客户', currency: 'CNY', version: 0, tiers: [{ feeType: 'STORAGE' as const, billingUnit: 'CBM' as const, startDay: 1, endDay: null, unitPrice: 0 }] }
|
||||
const result = await updateCustomerOverseasStorageFees(config.customerId, request)
|
||||
expect(http.put).toHaveBeenCalledWith(`/platform/v1/iam/tenants/${config.customerId}/overseas-storage-fees`, request)
|
||||
expect(result.version).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
|
||||
export type OverseasFeeType = 'STORAGE' | 'RETURN_STORAGE'
|
||||
export type OverseasBillingUnit = 'CBM' | 'PALLET'
|
||||
|
||||
export interface OverseasStorageFeeTier {
|
||||
id: string
|
||||
startDay: number
|
||||
endDay: number | null
|
||||
unitPrice: number
|
||||
sortNo: number
|
||||
feeType: OverseasFeeType
|
||||
billingUnit: OverseasBillingUnit
|
||||
}
|
||||
|
||||
export interface OverseasStorageFeeConfig {
|
||||
id: string
|
||||
customerId: string
|
||||
customerName: string
|
||||
currency: string
|
||||
tiers: OverseasStorageFeeTier[]
|
||||
version: number
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
export interface OverseasStorageFeeUpdateRequest {
|
||||
customerId: string
|
||||
customerName: string
|
||||
currency: string
|
||||
tiers: Array<Pick<OverseasStorageFeeTier, 'feeType' | 'billingUnit' | 'startDay' | 'endDay' | 'unitPrice'>>
|
||||
version: number
|
||||
}
|
||||
|
||||
const path = (tenantId: string) => `/platform/v1/iam/tenants/${encodeURIComponent(tenantId)}/overseas-storage-fees`
|
||||
|
||||
type RawConfig = Omit<OverseasStorageFeeConfig, 'id' | 'customerId' | 'version' | 'tiers'> & {
|
||||
id: string | number
|
||||
customerId: string | number
|
||||
version: string | number
|
||||
tiers: Array<Omit<OverseasStorageFeeTier, 'id'> & { id: string | number; unitPrice: number | string }>
|
||||
}
|
||||
|
||||
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 })),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCustomerOverseasStorageFees(tenantId: string) {
|
||||
const response = await http.get<never, ApiEnvelope<RawConfig>>(path(tenantId))
|
||||
return normalize(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function updateCustomerOverseasStorageFees(tenantId: string, request: OverseasStorageFeeUpdateRequest) {
|
||||
await http.put<never, ApiEnvelope<null>>(path(tenantId), request)
|
||||
return getCustomerOverseasStorageFees(tenantId)
|
||||
}
|
||||
|
||||
export async function listOverseasStorageFees(page = 1, size = 20, customerName?: string) {
|
||||
const response = await http.get<never, ApiEnvelope<{ records: RawConfig[]; total: number; page: number; size: number }>>('/platform/v1/overseas-storage-fees', { params: { page, size, customerName } })
|
||||
const result = unwrapApiResponse(response)
|
||||
return { ...result, records: result.records.map(normalize) }
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v
|
||||
import StatusBadge from '@/shared/components/StatusBadge.vue'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import CustomerBillingRulesPanel from '@/modules/customers/components/CustomerBillingRulesPanel.vue'
|
||||
import CustomerOverseasStorageFeePanel from '@/modules/customers/CustomerOverseasStorageFeePanel.vue'
|
||||
|
||||
type DetailTab = 'basic' | 'business' | 'orders' | 'finance' | 'logs' | 'billing'
|
||||
type PreviewState = { url: string; kind: 'image' | 'pdf'; loading: boolean; failed: boolean }
|
||||
@@ -423,7 +424,7 @@ watch(orderPage, (page, previousPage) => {
|
||||
|
||||
<section v-else-if="activeTab === 'finance'" class="detail-panel module-panel"><div class="panel-title"><div><h2>财务账单</h2><p>客户应收、回款与对账记录</p></div><ReceiptText :size="19" /></div><div class="module-empty"><ReceiptText :size="30" /><strong>财务账单尚未接入</strong><p>当前阶段没有可用的客户账单 API,页面不会使用原型中的演示金额。</p></div></section>
|
||||
<section v-else-if="activeTab === 'logs'" class="detail-panel module-panel"><div class="panel-title"><div><h2>操作日志</h2><p>客户档案与状态变更记录</p></div><History :size="19" /></div><div class="module-empty"><History :size="30" /><strong>操作日志尚未接入</strong><p>等待审计服务提供按租户查询的只读接口后展示真实操作记录。</p></div></section>
|
||||
<section v-else class="detail-panel module-panel"><CustomerBillingRulesPanel :customer-id="tenantId" :customer-name="customer.tenantName" :customer-code="customer.tenantCode" /></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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Edit, RefreshCw, Search } from '@lucide/vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { getApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listOverseasStorageFees, updateCustomerOverseasStorageFees, type OverseasStorageFeeConfig, type OverseasStorageFeeTier } from '../overseasStorageFee'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const loading = ref(false)
|
||||
const error = ref<ReturnType<typeof getApiErrorDetails> | null>(null)
|
||||
const rows = ref<OverseasStorageFeeConfig[]>([])
|
||||
const page = ref(1)
|
||||
const size = ref(20)
|
||||
const total = ref(0)
|
||||
const keyword = ref('')
|
||||
const dialogOpen = ref(false)
|
||||
const saving = ref(false)
|
||||
const editing = ref<OverseasStorageFeeConfig | null>(null)
|
||||
const form = reactive({ currency: 'USD', version: 0, tiers: [] as OverseasStorageFeeTier[] })
|
||||
|
||||
const storageTiers = computed(() => form.tiers.filter((tier) => tier.feeType === 'STORAGE'))
|
||||
const returnTiers = computed(() => form.tiers.filter((tier) => tier.feeType === 'RETURN_STORAGE'))
|
||||
|
||||
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(' / ') }
|
||||
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) }
|
||||
catch (cause) { error.value = getApiErrorDetails(cause, '海外仓阶梯费用加载失败,请稍后重试') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function search() { page.value = 1; void load() }
|
||||
function openEdit(row: OverseasStorageFeeConfig) { editing.value = row; form.currency = row.currency || 'USD'; form.version = row.version; form.tiers = clone(row.tiers); dialogOpen.value = true }
|
||||
async function save() {
|
||||
if (!editing.value || storageTiers.value.length !== 5 || returnTiers.value.length !== 4) { ElMessage.error('费用配置必须包含仓租5档和退货仓储费4档'); return }
|
||||
saving.value = true
|
||||
try { await updateCustomerOverseasStorageFees(editing.value.customerId, { customerId: editing.value.customerId, customerName: editing.value.customerName, currency: form.currency, version: form.version, tiers: form.tiers.map(({ feeType, billingUnit, startDay, endDay, unitPrice }) => ({ feeType, billingUnit, startDay, endDay, unitPrice })) }); dialogOpen.value = false; ElMessage.success('海外仓费用已保存'); await load() }
|
||||
catch (cause) { ElMessage.error(getApiErrorDetails(cause, '保存失败,请刷新后重试').message) }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
onMounted(() => { void load() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="business-page overseas-fees-page">
|
||||
<section class="page-header"><div><h1>海外仓阶梯费用</h1><p>维护客户仓租和退货仓储费,初始化后可调整天数、价格和币种。</p></div><el-button :loading="loading" @click="load"><RefreshCw :size="16" />刷新</el-button></section>
|
||||
<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>
|
||||
<section v-else class="surface-panel">
|
||||
<div class="toolbar"><el-input v-model="keyword" clearable placeholder="搜索客户名称" @keyup.enter="search"><template #prefix><Search :size="16" /></template></el-input><el-button type="primary" @click="search">查询</el-button></div>
|
||||
<el-table :data="rows" v-loading="loading" row-key="id" empty-text="暂无海外仓阶梯费用配置">
|
||||
<el-table-column label="操作" width="100" fixed="left"><template #default="{ row }"><el-button v-if="access.can('platform:customer:edit')" link type="primary" @click="openEdit(row)"><Edit :size="15" />编辑</el-button></template></el-table-column>
|
||||
<el-table-column prop="customerName" label="客户" min-width="180" /><el-table-column prop="customerId" label="客户ID" min-width="180" />
|
||||
<el-table-column prop="currency" label="币种" width="90" /><el-table-column label="仓租" min-width="260"><template #default="{ row }">{{ summary(row as OverseasStorageFeeConfig, 'STORAGE') }}</template></el-table-column>
|
||||
<el-table-column label="退货仓储费" min-width="260"><template #default="{ row }">{{ summary(row as OverseasStorageFeeConfig, 'RETURN_STORAGE') }}</template></el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination"><el-pagination v-model:current-page="page" v-model:page-size="size" :total="total" layout="total, sizes, prev, pager, next" @current-change="load" @size-change="search" /></div>
|
||||
</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>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</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; } }
|
||||
</style>
|
||||
@@ -7,6 +7,12 @@ export const customerRoutes = [
|
||||
component: () => import('@/modules/customers/pages/CustomersView.vue'),
|
||||
meta: { title: '客户管理', description: '维护客户企业资料、合作状态和注册审核', permission: 'platform:customer:list', permissionAnyOf: ['platform:customer:review'] },
|
||||
},
|
||||
{
|
||||
path: 'customers/overseas-storage-fees',
|
||||
name: 'overseas-storage-fees',
|
||||
component: () => import('@/modules/customers/pages/OverseasStorageFeesView.vue'),
|
||||
meta: { title: '海外仓阶梯费用', description: '维护客户仓租和退货仓储费阶梯', permission: 'platform:overseas-storage-fee:list' },
|
||||
},
|
||||
{
|
||||
path: 'customers/:tenantId',
|
||||
name: 'customer-detail',
|
||||
|
||||
Reference in New Issue
Block a user