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:
parent
6b71ef3b55
commit
c21e9de8e5
|
|
@ -46,6 +46,10 @@ export default function LearnPage() {
|
||||||
const [showHint, setShowHint] = useState(false)
|
const [showHint, setShowHint] = useState(false)
|
||||||
const [isFlipped, setIsFlipped] = useState(false)
|
const [isFlipped, setIsFlipped] = useState(false)
|
||||||
|
|
||||||
|
// 測驗進度狀態
|
||||||
|
const [totalTests, setTotalTests] = useState(0) // 所有測驗總數
|
||||||
|
const [completedTests, setCompletedTests] = useState(0) // 已完成測驗數
|
||||||
|
|
||||||
// UI狀態
|
// UI狀態
|
||||||
const [modalImage, setModalImage] = useState<string | null>(null)
|
const [modalImage, setModalImage] = useState<string | null>(null)
|
||||||
const [showReportModal, setShowReportModal] = useState(false)
|
const [showReportModal, setShowReportModal] = useState(false)
|
||||||
|
|
@ -121,6 +125,23 @@ export default function LearnPage() {
|
||||||
console.log('✅ 載入後端API數據成功:', cardsToUse.length, '張詞卡');
|
console.log('✅ 載入後端API數據成功:', cardsToUse.length, '張詞卡');
|
||||||
console.log('📋 詞卡列表:', cardsToUse.map(c => c.word));
|
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);
|
setDueCards(cardsToUse);
|
||||||
|
|
||||||
// 設置第一張卡片
|
// 設置第一張卡片
|
||||||
|
|
@ -273,6 +294,23 @@ export default function LearnPage() {
|
||||||
return mapping[cefr] || 50;
|
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 getCurrentContext = (userCEFR: string, wordCEFR: string): string => {
|
||||||
const userLevel = getCEFRToLevel(userCEFR);
|
const userLevel = getCEFRToLevel(userCEFR);
|
||||||
|
|
@ -548,7 +586,7 @@ export default function LearnPage() {
|
||||||
await submitReviewResult(isCorrect, answer);
|
await submitReviewResult(isCorrect, answer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交復習結果
|
// 提交復習結果並更新測驗進度
|
||||||
const submitReviewResult = async (isCorrect: boolean, userAnswer?: string, confidenceLevel?: number) => {
|
const submitReviewResult = async (isCorrect: boolean, userAnswer?: string, confidenceLevel?: number) => {
|
||||||
if (!currentCard) return;
|
if (!currentCard) return;
|
||||||
|
|
||||||
|
|
@ -572,9 +610,18 @@ export default function LearnPage() {
|
||||||
} else {
|
} else {
|
||||||
console.log('復習結果提交失敗,繼續運行');
|
console.log('復習結果提交失敗,繼續運行');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新測驗進度(無論提交成功或失敗)
|
||||||
|
setCompletedTests(prev => {
|
||||||
|
const newCompleted = prev + 1;
|
||||||
|
console.log(`📈 測驗進度更新: ${newCompleted}/${totalTests} (${Math.round((newCompleted/totalTests)*100)}%)`);
|
||||||
|
return newCompleted;
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交復習結果失敗:', error);
|
console.error('提交復習結果失敗:', error);
|
||||||
// 不中斷流程,允許用戶繼續學習
|
// 即使出錯也更新進度,避免卡住
|
||||||
|
setCompletedTests(prev => prev + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -652,6 +699,8 @@ export default function LearnPage() {
|
||||||
|
|
||||||
const handleRestart = async () => {
|
const handleRestart = async () => {
|
||||||
setScore({ correct: 0, total: 0 })
|
setScore({ correct: 0, total: 0 })
|
||||||
|
setCompletedTests(0)
|
||||||
|
setTotalTests(0)
|
||||||
setShowComplete(false)
|
setShowComplete(false)
|
||||||
setShowNoDueCards(false)
|
setShowNoDueCards(false)
|
||||||
await loadDueCards(); // 重新載入到期詞卡
|
await loadDueCards(); // 重新載入到期詞卡
|
||||||
|
|
@ -742,7 +791,10 @@ export default function LearnPage() {
|
||||||
<span className="text-sm text-gray-600">進度</span>
|
<span className="text-sm text-gray-600">進度</span>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm text-gray-600">
|
<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>
|
</span>
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<span className="text-green-600 font-semibold">{score.correct}</span>
|
<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="w-full bg-gray-200 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className="bg-primary h-2 rounded-full transition-all"
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue