体验修复:概览看当日具体任务+模板实时对账+打卡圈缩小+孩子我的记录

- 家长学习概览日历点击某天可查看当日具体任务及完成状态(1.1)
- 今日任务快照改为实时对账:模板删除则移除未完成快照、新增则补上,
  解决"模板无任务但孩子端仍显示"(1.2);历史日保持不变
- 孩子端打卡选中圈由 size-12 缩小到 size-9,更精致(2.1)
- 孩子端新增"我的记录":累计打卡天数 + 最近7天已完成任务(2.2)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-04 22:28:06 +08:00
co-authored by Claude Opus 4.8
parent 11c2997409
commit 053f818158
6 changed files with 158 additions and 45 deletions
+78 -34
View File
@@ -2,7 +2,7 @@
import { and, asc, eq, inArray } from "drizzle-orm";
import { db } from "@/db";
import { photos, taskInstances, taskTemplates } from "@/db/schema";
import { dowOf, mondayOf } from "@/lib/dates";
import { dowOf, mondayOf, todayStr, daysAgo } from "@/lib/dates";
import { viewUrl } from "@/lib/oss";
// 模板是否覆盖某一天(weekly/date/range/week 四种生效方式)
@@ -18,6 +18,83 @@ function coversDate(
return false;
}
// 生成/对账某天任务快照。历史日:只生成一次(保历史);今天:实时对账,
// 模板删了→移除未完成快照,模板加了→补上,保证孩子端与家长模板一致。
export async function ensureDay(childId: number, date: string) {
const isToday = date === todayStr();
await db.transaction(async (tx) => {
const all = await tx
.select()
.from(taskTemplates)
.where(eq(taskTemplates.childUserId, childId))
.orderBy(asc(taskTemplates.sortOrder));
const templates = all.filter((t) => coversDate(t, date));
const instances = await tx
.select()
.from(taskInstances)
.where(and(eq(taskInstances.childUserId, childId), eq(taskInstances.date, date)));
if (!isToday) {
if (instances.length > 0) return;
if (templates.length > 0)
await tx.insert(taskInstances).values(
templates.map((t) => ({
childUserId: childId,
date,
title: t.title,
subject: t.subject,
requiresPhoto: t.requiresPhoto,
sortOrder: t.sortOrder,
sourceTemplateId: t.id,
})),
);
return;
}
// 今天:对账
const tIds = new Set(templates.map((t) => t.id));
for (const inst of instances) {
if (
inst.status === "pending" &&
inst.sourceTemplateId != null &&
!tIds.has(inst.sourceTemplateId)
) {
await tx.delete(taskInstances).where(eq(taskInstances.id, inst.id));
}
}
const haveIds = new Set(instances.map((i) => i.sourceTemplateId));
for (const t of templates) {
if (!haveIds.has(t.id)) {
await tx.insert(taskInstances).values({
childUserId: childId,
date,
title: t.title,
subject: t.subject,
requiresPhoto: t.requiresPhoto,
sortOrder: t.sortOrder,
sourceTemplateId: t.id,
});
}
}
});
}
// 孩子打卡统计:累计打卡天数 + 最近 n 天已完成任务
export async function childCheckStats(childId: number, n = 7) {
const rows = await db
.select({ date: taskInstances.date, status: taskInstances.status, title: taskInstances.title })
.from(taskInstances)
.where(eq(taskInstances.childUserId, childId));
const doneDates = new Set(rows.filter((r) => r.status === "done").map((r) => r.date));
const recent: { date: string; tasks: string[] }[] = [];
for (let i = 0; i < n; i++) {
const d = daysAgo(i);
const done = rows.filter((r) => r.date === d && r.status === "done").map((r) => r.title);
if (done.length > 0) recent.push({ date: d, tasks: done });
}
return { totalDays: doneDates.size, recent };
}
export type TaskView = {
id: number;
title: string;
@@ -28,39 +105,6 @@ export type TaskView = {
photoUrls: string[];
};
// 确保某天的任务快照存在:第一次访问当天时由模板生成,之后读快照
export async function ensureDay(childId: number, date: string) {
await db.transaction(async (tx) => {
const existing = await tx
.select({ id: taskInstances.id })
.from(taskInstances)
.where(and(eq(taskInstances.childUserId, childId), eq(taskInstances.date, date)))
.limit(1);
if (existing.length > 0) return;
const all = await tx
.select()
.from(taskTemplates)
.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(
templates.map((t) => ({
childUserId: childId,
date,
title: t.title,
subject: t.subject,
requiresPhoto: t.requiresPhoto,
sortOrder: t.sortOrder,
sourceTemplateId: t.id,
})),
);
}
});
}
// 孩子端当日任务视图(含照片签名地址)
export async function dayTasks(childId: number, date: string): Promise<TaskView[]> {
await ensureDay(childId, date);