task-78(ASIN 数据中心): 实现商品类目树加载和搜索
GET /api/admin/product-categories(+keyword) 树(嵌套 children/path/level/ childCount)与 /product-categories/search 分页搜索解析, 9 个契约测试。
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/** 商品类目树/搜索适配(任务 78):GET /api/admin/product-categories(+keyword) 与 /product-categories/search。 */
|
||||
import { http } from '@/api/http'
|
||||
import {
|
||||
parseProductCategoryList,
|
||||
parseProductCategorySearchPage,
|
||||
type ProductCategoryListResult,
|
||||
type ProductCategorySearchPage,
|
||||
} from './product-category-model'
|
||||
|
||||
export const PRODUCT_CATEGORY_ENDPOINT = '/api/admin/product-categories'
|
||||
|
||||
/** 拉取类目树(keyword 非空时由服务端过滤并保留父路径)。 */
|
||||
export async function fetchProductCategoryTree(keyword = ''): Promise<ProductCategoryListResult> {
|
||||
const trimmed = keyword.trim()
|
||||
const { data } = await http.get<unknown>(PRODUCT_CATEGORY_ENDPOINT, {
|
||||
params: trimmed ? { keyword: trimmed } : {},
|
||||
})
|
||||
return parseProductCategoryList(data)
|
||||
}
|
||||
|
||||
/** 分页搜索类目(keyword 空时返回空页)。 */
|
||||
export async function searchProductCategories(
|
||||
keyword: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<ProductCategorySearchPage> {
|
||||
const params: Record<string, string | number> = { page, pageSize }
|
||||
const trimmed = keyword.trim()
|
||||
if (trimmed) params.keyword = trimmed
|
||||
const { data } = await http.get<unknown>(`${PRODUCT_CATEGORY_ENDPOINT}/search`, { params })
|
||||
return parseProductCategorySearchPage(data)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/** 商品类目树/搜索模型(任务 78):解析 ProductCategoryListVo(嵌套 tree/path/level/childCount),纯逻辑。 */
|
||||
import { unwrap } from '../../api/envelope.ts'
|
||||
|
||||
export const PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
export interface ProductCategoryNode {
|
||||
id: number
|
||||
parentId: number | null
|
||||
name: string
|
||||
categoryKey: string
|
||||
sortOrder: number | null
|
||||
description: string
|
||||
isBuiltin: boolean
|
||||
childCount: number
|
||||
level: number | null
|
||||
path: string
|
||||
children: ProductCategoryNode[]
|
||||
}
|
||||
|
||||
export interface ProductCategoryListResult {
|
||||
tree: ProductCategoryNode[]
|
||||
items: ProductCategoryNode[]
|
||||
}
|
||||
|
||||
export interface ProductCategorySearchPage {
|
||||
items: ProductCategoryNode[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
hasMore: boolean
|
||||
}
|
||||
|
||||
function text(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
}
|
||||
|
||||
function int(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
|
||||
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) {
|
||||
return Math.floor(Number(value))
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function asBoolean(value: unknown): boolean {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'number') return value !== 0
|
||||
if (typeof value === 'string') {
|
||||
const v = value.trim().toLowerCase()
|
||||
return v === 'true' || v === '1'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 解析单条类目节点(递归 children);缺 id 视为无效。 */
|
||||
export function toProductCategoryNode(raw: unknown): ProductCategoryNode | null {
|
||||
if (!raw || typeof raw !== 'object') return null
|
||||
const record = raw as Record<string, unknown>
|
||||
const id = int(record.id)
|
||||
if (id === null) return null
|
||||
const node: ProductCategoryNode = {
|
||||
id,
|
||||
parentId: int(record.parentId ?? record.parent_id),
|
||||
name: text(record.name),
|
||||
categoryKey: text(record.categoryKey ?? record.category_key),
|
||||
sortOrder: int(record.sortOrder ?? record.sort_order),
|
||||
description: text(record.description),
|
||||
isBuiltin: asBoolean(record.isBuiltin ?? record.builtin),
|
||||
childCount: int(record.childCount ?? record.child_count) ?? 0,
|
||||
level: int(record.level),
|
||||
path: text(record.path),
|
||||
children: [],
|
||||
}
|
||||
if (Array.isArray(record.children)) {
|
||||
node.children = record.children
|
||||
.map((child) => toProductCategoryNode(child))
|
||||
.filter((child): child is ProductCategoryNode => child !== null)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
function nodesOf(value: unknown): ProductCategoryNode[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value
|
||||
.map((raw) => toProductCategoryNode(raw))
|
||||
.filter((node): node is ProductCategoryNode => node !== null)
|
||||
}
|
||||
|
||||
/** 解析类目 VO:树加载(list) 取嵌套 tree 与扁平 items。 */
|
||||
export function parseProductCategoryList(payload: unknown): ProductCategoryListResult {
|
||||
const core = unwrap<unknown>(payload)
|
||||
const record = core && typeof core === 'object' ? (core as Record<string, unknown>) : null
|
||||
return {
|
||||
tree: nodesOf(record?.tree),
|
||||
items: nodesOf(record?.items),
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析类目搜索分页 VO;缺省字段回默认。 */
|
||||
export function parseProductCategorySearchPage(payload: unknown): ProductCategorySearchPage {
|
||||
const core = unwrap<unknown>(payload)
|
||||
const record = core && typeof core === 'object' ? (core as Record<string, unknown>) : null
|
||||
const items = nodesOf(record?.items)
|
||||
const total = int(record?.total) ?? 0
|
||||
const page = int(record?.page) ?? 1
|
||||
const pageSize = int(record?.pageSize ?? record?.page_size) ?? PRODUCT_CATEGORY_DEFAULT_PAGE_SIZE
|
||||
const hasMore = typeof record?.hasMore === 'boolean' ? record.hasMore : page * pageSize < total
|
||||
return { items, total, page, pageSize, hasMore }
|
||||
}
|
||||
Reference in New Issue
Block a user