课程管理新增单词与数学思维内容编辑:单词词库落库(word_entry+0007迁移,表空回退words.json),家长端按天增删改单词;数学思维编辑器对标语法编辑器;课程管理加入两张入口卡片

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-17 17:25:04 +08:00
co-authored by Claude Opus 4.8
parent ab84bec1b4
commit 5f207e4a57
17 changed files with 565 additions and 22 deletions
+42
View File
@@ -0,0 +1,42 @@
import Link from "next/link";
import { redirect } from "next/navigation";
import { BackgroundBlobs } from "@/components/background-blobs";
import { Badge } from "@/components/ui/badge";
import { getSessionUser } from "@/lib/auth";
import { getMathPoint } from "@/lib/math";
import { EditorForm } from "../editor-form";
// xuchao: 单个数学知识点编辑页
export default async function MathEditPage({ params }: { params: Promise<{ pointId: string }> }) {
const parent = await getSessionUser();
if (!parent) redirect("/login");
if (parent.role !== "parent") redirect("/kid");
const pointId = (await params).pointId;
const point = await getMathPoint(pointId);
if (!point) redirect("/parent/math");
return (
<main className="relative mx-auto max-w-3xl space-y-5 p-4 pb-10">
<BackgroundBlobs />
<header className="flex flex-wrap items-center justify-between gap-2">
<Link href="/parent/math" className="text-sm font-medium text-indigo-600">
← 课程列表
</Link>
<div className="flex items-center gap-2">
<Badge className="bg-indigo-100 text-indigo-700 hover:bg-indigo-100">{point.moduleName}</Badge>
<h1 className="text-lg font-extrabold text-slate-900">{point.title}</h1>
</div>
</header>
<div className="rounded-2xl border border-slate-100 bg-white p-5 shadow-soft">
<EditorForm
pointId={point.pointId}
initialTitle={point.title}
initialSummary={point.summary}
initialMd={point.md}
/>
</div>
</main>
);
}
+34
View File
@@ -0,0 +1,34 @@
"use server";
import { revalidatePath } from "next/cache";
import { eq } from "drizzle-orm";
import { db } from "@/db";
import { mathPoints } from "@/db/schema";
import { getSessionUser } from "@/lib/auth";
// xuchao: 家长编辑数学知识点(标题/摘要/课文 markdown)
export async function updateMathPoint(
pointId: string,
data: { title: string; summary: string; md: string },
): Promise<{ ok?: true; error?: string }> {
const parent = await getSessionUser();
if (!parent || parent.role !== "parent") return { error: "请先以家长身份登录" };
if (!data.title.trim()) return { error: "标题不能为空" };
if (!data.md.trim()) return { error: "课文内容不能为空" };
await db
.update(mathPoints)
.set({
title: data.title.trim(),
summary: data.summary,
md: data.md,
updatedAt: new Date(),
})
.where(eq(mathPoints.pointId, pointId));
revalidatePath("/math");
revalidatePath(`/math/${pointId}`);
revalidatePath("/parent/math");
revalidatePath(`/parent/math/${pointId}`);
return { ok: true };
}
+92
View File
@@ -0,0 +1,92 @@
"use client";
import { useState, useTransition } from "react";
import { Eye, EyeOff, Save } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Md } from "@/app/learn/md";
import { extractQuiz } from "@/lib/quiz-parse";
import { updateMathPoint } from "./actions";
// xuchao: 数学课程编辑器——结构同语法编辑器:改标题/摘要/课文 markdown,可预览,保存即生效
export function EditorForm({
pointId,
initialTitle,
initialSummary,
initialMd,
}: {
pointId: string;
initialTitle: string;
initialSummary: string | null;
initialMd: string;
}) {
const [title, setTitle] = useState(initialTitle);
const [summary, setSummary] = useState(initialSummary ?? "");
const [md, setMd] = useState(initialMd);
const [preview, setPreview] = useState(false);
const [msg, setMsg] = useState<{ ok?: boolean; text: string } | null>(null);
const [pending, startTransition] = useTransition();
const quizCount = extractQuiz(md).length;
const save = () => {
setMsg(null);
startTransition(async () => {
const r = await updateMathPoint(pointId, { title, summary, md });
if (r.error) setMsg({ text: r.error });
else setMsg({ ok: true, text: "已保存,孩子端立即生效" });
});
};
return (
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1">
<Label>标题</Label>
<Input value={title} onChange={(e) => setTitle(e.target.value)} />
</div>
<div className="space-y-1">
<Label>学习目标(一句话)</Label>
<Input value={summary} onChange={(e) => setSummary(e.target.value)} />
</div>
</div>
<div className="space-y-1">
<div className="flex items-center justify-between">
<Label>课文内容(Markdown,含 ```quiz 练习块)</Label>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>检测到练习题 {quizCount} 道</span>
<Button variant="outline" size="sm" type="button" onClick={() => setPreview((p) => !p)}>
{preview ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
{preview ? "编辑" : "预览"}
</Button>
</div>
</div>
{preview ? (
<div className="max-h-[480px] overflow-y-auto rounded-xl border border-slate-100 bg-white p-4">
<Md text={md} />
</div>
) : (
<textarea
value={md}
onChange={(e) => setMd(e.target.value)}
rows={18}
className="w-full rounded-lg border border-slate-200 bg-white p-3 font-mono text-xs leading-relaxed focus:border-indigo-500 focus:ring-2 focus:ring-indigo-200"
/>
)}
</div>
<div className="flex items-center gap-3">
<Button size="lg" disabled={pending} onClick={save}>
<Save className="size-4" /> 保存
</Button>
{msg && (
<p className={msg.ok ? "text-sm font-medium text-emerald-600" : "text-sm font-medium text-destructive"}>
{msg.text}
</p>
)}
</div>
</div>
);
}
+55
View File
@@ -0,0 +1,55 @@
import Link from "next/link";
import { Pencil } from "lucide-react";
import { redirect } from "next/navigation";
import { BackgroundBlobs } from "@/components/background-blobs";
import { getSessionUser } from "@/lib/auth";
import { listMathPoints, MODULES } from "@/lib/math";
// xuchao: 数学思维课程编辑列表——按七大模块分组,点击进入单课编辑
export default async function MathAdmin() {
const parent = await getSessionUser();
if (!parent) redirect("/login");
if (parent.role !== "parent") redirect("/kid");
const points = await listMathPoints();
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>
</header>
<p className="rounded-lg border-l-4 border-indigo-400 bg-indigo-50 px-3 py-2 text-sm text-indigo-800">
点击任意课程即可修改标题、目标与课文内容,保存后孩子端立即生效。
</p>
{MODULES.map((mod) => {
const group = points.filter((p) => p.module === mod.id);
if (group.length === 0) return null;
return (
<section key={mod.id} className="rounded-xl border border-slate-100 bg-white p-4 shadow-soft">
<h2 className="mb-3 text-lg font-extrabold text-slate-900">
{mod.name}
<span className="ml-1 text-amber-500">{"★".repeat(mod.stars)}</span>
</h2>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{group.map((p) => (
<Link
key={p.pointId}
href={`/parent/math/${p.pointId}`}
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm text-slate-700 transition-all duration-200 hover:-translate-y-0.5 hover:border-indigo-300"
>
<Pencil className="size-3.5 shrink-0 text-indigo-400" />
<span className="truncate">{p.title}</span>
</Link>
))}
</div>
</section>
);
})}
</main>
);
}