feat: 完善题目状态流转 UI(Phase 1b)

- 新建 StatusTransitionDialog 确认对话框,显示流转方向和操作说明
- 状态列增加快速操作按钮(主流转),下拉菜单保留全部流转路径
- 新增 TRANSITION_LABELS 常量定义各状态的流转动作标签
This commit is contained in:
Wang Zhuoxuan 2026-04-07 23:20:16 +08:00
parent 9314dc8505
commit a5025e633e
4 changed files with 140 additions and 3 deletions

View File

@ -0,0 +1,74 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { StatusBadge } from "@/components/question/StatusBadge"
import { TRANSITION_LABELS } from "@/lib/constants"
import type { Question, QuestionStatus } from "@/types/question"
interface StatusTransitionDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
question: Question | null
targetStatus: QuestionStatus | null
onConfirm: () => void
}
export function StatusTransitionDialog({
open,
onOpenChange,
question,
targetStatus,
onConfirm,
}: StatusTransitionDialogProps) {
if (!question || !targetStatus) return null
const label = TRANSITION_LABELS[targetStatus]
const description = getDescription(question.status, targetStatus)
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{label.title}</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-3">
<p>{description}</p>
<div className="flex items-center gap-2">
<StatusBadge status={question.status} />
<span className="text-muted-foreground"></span>
<StatusBadge status={targetStatus} />
</div>
<p className="text-xs text-muted-foreground">
{question.stem.length > 40 ? question.stem.slice(0, 40) + "..." : question.stem}
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>
{label.action}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
function getDescription(from: QuestionStatus, to: QuestionStatus): string {
const descriptions: Record<string, string> = {
"draft→reviewing": "提交后题目将进入审核队列,等待审核通过后才能发布。",
"reviewing→published": "审核通过后题目将对所有用户可见,请确认题目内容无误。",
"reviewing→draft": "将题目退回草稿状态,可以继续修改后重新提交。",
"published→archived": "下架后题目将对用户不可见,但数据会保留。可随时恢复为草稿。",
"archived→draft": "恢复为草稿后可以重新编辑并提交审核。",
}
return descriptions[`${from}${to}`] ?? `确定要将题目状态从「${from}」改为「${to}」吗?`
}

View File

@ -14,7 +14,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { StatusBadge } from "@/components/question/StatusBadge"
import { DIFFICULTY_LABELS, QUESTION_STATUSES } from "@/lib/constants"
import { DIFFICULTY_LABELS, QUESTION_STATUSES, TRANSITION_LABELS } from "@/lib/constants"
import type { Question, QuestionStatus } from "@/types/question"
import type { Category } from "@/types/category"
@ -37,6 +37,19 @@ function getQuestionStatusesForTransition(current: QuestionStatus): QuestionStat
}
}
function getPrimaryTransition(current: QuestionStatus): QuestionStatus | null {
switch (current) {
case "draft":
return "reviewing"
case "reviewing":
return "published"
case "published":
return "archived"
case "archived":
return "draft"
}
}
export function getColumns(ctx: ColumnContext): ColumnDef<Question>[] {
return [
{
@ -79,7 +92,24 @@ export function getColumns(ctx: ColumnContext): ColumnDef<Question>[] {
{
accessorKey: "status",
header: "状态",
cell: ({ row }) => <StatusBadge status={row.getValue("status") as QuestionStatus} />,
cell: ({ row }) => {
const status = row.getValue("status") as QuestionStatus
const primary = getPrimaryTransition(status)
return (
<div className="flex items-center gap-2">
<StatusBadge status={status} />
{primary && (
<button
type="button"
className="text-xs text-muted-foreground hover:text-foreground underline-offset-2 hover:underline"
onClick={() => ctx.onStatusChange(row.original, primary)}
>
{TRANSITION_LABELS[primary].title}
</button>
)}
</div>
)
},
},
{
accessorKey: "distractors",

View File

@ -1,4 +1,5 @@
import type { CategoryStatus } from "@/types/category"
import type { QuestionStatus } from "@/types/question"
export const QUESTION_STATUSES = {
draft: "草稿",
@ -7,6 +8,13 @@ export const QUESTION_STATUSES = {
archived: "已下架",
} as const
export const TRANSITION_LABELS: Record<QuestionStatus, { title: string; action: string }> = {
draft: { title: "退回草稿", action: "确认退回" },
reviewing: { title: "提交审核", action: "确认提交" },
published: { title: "发布题目", action: "确认发布" },
archived: { title: "下架题目", action: "确认下架" },
}
export const DIFFICULTY_LABELS: Record<number, string> = {
1: "入门",
2: "简单",

View File

@ -34,6 +34,7 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { getColumns } from "@/components/question/columns"
import { StatusTransitionDialog } from "@/components/question/StatusTransitionDialog"
import { fetchQuestions, deleteQuestion, updateQuestionStatus } from "@/lib/api/question-api"
import { fetchCategories } from "@/lib/api/category-api"
import { DIFFICULTY_LABELS, QUESTION_STATUSES } from "@/lib/constants"
@ -57,6 +58,11 @@ export default function QuestionsPage() {
const [deleteOpen, setDeleteOpen] = useState(false)
const [deletingQuestion, setDeletingQuestion] = useState<Question | null>(null)
// 状态流转对话框
const [statusDialogOpen, setStatusDialogOpen] = useState(false)
const [statusTarget, setStatusTarget] = useState<Question | null>(null)
const [statusTargetState, setStatusTargetState] = useState<QuestionStatus | null>(null)
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
// 加载分类列表(用于筛选和列显示)
@ -99,7 +105,17 @@ export default function QuestionsPage() {
}
async function handleStatusChange(question: Question, status: QuestionStatus) {
await updateQuestionStatus(question.id, status)
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()
}
@ -312,6 +328,15 @@ export default function QuestionsPage() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 状态流转确认 */}
<StatusTransitionDialog
open={statusDialogOpen}
onOpenChange={setStatusDialogOpen}
question={statusTarget}
targetStatus={statusTargetState}
onConfirm={confirmStatusChange}
/>
</div>
)
}