diff --git a/src/app/parent/overview-tab.tsx b/src/app/parent/overview-tab.tsx index 675e135..40c7410 100644 --- a/src/app/parent/overview-tab.tsx +++ b/src/app/parent/overview-tab.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import Link from "next/link"; import { cn } from "@/lib/utils"; import { addDays, dowOf, todayStr } from "@/lib/dates"; import { subjectMeta } from "@/lib/subjects"; @@ -44,10 +45,13 @@ export function OverviewTab({ data }: { data: OverviewData }) { {kid.grammar.done}/{kid.grammar.total}

-
+

待掌握错题

{kid.wrongOpen}

-
+

最近周测

diff --git a/src/app/parent/wrongbook/actions.ts b/src/app/parent/wrongbook/actions.ts new file mode 100644 index 0000000..fbc2760 --- /dev/null +++ b/src/app/parent/wrongbook/actions.ts @@ -0,0 +1,23 @@ +"use server"; + +import { eq } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; +import { db } from "@/db"; +import { users, wrongBook } from "@/db/schema"; +import { getSessionUser } from "@/lib/auth"; +import { setMastered } from "@/lib/wrongbook"; + +// xuchao: 家长标记某错题已掌握/未掌握(仅限本家庭孩子,防越权) +export async function markMastered(id: number, mastered: boolean): Promise { + const parent = await getSessionUser(); + if (!parent || parent.role !== "parent") return; + + const [row] = await db.select().from(wrongBook).where(eq(wrongBook.id, id)).limit(1); + if (!row) return; + const [child] = await db.select().from(users).where(eq(users.id, row.childUserId)).limit(1); + if (!child || child.role !== "child" || child.familyId !== parent.familyId) return; + + await setMastered(id, row.childUserId, mastered); + revalidatePath("/parent"); + revalidatePath("/parent/wrongbook"); +} \ No newline at end of file diff --git a/src/app/parent/wrongbook/page.tsx b/src/app/parent/wrongbook/page.tsx new file mode 100644 index 0000000..58815a9 --- /dev/null +++ b/src/app/parent/wrongbook/page.tsx @@ -0,0 +1,80 @@ +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { and, eq } from "drizzle-orm"; +import { BackgroundBlobs } from "@/components/background-blobs"; +import { db } from "@/db"; +import { users } from "@/db/schema"; +import { getSessionUser } from "@/lib/auth"; +import { listWrong } from "@/lib/wrongbook"; +import { photoViewUrl } from "@/lib/oss"; +import type { QuizQ } from "@/lib/quiz-parse"; +import { WrongRow } from "@/app/wrongbook/wrong-row"; +import { markMastered } from "./actions"; + +// xuchao: 家长查看某孩子待掌握错题(?kid=),家庭归属校验防越权 +export default async function ParentWrongBookPage({ + searchParams, +}: { + searchParams: Promise<{ kid?: string }>; +}) { + const parent = await getSessionUser(); + if (!parent) redirect("/login"); + if (parent.role !== "parent") redirect("/kid"); + + const kidId = Number((await searchParams).kid); + if (!Number.isInteger(kidId)) redirect("/parent"); + const [kid] = await db + .select({ id: users.id, name: users.displayName }) + .from(users) + .where(and(eq(users.id, kidId), eq(users.role, "child"), eq(users.familyId, parent.familyId))) + .limit(1); + if (!kid) redirect("/parent"); + + const rows = (await listWrong(kid.id)).filter((r) => !r.mastered); + + return ( +

+ +
+ + ← 返回家长中心 + +

待掌握错题

+ {kid.name} · {rows.length} 题 +
+ + {rows.length === 0 ? ( +
+

🎉

+

没有待掌握错题!

+
+ ) : ( +
+ {rows.map((r) => { + let q: QuizQ; + try { + q = JSON.parse(r.question) as QuizQ; + } catch { + return null; + } + // xuchao: 解答题展示当时的作答照片(OSS 签名地址;开发环境为本地回退) + const photoUrl = q.type === "solve" && q.solveSnap?.ossKey ? photoViewUrl(q.solveSnap.ossKey) : ""; + return ( + + ); + })} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/app/wrongbook/wrong-row.tsx b/src/app/wrongbook/wrong-row.tsx index 2e341f8..c7f92a9 100644 --- a/src/app/wrongbook/wrong-row.tsx +++ b/src/app/wrongbook/wrong-row.tsx @@ -7,6 +7,7 @@ import { cn } from "@/lib/utils"; import type { QuizQ } from "@/lib/quiz-parse"; import { toggleMastered } from "./actions"; +// xuchao: markAction 供家长版错题页注入专属标记动作,缺省走孩子端 toggleMastered export function WrongRow({ id, sourceTitle, @@ -15,6 +16,7 @@ export function WrongRow({ correctAnswer, mastered, photoUrl = "", + markAction, }: { id: number; sourceTitle: string | null; @@ -23,6 +25,7 @@ export function WrongRow({ correctAnswer: string | null; mastered: boolean; photoUrl?: string; + markAction?: (id: number, mastered: boolean) => Promise; }) { const [pending, startTransition] = useTransition(); const isSolve = q.type === "solve"; @@ -81,7 +84,7 @@ export function WrongRow({ variant={mastered ? "outline" : "secondary"} size="sm" disabled={pending} - onClick={() => startTransition(() => toggleMastered(id, !mastered))} + onClick={() => startTransition(() => (markAction ?? toggleMastered)(id, !mastered))} > {mastered ? ( <>