import { useEffect, useMemo, useState } from "react"; import { Alert, App, Button, Input, InputNumber } from "antd"; import { Check, CheckCircle2, ChevronRight, FolderTree, Plus, RefreshCw, Search, Shapes, SlidersHorizontal } from "lucide-react"; import { AdminPageFrame } from "./components/admin-shell"; import { AdminStatusBadge, AdminTableEmpty, SettingsSectionCard } from "./components/admin-ui"; import { AdminSwitch } from "./ui/controls"; import { getSkillCuration, updateSkillCuration, type CurationCategory, type CurationChange, type CurationRoot, type SkillCuration } from "@/services/api/skill-curation"; import { curationIcon, curationIconKeys, curationIconLabels } from "@/components/skills/skill-curation-browser"; import { listSkills, type Skill } from "@/services/api/skills"; import "./skill-curation-page.css"; const emptyRoot = (): CurationRoot => ({ id: "", name: "", iconKey: "shapes", sortOrder: 0, enabled: true }); const emptyCategory = (rootTag = "drama"): CurationCategory => ({ id: "", rootTag, name: "", sortOrder: 0, enabled: true }); type EditorTarget = | { kind: "root"; id: string } | { kind: "category"; id: string } | { kind: "new-root" } | { kind: "new-category"; rootTag: string } | null; function normalizeName(value: string) { return value.replace(/\s+/g, " ").trim(); } export default function SkillCurationPage() { const { message } = App.useApp(); const [data, setData] = useState(null); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const [root, setRoot] = useState(emptyRoot); const [category, setCategory] = useState(emptyCategory); const [target, setTarget] = useState(null); const [search, setSearch] = useState(""); const [skills, setSkills] = useState([]); const [skillId, setSkillId] = useState(""); const [assignedRoot, setAssignedRoot] = useState(""); const [categoryIds, setCategoryIds] = useState([]); const sortedRoots = useMemo(() => [...(data?.roots || [])].sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)), [data?.roots]); const sortedCategories = useMemo(() => [...(data?.categories || [])].sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)), [data?.categories]); const categoriesByRoot = useMemo(() => { const grouped = new Map(); sortedCategories.forEach((item) => { const list = grouped.get(item.rootTag) || []; list.push(item); grouped.set(item.rootTag, list); }); return grouped; }, [sortedCategories]); const orphanCategories = useMemo(() => sortedCategories.filter((item) => !data?.roots?.some((rootItem) => rootItem.id === item.rootTag)), [data?.roots, sortedCategories]); const selectedSkill = skills.find((skill) => skill.skillId === skillId); const selectedRoot = data?.roots?.find((item) => item.id === assignedRoot); const availableCategories = data?.categories.filter((item) => item.enabled && item.rootTag === (assignedRoot || selectedSkill?.tag)) ?? []; const selectedEditorRootId = target?.kind === "root" ? target.id : target?.kind === "category" ? category.rootTag : target?.kind === "new-category" ? target.rootTag : ""; const editorIsRoot = target?.kind === "root" || target?.kind === "new-root"; const editorIsCategory = target?.kind === "category" || target?.kind === "new-category"; const editorTitle = target?.kind === "new-root" ? "新增一级分类" : target?.kind === "new-category" ? "新增子分类" : target?.kind === "root" ? `编辑一级分类 · ${root.name || "未命名"}` : target?.kind === "category" ? `编辑子分类 · ${category.name || "未命名"}` : "选择一个分类"; const editorDescription = editorIsRoot ? "一级分类用于组织技能库的主导航和图示。" : editorIsCategory ? "子分类必须归属于一个启用中的一级分类。" : "从左侧分类树选择一个节点,或先创建新的一级分类。"; const reload = async () => { try { setData(await getSkillCuration(true)); setError(""); } catch (e) { setError(String(e)); } }; useEffect(() => { void reload(); }, []); useEffect(() => { let cancelled = false; const timer = setTimeout(() => { listSkills({ scope: "public", search, pageSize: 50 }) .then((result) => { if (!cancelled) setSkills(result.skills); }) .catch((e) => { if (!cancelled) setError(String(e)); }); }, 250); return () => { cancelled = true; clearTimeout(timer); }; }, [search]); useEffect(() => { if (!data || target) return; const firstRoot = sortedRoots[0]; if (firstRoot) { setRoot({ ...firstRoot }); setTarget({ kind: "root", id: firstRoot.id }); } }, [data, sortedRoots, target]); useEffect(() => { if (!data || !target) return; if (target.kind === "root") { const next = data.roots?.find((item) => item.id === target.id); if (next) setRoot({ ...next }); else setTarget(null); } if (target.kind === "category") { const next = data.categories.find((item) => item.id === target.id); if (next) setCategory({ ...next }); else setTarget(null); } }, [data, target]); const save = async (change: CurationChange, onSaved?: (next: SkillCuration) => void) => { if (!data) return; setBusy(true); try { const next = await updateSkillCuration(data.revision, change); setData(next); setError(""); onSaved?.(next); message.success("已保存"); window.dispatchEvent(new Event("canvas-skills-changed")); } catch (e) { setError(`${String(e)};请重新加载后核对修改`); } finally { setBusy(false); } }; const selectRoot = (item: CurationRoot) => { setRoot({ ...item }); setTarget({ kind: "root", id: item.id }); }; const selectCategory = (item: CurationCategory) => { setCategory({ ...item }); setTarget({ kind: "category", id: item.id }); }; const startNewRoot = () => { setRoot(emptyRoot()); setTarget({ kind: "new-root" }); }; const startNewCategory = (rootId = selectedEditorRootId) => { const nextRootId = rootId && sortedRoots.some((item) => item.id === rootId && item.enabled) ? rootId : sortedRoots.find((item) => item.enabled)?.id || ""; if (!nextRootId) return; setCategory(emptyCategory(nextRootId)); setTarget({ kind: "new-category", rootTag: nextRootId }); }; const resetEditor = () => { if (target?.kind === "root") { const current = data?.roots?.find((item) => item.id === target.id); if (current) setRoot({ ...current }); return; } if (target?.kind === "category") { const current = data?.categories.find((item) => item.id === target.id); if (current) setCategory({ ...current }); return; } if (target?.kind === "new-category") { setCategory(emptyCategory(target.rootTag)); return; } setRoot(emptyRoot()); }; const saveRoot = () => { const payload = { ...root, name: normalizeName(root.name) }; void save({ root: payload }, (next) => { const saved = next.roots?.find((item) => item.id === payload.id || item.name === payload.name); if (saved) { setRoot({ ...saved }); setTarget({ kind: "root", id: saved.id }); } }); }; const saveCategory = () => { const payload = { ...category, name: normalizeName(category.name) }; void save({ category: payload }, (next) => { const saved = next.categories.find((item) => item.id === payload.id || (item.rootTag === payload.rootTag && item.name === payload.name)); if (saved) { setCategory({ ...saved }); setTarget({ kind: "category", id: saved.id }); } }); }; const selectSkill = (id: string) => { const nextRoot = data?.rootAssignments?.find((item) => item.skillId === id)?.rootId || ""; setSkillId(id); setAssignedRoot(nextRoot); setCategoryIds(data?.assignments.filter((item) => item.skillId === id).map((item) => item.categoryId) || []); }; return ( } onClick={() => void reload()} disabled={busy}> 重新加载 } >
{error ? : null}
平台策展模式

开启后,技能库会优先使用这里维护的分类和归属;关闭时不影响现有技能展示。

void save({ enabled })} />
); }