import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"; import { useAuthStore } from "@/stores/auth"; import { createSkillTag, deleteSkillTag, getSkillTags, updateSkillTag, type SkillTag, } from "@/api/skill"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { useToast } from "@/components/ui/toast"; import { isManagerRole } from "@/lib/rbac"; import { toZhSkillCategory } from "@/lib/display"; const CATEGORY_COLORS: Record = { dev: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300", algorithm: "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300", product: "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300", pm: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300", qa: "bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-300", language: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300", framework: "bg-violet-100 text-violet-800 dark:bg-violet-900/30 dark:text-violet-300", skill: "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300", tool: "bg-stone-100 text-stone-800 dark:bg-stone-800 dark:text-stone-300", }; const CATEGORY_OPTIONS = ["dev", "algorithm", "product", "pm", "qa", "language", "framework", "skill", "tool"]; function categoryClass(category?: string | null) { return CATEGORY_COLORS[(category || "").toLowerCase()] ?? "bg-muted text-muted-foreground"; } export default function SkillsPage() { const user = useAuthStore((s) => s.user); const canManage = isManagerRole(user?.role); const queryClient = useQueryClient(); const { toast } = useToast(); const [search, setSearch] = useState(""); const [categoryFilter, setCategoryFilter] = useState(""); const [createOpen, setCreateOpen] = useState(false); const [editRow, setEditRow] = useState(null); const [deleteRow, setDeleteRow] = useState(null); const { data, isLoading, error } = useQuery({ queryKey: ["skill-tags", categoryFilter], queryFn: () => getSkillTags({ category: categoryFilter || undefined }), }); const createMutation = useMutation({ mutationFn: (payload: { name: string; slug: string; category?: string; description?: string }) => createSkillTag(payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["skill-tags"] }); setCreateOpen(false); toast({ title: "技能标签创建成功", variant: "success" }); }, onError: () => toast({ title: "技能标签创建失败", variant: "error" }), }); const updateMutation = useMutation({ mutationFn: ({ tagId, payload }: { tagId: string; payload: { name?: string; category?: string; description?: string } }) => updateSkillTag(tagId, payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["skill-tags"] }); setEditRow(null); toast({ title: "技能标签更新成功", variant: "success" }); }, onError: () => toast({ title: "技能标签更新失败", variant: "error" }), }); const deleteMutation = useMutation({ mutationFn: (tagId: string) => deleteSkillTag(tagId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["skill-tags"] }); setDeleteRow(null); toast({ title: "技能标签删除成功", variant: "success" }); }, onError: () => toast({ title: "技能标签删除失败", variant: "error" }), }); const rows = useMemo(() => { const q = search.trim().toLowerCase(); return (data ?? []).filter((row) => { if (!q) return true; return ( row.name.toLowerCase().includes(q) || (row.slug || "").toLowerCase().includes(q) || (row.category || "").toLowerCase().includes(q) ); }); }, [data, search]); const renderTagActions = (tag: SkillTag) => ( setEditRow(tag)}> 编辑 setDeleteRow(tag)} > 删除 ); return (

能力词库

高级设置入口;标签只作为执行能力图谱的参考信号,不再作为任务分配中心。

{canManage && ( )}
标签列表 共 {rows.length} 个
setSearch(e.target.value)} className="w-full sm:w-72" />
{isLoading ? (

加载中…

) : error ? (

加载失败

) : rows.length === 0 ? (

暂无技能标签

) : ( <>
名称 分类 关联人数 描述 {canManage && } {rows.map((tag) => (
{tag.name} {tag.is_system && ( 系统 )}
{toZhSkillCategory(tag.category)} {tag.linked_user_count ?? 0} {tag.description || "-"} {canManage && {renderTagActions(tag)}}
))}
{rows.map((tag) => (
{tag.name} {tag.is_system && ( 系统 )}
{toZhSkillCategory(tag.category)}
{canManage && renderTagActions(tag)}

{tag.description || "暂无描述"}

关联人数:{tag.linked_user_count ?? 0}

))}
)}
{ if (!payload.name || !payload.slug) return; createMutation.mutate({ name: payload.name, slug: payload.slug, category: payload.category, description: payload.description, }); }} isPending={createMutation.isPending} /> !open && setEditRow(null)} defaultValue={editRow || undefined} disableName={!!editRow?.is_system} onSubmit={(payload) => { if (!editRow) return; updateMutation.mutate({ tagId: editRow.id, payload }); }} isPending={updateMutation.isPending} /> !open && setDeleteRow(null)}> 确认删除 删除后将移除该标签及关联关系,无法恢复。

标签:{deleteRow?.name}

); } function SkillTagDialog(props: { open: boolean; onOpenChange: (open: boolean) => void; title: string; description: string; defaultValue?: SkillTag; disableName?: boolean; isPending?: boolean; onSubmit: (payload: { name?: string; slug?: string; category?: string; description?: string }) => void; }) { const { open, onOpenChange, title, description, defaultValue, disableName, isPending, onSubmit } = props; const [name, setName] = useState(defaultValue?.name || ""); const [slug, setSlug] = useState(defaultValue?.slug || ""); const [category, setCategory] = useState(defaultValue?.category || "dev"); const [descriptionValue, setDescriptionValue] = useState(defaultValue?.description || ""); const isCreate = !defaultValue; return ( { if (next) { setName(defaultValue?.name || ""); setSlug(defaultValue?.slug || ""); setCategory(defaultValue?.category || "dev"); setDescriptionValue(defaultValue?.description || ""); } onOpenChange(next); }} > {title} {description}
setName(e.target.value)} disabled={disableName} placeholder="例如:前端框架" />
{isCreate && (
setSlug(e.target.value)} placeholder="例如:qian-duan-kuang-jia" />
)}
setDescriptionValue(e.target.value)} placeholder="可选" />
); }