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:
parent
1fa8835e09
commit
914c981c4b
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
// 使用自定義 Hook 管理複習狀態
|
||||
const {
|
||||
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('🔄 複習進度已重置')
|
||||
}
|
||||
isComplete,
|
||||
currentCard,
|
||||
sortedCards,
|
||||
handleAnswer,
|
||||
handleSkip,
|
||||
handleRestart
|
||||
} = useReviewSession()
|
||||
|
||||
// 顯示結果頁面
|
||||
if (isComplete) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue