import { useState, useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Plus, FolderKanban } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Badge } from '@/components/ui/badge'; import { Spinner } from '@/components/ui/spinner'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { notify } from '@/lib/notify'; import { cn } from '@/lib/utils'; import { api } from '../api/client'; import { useAdminGuard } from '../layouts/AdminLayout'; import type { Board } from '../api/types'; import { BoardColorPicker, BoardIconPicker } from '../components/BoardAppearancePicker'; import BoardIconDisplay from '../components/BoardIconDisplay'; import { getBoardThemeIndex } from '../utils/boardTheme'; import AdminSortableList, { SortableDragHandle, SortableMoveButtons } from '../components/admin/AdminSortableList'; import { persistSortOrderChanges, shouldShowSortableMoveButtons } from '../utils/sortOrder'; const boardSchema = z.object({ name: z.string().min(1, '请输入名称').max(64), description: z.string().max(500).optional(), sort_order: z.coerce.number().min(0), icon: z.string().max(64).optional(), color_index: z.coerce.number().min(-1).max(7), }); type BoardFormValues = z.infer; export default function BoardsManagePage() { const { ready } = useAdminGuard(); const [boards, setBoards] = useState([]); const [loading, setLoading] = useState(true); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [submitting, setSubmitting] = useState(false); const [reordering, setReordering] = useState(false); const form = useForm({ resolver: zodResolver(boardSchema), defaultValues: { name: '', description: '', sort_order: 1, icon: '', color_index: -1 }, }); const watchColorIndex = form.watch('color_index'); const editingPreviewId = editing?.id ?? boards.length + 1; const load = () => { setLoading(true); api.boards() .then(d => setBoards(d.boards ?? [])) .catch(e => notify.error(e.message)) .finally(() => setLoading(false)); }; useEffect(() => { if (ready) load(); }, [ready]); const openCreate = () => { setEditing(null); form.reset({ name: '', description: '', sort_order: boards.length + 1, icon: '', color_index: -1 }); setModalOpen(true); }; const openEdit = (board: Board) => { setEditing(board); form.reset({ name: board.name, description: board.description ?? '', sort_order: board.sort_order, icon: board.icon ?? '', color_index: board.color_index ?? -1, }); setModalOpen(true); }; const handleSubmit = async (values: BoardFormValues) => { setSubmitting(true); try { const body = { name: values.name, description: values.description ?? '', sort_order: values.sort_order, icon: values.icon ?? '', color_index: values.color_index ?? -1, }; if (editing) { await api.updateBoard(editing.id, body); notify.success('板块已更新'); } else { await api.createBoard(body); notify.success('板块已创建'); } setModalOpen(false); load(); window.dispatchEvent(new Event('boards-refresh')); } catch (e: unknown) { notify.error(e instanceof Error ? e.message : '操作失败'); } finally { setSubmitting(false); } }; const handleDelete = async (id: number) => { try { await api.deleteBoard(id); notify.success('板块已删除'); load(); window.dispatchEvent(new Event('boards-refresh')); } catch (e: unknown) { notify.error(e instanceof Error ? e.message : '删除失败'); } }; const handleBoardReorder = async (reordered: Board[]) => { const before = [...boards]; setReordering(true); try { const after = await persistSortOrderChanges(before, reordered, board => api.updateBoard(board.id, { name: board.name, description: board.description ?? '', sort_order: board.sort_order ?? 0, icon: board.icon ?? '', color_index: board.color_index ?? -1, }).then(() => undefined), ); setBoards(after); notify.success('板块排序已更新'); window.dispatchEvent(new Event('boards-refresh')); } catch (e: unknown) { setBoards(before); notify.error(e instanceof Error ? e.message : '排序保存失败'); } finally { setReordering(false); } }; const showMoveButtons = shouldShowSortableMoveButtons(boards.length); if (!ready) { return
; } return (

板块管理

创建、编辑或删除论坛板块;可为每个板块自定义图标与色标

{loading ? (
) : ( <> 排序 ID 图标 名称 简介 权重 帖子数 操作 board.id} onReorder={handleBoardReorder} showMoveButtons="auto" renderItem={(board, _index, controls) => { const themeIdx = getBoardThemeIndex(board); return (
{showMoveButtons && }
{board.id} {board.name} {board.description} {board.sort_order} {board.post_count ?? 0}
确定删除该板块? 删除后该板块下的帖子将无法通过板块筛选,此操作不可撤销。 取消 handleDelete(board.id)}> 删除
); }} />
{boards.length === 0 && (

还没有板块,点击右上角创建第一个

)} )}
{editing ? '编辑板块' : '新建板块'} {editing ? '修改名称、简介、图标与色标后保存。' : '填写板块信息,创建后即可在发帖时选择。'}
( 板块名称 )} /> ( 简介