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 // 答錯次數
|
wrongCount: number // 答錯次數
|
||||||
isCompleted: boolean // 是否已完成
|
isCompleted: boolean // 是否已完成
|
||||||
originalOrder: number // 原始順序
|
originalOrder: number // 原始順序
|
||||||
|
|
||||||
// 計算屬性
|
|
||||||
delayScore: number // 延遲分數 = skipCount + wrongCount
|
|
||||||
lastAttemptAt: Date // 最後嘗試時間
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResponse {
|
export interface ApiResponse {
|
||||||
|
|
@ -54,9 +50,7 @@ const addStateFields = (flashcard: ApiFlashcard, index: number): CardState => ({
|
||||||
skipCount: 0,
|
skipCount: 0,
|
||||||
wrongCount: 0,
|
wrongCount: 0,
|
||||||
isCompleted: false,
|
isCompleted: false,
|
||||||
originalOrder: index,
|
originalOrder: index
|
||||||
delayScore: 0,
|
|
||||||
lastAttemptAt: new Date()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 提取詞卡數據 (方便組件使用)
|
// 提取詞卡數據 (方便組件使用)
|
||||||
|
|
@ -81,29 +75,3 @@ export const sortCardsByPriority = (cards: CardState[]): CardState[] => {
|
||||||
return a.originalOrder - b.originalOrder
|
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'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { Navigation } from '@/components/shared/Navigation'
|
import { Navigation } from '@/components/shared/Navigation'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import { SimpleFlipCard } from './components/SimpleFlipCard'
|
import { SimpleFlipCard } from './components/SimpleFlipCard'
|
||||||
import { SimpleProgress } from './components/SimpleProgress'
|
import { SimpleProgress } from './components/SimpleProgress'
|
||||||
import { SimpleResults } from './components/SimpleResults'
|
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() {
|
export default function SimpleReviewPage() {
|
||||||
// 延遲計數狀態管理
|
// 使用自定義 Hook 管理複習狀態
|
||||||
const [cards, setCards] = useState<CardState[]>(SIMPLE_CARDS)
|
const {
|
||||||
const [currentCardIndex, setCurrentCardIndex] = useState(0)
|
cards,
|
||||||
const [score, setScore] = useState({ correct: 0, total: 0 })
|
score,
|
||||||
const [isComplete, setIsComplete] = useState(false)
|
isComplete,
|
||||||
|
currentCard,
|
||||||
// 智能排序獲取當前卡片
|
sortedCards,
|
||||||
const sortedCards = sortCardsByPriority(cards)
|
handleAnswer,
|
||||||
const incompleteCards = sortedCards.filter((card: CardState) => !card.isCompleted)
|
handleSkip,
|
||||||
const currentCard = incompleteCards[0] // 總是選擇優先級最高的未完成卡片
|
handleRestart
|
||||||
const isLastCard = incompleteCards.length <= 1
|
} = useReviewSession()
|
||||||
|
|
||||||
// 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('🔄 複習進度已重置')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 顯示結果頁面
|
// 顯示結果頁面
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue