refactor(exams): redesign exam creation page with 3-mode selector

- Replace cramped 3-column grid with vertical layout
- Add 3 large selectable cards at top: Manual / AI / Rich Text Editor
- Rich Text Editor mode redirects to /teacher/exams/new
- Basic info form is now always visible (not hidden in AI mode)
- Exam mode config always visible at bottom
- Add "rich" to mode enum with validation bypass
- Replace all hardcoded English/Chinese strings with i18n keys
- Add 20+ new i18n keys to zh-CN and en (mode labels, descriptions, actions)
- Clean up mixed-language UI text
This commit is contained in:
SpecialX
2026-06-24 13:23:13 +08:00
parent 6114607c1e
commit d1e4ccbf98
7 changed files with 260 additions and 139 deletions

View File

@@ -1,18 +1,18 @@
import type { JSX } from "react" import type { JSX } from "react"
import { getTranslations } from "next-intl/server"
import { ExamForm } from "@/modules/exams/components/exam-form" import { ExamForm } from "@/modules/exams/components/exam-form"
export const dynamic = "force-dynamic" export const dynamic = "force-dynamic"
export default function CreateExamPage(): JSX.Element { export default async function CreateExamPage(): Promise<JSX.Element> {
const t = await getTranslations("examHomework")
return ( return (
<div className="flex w-full justify-center items-center min-h-[calc(100vh-160px)] p-8 max-w-[1200px] mx-auto"> <div className="mx-auto w-full max-w-[1200px] space-y-6 p-6">
<div className="w-full space-y-6"> <div>
<div> <h1 className="text-2xl font-bold tracking-tight">{t("exam.form.createTitle")}</h1>
<h1 className="text-2xl font-bold tracking-tight">Create Exam</h1> <p className="text-muted-foreground">{t("exam.form.createDescription")}</p>
<p className="text-muted-foreground">Configure a new exam for your classes.</p>
</div>
<ExamForm />
</div> </div>
<ExamForm />
</div> </div>
) )
} }

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { useTranslations } from "next-intl"
import type { Control } from "react-hook-form" import type { Control } from "react-hook-form"
import { import {
Card, Card,
@@ -20,14 +21,6 @@ type ExamBasicInfoFormProps = {
loadingGrades: boolean loadingGrades: boolean
} }
const DIFFICULTY_OPTIONS = [
{ value: "1", label: "Level 1 (Easy)" },
{ value: "2", label: "Level 2" },
{ value: "3", label: "Level 3 (Medium)" },
{ value: "4", label: "Level 4" },
{ value: "5", label: "Level 5 (Hard)" },
]
export function ExamBasicInfoForm({ export function ExamBasicInfoForm({
control, control,
subjects, subjects,
@@ -35,36 +28,46 @@ export function ExamBasicInfoForm({
loadingSubjects, loadingSubjects,
loadingGrades, loadingGrades,
}: ExamBasicInfoFormProps) { }: ExamBasicInfoFormProps) {
const t = useTranslations("examHomework")
const DIFFICULTY_OPTIONS = [
{ value: "1", label: t("exam.form.difficultyLevel1") },
{ value: "2", label: t("exam.form.difficultyLevel2") },
{ value: "3", label: t("exam.form.difficultyLevel3") },
{ value: "4", label: t("exam.form.difficultyLevel4") },
{ value: "5", label: t("exam.form.difficultyLevel5") },
]
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Exam Details</CardTitle> <CardTitle>{t("exam.form.detailsTitle")}</CardTitle>
<CardDescription> <CardDescription>
Define the core information for your exam. {t("exam.form.detailsDesc")}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="grid gap-6"> <CardContent className="grid gap-6">
<TextField <TextField
control={control} control={control}
name="title" name="title"
label="Title" label={t("exam.form.title")}
placeholder="e.g. Midterm Mathematics Exam" placeholder={t("exam.form.titlePlaceholder")}
/> />
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<SelectField <SelectField
control={control} control={control}
name="subject" name="subject"
label="Subject" label={t("exam.form.subject")}
placeholder={loadingSubjects ? "Loading subjects..." : "Select subject"} placeholder={loadingSubjects ? t("exam.form.loadingSubjects") : t("exam.form.subjectPlaceholder")}
options={subjects.map((s) => ({ value: s.id, label: s.name }))} options={subjects.map((s) => ({ value: s.id, label: s.name }))}
disabled={loadingSubjects} disabled={loadingSubjects}
/> />
<SelectField <SelectField
control={control} control={control}
name="grade" name="grade"
label="Grade Level" label={t("exam.form.grade")}
placeholder={loadingGrades ? "Loading grades..." : "Select grade level"} placeholder={loadingGrades ? t("exam.form.loadingGrades") : t("exam.form.gradePlaceholder")}
options={grades.map((g) => ({ value: g.id, label: g.name }))} options={grades.map((g) => ({ value: g.id, label: g.name }))}
disabled={loadingGrades} disabled={loadingGrades}
/> />
@@ -74,20 +77,20 @@ export function ExamBasicInfoForm({
<SelectField <SelectField
control={control} control={control}
name="difficulty" name="difficulty"
label="Difficulty" label={t("exam.form.difficulty")}
placeholder="Select level" placeholder={t("exam.form.difficultyPlaceholder")}
options={DIFFICULTY_OPTIONS} options={DIFFICULTY_OPTIONS}
/> />
<TextField <TextField
control={control} control={control}
name="totalScore" name="totalScore"
label="Total Score" label={t("exam.form.totalScore")}
type="number" type="number"
/> />
<TextField <TextField
control={control} control={control}
name="durationMin" name="durationMin"
label="Duration (min)" label={t("exam.form.durationMin")}
type="number" type="number"
/> />
</div> </div>
@@ -95,9 +98,9 @@ export function ExamBasicInfoForm({
<TextField <TextField
control={control} control={control}
name="scheduledAt" name="scheduledAt"
label="Schedule Start Time (Optional)" label={t("exam.form.scheduledAt")}
type="datetime-local" type="datetime-local"
description="If set, this exam will be scheduled for a specific time." description={t("exam.form.scheduledAtDesc")}
/> />
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -11,7 +11,7 @@ export const formSchema = z.object({
totalScore: z.coerce.number().min(1, "Total score must be at least 1.").optional(), totalScore: z.coerce.number().min(1, "Total score must be at least 1.").optional(),
durationMin: z.coerce.number().min(10, "Duration must be at least 10 minutes.").optional(), durationMin: z.coerce.number().min(10, "Duration must be at least 10 minutes.").optional(),
scheduledAt: z.string().optional(), scheduledAt: z.string().optional(),
mode: z.enum(["manual", "ai"]), mode: z.enum(["manual", "ai", "rich"]),
aiSourceText: z.string().optional(), aiSourceText: z.string().optional(),
aiQuestionCount: z.coerce.number().min(1).max(200).optional(), aiQuestionCount: z.coerce.number().min(1).max(200).optional(),
aiProviderId: z.string().optional(), aiProviderId: z.string().optional(),
@@ -23,6 +23,9 @@ export const formSchema = z.object({
lateStartGraceMinutes: z.coerce.number().int().min(0).default(0), lateStartGraceMinutes: z.coerce.number().int().min(0).default(0),
antiCheatEnabled: z.boolean().default(false), antiCheatEnabled: z.boolean().default(false),
}).superRefine((data, ctx) => { }).superRefine((data, ctx) => {
// 富文本模式不需要表单校验,直接跳转到 /teacher/exams/new
if (data.mode === "rich") return
// 监考模式必须设置考试时长 // 监考模式必须设置考试时长
if (data.examMode === "proctored" && (data.durationMinutes === null || data.durationMinutes === undefined || data.durationMinutes < 1)) { if (data.examMode === "proctored" && (data.durationMinutes === null || data.durationMinutes === undefined || data.durationMinutes < 1)) {
ctx.addIssue({ ctx.addIssue({

View File

@@ -2,6 +2,7 @@
import { useTransition, useEffect, useState, type FormEvent } from "react" import { useTransition, useEffect, useState, type FormEvent } from "react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useTranslations } from "next-intl"
import { useForm, type Resolver } from "react-hook-form" import { useForm, type Resolver } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { toast } from "sonner" import { toast } from "sonner"
@@ -24,6 +25,7 @@ export type { ExamFormValues } from "./exam-form-types"
export function ExamForm() { export function ExamForm() {
const router = useRouter() const router = useRouter()
const t = useTranslations("examHomework")
const [isPending, startTransition] = useTransition() const [isPending, startTransition] = useTransition()
const [subjects, setSubjects] = useState<{ id: string; name: string }[]>([]) const [subjects, setSubjects] = useState<{ id: string; name: string }[]>([])
const [loadingSubjects, setLoadingSubjects] = useState(true) const [loadingSubjects, setLoadingSubjects] = useState(true)
@@ -56,7 +58,7 @@ export function ExamForm() {
form.setValue("subject", subjectsResult.data[0].id) form.setValue("subject", subjectsResult.data[0].id)
} }
} else { } else {
toast.error("Failed to load subjects") toast.error(t("exam.form.loadSubjectsFailed"))
} }
if (gradesResult.success && gradesResult.data) { if (gradesResult.success && gradesResult.data) {
@@ -65,7 +67,7 @@ export function ExamForm() {
form.setValue("grade", gradesResult.data[0].id) form.setValue("grade", gradesResult.data[0].id)
} }
} else { } else {
toast.error("Failed to load grades") toast.error(t("exam.form.loadGradesFailed"))
} }
if (aiProvidersResult.success && aiProvidersResult.data) { if (aiProvidersResult.success && aiProvidersResult.data) {
setAiProviders(aiProvidersResult.data) setAiProviders(aiProvidersResult.data)
@@ -79,7 +81,7 @@ export function ExamForm() {
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
toast.error("Failed to load form data") toast.error(t("exam.form.loadFormFailed"))
} finally { } finally {
setLoadingSubjects(false) setLoadingSubjects(false)
setLoadingGrades(false) setLoadingGrades(false)
@@ -87,10 +89,11 @@ export function ExamForm() {
} }
} }
void fetchMetadata() void fetchMetadata()
}, [form]) }, [form, t])
const mode = form.watch("mode") const mode = form.watch("mode")
const isAiMode = mode === "ai" const isAiMode = mode === "ai"
const isRichMode = mode === "rich"
const previewSubject = form.watch("subject") const previewSubject = form.watch("subject")
const previewGrade = form.watch("grade") const previewGrade = form.watch("grade")
const previewDuration = form.watch("durationMin") ?? 90 const previewDuration = form.watch("durationMin") ?? 90
@@ -98,6 +101,12 @@ export function ExamForm() {
const previewTitleValue = form.watch("title") const previewTitleValue = form.watch("title")
function onSubmit(data: ExamFormValues) { function onSubmit(data: ExamFormValues) {
// 富文本模式不通过表单提交,直接跳转
if (data.mode === "rich") {
router.push("/teacher/exams/new")
return
}
const resolvedTitle = data.mode === "ai" && !data.title?.trim() ? "AI Exam" : data.title?.trim() ?? "" const resolvedTitle = data.mode === "ai" && !data.title?.trim() ? "AI Exam" : data.title?.trim() ?? ""
const resolvedSubject = data.subject?.trim() || subjects[0]?.id || "" const resolvedSubject = data.subject?.trim() || subjects[0]?.id || ""
const resolvedGrade = data.grade?.trim() || grades[0]?.id || "" const resolvedGrade = data.grade?.trim() || grades[0]?.id || ""
@@ -106,13 +115,13 @@ export function ExamForm() {
const resolvedDurationMin = typeof data.durationMin === "number" && data.durationMin > 0 ? data.durationMin : 90 const resolvedDurationMin = typeof data.durationMin === "number" && data.durationMin > 0 ? data.durationMin : 90
if (data.mode === "ai" && (!resolvedSubject || !resolvedGrade)) { if (data.mode === "ai" && (!resolvedSubject || !resolvedGrade)) {
toast.error("Missing subject or grade configuration") toast.error(t("exam.form.missingSubjectOrGrade"))
return return
} }
if (data.mode === "ai") { if (data.mode === "ai") {
const signature = preview.buildPreviewSignature(data) const signature = preview.buildPreviewSignature(data)
if (!preview.previewSignature || signature !== preview.previewSignature || preview.previewNodes.length === 0) { if (!preview.previewSignature || signature !== preview.previewSignature || preview.previewNodes.length === 0) {
toast.error("Please preview and confirm before creating") toast.error(t("exam.form.previewBeforeCreate"))
return return
} }
} }
@@ -153,17 +162,21 @@ export function ExamForm() {
: await createExamAction(null, formData) : await createExamAction(null, formData)
if (result.success && result.data) { if (result.success && result.data) {
toast.success("Exam draft created", { toast.success(t("exam.form.createSuccess"), {
description: "Redirecting to exam builder...", description: t("exam.form.redirecting"),
}) })
router.push(`/teacher/exams/${result.data}/build`) router.push(`/teacher/exams/${result.data}/build`)
} else { } else {
toast.error(result.message || "Failed to create exam") toast.error(result.message || t("exam.form.createFailed"))
} }
}) })
} }
const handleCreateClick = () => { const handleCreateClick = () => {
if (isRichMode) {
router.push("/teacher/exams/new")
return
}
if (isAiMode) { if (isAiMode) {
preview.handleBackgroundPreview() preview.handleBackgroundPreview()
return return
@@ -198,10 +211,20 @@ export function ExamForm() {
} }
return ( return (
<div className="grid w-full grid-cols-3 gap-8"> <div className="space-y-6">
<Form {...form}> {/* 顶部: 创建方式选择 */}
<form onSubmit={handleFormSubmit} className="col-span-2 space-y-6"> <ExamModeSelector
{!isAiMode && ( mode={mode}
setMode={(value) => form.setValue("mode", value)}
isPending={isPending}
handleCreateClick={handleCreateClick}
/>
{/* 富文本模式: 不显示下方表单 */}
{isRichMode ? null : (
<Form {...form}>
<form onSubmit={handleFormSubmit} className="space-y-6">
{/* 基本信息(始终可见) */}
<ExamBasicInfoForm <ExamBasicInfoForm
control={form.control} control={form.control}
subjects={subjects} subjects={subjects}
@@ -209,27 +232,31 @@ export function ExamForm() {
loadingSubjects={loadingSubjects} loadingSubjects={loadingSubjects}
loadingGrades={loadingGrades} loadingGrades={loadingGrades}
/> />
)}
{isAiMode && ( {/* AI 模式: 显示 AI 生成器 */}
<ExamAiGenerator {isAiMode && (
form={form} <ExamAiGenerator
control={form.control} form={form}
aiProviders={aiProviders} control={form.control}
setAiProviders={setAiProviders} aiProviders={aiProviders}
loadingAiProviders={loadingAiProviders} setAiProviders={setAiProviders}
handlePreview={preview.handlePreview} loadingAiProviders={loadingAiProviders}
handleBackgroundPreview={preview.handleBackgroundPreview} handlePreview={preview.handlePreview}
previewLoading={preview.previewLoading} handleBackgroundPreview={preview.handleBackgroundPreview}
previewTasks={preview.previewTasks} previewLoading={preview.previewLoading}
handleOpenPreviewTask={preview.handleOpenPreviewTask} previewTasks={preview.previewTasks}
activePreviewTaskCount={activePreviewTaskCount} handleOpenPreviewTask={preview.handleOpenPreviewTask}
runningPreviewTaskCount={runningPreviewTaskCount} activePreviewTaskCount={activePreviewTaskCount}
queuedPreviewTaskCount={queuedPreviewTaskCount} runningPreviewTaskCount={runningPreviewTaskCount}
/> queuedPreviewTaskCount={queuedPreviewTaskCount}
)} />
<ExamModeConfig /> )}
</form>
</Form> {/* 考试模式配置(始终可见) */}
<ExamModeConfig />
</form>
</Form>
)}
<ExamPreviewDialog <ExamPreviewDialog
previewOpen={preview.previewOpen} previewOpen={preview.previewOpen}
@@ -254,15 +281,6 @@ export function ExamForm() {
handleConfirmCreate={handleConfirmCreate} handleConfirmCreate={handleConfirmCreate}
previewTitleValue={previewTitleValue} previewTitleValue={previewTitleValue}
/> />
<div className="col-span-1 space-y-6">
<ExamModeSelector
mode={mode}
setMode={(value) => form.setValue("mode", value)}
isPending={isPending}
handleCreateClick={handleCreateClick}
/>
</div>
</div> </div>
) )
} }

View File

@@ -1,20 +1,16 @@
"use client" "use client"
import { Loader2, Sparkles, BookOpen } from "lucide-react" import { useRouter } from "next/navigation"
import { useTranslations } from "next-intl"
import { Loader2, Sparkles, BookOpen, FileText, ArrowRight } from "lucide-react"
import { cn } from "@/shared/lib/utils" import { cn } from "@/shared/lib/utils"
import { Button } from "@/shared/components/ui/button" import { Button } from "@/shared/components/ui/button"
import {
Card, export type ExamCreationMode = "manual" | "ai" | "rich"
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/shared/components/ui/card"
type ExamModeSelectorProps = { type ExamModeSelectorProps = {
mode: "manual" | "ai" mode: ExamCreationMode
setMode: (mode: "manual" | "ai") => void setMode: (mode: ExamCreationMode) => void
isPending: boolean isPending: boolean
handleCreateClick: () => void handleCreateClick: () => void
} }
@@ -25,61 +21,112 @@ export function ExamModeSelector({
isPending, isPending,
handleCreateClick, handleCreateClick,
}: ExamModeSelectorProps) { }: ExamModeSelectorProps) {
return ( const router = useRouter()
<Card> const t = useTranslations("examHomework")
<CardHeader>
<CardTitle>Assembly Mode</CardTitle>
<CardDescription>
Choose how to build the exam structure.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col space-y-3">
<button
type="button"
className={cn(
"relative flex cursor-pointer flex-col rounded-lg border p-4 shadow-sm outline-none transition-all hover:bg-accent hover:text-accent-foreground text-left",
mode === "manual" ? "border-primary ring-1 ring-primary" : "border-border"
)}
onClick={() => setMode("manual")}
>
<div className="flex items-center gap-2">
<BookOpen className="h-4 w-4 text-primary" />
<span className="font-medium">Manual Assembly</span>
</div>
<span className="mt-1 text-xs text-muted-foreground">
Manually select questions from the bank and organize structure.
</span>
</button>
<button const modes: Array<{
key: ExamCreationMode
icon: typeof BookOpen
title: string
description: string
}> = [
{
key: "manual",
icon: BookOpen,
title: t("exam.form.modeManual"),
description: t("exam.form.modeManualDesc"),
},
{
key: "ai",
icon: Sparkles,
title: t("exam.form.modeAi"),
description: t("exam.form.modeAiDesc"),
},
{
key: "rich",
icon: FileText,
title: t("exam.form.modeRich"),
description: t("exam.form.modeRichDesc"),
},
]
return (
<div className="space-y-4">
{/* 顶部3个大卡片 */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
{modes.map((m) => {
const Icon = m.icon
const isActive = mode === m.key
return (
<button
key={m.key}
type="button"
className={cn(
"relative flex cursor-pointer flex-col rounded-lg border-2 p-5 shadow-sm outline-none transition-all text-left",
isActive
? "border-primary bg-primary/5 ring-2 ring-primary/20"
: "border-border hover:border-primary/50 hover:bg-accent/50"
)}
onClick={() => setMode(m.key)}
>
<div className="flex items-center gap-2">
<div className={cn(
"flex h-10 w-10 items-center justify-center rounded-lg",
isActive ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
)}>
<Icon className="h-5 w-5" />
</div>
<span className="font-semibold">{m.title}</span>
</div>
<span className="mt-2 text-sm text-muted-foreground leading-relaxed">
{m.description}
</span>
{isActive && (
<div className="absolute right-3 top-3 h-2 w-2 rounded-full bg-primary" />
)}
</button>
)
})}
</div>
{/* 富文本模式: 显示跳转按钮 */}
{mode === "rich" && (
<div className="rounded-lg border bg-muted/30 p-6 text-center">
<p className="text-sm text-muted-foreground mb-4">
{t("exam.form.modeRichHint")}
</p>
<Button
type="button" type="button"
className={cn( size="lg"
"relative flex cursor-pointer flex-col rounded-lg border p-4 shadow-sm outline-none transition-all hover:bg-accent hover:text-accent-foreground text-left", onClick={() => router.push("/teacher/exams/new")}
mode === "ai" ? "border-primary ring-1 ring-primary" : "border-border" className="gap-2"
)}
onClick={() => setMode("ai")}
> >
<div className="flex items-center gap-2"> <FileText className="h-4 w-4" />
<Sparkles className="h-4 w-4 text-primary" /> {t("exam.form.modeRichAction")}
<span className="font-medium">AI Generation</span> <ArrowRight className="h-4 w-4" />
</div> </Button>
<span className="mt-1 text-xs text-muted-foreground">
Automatically generate a draft exam based on your input.
</span>
</button>
</div> </div>
</CardContent> )}
<CardFooter>
<Button type="button" className="w-full" disabled={isPending} onClick={handleCreateClick}> {/* 手动/AI 模式: 显示创建按钮 */}
{isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} {mode !== "rich" && (
{isPending <div className="flex justify-end">
? "Creating Draft..." <Button
: mode === "ai" type="button"
? "后台生成试卷" size="lg"
: "Create & Start Building"} disabled={isPending}
</Button> onClick={handleCreateClick}
</CardFooter> className="gap-2 min-w-[200px]"
</Card> >
{isPending && <Loader2 className="h-4 w-4 animate-spin" />}
{isPending
? t("exam.form.creating")
: mode === "ai"
? t("exam.form.modeAiAction")
: t("exam.form.modeManualAction")}
</Button>
</div>
)}
</div>
) )
} }

View File

@@ -32,7 +32,32 @@
"createFailed": "Failed to create exam", "createFailed": "Failed to create exam",
"loadFormFailed": "Failed to load form data", "loadFormFailed": "Failed to load form data",
"loadSubjectsFailed": "Failed to load subjects", "loadSubjectsFailed": "Failed to load subjects",
"loadGradesFailed": "Failed to load grades" "loadGradesFailed": "Failed to load grades",
"creating": "Creating...",
"detailsTitle": "Exam Details",
"detailsDesc": "Define the core information for your exam.",
"titlePlaceholder": "e.g. Midterm Mathematics Exam",
"subjectPlaceholder": "Select subject",
"gradePlaceholder": "Select grade level",
"difficultyPlaceholder": "Select level",
"difficultyLevel1": "Level 1 (Easy)",
"difficultyLevel2": "Level 2",
"difficultyLevel3": "Level 3 (Medium)",
"difficultyLevel4": "Level 4",
"difficultyLevel5": "Level 5 (Hard)",
"loadingSubjects": "Loading subjects...",
"loadingGrades": "Loading grades...",
"scheduledAtDesc": "If set, this exam will be scheduled for a specific time.",
"modeManual": "Manual Assembly",
"modeManualDesc": "Manually select questions from the bank and organize structure.",
"modeManualAction": "Create & Start Building",
"modeAi": "AI Generation",
"modeAiDesc": "Paste exam text and AI will auto-parse questions into structured preview.",
"modeAiAction": "Generate in Background",
"modeRich": "Rich Text Editor",
"modeRichDesc": "Use rich text editor to paste exam text, manually or AI-assisted marking of questions, groups, blanks, etc.",
"modeRichHint": "The rich text editor supports selecting text to mark as questions/groups/dotted chars/blanks, with live preview on the right.",
"modeRichAction": "Open Rich Text Editor"
}, },
"status": { "status": {
"draft": "Draft", "draft": "Draft",

View File

@@ -32,7 +32,32 @@
"createFailed": "创建考试失败", "createFailed": "创建考试失败",
"loadFormFailed": "加载表单数据失败", "loadFormFailed": "加载表单数据失败",
"loadSubjectsFailed": "加载科目失败", "loadSubjectsFailed": "加载科目失败",
"loadGradesFailed": "加载年级失败" "loadGradesFailed": "加载年级失败",
"creating": "创建中...",
"detailsTitle": "基本信息",
"detailsDesc": "设置考试的核心信息。",
"titlePlaceholder": "例如:期中数学考试",
"subjectPlaceholder": "选择科目",
"gradePlaceholder": "选择年级",
"difficultyPlaceholder": "选择难度",
"difficultyLevel1": "1级 (简单)",
"difficultyLevel2": "2级",
"difficultyLevel3": "3级 (中等)",
"difficultyLevel4": "4级",
"difficultyLevel5": "5级 (困难)",
"loadingSubjects": "加载科目中...",
"loadingGrades": "加载年级中...",
"scheduledAtDesc": "设置后,考试将在指定时间开放。",
"modeManual": "手动组卷",
"modeManualDesc": "从题库手动选择题目并组织试卷结构。",
"modeManualAction": "创建并开始组卷",
"modeAi": "AI 生成",
"modeAiDesc": "粘贴试卷文本,AI 自动解析题目并生成结构化预览。",
"modeAiAction": "后台生成试卷",
"modeRich": "富文本编辑器",
"modeRichDesc": "使用富文本编辑器粘贴试卷文本,手动或 AI 辅助标记题目、分组、填空等。",
"modeRichHint": "富文本编辑器支持选中文本标记题目/分组/加点字/填空,右侧实时预览试卷效果。",
"modeRichAction": "打开富文本编辑器"
}, },
"status": { "status": {
"draft": "草稿", "draft": "草稿",