feat(categories): tree browser, CRUD dialog, pending approval queue with merge

This commit is contained in:
2026-07-07 22:19:07 +08:00
parent eb617ce436
commit 21ef5c1c37
5 changed files with 635 additions and 9 deletions
+80
View File
@@ -0,0 +1,80 @@
import { api } from '@/api/client'
import type {
ApiResponse,
Category,
CategoryTranslation,
ListParams,
PaginatedResponse,
} from '@/api/types'
const BASE = '/api/admin/v1/categories'
export interface CategoryPayload {
parent_id?: string | null
slug?: string
sort_order?: number
is_active?: boolean
translations?: Array<{ locale: string; name: string; description?: string | null }>
}
export function fetchCategoryTree(includeInactive = true) {
return api.get<ApiResponse<Category[]>>(`${BASE}/tree`, {
include_inactive: includeInactive,
})
}
export function fetchPendingCategories(params: ListParams) {
return api.get<PaginatedResponse<Category>>(`${BASE}/pending`, params)
}
export function fetchCategory(id: string) {
return api.get<ApiResponse<Category>>(`${BASE}/${id}`)
}
export function createCategory(payload: CategoryPayload) {
return api.post<ApiResponse<Category>>(BASE, payload)
}
export function updateCategory(id: string, payload: CategoryPayload) {
return api.put<ApiResponse<Category>>(`${BASE}/${id}`, payload)
}
export function deleteCategory(id: string) {
return api.delete<ApiResponse<null>>(`${BASE}/${id}`)
}
export function approveCategory(id: string) {
return api.post<ApiResponse<Category>>(`${BASE}/${id}/approve`)
}
export function rejectCategory(id: string) {
return api.post<ApiResponse<Category>>(`${BASE}/${id}/reject`)
}
export function mergeCategory(id: string, targetId: string) {
return api.post<ApiResponse<Category>>(`${BASE}/${id}/merge`, { target_id: targetId })
}
export function fetchCategoryTranslations(id: string) {
return api.get<ApiResponse<CategoryTranslation[]>>(`${BASE}/${id}/translations`)
}
export function upsertCategoryTranslation(
id: string,
locale: string,
payload: { name: string; description?: string | null },
) {
return api.post<ApiResponse<CategoryTranslation>>(`${BASE}/${id}/translations/${locale}`, payload)
}
export function deleteCategoryTranslation(id: string, locale: string) {
return api.delete<ApiResponse<null>>(`${BASE}/${id}/translations/${locale}`)
}
/** Flatten the tree for parent/merge-target pickers. */
export function flattenTree(nodes: Category[], depth = 0): Array<Category & { depth: number }> {
return nodes.flatMap((node) => [
{ ...node, depth },
...flattenTree(node.children ?? [], depth + 1),
])
}