"use client"; import { useState, useTransition } from "react"; import Link from "next/link"; import { BookOpen, Check, Mic, Square, Volume2 } from "lucide-react"; import { BackgroundBlobs } from "@/components/background-blobs"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { getRecUploadUrl, markLearned, saveRecording, submitWordTest } from "./actions"; type Item = { word: string; meaning: string; similar: { word: string; meaning: string }[] }; type TestQ = { word: string; options: string[]; answer: number }; const speak = (word: string) => { const a = new Audio(`https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(word)}&type=2`); a.play(); }; // 跟读录音按钮:录音→直传 OSS→登记 function RecordBtn({ day, word }: { day: number; word: string }) { const [rec, setRec] = useState(null); const [busy, setBusy] = useState(false); const [saved, setSaved] = useState(false); const stop = () => { rec?.stop(); setRec(null); }; const start = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mr = new MediaRecorder(stream); const chunks: Blob[] = []; mr.ondataavailable = (e) => chunks.push(e.data); mr.onstop = async () => { setBusy(true); stream.getTracks().forEach((t) => t.stop()); const blob = new Blob(chunks, { type: mr.mimeType || "audio/webm" }); 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); }; mr.start(); setRec(mr); } catch { setBusy(false); } }; if (rec) return ( ); return ( ); } export function WordDayClient({ day, items, learned, test, passed, score, }: { day: number; items: Item[]; learned: string[]; test: TestQ[]; passed: boolean; score: number | null; }) { const [learnedSet, setLearnedSet] = useState(new Set(learned)); const [open, setOpen] = useState(null); const [example, setExample] = useState>({}); const [testOpen, setTestOpen] = useState(false); const [answers, setAnswers] = useState<(number | null)[]>(test.map(() => null)); const [result, setResult] = useState<{ score: number; passed: boolean } | null>(passed ? { score: score ?? 0, passed } : null); const [pending, startTransition] = useTransition(); const toggleLearn = (word: string) => { startTransition(async () => { await markLearned(day, word); setLearnedSet((s) => { const n = new Set(s); n.has(word) ? n.delete(word) : n.add(word); return n; }); }); }; const openDetail = async (word: string) => { setOpen(open === word ? null : word); if (open !== word && !example[word]) { try { const r = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`); const j = await r.json(); const def = j?.[0]?.meanings?.[0]?.definitions?.[0]; const ex = def?.example || j?.[0]?.meanings?.[0]?.definitions?.[1]?.example || def?.definition || ""; setExample((e) => ({ ...e, [word]: ex })); } catch { setExample((e) => ({ ...e, [word]: "" })); } } }; const submit = () => { startTransition(async () => { const r = await submitWordTest(day, answers.map((a) => a as number)); if (!("error" in r)) setResult({ score: r.score, passed: r.passed }); }); }; return (
← 单词地图

第 {day} 天 · {items.length} 词

已学 {learnedSet.size}/{items.length}
{items.map((it) => { const done = learnedSet.has(it.word); return (
{it.meaning}
{open === it.word && (

释义: {it.meaning || "见例句"}

例句: {example[it.word] === undefined ? "加载中…" : example[it.word] || "暂无例句"}

形近词: {it.similar.map((s) => `${s.word}(${s.meaning})`).join("、") || "无"}

)}
); })}

每日检测

{result && ( {result.score} 分 {result.passed ? "· 已点亮" : ""} )}
{!testOpen ? ( ) : (
{test.map((q, qi) => (

{qi + 1}. 「{q.word}」的意思是?

{q.options.map((opt, oi) => ( ))}
))}
)}
); }