家长中心待掌握错题查看:概览卡片可点击,进入孩子错题页并可标记已掌握
概览页「待掌握错题」卡片改 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";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { addDays, dowOf, todayStr } from "@/lib/dates";
|
import { addDays, dowOf, todayStr } from "@/lib/dates";
|
||||||
import { subjectMeta } from "@/lib/subjects";
|
import { subjectMeta } from "@/lib/subjects";
|
||||||
@@ -44,10 +45,13 @@ export function OverviewTab({ data }: { data: OverviewData }) {
|
|||||||
{kid.grammar.done}/{kid.grammar.total}
|
{kid.grammar.done}/{kid.grammar.total}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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-xs text-muted-foreground">待掌握错题</p>
|
||||||
<p className="text-lg font-extrabold text-rose-600">{kid.wrongOpen}</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">
|
<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-xs text-muted-foreground">最近周测</p>
|
||||||
<p className="text-lg font-extrabold text-violet-600">
|
<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 type { QuizQ } from "@/lib/quiz-parse";
|
||||||
import { toggleMastered } from "./actions";
|
import { toggleMastered } from "./actions";
|
||||||
|
|
||||||
|
// xuchao: markAction 供家长版错题页注入专属标记动作,缺省走孩子端 toggleMastered
|
||||||
export function WrongRow({
|
export function WrongRow({
|
||||||
id,
|
id,
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
@@ -15,6 +16,7 @@ export function WrongRow({
|
|||||||
correctAnswer,
|
correctAnswer,
|
||||||
mastered,
|
mastered,
|
||||||
photoUrl = "",
|
photoUrl = "",
|
||||||
|
markAction,
|
||||||
}: {
|
}: {
|
||||||
id: number;
|
id: number;
|
||||||
sourceTitle: string | null;
|
sourceTitle: string | null;
|
||||||
@@ -23,6 +25,7 @@ export function WrongRow({
|
|||||||
correctAnswer: string | null;
|
correctAnswer: string | null;
|
||||||
mastered: boolean;
|
mastered: boolean;
|
||||||
photoUrl?: string;
|
photoUrl?: string;
|
||||||
|
markAction?: (id: number, mastered: boolean) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const isSolve = q.type === "solve";
|
const isSolve = q.type === "solve";
|
||||||
@@ -81,7 +84,7 @@ export function WrongRow({
|
|||||||
variant={mastered ? "outline" : "secondary"}
|
variant={mastered ? "outline" : "secondary"}
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
onClick={() => startTransition(() => toggleMastered(id, !mastered))}
|
onClick={() => startTransition(() => (markAction ?? toggleMastered)(id, !mastered))}
|
||||||
>
|
>
|
||||||
{mastered ? (
|
{mastered ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user