12 KiB
12 KiB
複習系統技術實作規格書
版本: 1.0 對應PRD: 產品需求規格.md 目標讀者: 開發者、技術主管 最後更新: 2025-10-03
📊 核心算法和公式
進度條計算公式
進度百分比 = (今日完成) / (今日完成 + 今日到期) * 100
實作範例:
const progressPercentage = completedToday / (completedToday + dueToday) * 100
複習間隔算法
下一次複習時間 = 當前時間 + (2^成功複習次數) 天
實作範例:
const nextReviewDate = new Date()
nextReviewDate.setDate(nextReviewDate.getDate() + Math.pow(2, successCount))
🏷️ 卡片狀態管理機制
延遲註記系統
新增延遲註記的情況:
- 用戶點選 "跳過" → 卡片標記
isDelayed: true - 用戶答錯題目 → 卡片標記
isDelayed: true
延遲註記的影響:
// 撈取下一個複習卡片時排除延遲卡片
const nextCards = allCards.filter(card => !card.isDelayed)
消除延遲註記:
// 用戶答對題目時
if (isCorrect) {
card.isDelayed = false
card.successCount++
card.nextReviewDate = calculateNextReviewDate(card.successCount)
}
🎯 測驗模式實作
階段1: 翻卡記憶 (已實作)
數據結構:
interface FlashcardData {
id: string
word: string
definition: string
example: string
exampleTranslation: string
pronunciation: string
synonyms: string[]
cefr: string
}
狀態管理:
const [isFlipped, setIsFlipped] = useState(false)
const [selectedConfidence, setSelectedConfidence] = useState<number | null>(null)
const [cardHeight, setCardHeight] = useState<number>(400)
信心度評分映射:
const confidenceToScore = {
1: 0, // 完全不懂 → 答錯
2: 0, // 模糊 → 答錯
3: 1, // 一般 → 答對
4: 1, // 熟悉 → 答對
5: 1 // 非常熟悉 → 答對
}
階段2: 詞彙選擇題 (已設計完成)
組件架構 (基於您的設計):
interface VocabChoiceTestProps {
cardData: FlashcardData
options: string[] // 4選1選項
onAnswer: (answer: string) => void
onReportError: () => void
disabled?: boolean
}
// 組件分區設計
const VocabChoiceTest = () => {
const questionArea = // 問題顯示區域
const optionsArea = // 選項網格區域
const resultArea = // 結果顯示區域
return (
<ChoiceTestContainer
questionArea={questionArea}
optionsArea={optionsArea}
resultArea={resultArea}
/>
)
}
問題顯示區域設計:
const questionArea = (
<div className="text-center">
<div className="bg-gray-50 rounded-lg p-6">
<h3 className="font-semibold text-gray-900 mb-3 text-left">定義</h3>
<p className="text-gray-700 text-left text-lg leading-relaxed">
{cardData.definition}
</p>
</div>
<p className="text-lg text-gray-700 mt-4 text-left">
請選擇符合上述定義的英文詞彙:
</p>
</div>
)
狀態管理設計:
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null)
const [showResult, setShowResult] = useState(false)
// 答案驗證邏輯
const isCorrect = useMemo(() =>
selectedAnswer === cardData.word,
[selectedAnswer, cardData.word]
)
// 選擇處理
const handleAnswerSelect = useCallback((answer: string) => {
if (disabled || showResult) return
setSelectedAnswer(answer)
setShowResult(true)
onAnswer(answer)
}, [disabled, showResult, onAnswer])
選項生成算法:
// MVP階段: 固定選項
const generateSimpleOptions = (correctWord: string): string[] => {
const fixedDistractors = ['apple', 'orange', 'banana']
return shuffle([correctWord, ...fixedDistractors])
}
// 階段3升級方案: 智能干擾項
const generateSmartOptions = (correctWord: Flashcard, allWords: Flashcard[]): string[] => {
const samePOS = allWords.filter(w => w.partOfSpeech === correctWord.partOfSpeech)
const sameCEFR = allWords.filter(w => w.cefr === correctWord.cefr)
const distractors = selectRandom([...samePOS, ...sameCEFR], 3)
return shuffle([correctWord.word, ...distractors.map(w => w.word)])
}
ChoiceGrid組件設計 (您的完整設計):
interface ChoiceGridProps {
options: string[] // 4個選項
selectedOption?: string | null
correctAnswer?: string
showResult?: boolean // 控制結果顯示
onSelect: (option: string) => void
disabled?: boolean
className?: string
}
// 響應式網格布局
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{options.map((option, index) => (
<ChoiceOption
key={`${option}-${index}`}
option={option}
index={index}
isSelected={selectedOption === option}
isCorrect={showResult && option === correctAnswer}
isIncorrect={showResult && option !== correctAnswer}
showResult={showResult}
onSelect={onSelect}
disabled={disabled}
/>
))}
</div>
ChoiceOption樣式規格 (您的設計):
// 選項按鈕狀態樣式
const getOptionStyles = (isSelected, isCorrect, isIncorrect, showResult) => {
if (showResult) {
if (isCorrect) return 'border-green-500 bg-green-50 text-green-700'
if (isIncorrect && isSelected) return 'border-red-500 bg-red-50 text-red-700'
return 'border-gray-200 bg-gray-50 text-gray-500'
}
if (isSelected) return 'border-blue-500 bg-blue-50 text-blue-700'
return 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
}
// 按鈕基礎樣式
className={`p-4 text-center rounded-lg border-2 transition-all ${getOptionStyles()}`}
UI組件層次設計:
// 您設計的完整組件結構
<ChoiceTestContainer> // 外層容器和錯誤處理
<TestHeader title="詞彙選擇" cefr={cardData.cefr} />
<QuestionArea> // 問題顯示區域
<div className="bg-gray-50 rounded-lg p-6">
<h3>定義</h3>
<p className="text-lg leading-relaxed">{definition}</p>
</div>
<p className="text-lg mt-4">請選擇符合上述定義的英文詞彙:</p>
</QuestionArea>
<OptionsArea> // 選項區域
<ChoiceGrid // 響應式2x2網格
options={options}
selectedOption={selectedAnswer}
correctAnswer={cardData.word}
showResult={showResult}
onSelect={handleAnswerSelect}
disabled={disabled}
/>
</OptionsArea>
<ResultArea> // 結果顯示區域
{showResult && (
<TestResultDisplay
isCorrect={isCorrect}
correctAnswer={cardData.word}
userAnswer={selectedAnswer}
pronunciation={cardData.pronunciation}
example={cardData.example}
/>
)}
</ResultArea>
</ChoiceTestContainer>
🎨 UI/UX實作規格
翻卡動畫參數
/* 3D翻卡動畫 - 已調校的完美參數 */
.flip-card {
perspective: 1000px;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.flip-card.flipped {
transform: rotateY(180deg);
}
響應式高度計算
const calculateCardHeight = () => {
const backHeight = backRef.current?.scrollHeight || 0
const minHeight = window.innerWidth <= 480 ? 300 :
window.innerWidth <= 768 ? 350 : 400
return Math.max(minHeight, backHeight)
}
信心度按鈕配色
const confidenceColors = {
1: 'bg-red-100 text-red-700 border-red-200', // 完全不懂
2: 'bg-orange-100 text-orange-700 border-orange-200', // 模糊
3: 'bg-yellow-100 text-yellow-700 border-yellow-200', // 一般
4: 'bg-blue-100 text-blue-700 border-blue-200', // 熟悉
5: 'bg-green-100 text-green-700 border-green-200' // 非常熟悉
}
📱 API設計規格 (階段3實作)
獲取複習卡片API
GET /api/flashcards/due?limit=10
Response:
{
"success": true,
"data": {
"flashcards": FlashcardData[],
"count": number
},
"timestamp": string
}
記錄複習結果API
POST /api/flashcards/{id}/review
Request:
{
"confidence": number, // 1-5
"isCorrect": boolean, // 基於confidence >= 3判斷
"reviewType": "flip-memory",
"responseTimeMs": number
}
Response:
{
"success": boolean,
"nextReviewDate": string,
"newSuccessCount": number
}
🗄️ 數據存儲設計
階段1: 內存狀態 (當前)
// 純React狀態,會話結束即消失
const [currentIndex, setCurrentIndex] = useState(0)
const [score, setScore] = useState({ correct: 0, total: 0 })
階段2: 本地存儲 (計劃)
// localStorage持久化
const saveProgress = (progress: ReviewProgress) => {
localStorage.setItem('review-progress', JSON.stringify({
currentIndex,
score,
timestamp: Date.now()
}))
}
階段3: 後端同步 (遠期)
// 與後端API同步
const syncProgress = async (progress: ReviewProgress) => {
await api.post('/api/user/review-progress', progress)
}
🔒 業務邏輯約束
狀態轉換規則
// 卡片狀態轉換
enum CardState {
PENDING = 'pending', // 未開始
COMPLETED = 'completed', // 已完成
DELAYED = 'delayed' // 延遲 (跳過或答錯)
}
// 狀態轉換邏輯
const handleAnswer = (confidence: number) => {
if (confidence >= 3) {
// 答對: 移除延遲標記,標記完成
card.state = CardState.COMPLETED
card.isDelayed = false
card.successCount++
} else {
// 答錯: 添加延遲標記
card.isDelayed = true
card.state = CardState.DELAYED
}
}
隊列管理邏輯
// 下一張卡片選擇邏輯
const getNextCard = (cards: FlashCard[]) => {
// 1. 優先選擇未延遲的卡片
const normalCards = cards.filter(c => !c.isDelayed && c.state === CardState.PENDING)
if (normalCards.length > 0) {
return normalCards[0]
}
// 2. 如果沒有正常卡片,選擇延遲卡片
const delayedCards = cards.filter(c => c.isDelayed)
return delayedCards[0] || null
}
⚡ 性能和優化規格
載入性能要求
// 性能指標
const PERFORMANCE_TARGETS = {
INITIAL_LOAD: 2000, // 初始載入 < 2秒
CARD_FLIP: 500, // 翻卡響應 < 500ms
CONFIDENCE_SELECT: 200, // 按鈕響應 < 200ms
NAVIGATION: 300 // 頁面切換 < 300ms
}
記憶體管理
// 避免記憶體洩漏
useEffect(() => {
const timer = setTimeout(updateHeight, 100)
const resizeHandler = () => updateHeight()
window.addEventListener('resize', resizeHandler)
return () => {
clearTimeout(timer)
window.removeEventListener('resize', resizeHandler)
}
}, [dependencies])
🧪 測試規格 (TDD Implementation)
測試覆蓋要求
- 核心邏輯: 100% 測試覆蓋
- UI組件: 關鍵交互測試
- API調用: Mock測試
- 狀態管理: 狀態轉換測試
測試案例範例
// 翻卡邏輯測試
describe('FlipCard Component', () => {
test('應該在點擊時切換翻轉狀態', () => {
const { result } = renderHook(() => useFlipCard())
act(() => result.current.flip())
expect(result.current.isFlipped).toBe(true)
})
test('應該在選擇信心度時觸發提交', () => {
const onSubmit = jest.fn()
const card = render(<FlipCard onSubmit={onSubmit} />)
card.selectConfidence(4)
expect(onSubmit).toHaveBeenCalledWith(4)
})
})
📋 實作檢查清單
每個功能完成時檢查
- 功能符合產品需求規格
- 遵循技術約束和算法
- 通過所有相關測試
- 性能指標達標
- 無記憶體洩漏
- 錯誤處理完善
代碼品質標準
- TypeScript 無錯誤
- ESLint 無警告
- 組件 < 200行
- 函數 < 20行
- 嵌套層次 < 3層
技術實作規格維護者: 開發團隊 版本控制: 與產品需求規格同步更新 目的: 確保實作準確性,避免開發失控