diff --git a/drizzle/0007_word-entry.sql b/drizzle/0007_word-entry.sql new file mode 100644 index 0000000..e2eb91d --- /dev/null +++ b/drizzle/0007_word-entry.sql @@ -0,0 +1,12 @@ +-- xuchao: 单词词库落库,供家长端按天编辑。幂等:已存在则跳过。 +-- 初始数据由 scripts/seed-words.ts 从 src/content/words/words.json 灌入; +-- 表空时 lib/words.ts 自动回退读 JSON,部署顺序不影响孩子端使用。 +CREATE TABLE IF NOT EXISTS `word_entry` ( + `id` bigint AUTO_INCREMENT NOT NULL, + `word` varchar(60) NOT NULL, + `meaning` varchar(300) NOT NULL DEFAULT '', + `day` int NOT NULL, + `seq` int NOT NULL, + CONSTRAINT `word_entry_id` PRIMARY KEY(`id`), + KEY `idx_word_entry_day` (`day`) +); diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index efa47ec..f5d4a87 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1786064400000, "tag": "0006_fill-missing-tables", "breakpoints": true + }, + { + "idx": 7, + "version": "5", + "when": 1786147200000, + "tag": "0007_word-entry", + "breakpoints": true } ] } \ No newline at end of file diff --git a/scripts/seed-words.ts b/scripts/seed-words.ts new file mode 100644 index 0000000..cb4d112 --- /dev/null +++ b/scripts/seed-words.ts @@ -0,0 +1,33 @@ +// xuchao: 单词词库种子——words.json 灌入 word_entry;仅表空时全量灌入,已有数据视为家长已编辑,跳过 +import { sql } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/mysql2"; +import mysql from "mysql2/promise"; +import raw from "../src/content/words/words.json"; +import { wordEntry } from "../src/db/schema"; + +async function main() { + const conn = await mysql.createConnection(process.env.DATABASE_URL!); + const db = drizzle(conn); + + const [cnt] = await db.select({ n: sql`COUNT(*)` }).from(wordEntry); + if (cnt.n > 0) { + console.log("word_entry 已有", cnt.n, "条,跳过灌入(视为已编辑数据)"); + await conn.end(); + return; + } + + const rows = (raw as { word: string; meaning: string; day: number; seq: number }[]).map((w) => ({ + word: w.word, + meaning: w.meaning, + day: w.day, + seq: w.seq, + })); + // 分批插入,避免单条 SQL 过大 + for (let i = 0; i < rows.length; i += 500) { + await db.insert(wordEntry).values(rows.slice(i, i + 500)); + } + console.log("word_entry 灌入完成:", rows.length, "条"); + await conn.end(); +} + +main(); diff --git a/src/app/parent/dashboard.tsx b/src/app/parent/dashboard.tsx index e43a19b..7a39559 100644 --- a/src/app/parent/dashboard.tsx +++ b/src/app/parent/dashboard.tsx @@ -113,7 +113,7 @@ export function ParentDashboard({ -
+
@@ -121,6 +121,18 @@ export function ParentDashboard({

改标题/目标/课文,保存即生效

+ +
+

📖 编辑英语单词

+

按天增删改单词与释义,保存即生效

+
+ + +
+

🧮 编辑数学思维课程

+

改标题/目标/课文,保存即生效

+
+

📝 每周检测成绩

diff --git a/src/app/parent/math/[pointId]/page.tsx b/src/app/parent/math/[pointId]/page.tsx new file mode 100644 index 0000000..a4e7a30 --- /dev/null +++ b/src/app/parent/math/[pointId]/page.tsx @@ -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 ( +
+ +
+ + ← 课程列表 + +
+ {point.moduleName} +

{point.title}

+
+
+ +
+ +
+
+ ); +} diff --git a/src/app/parent/math/actions.ts b/src/app/parent/math/actions.ts new file mode 100644 index 0000000..9698280 --- /dev/null +++ b/src/app/parent/math/actions.ts @@ -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 }; +} diff --git a/src/app/parent/math/editor-form.tsx b/src/app/parent/math/editor-form.tsx new file mode 100644 index 0000000..7725180 --- /dev/null +++ b/src/app/parent/math/editor-form.tsx @@ -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 ( +
+
+
+ + setTitle(e.target.value)} /> +
+
+ + setSummary(e.target.value)} /> +
+
+ +
+
+ +
+ 检测到练习题 {quizCount} 道 + +
+
+ {preview ? ( +
+ +
+ ) : ( +