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}
-
最近周测
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 ? (
<>