dramaling-vocab-learning/note/複習系統/技術實作規格.md

568 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 複習系統技術實作規格書
**版本**: 1.0
**對應PRD**: 產品需求規格.md
**目標讀者**: 開發者、技術主管
**最後更新**: 2025-10-03
---
## 📊 **核心算法和公式**
### **進度條計算公式**
```typescript
進度百分比 = (今日完成) / (今日完成 + 今日到期) * 100
```
**實作範例**:
```typescript
const progressPercentage = completedToday / (completedToday + dueToday) * 100
```
### **複習間隔算法**
```typescript
下一次複習時間 = 當前時間 + (2^成功複習次數)
```
**實作範例**:
```typescript
const nextReviewDate = new Date()
nextReviewDate.setDate(nextReviewDate.getDate() + Math.pow(2, successCount))
```
---
## 🏷️ **卡片狀態管理機制**
### **延遲計數系統** (前端處理)
**數據結構設計**:
```typescript
interface CardState {
id: string
// 原始卡片數據...
// 前端狀態計數器
skipCount: number // 跳過次數
wrongCount: number // 答錯次數
successCount: number // 答對次數
isCompleted: boolean // 是否已完成
originalOrder: number // 原始順序
}
```
**增加延遲計數的情況**:
1. **用戶點擊跳過**`skipCount++`
2. **用戶答錯題目**`wrongCount++`
3. **效果相同** → 都會降低該卡片的優先級
**前端排序邏輯** (您的需求):
```typescript
const sortCardsByPriority = (cards: 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
})
}
```
**狀態更新邏輯**:
```typescript
// 跳過處理
const handleSkip = (cardIndex: number) => {
setCards(prevCards =>
prevCards.map((card, index) =>
index === cardIndex
? { ...card, skipCount: card.skipCount + 1 }
: card
)
)
// 重新排序並移到下一張
proceedToNext()
}
// 答錯處理
const handleWrongAnswer = (cardIndex: number) => {
setCards(prevCards =>
prevCards.map((card, index) =>
index === cardIndex
? { ...card, wrongCount: card.wrongCount + 1 }
: card
)
)
proceedToNext()
}
// 答對處理 (完成卡片)
const handleCorrectAnswer = (cardIndex: number) => {
setCards(prevCards =>
prevCards.map((card, index) =>
index === cardIndex
? { ...card, isCompleted: true, successCount: card.successCount + 1 }
: card
)
)
proceedToNext()
}
```
---
## 🎯 **測驗模式實作**
### **階段1: 翻卡記憶 (已實作)**
**數據結構**:
```typescript
interface FlashcardData {
id: string
word: string
definition: string
example: string
exampleTranslation: string
pronunciation: string
synonyms: string[]
cefr: string
}
```
**狀態管理**:
```typescript
const [isFlipped, setIsFlipped] = useState(false)
const [selectedConfidence, setSelectedConfidence] = useState<number | null>(null)
const [cardHeight, setCardHeight] = useState<number>(400)
```
**信心度評分映射** (簡化為3選項):
```typescript
const confidenceToScore = {
1: 0, // 模糊 → 答錯
2: 1, // 一般 → 答對
3: 1 // 熟悉 → 答對
}
// 對應的UI配置
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' }
]
```
### **階段2: 詞彙選擇題 (已設計完成)**
**組件架構** (基於您的設計):
```typescript
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}
/>
)
}
```
**問題顯示區域設計**:
```typescript
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>
)
```
**狀態管理設計**:
```typescript
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])
```
**選項生成算法**:
```typescript
// 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組件設計** (您的完整設計):
```typescript
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樣式規格** (您的設計):
```typescript
// 選項按鈕狀態樣式
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組件層次設計**:
```typescript
// 您設計的完整組件結構
<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實作規格**
### **翻卡動畫參數**
```css
/* 3D翻卡動畫 - 已調校的完美參數 */
.flip-card {
perspective: 1000px;
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.flip-card.flipped {
transform: rotateY(180deg);
}
```
### **響應式高度計算**
```typescript
const calculateCardHeight = () => {
const backHeight = backRef.current?.scrollHeight || 0
const minHeight = window.innerWidth <= 480 ? 300 :
window.innerWidth <= 768 ? 350 : 400
return Math.max(minHeight, backHeight)
}
```
### **信心度按鈕配色**
```typescript
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**
```typescript
GET /api/flashcards/due?limit=10
Response:
{
"success": true,
"data": {
"flashcards": FlashcardData[],
"count": number
},
"timestamp": string
}
```
### **記錄複習結果API**
```typescript
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: 內存狀態 (當前)**
```typescript
// 純React狀態會話結束即消失
const [currentIndex, setCurrentIndex] = useState(0)
const [score, setScore] = useState({ correct: 0, total: 0 })
```
### **階段2: 本地存儲 (計劃)**
```typescript
// localStorage持久化
const saveProgress = (progress: ReviewProgress) => {
localStorage.setItem('review-progress', JSON.stringify({
currentIndex,
score,
timestamp: Date.now()
}))
}
```
### **階段3: 後端同步 (遠期)**
```typescript
// 與後端API同步
const syncProgress = async (progress: ReviewProgress) => {
await api.post('/api/user/review-progress', progress)
}
```
---
## 🔒 **業務邏輯約束**
### **狀態轉換規則**
```typescript
// 卡片狀態轉換
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
}
}
```
### **隊列管理邏輯**
```typescript
// 下一張卡片選擇邏輯
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
}
```
---
## ⚡ **性能和優化規格**
### **載入性能要求**
```typescript
// 性能指標
const PERFORMANCE_TARGETS = {
INITIAL_LOAD: 2000, // 初始載入 < 2秒
CARD_FLIP: 500, // 翻卡響應 < 500ms
CONFIDENCE_SELECT: 200, // 按鈕響應 < 200ms
NAVIGATION: 300 // 頁面切換 < 300ms
}
```
### **記憶體管理**
```typescript
// 避免記憶體洩漏
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測試
- **狀態管理**: 狀態轉換測試
### **測試案例範例**
```typescript
// 翻卡邏輯測試
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層
---
*技術實作規格維護者: 開發團隊*
*版本控制: 與產品需求規格同步更新*
*目的: 確保實作準確性避免開發失控*