# 複習系統前端規格書 **版本**: 1.0 **對應**: 技術實作規格.md + 產品需求規格.md **技術棧**: React 18 + TypeScript + Tailwind CSS **狀態管理**: React useState (極簡架構) **最後更新**: 2025-10-03 --- ## 📱 **前端架構設計** ### **目錄結構** ``` app/review-simple/ ├── page.tsx # 主頁面邏輯 ├── data.ts # 數據和類型定義 ├── globals.css # 翻卡動畫樣式 └── components/ ├── SimpleFlipCard.tsx # 翻卡記憶組件 ├── SimpleChoiceTest.tsx # 詞彙選擇組件 (階段2) ├── SimpleProgress.tsx # 進度顯示組件 ├── SimpleResults.tsx # 結果統計組件 └── SimpleTestHeader.tsx # 測試標題組件 ``` --- ## 🗃️ **數據結構設計** ### **卡片狀態接口** ```typescript interface CardState extends ApiFlashcard { // 前端狀態管理欄位 skipCount: number // 跳過次數 wrongCount: number // 答錯次數 successCount: number // 答對次數 isCompleted: boolean // 是否已完成 originalOrder: number // 原始順序索引 // 計算屬性 delayScore: number // 延遲分數 = skipCount + wrongCount lastAttemptAt: Date // 最後嘗試時間 } ``` ### **學習會話狀態** ```typescript interface ReviewSessionState { // 卡片管理 cards: CardState[] currentIndex: number // 進度統計 score: { correct: number // 答對總數 total: number // 嘗試總數 } // 會話控制 isComplete: boolean startTime: Date // UI狀態 currentMode: 'flip' | 'choice' // 階段2需要 } ``` --- ## ⚙️ **核心邏輯函數** ### **延遲計數管理** ```typescript // 跳過處理 const handleSkip = useCallback((cards: CardState[], currentIndex: number) => { const updatedCards = cards.map((card, index) => index === currentIndex ? { ...card, skipCount: card.skipCount + 1, delayScore: card.skipCount + 1 + card.wrongCount, lastAttemptAt: new Date() } : card ) return { updatedCards, nextIndex: getNextCardIndex(updatedCards, currentIndex) } }, []) // 答錯處理 const handleWrongAnswer = useCallback((cards: CardState[], currentIndex: number) => { const updatedCards = cards.map((card, index) => index === currentIndex ? { ...card, wrongCount: card.wrongCount + 1, delayScore: card.skipCount + card.wrongCount + 1, lastAttemptAt: new Date() } : card ) return { updatedCards, nextIndex: getNextCardIndex(updatedCards, currentIndex) } }, []) // 答對處理 const handleCorrectAnswer = useCallback((cards: CardState[], currentIndex: number) => { const updatedCards = cards.map((card, index) => index === currentIndex ? { ...card, isCompleted: true, successCount: card.successCount + 1, lastAttemptAt: new Date() } : card ) return { updatedCards, nextIndex: getNextCardIndex(updatedCards, currentIndex) } }, []) ``` ### **智能排序系統** ```typescript // 卡片優先級排序 (您的核心需求) const sortCardsByPriority = useCallback((cards: CardState[]): CardState[] => { return cards.sort((a, b) => { // 1. 已完成的卡片排到最後 if (a.isCompleted && !b.isCompleted) return 1 if (!a.isCompleted && b.isCompleted) return -1 // 2. 未完成卡片按延遲分數排序 (越少越前面) const aDelayScore = a.skipCount + a.wrongCount const bDelayScore = b.skipCount + b.wrongCount if (aDelayScore !== bDelayScore) { return aDelayScore - bDelayScore } // 3. 延遲分數相同時按原始順序 return a.originalOrder - b.originalOrder }) }, []) // 獲取下一張卡片索引 const getNextCardIndex = (cards: CardState[], currentIndex: number): number => { const sortedCards = sortCardsByPriority(cards) const incompleteCards = sortedCards.filter(card => !card.isCompleted) if (incompleteCards.length === 0) return -1 // 全部完成 // 返回排序後第一張未完成卡片的索引 const nextCard = incompleteCards[0] return cards.findIndex(card => card.id === nextCard.id) } ``` --- ## 🎯 **組件設計規格** ### **SimpleFlipCard.tsx (階段1)** ```typescript interface SimpleFlipCardProps { card: CardState onAnswer: (confidence: 1|2|3) => void // 簡化為3選項 onSkip: () => void } // 內部狀態 const [isFlipped, setIsFlipped] = useState(false) const [selectedConfidence, setSelectedConfidence] = useState(null) const [cardHeight, setCardHeight] = useState(400) // 信心度選項 (簡化版) const confidenceOptions = [ { level: 1, label: '模糊', color: 'bg-orange-100 text-orange-700 border-orange-200' }, { level: 2, label: '一般', color: 'bg-yellow-100 text-yellow-700 border-yellow-200' }, { level: 3, label: '熟悉', color: 'bg-green-100 text-green-700 border-green-200' } ] ``` ### **SimpleChoiceTest.tsx (階段2)** ```typescript interface SimpleChoiceTestProps { card: CardState options: string[] // 4選1選項 onAnswer: (answer: string) => void onSkip: () => void } // 內部狀態 const [selectedAnswer, setSelectedAnswer] = useState(null) const [showResult, setShowResult] = useState(false) // 答案驗證 const isCorrect = useMemo(() => selectedAnswer === card.word, [selectedAnswer, card.word]) ``` ### **SimpleProgress.tsx** ```typescript interface SimpleProgressProps { cards: CardState[] currentIndex: number score: { correct: number; total: number } } // 進度計算 const completedCount = cards.filter(card => card.isCompleted).length const totalCount = cards.length const progressPercentage = (completedCount / totalCount) * 100 // 延遲統計 (顯示跳過次數) const delayedCards = cards.filter(card => card.skipCount + card.wrongCount > 0) const totalSkips = cards.reduce((sum, card) => sum + card.skipCount, 0) const totalWrongs = cards.reduce((sum, card) => sum + card.wrongCount, 0) ``` --- ## 🌐 **API呼叫策略** (各階段明確區分) ### **階段1: 純靜態數據 (當前MVP)** ```typescript // 完全不呼叫任何API export default function SimpleReviewPage() { useEffect(() => { // 直接使用靜態數據,無網路依賴 const staticCards = SIMPLE_CARDS.map((card, index) => ({ ...card, skipCount: 0, wrongCount: 0, // ... 其他前端狀態 })) setCards(staticCards) }, []) // 答題完成時:只更新前端狀態,不呼叫API const handleAnswer = (confidence: number) => { // 純前端邏輯,無API調用 updateLocalState(confidence) } } ``` ### **階段2: 本地持久化 (localStorage)** ```typescript // 仍然不呼叫API,只添加本地存儲 export default function SimpleReviewPage() { useEffect(() => { // 1. 嘗試從localStorage載入 const savedProgress = loadFromLocalStorage() if (savedProgress && isSameDay(savedProgress.timestamp)) { setCards(savedProgress.cards) setCurrentIndex(savedProgress.currentIndex) } else { // 2. 無有效存檔則使用靜態數據 setCards(SIMPLE_CARDS.map(addStateFields)) } }, []) // 答題時:更新狀態 + 保存到localStorage const handleAnswer = (confidence: number) => { const newState = updateLocalState(confidence) saveToLocalStorage(newState) // 本地持久化 // 仍然不呼叫API } } ``` ### **階段3: API集成 (遠期)** ```typescript // 明確的API呼叫時機和策略 export default function SimpleReviewPage() { const [dataSource, setDataSource] = useState<'static' | 'api'>('static') useEffect(() => { const loadCards = async () => { setLoading(true) try { // 嘗試API呼叫 const response = await fetch('/api/flashcards/due?limit=10', { headers: { 'Authorization': `Bearer ${getAuthToken()}` } }) if (response.ok) { const apiData = await response.json() setCards(apiData.data.flashcards.map(addStateFields)) setDataSource('api') } else { throw new Error('API failed') } } catch (error) { console.warn('API unavailable, using static data:', error) // 降級到靜態數據 setCards(SIMPLE_CARDS.map(addStateFields)) setDataSource('static') } finally { setLoading(false) } } loadCards() }, []) // 答題時的API呼叫邏輯 const handleAnswer = async (confidence: number) => { // 1. 立即更新前端狀態 (即時響應) const newState = updateLocalState(confidence) // 2. 如果使用API,同步到後端 if (dataSource === 'api') { try { await fetch(`/api/flashcards/${currentCard.id}/review`, { method: 'POST', headers: { 'Authorization': `Bearer ${getAuthToken()}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ confidence: confidence, isCorrect: confidence >= 2 }) }) } catch (error) { console.warn('Failed to sync to backend:', error) // 前端狀態已更新,API失敗不影響用戶體驗 } } } } ``` ### **API呼叫判斷邏輯** ```typescript // 何時使用API vs 靜態數據 const determineDataSource = () => { // 檢查是否有認證Token const hasAuth = getAuthToken() !== null // 檢查是否在生產環境 const isProduction = process.env.NODE_ENV === 'production' // 檢查是否明確要求使用API const forceApi = window.location.search.includes('api=true') return (hasAuth && isProduction) || forceApi ? 'api' : 'static' } ``` --- ## 🔄 **狀態管理設計** ### **主頁面狀態 (page.tsx)** ```typescript export default function SimpleReviewPage() { // 核心狀態 const [cards, setCards] = useState([]) const [currentIndex, setCurrentIndex] = useState(0) const [score, setScore] = useState({ correct: 0, total: 0 }) const [isComplete, setIsComplete] = useState(false) const [mode, setMode] = useState<'flip' | 'choice'>('flip') // 階段2需要 // 初始化卡片狀態 useEffect(() => { const initialCards: CardState[] = SIMPLE_CARDS.map((card, index) => ({ ...card, skipCount: 0, wrongCount: 0, successCount: 0, isCompleted: false, originalOrder: index, delayScore: 0, lastAttemptAt: new Date() })) setCards(initialCards) }, []) // 答題處理 const handleAnswer = useCallback((confidence: number) => { const isCorrect = confidence >= 2 // 一般以上算答對 if (isCorrect) { const result = handleCorrectAnswer(cards, currentIndex) setCards(result.updatedCards) setCurrentIndex(result.nextIndex) } else { const result = handleWrongAnswer(cards, currentIndex) setCards(result.updatedCards) setCurrentIndex(result.nextIndex) } // 更新分數統計 setScore(prev => ({ correct: prev.correct + (isCorrect ? 1 : 0), total: prev.total + 1 })) // 檢查是否完成 checkIfComplete(result.updatedCards) }, [cards, currentIndex]) // 跳過處理 const handleSkipCard = useCallback(() => { const result = handleSkip(cards, currentIndex) setCards(result.updatedCards) setCurrentIndex(result.nextIndex) checkIfComplete(result.updatedCards) }, [cards, currentIndex]) } ``` --- ## 🎨 **UI/UX規格** ### **翻卡動畫CSS** (您調教過的) ```css /* 3D翻卡動畫 */ .flip-card-container { perspective: 1000px; } .flip-card { transform-style: preserve-3d; transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); } .flip-card.flipped { transform: rotateY(180deg); } .flip-card-front, .flip-card-back { backface-visibility: hidden; position: absolute; width: 100%; height: 100%; } .flip-card-back { transform: rotateY(180deg); } ``` ### **響應式設計規格** ```typescript // 智能高度計算 (您的原設計) const calculateCardHeight = useCallback(() => { if (backRef.current) { const backHeight = backRef.current.scrollHeight const minHeight = window.innerWidth <= 480 ? 300 : window.innerWidth <= 768 ? 350 : 400 return Math.max(minHeight, backHeight) } return 400 }, []) // 信心度按鈕響應式 const buttonLayout = window.innerWidth <= 640 ? 'grid-cols-1 gap-2' // 手機版: 垂直排列 : 'grid-cols-3 gap-3' // 桌面版: 水平排列 ``` --- ## 🔄 **本地存儲設計** (階段2+) ### **進度持久化** ```typescript // localStorage 結構 interface StoredProgress { sessionId: string cards: CardState[] currentIndex: number score: { correct: number; total: number } lastSaveTime: string } // 儲存進度 const saveProgress = (cards: CardState[], currentIndex: number, score: any) => { const progress: StoredProgress = { sessionId: `review_${Date.now()}`, cards, currentIndex, score, lastSaveTime: new Date().toISOString() } localStorage.setItem('review-progress', JSON.stringify(progress)) } // 載入進度 const loadProgress = (): StoredProgress | null => { const saved = localStorage.getItem('review-progress') if (!saved) return null try { const progress = JSON.parse(saved) // 檢查是否是當日進度 (避免過期數據) const saveTime = new Date(progress.lastSaveTime) const now = new Date() const isToday = saveTime.toDateString() === now.toDateString() return isToday ? progress : null } catch { return null } } ``` --- ## 🎯 **路由和導航設計** ### **頁面路由** ```typescript // 路由配置 const reviewRoutes = { main: '/review-simple', // 主複習頁面 maintenance: '/review', // 維護頁面 (舊版本隔離) backup: '/review-old' // 備份頁面 (複雜版本) } // 導航更新 const navigationItems = [ { href: '/dashboard', label: '儀表板' }, { href: '/flashcards', label: '詞卡' }, { href: '/review-simple', label: '複習' }, // 指向可用版本 { href: '/generate', label: 'AI 生成' } ] ``` ### **頁面跳轉邏輯** ```typescript // 會話完成後的跳轉 const handleComplete = () => { setIsComplete(true) // 可選: 3秒後自動跳轉 setTimeout(() => { router.push('/dashboard') }, 3000) } // 中途退出處理 const handleExit = () => { if (window.confirm('確定要退出複習嗎?進度將不會保存。')) { router.push('/flashcards') } } ``` --- ## 📊 **性能優化規格** ### **React性能優化** ```typescript // 使用 memo 避免不必要重渲染 export const SimpleFlipCard = memo(SimpleFlipCardComponent) export const SimpleProgress = memo(SimpleProgressComponent) // useCallback 穩定化函數引用 const handleAnswer = useCallback((confidence: number) => { // ... 邏輯 }, [cards, currentIndex]) // useMemo 緩存計算結果 const sortedCards = useMemo(() => sortCardsByPriority(cards), [cards] ) const currentCard = useMemo(() => cards[currentIndex], [cards, currentIndex] ) ``` ### **載入性能目標** ```typescript // 性能指標 const PERFORMANCE_TARGETS = { INITIAL_LOAD: 1500, // 初始載入 < 1.5秒 CARD_FLIP: 300, // 翻卡動畫 < 300ms SORT_OPERATION: 100, // 排序計算 < 100ms STATE_UPDATE: 50, // 狀態更新 < 50ms NAVIGATION: 200 // 頁面跳轉 < 200ms } // 性能監控 const measurePerformance = (operation: string, fn: Function) => { const start = performance.now() const result = fn() const end = performance.now() console.log(`${operation}: ${end - start}ms`) return result } ``` --- ## 🧪 **測試架構** ### **測試文件結構** ``` __tests__/ ├── delay-counting-system.test.ts # 延遲計數邏輯測試 ├── card-sorting.test.ts # 排序算法測試 ├── confidence-mapping.test.ts # 信心度映射測試 └── components/ ├── SimpleFlipCard.test.tsx # 翻卡組件測試 ├── SimpleProgress.test.tsx # 進度組件測試 └── integration.test.tsx # 完整流程集成測試 ``` ### **Mock數據設計** ```typescript // 測試用的 Mock 數據 export const MOCK_CARDS: CardState[] = [ { id: 'test-1', word: 'evidence', definition: 'facts or information indicating truth', skipCount: 0, wrongCount: 0, successCount: 0, isCompleted: false, originalOrder: 0, delayScore: 0, // ... 其他 API 欄位 }, { id: 'test-2', word: 'priority', definition: 'the fact of being more important', skipCount: 2, wrongCount: 1, successCount: 0, isCompleted: false, originalOrder: 1, delayScore: 3, // ... 其他 API 欄位 } ] ``` --- ## 🔧 **開發工具配置** ### **TypeScript 配置** ```typescript // 嚴格的類型檢查 interface 必須完整定義 Props 必須有明確類型 回調函數必須有返回值類型 狀態更新必須使用正確的類型 // 避免 any 類型 禁止: any, object, Function 建議: 具體的接口定義 ``` ### **ESLint 規則** ```typescript // 代碼品質規則 'react-hooks/exhaustive-deps': 'error' // 確保 useEffect 依賴正確 'react/no-array-index-key': 'warn' // 避免使用 index 作為 key '@typescript-eslint/no-unused-vars': 'error' // 禁止未使用變數 // 複雜度控制 'max-lines': ['error', 200] // 組件最多200行 'max-params': ['error', 5] // 函數最多5個參數 'complexity': ['error', 10] // 圈複雜度最多10 ``` --- ## 📱 **使用者體驗規格** ### **載入狀態處理** ```typescript // 載入狀態 const [isLoading, setIsLoading] = useState(true) // 載入動畫 const LoadingSpinner = () => (
準備詞卡中...
) ``` ### **錯誤處理** ```typescript // 錯誤狀態 const [error, setError] = useState(null) // 錯誤邊界 const ErrorBoundary = ({ error, onRetry }) => (

發生錯誤

{error}

) ``` ### **無障礙設計** ```typescript // 鍵盤操作支援 const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'ArrowLeft': handleSkip(); break case 'ArrowRight': handleAnswer(2); break // 一般 case 'ArrowUp': handleAnswer(3); break // 熟悉 case 'ArrowDown': handleAnswer(1); break // 模糊 case ' ': handleFlip(); break // 空格翻卡 } } // ARIA 標籤 ``` --- ## 📋 **開發檢查清單** ### **組件開發完成標準** - [ ] TypeScript 無錯誤和警告 - [ ] 所有 props 都有預設值或必填檢查 - [ ] 使用 memo/useCallback 優化性能 - [ ] 響應式設計在手機和桌面都正常 - [ ] 無障礙功能完整 (鍵盤、ARIA) - [ ] 錯誤狀態有適當處理 ### **功能測試標準** - [ ] 所有延遲計數測試通過 - [ ] 排序邏輯測試通過 - [ ] 信心度映射測試通過 - [ ] 完整流程集成測試通過 - [ ] 邊界條件測試通過 --- *前端規格維護: 開發團隊* *更新觸發: 產品需求變更或技術實作調整* *目標: 確保前端實作準確性和一致性*