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>>()
|
.json<ApiResponse<Question>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BatchAction = "publish" | "archive" | "delete"
|
export interface BatchFailureItem {
|
||||||
|
id: string
|
||||||
export interface BatchResult {
|
reason: string
|
||||||
affected: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function batchOperateQuestions(
|
export interface BatchResult {
|
||||||
ids: string[],
|
total: number
|
||||||
action: BatchAction
|
succeeded: number
|
||||||
|
failed: BatchFailureItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function batchPublishQuestions(
|
||||||
|
ids: string[]
|
||||||
): Promise<ApiResponse<BatchResult>> {
|
): Promise<ApiResponse<BatchResult>> {
|
||||||
return apiClient
|
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>>()
|
.json<ApiResponse<BatchResult>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,10 +37,12 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog"
|
} from "@/components/ui/alert-dialog"
|
||||||
import { getColumns } from "@/components/question/columns"
|
import { getColumns } from "@/components/question/columns"
|
||||||
import { StatusTransitionDialog } from "@/components/question/StatusTransitionDialog"
|
import { StatusTransitionDialog } from "@/components/question/StatusTransitionDialog"
|
||||||
|
import { BatchResultDialog } from "@/components/question/BatchResultDialog"
|
||||||
import { ImportQuestionsDialog } from "@/components/question/ImportQuestionsDialog"
|
import { ImportQuestionsDialog } from "@/components/question/ImportQuestionsDialog"
|
||||||
import { UgcReviewDialog } from "@/components/question/UgcReviewDialog"
|
import { UgcReviewDialog } from "@/components/question/UgcReviewDialog"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
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 { fetchCategories } from "@/lib/api/category-api"
|
||||||
import { exportToCsv } from "@/lib/csv-export"
|
import { exportToCsv } from "@/lib/csv-export"
|
||||||
import { DIFFICULTY_LABELS, QUESTION_STATUSES } from "@/lib/constants"
|
import { DIFFICULTY_LABELS, QUESTION_STATUSES } from "@/lib/constants"
|
||||||
@ -107,6 +109,8 @@ export default function QuestionsPage() {
|
|||||||
const [batchConfirmOpen, setBatchConfirmOpen] = useState(false)
|
const [batchConfirmOpen, setBatchConfirmOpen] = useState(false)
|
||||||
const [batchAction, setBatchAction] = useState<"publish" | "archive" | "delete">("delete")
|
const [batchAction, setBatchAction] = useState<"publish" | "archive" | "delete">("delete")
|
||||||
const [batchSubmitting, setBatchSubmitting] = useState(false)
|
const [batchSubmitting, setBatchSubmitting] = useState(false)
|
||||||
|
const [batchResult, setBatchResult] = useState<BatchResult | null>(null)
|
||||||
|
const [batchResultOpen, setBatchResultOpen] = useState(false)
|
||||||
const [exporting, setExporting] = useState(false)
|
const [exporting, setExporting] = useState(false)
|
||||||
|
|
||||||
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
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() {
|
async function confirmBatchAction() {
|
||||||
setBatchSubmitting(true)
|
setBatchSubmitting(true)
|
||||||
try {
|
try {
|
||||||
const ids = table.getFilteredSelectedRowModel().rows.map((r) => r.original.id)
|
const ids = table.getFilteredSelectedRowModel().rows.map((r) => r.original.id)
|
||||||
await batchOperateQuestions(ids, batchAction)
|
const res = await BATCH_OPERATIONS[batchAction](ids)
|
||||||
setBatchConfirmOpen(false)
|
setBatchConfirmOpen(false)
|
||||||
table.resetRowSelection()
|
table.resetRowSelection()
|
||||||
await loadQuestions()
|
await loadQuestions()
|
||||||
|
if (res.data) {
|
||||||
|
setBatchResult(res.data)
|
||||||
|
setBatchResultOpen(true)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setBatchSubmitting(false)
|
setBatchSubmitting(false)
|
||||||
}
|
}
|
||||||
@ -620,14 +634,14 @@ export default function QuestionsPage() {
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
{batchAction === "delete" ? "批量删除" : batchAction === "publish" ? "批量发布" : "批量下架"}
|
{batchAction === "delete" ? "批量删除" : batchAction === "publish" ? "批量发布" : "批量归档"}
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{batchAction === "delete"
|
{batchAction === "delete"
|
||||||
? `确定要删除选中的 ${selectedCount} 道题目吗?此操作不可撤销。`
|
? `确定要删除选中的 ${selectedCount} 道题目吗?仅 draft/reviewing/published 状态的题目可被归档。`
|
||||||
: batchAction === "publish"
|
: batchAction === "publish"
|
||||||
? `确定要将选中的 ${selectedCount} 道题目发布吗?`
|
? `确定要将选中的 ${selectedCount} 道题目发布吗?仅 reviewing 状态的题目可被发布。`
|
||||||
: `确定要将选中的 ${selectedCount} 道题目下架吗?`}
|
: `确定要将选中的 ${selectedCount} 道题目归档吗?`}
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
@ -642,6 +656,14 @@ export default function QuestionsPage() {
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|
||||||
|
{/* 批量操作结果 */}
|
||||||
|
<BatchResultDialog
|
||||||
|
open={batchResultOpen}
|
||||||
|
onOpenChange={setBatchResultOpen}
|
||||||
|
result={batchResult}
|
||||||
|
action={batchAction}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user