This commit is contained in:
2026-08-05 15:54:59 +08:00
parent eede351038
commit 4b31eb7786
43 changed files with 2117 additions and 851 deletions
@@ -0,0 +1,84 @@
import ElementPlus from 'element-plus'
import { createPinia, setActivePinia } from 'pinia'
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
getCustomerPricingRule,
listCustomerPricingRules,
type CustomerPricingRule,
} from '@/modules/fba-pricing'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import CustomerBillingRulesPanel from './CustomerBillingRulesPanel.vue'
vi.mock('@/modules/fba-pricing', () => ({
createCustomerPricingRuleVersion: vi.fn(),
disableCustomerPricingRule: vi.fn(),
getCustomerPricingRule: vi.fn(),
listCustomerPricingRules: vi.fn(),
}))
class ResizeObserverStub implements ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}
const rule: CustomerPricingRule = {
id: '91', tenantId: '8', ruleCode: 'FBA-SEA-KG', customerId: '8', customerName: '柏悦物流',
businessType: 'FBA_SEA', chargeMode: 'KG', taxMode: 'TAX_INCLUDED', conversionFactor: 167,
roundingScale: 3, roundingMode: 'CEILING', currency: 'CNY', effectiveFrom: '2026-08-01T00:00:00',
effectiveTo: null, ruleVersion: 2, status: 'ACTIVE', remark: '标准规则', createdAt: null, updatedAt: null,
}
enableAutoUnmount(afterEach)
describe('CustomerBillingRulesPanel', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
document.body.innerHTML = ''
const pinia = createPinia()
setActivePinia(pinia)
const auth = useAuthStore()
auth.token = 'token'
auth.permissions = ['firstleg:fba-pricing:rule:view', 'firstleg:fba-pricing:rule:edit']
useAccessPreviewStore().initializeAfterLogin()
vi.mocked(listCustomerPricingRules).mockResolvedValue([rule])
vi.mocked(getCustomerPricingRule).mockResolvedValue(rule)
})
it('loads only the current customer rules and exposes version actions', async () => {
mount(CustomerBillingRulesPanel, {
attachTo: document.body,
props: { customerId: '8', customerName: '柏悦物流', customerCode: 'TEN8' },
global: { plugins: [ElementPlus] },
})
await flushPromises()
expect(listCustomerPricingRules).toHaveBeenCalledWith({ tenantId: '8' })
expect(document.body.textContent).toContain('FBA-SEA-KG')
expect(document.body.textContent).toContain('FBA 海运')
expect(document.body.textContent).toContain('新建规则')
expect(document.body.textContent).toContain('新版本')
expect(document.body.textContent).toContain('停用')
})
it('loads rule details with the customer tenant scope', async () => {
mount(CustomerBillingRulesPanel, {
attachTo: document.body,
props: { customerId: '8', customerName: '柏悦物流' },
global: { plugins: [ElementPlus] },
})
await flushPromises()
const detailButtons = [...document.querySelectorAll<HTMLButtonElement>('button')]
.filter((button) => button.textContent?.trim() === '详情')
const detailButton = detailButtons.at(-1)
detailButton?.click()
await flushPromises()
expect(getCustomerPricingRule).toHaveBeenCalledWith('8', '91')
expect(document.body.textContent).toContain('标准规则')
})
})
@@ -0,0 +1,311 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { CircleSlash2, Eye, Plus, RefreshCw, Scale } from '@lucide/vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import {
createCustomerPricingRuleVersion,
disableCustomerPricingRule,
getCustomerPricingRule,
listCustomerPricingRules,
type CustomerPricingRule,
type CustomerRuleFormModel,
} from '@/modules/fba-pricing'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDrawer from '@/shared/components/AppDrawer.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
const props = defineProps<{
customerId: string
customerName: string
customerCode?: string
}>()
const access = useAccessPreviewStore()
const canView = computed(() => access.can('firstleg:fba-pricing:rule:view'))
const canEdit = computed(() => canView.value && access.can('firstleg:fba-pricing:rule:edit'))
const rules = ref<CustomerPricingRule[]>([])
const loading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const detailVisible = ref(false)
const detailLoading = ref(false)
const detailError = ref<ApiErrorDetails | null>(null)
const selectedRule = ref<CustomerPricingRule | null>(null)
const editorVisible = ref(false)
const editorSaving = ref(false)
const editorDirty = ref(false)
const editorForm = ref<FormInstance>()
const page = ref(1)
const pageSize = ref(20)
const columns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 210, fixed: 'left', slotName: 'actions' },
{ key: 'rule', label: '规则版本', minWidth: 160, slotName: 'rule' },
{ key: 'businessType', label: '业务类型', minWidth: 120, slotName: 'businessType' },
{ key: 'chargeMode', label: '计费方式', width: 105, slotName: 'chargeMode' },
{ key: 'taxMode', label: '税务模式', width: 100, slotName: 'taxMode' },
{ key: 'conversionFactor', prop: 'conversionFactor', label: '换算系数', width: 105 },
{ key: 'rounding', label: '舍入规则', minWidth: 165, slotName: 'rounding' },
{ key: 'currency', prop: 'currency', label: '币种', width: 80 },
{ key: 'period', label: '有效期', minWidth: 225, slotName: 'period' },
{ key: 'status', label: '状态', width: 90, slotName: 'status' },
]
const pagedRules = computed(() => rules.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
const emptyForm = (): CustomerRuleFormModel => ({
ruleCode: '',
customerId: props.customerId,
customerName: props.customerName,
businessType: 'FBA_SEA',
chargeMode: 'KG',
taxMode: 'TAX_INCLUDED',
conversionFactor: 167,
roundingScale: 3,
roundingMode: 'CEILING',
currency: 'CNY',
effectiveFrom: '',
effectiveTo: null,
remark: null,
})
const form = reactive<CustomerRuleFormModel>(emptyForm())
const formRules: FormRules<CustomerRuleFormModel> = {
ruleCode: [{ required: true, message: '请输入规则编码', trigger: 'blur' }],
businessType: [{ required: true, message: '请输入业务类型', trigger: 'blur' }],
chargeMode: [{ required: true, message: '请选择计费方式', trigger: 'change' }],
taxMode: [{ required: true, message: '请选择税务模式', trigger: 'change' }],
conversionFactor: [{ required: true, message: '请输入大于 0 的换算系数', trigger: 'blur', type: 'number', min: 0.000001 }],
roundingScale: [{ required: true, message: '请选择保留位数', trigger: 'change', type: 'number', min: 0, max: 4 }],
roundingMode: [{ required: true, message: '请选择舍入方式', trigger: 'change' }],
currency: [{ required: true, message: '请选择币种', trigger: 'change' }],
effectiveFrom: [{ required: true, message: '请选择生效时间', trigger: 'change' }],
}
const { captureBaseline } = useUnsavedWorkRegistration({
key: `customer-pricing-rule-${props.customerId}`,
label: computed(() => `${props.customerName}的计费规则版本`),
isOpen: editorVisible,
model: form,
requiredPermission: computed(() => 'firstleg:fba-pricing:rule:edit'),
})
watch(form, () => {
if (editorVisible.value) editorDirty.value = true
}, { deep: true })
async function loadRules() {
if (!canView.value) return
loading.value = true
loadError.value = null
try {
rules.value = await listCustomerPricingRules({ tenantId: props.customerId })
page.value = 1
} catch (error) {
rules.value = []
loadError.value = getApiErrorDetails(error, '客户计费规则加载失败,请稍后重试')
} finally {
loading.value = false
}
}
function resetForm(source?: CustomerPricingRule) {
Object.assign(form, emptyForm(), source ? {
ruleCode: source.ruleCode,
businessType: source.businessType,
chargeMode: source.chargeMode,
taxMode: source.taxMode,
conversionFactor: source.conversionFactor,
roundingScale: source.roundingScale,
roundingMode: source.roundingMode,
currency: source.currency,
remark: source.remark,
} : {})
form.customerId = props.customerId
form.customerName = props.customerName
}
async function openEditor(source?: CustomerPricingRule) {
resetForm(source)
editorDirty.value = false
editorVisible.value = true
await nextTick()
captureBaseline()
editorForm.value?.clearValidate()
}
async function confirmAbandon(): Promise<boolean> {
try {
await ElMessageBox.confirm('尚有未保存的规则版本,确定放弃吗?', '放弃编辑', {
confirmButtonText: '放弃', cancelButtonText: '继续编辑', type: 'warning',
})
return true
} catch {
return false
}
}
async function beforeEditorClose(done: () => void) {
if (editorSaving.value) return
if (!editorDirty.value || await confirmAbandon()) done()
}
async function closeEditor() {
if (editorDirty.value && !await confirmAbandon()) {
editorVisible.value = true
return
}
editorVisible.value = false
}
async function saveVersion() {
if (!await editorForm.value?.validate()) return
if (form.effectiveTo && form.effectiveTo <= form.effectiveFrom) {
ElMessage.warning('失效时间必须晚于生效时间')
return
}
editorSaving.value = true
try {
await createCustomerPricingRuleVersion(props.customerId, {
...form,
ruleCode: form.ruleCode.trim(),
customerId: props.customerId,
customerName: props.customerName,
businessType: form.businessType.trim(),
effectiveTo: form.effectiveTo || null,
remark: form.remark?.trim() || null,
})
editorDirty.value = false
editorVisible.value = false
ElMessage.success('计费规则新版本已创建')
await loadRules()
} catch (error) {
ElMessage.error(getApiErrorDetails(error, '计费规则版本创建失败').message)
} finally {
editorSaving.value = false
}
}
async function openDetail(row: CustomerPricingRule) {
detailVisible.value = true
detailLoading.value = true
detailError.value = null
selectedRule.value = row
try {
selectedRule.value = await getCustomerPricingRule(props.customerId, row.id)
} catch (error) {
detailError.value = getApiErrorDetails(error, '计费规则详情加载失败')
} finally {
detailLoading.value = false
}
}
async function disableRule(row: CustomerPricingRule) {
try {
await ElMessageBox.confirm(`确认停用规则“${row.ruleCode}”V${row.ruleVersion ?? '—'}吗?历史费用不会改变。`, '停用计费规则', {
confirmButtonText: '确认停用', cancelButtonText: '取消', type: 'warning',
})
await disableCustomerPricingRule(props.customerId, row.id)
ElMessage.success('计费规则已停用')
await loadRules()
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(getApiErrorDetails(error, '计费规则停用失败').message)
}
}
function asRule(value: unknown) { return value as CustomerPricingRule }
function formatDate(value: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '长期有效' }
function modeLabel(value: string) { return value === 'KG' ? '按 KG' : value === 'CBM' ? '按 CBM' : value }
function taxLabel(value: string) { return value === 'TAX_INCLUDED' ? '包税' : value === 'TAX_EXCLUDED' ? '不包税' : value }
function businessTypeLabel(value: string) { return value === 'FBA_SEA' ? 'FBA 海运' : value }
onMounted(loadRules)
</script>
<template>
<div class="billing-rules-panel">
<div class="panel-title billing-heading">
<div><h2>计费规则</h2><p>{{ customerName }}{{ customerCode ? `(${customerCode})` : '' }}的专属 FBA 计费版本</p></div>
<div class="heading-actions">
<el-button v-if="canView" :loading="loading" aria-label="刷新计费规则" @click="loadRules"><RefreshCw :size="16" />刷新</el-button>
<el-button v-if="canEdit" type="primary" :disabled="loading" @click="openEditor()"><Plus :size="16" />新建规则</el-button>
</div>
</div>
<div v-if="!canView" class="billing-state">
<Scale :size="30" /><strong>暂无计费规则查看权限</strong><p>需要权限 firstleg:fba-pricing:rule:view。</p>
</div>
<div v-else-if="loadError" class="billing-error" role="alert">
<div><strong>客户计费规则加载失败</strong><p>{{ loadError.message }}</p><ApiErrorMeta :details="loadError" /></div>
<el-button type="primary" plain :loading="loading" @click="loadRules">重新加载</el-button>
</div>
<DataTable
v-else
v-model:page="page"
v-model:page-size="pageSize"
:data="pagedRules"
:columns="columns"
:loading="loading"
:total="rules.length"
:page-sizes="[10, 20, 50]"
row-key="id"
empty-text="当前客户暂无计费规则"
>
<template #actions="{ row }">
<div class="row-actions">
<el-button link type="primary" @click="openDetail(asRule(row))"><Eye :size="15" />详情</el-button>
<el-button v-if="canEdit" link type="primary" @click="openEditor(asRule(row))"><Plus :size="15" />新版本</el-button>
<el-button v-if="canEdit && row.status === 'ACTIVE'" link type="danger" @click="disableRule(asRule(row))"><CircleSlash2 :size="15" />停用</el-button>
</div>
</template>
<template #rule="{ row }"><div class="primary-cell"><strong>{{ row.ruleCode }}</strong><small>V{{ row.ruleVersion ?? '—' }}</small></div></template>
<template #businessType="{ row }">{{ businessTypeLabel(row.businessType) }}</template>
<template #chargeMode="{ row }"><el-tag effect="plain">{{ modeLabel(row.chargeMode) }}</el-tag></template>
<template #taxMode="{ row }">{{ taxLabel(row.taxMode) }}</template>
<template #rounding="{ row }">保留 {{ row.roundingScale }} 位 · {{ row.roundingMode }}</template>
<template #period="{ row }"><span>{{ formatDate(row.effectiveFrom) }}</span><small class="subline">至 {{ formatDate(row.effectiveTo) }}</small></template>
<template #status="{ row }"><el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'" effect="light">{{ row.status === 'ACTIVE' ? '生效中' : '已停用' }}</el-tag></template>
<template #empty>
<div class="billing-state"><Scale :size="28" /><strong>当前客户暂无计费规则</strong><p v-if="canEdit">创建第一条规则后,入库计费才能匹配该客户的计费口径。</p></div>
</template>
<template #pagination-left>共 {{ rules.length }} 个规则版本</template>
</DataTable>
<AppDrawer v-model="detailVisible" title="计费规则详情" size="min(600px, 94vw)" :loading="detailLoading" :show-footer="false">
<div v-if="detailError" class="billing-error" role="alert"><div><strong>规则详情加载失败</strong><p>{{ detailError.message }}</p><ApiErrorMeta :details="detailError" /></div></div>
<div v-else-if="selectedRule" class="rule-detail">
<section><h3>规则归属</h3><dl class="detail-grid"><div><dt>规则编码</dt><dd>{{ selectedRule.ruleCode }}</dd></div><div><dt>版本</dt><dd>V{{ selectedRule.ruleVersion ?? '—' }}</dd></div><div><dt>适用客户</dt><dd>{{ selectedRule.customerName }}</dd></div><div><dt>业务类型</dt><dd>{{ businessTypeLabel(selectedRule.businessType) }}</dd></div><div><dt>状态</dt><dd><el-tag :type="selectedRule.status === 'ACTIVE' ? 'success' : 'info'">{{ selectedRule.status === 'ACTIVE' ? '生效中' : '已停用' }}</el-tag></dd></div></dl></section>
<section><h3>计费口径</h3><dl class="detail-grid"><div><dt>计费方式</dt><dd>{{ modeLabel(selectedRule.chargeMode) }}</dd></div><div><dt>税务模式</dt><dd>{{ taxLabel(selectedRule.taxMode) }}</dd></div><div><dt>换算系数</dt><dd>{{ selectedRule.conversionFactor }}</dd></div><div><dt>舍入规则</dt><dd>保留 {{ selectedRule.roundingScale }} 位,{{ selectedRule.roundingMode }}</dd></div><div><dt>币种</dt><dd>{{ selectedRule.currency }}</dd></div></dl></section>
<section><h3>版本有效期</h3><dl class="detail-grid"><div><dt>生效时间</dt><dd>{{ formatDate(selectedRule.effectiveFrom) }}</dd></div><div><dt>失效时间</dt><dd>{{ formatDate(selectedRule.effectiveTo) }}</dd></div><div class="detail-wide"><dt>备注</dt><dd>{{ selectedRule.remark || '—' }}</dd></div></dl></section>
</div>
</AppDrawer>
<AppDrawer v-model="editorVisible" title="创建计费规则版本" size="min(760px, 94vw)" :confirm-loading="editorSaving" confirm-text="创建版本" :before-close="beforeEditorClose" @confirm="saveVersion" @cancel="closeEditor">
<el-form ref="editorForm" :model="form" :rules="formRules" label-position="top" class="rule-form">
<div class="customer-context"><span>适用客户</span><strong>{{ customerName }}</strong><small>{{ customerCode || customerId }}</small></div>
<section class="form-section"><h3>规则归属</h3><div class="form-grid"><el-form-item label="规则编码" prop="ruleCode"><el-input v-model="form.ruleCode" maxlength="50" placeholder="例如 FBA-SEA-TAX-KG" /></el-form-item><el-form-item label="业务类型" prop="businessType"><el-input v-model="form.businessType" maxlength="32" placeholder="例如 FBA_SEA" /></el-form-item></div></section>
<section class="form-section"><h3>计费口径</h3><div class="form-grid"><el-form-item label="计费方式" prop="chargeMode"><el-segmented v-model="form.chargeMode" :options="[{ label: '按 KG', value: 'KG' }, { label: '按 CBM', value: 'CBM' }]" /></el-form-item><el-form-item label="税务模式" prop="taxMode"><el-select v-model="form.taxMode"><el-option label="包税" value="TAX_INCLUDED" /><el-option label="不包税" value="TAX_EXCLUDED" /></el-select></el-form-item><el-form-item label="换算系数" prop="conversionFactor"><el-input-number v-model="form.conversionFactor" :min="0.000001" :precision="6" :controls="false" /></el-form-item><el-form-item label="保留小数位" prop="roundingScale"><el-select v-model="form.roundingScale"><el-option v-for="scale in [0, 1, 2, 3, 4]" :key="scale" :label="`${scale} 位`" :value="scale" /></el-select></el-form-item><el-form-item label="舍入方式" prop="roundingMode"><el-select v-model="form.roundingMode"><el-option label="向上取整 CEILING" value="CEILING" /><el-option label="向上舍入 UP" value="UP" /><el-option label="向下舍入 DOWN" value="DOWN" /><el-option label="向下取整 FLOOR" value="FLOOR" /><el-option label="四舍五入 HALF_UP" value="HALF_UP" /><el-option label="五舍六入 HALF_DOWN" value="HALF_DOWN" /><el-option label="银行家舍入 HALF_EVEN" value="HALF_EVEN" /></el-select></el-form-item><el-form-item label="币种" prop="currency"><el-select v-model="form.currency" filterable allow-create default-first-option><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></div></section>
<section class="form-section"><h3>生效范围</h3><div class="form-grid"><el-form-item label="生效时间" prop="effectiveFrom"><el-date-picker v-model="form.effectiveFrom" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="选择生效时间" /></el-form-item><el-form-item label="失效时间"><el-date-picker v-model="form.effectiveTo" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" placeholder="不填表示长期有效" /></el-form-item><el-form-item label="备注" class="wide"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item></div></section>
</el-form>
</AppDrawer>
</div>
</template>
<style scoped>
.billing-rules-panel { min-width:0; }
.billing-heading { align-items:center; }
.heading-actions,.row-actions { display:flex; align-items:center; gap:8px; }
.heading-actions :deep(.el-button),.row-actions :deep(.el-button) { margin-left:0; }
.billing-state { display:flex; min-height:220px; align-items:center; justify-content:center; flex-direction:column; gap:8px; color:#8797b2; text-align:center; }
.billing-state strong { color:#475467; font-size:13px; }.billing-state p { max-width:520px; margin:0; color:var(--cc-muted); font-size:12px; line-height:1.7; }
.billing-error { display:flex; align-items:center; justify-content:space-between; gap:18px; padding:16px; border-radius:12px; background:#fff4f3; }.billing-error strong { color:#7a271a; }.billing-error p { margin:5px 0; color:#b42318; font-size:12px; }
.primary-cell { display:flex; flex-direction:column; gap:3px; }.primary-cell small,.subline { display:block; margin-top:3px; color:var(--cc-muted); font-size:11px; }
.rule-detail { display:grid; gap:24px; }.rule-detail section + section,.form-section + .form-section { padding-top:22px; border-top:1px solid var(--cc-border); }.rule-detail h3,.form-section h3 { margin:0 0 14px; color:var(--cc-text); font-size:15px; }
.detail-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:16px 24px; margin:0; }.detail-grid > div { min-width:0; }.detail-grid dt { margin-bottom:5px; color:var(--cc-muted); font-size:12px; }.detail-grid dd { margin:0; color:var(--cc-text); line-height:1.55; overflow-wrap:anywhere; }.detail-wide { grid-column:1 / -1; }
.rule-form { display:grid; gap:24px; }.customer-context { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:12px; padding:14px 16px; border-radius:12px; background:#f5f7ff; }.customer-context span,.customer-context small { color:var(--cc-muted); font-size:12px; }.customer-context strong { color:#25324a; }.form-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:2px 20px; }.wide { grid-column:1 / -1; }.form-grid :deep(.el-select),.form-grid :deep(.el-input-number),.form-grid :deep(.el-date-editor) { width:100%; }
@media (max-width:760px) { .billing-heading { align-items:stretch; flex-direction:column; }.heading-actions { width:100%; }.heading-actions :deep(.el-button) { min-height:44px; flex:1; }.form-grid,.detail-grid { grid-template-columns:1fr; }.wide,.detail-wide { grid-column:auto; }.billing-error { align-items:stretch; flex-direction:column; } }
@media (max-width:480px) { .customer-context { grid-template-columns:1fr; gap:4px; }.row-actions { flex-wrap:wrap; } }
</style>
@@ -53,6 +53,7 @@ describe('CustomerDetailView', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { configurable: true, value: vi.fn() })
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:qualification-preview') })
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
document.body.innerHTML = ''
@@ -82,6 +83,13 @@ describe('CustomerDetailView', () => {
expect(document.body.textContent).not.toContain('0 人正常使用')
expect(document.body.textContent).toContain('已替换')
expect(document.body.textContent).toContain('12票')
const billingTab = [...document.querySelectorAll<HTMLButtonElement>('.detail-tabs button')]
.find((button) => button.textContent?.trim() === '计费规则')
billingTab?.click()
await flushPromises()
expect(document.body.textContent).toContain('暂无计费规则查看权限')
expect(document.body.textContent).not.toContain('计费规则尚未接入')
})
it('opens an authenticated qualification preview', async () => {
@@ -21,6 +21,7 @@ import {
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import CustomerBillingRulesPanel from '@/modules/customers/components/CustomerBillingRulesPanel.vue'
type DetailTab = 'basic' | 'business' | 'orders' | 'finance' | 'logs' | 'billing'
type PreviewState = { url: string; kind: 'image' | 'pdf'; loading: boolean; failed: boolean }
@@ -280,7 +281,7 @@ onBeforeUnmount(clearPreviews)
<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"><div class="panel-title"><div><h2>计费规则</h2><p>客户专属费用与箱规计价配置</p></div><Scale :size="19" /></div><div class="module-empty"><Scale :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>
</div>
</div>
</template>