diff --git a/frontend/src/pages/products/__tests__/index.spec.ts b/frontend/src/pages/products/__tests__/index.spec.ts index b00272d..1136758 100644 --- a/frontend/src/pages/products/__tests__/index.spec.ts +++ b/frontend/src/pages/products/__tests__/index.spec.ts @@ -224,6 +224,42 @@ describe('useProductStore', () => { expect(store.platformMap.get(1)).toBe('平台 #1') expect(store.storeMap.get(1)).toBe('店铺1') }) + + it('handles loadLookups failure gracefully', async () => { + const { api } = await import('@/utils/request') + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + vi.mocked(api.get).mockRejectedValueOnce(new Error('Network error')) + + const store = useProductStore() + await store.loadLookups() + + expect(warnSpy).toHaveBeenCalledWith('加载查找表数据失败', expect.any(Error)) + expect(store.companyMap.size).toBe(0) + expect(store.platformMap.size).toBe(0) + expect(store.storeMap.size).toBe(0) + warnSpy.mockRestore() + }) + }) + + describe('fetchProducts error handling', () => { + it('clears products on fetch failure', async () => { + const { api } = await import('@/utils/request') + const { message } = await import('ant-design-vue') + vi.spyOn(message, 'error') + + const store = useProductStore() + // 先加载一些数据 + vi.mocked(api.get).mockResolvedValueOnce(mockPaginatedResponse) + await store.fetchProducts() + expect(store.products.length).toBe(2) + + // 再让请求失败 + vi.mocked(api.get).mockRejectedValueOnce(new Error('Server error')) + await store.fetchProducts() + + expect(store.products).toEqual([]) + expect(store.pagination.total).toBe(0) + }) }) }) diff --git a/frontend/src/pages/products/index.vue b/frontend/src/pages/products/index.vue index e6e7dc8..468343a 100644 --- a/frontend/src/pages/products/index.vue +++ b/frontend/src/pages/products/index.vue @@ -16,6 +16,7 @@ const drawerVisible = ref(false) const drawerLoading = ref(false) const productDetail = ref(null) const rawDetail = ref(null) +let detailRequestId = 0 const statusMap: Record = { 1: { text: '上架', color: 'green' }, @@ -57,6 +58,7 @@ function handlePageChange(page: number, pageSize: number) { } function formatPrice(record: { price: string; currency: string }) { + if (!record.price || isNaN(Number(record.price))) return '-' const symbols: Record = { CNY: '¥', USD: '$', @@ -68,12 +70,23 @@ function formatPrice(record: { price: string; currency: string }) { return `${symbol}${record.price}` } +function isSafeUrl(url: string | null): boolean { + if (!url) return false + try { + const parsed = new URL(url) + return ['http:', 'https:'].includes(parsed.protocol) + } catch { + return false + } +} + function formatTime(time: string | null) { if (!time) return '-' return time.replace('T', ' ').substring(0, 16) } async function handleViewDetail(record: { id: number }) { + const currentRequestId = ++detailRequestId drawerVisible.value = true drawerLoading.value = true productDetail.value = null @@ -84,6 +97,9 @@ async function handleViewDetail(record: { id: number }) { api.get(`/api/v1/raw/products/${record.id}`), ]) + // 竞态保护:若用户已切换到另一商品详情,丢弃过期响应 + if (currentRequestId !== detailRequestId) return + if (normalResult.status === 'fulfilled') { productDetail.value = normalResult.value } else { @@ -92,6 +108,8 @@ async function handleViewDetail(record: { id: number }) { if (rawResult.status === 'fulfilled') { rawDetail.value = rawResult.value + } else { + console.warn('加载原始数据(raw)失败', rawResult.reason) } drawerLoading.value = false @@ -260,10 +278,10 @@ async function handleViewDetail(record: { id: number }) { {{ productDetail.hscode || '-' }} - + {{ productDetail.url }} - - + {{ productDetail.url || '-' }} {{ formatTime(productDetail.created_date) }} diff --git a/frontend/src/stores/product.ts b/frontend/src/stores/product.ts index 6cc4a5e..25e77a1 100644 --- a/frontend/src/stores/product.ts +++ b/frontend/src/stores/product.ts @@ -126,6 +126,8 @@ export const useProductStore = defineStore('product', () => { pagination.total = data.total pagination.page = data.page } catch (err: unknown) { + products.value = [] + pagination.total = 0 const msg = err instanceof Error ? err.message : '获取产品列表失败' message.error(msg) } finally {