23 KiB
23 KiB
ReviewRunner 組件詳細說明文檔
📋 組件概覽
ReviewRunner 是複習系統的核心容器組件,負責協調 7 種不同的複習模式、管理測驗生命週期、處理答題邏輯,以及控制測驗間的導航流程。
文件位置: /frontend/components/review/ReviewRunner.tsx
組件類型: 容器組件 (Container Component)
職責範圍: 業務邏輯 + 狀態管理 + 組件編排
🏗️ 組件架構設計
架構模式:容器-展示分離
ReviewRunner (容器組件)
├── 狀態管理 (4個 Zustand Store)
├── 業務邏輯 (答題處理、導航控制)
├── 組件編排 (動態渲染7種測驗)
└── 智能導航 (SmartNavigationController)
設計哲學:
- 容器組件: 處理邏輯和狀態,不涉及UI細節
- 展示組件: 純UI渲染,接收 props 和回調
- 關注點分離: 業務邏輯與UI邏輯完全分離
📊 依賴關係分析
Store 依賴關係
// 4個 Zustand Store 的使用
useReviewSessionStore // 當前卡片、錯誤狀態
├── currentCard // 當前複習的詞卡
├── error // 會話錯誤狀態
useTestQueueStore // 測驗佇列管理
├── currentMode // 當前測驗模式
├── testItems // 測驗項目陣列
├── currentTestIndex // 當前測驗索引
├── markTestCompleted // 標記測驗完成
├── goToNextTest // 切換下一個測驗
└── skipCurrentTest // 跳過當前測驗
useTestResultStore // 測驗結果記錄
├── score // 當前分數統計
├── updateScore // 更新分數
└── recordTestResult // 記錄到後端
useReviewUIStore // UI 狀態管理
├── openReportModal // 開啟錯誤報告彈窗
└── openImageModal // 開啟圖片放大彈窗
依賴流程圖:
TestQueueStore (提供測驗項目)
↓
ReviewRunner (協調各Store + 渲染組件)
↓
TestComponent (處理用戶交互)
↓ (答案回調)
ReviewRunner.handleAnswer()
↓ (更新狀態)
TestResultStore + TestQueueStore + 後端API
🔄 核心方法詳解
1. handleAnswer - 答題處理核心邏輯
const handleAnswer = useCallback(async (answer: string, confidenceLevel?: number) => {
// 防護性檢查:避免重複提交或無效狀態下提交
if (!currentCard || hasAnswered || isProcessingAnswer) return
setIsProcessingAnswer(true) // 設置處理中狀態
try {
// 第1步:答案驗證
const isCorrect = checkAnswer(answer, currentCard, currentMode)
// 第2步:本地狀態立即更新
updateScore(isCorrect)
// 第3步:異步同步到後端
const success = await recordTestResult({
flashcardId: currentCard.id,
testType: currentMode,
isCorrect,
userAnswer: answer,
confidenceLevel,
responseTimeMs: 2000 // 可以改為實際測量值
})
// 第4步:更新測驗佇列狀態
if (success) {
markTestCompleted(currentTestIndex)
setHasAnswered(true) // 啟用"繼續"按鈕
// 第5步:答錯處理邏輯 (TODO: 未完全實現)
if (!isCorrect && currentMode !== 'flip-memory') {
console.log('答錯,將重新排入隊列')
// TODO: 實現優先級重排邏輯
}
}
} catch (error) {
console.error('答題處理失敗:', error)
// 錯誤處理:可以顯示錯誤提示或重試機制
} finally {
setIsProcessingAnswer(false) // 解除處理中狀態
}
}, [currentCard, hasAnswered, isProcessingAnswer, currentMode, updateScore, recordTestResult, markTestCompleted, currentTestIndex])
設計特色:
- 防護性檢查: 避免重複提交和無效狀態
- 樂觀更新: 本地狀態立即更新,異步同步後端
- 錯誤容錯: 完整的 try-catch 錯誤處理
- 狀態控制:
isProcessingAnswer防止按鈕重複點擊
2. checkAnswer - 答案驗證邏輯
const checkAnswer = (answer: string, card: any, mode: string): boolean => {
switch (mode) {
case 'flip-memory':
return true // 翻卡記憶沒有對錯,只有信心等級
case 'vocab-choice':
case 'vocab-listening':
return answer === card.word // 精確匹配詞彙
case 'sentence-fill':
return answer.toLowerCase().trim() === card.word.toLowerCase() // 忽略大小寫
case 'sentence-reorder':
case 'sentence-listening':
return answer.toLowerCase().trim() === card.example.toLowerCase().trim() // 句子匹配
case 'sentence-speaking':
return true // 口說測驗通常算正確 (語音識別待實現)
default:
return false
}
}
演算法特色:
- 模式特化: 每種測驗類型有專門的驗證邏輯
- 容錯設計: 忽略大小寫和空格
- 擴展性: 易於添加新的測驗類型驗證
3. generateOptions - 選項生成演算法
const generateOptions = (card: any, mode: string): string[] => {
switch (mode) {
case 'vocab-choice':
case 'vocab-listening':
// 詞彙選擇:生成3個干擾項 + 1個正確答案
return [card.word, '其他選項1', '其他選項2', '其他選項3']
.sort(() => Math.random() - 0.5) // 隨機排序
case 'sentence-listening':
// 句子聽力:生成3個例句干擾項 + 1個正確例句
return [
card.example,
'其他例句選項1',
'其他例句選項2',
'其他例句選項3'
].sort(() => Math.random() - 0.5)
default:
return [] // 其他模式不需要選項
}
}
改進空間 (目前為簡化實現):
- 真實干擾項應基於詞性、CEFR等級、語義相似性生成
- 需要避免過於簡單或過於困難的干擾項
- 可考慮用戶歷史錯誤答案作為干擾項
🎛️ 狀態管理流程
本地狀態設計
interface ReviewRunnerState {
hasAnswered: boolean // 是否已答題(控制導航按鈕顯示)
isProcessingAnswer: boolean // 是否正在處理答案(防重複提交)
}
狀態轉換流程:
初始狀態: hasAnswered=false, isProcessingAnswer=false
↓ (用戶答題)
處理中: hasAnswered=false, isProcessingAnswer=true
↓ (處理完成)
已答題: hasAnswered=true, isProcessingAnswer=false
↓ (點擊繼續/跳過)
重置狀態: hasAnswered=false, isProcessingAnswer=false (下一題)
生命週期管理
// 測驗切換時自動重置狀態
useEffect(() => {
setHasAnswered(false)
setIsProcessingAnswer(false)
}, [currentTestIndex, currentMode])
重置觸發條件:
currentTestIndex改變:切換到新測驗currentMode改變:切換測驗類型- 用戶主動跳過或繼續
🎮 動態組件渲染系統
組件映射機制
// 測驗組件映射表
const TEST_COMPONENTS = {
'flip-memory': FlipMemoryTest,
'vocab-choice': VocabChoiceTest,
'sentence-fill': SentenceFillTest,
'sentence-reorder': SentenceReorderTest,
'vocab-listening': VocabListeningTest,
'sentence-listening': SentenceListeningTest,
'sentence-speaking': SentenceSpeakingTest
} as const
動態渲染邏輯:
const renderTestContent = () => {
// 基於 currentMode 動態選擇組件
switch (currentMode) {
case 'flip-memory':
return (
<FlipMemoryTest
{...commonProps} // 共通 props
onConfidenceSubmit={(level) => handleAnswer('', level)} // 特殊處理
disabled={isProcessingAnswer} // 處理中禁用
/>
)
// ... 其他模式
}
}
設計優勢:
- 統一介面: 所有測驗組件使用相同的
commonProps - 特化處理: 各測驗的特殊需求通過額外 props 處理
- 類型安全: TypeScript 確保正確的 props 傳遞
🔀 雙重渲染模式
模式1:真實數據模式 (Production)
// 使用真實的 currentCard 數據
if (currentCard) {
const cardData = {
id: currentCard.id,
word: currentCard.word,
definition: currentCard.definition,
// ... 完整詞卡數據
}
return renderTestContent() // 渲染真實測驗
}
模式2:模擬數據模式 (Development/Testing)
// 當沒有真實數據時,使用 mockFlashcards
if (!currentCard && testItems.length > 0) {
const currentTest = testItems[currentTestIndex]
const mockCard = mockFlashcards.find(card => card.id === currentTest.cardId)
if (mockCard) {
return renderTestContentWithMockData(mockCard, currentTest.testType, mockOptions)
}
}
雙重模式的價值:
- 開發便利: 無需後端數據即可測試複習功能
- 錯誤容錯: 真實數據載入失敗時的降級方案
- 獨立測試: 前端邏輯可獨立於後端進行測試
🎯 導航控制邏輯
SmartNavigationController 整合
<SmartNavigationController
hasAnswered={hasAnswered} // 控制按鈕顯示邏輯
disabled={isProcessingAnswer} // 處理中時禁用按鈕
onSkipCallback={handleSkip} // 跳過處理函數
onContinueCallback={handleContinue} // 繼續處理函數
className="mt-4"
/>
導航邏輯流程:
未答題階段: 顯示"跳過"按鈕
↓ (用戶答題)
已答題階段: 顯示"繼續"按鈕
↓ (用戶點擊繼續)
狀態重置: 準備下一題
跳過和繼續處理
// 跳過邏輯
const handleSkip = useCallback(() => {
if (hasAnswered) return // 已答題後不能跳過
skipCurrentTest() // 更新 TestQueue Store
// 重置本地狀態,準備下一題
setHasAnswered(false)
setIsProcessingAnswer(false)
}, [hasAnswered, skipCurrentTest])
// 繼續邏輯
const handleContinue = useCallback(() => {
if (!hasAnswered) return // 未答題不能繼續
goToNextTest() // 切換到下一個測驗
// 重置本地狀態,準備下一題
setHasAnswered(false)
setIsProcessingAnswer(false)
}, [hasAnswered, goToNextTest])
📈 進度追蹤系統
ProgressBar 整合
{/* 進度條顯示邏輯 */}
{testItems.length > 0 && (
<div className="mb-6">
<ProgressBar
current={currentTestIndex} // 當前進度
total={testItems.length} // 總測驗數
correct={score.correct} // 正確數量
incorrect={score.total - score.correct} // 錯誤數量
skipped={testItems.filter(item => item.isSkipped).length} // 跳過數量
/>
</div>
)}
進度計算邏輯:
- 完成進度:
currentTestIndex / testItems.length * 100 - 正確率:
score.correct / score.total * 100 - 跳過統計: 實時統計跳過的測驗數量
🧮 測驗組件 Props 設計
統一的 commonProps
const commonProps = {
cardData, // 標準化的卡片數據
onAnswer: handleAnswer, // 統一的答題回調
onReportError: () => openReportModal(currentCard) // 錯誤報告回調
}
特化的額外 Props
// 翻卡記憶:信心度提交
<FlipMemoryTest
{...commonProps}
onConfidenceSubmit={(level) => handleAnswer('', level)} // 信心度→答題
disabled={isProcessingAnswer}
/>
// 選擇題類型:選項陣列
<VocabChoiceTest
{...commonProps}
options={generateOptions(currentCard, currentMode)} // 動態選項生成
disabled={isProcessingAnswer}
/>
// 圖片相關測驗:圖片處理
<SentenceReorderTest
{...commonProps}
exampleImage={cardData.exampleImage} // 圖片數據
onImageClick={openImageModal} // 圖片點擊處理
disabled={isProcessingAnswer}
/>
🎨 用戶體驗設計
載入和錯誤狀態處理
// 錯誤狀態顯示
if (error) {
return (
<div className="text-center py-8">
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-red-700 mb-2">發生錯誤</h3>
<p className="text-red-600">{error}</p>
</div>
</div>
)
}
// 載入狀態顯示
if (!currentCard) {
return (
<div className="text-center py-8">
<div className="text-gray-500">載入測驗中...</div>
</div>
)
}
UX 設計原則:
- 即時反饋: 用戶操作立即得到視覺回饋
- 狀態明確: 清晰區分載入、錯誤、正常狀態
- 防誤操作: 處理中狀態禁用所有交互
視覺層次和佈局
return (
<div className={`review-runner ${className}`}>
{/* 第1層:進度追蹤 */}
<div className="mb-6">
<ProgressBar {...progressProps} />
</div>
{/* 第2層:測驗內容 (主要區域) */}
<div className="mb-6">
{renderTestContent()}
</div>
{/* 第3層:導航控制 */}
<div className="border-t pt-6">
<SmartNavigationController {...navigationProps} />
</div>
</div>
)
佈局設計:
- 視覺層次: 進度→內容→導航,符合用戶視線流
- 間距統一: 使用
mb-6保持一致間距 - 分隔線:
border-t明確區分導航區域
⚡ 性能優化策略
useCallback 優化
// 依賴項精確控制,避免不必要的重新創建
const handleAnswer = useCallback(async (answer: string, confidenceLevel?: number) => {
// 答題邏輯
}, [currentCard, hasAnswered, isProcessingAnswer, currentMode, updateScore, recordTestResult, markTestCompleted, currentTestIndex])
// 依賴項最小化
const handleSkip = useCallback(() => {
// 跳過邏輯
}, [hasAnswered, skipCurrentTest])
優化原則:
- 依賴項精確: 只包含實際使用的變數
- 穩定引用: 避免子組件不必要重渲染
- 記憶化: 複雜函數使用 useCallback
條件渲染優化
// 避免不必要的組件創建
{testItems.length > 0 && ( // 條件:有測驗項目
<ProgressBar {...props} /> // 才創建進度條
)}
// 提前返回,減少後續計算
if (error) return <ErrorComponent />
if (!currentCard) return <LoadingComponent />
🔧 技術債務和改進點
當前技術債務
- generateOptions 實現簡化:
// 當前實現:硬編碼假選項
return [card.word, '其他選項1', '其他選項2', '其他選項3']
// 理想實現:智能干擾項生成
const generateIntelligentDistractors = (correctWord: string, allCards: Card[]): string[] => {
const samePOS = allCards.filter(c => c.partOfSpeech === correctWord.partOfSpeech)
const similarCEFR = allCards.filter(c => c.cefr === correctWord.cefr)
const semanticallySimilar = findSemanticallySimilar(correctWord, allCards)
return intelligentlySelect(samePOS, similarCEFR, semanticallySimilar, 3)
}
- 答錯重排邏輯未完整實現:
// TODO 部分:需要實現完整的優先級重排
if (!isCorrect && currentMode !== 'flip-memory') {
// 當前:只有 console.log
console.log('答錯,將重新排入隊列')
// 應該實現:
const { reorderByPriority, markTestIncorrect } = useTestQueueStore()
markTestIncorrect(currentTestIndex)
reorderByPriority()
}
- responseTimeMs 測量缺失:
// 當前:硬編碼
responseTimeMs: 2000
// 應該實現:實際測量
const [startTime, setStartTime] = useState<number>()
useEffect(() => {
setStartTime(Date.now()) // 測驗開始時記錄
}, [currentTestIndex])
const actualResponseTime = Date.now() - (startTime || 0)
建議的改進方向
1. 智能干擾項生成系統
interface DistractorGenerationEngine {
// 基於詞性的干擾項
generateByPartOfSpeech(word: string, pos: string): string[]
// 基於CEFR等級的干擾項
generateByCEFRLevel(word: string, level: string): string[]
// 基於語義相似性的干擾項
generateBySemantics(word: string): string[]
// 基於用戶歷史錯誤的干擾項
generateByUserMistakes(word: string, userHistory: ErrorHistory[]): string[]
}
2. 完整的答題分析系統
interface AnswerAnalyticsEngine {
// 答題時間分析
analyzeResponseTime(startTime: number, endTime: number): ResponseMetrics
// 答錯模式分析
categorizeError(
userAnswer: string,
correctAnswer: string,
testType: ReviewMode
): ErrorCategory
// 學習建議生成
generateLearningAdvice(
errorPattern: ErrorPattern,
userProfile: UserProfile
): LearningAdvice[]
}
3. 自適應難度調整
interface AdaptiveDifficultyEngine {
// 動態調整測驗難度
adjustDifficulty(
currentPerformance: PerformanceMetrics,
userProfile: UserProfile
): DifficultyAdjustment
// 個性化測驗序列
optimizeTestSequence(
remainingTests: TestItem[],
userStrongWeakPoints: UserAnalytics
): TestItem[]
}
📊 性能指標和監控
關鍵性能指標
渲染性能:
- 組件切換時間: 目標 <300ms
- 答題處理時間: 目標 <500ms
- 狀態更新延遲: 目標 <100ms
記憶體使用:
- 組件記憶體: 每個測驗組件 <5MB
- 狀態記憶體: 整體 Store 狀態 <10MB
- 清理機制: 組件卸載時自動清理
網路性能:
- 答題同步: 目標 <1秒
- 佇列載入: 目標 <2秒
- 錯誤重試: 自動重試 3 次
性能監控實現
// 可添加的性能監控邏輯
const usePerformanceMonitoring = () => {
const startTime = useRef<number>()
useEffect(() => {
startTime.current = performance.now()
}, [currentTestIndex])
const recordMetrics = useCallback((action: string) => {
if (startTime.current) {
const duration = performance.now() - startTime.current
console.log(`${action} took ${duration.toFixed(2)}ms`)
// 可以發送到分析服務
analytics.track('test_performance', {
action,
duration,
testType: currentMode,
cardId: currentCard?.id
})
}
}, [currentMode, currentCard])
return { recordMetrics }
}
🔮 未來擴展可能性
1. 實時協作學習
interface CollaborativeLearning {
// 多人同時複習
joinSession(sessionId: string): Promise<CollaborativeSession>
// 實時同步進度
syncProgress(progress: ProgressState): Promise<void>
// 互助提示系統
requestHint(testId: string): Promise<PeerHint>
provideHint(testId: string, hint: string): Promise<void>
}
2. AI輔助學習
interface AIAssistedLearning {
// 智能提示系統
generateHint(
testType: ReviewMode,
cardData: ReviewCardData,
userAttempts: Attempt[]
): LearningHint
// 個性化難度
adjustDifficulty(
userPerformance: PerformanceHistory,
targetAccuracy: number
): DifficultyParams
// 學習路徑優化
optimizeLearningPath(
userWeaknesses: WeaknessProfile,
availableTime: number
): OptimizedPath
}
3. 多模態學習整合
interface MultimodalLearning {
// VR/AR 學習環境
enterVRMode(testType: ReviewMode): Promise<VRSession>
// 語音評估整合
enableSpeechAssessment(): Promise<SpeechEvaluator>
// 手寫識別
enableHandwritingRecognition(): Promise<HandwritingEngine>
// 眼動追蹤學習分析
trackLearningAttention(): Promise<AttentionMetrics>
}
🏆 組件設計優勢
架構優勢
- 模組化設計: 清晰的職責分離,易於維護和擴展
- 類型安全: 完整的 TypeScript 類型定義,編譯時錯誤檢查
- 狀態管理: Zustand 提供高效的跨組件狀態同步
- 性能優化: useCallback 和條件渲染減少不必要的重新渲染
- 錯誤處理: 完整的錯誤邊界和降級方案
開發體驗優勢
- 開發效率: 模擬數據模式支援獨立開發
- 測試友好: 純函數設計便於單元測試
- 調試便利: 詳細的 console.log 和錯誤訊息
- 擴展性: 新測驗類型可透過 switch case 輕易添加
學習體驗優勢
- 即時反饋: 答題結果立即顯示
- 進度可視: 詳細的進度追蹤和統計
- 智能導航: 根據答題狀態智能顯示操作選項
- 容錯機制: 跳過和重試機制避免學習中斷
🔧 使用指南和最佳實踐
組件使用方式
// 在頁面中使用 ReviewRunner
import { ReviewRunner } from '@/components/review/ReviewRunner'
const ReviewPage = () => {
return (
<div className="review-page">
<Navigation />
<div className="max-w-4xl mx-auto px-4 py-8">
<ReviewRunner className="review-content" />
</div>
</div>
)
}
自定義測驗類型擴展
// 1. 創建新的測驗組件
const NewTestType = ({ cardData, onAnswer, disabled }) => {
// 測驗邏輯實現
return <div>新測驗類型UI</div>
}
// 2. 在 ReviewRunner 中添加映射
case 'new-test-type':
return (
<NewTestType
{...commonProps}
disabled={isProcessingAnswer}
// 特化 props
/>
)
// 3. 更新類型定義
export type ReviewMode =
| 'flip-memory'
| 'vocab-choice'
| 'new-test-type' // 新增
Store 狀態訂閱最佳實踐
// 精確訂閱,避免不必要重渲染
const currentTest = useTestQueueStore(state =>
state.testItems[state.currentTestIndex]
)
// 避免:訂閱整個 Store
const store = useTestQueueStore() // ❌ 會導致所有變化都重渲染
// 推薦:選擇性訂閱
const { currentMode, currentTestIndex } = useTestQueueStore(state => ({
currentMode: state.currentMode,
currentTestIndex: state.currentTestIndex
})) // ✅ 只有這兩個屬性變化才重渲染
📋 總結
ReviewRunner 是複習系統的核心控制中樞,展現了現代 React 應用的最佳實踐:
- 容器-展示分離: 邏輯與UI完全分離
- 狀態管理: 多Store協作,職責分明
- 動態渲染: 基於狀態的智能組件切換
- 用戶體驗: 完整的錯誤處理和載入狀態
- 性能優化: useCallback和條件渲染優化
- 可擴展性: 新測驗類型易於添加
這個組件是複習功能架構設計的精華,體現了複雜業務邏輯的優雅實現。
文檔版本: v1.0 分析對象: ReviewRunner.tsx (440行) 最後更新: 2025-10-02