- {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) => (
+
+ ))}
+
+ )}
+
);
})()}
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);