task-79(ASIN 数据中心): 实现商品类目新增/编辑/导出

POST /api/admin/product-category + PUT /product-category/{id} 保存适配
(名称必填/排序可选非负整数校验), 导出 URL(keyword), 8 个契约测试。
This commit is contained in:
2026-09-05 16:17:15 +08:00
parent a46c35f01d
commit 3828c90b4c
4 changed files with 159 additions and 0 deletions
@@ -0,0 +1,15 @@
/** 商品类目新增/编辑适配(任务 79):POST /api/admin/product-category + PUT /api/admin/product-category/{id}。 */
import { http } from '@/api/http'
import type { ProductCategorySaveRequest } from './product-category-editor-model'
export const PRODUCT_CATEGORY_ITEM_ENDPOINT = '/api/admin/product-category'
/** 新增类目。 */
export async function createProductCategory(body: ProductCategorySaveRequest): Promise<void> {
await http.post<unknown>(PRODUCT_CATEGORY_ITEM_ENDPOINT, body)
}
/** 更新类目。 */
export async function updateProductCategory(id: number, body: ProductCategorySaveRequest): Promise<void> {
await http.put<unknown>(`${PRODUCT_CATEGORY_ITEM_ENDPOINT}/${id}`, body)
}
@@ -0,0 +1,54 @@
/** 商品类目新增/编辑表单模型(任务 79):保存草稿校验与序列化(名称必填、排序可选非负整数),纯逻辑。 */
export interface ProductCategoryDraft {
parentId: number | null
name: string
sortOrder: string
description: string
}
export function createProductCategoryDraft(): ProductCategoryDraft {
return { parentId: null, name: '', sortOrder: '', description: '' }
}
export interface ProductCategorySaveRequest {
parentId?: number
name: string
sortOrder?: number
description?: string
}
function parseSortOrder(text: string): number | null {
const trimmed = (text || '').trim()
if (!trimmed) return null
if (!/^\d+$/.test(trimmed)) return null
const num = Number(trimmed)
return Number.isInteger(num) ? num : null
}
/** 校验保存草稿;合法返回 null,否则返回可读错误。 */
export function categoryDraftError(draft: ProductCategoryDraft): string | null {
if (!draft || typeof draft !== 'object') return '类目表单数据无效'
const name = typeof draft.name === 'string' ? draft.name.trim() : ''
if (!name) return '类目名称不能为空'
const rawSort = typeof draft.sortOrder === 'string' ? draft.sortOrder : ''
if (rawSort.trim() !== '' && parseSortOrder(rawSort) === null) {
return '排序需为非负整数'
}
return null
}
/** 草稿 → 保存请求;仅提交非空/合法字段,名称 trim。 */
export function toCategorySaveRequest(draft: ProductCategoryDraft): ProductCategorySaveRequest {
const error = categoryDraftError(draft)
if (error) throw new Error(error)
const request: ProductCategorySaveRequest = { name: draft.name.trim() }
if (typeof draft.parentId === 'number' && Number.isFinite(draft.parentId) && draft.parentId >= 1) {
request.parentId = Math.floor(draft.parentId)
}
const sortOrder = parseSortOrder(draft.sortOrder)
if (sortOrder !== null) request.sortOrder = sortOrder
const description = typeof draft.description === 'string' ? draft.description.trim() : ''
if (description) request.description = description
return request
}
@@ -0,0 +1,10 @@
/** 商品类目导出 URL 构建(任务 79):keyword 非空时带查询参数,纯逻辑。 */
export const PRODUCT_CATEGORY_EXPORT_ENDPOINT = '/api/admin/product-categories/export'
/** 导出下载地址:浏览器直接访问该 URL 触发流式下载。 */
export function buildProductCategoryExportUrl(keyword: string): string {
const trimmed = (keyword || '').trim()
if (!trimmed) return PRODUCT_CATEGORY_EXPORT_ENDPOINT
return `${PRODUCT_CATEGORY_EXPORT_ENDPOINT}?keyword=${encodeURIComponent(trimmed)}`
}