This commit is contained in:
2026-08-07 20:07:37 +08:00
parent 28d0b81c5a
commit 99413ee155
27 changed files with 1065 additions and 595 deletions
@@ -15,11 +15,19 @@ import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import CustomerDetailView from './CustomerDetailView.vue'
const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() }))
const { routerPush, routeControl } = vi.hoisted(() => ({
routerPush: vi.fn(),
routeControl: { setTenantId: (_tenantId: string) => {} },
}))
vi.mock('vue-router', async (importOriginal) => ({
...await importOriginal<typeof import('vue-router')>(),
useRoute: () => ({ params: { tenantId: '8' } }),
...await (async () => {
const { reactive } = await import('vue')
const route = reactive({ params: { tenantId: '8' } })
routeControl.setTenantId = (tenantId: string) => { route.params.tenantId = tenantId }
return { useRoute: () => route }
})(),
useRouter: () => ({ push: routerPush }),
}))
@@ -59,6 +67,7 @@ describe('CustomerDetailView', () => {
beforeEach(() => {
vi.clearAllMocks()
routeControl.setTenantId('8')
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') })
@@ -121,7 +130,7 @@ describe('CustomerDetailView', () => {
.find((button) => button.textContent?.trim() === '计费规则')
billingTab?.click()
await flushPromises()
expect(document.body.textContent).toContain('暂无计费规则查看权限')
expect(document.body.textContent).toContain('暂无尺寸计算规则查看权限')
expect(document.body.textContent).not.toContain('计费规则尚未接入')
})
@@ -159,4 +168,28 @@ describe('CustomerDetailView', () => {
expect(listPlatformFirstLegOrders).toHaveBeenCalledTimes(3)
expect(listPlatformFirstLegOrders).toHaveBeenLastCalledWith({ tenantId: tenant.id, page: 1, size: 10 })
})
it('reloads a reused detail view and ignores the previous customer response', async () => {
const secondTenant = { ...tenant, id: '9', tenantCode: 'TEN9', tenantName: '远航供应链', legalName: '远航供应链有限公司' }
let resolveFirstTenant!: (value: PlatformTenantResource) => void
const firstTenantRequest = new Promise<PlatformTenantResource>((resolve) => { resolveFirstTenant = resolve })
vi.mocked(getPlatformTenant).mockImplementation((tenantId) => tenantId === '8' ? firstTenantRequest : Promise.resolve(secondTenant))
vi.mocked(listTenantQualifications).mockResolvedValue([])
vi.mocked(listPlatformTenantMembers).mockResolvedValue([])
vi.mocked(listPlatformTenantOrderCounts).mockImplementation(async (tenantIds) => [{ tenantId: tenantIds[0], orderCount: 0 }])
vi.mocked(listPlatformFirstLegOrders).mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 })
mount(CustomerDetailView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
routeControl.setTenantId('9')
await flushPromises()
expect(getPlatformTenant).toHaveBeenCalledWith('9')
expect(document.body.textContent).toContain('远航供应链')
resolveFirstTenant(tenant)
await flushPromises()
expect(document.body.textContent).toContain('远航供应链')
expect(document.body.textContent).not.toContain('柏悦物流')
})
})
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
ArrowLeft, BarChart3, Box, Building2, CircleDollarSign, ClipboardList, Eye,
@@ -54,6 +54,9 @@ const orderPageSize = ref(10)
const customerOrdersLoading = ref(false)
const customerOrdersError = ref<ApiErrorDetails | null>(null)
let skipNextOrderPageLoad = false
let detailRequestSerial = 0
let orderRequestSerial = 0
let disposed = false
const previews = ref<Record<string, PreviewState>>({})
const previewOpen = ref(false)
const previewName = ref('')
@@ -124,20 +127,23 @@ function transportLabel(order: PlatformFirstLegOrderResource) {
}
function formattedDate(value?: string | null) { return value ? value.replace('T', ' ').slice(0, 16) : '—' }
async function loadCustomerOrders(page = orderPage.value) {
async function loadCustomerOrders(page = orderPage.value, requestedTenantId = tenantId.value) {
if (!access.can('platform:order:list')) return
const serial = ++orderRequestSerial
customerOrdersLoading.value = true
customerOrdersError.value = null
try {
const result = await listPlatformFirstLegOrders({ tenantId: tenantId.value, page, size: orderPageSize.value })
const result = await listPlatformFirstLegOrders({ tenantId: requestedTenantId, page, size: orderPageSize.value })
if (disposed || serial !== orderRequestSerial || requestedTenantId !== tenantId.value) return
customerOrders.value = result.records
customerOrderTotal.value = result.total
if (orderCount.value === null) orderCount.value = result.total
} catch (error) {
if (disposed || serial !== orderRequestSerial || requestedTenantId !== tenantId.value) return
customerOrders.value = []
customerOrdersError.value = getApiErrorDetails(error, '运单记录加载失败,请稍后重试')
} finally {
customerOrdersLoading.value = false
if (!disposed && serial === orderRequestSerial) customerOrdersLoading.value = false
}
}
@@ -178,15 +184,25 @@ async function openPreview(item: PlatformTenantQualification) {
}
async function loadDetail() {
const requestedTenantId = tenantId.value
const serial = ++detailRequestSerial
loading.value = true
loadError.value = null
clearPreviews()
customer.value = null
qualifications.value = []
members.value = []
orderCount.value = null
customerOrders.value = []
customerOrderTotal.value = 0
customerOrdersError.value = null
try {
const [tenantResult, qualificationResult, memberResult] = await Promise.all([
getPlatformTenant(tenantId.value),
listTenantQualifications(tenantId.value),
listPlatformTenantMembers(tenantId.value),
getPlatformTenant(requestedTenantId),
listTenantQualifications(requestedTenantId),
listPlatformTenantMembers(requestedTenantId),
])
if (disposed || serial !== detailRequestSerial || requestedTenantId !== tenantId.value) return
customer.value = tenantResult
qualifications.value = qualificationResult
members.value = memberResult
@@ -198,12 +214,13 @@ async function loadDetail() {
if (access.can('platform:order:list')) {
if (orderPage.value !== 1) skipNextOrderPageLoad = true
orderPage.value = 1
customerOrders.value = []
customerOrderTotal.value = 0
optional.push(listPlatformTenantOrderCounts([tenantId.value]).then((records) => {
orderCount.value = Number(records.find((item) => String(item.tenantId) === tenantId.value)?.orderCount ?? 0)
}).catch(() => { orderCount.value = null }))
optional.push(loadCustomerOrders(1))
optional.push(listPlatformTenantOrderCounts([requestedTenantId]).then((records) => {
if (disposed || serial !== detailRequestSerial || requestedTenantId !== tenantId.value) return
orderCount.value = Number(records.find((item) => String(item.tenantId) === requestedTenantId)?.orderCount ?? 0)
}).catch(() => {
if (!disposed && serial === detailRequestSerial && requestedTenantId === tenantId.value) orderCount.value = null
}))
optional.push(loadCustomerOrders(1, requestedTenantId))
} else {
customerOrdersLoading.value = false
customerOrdersError.value = null
@@ -212,11 +229,14 @@ async function loadDetail() {
}
await Promise.all(optional)
} catch (error) {
if (disposed || serial !== detailRequestSerial || requestedTenantId !== tenantId.value) return
customer.value = null
qualifications.value = []
members.value = []
loadError.value = getApiErrorDetails(error, '客户详情加载失败,请稍后重试')
} finally { loading.value = false }
} finally {
if (!disposed && serial === detailRequestSerial) loading.value = false
}
}
function openOrder(order: PlatformFirstLegOrderResource) {
@@ -253,8 +273,25 @@ function handleTabKeydown(event: KeyboardEvent, key: DetailTab) {
button?.focus()
selectTab(tabs[next].key, button)
}
onMounted(loadDetail)
onBeforeUnmount(clearPreviews)
watch(tenantId, (value) => {
activeTab.value = 'basic'
if (value) void loadDetail()
else {
detailRequestSerial += 1
orderRequestSerial += 1
loading.value = false
customer.value = null
qualifications.value = []
members.value = []
customerOrders.value = []
}
}, { immediate: true })
onBeforeUnmount(() => {
disposed = true
detailRequestSerial += 1
orderRequestSerial += 1
clearPreviews()
})
watch(orderPage, (page, previousPage) => {
if (skipNextOrderPageLoad) {
skipNextOrderPageLoad = false
@@ -23,11 +23,23 @@ import QueryToolbar from '@/shared/components/QueryToolbar.vue'
import { showExportTaskCreatedDialog } from '@/shared/components/exportTaskCreatedDialog'
import CustomersView from './CustomersView.vue'
const { routerPush, routerReplace } = vi.hoisted(() => ({ routerPush: vi.fn(), routerReplace: vi.fn() }))
const { routerPush, routerReplace, routeControl } = vi.hoisted(() => ({
routerPush: vi.fn(),
routerReplace: vi.fn(),
routeControl: { setEditId: (_editId: string) => {} },
}))
vi.mock('vue-router', async (importOriginal) => ({
...await importOriginal<typeof import('vue-router')>(),
useRoute: () => ({ query: {} }),
...await (async () => {
const { reactive } = await import('vue')
const route = reactive<{ query: { edit?: string } }>({ query: {} })
routeControl.setEditId = (editId: string) => {
if (editId) route.query.edit = editId
else delete route.query.edit
}
return { useRoute: () => route }
})(),
useRouter: () => ({ push: routerPush, replace: routerReplace }),
}))
@@ -89,6 +101,7 @@ describe('CustomersView', () => {
beforeEach(() => {
vi.clearAllMocks()
routeControl.setEditId('')
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:qualification-preview') })
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
@@ -208,6 +221,18 @@ describe('CustomersView', () => {
expect(getPlatformTenant).not.toHaveBeenCalled()
})
it('opens the requested editor when a cached list receives an edit query', async () => {
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
routeControl.setEditId(String(fullTenant.id))
await flushPromises()
expect(getPlatformTenant).toHaveBeenCalledWith(fullTenant.id)
expect(document.querySelector<HTMLInputElement>('input[placeholder="请输入手机号或固定电话"]')?.value).toBe(fullTenant.contactPhone)
expect(routerReplace).toHaveBeenCalledWith({ name: 'customers' })
})
it('saves both required files and the credit code through one package action', async () => {
vi.mocked(listTenantQualifications).mockResolvedValue([{
id: 'qualification-legal', tenantId: fullTenant.id, qualificationType: 'LEGAL_PERSON_ID',
+17 -12
View File
@@ -652,7 +652,23 @@ async function exportCustomers() {
if (openDownloadCenter) await router.push({ name: 'download-center' })
}
async function openRouteEditor(editId: string) {
if (!editId || !access.can('platform:customer:edit')) return
const row = result.value.records.find((item) => String(item.id) === editId)
try {
await openEdit(row ?? await getPlatformTenant(editId))
} catch (error) {
ElMessage.error(getApiErrorMessage(error, '客户资料加载失败'))
}
if (route.query.edit === editId) void router.replace({ name: 'customers' })
}
watch([page, pageSize], () => { void loadCustomers() })
watch(
() => typeof route.query.edit === 'string' ? route.query.edit : '',
(editId) => { if (editId) void openRouteEditor(editId) },
{ immediate: true },
)
watch(editorOpen, (open) => {
if (!open) {
editorLoadSequence += 1
@@ -666,18 +682,7 @@ watch(
},
)
onBeforeUnmount(clearQualificationPreviews)
onMounted(async () => {
await loadCustomers()
const editId = typeof route.query.edit === 'string' ? route.query.edit : ''
if (!editId || !access.can('platform:customer:edit')) return
const row = result.value.records.find((item) => String(item.id) === editId)
if (row) await openEdit(row)
else {
try { await openEdit(await getPlatformTenant(editId)) }
catch (error) { ElMessage.error(getApiErrorMessage(error, '客户资料加载失败')) }
}
void router.replace({ name: 'customers' })
})
onMounted(() => { void loadCustomers() })
</script>
<template>