import { useCallback, useEffect, useState } from "react" import { Link, useSearchParams } from "react-router" import { useReactTable, getCoreRowModel, flexRender, type ColumnDef, } from "@tanstack/react-table" import { Plus, Search, Trash2, EyeOff, Send, Download, FileCheck } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { getColumns } from "@/components/question/columns" import { StatusTransitionDialog } from "@/components/question/StatusTransitionDialog" import { ImportQuestionsDialog } from "@/components/question/ImportQuestionsDialog" import { UgcReviewDialog } from "@/components/question/UgcReviewDialog" import { Checkbox } from "@/components/ui/checkbox" import { fetchQuestions, deleteQuestion, updateQuestionStatus, batchOperateQuestions } from "@/lib/api/question-api" import { fetchCategories } from "@/lib/api/category-api" import { exportToCsv } from "@/lib/csv-export" import { DIFFICULTY_LABELS, QUESTION_STATUSES } from "@/lib/constants" import type { Question, QuestionStatus, Difficulty } from "@/types/question" import type { Category } from "@/types/category" const PAGE_SIZE = 20 type SourceTab = "all" | "system" | "ugc" const SOURCE_TABS = [ { value: "all" as const, label: "全部题目" }, { value: "system" as const, label: "官方题库" }, { value: "ugc" as const, label: "用户投稿" }, ] as const export default function QuestionsPage() { const [searchParams, setSearchParams] = useSearchParams() const [questions, setQuestions] = useState([]) const [categories, setCategories] = useState([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [search, setSearch] = useState("") const [statusFilter, setStatusFilter] = useState("all") const [categoryFilter, setCategoryFilter] = useState("all") const [difficultyFilter, setDifficultyFilter] = useState("all") // 从 URL 查询参数读取 source,如果没有则默认为 "all" const [sourceTab, setSourceTab] = useState( () => (searchParams.get("source") as SourceTab) || "all" ) // 当 sourceTab 改变时,更新 URL useEffect(() => { const newParams = new URLSearchParams(searchParams) if (sourceTab === "all") { newParams.delete("source") } else { newParams.set("source", sourceTab) } setSearchParams(newParams, { replace: true }) }, [sourceTab, searchParams, setSearchParams]) // 删除对话框 const [deleteOpen, setDeleteOpen] = useState(false) const [deletingQuestion, setDeletingQuestion] = useState(null) // 状态流转对话框 const [statusDialogOpen, setStatusDialogOpen] = useState(false) const [statusTarget, setStatusTarget] = useState(null) const [statusTargetState, setStatusTargetState] = useState(null) // 批量导入对话框 const [importOpen, setImportOpen] = useState(false) // UGC 审核对话框 const [ugcReviewOpen, setUgcReviewOpen] = useState(false) const [ugcReviewQuestion, setUgcReviewQuestion] = useState(null) // 批量操作 const [batchConfirmOpen, setBatchConfirmOpen] = useState(false) const [batchAction, setBatchAction] = useState<"publish" | "archive" | "delete">("delete") const [batchSubmitting, setBatchSubmitting] = useState(false) const [exporting, setExporting] = useState(false) const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) // 加载分类列表(用于筛选和列显示) useEffect(() => { fetchCategories({ limit: 100 }).then((res) => setCategories(res.data)) }, []) const loadQuestions = useCallback(async () => { setLoading(true) try { const res = await fetchQuestions({ page, limit: PAGE_SIZE, search: search || undefined, status: statusFilter !== "all" ? statusFilter : undefined, categoryId: categoryFilter !== "all" ? categoryFilter : undefined, difficulty: difficultyFilter !== "all" ? (Number(difficultyFilter) as Difficulty) : undefined, source: sourceTab !== "all" ? sourceTab : undefined, }) setQuestions(res.data) setTotal(res.pagination.total) } catch { setQuestions([]) } finally { setLoading(false) } }, [page, search, statusFilter, categoryFilter, difficultyFilter, sourceTab]) useEffect(() => { loadQuestions() }, [loadQuestions]) async function handleDelete() { if (!deletingQuestion) return await deleteQuestion(deletingQuestion.id) setDeleteOpen(false) setDeletingQuestion(null) await loadQuestions() } async function handleStatusChange(question: Question, status: QuestionStatus) { setStatusTarget(question) setStatusTargetState(status) setStatusDialogOpen(true) } async function confirmStatusChange() { if (!statusTarget || !statusTargetState) return await updateQuestionStatus(statusTarget.id, statusTargetState) setStatusDialogOpen(false) setStatusTarget(null) setStatusTargetState(null) await loadQuestions() } function openDelete(question: Question) { setDeletingQuestion(question) setDeleteOpen(true) } // 批量操作 async function confirmBatchAction() { setBatchSubmitting(true) try { const ids = table.getFilteredSelectedRowModel().rows.map((r) => r.original.id) await batchOperateQuestions(ids, batchAction) setBatchConfirmOpen(false) table.resetRowSelection() await loadQuestions() } finally { setBatchSubmitting(false) } } function openBatchConfirm(action: "publish" | "archive" | "delete") { setBatchAction(action) setBatchConfirmOpen(true) } // UGC 审核 function openUgcReview(question: Question) { setUgcReviewQuestion(question) setUgcReviewOpen(true) } async function handleApproveUgc(_note?: string) { if (!ugcReviewQuestion) return await updateQuestionStatus(ugcReviewQuestion.id, "published") setUgcReviewOpen(false) setUgcReviewQuestion(null) await loadQuestions() } async function handleRejectUgc(_note: string) { if (!ugcReviewQuestion) return // TODO: 这里可以添加 API 调用来保存审核备注 // 暂时只更新状态 await updateQuestionStatus(ugcReviewQuestion.id, "draft") setUgcReviewOpen(false) setUgcReviewQuestion(null) await loadQuestions() } const columns = getColumns({ categories, onDelete: openDelete, onStatusChange: handleStatusChange, onReview: sourceTab === "ugc" ? openUgcReview : undefined, }) // 选择列 + 数据列 const tableColumns: ColumnDef[] = [ { id: "select", header: ({ table: t }) => ( t.toggleAllPageRowsSelected(!!value)} aria-label="全选" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="选择" /> ), }, ...columns, ] const table = useReactTable({ data: questions, columns: tableColumns, getCoreRowModel: getCoreRowModel(), enableRowSelection: true, }) const selectedCount = table.getFilteredSelectedRowModel().rows.length async function handleExport() { setExporting(true) try { const res = await fetchQuestions({ limit: 10000, search: search || undefined, status: statusFilter !== "all" ? statusFilter : undefined, categoryId: categoryFilter !== "all" ? categoryFilter : undefined, difficulty: difficultyFilter !== "all" ? (Number(difficultyFilter) as Difficulty) : undefined, }) exportToCsv("questions.csv", [ { key: "stem", label: "题干" }, { key: "categoryId", label: "分类" }, { key: "difficulty", label: "难度" }, { key: "status", label: "状态" }, { key: "answerCount", label: "答题次数" }, { key: "correctRate", label: "正确率" }, { key: "updatedAt", label: "更新时间" }, ], res.data as unknown as Record[]) } finally { setExporting(false) } } return (
{/* 页面头部 */}

题库管理

{ setSourceTab(val as SourceTab); setPage(1) }}> {SOURCE_TABS.map((tab) => ( {tab.label} ))}
{sourceTab === "ugc" && ( )} {sourceTab === "system" && ( )}
{/* 筛选栏 */}
{ setSearch(e.target.value) setPage(1) }} />
{/* 批量操作栏 */} {selectedCount > 0 && (
已选 {selectedCount} 项
)} {/* 表格 */}
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} ))} ))} {loading ? ( 加载中... ) : questions.length === 0 ? ( 暂无题目数据 ) : ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext() )} ))} )) )}
{/* 分页 */} {totalPages > 1 && (
共 {total} 条,第 {page}/{totalPages} 页
)} {/* 删除确认 */} 确认删除 确定要删除这道题目吗?此操作不可撤销。 取消 删除 {/* 状态流转确认 */} {/* 批量导入 */} {/* UGC 审核对话框 */} {/* 批量操作确认 */} {batchAction === "delete" ? "批量删除" : batchAction === "publish" ? "批量发布" : "批量下架"} {batchAction === "delete" ? `确定要删除选中的 ${selectedCount} 道题目吗?此操作不可撤销。` : batchAction === "publish" ? `确定要将选中的 ${selectedCount} 道题目发布吗?` : `确定要将选中的 ${selectedCount} 道题目下架吗?`} 取消 {batchSubmitting ? "处理中..." : "确认"}
) }