"use client" import { closestCenter, DndContext, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core" import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable" import { CSS } from "@dnd-kit/utilities" import { useCallback, useEffect, useState, type CSSProperties } from "react" import { BrainCircuitIcon, GripVerticalIcon, MoreHorizontalIcon, PlusIcon, RefreshCwIcon, SearchIcon, Trash2Icon, } from "lucide-react" import { toast } from "sonner" import { ListPagination } from "@/components/list-pagination" import { OptionCombobox } from "@/components/option-combobox" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { ButtonGroup } from "@/components/ui/button-group" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Switch } from "@/components/ui/switch" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { createSkillDefinition, deleteSkillDefinition, fetchSkillDefinitions, updateSkillDefinition, updateSkillDefinitionPriority, updateSkillDefinitionStatus, type CreateSkillDefinitionPayload, type PageResult, type SkillDefinition, } from "@/lib/api/admin" import { Status, StatusLabels, } from "@/lib/generated/enums" import { getEnumLabel, getEnumOptions } from "@/lib/enums" import { cn, formatDateTime } from "@/lib/utils" import { EditDialog } from "./_components/edit" const statusFilterOptions = [ { value: "all", label: "全部状态" }, ...getEnumOptions(StatusLabels).map((option) => ({ value: String(option.value), label: option.label, })), ] type SortableSkillRowProps = { item: SkillDefinition disabled: boolean actionLoadingId: number | null openEditDialog: (item: SkillDefinition) => void handleToggleStatus: (item: SkillDefinition) => void handleDelete: (item: SkillDefinition) => void } function SortableSkillRow({ item, disabled, actionLoadingId, openEditDialog, handleToggleStatus, handleDelete, }: SortableSkillRowProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.id, disabled, }) const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition, } return (
{item.name}
{item.code}
{item.description || "暂无描述"}
void handleToggleStatus(item)} aria-label={`${item.name} 状态切换`} /> {getEnumLabel( StatusLabels, item.status as keyof typeof StatusLabels )}
{item.priority}
{formatDateTime(item.updatedAt)}
{item.updateUserName || "-"}
} aria-label={`更多操作 ${item.name}`} > void handleDelete(item)} className="text-destructive focus:text-destructive" > {actionLoadingId === item.id ? "删除中..." : "删除"}
) } export default function DashboardSkillsPage() { const [nameInput, setNameInput] = useState("") const [codeInput, setCodeInput] = useState("") const [statusFilterInput, setStatusFilterInput] = useState("all") const [name, setName] = useState("") const [code, setCode] = useState("") const [statusFilter, setStatusFilter] = useState("all") const [page, setPage] = useState(1) const [limit, setLimit] = useState(20) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [actionLoadingId, setActionLoadingId] = useState(null) const [sorting, setSorting] = useState(false) const [dialogOpen, setDialogOpen] = useState(false) const [editingItem, setEditingItem] = useState(null) const [result, setResult] = useState>({ results: [], page: { page: 1, limit: 20, total: 0 }, }) const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 8 }, }), useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 8 }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ) const loadData = useCallback(async () => { setLoading(true) try { const data = await fetchSkillDefinitions({ name: name.trim() || undefined, code: code.trim() || undefined, status: statusFilter === "all" ? undefined : Number(statusFilter), page, limit, }) setResult(data) } catch (error) { toast.error(error instanceof Error ? error.message : "加载 Skills 失败") } finally { setLoading(false) } }, [name, code, statusFilter, page, limit]) useEffect(() => { void loadData() }, [loadData]) function applyFilters() { setName(nameInput) setCode(codeInput) setStatusFilter(statusFilterInput) setPage(1) } function handleFilterKeyDown(event: React.KeyboardEvent) { if (event.key !== "Enter") { return } event.preventDefault() applyFilters() } function handlePageChange(nextPage: number) { if (nextPage < 1 || nextPage === page) { return } setPage(nextPage) } function openCreateDialog() { setEditingItem(null) setDialogOpen(true) } function openEditDialog(item: SkillDefinition) { setEditingItem(item) setDialogOpen(true) } function handleDialogOpenChange(open: boolean) { if (saving) { return } if (!open) { setEditingItem(null) } setDialogOpen(open) } async function handleSubmit(payload: CreateSkillDefinitionPayload) { if (saving) { return } setSaving(true) try { if (editingItem) { await updateSkillDefinition({ id: editingItem.id, ...payload, }) toast.success(`已更新 Skill:${editingItem.name}`) } else { await createSkillDefinition(payload) toast.success(`已创建 Skill:${payload.name}`) } setDialogOpen(false) setEditingItem(null) await loadData() } catch (error) { toast.error(error instanceof Error ? error.message : "保存 Skill 失败") } finally { setSaving(false) } } async function handleToggleStatus(item: SkillDefinition) { const nextStatus = item.status === Status.Ok ? Status.Disabled : Status.Ok setActionLoadingId(item.id) try { await updateSkillDefinitionStatus(item.id, nextStatus) toast.success(`已${nextStatus === Status.Ok ? "启用" : "停用"}:${item.name}`) await loadData() } catch (error) { toast.error(error instanceof Error ? error.message : "更新状态失败") } finally { setActionLoadingId(null) } } async function handleDelete(item: SkillDefinition) { setActionLoadingId(item.id) try { await deleteSkillDefinition(item.id) toast.success(`已删除 Skill:${item.name}`) await loadData() } catch (error) { toast.error(error instanceof Error ? error.message : "删除 Skill 失败") } finally { setActionLoadingId(null) } } async function handleDragEnd(event: DragEndEvent) { const { active, over } = event if (!over || active.id === over.id || sorting) { return } const previousResults = result.results const oldIndex = previousResults.findIndex((item) => item.id === active.id) const newIndex = previousResults.findIndex((item) => item.id === over.id) if (oldIndex < 0 || newIndex < 0) { return } const nextResults = arrayMove(previousResults, oldIndex, newIndex) setResult((current) => ({ ...current, results: nextResults, })) setSorting(true) try { await updateSkillDefinitionPriority(nextResults.map((item) => item.id)) toast.success("Skill 优先级已更新") await loadData() } catch (error) { setResult((current) => ({ ...current, results: previousResults, })) toast.error(error instanceof Error ? error.message : "更新优先级失败") } finally { setSorting(false) } } return ( <>
setNameInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder="按名称筛选" className="pl-9" />
setCodeInput(event.target.value)} onKeyDown={handleFilterKeyDown} placeholder="按编码筛选" className="w-full xl:w-56" />
Skill 状态 优先级 最近更新 操作 {!loading && result.results.length === 0 ? ( 没有匹配的 Skill ) : null} item.id)} strategy={verticalListSortingStrategy} > {result.results.map((item) => ( ))}
{ setLimit(nextLimit) setPage(1) }} />
) }