feat: 对接题目批量发布/归档/删除接口
替换原单一 batch 端点为 batch-publish、batch-archive、batch-delete 三个独立端点, BatchResult 类型对齐 API 规范(total/succeeded/failed),新增 BatchResultDialog 展示批量操作结果及失败项详情。
This commit is contained in:
parent
d1af1dbe11
commit
4cb26daa02
86
src/components/question/BatchResultDialog.tsx
Normal file
86
src/components/question/BatchResultDialog.tsx
Normal file
@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{allSucceeded ? (
|
||||
<CheckCircle2 className="size-5 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="size-5 text-yellow-600" />
|
||||
)}
|
||||
批量{ACTION_LABELS[action]}结果
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
共提交 {result.total} 项,成功 {result.succeeded} 项
|
||||
{hasFailures ? `,失败 ${result.failed.length} 项` : ""}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{hasFailures && (
|
||||
<div className="max-h-60 overflow-y-auto rounded-lg border">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50 sticky top-0">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left font-medium">题目 ID</th>
|
||||
<th className="px-3 py-2 text-left font-medium">失败原因</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{result.failed.map((item) => (
|
||||
<tr key={item.id} className="border-t">
|
||||
<td className="px-3 py-1.5 font-mono text-xs">
|
||||
{item.id.slice(0, 8)}...
|
||||
</td>
|
||||
<td className="px-3 py-1.5 text-muted-foreground">
|
||||
{item.reason}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={() => onOpenChange(false)}>确定</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -61,18 +61,38 @@ export async function updateQuestionStatus(
|
||||
.json<ApiResponse<Question>>()
|
||||
}
|
||||
|
||||
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<ApiResponse<BatchResult>> {
|
||||
return apiClient
|
||||
.post("questions/batch", { json: { ids, action } })
|
||||
.post("questions/batch-publish", { json: { ids } })
|
||||
.json<ApiResponse<BatchResult>>()
|
||||
}
|
||||
|
||||
export async function batchArchiveQuestions(
|
||||
ids: string[]
|
||||
): Promise<ApiResponse<BatchResult>> {
|
||||
return apiClient
|
||||
.post("questions/batch-archive", { json: { ids } })
|
||||
.json<ApiResponse<BatchResult>>()
|
||||
}
|
||||
|
||||
export async function batchDeleteQuestions(
|
||||
ids: string[]
|
||||
): Promise<ApiResponse<BatchResult>> {
|
||||
return apiClient
|
||||
.post("questions/batch-delete", { json: { ids } })
|
||||
.json<ApiResponse<BatchResult>>()
|
||||
}
|
||||
|
||||
|
||||
@ -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<BatchResult | null>(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() {
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{batchAction === "delete" ? "批量删除" : batchAction === "publish" ? "批量发布" : "批量下架"}
|
||||
{batchAction === "delete" ? "批量删除" : batchAction === "publish" ? "批量发布" : "批量归档"}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{batchAction === "delete"
|
||||
? `确定要删除选中的 ${selectedCount} 道题目吗?此操作不可撤销。`
|
||||
? `确定要删除选中的 ${selectedCount} 道题目吗?仅 draft/reviewing/published 状态的题目可被归档。`
|
||||
: batchAction === "publish"
|
||||
? `确定要将选中的 ${selectedCount} 道题目发布吗?`
|
||||
: `确定要将选中的 ${selectedCount} 道题目下架吗?`}
|
||||
? `确定要将选中的 ${selectedCount} 道题目发布吗?仅 reviewing 状态的题目可被发布。`
|
||||
: `确定要将选中的 ${selectedCount} 道题目归档吗?`}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@ -642,6 +656,14 @@ export default function QuestionsPage() {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* 批量操作结果 */}
|
||||
<BatchResultDialog
|
||||
open={batchResultOpen}
|
||||
onOpenChange={setBatchResultOpen}
|
||||
result={batchResult}
|
||||
action={batchAction}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user