diff --git a/src/components/question/BatchResultDialog.tsx b/src/components/question/BatchResultDialog.tsx new file mode 100644 index 0000000..4f45c0e --- /dev/null +++ b/src/components/question/BatchResultDialog.tsx @@ -0,0 +1,86 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { CheckCircle2, XCircle } from "lucide-react" +import type { BatchResult } from "@/lib/api/question-api" + +interface BatchResultDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + result: BatchResult | null + action: "publish" | "archive" | "delete" +} + +const ACTION_LABELS = { + publish: "发布", + archive: "归档", + delete: "删除", +} as const + +export function BatchResultDialog({ + open, + onOpenChange, + result, + action, +}: BatchResultDialogProps) { + if (!result) return null + + const hasFailures = result.failed.length > 0 + const allSucceeded = result.succeeded === result.total + + return ( + + + + + {allSucceeded ? ( + + ) : ( + + )} + 批量{ACTION_LABELS[action]}结果 + + + 共提交 {result.total} 项,成功 {result.succeeded} 项 + {hasFailures ? `,失败 ${result.failed.length} 项` : ""} + + + + {hasFailures && ( +
+ + + + + + + + + {result.failed.map((item) => ( + + + + + ))} + +
题目 ID失败原因
+ {item.id.slice(0, 8)}... + + {item.reason} +
+
+ )} + + + + +
+
+ ) +} diff --git a/src/lib/api/question-api.ts b/src/lib/api/question-api.ts index afe48d0..19bf442 100644 --- a/src/lib/api/question-api.ts +++ b/src/lib/api/question-api.ts @@ -61,18 +61,38 @@ export async function updateQuestionStatus( .json>() } -export type BatchAction = "publish" | "archive" | "delete" - -export interface BatchResult { - affected: number +export interface BatchFailureItem { + id: string + reason: string } -export async function batchOperateQuestions( - ids: string[], - action: BatchAction +export interface BatchResult { + total: number + succeeded: number + failed: BatchFailureItem[] +} + +export async function batchPublishQuestions( + ids: string[] ): Promise> { return apiClient - .post("questions/batch", { json: { ids, action } }) + .post("questions/batch-publish", { json: { ids } }) + .json>() +} + +export async function batchArchiveQuestions( + ids: string[] +): Promise> { + return apiClient + .post("questions/batch-archive", { json: { ids } }) + .json>() +} + +export async function batchDeleteQuestions( + ids: string[] +): Promise> { + return apiClient + .post("questions/batch-delete", { json: { ids } }) .json>() } diff --git a/src/routes/questions/index.tsx b/src/routes/questions/index.tsx index 6d7568c..3e1c5a1 100644 --- a/src/routes/questions/index.tsx +++ b/src/routes/questions/index.tsx @@ -37,10 +37,12 @@ import { } from "@/components/ui/alert-dialog" import { getColumns } from "@/components/question/columns" import { StatusTransitionDialog } from "@/components/question/StatusTransitionDialog" +import { BatchResultDialog } from "@/components/question/BatchResultDialog" 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 { fetchQuestions, deleteQuestion, updateQuestionStatus, batchPublishQuestions, batchArchiveQuestions, batchDeleteQuestions } from "@/lib/api/question-api" +import type { BatchResult } 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" @@ -107,6 +109,8 @@ export default function QuestionsPage() { const [batchConfirmOpen, setBatchConfirmOpen] = useState(false) const [batchAction, setBatchAction] = useState<"publish" | "archive" | "delete">("delete") const [batchSubmitting, setBatchSubmitting] = useState(false) + const [batchResult, setBatchResult] = useState(null) + const [batchResultOpen, setBatchResultOpen] = useState(false) const [exporting, setExporting] = useState(false) const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) @@ -180,14 +184,24 @@ export default function QuestionsPage() { // 批量操作 + const BATCH_OPERATIONS = { + publish: batchPublishQuestions, + archive: batchArchiveQuestions, + delete: batchDeleteQuestions, + } as const + async function confirmBatchAction() { setBatchSubmitting(true) try { const ids = table.getFilteredSelectedRowModel().rows.map((r) => r.original.id) - await batchOperateQuestions(ids, batchAction) + const res = await BATCH_OPERATIONS[batchAction](ids) setBatchConfirmOpen(false) table.resetRowSelection() await loadQuestions() + if (res.data) { + setBatchResult(res.data) + setBatchResultOpen(true) + } } finally { setBatchSubmitting(false) } @@ -620,14 +634,14 @@ export default function QuestionsPage() { - {batchAction === "delete" ? "批量删除" : batchAction === "publish" ? "批量发布" : "批量下架"} + {batchAction === "delete" ? "批量删除" : batchAction === "publish" ? "批量发布" : "批量归档"} {batchAction === "delete" - ? `确定要删除选中的 ${selectedCount} 道题目吗?此操作不可撤销。` + ? `确定要删除选中的 ${selectedCount} 道题目吗?仅 draft/reviewing/published 状态的题目可被归档。` : batchAction === "publish" - ? `确定要将选中的 ${selectedCount} 道题目发布吗?` - : `确定要将选中的 ${selectedCount} 道题目下架吗?`} + ? `确定要将选中的 ${selectedCount} 道题目发布吗?仅 reviewing 状态的题目可被发布。` + : `确定要将选中的 ${selectedCount} 道题目归档吗?`} @@ -642,6 +656,14 @@ export default function QuestionsPage() { + + {/* 批量操作结果 */} + ) }