From b8db3ae7079fe960bc05e33016202a127ac82b44 Mon Sep 17 00:00:00 2001 From: jacobxu666 Date: Wed, 5 Aug 2026 20:53:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=A6=E4=B9=A0=E6=A6=82=E8=A7=88=E6=97=A5?= =?UTF-8?q?=E5=8E=86=E5=8F=AF=E6=9F=A5=E7=9C=8B=E5=BD=93=E6=97=A5=E8=AF=AD?= =?UTF-8?q?=E6=B3=95=E6=A3=80=E6=B5=8B+=E8=B7=9F=E8=AF=BB=E5=BD=95?= =?UTF-8?q?=E9=9F=B3;=E4=BF=AE=E5=A4=8D=E8=B7=9F=E8=AF=BB=E5=BD=95?= =?UTF-8?q?=E9=9F=B3=E7=AD=BE=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 学习概览选中某天展示:打卡任务+英语语法检测(分数)+单词跟读录音(回放) - 修复跟读录音无法听:Safari 录音为 audio/mp4,原写死 audio/webm 致签名 不匹配上传失败;改为按实际录音类型签名,扩展名 m4a/webm Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/parent/dashboard.tsx | 2 + src/app/parent/overview-tab.tsx | 81 ++++++++++++++++++++++++--------- src/app/parent/page.tsx | 22 ++++++++- src/app/words/actions.ts | 9 ++-- src/app/words/word-day.tsx | 5 +- 5 files changed, 91 insertions(+), 28 deletions(-) diff --git a/src/app/parent/dashboard.tsx b/src/app/parent/dashboard.tsx index 47e080e..e496c58 100644 --- a/src/app/parent/dashboard.tsx +++ b/src/app/parent/dashboard.tsx @@ -16,6 +16,8 @@ export type DayCell = { grammar: number; weekly: number | null; tasks: { title: string; status: "pending" | "done"; subject: string }[]; + recs: { word: string; url: string }[]; + gtests: { title: string; score: number; passed: boolean }[]; }; export type OverviewData = { kids: { diff --git a/src/app/parent/overview-tab.tsx b/src/app/parent/overview-tab.tsx index a6ced3f..b14d925 100644 --- a/src/app/parent/overview-tab.tsx +++ b/src/app/parent/overview-tab.tsx @@ -100,33 +100,72 @@ export function OverviewTab({ data }: { data: OverviewData }) { 周检测

-
+

- {selected} {selected === today ? "(今天)" : ""} 的具体任务 + {selected} {selected === today ? "(今天)" : ""} 的学习详情

{(() => { const cell = kid.days[selected]; - if (!cell || cell.tasks.length === 0) - return

这一天没有打卡任务。

; + if (!cell) return

这一天暂无记录。

; return ( -
- {cell.tasks.map((t, i) => { - const sm = subjectMeta(t.subject); - return ( -
- - {t.title} - - {t.status === "done" ? "已完成" : "未完成"} - +
+
+

打卡任务

+ {cell.tasks.length === 0 ? ( +

无打卡任务

+ ) : ( +
+ {cell.tasks.map((t, i) => { + const sm = subjectMeta(t.subject); + return ( +
+ + {t.title} + + {t.status === "done" ? "已完成" : "未完成"} + +
+ ); + })}
- ); - })} + )} +
+ +
+

英语语法检测

+ {cell.gtests.length === 0 ? ( +

当天未做语法检测

+ ) : ( +
+ {cell.gtests.map((g, i) => ( +

+ {g.title} · {g.score} 分 {g.passed ? "✅" : "❌"} +

+ ))} +
+ )} +
+ +
+

单词跟读录音

+ {cell.recs.length === 0 ? ( +

当天无跟读录音

+ ) : ( +
+ {cell.recs.map((r, i) => ( +
+ {r.word} +
+ ))} +
+ )} +
); })()} diff --git a/src/app/parent/page.tsx b/src/app/parent/page.tsx index f316589..d0327b7 100644 --- a/src/app/parent/page.tsx +++ b/src/app/parent/page.tsx @@ -50,7 +50,7 @@ export default async function ParentPage() { // ===== 学习概览(日历) ===== const overview: OverviewData = { kids: [] }; for (const kid of kids) { - const [instRows, gRows, wRows, wrongRow, gDone] = await Promise.all([ + const [instRows, gRows, wRows, wrongRow, gDone, gtestRows, recMonth] = await Promise.all([ db .select({ date: taskInstances.date, @@ -70,6 +70,20 @@ export default async function ParentPage() { .from(wrongBook) .where(and(eq(wrongBook.childUserId, kid.id), eq(wrongBook.mastered, false))), passedCount(kid.id), + db + .select({ + completedAt: grammarProgress.completedAt, + score: grammarProgress.score, + passed: grammarProgress.passed, + title: grammarPoints.title, + }) + .from(grammarProgress) + .innerJoin(grammarPoints, eq(grammarPoints.pointId, grammarProgress.pointId)) + .where(and(eq(grammarProgress.childUserId, kid.id), gte(grammarProgress.completedAt, new Date(`${mStart}T00:00:00`)))), + db + .select({ word: wordRecording.word, ossKey: wordRecording.ossKey, createdAt: wordRecording.createdAt }) + .from(wordRecording) + .where(and(eq(wordRecording.childUserId, kid.id), gte(wordRecording.createdAt, new Date(`${mStart}T00:00:00`)))), ]); const days: OverviewData["kids"][number]["days"] = {}; @@ -84,6 +98,12 @@ export default async function ParentPage() { grammar, weekly: w ? w.score : null, tasks: inst.map((r) => ({ title: r.title, status: r.status, subject: r.subject })), + recs: recMonth + .filter((r) => dstr(r.createdAt) === d) + .map((r) => ({ word: r.word, url: viewUrl(r.ossKey) })), + gtests: gtestRows + .filter((r) => r.completedAt && dstr(r.completedAt) === d) + .map((r) => ({ title: r.title, score: r.score ?? 0, passed: r.passed })), }; } overview.kids.push({ diff --git a/src/app/words/actions.ts b/src/app/words/actions.ts index cba1aed..fe84cec 100644 --- a/src/app/words/actions.ts +++ b/src/app/words/actions.ts @@ -9,10 +9,11 @@ import { buildWordTest } from "@/lib/words"; import { uploadUrl } from "@/lib/oss"; import { todayStr } from "@/lib/dates"; -// 跟读录音直传签名(浏览器直接 PUT 到对象存储) -export async function getRecUploadUrl(word: string): Promise<{ url: string; key: string }> { - const key = `recordings/${word}-${Date.now()}.webm`; - return { url: uploadUrl(key, "audio/webm"), key }; +// 跟读录音直传签名(按实际录音类型签名,避免签名不匹配) +export async function getRecUploadUrl(word: string, contentType: string): Promise<{ url: string; key: string }> { + const ext = contentType.includes("mp4") ? "m4a" : "webm"; + const key = `recordings/${word}-${Date.now()}.${ext}`; + return { url: uploadUrl(key, contentType), key }; } async function child() { diff --git a/src/app/words/word-day.tsx b/src/app/words/word-day.tsx index e102f93..b0a83cb 100644 --- a/src/app/words/word-day.tsx +++ b/src/app/words/word-day.tsx @@ -36,8 +36,9 @@ function RecordBtn({ day, word }: { day: number; word: string }) { setBusy(true); stream.getTracks().forEach((t) => t.stop()); const blob = new Blob(chunks, { type: mr.mimeType || "audio/webm" }); - const { url, key } = await getRecUploadUrl(word); - await fetch(url, { method: "PUT", headers: { "Content-Type": blob.type || "audio/webm" }, body: blob }); + const ct = blob.type || "audio/webm"; + const { url, key } = await getRecUploadUrl(word, ct); + await fetch(url, { method: "PUT", headers: { "Content-Type": ct }, body: blob }); await saveRecording(day, word, key); setBusy(false); setSaved(true);