家长端结构化题目编辑器:题型联动表单替代 JSON 文本域,应用题支持 judgeMode 切换
- 新增 question-list.tsx:结构化题目卡片列表(选择题/判断题/填空题/应用题) - 新增 question-list.test.tsx:4 个 RTL 组件测试覆盖渲染/新增/编辑/校验 - paper-form.tsx:JSON textarea 替换为 QuestionList,保存时序列化 QuizQ[] - vitest.config.ts:加 @vitejs/plugin-react 支持 JSX 组件测试 - test-setup.ts:注册 afterEach cleanup 避免跨测试 DOM 污染 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
255c9a244c
commit
0749ba2c38
@@ -6,7 +6,9 @@ import { CheckCircle2, Save, Trash2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import type { QuizQ } from "@/lib/quiz-parse";
|
||||
import { deleteExamPaper, saveExamPaper } from "../actions";
|
||||
import { QuestionList } from "./question-list";
|
||||
|
||||
// xuchao: 与 lib/exam.ts 的 EXAM_TYPE_LABEL 保持同步(客户端不能直接引用服务端模块)
|
||||
const EXAM_TYPE_LABEL: Record<string, string> = {
|
||||
@@ -32,7 +34,7 @@ export type PaperMeta = {
|
||||
questions: string;
|
||||
};
|
||||
|
||||
// xuchao: 试卷编辑器——元信息表单 + 题目 JSON 文本域;保存走服务端完整校验
|
||||
// xuchao: 试卷编辑器——元信息表单 + 结构化题目编辑器;保存走服务端完整校验
|
||||
export function PaperForm({ paper, isNew }: { paper: PaperMeta | null; isNew: boolean }) {
|
||||
const router = useRouter();
|
||||
const [f, setF] = useState({
|
||||
@@ -47,26 +49,23 @@ export function PaperForm({ paper, isNew }: { paper: PaperMeta | null; isNew: bo
|
||||
seq: String(paper?.seq ?? 1),
|
||||
sourceUrl: paper?.sourceUrl ?? "",
|
||||
summary: paper?.summary ?? "",
|
||||
questions: paper ? JSON.stringify(JSON.parse(paper.questions), null, 2) : "[]",
|
||||
});
|
||||
// xuchao: 题目用结构化数组管理,保存时序列化为 JSON(与旧 JSON 形状一致)
|
||||
const [questions, setQuestions] = useState<QuizQ[]>(() => {
|
||||
try {
|
||||
return paper ? (JSON.parse(paper.questions) as QuizQ[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const [msg, setMsg] = useState<{ ok?: boolean; text: string } | null>(null);
|
||||
const [pending, startTransition] = useTransition();
|
||||
|
||||
// xuchao: 客户端轻量预览:JSON 可解析则显示题数,失败显示错误(完整校验在服务端)
|
||||
const preview = (() => {
|
||||
try {
|
||||
const arr = JSON.parse(f.questions) as unknown[];
|
||||
if (!Array.isArray(arr)) return { ok: false, text: "题目必须是 JSON 数组" };
|
||||
return { ok: true, text: `共 ${arr.length} 道题` };
|
||||
} catch (e) {
|
||||
return { ok: false, text: `JSON 语法错误:${(e as Error).message}` };
|
||||
}
|
||||
})();
|
||||
|
||||
const save = () => {
|
||||
setMsg(null);
|
||||
const fd = new FormData();
|
||||
(Object.keys(f) as (keyof typeof f)[]).forEach((k) => fd.set(k, f[k]));
|
||||
fd.set("questions", JSON.stringify(questions));
|
||||
startTransition(() => {
|
||||
saveExamPaper(fd).then((r) => {
|
||||
if (r.error) setMsg({ text: r.error });
|
||||
@@ -158,15 +157,10 @@ export function PaperForm({ paper, isNew }: { paper: PaperMeta | null; isNew: bo
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>题目(quiz JSON 数组)</Label>
|
||||
<span className={preview.ok ? "text-xs text-emerald-600" : "text-xs text-destructive"}>{preview.text}</span>
|
||||
</div>
|
||||
<textarea
|
||||
value={f.questions}
|
||||
onChange={n("questions").onChange}
|
||||
rows={16}
|
||||
className="w-full rounded-lg border border-slate-200 bg-white p-3 font-mono text-xs leading-relaxed focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200"
|
||||
<Label>题目(结构化编辑)</Label>
|
||||
<QuestionList
|
||||
value={questions}
|
||||
onChange={(qs) => setQuestions(qs)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user