学习闭环增强:错题本+每周能力检测+学习概览日历+模板日历+课程管理
- 错题本:所有检测错题自动入库,带详细讲解,可标记掌握,孩子端可查 - 每周能力检测:从已学知识点确定性组卷10题,计分记录,达标关联当日 英语打卡,错题进错题本,家长可见成绩 - 家长"今日概览"改"学习概览",月日历展示每日打卡/语法点亮/周检测 - 任务模板改日历展示,灵活添加:某一天/每周X/本周/日期区间批量,可删 - 家长中心新增"课程管理"栏目(语法课程编辑+周测成绩),设备管理弱化为 栏目内二级区块 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f98a21672d
commit
d84d827202
+26
-3
@@ -13,12 +13,35 @@ export function dowOf(dateStr: string): number {
|
||||
|
||||
// 今天往前推 n 天的日期字符串
|
||||
export function daysAgo(n: number): string {
|
||||
const now = new Date();
|
||||
// 先定位上海时区的今天零点,再回退 n 天
|
||||
const today = new Date(
|
||||
new Date().toLocaleDateString("en-US", { timeZone: "Asia/Shanghai" }),
|
||||
);
|
||||
today.setDate(today.getDate() - n);
|
||||
void now;
|
||||
return today.toLocaleDateString("en-CA");
|
||||
}
|
||||
|
||||
function parse(dateStr: string): Date {
|
||||
const [y, m, d] = dateStr.split("-").map(Number);
|
||||
return new Date(Date.UTC(y, m - 1, d));
|
||||
}
|
||||
const fmt = (d: Date) => d.toISOString().slice(0, 10);
|
||||
|
||||
// 日期加减 n 天
|
||||
export function addDays(dateStr: string, n: number): string {
|
||||
const d = parse(dateStr);
|
||||
d.setUTCDate(d.getUTCDate() + n);
|
||||
return fmt(d);
|
||||
}
|
||||
|
||||
// 该日期所在周的周一(用于"某一周"与周键)
|
||||
export function mondayOf(dateStr: string): string {
|
||||
const dow = dowOf(dateStr); // 0=周日..6=周六
|
||||
const offset = (dow + 6) % 7; // 距周一的天数
|
||||
return addDays(dateStr, -offset);
|
||||
}
|
||||
|
||||
// 周键:用该周周一的日期字符串,唯一且直观
|
||||
export function weekKey(dateStr: string): string {
|
||||
return mondayOf(dateStr);
|
||||
}
|
||||
|
||||
|
||||
+17
-3
@@ -2,9 +2,22 @@
|
||||
import { and, asc, eq, inArray } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { photos, taskInstances, taskTemplates } from "@/db/schema";
|
||||
import { dowOf } from "@/lib/dates";
|
||||
import { dowOf, mondayOf } from "@/lib/dates";
|
||||
import { viewUrl } from "@/lib/oss";
|
||||
|
||||
// 模板是否覆盖某一天(weekly/date/range/week 四种生效方式)
|
||||
function coversDate(
|
||||
t: { mode: string; dow: number; dateStart: string | null; dateEnd: string | null },
|
||||
date: string,
|
||||
): boolean {
|
||||
if (t.mode === "weekly") return t.dow === dowOf(date);
|
||||
if (t.mode === "date") return t.dateStart === date;
|
||||
if (t.mode === "range")
|
||||
return !!t.dateStart && !!t.dateEnd && date >= t.dateStart && date <= t.dateEnd;
|
||||
if (t.mode === "week") return !!t.dateStart && mondayOf(t.dateStart) === mondayOf(date);
|
||||
return false;
|
||||
}
|
||||
|
||||
export type TaskView = {
|
||||
id: number;
|
||||
title: string;
|
||||
@@ -25,11 +38,12 @@ export async function ensureDay(childId: number, date: string) {
|
||||
.limit(1);
|
||||
if (existing.length > 0) return;
|
||||
|
||||
const templates = await tx
|
||||
const all = await tx
|
||||
.select()
|
||||
.from(taskTemplates)
|
||||
.where(and(eq(taskTemplates.childUserId, childId), eq(taskTemplates.dow, dowOf(date))))
|
||||
.where(eq(taskTemplates.childUserId, childId))
|
||||
.orderBy(asc(taskTemplates.sortOrder));
|
||||
const templates = all.filter((t) => coversDate(t, date));
|
||||
|
||||
if (templates.length > 0) {
|
||||
await tx.insert(taskInstances).values(
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { grammarPoints, grammarProgress, weeklyTest } from "@/db/schema";
|
||||
import { extractQuiz, type QuizQ } from "@/lib/quiz-parse";
|
||||
import { todayStr, weekKey } from "@/lib/dates";
|
||||
|
||||
export const currentWeekKey = () => weekKey(todayStr());
|
||||
|
||||
// 确定性随机:同 (seed) 每次得到相同组卷,保证渲染与判分一致
|
||||
function hashStr(s: string): number {
|
||||
let h = 5381;
|
||||
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
|
||||
return h >>> 0;
|
||||
}
|
||||
function mulberry32(a: number) {
|
||||
return () => {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
function seededSample<T>(arr: T[], seed: string, n: number): T[] {
|
||||
const rnd = mulberry32(hashStr(seed));
|
||||
const pool = [...arr];
|
||||
for (let i = pool.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rnd() * (i + 1));
|
||||
[pool[i], pool[j]] = [pool[j], pool[i]];
|
||||
}
|
||||
return pool.slice(0, n);
|
||||
}
|
||||
|
||||
// 从已点亮知识点组卷(不足则补入门题),每周固定 10 题
|
||||
export async function buildWeeklyQuestions(childId: number, wk: string): Promise<QuizQ[]> {
|
||||
const lit = await db
|
||||
.select({ pointId: grammarProgress.pointId })
|
||||
.from(grammarProgress)
|
||||
.where(and(eq(grammarProgress.childUserId, childId), eq(grammarProgress.passed, true)));
|
||||
const litIds = new Set(lit.map((r) => r.pointId));
|
||||
|
||||
const all = await db.select().from(grammarPoints);
|
||||
let pool: QuizQ[] = [];
|
||||
for (const p of all) {
|
||||
if (litIds.has(p.pointId)) pool.push(...extractQuiz(p.md));
|
||||
}
|
||||
if (pool.length < 5) {
|
||||
for (const p of all) if (p.level === "starter") pool.push(...extractQuiz(p.md));
|
||||
}
|
||||
return seededSample(pool, `${childId}:${wk}`, 10);
|
||||
}
|
||||
|
||||
export async function getWeeklyRecord(childId: number, wk: string) {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(weeklyTest)
|
||||
.where(and(eq(weeklyTest.childUserId, childId), eq(weeklyTest.weekKey, wk)))
|
||||
.limit(1);
|
||||
return row ?? null;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { and, eq, desc } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { wrongBook } from "@/db/schema";
|
||||
import type { QuizQ } from "@/lib/quiz-parse";
|
||||
import type { AnswerValue } from "@/lib/grammar";
|
||||
|
||||
// 各类题型的"用户作答"与"标准答案"可读文本
|
||||
export function answerText(q: QuizQ, a: AnswerValue | null | undefined): string {
|
||||
if (a == null) return "未作答";
|
||||
if (q.type === "fill") return String(a);
|
||||
if (q.type === "order") return Array.isArray(a) ? a.join(" ") : String(a);
|
||||
if (q.type === "match") return Array.isArray(a) ? (a as string[]).join("、") : String(a);
|
||||
const opts = q.type === "judge" ? ["对", "错"] : q.options ?? [];
|
||||
return opts[Number(a)] ?? String(a);
|
||||
}
|
||||
export function correctText(q: QuizQ): string {
|
||||
if (q.type === "fill") return (q.answers ?? []).join(" / ");
|
||||
if (q.type === "order") return (q.answer as string[]).join(" ");
|
||||
if (q.type === "match") return (q.pairs ?? []).map((p) => `${p[0]}→${p[1]}`).join("、");
|
||||
const opts = q.type === "judge" ? ["对", "错"] : q.options ?? [];
|
||||
return opts[q.answer as number] ?? "";
|
||||
}
|
||||
|
||||
// 错题入库(同一孩子同一来源同一题干去重)
|
||||
export async function recordWrong(
|
||||
childId: number,
|
||||
sourceType: "lesson" | "weekly",
|
||||
sourceId: string,
|
||||
sourceTitle: string,
|
||||
q: QuizQ,
|
||||
user: AnswerValue | null | undefined,
|
||||
) {
|
||||
const rows = await db
|
||||
.select({ question: wrongBook.question })
|
||||
.from(wrongBook)
|
||||
.where(and(eq(wrongBook.childUserId, childId), eq(wrongBook.sourceId, sourceId)));
|
||||
const dup = rows.some((r) => {
|
||||
try {
|
||||
return (JSON.parse(r.question) as QuizQ).stem === q.stem;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (dup) return;
|
||||
await db.insert(wrongBook).values({
|
||||
childUserId: childId,
|
||||
sourceType,
|
||||
sourceId,
|
||||
sourceTitle,
|
||||
question: JSON.stringify(q),
|
||||
userAnswer: answerText(q, user),
|
||||
correctAnswer: correctText(q),
|
||||
});
|
||||
}
|
||||
|
||||
export async function listWrong(childId: number) {
|
||||
return db.select().from(wrongBook).where(eq(wrongBook.childUserId, childId)).orderBy(desc(wrongBook.createdAt));
|
||||
}
|
||||
|
||||
export async function setMastered(id: number, childId: number, mastered: boolean) {
|
||||
await db
|
||||
.update(wrongBook)
|
||||
.set({ mastered, updatedAt: new Date() })
|
||||
.where(and(eq(wrongBook.id, id), eq(wrongBook.childUserId, childId)));
|
||||
}
|
||||
Reference in New Issue
Block a user