From 0e118b6eafd6ab9ae42dec969a84ed51ecc944f6 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 17 Sep 2026 12:37:58 +0800 Subject: [PATCH] Add study review planning and mock exam hub --- app/api/study/route.ts | 34 ++++++++++++++++++++++++++++++++++ app/checkin-client.tsx | 2 +- app/globals.css | 4 ++++ app/review/page.tsx | 2 ++ app/review/review-client.tsx | 28 ++++++++++++++++++++++++++++ db/schema.ts | 20 ++++++++++++++++++++ drizzle/0002_add_study_hub.sql | 20 ++++++++++++++++++++ 7 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 app/api/study/route.ts create mode 100644 app/review/page.tsx create mode 100644 app/review/review-client.tsx create mode 100644 drizzle/0002_add_study_hub.sql diff --git a/app/api/study/route.ts b/app/api/study/route.ts new file mode 100644 index 0000000..4e07510 --- /dev/null +++ b/app/api/study/route.ts @@ -0,0 +1,34 @@ +import { and, eq } from "drizzle-orm"; +import { getDb } from "../../../db"; +import { mockExams, studyDays } from "../../../db/schema"; + +const user = (r: Request) => r.headers.get("oai-authenticated-user-id"); +const validDate = (s?: string) => /^2026-(09|10|11|12)-\d{2}$/.test(s || ""); + +export async function GET(r: Request) { + const id = user(r); if (!id) return Response.json({ error: "请登录后再使用学习中心。" }, { status: 401 }); + try { const db = getDb(); return Response.json({ studyDays: await db.select().from(studyDays).where(eq(studyDays.userId, id)), mockExams: await db.select().from(mockExams).where(eq(mockExams.userId, id)) }); } + catch { return Response.json({ error: "暂时无法读取学习数据。" }, { status: 500 }); } +} + +export async function PUT(r: Request) { + const id = user(r); if (!id) return Response.json({ error: "请登录后再保存。" }, { status: 401 }); + const b = await r.json() as { type?: string; date?: string; durations?: Record; weeklyReview?: string; nextFocus?: string; listening?: string; reading?: string; writing?: string; translation?: string; note?: string }; + if (!validDate(b.date)) return Response.json({ error: "日期格式不正确。" }, { status: 400 }); + try { + const db = getDb(), now = new Date().toISOString(); + if (b.type === "day") { + const durations = Object.fromEntries(Object.entries(b.durations || {}).map(([k, v]) => [k, Math.max(0, Math.min(600, Number(v) || 0))])); + const values = { userId: id, planDate: b.date!, durations: JSON.stringify(durations), weeklyReview: (b.weeklyReview || "").slice(0, 1500), nextFocus: (b.nextFocus || "").slice(0, 400), updatedAt: now }; + await db.insert(studyDays).values(values).onConflictDoUpdate({ target: [studyDays.userId, studyDays.planDate], set: values }); + return Response.json({ studyDay: (await db.select().from(studyDays).where(and(eq(studyDays.userId, id), eq(studyDays.planDate, b.date!))))[0] }); + } + if (b.type === "mock") { + const clean = (v?: string) => String(v || "").replace(/[^\d.]/g, "").slice(0, 6); + const values = { userId: id, examDate: b.date!, listening: clean(b.listening), reading: clean(b.reading), writing: clean(b.writing), translation: clean(b.translation), note: (b.note || "").slice(0, 1000), updatedAt: now }; + await db.insert(mockExams).values(values).onConflictDoUpdate({ target: [mockExams.userId, mockExams.examDate], set: values }); + return Response.json({ mockExam: (await db.select().from(mockExams).where(and(eq(mockExams.userId, id), eq(mockExams.examDate, b.date!))))[0] }); + } + return Response.json({ error: "请求类型不正确。" }, { status: 400 }); + } catch { return Response.json({ error: "暂时无法保存,请稍后重试。" }, { status: 500 }); } +} diff --git a/app/checkin-client.tsx b/app/checkin-client.tsx index e6d7fcf..4cfd07f 100644 --- a/app/checkin-client.tsx +++ b/app/checkin-client.tsx @@ -41,7 +41,7 @@ export default function CheckinClient() { const closeCountdown = () => { const today = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Shanghai" }).format(new Date()); localStorage.setItem("cet6-countdown-notice", today); setShowCountdown(false); }; return
{ if (!open) closeCountdown(); }}>

CET-6 · 考试倒计时

今天也离目标更近一步2026 年 12 月 12 日 15:00 笔试
{remaining.days}天{String(remaining.hours).padStart(2, "0")}时{String(remaining.minutes).padStart(2, "0")}分

今天的任务,完成一项就是一次推进。

-

CET-6 · 2026

大学英语六级 · 90天打卡

从今天的一件事开始,完成比完美重要。

任务总览数据统计

{saving ? "正在云端保存…" : status}

+

CET-6 · 2026

大学英语六级 · 90天打卡

从今天的一件事开始,完成比完美重要。

任务总览数据统计复盘与冲刺

{saving ? "正在云端保存…" : status}

} value={Math.round(finished / total * 100) + "%"} label="总体完成度" detail={finished + " / " + total + " 项任务"} />} value={complete + " 天"} label="完整完成" detail="目标共 90 天" />} value={day.phase} label="当前阶段" detail="12 月 12 日笔试" />

距离六级笔试

{remaining.days}天 {remaining.hours}时 {remaining.minutes}分2026 年 12 月 12 日 15:00

{day.phase} · {day.week}

{fmt(day.date)}

{isFuture ? "未来日期,暂不可打卡" : isMakeup ? "补打卡 · 已完成项目不可取消" : "今日打卡"} · 已完成 {done.length} / {day.tasks.length} 项
{isToday ? "正在处理今天的任务" : "正在查看 " + fmt(day.date)}{!isToday && }
{day.tasks.map(t => { const locked = isFuture || (isMakeup && done.includes(t.key)); return ; })}

今日状态

{["专注", "平稳", "有点累", "需要调整"].map(m => )}