From 51e58703907169b955baadeebde0f64acd70c722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=84=AD=E6=B2=9B=E8=BB=92?= Date: Sun, 5 Oct 2025 04:06:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=A6=E7=8F=BE=E7=B7=9A=E6=80=A7?= =?UTF-8?q?=E9=9B=99=E6=B8=AC=E9=A9=97=E6=B5=81=E7=A8=8B=E7=B3=BB=E7=B5=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 主要功能 - 實現線性複習流程:A翻卡 → A選擇 → B翻卡 → B選擇... - 測驗項目級別的狀態管理和進度追蹤 - 自動測驗類型切換,無需用戶選擇 ## 核心改進 - 新增 TestItem 數據結構支援線性流程 - 重構 useReviewSession Hook 管理測驗項目 - 修正延遲計數系統優先級排序邏輯 - 統一兩種測驗的跳過按鈕位置 ## 評分標準修正 - 翻卡記憶:一般(1分)以上算答對 - 詞彙選擇:正確選擇算答對 - 答錯的測驗項目不標記完成,會重新出現 ## 用戶體驗改善 - 進入頁面自動開始線性測驗 - 清楚的測驗類型和進度指示 - 測驗項目序列可視化 - 延遲計數系統視覺反饋 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/review-simple/page.tsx | 94 +++++++--- frontend/app/test-vocab-choice/page.tsx | 124 ------------- .../review/simple/SimpleProgress.tsx | 99 ++++++----- .../review/simple/SimpleResults.tsx | 3 - .../review/simple/VocabChoiceTest.tsx | 168 +++++++++--------- frontend/hooks/review/useReviewSession.ts | 168 ++++++++++-------- frontend/lib/data/reviewSimpleData.ts | 81 ++++++++- note/複習系統/產品需求規格.md | 115 +++++++++--- 8 files changed, 480 insertions(+), 372 deletions(-) delete mode 100644 frontend/app/test-vocab-choice/page.tsx diff --git a/frontend/app/review-simple/page.tsx b/frontend/app/review-simple/page.tsx index 6894fbc..d718bff 100644 --- a/frontend/app/review-simple/page.tsx +++ b/frontend/app/review-simple/page.tsx @@ -3,19 +3,23 @@ import { Navigation } from '@/components/shared/Navigation' import './globals.css' import { SimpleFlipCard } from '@/components/review/simple/SimpleFlipCard' +import { VocabChoiceTest } from '@/components/review/simple/VocabChoiceTest' import { SimpleProgress } from '@/components/review/simple/SimpleProgress' import { SimpleResults } from '@/components/review/simple/SimpleResults' -import { SIMPLE_CARDS, CardState } from '@/lib/data/reviewSimpleData' +import { SIMPLE_CARDS } from '@/lib/data/reviewSimpleData' import { useReviewSession } from '@/hooks/review/useReviewSession' export default function SimpleReviewPage() { - // 使用自定義 Hook 管理複習狀態 + // 使用重構後的 Hook 管理線性複習狀態 const { - cards, + testItems, score, isComplete, + currentTestItem, currentCard, - sortedCards, + vocabOptions, + totalTestItems, + completedTestItems, handleAnswer, handleSkip, handleRestart @@ -27,42 +31,86 @@ export default function SimpleReviewPage() {
- +
+ + + {/* 線性測驗完成統計 */} +
+

測驗統計

+
+
+
{completedTestItems}
+
完成測驗項目
+
+
+
{SIMPLE_CARDS.length}
+
練習詞卡數
+
+
+
+ {Math.round((score.correct / score.total) * 100)}% +
+
正確率
+
+
+
+
) } - // 主要複習頁面 + // 主要線性測驗頁面 return (
- {/* 進度顯示 */} + {/* 使用修改後的 SimpleProgress 組件 */} card.isCompleted).length + 1} - total={cards.length} + currentTestItem={currentTestItem} + totalTestItems={totalTestItems} + completedTestItems={completedTestItems} score={score} - cards={cards} - sortedCards={sortedCards} - currentCard={currentCard} + testItems={testItems} /> - {/* 翻卡組件 */} - {currentCard && ( - + {/* 根據當前測驗項目類型渲染對應組件 */} + {currentTestItem && currentCard && ( + <> + {currentTestItem.testType === 'flip-card' && ( + + )} + + {currentTestItem.testType === 'vocab-choice' && ( + + )} + )} + {/* 控制按鈕 */} +
+ +
diff --git a/frontend/app/test-vocab-choice/page.tsx b/frontend/app/test-vocab-choice/page.tsx deleted file mode 100644 index 673a1fd..0000000 --- a/frontend/app/test-vocab-choice/page.tsx +++ /dev/null @@ -1,124 +0,0 @@ -'use client' - -import { useState } from 'react' -import { Navigation } from '@/components/shared/Navigation' -import { VocabChoiceTest } from '@/components/review/simple/VocabChoiceTest' -import { SIMPLE_CARDS, CardState } from '@/lib/data/reviewSimpleData' - -export default function TestVocabChoicePage() { - const [currentCardIndex, setCurrentCardIndex] = useState(0) - const [score, setScore] = useState({ correct: 0, total: 0 }) - - const currentCard = SIMPLE_CARDS[currentCardIndex] - - // 生成測驗選項 (包含正確答案和3個干擾項) - const generateOptions = (correctWord: string) => { - const allWords = SIMPLE_CARDS.map(card => card.word).filter(word => word !== correctWord) - const shuffledWords = allWords.sort(() => Math.random() - 0.5) - const distractors = shuffledWords.slice(0, 3) - - const options = [correctWord, ...distractors] - return options.sort(() => Math.random() - 0.5) // 隨機排列選項 - } - - const handleAnswer = (confidence: number) => { - const isCorrect = confidence >= 2 - setScore(prev => ({ - correct: prev.correct + (isCorrect ? 1 : 0), - total: prev.total + 1 - })) - - // 移動到下一張卡片 - setTimeout(() => { - if (currentCardIndex < SIMPLE_CARDS.length - 1) { - setCurrentCardIndex(prev => prev + 1) - } else { - alert(`測試完成!正確率: ${score.correct + (isCorrect ? 1 : 0)}/${score.total + 1}`) - } - }, 100) - } - - const handleSkip = () => { - setScore(prev => ({ ...prev, total: prev.total + 1 })) - - if (currentCardIndex < SIMPLE_CARDS.length - 1) { - setCurrentCardIndex(prev => prev + 1) - } else { - alert(`測試完成!正確率: ${score.correct}/${score.total + 1}`) - } - } - - const handleRestart = () => { - setCurrentCardIndex(0) - setScore({ correct: 0, total: 0 }) - } - - if (!currentCard) { - return ( -
- -
-
-

測試完成!

- -
-
-
- ) - } - - return ( -
- - -
-
- {/* 進度顯示 */} -
-
-

詞彙選擇測驗

-
- 第 {currentCardIndex + 1} / {SIMPLE_CARDS.length} 題 -
-
-
-
- 得分: {score.correct} / {score.total} - {score.total > 0 && ` (${Math.round(score.correct / score.total * 100)}%)`} -
-
-
-
-
-
- - {/* 測驗組件 */} - - - {/* 控制按鈕 */} -
- -
-
-
-
- ) -} \ No newline at end of file diff --git a/frontend/components/review/simple/SimpleProgress.tsx b/frontend/components/review/simple/SimpleProgress.tsx index 6e9dcc1..0f90ccb 100644 --- a/frontend/components/review/simple/SimpleProgress.tsx +++ b/frontend/components/review/simple/SimpleProgress.tsx @@ -1,32 +1,43 @@ -import { CardState } from '@/lib/data/reviewSimpleData' +import { TestItem } from '@/lib/data/reviewSimpleData' interface SimpleProgressProps { - current: number - total: number + currentTestItem?: TestItem + totalTestItems: number + completedTestItems: number score: { correct: number; total: number } - cards?: CardState[] // 可選:用於顯示延遲統計 - sortedCards?: CardState[] // 智能排序後的卡片 - currentCard?: CardState // 當前正在練習的卡片 + testItems?: TestItem[] // 用於顯示測驗項目統計 } -export function SimpleProgress({ current, total, score, cards, sortedCards, currentCard }: SimpleProgressProps) { - const progress = (current - 1) / total * 100 +export function SimpleProgress({ currentTestItem, totalTestItems, completedTestItems, score, testItems }: SimpleProgressProps) { + const progress = (completedTestItems / totalTestItems) * 100 const accuracy = score.total > 0 ? Math.round((score.correct / score.total) * 100) : 0 - // 延遲統計計算 - const delayStats = cards ? { - totalSkips: cards.reduce((sum, card) => sum + card.skipCount, 0), - totalWrongs: cards.reduce((sum, card) => sum + card.wrongCount, 0), - delayedCards: cards.filter(card => card.skipCount + card.wrongCount > 0).length + // 測驗項目延遲統計計算 + const delayStats = testItems ? { + totalSkips: testItems.reduce((sum, item) => sum + item.skipCount, 0), + totalWrongs: testItems.reduce((sum, item) => sum + item.wrongCount, 0), + delayedItems: testItems.filter(item => item.skipCount + item.wrongCount > 0).length } : null return (
- 學習進度 -
+
+ 線性複習進度 + {currentTestItem && ( +
+ + {currentTestItem.testType === 'flip-card' ? '🔄' : '🎯'} + + + {currentTestItem.testType === 'flip-card' ? '翻卡記憶' : '詞彙選擇'} • {currentTestItem.cardData.word} + +
+ )} +
+
- {current}/{total} + {completedTestItems}/{totalTestItems} 項目 {score.total > 0 && ( @@ -69,53 +80,56 @@ export function SimpleProgress({ current, total, score, cards, sortedCards, curr
- 跳過卡片 {delayStats.delayedCards} + 跳過卡片 {delayStats.delayedItems}
)}
- {/* 詞彙順序可視化 - 便於驗證延遲計數系統 */} - {sortedCards && currentCard && ( + {/* 測驗項目順序可視化 */} + {testItems && currentTestItem && (
-

詞彙順序 (按延遲分數排序):

+

測驗項目序列 (線性流程):

- {sortedCards.map((card, index) => { - const isCompleted = card.isCompleted - const delayScore = card.skipCount + card.wrongCount + {testItems.slice(0, 12).map((item) => { + const isCompleted = item.isCompleted + const isCurrent = item.id === currentTestItem?.id + const delayScore = item.skipCount + item.wrongCount // 狀態顏色 - let cardStyle = '' + let itemStyle = '' let statusText = '' - if (isCompleted) { - cardStyle = 'bg-green-100 text-green-700 border-green-300' + if (isCompleted) { + itemStyle = 'bg-green-100 text-green-700 border-green-300' statusText = '✓' } else if (delayScore > 0) { - if (card.skipCount > 0 && card.wrongCount > 0) { - cardStyle = 'bg-red-100 text-red-700 border-red-300' - statusText = `跳${card.skipCount}錯${card.wrongCount}` - } else if (card.skipCount > 0) { - cardStyle = 'bg-yellow-100 text-yellow-700 border-yellow-300' - statusText = `跳${card.skipCount}` + if (item.skipCount > 0 && item.wrongCount > 0) { + itemStyle = 'bg-red-100 text-red-700 border-red-300' + statusText = `${item.skipCount}+${item.wrongCount}` + } else if (item.skipCount > 0) { + itemStyle = 'bg-yellow-100 text-yellow-700 border-yellow-300' + statusText = `跳${item.skipCount}` } else { - cardStyle = 'bg-orange-100 text-orange-700 border-orange-300' - statusText = `錯${card.wrongCount}` + itemStyle = 'bg-orange-100 text-orange-700 border-orange-300' + statusText = `錯${item.wrongCount}` } } else { - cardStyle = 'bg-gray-100 text-gray-600 border-gray-300' + itemStyle = 'bg-gray-100 text-gray-600 border-gray-300' statusText = '' } return (
- {index + 1}. - {card.word} + + {item.testType === 'flip-card' ? '🔄' : '🎯'} + + {item.cardData.word} {statusText && ( ({statusText}) )} @@ -123,9 +137,14 @@ export function SimpleProgress({ current, total, score, cards, sortedCards, curr
) })} + {testItems.length > 12 && ( +
+ ...還有 {testItems.length - 12} 個項目 +
+ )}

- 🟢完成、🟡跳過、🟠答錯、🔴跳過+答錯、⚪未開始 + 🟢完成、🟡跳過、🟠答錯、🔴多次&答錯、⚪待進行

diff --git a/frontend/components/review/simple/SimpleResults.tsx b/frontend/components/review/simple/SimpleResults.tsx index ccaf144..34beb4a 100644 --- a/frontend/components/review/simple/SimpleResults.tsx +++ b/frontend/components/review/simple/SimpleResults.tsx @@ -1,9 +1,6 @@ -import { CardState } from '../data' - interface SimpleResultsProps { score: { correct: number; total: number } totalCards: number - cards?: CardState[] onRestart: () => void } diff --git a/frontend/components/review/simple/VocabChoiceTest.tsx b/frontend/components/review/simple/VocabChoiceTest.tsx index d338313..ab9c014 100644 --- a/frontend/components/review/simple/VocabChoiceTest.tsx +++ b/frontend/components/review/simple/VocabChoiceTest.tsx @@ -41,97 +41,99 @@ export function VocabChoiceTest({ card, options, onAnswer, onSkip }: VocabChoice const isCorrect = selectedAnswer === card.word return ( -
- +
+
+ - {/* 問題區域 */} -
-
-

定義

-

{card.definition}

+ {/* 問題區域 */} +
+
+

定義

+

{card.definition}

+
+

+ 請選擇符合上述定義的英文詞彙: +

-

- 請選擇符合上述定義的英文詞彙: -

-
- {/* 選項區域 */} -
-
- {options.map((option, index) => { - const isSelected = selectedAnswer === option - const isCorrectOption = option === card.word - - let buttonClass = 'p-4 rounded-lg border-2 text-center font-medium transition-all duration-200 cursor-pointer active:scale-95' - - if (showResult) { - if (isSelected && isCorrectOption) { - // 選中且正確 - buttonClass += ' bg-green-100 text-green-700 border-green-200 ring-2 ring-green-400' - } else if (isSelected && !isCorrectOption) { - // 選中但錯誤 - buttonClass += ' bg-red-100 text-red-700 border-red-200 ring-2 ring-red-400' - } else if (!isSelected && isCorrectOption) { - // 未選中但是正確答案 - buttonClass += ' bg-green-50 text-green-600 border-green-200' - } else { - // 未選中且非正確答案 - buttonClass += ' bg-gray-50 text-gray-500 border-gray-200' - } - } else { - // 未答題狀態 - buttonClass += ' bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100' - } - - return ( - - ) - })} -
-
- - {/* 結果顯示區域 */} - {showResult && ( + {/* 選項區域 */}
-
-
- - {isCorrect ? '✅' : '❌'} - -

- {isCorrect ? '答對了!' : '答錯了'} -

-
+
+ {options.map((option, index) => { + const isSelected = selectedAnswer === option + const isCorrectOption = option === card.word -
-

- 正確答案: {card.word} -

-

- 發音: {card.pronunciation} -

-

- 例句: "{card.example}" -

-

- {card.exampleTranslation} -

-
+ let buttonClass = 'p-4 rounded-lg border-2 text-center font-medium transition-all duration-200 cursor-pointer active:scale-95' + + if (showResult) { + if (isSelected && isCorrectOption) { + // 選中且正確 + buttonClass += ' bg-green-100 text-green-700 border-green-200 ring-2 ring-green-400' + } else if (isSelected && !isCorrectOption) { + // 選中但錯誤 + buttonClass += ' bg-red-100 text-red-700 border-red-200 ring-2 ring-red-400' + } else if (!isSelected && isCorrectOption) { + // 未選中但是正確答案 + buttonClass += ' bg-green-50 text-green-600 border-green-200' + } else { + // 未選中且非正確答案 + buttonClass += ' bg-gray-50 text-gray-500 border-gray-200' + } + } else { + // 未答題狀態 + buttonClass += ' bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100' + } + + return ( + + ) + })}
- )} - {/* 跳過按鈕 */} + {/* 結果顯示區域 */} + {showResult && ( +
+
+
+ + {isCorrect ? '✅' : '❌'} + +

+ {isCorrect ? '答對了!' : '答錯了'} +

+
+ +
+

+ 正確答案: {card.word} +

+

+ 發音: {card.pronunciation} +

+

+ 例句: "{card.example}" +

+

+ {card.exampleTranslation} +

+
+
+
+ )} +
+ + {/* 跳過按鈕 - 移到卡片外面 */} {!showResult && (