From f6d5aa868012b5750e79538d5b60e2cc1e320b1e Mon Sep 17 00:00:00 2001 From: jacobxu666 Date: Wed, 19 Aug 2026 11:23:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=B6=E9=95=BF=E4=B8=AD=E5=BF=83=E5=BE=85?= =?UTF-8?q?=E6=8E=8C=E6=8F=A1=E9=94=99=E9=A2=98=E6=9F=A5=E7=9C=8B:?= =?UTF-8?q?=E6=A6=82=E8=A7=88=E5=8D=A1=E7=89=87=E5=8F=AF=E7=82=B9=E5=87=BB?= =?UTF-8?q?,=E8=BF=9B=E5=85=A5=E5=AD=A9=E5=AD=90=E9=94=99=E9=A2=98?= =?UTF-8?q?=E9=A1=B5=E5=B9=B6=E5=8F=AF=E6=A0=87=E8=AE=B0=E5=B7=B2=E6=8E=8C?= =?UTF-8?q?=E6=8F=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 概览页「待掌握错题」卡片改 Link 进入 /parent/wrongbook?kid=xx(仅未掌握), WrongRow 注入 markAction 由家长 action 标记,家庭归属防越权,孩子端零改动。 Co-Authored-By: Claude Haiku 4.5 --- src/app/parent/overview-tab.tsx | 8 ++- src/app/parent/wrongbook/actions.ts | 23 +++++++++ src/app/parent/wrongbook/page.tsx | 80 +++++++++++++++++++++++++++++ src/app/wrongbook/wrong-row.tsx | 5 +- 4 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/app/parent/wrongbook/actions.ts create mode 100644 src/app/parent/wrongbook/page.tsx 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 ? ( <>