task-118(任务与重复分析中心): 实现重复检查详情抽屉

duplicate-model.ts 增加 DuplicateDetail 解析与详情分页负载解析,duplicate-api.ts
增加 fetchDuplicateDetail(GET /duplicate-check-detail)。

TDD: task-118.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
2026-09-05 17:26:54 +08:00
parent 479cc449d6
commit 9b21e1f22e
3 changed files with 153 additions and 1 deletions
@@ -1,6 +1,11 @@
/** 店铺数据重复检查适配(任务 115-119):/api/admin/shop-data-crawl/duplicate-check-* 系列端点。 */
import { http } from '@/api/http'
import { parseDuplicateItemsPage, parseDuplicateOverview, type DuplicateOverview } from './duplicate-model.ts'
import {
parseDuplicateDetailPage,
parseDuplicateItemsPage,
parseDuplicateOverview,
type DuplicateOverview,
} from './duplicate-model.ts'
import { toDuplicateItemsQuery, type DuplicateFilter } from './duplicate-filter.ts'
export const DUPLICATE_CHECK_ENDPOINT = '/api/admin/shop-data-crawl'
@@ -24,3 +29,15 @@ export async function fetchDuplicateItems(
})
return parseDuplicateItemsPage(data)
}
/** 加载撞款详情卡片分页:GET /duplicate-check-detail。 */
export async function fetchDuplicateDetail(
filter: DuplicateFilter,
page: number,
pageSize: number,
): Promise<ReturnType<typeof parseDuplicateDetailPage>> {
const { data } = await http.get<unknown>(`${DUPLICATE_CHECK_ENDPOINT}/duplicate-check-detail`, {
params: toDuplicateItemsQuery(filter, page, pageSize),
})
return parseDuplicateDetailPage(data)
}
@@ -139,6 +139,60 @@ function parseDuplicateMetrics(raw: unknown): DuplicateMetrics | null {
}
}
/** 撞款详情卡(抽屉):跨店聚合一张卡片。 */
export interface DuplicateDetail {
asin: string
shopCount: number
recordCount: number
shopNames: string[]
brand: string
firstDate: string
occurrences: DuplicateOccurrence[]
}
/** 解析单张撞款详情卡;缺 asin 视为无效。 */
export function parseDuplicateDetail(raw: unknown): DuplicateDetail | null {
if (!raw || typeof raw !== 'object') return null
const r = raw as Record<string, unknown>
const asin = text(r.asin)
if (!asin) return null
const shopNamesRaw = r.shop_names ?? r.shopNames
const shopNames = Array.isArray(shopNamesRaw)
? (shopNamesRaw as unknown[]).map((name) => text(name)).filter(Boolean)
: []
return {
asin,
shopCount: count(r.shop_count ?? r.shopCount),
recordCount: count(r.record_count ?? r.recordCount),
shopNames,
brand: text(r.brand),
firstDate: text(r.first_date ?? r.firstDate),
occurrences: Array.isArray(r.occurrences)
? r.occurrences.map((occ) => parseDuplicateOccurrence(occ)).filter((occ): occ is DuplicateOccurrence => occ !== null)
: [],
}
}
/** 详情分页负载(data: {pending,items,total,page,page_size}) → 前端结果。 */
export function parseDuplicateDetailPage(payload: unknown): {
pending: boolean
items: DuplicateDetail[]
total: number
page: number
pageSize: number
} {
const core = unwrap<unknown>(payload)
const record = core && typeof core === 'object' ? (core as Record<string, unknown>) : {}
const items = Array.isArray(record.items)
? record.items.map((raw) => parseDuplicateDetail(raw)).filter((detail): detail is DuplicateDetail => detail !== null)
: []
const total = count(record.total)
const pageNumber = count(record.page) >= 1 ? count(record.page) : 1
const rawSize = record.page_size ?? record.pageSize
const pageSize = count(rawSize) >= 1 ? count(rawSize) : 20
return { pending: record.pending === true, items, total, page: pageNumber, pageSize }
}
/** 明细列表分页负载(data: {pending,items,total,page,page_size}) → 前端结果。 */
export function parseDuplicateItemsPage(payload: unknown): {
pending: boolean