verify product list

This commit is contained in:
2026-03-19 14:10:05 +08:00
parent 598b48c3fa
commit ab675c50d6
3 changed files with 58 additions and 2 deletions
@@ -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)
})
})
})
+20 -2
View File
@@ -16,6 +16,7 @@ const drawerVisible = ref(false)
const drawerLoading = ref(false)
const productDetail = ref<ProductDetail | null>(null)
const rawDetail = ref<RawProductDetail | null>(null)
let detailRequestId = 0
const statusMap: Record<number, { text: string; color: string }> = {
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<string, string> = {
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<RawProductDetail>(`/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 || '-' }}
</a-descriptions-item>
<a-descriptions-item label="商品链接" :span="2">
<a v-if="productDetail.url" :href="productDetail.url" target="_blank">
<a v-if="isSafeUrl(productDetail.url)" :href="productDetail.url!" target="_blank" rel="noopener noreferrer">
{{ productDetail.url }}
</a>
<span v-else>-</span>
<span v-else>{{ productDetail.url || '-' }}</span>
</a-descriptions-item>
<a-descriptions-item label="平台创建时间">
{{ formatTime(productDetail.created_date) }}
+2
View File
@@ -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 {