feat: 改進進度條為測驗數量追蹤,更準確反映學習進度

- 新增測驗進度狀態管理 (totalTests, completedTests)
- 實現智能測驗數量計算,基於CEFR情境判斷每詞卡測驗數
- 進度條改為基於測驗完成度而非詞卡完成度
- 新增詳細調試日誌,顯示測驗總數計算和分布
- 進度顯示格式:X/Y 測驗 + 詞卡位置 + 答題分數
- 更準確反映不同難度詞彙的實際學習工作量

範例:A1學習者3測驗 + 困難詞彙2測驗 + 適中詞彙3測驗 = 8測驗總數

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-26 11:00:45 +08:00
parent 6b71ef3b55
commit c21e9de8e5
1 changed files with 56 additions and 4 deletions

View File

@ -46,6 +46,10 @@ export default function LearnPage() {
const [showHint, setShowHint] = useState(false)
const [isFlipped, setIsFlipped] = useState(false)
// 測驗進度狀態
const [totalTests, setTotalTests] = useState(0) // 所有測驗總數
const [completedTests, setCompletedTests] = useState(0) // 已完成測驗數
// UI狀態
const [modalImage, setModalImage] = useState<string | null>(null)
const [showReportModal, setShowReportModal] = useState(false)
@ -121,6 +125,23 @@ export default function LearnPage() {
console.log('✅ 載入後端API數據成功:', cardsToUse.length, '張詞卡');
console.log('📋 詞卡列表:', cardsToUse.map(c => c.word));
// 計算所有測驗總數
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
let totalTestCount = 0;
cardsToUse.forEach(card => {
const wordCEFR = card.difficultyLevel || 'A2';
const testsForCard = calculateTestsForCard(userCEFR, wordCEFR);
totalTestCount += testsForCard;
});
console.log('📊 測驗總數計算:', totalTestCount, '個測驗');
console.log('📝 詞卡測驗分布:', cardsToUse.map(card => {
const wordCEFR = card.difficultyLevel || 'A2';
return `${card.word}: ${calculateTestsForCard(userCEFR, wordCEFR)}個測驗`;
}));
setTotalTests(totalTestCount);
setCompletedTests(0);
setDueCards(cardsToUse);
// 設置第一張卡片
@ -273,6 +294,23 @@ export default function LearnPage() {
return mapping[cefr] || 50;
}
// 計算每張詞卡的測驗數量
const calculateTestsForCard = (userCEFR: string, wordCEFR: string): number => {
const userLevel = getCEFRToLevel(userCEFR);
const wordLevel = getCEFRToLevel(wordCEFR);
const difficulty = wordLevel - userLevel;
if (userCEFR === 'A1') {
return 3; // A1學習者翻卡、選擇、聽力
} else if (difficulty < -10) {
return 2; // 簡單詞彙:填空、重組
} else if (difficulty >= -10 && difficulty <= 10) {
return 3; // 適中詞彙:填空、重組、口說
} else {
return 2; // 困難詞彙:翻卡、選擇
}
}
// 取得當前學習情境
const getCurrentContext = (userCEFR: string, wordCEFR: string): string => {
const userLevel = getCEFRToLevel(userCEFR);
@ -548,7 +586,7 @@ export default function LearnPage() {
await submitReviewResult(isCorrect, answer);
}
// 提交復習結果
// 提交復習結果並更新測驗進度
const submitReviewResult = async (isCorrect: boolean, userAnswer?: string, confidenceLevel?: number) => {
if (!currentCard) return;
@ -572,9 +610,18 @@ export default function LearnPage() {
} else {
console.log('復習結果提交失敗,繼續運行');
}
// 更新測驗進度(無論提交成功或失敗)
setCompletedTests(prev => {
const newCompleted = prev + 1;
console.log(`📈 測驗進度更新: ${newCompleted}/${totalTests} (${Math.round((newCompleted/totalTests)*100)}%)`);
return newCompleted;
});
} catch (error) {
console.error('提交復習結果失敗:', error);
// 不中斷流程,允許用戶繼續學習
// 即使出錯也更新進度,避免卡住
setCompletedTests(prev => prev + 1);
}
}
@ -652,6 +699,8 @@ export default function LearnPage() {
const handleRestart = async () => {
setScore({ correct: 0, total: 0 })
setCompletedTests(0)
setTotalTests(0)
setShowComplete(false)
setShowNoDueCards(false)
await loadDueCards(); // 重新載入到期詞卡
@ -742,7 +791,10 @@ export default function LearnPage() {
<span className="text-sm text-gray-600"></span>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600">
{currentCardIndex + 1} / {dueCards.length}
{completedTests} / {totalTests}
</span>
<span className="text-xs text-gray-500">
{currentCardIndex + 1}/{dueCards.length}
</span>
<div className="text-sm">
<span className="text-green-600 font-semibold">{score.correct}</span>
@ -759,7 +811,7 @@ export default function LearnPage() {
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-primary h-2 rounded-full transition-all"
style={{ width: `${((currentCardIndex + 1) / dueCards.length) * 100}%` }}
style={{ width: `${totalTests > 0 ? (completedTests / totalTests) * 100 : 0}%` }}
></div>
</div>
</div>