forked from wangziqi/gongxue-base
feat: expand vocabulary study overview
This commit is contained in:
@@ -689,6 +689,23 @@
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.vocabulary-overview-panel {
|
||||
border-color: #bbf7d0;
|
||||
background: #f7fdf9;
|
||||
}
|
||||
|
||||
.vocabulary-stats-grid {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.vocabulary-plan-line {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.vocabulary-mastery-track {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.vocabulary-study {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -21,6 +21,12 @@ type SessionStats = {
|
||||
unknown: number;
|
||||
};
|
||||
|
||||
type ReviewPlanSummary = {
|
||||
dueCount: number;
|
||||
newCount: number;
|
||||
totalPlanned: number;
|
||||
};
|
||||
|
||||
function wordKey(word: VocabularyWord) {
|
||||
return word.wordId || word.id || word.word;
|
||||
}
|
||||
@@ -38,6 +44,11 @@ function statNumber(stats: Record<string, unknown> | null, key: string) {
|
||||
return typeof value === 'number' ? value : Number(value || 0);
|
||||
}
|
||||
|
||||
function percentText(value: number, total: number) {
|
||||
if (!total) return '0%';
|
||||
return `${Math.round((value / total) * 100)}%`;
|
||||
}
|
||||
|
||||
function modeLabel(mode: StudyMode) {
|
||||
if (mode === 'favorites') return '收藏练习';
|
||||
if (mode === 'unit') return '单元学习';
|
||||
@@ -51,6 +62,7 @@ export default function StudentVocabularyPage() {
|
||||
const [words, setWords] = useState<VocabularyWord[]>([]);
|
||||
const [favoriteIds, setFavoriteIds] = useState<Record<string, boolean>>({});
|
||||
const [stats, setStats] = useState<Record<string, unknown> | null>(null);
|
||||
const [planSummary, setPlanSummary] = useState<ReviewPlanSummary>({ dueCount: 0, newCount: 0, totalPlanned: 0 });
|
||||
const [index, setIndex] = useState(0);
|
||||
const [showAnswer, setShowAnswer] = useState(false);
|
||||
const [completed, setCompleted] = useState(false);
|
||||
@@ -80,7 +92,7 @@ export default function StudentVocabularyPage() {
|
||||
Promise.all([
|
||||
mode === 'favorites' ? loadFavoriteWords(unitId) : Promise.resolve({ items: [] as VocabularyWord[] }),
|
||||
mode !== 'favorites' ? loadVocabularyWords(unitId) : Promise.resolve({ items: [] as VocabularyWord[] }),
|
||||
mode === 'plan' ? loadVocabularyReviewPlan(unitId) : Promise.resolve({ item: { words: [] as VocabularyWord[] } }),
|
||||
loadVocabularyReviewPlan(unitId).catch(() => ({ item: { words: [] as VocabularyWord[], dueCount: 0, newCount: 0, totalPlanned: 0 } })),
|
||||
loadFavoriteWords(unitId).catch(() => ({ items: [] as VocabularyWord[] })),
|
||||
loadVocabularyStats(unitId),
|
||||
])
|
||||
@@ -93,6 +105,11 @@ export default function StudentVocabularyPage() {
|
||||
: planned.length
|
||||
? planned
|
||||
: wordPayload.items || [];
|
||||
setPlanSummary({
|
||||
dueCount: Number(planPayload.item?.dueCount || 0),
|
||||
newCount: Number(planPayload.item?.newCount || 0),
|
||||
totalPlanned: Number(planPayload.item?.totalPlanned || planned.length || 0),
|
||||
});
|
||||
const savedIndex = getStorage<number>(progressStorageKey(unitId, mode));
|
||||
setWords(nextWords);
|
||||
setFavoriteIds(Object.fromEntries((favoritesForStatus.items || []).map(item => wordRecordId(item)).filter(Boolean).map(id => [id, true])));
|
||||
@@ -111,6 +128,15 @@ export default function StudentVocabularyPage() {
|
||||
const current = words[index] || null;
|
||||
const progressPercent = words.length ? Math.round(((Math.min(index + 1, words.length)) / words.length) * 100) : 0;
|
||||
const favoriteCurrent = current ? !!favoriteIds[wordRecordId(current)] : false;
|
||||
const totalWords = statNumber(stats, 'totalWords');
|
||||
const progressedWords = statNumber(stats, 'progressedWords');
|
||||
const masteredWords = statNumber(stats, 'masteredWords');
|
||||
const learningWords = statNumber(stats, 'learningWords');
|
||||
const todayReviewed = statNumber(stats, 'todayReviewed');
|
||||
const favoriteWords = statNumber(stats, 'favoriteWords');
|
||||
const unstartedWords = Math.max(0, totalWords - progressedWords);
|
||||
const sessionTotal = sessionStats.known + sessionStats.unknown;
|
||||
const masteryBucket = Math.max(0, Math.min(10, Math.round((totalWords ? masteredWords / totalWords : 0) * 10)));
|
||||
|
||||
const unitName = useMemo(() => units.find(item => item.id === unitId)?.name || '单词单元', [units, unitId]);
|
||||
|
||||
@@ -151,11 +177,15 @@ export default function StudentVocabularyPage() {
|
||||
unknown: prev.unknown + (result === 'unknown' ? 1 : 0),
|
||||
}));
|
||||
if (mode === 'plan') {
|
||||
setWords(prev => prev.filter(item => wordKey(item) !== id));
|
||||
setIndex(prev => Math.min(prev, Math.max(0, words.length - 2)));
|
||||
const nextWords = words.filter(item => wordKey(item) !== id);
|
||||
setWords(nextWords);
|
||||
setCompleted(!nextWords.length);
|
||||
setIndex(prev => Math.min(prev, Math.max(0, nextWords.length - 1)));
|
||||
setShowAnswer(false);
|
||||
} else {
|
||||
nextWord();
|
||||
}
|
||||
void refreshStats();
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '复习提交失败');
|
||||
}
|
||||
@@ -176,6 +206,7 @@ export default function StudentVocabularyPage() {
|
||||
setWords(prev => prev.filter(item => wordKey(item) !== id));
|
||||
setIndex(prev => Math.min(prev, Math.max(0, words.length - 2)));
|
||||
}
|
||||
void refreshStats();
|
||||
Taro.showToast({ title: payload.favorite ? '已收藏' : '已取消收藏', icon: 'success' });
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '收藏操作失败');
|
||||
@@ -188,6 +219,16 @@ export default function StudentVocabularyPage() {
|
||||
setCompleted(false);
|
||||
}
|
||||
|
||||
async function refreshStats() {
|
||||
if (!unitId) return;
|
||||
try {
|
||||
const payload = await loadVocabularyStats(unitId);
|
||||
setStats(payload.item || null);
|
||||
} catch {
|
||||
// Keep the current snapshot; the next page reload or mode switch will reconcile stats.
|
||||
}
|
||||
}
|
||||
|
||||
async function playCurrent() {
|
||||
if (!current?.word) return;
|
||||
await playWordPronunciation(current.word, accent);
|
||||
@@ -218,9 +259,31 @@ export default function StudentVocabularyPage() {
|
||||
<Button className='secondary-button' onClick={() => resetPosition()}>重新开始</Button>
|
||||
</View>
|
||||
|
||||
<View className='grid-two section-block'>
|
||||
<View className='metric'><Text className='metric-value'>{String(statNumber(stats, 'masteredWords'))}</Text><Text className='metric-label'>已掌握</Text></View>
|
||||
<View className='metric'><Text className='metric-value'>{String(statNumber(stats, 'todayReviewed'))}</Text><Text className='metric-label'>今日复习</Text></View>
|
||||
<View className='section-block'>
|
||||
<Text className='section-heading'>学习概览</Text>
|
||||
<View className='report-panel vocabulary-overview-panel'>
|
||||
<View className='split-row'>
|
||||
<View>
|
||||
<Text className='row-main'>{unitName}</Text>
|
||||
<Text className='row-meta'>掌握率 {percentText(masteredWords, totalWords)} · 已开始 {percentText(progressedWords, totalWords)}</Text>
|
||||
</View>
|
||||
<Text className='status-badge'>{modeLabel(mode)}</Text>
|
||||
</View>
|
||||
<View className='progress-track vocabulary-mastery-track'>
|
||||
<View className={`progress-fill w-bucket-${masteryBucket}`} />
|
||||
</View>
|
||||
<View className='grid-two vocabulary-stats-grid'>
|
||||
<View className='metric compact-metric'><Text className='metric-value'>{String(totalWords)}</Text><Text className='metric-label'>总词数</Text></View>
|
||||
<View className='metric compact-metric'><Text className='metric-value'>{String(masteredWords)}</Text><Text className='metric-label'>已掌握</Text></View>
|
||||
<View className='metric compact-metric'><Text className='metric-value'>{String(learningWords)}</Text><Text className='metric-label'>学习中</Text></View>
|
||||
<View className='metric compact-metric'><Text className='metric-value'>{String(unstartedWords)}</Text><Text className='metric-label'>未开始</Text></View>
|
||||
<View className='metric compact-metric'><Text className='metric-value'>{String(todayReviewed)}</Text><Text className='metric-label'>今日复习</Text></View>
|
||||
<View className='metric compact-metric'><Text className='metric-value'>{String(favoriteWords)}</Text><Text className='metric-label'>已收藏</Text></View>
|
||||
</View>
|
||||
<Text className='row-meta vocabulary-plan-line'>
|
||||
今日计划 {String(planSummary.totalPlanned)} 个 · 待复习 {String(planSummary.dueCount)} 个 · 新词 {String(planSummary.newCount)} 个
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{loading ? <View className='empty-state section-block'>单词加载中...</View> : null}
|
||||
@@ -267,6 +330,8 @@ export default function StudentVocabularyPage() {
|
||||
<View className='grid-two section-block'>
|
||||
<View className='metric'><Text className='metric-value'>{String(sessionStats.known)}</Text><Text className='metric-label'>本次认识</Text></View>
|
||||
<View className='metric'><Text className='metric-value'>{String(sessionStats.unknown)}</Text><Text className='metric-label'>本次再记</Text></View>
|
||||
<View className='metric'><Text className='metric-value'>{String(sessionTotal)}</Text><Text className='metric-label'>本次复习</Text></View>
|
||||
<View className='metric'><Text className='metric-value'>{percentText(sessionStats.known, sessionTotal)}</Text><Text className='metric-label'>本次认识率</Text></View>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user