家长端每周检测成绩管理页:/parent/weekly-records 编辑分数达标+删除,替换课程管理只读块

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-19 16:51:50 +08:00
co-authored by Claude Haiku 4.5
parent 859fdcbda7
commit abcdd5bed8
4 changed files with 185 additions and 36 deletions
+5 -14
View File
@@ -68,8 +68,6 @@ export type DevicesData = {
isCurrent: boolean; isCurrent: boolean;
}[]; }[];
}; };
export type WeeklyRow = { childName: string; weekKey: string; score: number; passed: boolean };
// xuchao: 家庭管理数据——家庭名 + 家长成员 + 孩子成员 // xuchao: 家庭管理数据——家庭名 + 家长成员 + 孩子成员
export type FamilyData = { export type FamilyData = {
familyName: string; familyName: string;
@@ -93,14 +91,12 @@ export function ParentDashboard({
overview, overview,
templates, templates,
devices, devices,
weekly,
points, points,
family, family,
}: { }: {
overview: OverviewData; overview: OverviewData;
templates: TemplatesData; templates: TemplatesData;
devices: DevicesData; devices: DevicesData;
weekly: WeeklyRow[];
points: PointsData; points: PointsData;
family: FamilyData; family: FamilyData;
}) { }) {
@@ -161,17 +157,12 @@ export function ParentDashboard({
<p className="mt-1 text-xs text-muted-foreground">改标题/目标/课文,保存即生效</p> <p className="mt-1 text-xs text-muted-foreground">改标题/目标/课文,保存即生效</p>
</div> </div>
</Link> </Link>
<div className="rounded-xl border border-slate-100 bg-white p-4 shadow-soft"> <Link href="/parent/weekly-records" className="block">
<p className="font-bold text-slate-900">📝 每周检测成绩</p> <div className="rounded-xl border border-slate-100 bg-white p-4 shadow-soft transition-transform duration-200 hover:-translate-y-0.5">
<div className="mt-2 space-y-1"> <p className="font-bold text-slate-900">📝 每周检测成绩管理</p>
{weekly.length === 0 && <p className="text-xs text-muted-foreground">暂无检测记录</p>} <p className="mt-1 text-xs text-muted-foreground">改分数/达标,删除误生成记录</p>
{weekly.map((w, i) => (
<p key={i} className="text-xs text-slate-600">
{w.childName} · {w.weekKey} · {w.score} 分 {w.passed ? "✅" : "❌"}
</p>
))}
</div>
</div> </div>
</Link>
</div> </div>
</Section> </Section>
-22
View File
@@ -32,7 +32,6 @@ import {
type OverviewData, type OverviewData,
type PointsData, type PointsData,
type TemplatesData, type TemplatesData,
type WeeklyRow,
} from "./dashboard"; } from "./dashboard";
const SH = { timeZone: "Asia/Shanghai" } as const; const SH = { timeZone: "Asia/Shanghai" } as const;
@@ -181,26 +180,6 @@ export default async function ParentPage() {
})), })),
}; };
// ===== 周测成绩(课程管理) =====
const weeklyRows = await db
.select({
childName: users.displayName,
weekKey: weeklyTest.weekKey,
score: weeklyTest.score,
passed: weeklyTest.passed,
})
.from(weeklyTest)
.innerJoin(users, eq(users.id, weeklyTest.childUserId))
.where(eq(users.familyId, parent.familyId))
.orderBy(desc(weeklyTest.takenAt))
.limit(10);
const weekly: WeeklyRow[] = weeklyRows.map((w) => ({
childName: w.childName,
weekKey: w.weekKey,
score: w.score ?? 0,
passed: w.passed,
}));
// ===== 积分管理(规则 + 每孩子总积分/最近流水) ===== // ===== 积分管理(规则 + 每孩子总积分/最近流水) =====
const rules = await listPointRules(); const rules = await listPointRules();
const pointsData: PointsData = { rules, kids: [] }; const pointsData: PointsData = { rules, kids: [] };
@@ -243,7 +222,6 @@ export default async function ParentPage() {
overview={overview} overview={overview}
templates={templates} templates={templates}
devices={devices} devices={devices}
weekly={weekly}
points={pointsData} points={pointsData}
family={family} family={family}
/> />
+56
View File
@@ -0,0 +1,56 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { desc, eq } from "drizzle-orm";
import { BackgroundBlobs } from "@/components/background-blobs";
import { db } from "@/db";
import { users, weeklyTest } from "@/db/schema";
import { getSessionUser } from "@/lib/auth";
import { WeeklyList } from "./weekly-list";
// xuchao: 周测成绩管理——编辑分数/达标、删除误生成记录(不联动积分打卡)
export default async function WeeklyRecordsPage() {
const parent = await getSessionUser();
if (!parent) redirect("/login");
if (parent.role !== "parent") redirect("/kid");
const rows = await db
.select({
id: weeklyTest.id,
childName: users.displayName,
weekKey: weeklyTest.weekKey,
score: weeklyTest.score,
passed: weeklyTest.passed,
takenAt: weeklyTest.takenAt,
})
.from(weeklyTest)
.innerJoin(users, eq(users.id, weeklyTest.childUserId))
.where(eq(users.familyId, parent.familyId))
.orderBy(desc(weeklyTest.takenAt))
.limit(100);
return (
<main className="relative mx-auto max-w-2xl space-y-5 p-4 pb-10">
<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">最近 {rows.length} 条</span>
</header>
<p className="rounded-lg border-l-4 border-indigo-400 bg-indigo-50 px-3 py-2 text-sm text-indigo-800">
修改分数/达标状态或删除记录不会影响已发的积分与当日打卡。
</p>
<WeeklyList
rows={rows.map((r) => ({
id: r.id,
childName: r.childName,
weekKey: r.weekKey,
score: r.score ?? 0,
passed: r.passed,
takenAt: r.takenAt.toLocaleDateString("zh-CN"),
}))}
/>
</main>
);
}
@@ -0,0 +1,124 @@
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { Check, Pencil, RotateCcw, Trash2, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { deleteWeeklyRecord, updateWeeklyRecord } from "../actions";
export type WeeklyRowItem = {
id: number;
childName: string;
weekKey: string;
score: number;
passed: boolean;
takenAt: string;
};
// xuchao: 周测成绩行内编辑(分数+达标开关)、删除;保存后刷新服务端列表
export function WeeklyList({ rows }: { rows: WeeklyRowItem[] }) {
const router = useRouter();
const [editingId, setEditingId] = useState<number | null>(null);
const [score, setScore] = useState("");
const [passed, setPassed] = useState(false);
const [msg, setMsg] = useState("");
const [pending, startTransition] = useTransition();
const startEdit = (r: WeeklyRowItem) => {
setEditingId(r.id);
setScore(String(r.score));
setPassed(r.passed);
setMsg("");
};
const save = () => {
if (editingId == null) return;
const s = Number(score);
const err = (t: string) => setMsg(t);
startTransition(async () => {
const r = await updateWeeklyRecord(editingId, s, passed);
if (r) err(r);
else {
setEditingId(null);
router.refresh();
}
});
};
const remove = (id: number, label: string) => {
if (!confirm(`确定删除「${label}」的这条周测记录?孩子可重新检测。`)) return;
startTransition(() => {
deleteWeeklyRecord(id).then(() => router.refresh());
});
};
return (
<div className="space-y-2.5">
{rows.length === 0 && (
<div className="rounded-2xl border border-slate-100 bg-white p-8 text-center shadow-soft">
<p className="text-sm text-muted-foreground">暂无周测记录</p>
</div>
)}
{rows.map((r) => (
<div key={r.id} className="rounded-xl border border-slate-100 bg-white p-3 shadow-soft">
{editingId === r.id ? (
<div className="space-y-2.5">
<p className="text-sm font-medium text-slate-800">
{r.childName} · {r.weekKey}
</p>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5 text-sm text-slate-600">
分数
<Input
className="h-8 w-20"
type="number"
min={0}
max={1000}
value={score}
onChange={(e) => setScore(e.target.value)}
/>
</div>
<label className="flex items-center gap-1.5 text-sm text-slate-600">
<Checkbox checked={passed} onCheckedChange={(v) => setPassed(v === true)} />
达标
</label>
{msg && <span className="text-xs font-medium text-destructive">{msg}</span>}
</div>
<div className="flex gap-2">
<Button size="sm" disabled={pending} onClick={save}>
<Check className="size-4" /> 保存
</Button>
<Button size="sm" variant="outline" disabled={pending} onClick={() => setEditingId(null)}>
<X className="size-4" /> 取消
</Button>
</div>
</div>
) : (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-slate-800">{r.childName}</span>
<span className="rounded-full bg-slate-100 px-2 py-0.5 text-xs text-muted-foreground">{r.weekKey}</span>
<span className={cn("text-sm font-bold", r.passed ? "text-emerald-600" : "text-rose-600")}>
{r.score} 分 {r.passed ? "✅" : "❌"}
</span>
<span className="ml-auto text-xs text-muted-foreground">{r.takenAt}</span>
<button type="button" aria-label="编辑" className="text-muted-foreground hover:text-indigo-600" onClick={() => startEdit(r)}>
<Pencil className="size-4" />
</button>
<button
type="button"
aria-label="删除"
className="text-muted-foreground hover:text-destructive"
onClick={() => remove(r.id, `${r.childName} ${r.weekKey}`)}
>
<Trash2 className="size-4" />
</button>
</div>
)}
</div>
))}
</div>
);
}