feat: 完善题目状态流转 UI(Phase 1b)
- 新建 StatusTransitionDialog 确认对话框,显示流转方向和操作说明 - 状态列增加快速操作按钮(主流转),下拉菜单保留全部流转路径 - 新增 TRANSITION_LABELS 常量定义各状态的流转动作标签
This commit is contained in:
parent
9314dc8505
commit
a5025e633e
74
src/components/question/StatusTransitionDialog.tsx
Normal file
74
src/components/question/StatusTransitionDialog.tsx
Normal 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}」吗?`
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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: "简单",
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user