diff --git a/src/app/parent/dashboard.tsx b/src/app/parent/dashboard.tsx index b94708e..e848dde 100644 --- a/src/app/parent/dashboard.tsx +++ b/src/app/parent/dashboard.tsx @@ -169,6 +169,12 @@ export function ParentDashboard({

改考试成绩/达标,删除误生成记录

+ +
+

🗂️ 考试试卷管理

+

新增/编辑/删除试卷,孩子端立即生效

+
+ diff --git a/src/app/parent/exams/[paperId]/page.tsx b/src/app/parent/exams/[paperId]/page.tsx new file mode 100644 index 0000000..9b9e2e3 --- /dev/null +++ b/src/app/parent/exams/[paperId]/page.tsx @@ -0,0 +1,47 @@ +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { BackgroundBlobs } from "@/components/background-blobs"; +import { getSessionUser } from "@/lib/auth"; +import { getPaper } from "@/lib/exam"; +import { PaperForm } from "../paper-form"; + +// xuchao: 编辑试卷页——元信息 + 题目 JSON,保存即刷新孩子端 /exam +export default async function EditExamPage({ params }: { params: Promise<{ paperId: string }> }) { + const parent = await getSessionUser(); + if (!parent) redirect("/login"); + if (parent.role !== "parent") redirect("/kid"); + + const paper = await getPaper((await params).paperId); + if (!paper) redirect("/parent/exams"); + + return ( +
+ +
+ + ← 试卷列表 + +

{paper.title}

+
+
+ +
+
+ ); +} diff --git a/src/app/parent/exams/new/page.tsx b/src/app/parent/exams/new/page.tsx new file mode 100644 index 0000000..eaf4819 --- /dev/null +++ b/src/app/parent/exams/new/page.tsx @@ -0,0 +1,27 @@ +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { BackgroundBlobs } from "@/components/background-blobs"; +import { getSessionUser } from "@/lib/auth"; +import { PaperForm } from "../paper-form"; + +// xuchao: 新建试卷页(paperId 唯一;科目/年级/学期固定 math/g7/s1,孩子端立即出现) +export default async function NewExamPage() { + const parent = await getSessionUser(); + if (!parent) redirect("/login"); + if (parent.role !== "parent") redirect("/kid"); + + return ( +
+ +
+ + ← 试卷列表 + +

新建试卷

+
+
+ +
+
+ ); +} diff --git a/src/app/parent/exams/page.tsx b/src/app/parent/exams/page.tsx new file mode 100644 index 0000000..92e441e --- /dev/null +++ b/src/app/parent/exams/page.tsx @@ -0,0 +1,87 @@ +import Link from "next/link"; +import { Plus } from "lucide-react"; +import { redirect } from "next/navigation"; +import { BackgroundBlobs } from "@/components/background-blobs"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { getSessionUser } from "@/lib/auth"; +import { EXAM_TYPE_LABEL, listPapers } from "@/lib/exam"; + +// xuchao: 试卷管理列表——与孩子端考试中心同分组(月份),点击进入编辑 +const MONTH_LABEL: Record = { + 0: "入学 · 分班考", + 9: "9 月 · 有理数", + 10: "10 月 · 整式的加减", + 11: "11 月 · 一元一次方程", + 12: "12 月 · 几何图形初步", + 1: "1 月 · 期末全册", +}; +const MONTH_ORDER = [0, 9, 10, 11, 12, 1]; + +function countQuestions(qs: string): number { + try { + const arr = JSON.parse(qs) as unknown[]; + return Array.isArray(arr) ? arr.length : 0; + } catch { + return 0; + } +} + +export default async function ExamsAdmin() { + const parent = await getSessionUser(); + if (!parent) redirect("/login"); + if (parent.role !== "parent") redirect("/kid"); + + const papers = await listPapers("g7", "s1"); + + return ( +
+ +
+ + ← 返回家长中心 + +

考试试卷管理

+ + + +
+

+ 点击试卷可编辑元信息与题目,保存后孩子端考试中心立即生效。 +

+ + {papers.length === 0 && ( +
+

还没有试卷,点右上角新建

+
+ )} + + {MONTH_ORDER.map((m) => { + const group = papers.filter((p) => p.month === m); + if (group.length === 0) return null; + return ( +
+

{MONTH_LABEL[m]}

+
+ {group.map((p) => ( + + {p.title} + + {EXAM_TYPE_LABEL[p.examType] ?? p.examType} + + {p.seq} 号卷 + + ))} +
+
+ ); + })} +
+ ); +} diff --git a/src/app/parent/exams/paper-form.tsx b/src/app/parent/exams/paper-form.tsx new file mode 100644 index 0000000..8ded880 --- /dev/null +++ b/src/app/parent/exams/paper-form.tsx @@ -0,0 +1,187 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +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 { deleteExamPaper, saveExamPaper } from "../actions"; + +// xuchao: 与 lib/exam.ts 的 EXAM_TYPE_LABEL 保持同步(客户端不能直接引用服务端模块) +const EXAM_TYPE_LABEL: Record = { + entrance: "入学考", + monthly: "月考", + unit: "单元卷", + midterm: "期中", + final: "期末", +}; + +export type PaperMeta = { + paperId: string; + title: string; + examType: string; + region: string | null; + year: number | null; + duration: number | null; + fullScore: number | null; + month: number; + seq: number; + sourceUrl: string | null; + summary: string | null; + questions: string; +}; + +// xuchao: 试卷编辑器——元信息表单 + 题目 JSON 文本域;保存走服务端完整校验 +export function PaperForm({ paper, isNew }: { paper: PaperMeta | null; isNew: boolean }) { + const router = useRouter(); + const [f, setF] = useState({ + paperId: paper?.paperId ?? "", + title: paper?.title ?? "", + examType: paper?.examType ?? "monthly", + region: paper?.region ?? "", + year: paper?.year != null ? String(paper.year) : "", + duration: paper?.duration != null ? String(paper.duration) : "", + fullScore: paper?.fullScore != null ? String(paper.fullScore) : "100", + month: String(paper?.month ?? 9), + seq: String(paper?.seq ?? 1), + sourceUrl: paper?.sourceUrl ?? "", + summary: paper?.summary ?? "", + questions: paper ? JSON.stringify(JSON.parse(paper.questions), null, 2) : "[]", + }); + 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])); + startTransition(() => { + saveExamPaper(fd).then((r) => { + if (r.error) setMsg({ text: r.error }); + else setMsg({ ok: true, text: "已保存,孩子端立即生效" }); + }); + }); + }; + + const remove = () => { + if (!paper || !confirm(`确定删除试卷「${paper.title}」?孩子的历史作答记录会保留。`)) return; + startTransition(() => { + deleteExamPaper(paper.paperId).then(() => router.push("/parent/exams")); + }); + }; + + const n = (key: keyof typeof f) => ({ + value: f[key], + onChange: (e: React.ChangeEvent) => + setF((prev) => ({ ...prev, [key]: e.target.value })), + }); + + return ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ + {preview.text} +
+