From aba508f4f6e6447aa9dc811794a5b224563db18a Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 13 Sep 2026 14:25:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(worker-platform):=20=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E6=A0=87=E8=AF=86=E6=94=B9=E4=B8=BA=E7=B3=BB=E7=BB=9F=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E7=94=9F=E6=88=90=E5=B9=B6=E5=9C=A8=E5=89=8D=E7=AB=AF?= =?UTF-8?q?UI=E9=9A=90=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worker-platform/work-category-repo.ts | 60 +++++++++++ .../admin-worker-catalog-service.ts | 101 +++++++++++++++--- .../pages/admin/panels/CategoriesPanel.tsx | 16 +-- .../src/services/admin/worker-platform.ts | 3 +- 4 files changed, 151 insertions(+), 29 deletions(-) diff --git a/apps/backend/src/repositories/worker-platform/work-category-repo.ts b/apps/backend/src/repositories/worker-platform/work-category-repo.ts index 7c99ff77..33418cc8 100644 --- a/apps/backend/src/repositories/worker-platform/work-category-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-category-repo.ts @@ -33,6 +33,66 @@ export async function listAllWorkCategories(): Promise { return result.rows } +export async function createWorkCategory(input: { + categoryKey: string + name: string + parentId?: number | null + iconUrl?: string | null + sortOrder: number + status: string + now: string +}): Promise { + const result = await query( + ` + INSERT INTO work_categories (category_key, name, parent_id, icon_url, sort_order, status, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING * + `, + [ + input.categoryKey, + input.name, + input.parentId ?? null, + input.iconUrl ?? null, + input.sortOrder, + input.status, + input.now, + input.now, + ], + ) + return result.rows[0] || null +} + +export async function updateWorkCategoryById( + id: number, + input: { + name: string + parentId?: number | null + iconUrl?: string | null + sortOrder: number + status: string + now: string + }, +): Promise { + const result = await query( + ` + UPDATE work_categories + SET name = $1, parent_id = $2, icon_url = $3, sort_order = $4, status = $5, updated_at = $6 + WHERE id = $7 + RETURNING * + `, + [ + input.name, + input.parentId ?? null, + input.iconUrl ?? null, + input.sortOrder, + input.status, + input.now, + id, + ], + ) + return result.rows[0] || null +} + export async function upsertWorkCategory(input: { categoryKey: string name: string diff --git a/apps/backend/src/services/worker-platform/admin-worker-catalog-service.ts b/apps/backend/src/services/worker-platform/admin-worker-catalog-service.ts index ebc00e03..f186cbad 100644 --- a/apps/backend/src/services/worker-platform/admin-worker-catalog-service.ts +++ b/apps/backend/src/services/worker-platform/admin-worker-catalog-service.ts @@ -1,9 +1,11 @@ import { countWorkCategoryUsages, + createWorkCategory, deleteWorkCategory, getWorkCategoryById, getWorkCategoryByKey, listAllWorkCategories, + updateWorkCategoryById, upsertWorkCategory, } from '../../repositories/worker-platform/work-category-repo.js' import { @@ -69,12 +71,25 @@ export async function listAdminWorkCategories() { return { items: (await listAllWorkCategories()).map(mapWorkCategory) } } +async function generateUniqueCategoryKey(): Promise { + for (let i = 0; i < 10; i++) { + const key = `cat_${Date.now().toString(36)}_${randomId('', 3)}` + const existing = await getWorkCategoryByKey(key) + if (!existing) { + return key + } + } + return `cat_${Date.now().toString(36)}_${randomId('', 6)}` +} + export async function saveAdminWorkCategory(payload: JsonObject = {}) { const now = nowIso() - const categoryKey = normalizeSlugKey( - payload.categoryKey || payload.category_key || payload.name, - DEFAULT_CATEGORY_KEY, - ) + const rawCategoryId = payload.categoryId ?? payload.category_id ?? payload.id + const categoryId = + rawCategoryId !== undefined && rawCategoryId !== null && String(rawCategoryId).trim() !== '' + ? Number(rawCategoryId) + : null + const name = String(payload.name || '').trim() || '默认分类' const rawParentId = payload.parentId ?? payload.parent_id const parentId = @@ -87,6 +102,8 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) { : payload.icon_url !== undefined ? String(payload.icon_url || '').trim() || null : null + const sortOrder = normalizeInteger(payload.sortOrder ?? payload.sort_order, 100) + const status = normalizeEnabledStatus(payload.status) if (parentId) { const parent = await getWorkCategoryById(parentId) @@ -96,7 +113,7 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) { errorCode: 'parent_category_not_found', }) } - if (parent.category_key === categoryKey) { + if (categoryId && parent.id === categoryId) { throw createHttpError('上级分类不能是当前分类自身', { statusCode: 400, errorCode: 'invalid_parent_category', @@ -110,26 +127,80 @@ export async function saveAdminWorkCategory(payload: JsonObject = {}) { } } - const existing = await getWorkCategoryByKey(categoryKey) - if (existing && parentId) { - const usages = await countWorkCategoryUsages(existing.id) - if (usages.childCategoryCount > 0) { - throw createHttpError('该分类下已有子分类,无法再作为二级分类', { - statusCode: 400, - errorCode: 'category_has_children', + // 编辑已有分类 + if (categoryId) { + const existing = await getWorkCategoryById(categoryId) + if (!existing) { + throw createHttpError('分类不存在', { + statusCode: 404, + errorCode: 'work_category_not_found', }) } + + if (parentId) { + const usages = await countWorkCategoryUsages(existing.id) + if (usages.childCategoryCount > 0) { + throw createHttpError('该分类下已有子分类,无法再作为二级分类', { + statusCode: 400, + errorCode: 'category_has_children', + }) + } + } + + const category = await updateWorkCategoryById(categoryId, { + name, + parentId, + iconUrl, + sortOrder, + status, + now, + }) + if (!category) { + throw createHttpError('分类更新失败', { + statusCode: 500, + errorCode: 'category_update_failed', + }) + } + return { category: mapWorkCategory(category) } + } + + // 创建新分类:系统自动生成全局唯一 categoryKey,无需用户手动填写 + let categoryKey = '' + const providedKey = String(payload.categoryKey || payload.category_key || '').trim() + if (providedKey) { + const normalizedKey = normalizeSlugKey(providedKey, '') + if (normalizedKey) { + const existingWithKey = await getWorkCategoryByKey(normalizedKey) + if (existingWithKey) { + throw createHttpError('分类标识已存在,请勿重复创建', { + statusCode: 400, + errorCode: 'category_key_conflict', + }) + } + categoryKey = normalizedKey + } } - const category = await upsertWorkCategory({ + if (!categoryKey) { + categoryKey = await generateUniqueCategoryKey() + } + + const category = await createWorkCategory({ categoryKey, name, parentId, iconUrl, - sortOrder: normalizeInteger(payload.sortOrder ?? payload.sort_order, 100), - status: normalizeEnabledStatus(payload.status), + sortOrder, + status, now, }) + if (!category) { + throw createHttpError('创建分类失败', { + statusCode: 500, + errorCode: 'category_create_failed', + }) + } + return { category: mapWorkCategory(category) } } diff --git a/apps/frontend/src/pages/admin/panels/CategoriesPanel.tsx b/apps/frontend/src/pages/admin/panels/CategoriesPanel.tsx index af9ccce4..d232d955 100644 --- a/apps/frontend/src/pages/admin/panels/CategoriesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/CategoriesPanel.tsx @@ -28,7 +28,6 @@ import { uploadFile } from '@/services/files' import type { WorkCategory } from '@/types/worker-platform' type CategoryFormValues = { - categoryKey: string name: string parentId?: number iconUrl?: string @@ -63,7 +62,7 @@ export default function CategoriesPanel() { { value: 0, label: '无(作为一级分类)' }, ...level1Categories.map((c) => ({ value: c.categoryId, - label: `${c.name} (${c.categoryKey})`, + label: c.name, })), ] }, [rawCategories, editingCategory]) @@ -98,7 +97,6 @@ export default function CategoriesPanel() { function startEdit(category: WorkCategory) { setEditingCategory(category) form.setFieldsValue({ - categoryKey: category.categoryKey, name: category.name, parentId: category.parentId || 0, iconUrl: category.iconUrl || '', @@ -129,7 +127,7 @@ export default function CategoriesPanel() { async function saveCategory(values: CategoryFormValues) { try { await saveAdminWorkCategory({ - categoryKey: values.categoryKey, + categoryId: editingCategory ? editingCategory.categoryId : undefined, name: values.name, parentId: values.parentId ? Number(values.parentId) : null, iconUrl: values.iconUrl ? String(values.iconUrl).trim() : null, @@ -147,7 +145,7 @@ export default function CategoriesPanel() { async function toggleCategory(category: WorkCategory) { try { await saveAdminWorkCategory({ - categoryKey: category.categoryKey, + categoryId: category.categoryId, name: category.name, parentId: category.parentId || null, iconUrl: category.iconUrl || null, @@ -219,7 +217,6 @@ export default function CategoriesPanel() { return {parent ? parent.name : `ID: ${row.parentId}`} }, }, - { title: '标识', dataIndex: 'categoryKey', width: 150 }, { title: '排序', dataIndex: 'sortOrder', width: 90 }, { title: '状态', @@ -275,13 +272,6 @@ export default function CategoriesPanel() { > - diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts index f1faa844..419ee73f 100644 --- a/apps/frontend/src/services/admin/worker-platform.ts +++ b/apps/frontend/src/services/admin/worker-platform.ts @@ -147,7 +147,8 @@ export function fetchAdminWorkCategories() { } export function saveAdminWorkCategory(payload: { - categoryKey: string + categoryId?: number + categoryKey?: string name: string parentId?: number | null iconUrl?: string | null