家长中心待掌握错题查看:概览卡片可点击,进入孩子错题页并可标记已掌握
概览页「待掌握错题」卡片改 Link 进入 /parent/wrongbook?kid=xx(仅未掌握), WrongRow 注入 markAction 由家长 action 标记,家庭归属防越权,孩子端零改动。 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
7a11a89031
commit
f6d5aa8680
@@ -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}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-100 bg-white p-3 text-center shadow-soft">
|
||||
<Link
|
||||
href={`/parent/wrongbook?kid=${kid.id}`}
|
||||
className="rounded-xl border border-slate-100 bg-white p-3 text-center shadow-soft transition-all duration-200 hover:-translate-y-0.5 hover:border-rose-200"
|
||||
>
|
||||
<p className="text-xs text-muted-foreground">待掌握错题</p>
|
||||
<p className="text-lg font-extrabold text-rose-600">{kid.wrongOpen}</p>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="rounded-xl border border-slate-100 bg-white p-3 text-center shadow-soft">
|
||||
<p className="text-xs text-muted-foreground">最近周测</p>
|
||||
<p className="text-lg font-extrabold text-violet-600">
|
||||
|
||||
@@ -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<void> {
|
||||
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");
|
||||
}
|
||||
@@ -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 (
|
||||
<main className="relative mx-auto max-w-2xl space-y-5 p-4 pb-12">
|
||||
<BackgroundBlobs />
|
||||
<header className="flex items-center justify-between">
|
||||
<Link href="/parent" className="text-sm font-medium text-indigo-600">
|
||||
← 返回家长中心
|
||||
</Link>
|
||||
<h1 className="text-xl font-extrabold text-slate-900">待掌握错题</h1>
|
||||
<span className="text-xs text-muted-foreground">{kid.name} · {rows.length} 题</span>
|
||||
</header>
|
||||
|
||||
{rows.length === 0 ? (
|
||||
<div className="rounded-2xl border border-slate-100 bg-white p-8 text-center shadow-soft">
|
||||
<p className="text-3xl">🎉</p>
|
||||
<p className="mt-2 font-bold text-slate-900">没有待掌握错题!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{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 (
|
||||
<WrongRow
|
||||
key={r.id}
|
||||
id={r.id}
|
||||
sourceTitle={r.sourceTitle}
|
||||
q={q}
|
||||
userAnswer={r.userAnswer}
|
||||
correctAnswer={r.correctAnswer}
|
||||
mastered={r.mastered}
|
||||
photoUrl={photoUrl}
|
||||
markAction={markMastered}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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<void>;
|
||||
}) {
|
||||
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 ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user