353 lines
8.1 KiB
Markdown
353 lines
8.1 KiB
Markdown
# 複習系統技術實作規格書
|
||
|
||
**版本**: 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))
|
||
```
|
||
|
||
---
|
||
|
||
## 🏷️ **卡片狀態管理機制**
|
||
|
||
### **延遲註記系統**
|
||
|
||
**新增延遲註記的情況**:
|
||
1. 用戶點選 "跳過" → 卡片標記 `isDelayed: true`
|
||
2. 用戶答錯題目 → 卡片標記 `isDelayed: true`
|
||
|
||
**延遲註記的影響**:
|
||
```typescript
|
||
// 撈取下一個複習卡片時排除延遲卡片
|
||
const nextCards = allCards.filter(card => !card.isDelayed)
|
||
```
|
||
|
||
**消除延遲註記**:
|
||
```typescript
|
||
// 用戶答對題目時
|
||
if (isCorrect) {
|
||
card.isDelayed = false
|
||
card.successCount++
|
||
card.nextReviewDate = calculateNextReviewDate(card.successCount)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 **測驗模式實作**
|
||
|
||
### **階段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)
|
||
```
|
||
|
||
**信心度評分映射**:
|
||
```typescript
|
||
const confidenceToScore = {
|
||
1: 0, // 完全不懂 → 答錯
|
||
2: 0, // 模糊 → 答錯
|
||
3: 1, // 一般 → 答對
|
||
4: 1, // 熟悉 → 答對
|
||
5: 1 // 非常熟悉 → 答對
|
||
}
|
||
```
|
||
|
||
### **階段2: 詞彙選擇題 (計劃中)**
|
||
|
||
**選項生成算法**:
|
||
```typescript
|
||
// 暫時使用固定選項 (MVP階段)
|
||
const mockOptions = ['apple', 'orange', 'banana', correctAnswer]
|
||
const shuffledOptions = shuffle(mockOptions)
|
||
```
|
||
|
||
**未來升級方案**:
|
||
```typescript
|
||
// 基於詞性和CEFR等級生成干擾項
|
||
const generateDistractors = (correctWord, allWords) => {
|
||
const samePOS = allWords.filter(w => w.partOfSpeech === correctWord.partOfSpeech)
|
||
const sameCEFR = allWords.filter(w => w.cefr === correctWord.cefr)
|
||
return selectRandom([...samePOS, ...sameCEFR], 3)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎨 **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層
|
||
|
||
---
|
||
|
||
*技術實作規格維護者: 開發團隊*
|
||
*版本控制: 與產品需求規格同步更新*
|
||
*目的: 確保實作準確性,避免開發失控* |