refactor: 優化 review-simple 狀態管理架構

- 使用 useMemo 優化排序計算性能
- 創建 useReducer 統一狀態管理
- 抽離自定義 Hook useReviewSession
- 優化卡片查找邏輯,使用 Map 替代 findIndex
- 簡化 data.ts,移除過時的狀態處理函數
- 清理 CardState 接口,移除計算屬性

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-10-04 23:02:54 +08:00
parent 1fa8835e09
commit 914c981c4b
3 changed files with 224 additions and 153 deletions

View File

@ -29,10 +29,6 @@ export interface CardState extends ApiFlashcard {
wrongCount: number // 答錯次數
isCompleted: boolean // 是否已完成
originalOrder: number // 原始順序
// 計算屬性
delayScore: number // 延遲分數 = skipCount + wrongCount
lastAttemptAt: Date // 最後嘗試時間
}
export interface ApiResponse {
@ -54,9 +50,7 @@ const addStateFields = (flashcard: ApiFlashcard, index: number): CardState => ({
skipCount: 0,
wrongCount: 0,
isCompleted: false,
originalOrder: index,
delayScore: 0,
lastAttemptAt: new Date()
originalOrder: index
})
// 提取詞卡數據 (方便組件使用)
@ -81,29 +75,3 @@ export const sortCardsByPriority = (cards: CardState[]): CardState[] => {
return a.originalOrder - b.originalOrder
})
}
export const updateCardState = (
cards: CardState[],
currentIndex: number,
updates: Partial<CardState>
): CardState[] => {
return cards.map((card, index) =>
index === currentIndex
? {
...card,
...updates,
delayScore: (updates.skipCount ?? card.skipCount) + (updates.wrongCount ?? card.wrongCount),
lastAttemptAt: new Date()
}
: card
)
}
// 模擬API調用函數 (為未來API集成做準備)
export const mockApiCall = async (): Promise<ApiResponse> => {
// 模擬網路延遲
await new Promise(resolve => setTimeout(resolve, 500))
// 返回模擬數據
return MOCK_API_RESPONSE
}

View File

@ -0,0 +1,210 @@
import { useReducer, useEffect, useMemo } from 'react'
import { SIMPLE_CARDS, CardState, sortCardsByPriority } from '../data'
interface ReviewState {
cards: CardState[]
score: { correct: number; total: number }
isComplete: boolean
}
type ReviewAction =
| { type: 'LOAD_PROGRESS'; payload: ReviewState }
| { type: 'ANSWER_CARD'; payload: { cardId: string; confidence: number } }
| { type: 'SKIP_CARD'; payload: { cardId: string } }
| { type: 'RESTART' }
// 內部狀態更新函數
const updateCardState = (
cards: CardState[],
cardIndex: number,
updates: Partial<CardState>
): CardState[] => {
return cards.map((card, index) =>
index === cardIndex
? { ...card, ...updates }
: card
)
}
const reviewReducer = (state: ReviewState, action: ReviewAction): ReviewState => {
switch (action.type) {
case 'LOAD_PROGRESS':
return action.payload
case 'ANSWER_CARD': {
const { cardId, confidence } = action.payload
const isCorrect = confidence >= 2
// 使用 Map 優化查找性能
const cardMap = new Map(state.cards.map((card, index) => [card.id, { card, index }]))
const cardData = cardMap.get(cardId)
if (!cardData) return state
const { index: cardIndex } = cardData
const currentCard = state.cards[cardIndex]
const updatedCards = updateCardState(state.cards, cardIndex, {
isCompleted: isCorrect,
wrongCount: isCorrect ? currentCard.wrongCount : currentCard.wrongCount + 1
})
const newScore = {
correct: state.score.correct + (isCorrect ? 1 : 0),
total: state.score.total + 1
}
const remainingCards = updatedCards.filter(card => !card.isCompleted)
const isComplete = remainingCards.length === 0
return {
cards: updatedCards,
score: newScore,
isComplete
}
}
case 'SKIP_CARD': {
const { cardId } = action.payload
// 使用 Map 優化查找性能
const cardMap = new Map(state.cards.map((card, index) => [card.id, { card, index }]))
const cardData = cardMap.get(cardId)
if (!cardData) return state
const { index: cardIndex } = cardData
const currentCard = state.cards[cardIndex]
const updatedCards = updateCardState(state.cards, cardIndex, {
skipCount: currentCard.skipCount + 1
})
const remainingCards = updatedCards.filter(card => !card.isCompleted)
const isComplete = remainingCards.length === 0
return {
cards: updatedCards,
score: state.score,
isComplete
}
}
case 'RESTART':
return {
cards: SIMPLE_CARDS,
score: { correct: 0, total: 0 },
isComplete: false
}
default:
return state
}
}
export function useReviewSession() {
// 使用 useReducer 統一狀態管理
const [state, dispatch] = useReducer(reviewReducer, {
cards: SIMPLE_CARDS,
score: { correct: 0, total: 0 },
isComplete: false
})
const { cards, score, isComplete } = state
// 智能排序獲取當前卡片 - 使用 useMemo 優化性能
const sortedCards = useMemo(() => sortCardsByPriority(cards), [cards])
const incompleteCards = useMemo(() =>
sortedCards.filter((card: CardState) => !card.isCompleted),
[sortedCards]
)
const currentCard = incompleteCards[0] // 總是選擇優先級最高的未完成卡片
// localStorage進度保存和載入
useEffect(() => {
// 載入保存的進度
const savedProgress = localStorage.getItem('review-progress')
if (savedProgress) {
try {
const parsed = JSON.parse(savedProgress)
const saveTime = new Date(parsed.timestamp)
const now = new Date()
const isToday = saveTime.toDateString() === now.toDateString()
if (isToday && parsed.cards) {
dispatch({
type: 'LOAD_PROGRESS',
payload: {
cards: parsed.cards,
score: parsed.score || { correct: 0, total: 0 },
isComplete: parsed.isComplete || false
}
})
console.log('📖 載入保存的複習進度')
}
} catch (error) {
console.warn('進度載入失敗:', error)
localStorage.removeItem('review-progress')
}
}
}, [])
// 保存進度到localStorage
const saveProgress = () => {
const progress = {
cards,
score,
isComplete,
timestamp: new Date().toISOString()
}
localStorage.setItem('review-progress', JSON.stringify(progress))
console.log('💾 進度已保存')
}
// 處理答題 - 使用 dispatch 統一管理
const handleAnswer = (confidence: number) => {
if (!currentCard) return
dispatch({
type: 'ANSWER_CARD',
payload: { cardId: currentCard.id, confidence }
})
// 保存進度
setTimeout(() => saveProgress(), 100) // 延遲一點確保狀態更新
}
// 處理跳過 - 使用 dispatch 統一管理
const handleSkip = () => {
if (!currentCard) return
dispatch({
type: 'SKIP_CARD',
payload: { cardId: currentCard.id }
})
// 保存進度
setTimeout(() => saveProgress(), 100) // 延遲一點確保狀態更新
}
// 重新開始 - 重置所有狀態
const handleRestart = () => {
dispatch({ type: 'RESTART' })
localStorage.removeItem('review-progress') // 清除保存的進度
console.log('🔄 複習進度已重置')
}
return {
// 狀態
cards,
score,
isComplete,
currentCard,
sortedCards,
// 動作
handleAnswer,
handleSkip,
handleRestart
}
}

View File

@ -1,132 +1,25 @@
'use client'
import { useState, useEffect } from 'react'
import { Navigation } from '@/components/shared/Navigation'
import './globals.css'
import { SimpleFlipCard } from './components/SimpleFlipCard'
import { SimpleProgress } from './components/SimpleProgress'
import { SimpleResults } from './components/SimpleResults'
import { SIMPLE_CARDS, CardState, sortCardsByPriority, updateCardState } from './data'
import { SIMPLE_CARDS, CardState } from './data'
import { useReviewSession } from './hooks/useReviewSession'
export default function SimpleReviewPage() {
// 延遲計數狀態管理
const [cards, setCards] = useState<CardState[]>(SIMPLE_CARDS)
const [currentCardIndex, setCurrentCardIndex] = useState(0)
const [score, setScore] = useState({ correct: 0, total: 0 })
const [isComplete, setIsComplete] = useState(false)
// 智能排序獲取當前卡片
const sortedCards = sortCardsByPriority(cards)
const incompleteCards = sortedCards.filter((card: CardState) => !card.isCompleted)
const currentCard = incompleteCards[0] // 總是選擇優先級最高的未完成卡片
const isLastCard = incompleteCards.length <= 1
// localStorage進度保存和載入
useEffect(() => {
// 載入保存的進度
const savedProgress = localStorage.getItem('review-progress')
if (savedProgress) {
try {
const parsed = JSON.parse(savedProgress)
const saveTime = new Date(parsed.timestamp)
const now = new Date()
const isToday = saveTime.toDateString() === now.toDateString()
if (isToday && parsed.cards) {
setCards(parsed.cards)
setScore(parsed.score || { correct: 0, total: 0 })
console.log('📖 載入保存的複習進度')
}
} catch (error) {
console.warn('進度載入失敗:', error)
localStorage.removeItem('review-progress')
}
}
}, [])
// 保存進度到localStorage
const saveProgress = () => {
const progress = {
cards,
score,
timestamp: new Date().toISOString()
}
localStorage.setItem('review-progress', JSON.stringify(progress))
console.log('💾 進度已保存')
}
// 處理答題 - 延遲計數邏輯
const handleAnswer = (confidence: number) => {
if (!currentCard) return
// 信心度2以上算答對 (根據規格修正)
const isCorrect = confidence >= 2
// 找到當前卡片在原數組中的索引
const originalIndex = cards.findIndex(card => card.id === currentCard.id)
if (isCorrect) {
// 答對:標記為完成
const updatedCards = updateCardState(cards, originalIndex, {
isCompleted: true
})
setCards(updatedCards)
} else {
// 答錯:增加答錯次數
const updatedCards = updateCardState(cards, originalIndex, {
wrongCount: currentCard.wrongCount + 1
})
setCards(updatedCards)
}
// 更新分數統計
setScore(prevScore => ({
correct: prevScore.correct + (isCorrect ? 1 : 0),
total: prevScore.total + 1
}))
// 保存進度
saveProgress()
// 檢查是否完成所有卡片
const remainingCards = cards.filter((card: CardState) => !card.isCompleted)
if (remainingCards.length <= 1) {
setIsComplete(true)
}
}
// 處理跳過 - 延遲計數邏輯
const handleSkip = () => {
if (!currentCard) return
// 找到當前卡片在原數組中的索引
const originalIndex = cards.findIndex(card => card.id === currentCard.id)
// 增加跳過次數
const updatedCards = updateCardState(cards, originalIndex, {
skipCount: currentCard.skipCount + 1
})
setCards(updatedCards)
// 保存進度
saveProgress()
// 檢查是否完成所有卡片
const remainingCards = updatedCards.filter((card: CardState) => !card.isCompleted)
if (remainingCards.length === 0) {
setIsComplete(true)
}
}
// 重新開始 - 重置所有狀態
const handleRestart = () => {
setCards(SIMPLE_CARDS) // 重置為初始狀態
setCurrentCardIndex(0)
setScore({ correct: 0, total: 0 })
setIsComplete(false)
localStorage.removeItem('review-progress') // 清除保存的進度
console.log('🔄 複習進度已重置')
}
// 使用自定義 Hook 管理複習狀態
const {
cards,
score,
isComplete,
currentCard,
sortedCards,
handleAnswer,
handleSkip,
handleRestart
} = useReviewSession()
// 顯示結果頁面
if (isComplete) {