家长端考试记录管理页:/parent/exam-records 编辑分数达标+删除(卷已删兼容)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
abcdd5bed8
commit
1b5e95725e
@@ -163,6 +163,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>
|
||||||
|
<Link href="/parent/exam-records" className="block">
|
||||||
|
<div className="rounded-xl border border-slate-100 bg-white p-4 shadow-soft transition-transform duration-200 hover:-translate-y-0.5">
|
||||||
|
<p className="font-bold text-slate-900">📄 考试记录管理</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">改考试成绩/达标,删除误生成记录</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Check, Pencil, 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 { deleteExamRecord, updateExamRecord } from "../actions";
|
||||||
|
|
||||||
|
export type ExamRowItem = {
|
||||||
|
id: number;
|
||||||
|
childName: string;
|
||||||
|
paperTitle: string;
|
||||||
|
score: number;
|
||||||
|
passed: boolean;
|
||||||
|
takenAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// xuchao: 考试记录行内编辑(分数+达标开关)、删除;保存后刷新服务端列表
|
||||||
|
export function ExamList({ rows }: { rows: ExamRowItem[] }) {
|
||||||
|
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: ExamRowItem) => {
|
||||||
|
setEditingId(r.id);
|
||||||
|
setScore(String(r.score));
|
||||||
|
setPassed(r.passed);
|
||||||
|
setMsg("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (editingId == null) return;
|
||||||
|
const s = Number(score);
|
||||||
|
startTransition(async () => {
|
||||||
|
const r = await updateExamRecord(editingId, s, passed);
|
||||||
|
if (r) setMsg(r);
|
||||||
|
else {
|
||||||
|
setEditingId(null);
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const remove = (id: number, label: string) => {
|
||||||
|
if (!confirm(`确定删除「${label}」这条考试记录?孩子可重新考试。`)) return;
|
||||||
|
startTransition(() => {
|
||||||
|
deleteExamRecord(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.paperTitle}
|
||||||
|
</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="max-w-[45%] truncate rounded-full bg-slate-100 px-2 py-0.5 text-xs text-muted-foreground">
|
||||||
|
{r.paperTitle}
|
||||||
|
</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.paperTitle}`)}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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 { examPapers, examRecords, users } from "@/db/schema";
|
||||||
|
import { getSessionUser } from "@/lib/auth";
|
||||||
|
import { ExamList } from "./exam-list";
|
||||||
|
|
||||||
|
// xuchao: 考试成绩记录管理——编辑分数/达标、删除记录(卷已删时标题显示占位)
|
||||||
|
export default async function ExamRecordsPage() {
|
||||||
|
const parent = await getSessionUser();
|
||||||
|
if (!parent) redirect("/login");
|
||||||
|
if (parent.role !== "parent") redirect("/kid");
|
||||||
|
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
id: examRecords.id,
|
||||||
|
childName: users.displayName,
|
||||||
|
paperTitle: examPapers.title,
|
||||||
|
score: examRecords.score,
|
||||||
|
passed: examRecords.passed,
|
||||||
|
takenAt: examRecords.takenAt,
|
||||||
|
})
|
||||||
|
.from(examRecords)
|
||||||
|
.innerJoin(users, eq(users.id, examRecords.childUserId))
|
||||||
|
.leftJoin(examPapers, eq(examPapers.paperId, examRecords.paperId))
|
||||||
|
.where(eq(users.familyId, parent.familyId))
|
||||||
|
.orderBy(desc(examRecords.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>
|
||||||
|
<ExamList
|
||||||
|
rows={rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
childName: r.childName,
|
||||||
|
paperTitle: r.paperTitle ?? "已删试卷",
|
||||||
|
score: r.score ?? 0,
|
||||||
|
passed: r.passed,
|
||||||
|
takenAt: r.takenAt.toLocaleDateString("zh-CN"),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user