第一期:家庭自学打卡系统
- 家长手机号登录 + 孩子设备一次性激活(长期会话) - 周模板自动生成每日任务快照,支持拍照凭证直传对象存储 - 连击计算(空白日自动达成)、家长补卡带标记 - 家长端三标签:今日概览 / 任务模板 / 设备管理 - Docker 独立输出,经 Traefik 接入 jump.hn12301.net Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import crypto from "node:crypto";
|
||||
import { and, asc, desc, eq, inArray } from "drizzle-orm";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { db } from "@/db";
|
||||
import { sessions, taskInstances, taskTemplates, users } from "@/db/schema";
|
||||
import { SESSION_COOKIE, getSessionUser } from "@/lib/auth";
|
||||
import { daysAgo, todayStr } from "@/lib/dates";
|
||||
import { ensureDay } from "@/lib/tasks";
|
||||
import { streakOf } from "@/lib/streak";
|
||||
import { ParentDashboard, type DevicesData, type OverviewData, type TemplatesData } from "./dashboard";
|
||||
|
||||
const SHANGHAI = { timeZone: "Asia/Shanghai" } as const;
|
||||
const fmtDateTime = (d: Date) =>
|
||||
d.toLocaleString("zh-CN", { ...SHANGHAI, hour12: false }).replace(/\//g, "-");
|
||||
|
||||
export default async function ParentPage() {
|
||||
const parent = await getSessionUser();
|
||||
if (!parent) redirect("/login");
|
||||
if (parent.role !== "parent") redirect("/kid");
|
||||
|
||||
const kids = await db
|
||||
.select({ id: users.id, name: users.displayName })
|
||||
.from(users)
|
||||
.where(and(eq(users.familyId, parent.familyId), eq(users.role, "child")))
|
||||
.orderBy(asc(users.sortOrder));
|
||||
|
||||
const today = todayStr();
|
||||
const weekDates = Array.from({ length: 7 }, (_, i) => daysAgo(i)); // 今天在前
|
||||
|
||||
// ===== 今日概览 =====
|
||||
const overview: OverviewData = { kids: [] };
|
||||
for (const kid of kids) {
|
||||
await ensureDay(kid.id, today); // 家长先看时也要生成今天的快照
|
||||
const [streak, rows] = await Promise.all([
|
||||
streakOf(kid.id),
|
||||
db
|
||||
.select({
|
||||
id: taskInstances.id,
|
||||
date: taskInstances.date,
|
||||
title: taskInstances.title,
|
||||
status: taskInstances.status,
|
||||
isMakeup: taskInstances.isMakeup,
|
||||
sortOrder: taskInstances.sortOrder,
|
||||
})
|
||||
.from(taskInstances)
|
||||
.where(and(eq(taskInstances.childUserId, kid.id), inArray(taskInstances.date, weekDates)))
|
||||
.orderBy(desc(taskInstances.date), asc(taskInstances.sortOrder)),
|
||||
]);
|
||||
|
||||
const week = weekDates.map((date) => ({
|
||||
date,
|
||||
isToday: date === today,
|
||||
tasks: rows.filter((r) => r.date === date),
|
||||
}));
|
||||
const todayTasks = week.find((w) => w.isToday)?.tasks ?? [];
|
||||
|
||||
overview.kids.push({
|
||||
id: kid.id,
|
||||
name: kid.name,
|
||||
streak,
|
||||
today: {
|
||||
total: todayTasks.length,
|
||||
done: todayTasks.filter((t) => t.status === "done").length,
|
||||
},
|
||||
week,
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 周模板 =====
|
||||
const templateRows = kids.length
|
||||
? await db
|
||||
.select()
|
||||
.from(taskTemplates)
|
||||
.where(inArray(taskTemplates.childUserId, kids.map((k) => k.id)))
|
||||
.orderBy(asc(taskTemplates.childUserId), asc(taskTemplates.dow), asc(taskTemplates.sortOrder))
|
||||
: [];
|
||||
const templates: TemplatesData = {
|
||||
kids: kids.map((kid) => ({
|
||||
id: kid.id,
|
||||
name: kid.name,
|
||||
items: templateRows
|
||||
.filter((t) => t.childUserId === kid.id)
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
dow: t.dow,
|
||||
title: t.title,
|
||||
requiresPhoto: t.requiresPhoto,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
// ===== 设备会话 =====
|
||||
const token = (await cookies()).get(SESSION_COOKIE)?.value;
|
||||
const tokenHash = token ? crypto.createHash("sha256").update(token).digest("hex") : null;
|
||||
const sessionRows = await db
|
||||
.select({
|
||||
id: sessions.id,
|
||||
tokenHash: sessions.tokenHash,
|
||||
deviceLabel: sessions.deviceLabel,
|
||||
createdAt: sessions.createdAt,
|
||||
expiresAt: sessions.expiresAt,
|
||||
owner: users.displayName,
|
||||
role: users.role,
|
||||
})
|
||||
.from(sessions)
|
||||
.innerJoin(users, eq(users.id, sessions.userId))
|
||||
.where(eq(users.familyId, parent.familyId))
|
||||
.orderBy(desc(sessions.createdAt));
|
||||
const devices: DevicesData = {
|
||||
rows: sessionRows.map((s) => ({
|
||||
id: s.id,
|
||||
owner: s.role === "parent" ? `${s.owner}(家长)` : s.owner,
|
||||
deviceLabel: s.deviceLabel,
|
||||
createdAt: fmtDateTime(s.createdAt),
|
||||
expiresAt: fmtDateTime(s.expiresAt),
|
||||
isCurrent: s.tokenHash === tokenHash,
|
||||
})),
|
||||
};
|
||||
|
||||
return <ParentDashboard overview={overview} templates={templates} devices={devices} />;
|
||||
}
|
||||
Reference in New Issue
Block a user