feat: upgrade student vocabulary learning flow

This commit is contained in:
Codex
2026-06-30 00:07:52 +08:00
parent e17b44274b
commit 3d10497ea8
10 changed files with 518 additions and 57 deletions

View File

@@ -161,6 +161,8 @@ export interface VocabularyWord {
difficulty?: string | null;
status?: string;
dueLevel?: string;
favoritedAt?: string | null;
note?: string | null;
}
export async function createPracticeSession(body: {
@@ -281,6 +283,12 @@ export async function toggleWordFavorite(wordId: string, favorite: boolean) {
});
}
export async function loadFavoriteWords(unitId?: string, limit = 500) {
return apiRequest<{ items?: VocabularyWord[] }>('/api/learning/vocabulary/favorites', {
query: { unitId, limit },
});
}
export async function loadVocabularyStats(unitId?: string) {
return apiRequest<{ item?: Record<string, unknown> }>('/api/learning/vocabulary/stats', { query: { unitId } });
}

View File

@@ -0,0 +1,97 @@
import Taro from '@tarojs/taro';
export type AccentType = 'us' | 'uk';
const YOUDAO_TYPE: Record<AccentType, number> = {
us: 1,
uk: 2,
};
let currentAudio: HTMLAudioElement | null = null;
let currentInnerAudio: Taro.InnerAudioContext | null = null;
function pronunciationUrl(word: string, accent: AccentType) {
return `https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(word)}&type=${YOUDAO_TYPE[accent] || 1}`;
}
function stopCurrentAudio() {
try {
if (currentAudio) {
currentAudio.pause();
currentAudio.src = '';
currentAudio = null;
}
} catch (_error) {
// ignore playback cleanup errors
}
try {
if (currentInnerAudio) {
currentInnerAudio.stop();
currentInnerAudio.destroy();
currentInnerAudio = null;
}
} catch (_error) {
// ignore playback cleanup errors
}
try {
if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
window.speechSynthesis.cancel();
}
} catch (_error) {
// ignore unsupported runtime
}
}
function fallbackSpeech(word: string, accent: AccentType) {
if (typeof window === 'undefined' || !('speechSynthesis' in window)) return false;
try {
const utterance = new SpeechSynthesisUtterance(word);
utterance.lang = accent === 'uk' ? 'en-GB' : 'en-US';
utterance.rate = 0.86;
window.speechSynthesis.speak(utterance);
return true;
} catch (_error) {
return false;
}
}
export async function playWordPronunciation(word: string, accent: AccentType = 'us') {
const text = word.trim();
if (!text) return;
stopCurrentAudio();
const url = pronunciationUrl(text, accent);
if (process.env.TARO_ENV === 'h5' && typeof Audio !== 'undefined') {
try {
const audio = new Audio(url);
audio.preload = 'auto';
currentAudio = audio;
audio.onerror = () => {
currentAudio = null;
fallbackSpeech(text, accent);
};
await audio.play();
return;
} catch (_error) {
if (fallbackSpeech(text, accent)) return;
}
}
try {
const audio = Taro.createInnerAudioContext();
currentInnerAudio = audio;
audio.src = url;
audio.autoplay = true;
audio.onEnded(() => {
if (currentInnerAudio === audio) currentInnerAudio = null;
audio.destroy();
});
audio.onError(() => {
if (currentInnerAudio === audio) currentInnerAudio = null;
audio.destroy();
});
} catch (_error) {
Taro.showToast({ title: '当前环境暂不支持发音', icon: 'none' });
}
}