新增英语单词打卡模块:3694词/247天,发音/详情/跟读录音/每日检测

- 数据源:解析 3500 词 PDF 得 3694 词,按每天 15 词分 247 天,覆盖全部单词
- 孩子端 /words 地图 + 当天学习页:点词标准发音(有道语音)、详情(释义/
  形近词/例句)、学会标记、跟读录音(MediaRecorder 直传 OSS)、每日检测
  (选释义,达标点亮并计入英语打卡)
- 家长端课程管理可回放孩子跟读录音
- 形近词运行时编辑距离计算;例句按需取词典 API

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-05 00:04:46 +08:00
co-authored by Claude Opus 4.8
parent 38a64b4ec1
commit 2bbf349e0c
10 changed files with 573 additions and 1 deletions
+55
View File
@@ -0,0 +1,55 @@
import raw from "@/content/words/words.json";
export type Word = { word: string; meaning: string; day: number; seq: number };
export const WORDS = raw as Word[];
export const TOTAL_WORD_DAYS = WORDS.reduce((m, w) => Math.max(m, w.day), 0);
// 某一天的单词
export function wordsByDay(day: number): Word[] {
return WORDS.filter((w) => w.day === day);
}
// 标准发音(有道公共语音,美式 type=2 / 英式 type=1)
export function audioUrl(word: string, type: 1 | 2 = 2): string {
return `https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(word)}&type=${type}`;
}
// 编辑距离(形近词计算)
function lev(a: string, b: string): number {
const m = a.length,
n = b.length;
if (Math.abs(m - n) > 2) return 99;
const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
for (let j = 0; j <= n; j++) dp[0][j] = j;
for (let i = 1; i <= m; i++)
for (let j = 1; j <= n; j++)
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
return dp[m][n];
}
// 每日检测组卷(确定性):选最多 10 词,选中文释义,干扰项用相邻词的释义
export type WordTestQ = { word: string; options: string[]; answer: number };
export function buildWordTest(day: number): WordTestQ[] {
const ws = wordsByDay(day);
const pick = ws.slice(0, 10);
return pick.map((w) => {
const distractors = [1, 2, 3]
.map((off) => WORDS[(w.seq - 1 + off * 37) % WORDS.length].meaning)
.filter((m) => m && m !== w.meaning)
.slice(0, 3);
const options = [w.meaning || w.word, ...distractors];
// 打乱(确定性:按 seq)
const rnd = (w.seq * 2654435761) % 4;
const rotated = [...options.slice(rnd), ...options.slice(0, rnd)];
return { word: w.word, options: rotated, answer: rotated.indexOf(w.meaning || w.word) };
});
}
export function similarWords(word: string, n = 3): Word[] {
return WORDS.filter((w) => w.word !== word)
.map((w) => ({ w, d: lev(word, w.word) }))
.sort((a, b) => a.d - b.d)
.slice(0, n)
.map((x) => x.w);
}