From 7b41df191f87fb2b196918377a2bf0746612358b Mon Sep 17 00:00:00 2001 From: Wang Zhuoxuan Date: Wed, 8 Apr 2026 00:01:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E9=A2=98=E7=9B=AE?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E6=93=8D=E4=BD=9C=20=E2=80=94=20=E5=8F=91?= =?UTF-8?q?=E5=B8=83/=E4=B8=8B=E6=9E=B6/=E5=88=A0=E9=99=A4=EF=BC=88Phase?= =?UTF-8?q?=201c=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/checkbox.tsx | 30 ++++++++ src/lib/api/question-api.ts | 15 ++++ src/routes/questions/index.tsx | 129 ++++++++++++++++++++++++++++++++- 3 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 src/components/ui/checkbox.tsx diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..98ac83e --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import { CheckIcon } from "lucide-react" +import { Checkbox as CheckboxPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/src/lib/api/question-api.ts b/src/lib/api/question-api.ts index d4d72b5..c884fd4 100644 --- a/src/lib/api/question-api.ts +++ b/src/lib/api/question-api.ts @@ -55,6 +55,21 @@ export async function updateQuestionStatus( .json>() } +export type BatchAction = "publish" | "archive" | "delete" + +export interface BatchResult { + affected: number +} + +export async function batchOperateQuestions( + ids: string[], + action: BatchAction +): Promise> { + return apiClient + .post("questions/batch", { json: { ids, action } }) + .json>() +} + export interface ImportResult { imported: number failed: number diff --git a/src/routes/questions/index.tsx b/src/routes/questions/index.tsx index 95c65fd..17109e8 100644 --- a/src/routes/questions/index.tsx +++ b/src/routes/questions/index.tsx @@ -4,8 +4,9 @@ import { useReactTable, getCoreRowModel, flexRender, + type ColumnDef, } from "@tanstack/react-table" -import { Plus, Search } from "lucide-react" +import { Plus, Search, Trash2, EyeOff, Send } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { @@ -36,7 +37,8 @@ import { import { getColumns } from "@/components/question/columns" import { StatusTransitionDialog } from "@/components/question/StatusTransitionDialog" import { ImportQuestionsDialog } from "@/components/question/ImportQuestionsDialog" -import { fetchQuestions, deleteQuestion, updateQuestionStatus } from "@/lib/api/question-api" +import { Checkbox } from "@/components/ui/checkbox" +import { fetchQuestions, deleteQuestion, updateQuestionStatus, batchOperateQuestions } from "@/lib/api/question-api" import { fetchCategories } from "@/lib/api/category-api" import { DIFFICULTY_LABELS, QUESTION_STATUSES } from "@/lib/constants" import type { Question, QuestionStatus, Difficulty } from "@/types/question" @@ -67,6 +69,11 @@ export default function QuestionsPage() { // 批量导入对话框 const [importOpen, setImportOpen] = useState(false) + // 批量操作 + const [batchConfirmOpen, setBatchConfirmOpen] = useState(false) + const [batchAction, setBatchAction] = useState<"publish" | "archive" | "delete">("delete") + const [batchSubmitting, setBatchSubmitting] = useState(false) + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) // 加载分类列表(用于筛选和列显示) @@ -128,18 +135,63 @@ export default function QuestionsPage() { 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) + } + const columns = getColumns({ categories, onDelete: openDelete, onStatusChange: handleStatusChange, }) + // 选择列 + 数据列 + 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, + columns: tableColumns, getCoreRowModel: getCoreRowModel(), + enableRowSelection: true, }) + const selectedCount = table.getFilteredSelectedRowModel().rows.length + return (
{/* 页面头部 */} @@ -234,6 +286,49 @@ export default function QuestionsPage() {
+ {/* 批量操作栏 */} + {selectedCount > 0 && ( +
+ + 已选 {selectedCount} 项 + +
+ + + +
+ +
+ )} + {/* 表格 */}
@@ -353,6 +448,34 @@ export default function QuestionsPage() { onOpenChange={setImportOpen} onSuccess={loadQuestions} /> + + {/* 批量操作确认 */} + + + + + {batchAction === "delete" ? "批量删除" : batchAction === "publish" ? "批量发布" : "批量下架"} + + + {batchAction === "delete" + ? `确定要删除选中的 ${selectedCount} 道题目吗?此操作不可撤销。` + : batchAction === "publish" + ? `确定要将选中的 ${selectedCount} 道题目发布吗?` + : `确定要将选中的 ${selectedCount} 道题目下架吗?`} + + + + 取消 + + {batchSubmitting ? "处理中..." : "确认"} + + + + ) }