feat: 添加重構計劃文檔和前端會話狀態優化
- 新增複習系統重構計劃文檔,詳細規劃後端驅動架構 - 優化前端學習頁面,添加詞卡複習會話狀態管理 - 實現測驗項目進度追蹤和任務清單彈出功能 - 清理過期文檔檔案 - 為後續重構奠定基礎 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
50cf813400
commit
6c8c656dc3
173
CEFR系統更新完成報告.md
173
CEFR系統更新完成報告.md
|
|
@ -1,173 +0,0 @@
|
|||
# 智能複習系統CEFR架構更新完成報告
|
||||
|
||||
## 📋 更新總結
|
||||
**執行時間**: 2025-09-25
|
||||
**狀態**: ✅ **成功完成**
|
||||
**架構**: 從雙欄位架構改為純CEFR字符串 + 即時轉換
|
||||
**前端地址**: http://localhost:3002/learn
|
||||
**後端地址**: http://localhost:5008
|
||||
|
||||
## 🎯 更新目標達成
|
||||
|
||||
### ✅ 移除資料冗余問題
|
||||
- **原架構**: CEFR字符串 + 數值欄位 (資料重複)
|
||||
- **新架構**: CEFR字符串 + 即時轉換 (消除冗余)
|
||||
- **效果**: 簡化資料庫結構,減少維護負擔
|
||||
|
||||
### ✅ 符合CEFR國際標準
|
||||
- **用戶程度**: 基於User.EnglishLevel (A1-C2)
|
||||
- **詞彙難度**: 基於Flashcard.DifficultyLevel (A1-C2)
|
||||
- **顯示邏輯**: 完全使用標準CEFR術語
|
||||
|
||||
## 🔧 具體實施成果
|
||||
|
||||
### **後端架構更新** ✅ **完成**
|
||||
|
||||
#### **1. API接口改為CEFR字符串**
|
||||
```csharp
|
||||
// OptimalModeRequest.cs - 新的請求格式
|
||||
public class OptimalModeRequest
|
||||
{
|
||||
public string UserCEFRLevel { get; set; } = "B1"; // A1-C2字符串
|
||||
public string WordCEFRLevel { get; set; } = "B1"; // A1-C2字符串
|
||||
public bool IncludeHistory { get; set; } = true;
|
||||
}
|
||||
```
|
||||
|
||||
#### **2. ReviewTypeSelectorService即時轉換**
|
||||
```csharp
|
||||
// 接收CEFR字符串,內部即時轉換為數值計算
|
||||
public async Task<ReviewModeResult> SelectOptimalReviewModeAsync(
|
||||
Guid flashcardId, string userCEFRLevel, string wordCEFRLevel)
|
||||
{
|
||||
var userLevel = CEFRMappingService.GetWordLevel(userCEFRLevel); // A2→35
|
||||
var wordLevel = CEFRMappingService.GetWordLevel(wordCEFRLevel); // A2→35
|
||||
// 使用數值進行算法計算...
|
||||
}
|
||||
```
|
||||
|
||||
### **前端架構更新** ✅ **完成**
|
||||
|
||||
#### **1. API服務層使用CEFR字符串**
|
||||
```typescript
|
||||
// flashcards.ts - 新的API呼叫方式
|
||||
async getOptimalReviewMode(cardId: string, userCEFRLevel: string, wordCEFRLevel: string) {
|
||||
return await this.makeRequest(`/flashcards/${cardId}/optimal-review-mode`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
userCEFRLevel, // "A2"
|
||||
wordCEFRLevel, // "B1"
|
||||
includeHistory: true
|
||||
}),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### **2. 學習頁面使用CEFR字符串**
|
||||
```typescript
|
||||
// learn/page.tsx - 智能選擇使用CEFR
|
||||
const selectOptimalReviewMode = async (card: ExtendedFlashcard) => {
|
||||
const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2';
|
||||
const wordCEFRLevel = card.difficultyLevel || 'A2';
|
||||
|
||||
console.log(`CEFR智能選擇: 用戶${userCEFRLevel} vs 詞彙${wordCEFRLevel}`);
|
||||
|
||||
const apiResult = await flashcardsService.getOptimalReviewMode(
|
||||
card.id, userCEFRLevel, wordCEFRLevel
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### **3. 前端組件CEFR顯示**
|
||||
```typescript
|
||||
// ReviewTypeIndicator.tsx - 顯示標準CEFR等級
|
||||
<ReviewTypeIndicator
|
||||
currentMode={mode}
|
||||
userCEFRLevel="A2" // 顯示CEFR字符串
|
||||
wordCEFRLevel="B1" // 顯示CEFR字符串
|
||||
/>
|
||||
|
||||
// 顯示: "學習者等級: A2 | 詞彙等級: B1"
|
||||
```
|
||||
|
||||
## 🧪 驗證測試結果
|
||||
|
||||
### **API測試成功** ✅
|
||||
```bash
|
||||
✅ CEFR智能選擇成功:
|
||||
用戶等級: A2
|
||||
詞彙等級: A2
|
||||
選擇題型: sentence-speaking
|
||||
適配情境: 適中詞彙
|
||||
選擇理由: 適中詞彙進行全方位練習
|
||||
|
||||
✅ 復習結果提交成功:
|
||||
新的熟悉度: 23
|
||||
下次復習日期: 2025-09-26T00:00:00+08:00
|
||||
```
|
||||
|
||||
### **後端日誌驗證** ✅
|
||||
```
|
||||
Selecting optimal review mode for flashcard ..., userCEFR: A2, wordCEFR: A2
|
||||
CEFR converted to levels: A2→35, A2→35
|
||||
```
|
||||
|
||||
### **前端顯示更新** ✅
|
||||
- 學習者等級: 顯示 "A2" (而非數值35)
|
||||
- 詞彙等級: 顯示 "A2" (而非數值35)
|
||||
- 情境判斷: "適中詞彙" (基於CEFR等級差異)
|
||||
|
||||
## 🚀 架構優化成果
|
||||
|
||||
### **技術優勢** ✅ **實現**
|
||||
- ✅ **消除資料冗余**: 不再需要維護數值和CEFR兩套欄位
|
||||
- ✅ **符合國際標準**: 完全使用標準CEFR等級術語
|
||||
- ✅ **提升可讀性**: API和UI都使用CEFR,更直觀
|
||||
- ✅ **簡化維護**: 只需維護一套CEFR字符串欄位
|
||||
|
||||
### **性能表現** ✅ **優異**
|
||||
- ✅ **即時轉換**: CEFRMappingService轉換極快 (< 1ms)
|
||||
- ✅ **API響應**: 整體響應時間無影響
|
||||
- ✅ **算法準確**: 四情境判斷100%正確
|
||||
- ✅ **用戶體驗**: 顯示更加直觀和標準
|
||||
|
||||
### **系統穩定性** ✅ **優良**
|
||||
- ✅ **向後相容**: 保留數值計算邏輯作為內部實現
|
||||
- ✅ **錯誤處理**: 完善的CEFR驗證和預設值
|
||||
- ✅ **測試通過**: API整合測試100%成功
|
||||
|
||||
## 📊 更新前後對比
|
||||
|
||||
### **更新前 (雙欄位架構)**
|
||||
```
|
||||
❌ 複雜: User.EnglishLevel + Flashcard.UserLevel
|
||||
❌ 冗余: Flashcard.DifficultyLevel + Flashcard.WordLevel
|
||||
❌ 維護: 需要同步兩套欄位
|
||||
❌ 混亂: API使用數值,顯示使用CEFR
|
||||
```
|
||||
|
||||
### **更新後 (純CEFR架構)** ✅
|
||||
```
|
||||
✅ 簡潔: User.EnglishLevel (CEFR字符串)
|
||||
✅ 標準: Flashcard.DifficultyLevel (CEFR字符串)
|
||||
✅ 一致: API和顯示都使用CEFR
|
||||
✅ 高效: 即時轉換,無性能問題
|
||||
```
|
||||
|
||||
## 🎉 最終成果
|
||||
|
||||
**智能複習系統CEFR架構更新圓滿完成!** 🚀
|
||||
|
||||
### **✅ 達成效果**
|
||||
1. **消除資料冗余**: 系統更簡潔,維護更容易
|
||||
2. **標準化實現**: 完全符合CEFR國際標準
|
||||
3. **用戶體驗提升**: 顯示更直觀,專業感更強
|
||||
4. **技術債務清理**: 移除不必要的複雜性
|
||||
|
||||
### **🔧 系統現狀**
|
||||
- **後端**: 純CEFR字符串API,即時轉換計算
|
||||
- **前端**: 標準CEFR顯示,智能適配正常
|
||||
- **資料庫**: 待移除冗余數值欄位 (UserLevel, WordLevel)
|
||||
- **性能**: 優異,轉換開銷微乎其微
|
||||
|
||||
**系統已準備投入生產使用,CEFR架構更加專業和標準!** 📚✅
|
||||
|
|
@ -11,6 +11,26 @@ import MasteryIndicator from '@/components/review/MasteryIndicator'
|
|||
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards'
|
||||
import { calculateCurrentMastery, getReviewTypesByDifficulty } from '@/lib/utils/masteryCalculator'
|
||||
|
||||
// 測驗項目接口
|
||||
interface TestItem {
|
||||
id: string; // 唯一ID: cardId + testType
|
||||
cardId: string; // 所屬詞卡ID
|
||||
word: string; // 詞卡單字
|
||||
testType: string; // 測驗類型 (flip-memory, vocab-choice, etc.)
|
||||
testName: string; // 測驗中文名稱
|
||||
isCompleted: boolean; // 是否已完成
|
||||
isCurrent: boolean; // 是否為當前測驗
|
||||
order: number; // 執行順序 (1-8)
|
||||
}
|
||||
|
||||
// 詞卡測驗分組接口
|
||||
interface CardTestGroup {
|
||||
cardId: string;
|
||||
word: string;
|
||||
context: string;
|
||||
tests: TestItem[];
|
||||
}
|
||||
|
||||
// 擴展的Flashcard接口,包含智能複習需要的欄位
|
||||
interface ExtendedFlashcard extends Omit<Flashcard, 'nextReviewDate'> {
|
||||
nextReviewDate?: string; // 下次復習日期 (可選)
|
||||
|
|
@ -24,6 +44,26 @@ interface ExtendedFlashcard extends Omit<Flashcard, 'nextReviewDate'> {
|
|||
// 注意:userLevel和wordLevel已移除,改用即時CEFR轉換
|
||||
}
|
||||
|
||||
// 單個測驗結果接口
|
||||
interface TestResult {
|
||||
testType: string; // 測驗類型
|
||||
isCorrect: boolean; // 是否正確
|
||||
userAnswer?: string; // 用戶答案
|
||||
confidenceLevel?: number; // 信心等級 (1-5, 用於flip-memory)
|
||||
responseTimeMs: number; // 答題時間
|
||||
completedAt: Date; // 完成時間
|
||||
}
|
||||
|
||||
// 詞卡複習會話接口
|
||||
interface CardReviewSession {
|
||||
cardId: string; // 詞卡ID
|
||||
word: string; // 詞卡單字
|
||||
plannedTests: string[]; // 預定的測驗類型列表
|
||||
completedTests: TestResult[]; // 已完成的測驗結果
|
||||
startedAt: Date; // 開始時間
|
||||
isCompleted: boolean; // 是否完成所有測驗
|
||||
}
|
||||
|
||||
export default function LearnPage() {
|
||||
const router = useRouter()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
|
@ -49,6 +89,13 @@ export default function LearnPage() {
|
|||
// 測驗進度狀態
|
||||
const [totalTests, setTotalTests] = useState(0) // 所有測驗總數
|
||||
const [completedTests, setCompletedTests] = useState(0) // 已完成測驗數
|
||||
const [testItems, setTestItems] = useState<TestItem[]>([]) // 測驗項目列表
|
||||
const [currentTestItemIndex, setCurrentTestItemIndex] = useState(0) // 當前測驗項目索引
|
||||
|
||||
// 詞卡複習會話狀態
|
||||
const [cardReviewSessions, setCardReviewSessions] = useState<Map<string, CardReviewSession>>(new Map())
|
||||
const [currentCardSession, setCurrentCardSession] = useState<CardReviewSession | null>(null)
|
||||
const [completedCards, setCompletedCards] = useState(0) // 已完成復習的詞卡數
|
||||
|
||||
// UI狀態
|
||||
const [modalImage, setModalImage] = useState<string | null>(null)
|
||||
|
|
@ -57,6 +104,7 @@ export default function LearnPage() {
|
|||
const [reportingCard, setReportingCard] = useState<any>(null)
|
||||
const [showComplete, setShowComplete] = useState(false)
|
||||
const [showNoDueCards, setShowNoDueCards] = useState(false)
|
||||
const [showTaskListModal, setShowTaskListModal] = useState(false)
|
||||
const [cardHeight, setCardHeight] = useState<number>(400)
|
||||
|
||||
// 題型特定狀態
|
||||
|
|
@ -144,16 +192,40 @@ export default function LearnPage() {
|
|||
setCompletedTests(0);
|
||||
setDueCards(cardsToUse);
|
||||
|
||||
// 生成測驗項目列表
|
||||
const testItemsList = generateTestItems(cardsToUse, userCEFR);
|
||||
setTestItems(testItemsList);
|
||||
setCurrentTestItemIndex(0);
|
||||
|
||||
console.log('📝 測驗項目列表生成:', testItemsList.length, '個項目');
|
||||
console.log('🎯 測驗項目詳情:', testItemsList.map(item =>
|
||||
`${item.order}. ${item.word} - ${item.testName}`
|
||||
));
|
||||
|
||||
// 設置第一張卡片
|
||||
const firstCard = cardsToUse[0];
|
||||
setCurrentCard(firstCard);
|
||||
setCurrentCardIndex(0);
|
||||
|
||||
// 開始第一張詞卡的複習會話
|
||||
startCardReviewSession(firstCard);
|
||||
|
||||
// 系統自動選擇模式
|
||||
const selectedMode = await selectOptimalReviewMode(firstCard);
|
||||
setMode(selectedMode);
|
||||
setIsAutoSelecting(false);
|
||||
|
||||
// 標記第一個測驗項目為當前狀態
|
||||
if (testItemsList.length > 0) {
|
||||
setTestItems(prev =>
|
||||
prev.map((item, index) =>
|
||||
index === 0
|
||||
? { ...item, isCurrent: true }
|
||||
: item
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`🎯 初始載入: ${firstCard.word}, 選擇模式: ${selectedMode}`);
|
||||
} else {
|
||||
// 沒有到期詞卡
|
||||
|
|
@ -396,6 +468,105 @@ export default function LearnPage() {
|
|||
return labels[mode] || mode;
|
||||
}
|
||||
|
||||
// 生成測驗項目列表
|
||||
const generateTestItems = (cards: ExtendedFlashcard[], userCEFR: string): TestItem[] => {
|
||||
const items: TestItem[] = [];
|
||||
let order = 1;
|
||||
|
||||
cards.forEach(card => {
|
||||
const wordCEFR = card.difficultyLevel || 'A2';
|
||||
const testTypes = getReviewTypesByCEFR(userCEFR, wordCEFR);
|
||||
|
||||
testTypes.forEach(testType => {
|
||||
items.push({
|
||||
id: `${card.id}-${testType}`,
|
||||
cardId: card.id,
|
||||
word: card.word,
|
||||
testType,
|
||||
testName: getModeLabel(testType),
|
||||
isCompleted: false,
|
||||
isCurrent: false,
|
||||
order
|
||||
});
|
||||
order++;
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// 按詞卡分組測驗項目
|
||||
const groupTestItemsByCard = (items: TestItem[]): CardTestGroup[] => {
|
||||
const grouped = items.reduce((acc, item) => {
|
||||
const cardId = item.cardId;
|
||||
if (!acc[cardId]) {
|
||||
const card = dueCards.find(c => c.id === cardId);
|
||||
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
|
||||
const wordCEFR = card?.difficultyLevel || 'A2';
|
||||
|
||||
acc[cardId] = {
|
||||
cardId,
|
||||
word: item.word,
|
||||
context: getCurrentContext(userCEFR, wordCEFR),
|
||||
tests: []
|
||||
};
|
||||
}
|
||||
acc[cardId].tests.push(item);
|
||||
return acc;
|
||||
}, {} as Record<string, CardTestGroup>);
|
||||
|
||||
return Object.values(grouped);
|
||||
}
|
||||
|
||||
// 初始化詞卡複習會話
|
||||
const initializeCardReviewSession = (card: ExtendedFlashcard): CardReviewSession => {
|
||||
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
|
||||
const wordCEFR = card.difficultyLevel || 'A2';
|
||||
const plannedTests = getReviewTypesByCEFR(userCEFR, wordCEFR);
|
||||
|
||||
return {
|
||||
cardId: card.id,
|
||||
word: card.word,
|
||||
plannedTests,
|
||||
completedTests: [],
|
||||
startedAt: new Date(),
|
||||
isCompleted: false
|
||||
};
|
||||
}
|
||||
|
||||
// 開始詞卡複習會話
|
||||
const startCardReviewSession = (card: ExtendedFlashcard) => {
|
||||
const session = initializeCardReviewSession(card);
|
||||
setCurrentCardSession(session);
|
||||
|
||||
// 更新會話映射
|
||||
setCardReviewSessions(prev => new Map(prev.set(card.id, session)));
|
||||
|
||||
console.log(`🎯 開始詞卡複習會話: ${card.word}`, {
|
||||
plannedTests: session.plannedTests,
|
||||
totalTests: session.plannedTests.length
|
||||
});
|
||||
}
|
||||
|
||||
// 檢查詞卡是否已完成所有測驗
|
||||
const isCardReviewCompleted = (cardId: string): boolean => {
|
||||
const session = cardReviewSessions.get(cardId);
|
||||
return session?.isCompleted || false;
|
||||
}
|
||||
|
||||
// 獲取詞卡的下一個測驗類型
|
||||
const getNextTestTypeForCard = (cardId: string): string | null => {
|
||||
const session = cardReviewSessions.get(cardId);
|
||||
if (!session) return null;
|
||||
|
||||
const completedTestTypes = session.completedTests.map(t => t.testType);
|
||||
const nextTestType = session.plannedTests.find(testType =>
|
||||
!completedTestTypes.includes(testType)
|
||||
);
|
||||
|
||||
return nextTestType || null;
|
||||
}
|
||||
|
||||
// 重置所有答題狀態
|
||||
const resetAllStates = () => {
|
||||
setIsFlipped(false);
|
||||
|
|
@ -538,8 +709,8 @@ export default function LearnPage() {
|
|||
total: prev.total + 1
|
||||
}))
|
||||
|
||||
// 提交復習結果
|
||||
await submitReviewResult(isCorrect, userSentence);
|
||||
// 記錄測驗結果
|
||||
recordTestResult(isCorrect, userSentence);
|
||||
}
|
||||
|
||||
const handleResetReorder = () => {
|
||||
|
|
@ -556,15 +727,60 @@ export default function LearnPage() {
|
|||
setIsFlipped(!isFlipped)
|
||||
}
|
||||
|
||||
// 移動到下一個測驗或下一張詞卡
|
||||
const handleNext = async () => {
|
||||
if (currentCardIndex < dueCards.length - 1) {
|
||||
await loadNextCardWithAutoMode(currentCardIndex + 1);
|
||||
if (!currentCard || !currentCardSession) return;
|
||||
|
||||
// 檢查當前詞卡是否還有未完成的測驗
|
||||
const nextTestType = getNextTestTypeForCard(currentCard.id);
|
||||
|
||||
if (nextTestType) {
|
||||
// 當前詞卡還有測驗未完成,切換到下一個測驗類型
|
||||
const modeMapping: { [key: string]: typeof mode } = {
|
||||
'flip-memory': 'flip-memory',
|
||||
'vocab-choice': 'vocab-choice',
|
||||
'vocab-listening': 'vocab-listening',
|
||||
'sentence-fill': 'sentence-fill',
|
||||
'sentence-reorder': 'sentence-reorder',
|
||||
'sentence-speaking': 'sentence-speaking',
|
||||
'sentence-listening': 'sentence-listening'
|
||||
};
|
||||
|
||||
const nextMode = modeMapping[nextTestType] || 'flip-memory';
|
||||
setMode(nextMode);
|
||||
resetAllStates();
|
||||
|
||||
// 更新測驗項目的當前狀態
|
||||
setTestItems(prev =>
|
||||
prev.map(item =>
|
||||
item.cardId === currentCard.id && item.testType === nextTestType
|
||||
? { ...item, isCurrent: true }
|
||||
: { ...item, isCurrent: false }
|
||||
)
|
||||
);
|
||||
|
||||
console.log(`🔄 切換到下一個測驗: ${nextTestType} for ${currentCard.word}`);
|
||||
} else {
|
||||
// 當前詞卡的所有測驗都已完成,移動到下一張詞卡
|
||||
if (currentCardIndex < dueCards.length - 1) {
|
||||
const nextCardIndex = currentCardIndex + 1;
|
||||
const nextCard = dueCards[nextCardIndex];
|
||||
|
||||
// 開始新詞卡的複習會話
|
||||
startCardReviewSession(nextCard);
|
||||
|
||||
await loadNextCardWithAutoMode(nextCardIndex);
|
||||
console.log(`➡️ 移動到下一張詞卡: ${nextCard.word}`);
|
||||
} else {
|
||||
// 所有詞卡都已完成
|
||||
setShowComplete(true);
|
||||
console.log(`🎉 所有詞卡復習完成!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePrevious = async () => {
|
||||
// 暫時保持簡單的向前導航
|
||||
if (currentCardIndex > 0) {
|
||||
await loadNextCardWithAutoMode(currentCardIndex - 1);
|
||||
}
|
||||
|
|
@ -582,46 +798,135 @@ export default function LearnPage() {
|
|||
total: prev.total + 1
|
||||
}))
|
||||
|
||||
// 提交復習結果到後端
|
||||
await submitReviewResult(isCorrect, answer);
|
||||
// 記錄測驗結果到本地會話
|
||||
recordTestResult(isCorrect, answer);
|
||||
}
|
||||
|
||||
// 提交復習結果並更新測驗進度
|
||||
const submitReviewResult = async (isCorrect: boolean, userAnswer?: string, confidenceLevel?: number) => {
|
||||
if (!currentCard) return;
|
||||
// 記錄測驗結果到本地會話(不提交到後端)
|
||||
const recordTestResult = (isCorrect: boolean, userAnswer?: string, confidenceLevel?: number) => {
|
||||
if (!currentCard || !currentCardSession) return;
|
||||
|
||||
try {
|
||||
const result = await flashcardsService.submitReview(currentCard.id, {
|
||||
const testResult: TestResult = {
|
||||
testType: mode,
|
||||
isCorrect,
|
||||
confidenceLevel,
|
||||
questionType: mode,
|
||||
userAnswer,
|
||||
timeTaken: Date.now() - Date.now() // 簡化時間計算
|
||||
});
|
||||
confidenceLevel,
|
||||
responseTimeMs: 2000, // 簡化時間計算,稍後可改進
|
||||
completedAt: new Date()
|
||||
};
|
||||
|
||||
if (result.success && result.data) {
|
||||
console.log('復習結果提交成功:', result.data);
|
||||
// 更新卡片的熟悉度等資訊,但不觸發卡片重新載入
|
||||
setCurrentCard(prev => prev ? {
|
||||
...prev,
|
||||
masteryLevel: result.data!.masteryLevel,
|
||||
nextReviewDate: result.data!.nextReviewDate
|
||||
} : null);
|
||||
} else {
|
||||
console.log('復習結果提交失敗,繼續運行');
|
||||
// 更新當前會話的測驗結果
|
||||
const updatedSession = {
|
||||
...currentCardSession,
|
||||
completedTests: [...currentCardSession.completedTests, testResult]
|
||||
};
|
||||
|
||||
// 檢查是否完成所有預定測驗
|
||||
const isAllTestsCompleted = updatedSession.completedTests.length >= updatedSession.plannedTests.length;
|
||||
if (isAllTestsCompleted) {
|
||||
updatedSession.isCompleted = true;
|
||||
}
|
||||
|
||||
// 更新測驗進度(無論提交成功或失敗)
|
||||
setCurrentCardSession(updatedSession);
|
||||
|
||||
// 更新會話映射
|
||||
setCardReviewSessions(prev => new Map(prev.set(currentCard.id, updatedSession)));
|
||||
|
||||
// 更新測驗進度
|
||||
setCompletedTests(prev => {
|
||||
const newCompleted = prev + 1;
|
||||
console.log(`📈 測驗進度更新: ${newCompleted}/${totalTests} (${Math.round((newCompleted/totalTests)*100)}%)`);
|
||||
return newCompleted;
|
||||
});
|
||||
|
||||
// 標記當前測驗項目為完成
|
||||
setTestItems(prev =>
|
||||
prev.map((item, index) =>
|
||||
index === currentTestItemIndex
|
||||
? { ...item, isCompleted: true, isCurrent: false }
|
||||
: item
|
||||
)
|
||||
);
|
||||
|
||||
// 移到下一個測驗項目
|
||||
setCurrentTestItemIndex(prev => prev + 1);
|
||||
|
||||
console.log(`🔍 記錄測驗結果:`, {
|
||||
word: currentCard.word,
|
||||
testType: mode,
|
||||
isCorrect,
|
||||
completedTests: updatedSession.completedTests.length,
|
||||
plannedTests: updatedSession.plannedTests.length,
|
||||
isCardCompleted: updatedSession.isCompleted
|
||||
});
|
||||
|
||||
// 如果詞卡的所有測驗都完成了,觸發完整復習邏輯
|
||||
if (updatedSession.isCompleted) {
|
||||
console.log(`✅ 詞卡 ${currentCard.word} 的所有測驗已完成,準備提交復習結果`);
|
||||
completeCardReview(updatedSession);
|
||||
}
|
||||
}
|
||||
|
||||
// 完成詞卡複習並提交到後端
|
||||
const completeCardReview = async (session: CardReviewSession) => {
|
||||
try {
|
||||
// 計算綜合表現指標
|
||||
const correctCount = session.completedTests.filter(t => t.isCorrect).length;
|
||||
const totalTests = session.completedTests.length;
|
||||
const accuracy = totalTests > 0 ? correctCount / totalTests : 0;
|
||||
|
||||
// 計算平均信心等級(用於翻卡記憶測驗)
|
||||
const confidenceTests = session.completedTests.filter(t => t.confidenceLevel !== undefined);
|
||||
const avgConfidence = confidenceTests.length > 0
|
||||
? confidenceTests.reduce((sum, t) => sum + (t.confidenceLevel || 3), 0) / confidenceTests.length
|
||||
: 3;
|
||||
|
||||
// 計算平均答題時間
|
||||
const avgResponseTime = session.completedTests.reduce((sum, t) => sum + t.responseTimeMs, 0) / totalTests;
|
||||
|
||||
// 確定主要測驗類型(用於後端SM2算法)
|
||||
const primaryTestType = session.completedTests[0]?.testType || 'flip-memory';
|
||||
|
||||
console.log(`🔥 提交詞卡完整復習結果:`, {
|
||||
word: session.word,
|
||||
accuracy: `${Math.round(accuracy * 100)}%`,
|
||||
avgConfidence,
|
||||
avgResponseTime: `${avgResponseTime}ms`,
|
||||
primaryTestType,
|
||||
completedTests: session.completedTests.length
|
||||
});
|
||||
|
||||
// 提交到後端
|
||||
const result = await flashcardsService.submitReview(session.cardId, {
|
||||
isCorrect: accuracy >= 0.7, // 70%以上正確率視為通過
|
||||
confidenceLevel: Math.round(avgConfidence),
|
||||
questionType: primaryTestType,
|
||||
userAnswer: `綜合${totalTests}個測驗,正確率${Math.round(accuracy * 100)}%`,
|
||||
timeTaken: Math.round(avgResponseTime)
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
console.log('✅ 詞卡復習結果提交成功:', result.data);
|
||||
|
||||
// 更新詞卡的熟悉度等資訊
|
||||
if (currentCard && currentCard.id === session.cardId) {
|
||||
setCurrentCard(prev => prev ? {
|
||||
...prev,
|
||||
masteryLevel: result.data!.masteryLevel,
|
||||
nextReviewDate: result.data!.nextReviewDate
|
||||
} : null);
|
||||
}
|
||||
|
||||
// 增加已完成詞卡數量
|
||||
setCompletedCards(prev => prev + 1);
|
||||
|
||||
console.log(`🎉 詞卡 ${session.word} 復習完成!新熟悉度: ${result.data.masteryLevel}%, 下次復習: ${result.data.nextReviewDate}`);
|
||||
} else {
|
||||
console.error('詞卡復習結果提交失敗:', result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('提交復習結果失敗:', error);
|
||||
// 即使出錯也更新進度,避免卡住
|
||||
setCompletedTests(prev => prev + 1);
|
||||
console.error('完成詞卡復習時發生錯誤:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -636,8 +941,8 @@ export default function LearnPage() {
|
|||
total: prev.total + 1
|
||||
}))
|
||||
|
||||
// 提交復習結果
|
||||
await submitReviewResult(isCorrect, fillAnswer);
|
||||
// 記錄測驗結果
|
||||
recordTestResult(isCorrect, fillAnswer);
|
||||
}
|
||||
|
||||
const handleListeningAnswer = async (answer: string) => {
|
||||
|
|
@ -652,8 +957,8 @@ export default function LearnPage() {
|
|||
total: prev.total + 1
|
||||
}))
|
||||
|
||||
// 提交復習結果
|
||||
await submitReviewResult(isCorrect, answer);
|
||||
// 記錄測驗結果
|
||||
recordTestResult(isCorrect, answer);
|
||||
}
|
||||
|
||||
const handleSpeakingAnswer = async (transcript: string) => {
|
||||
|
|
@ -667,8 +972,8 @@ export default function LearnPage() {
|
|||
total: prev.total + 1
|
||||
}))
|
||||
|
||||
// 提交復習結果
|
||||
await submitReviewResult(isCorrect, transcript);
|
||||
// 記錄測驗結果
|
||||
recordTestResult(isCorrect, transcript);
|
||||
}
|
||||
|
||||
const handleSentenceListeningAnswer = async (answer: string) => {
|
||||
|
|
@ -683,8 +988,8 @@ export default function LearnPage() {
|
|||
total: prev.total + 1
|
||||
}))
|
||||
|
||||
// 提交復習結果
|
||||
await submitReviewResult(isCorrect, answer);
|
||||
// 記錄測驗結果
|
||||
recordTestResult(isCorrect, answer);
|
||||
}
|
||||
|
||||
const handleReportSubmit = () => {
|
||||
|
|
@ -701,6 +1006,8 @@ export default function LearnPage() {
|
|||
setScore({ correct: 0, total: 0 })
|
||||
setCompletedTests(0)
|
||||
setTotalTests(0)
|
||||
setTestItems([])
|
||||
setCurrentTestItemIndex(0)
|
||||
setShowComplete(false)
|
||||
setShowNoDueCards(false)
|
||||
await loadDueCards(); // 重新載入到期詞卡
|
||||
|
|
@ -787,172 +1094,77 @@ export default function LearnPage() {
|
|||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
{/* Progress Bar */}
|
||||
<div className="mb-8">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm text-gray-600">進度</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-600">
|
||||
{completedTests} / {totalTests} 測驗
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
詞卡 {currentCardIndex + 1}/{dueCards.length}
|
||||
</span>
|
||||
<div className="text-sm">
|
||||
<span className="text-green-600 font-semibold">{score.correct}</span>
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<span className="text-sm font-medium text-gray-900">學習進度</span>
|
||||
<div className="flex items-center gap-6">
|
||||
{/* 詞卡進度 */}
|
||||
<div className="text-sm text-gray-600">
|
||||
<span className="font-medium">詞卡:</span>
|
||||
<span className="ml-1 text-green-600 font-semibold">{completedCards}</span>
|
||||
<span className="text-gray-500">/</span>
|
||||
<span className="text-gray-600">{score.total}</span>
|
||||
{score.total > 0 && (
|
||||
<span className="text-blue-600 ml-2">
|
||||
({Math.round((score.correct / score.total) * 100)}%)
|
||||
<span className="text-gray-600">{dueCards.length}</span>
|
||||
{dueCards.length > 0 && (
|
||||
<span className="text-blue-600 ml-1">
|
||||
({Math.round((completedCards / dueCards.length) * 100)}%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 測驗進度 */}
|
||||
<button
|
||||
onClick={() => setShowTaskListModal(true)}
|
||||
className="text-sm text-gray-600 hover:text-blue-600 transition-colors cursor-pointer flex items-center gap-1"
|
||||
title="點擊查看詳細任務清單"
|
||||
>
|
||||
<span className="font-medium">測驗:</span>
|
||||
<span className="ml-1 text-blue-600 font-semibold">{completedTests}</span>
|
||||
<span className="text-gray-500">/</span>
|
||||
<span className="text-gray-600">{totalTests}</span>
|
||||
{totalTests > 0 && (
|
||||
<span className="text-blue-600 ml-1">
|
||||
({Math.round((completedTests / totalTests) * 100)}%)
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs ml-1">📋</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||
|
||||
{/* 雙層進度條 */}
|
||||
<div className="space-y-2">
|
||||
{/* 詞卡進度條 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500 w-12">詞卡</span>
|
||||
<div className="flex-1 bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-primary h-2 rounded-full transition-all"
|
||||
className="bg-green-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${dueCards.length > 0 ? (completedCards / dueCards.length) * 100 : 0}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 w-8">
|
||||
{dueCards.length > 0 ? Math.round((completedCards / dueCards.length) * 100) : 0}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 測驗進度條 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-500 w-12">測驗</span>
|
||||
<div
|
||||
className="flex-1 bg-gray-200 rounded-full h-2 cursor-pointer hover:bg-gray-300 transition-colors"
|
||||
onClick={() => setShowTaskListModal(true)}
|
||||
title="點擊查看詳細任務清單"
|
||||
>
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full transition-all hover:bg-blue-600"
|
||||
style={{ width: `${totalTests > 0 ? (completedTests / totalTests) * 100 : 0}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Smart Review Information Panel */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<h3 className="text-lg font-semibold text-blue-900 mb-3">🧠 CEFR智能複習系統</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 text-sm mb-4">
|
||||
<div className="bg-white rounded p-3">
|
||||
<div className="text-blue-700 font-medium">當前情境</div>
|
||||
<div className="text-gray-600">
|
||||
{currentCard && (() => {
|
||||
const userCEFRLevel = localStorage.getItem('userEnglishLevel') || 'A2';
|
||||
const wordCEFRLevel = currentCard.difficultyLevel || 'A2';
|
||||
const userLevel = getCEFRToLevel(userCEFRLevel);
|
||||
const wordLevel = getCEFRToLevel(wordCEFRLevel);
|
||||
const difficulty = wordLevel - userLevel;
|
||||
|
||||
if (userCEFRLevel === 'A1') return 'A1學習者';
|
||||
if (difficulty < -10) return '簡單詞彙';
|
||||
if (difficulty >= -10 && difficulty <= 10) return '適中詞彙';
|
||||
return '困難詞彙';
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded p-3">
|
||||
<div className="text-blue-700 font-medium">學習者等級</div>
|
||||
<div className="text-gray-600">{localStorage.getItem('userEnglishLevel') || 'A2'}</div>
|
||||
</div>
|
||||
<div className="bg-white rounded p-3">
|
||||
<div className="text-blue-700 font-medium">詞彙等級</div>
|
||||
<div className="text-gray-600">{currentCard?.difficultyLevel || 'A2'}</div>
|
||||
</div>
|
||||
<div className="bg-white rounded p-3">
|
||||
<div className="text-blue-700 font-medium">等級差異</div>
|
||||
<div className="text-gray-600">
|
||||
{currentCard ? (() => {
|
||||
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
|
||||
const wordCEFR = currentCard.difficultyLevel || 'A2';
|
||||
const diff = getCEFRToLevel(wordCEFR) - getCEFRToLevel(userCEFR);
|
||||
return diff > 0 ? `+${diff}` : diff.toString();
|
||||
})() : '--'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 當前選擇突出顯示 */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-blue-100 rounded-lg p-3 mb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
{currentCard && (() => {
|
||||
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
|
||||
const wordCEFR = currentCard.difficultyLevel || 'A2';
|
||||
const context = getCurrentContext(userCEFR, wordCEFR);
|
||||
const contextData = generateContextTable(userCEFR, wordCEFR).find(c => c.isCurrent);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span className="text-blue-800 font-medium">
|
||||
當前情境: {contextData?.icon} {context}
|
||||
<span className="text-xs text-gray-500 w-8">
|
||||
{totalTests > 0 ? Math.round((completedTests / totalTests) * 100) : 0}%
|
||||
</span>
|
||||
<div className="text-blue-600 text-xs mt-1">
|
||||
可用題型: {contextData?.reviewTypes.join(' | ')}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="text-blue-800 text-right">
|
||||
<div className="text-xs">系統已選擇</div>
|
||||
<div className="font-medium flex items-center gap-1">
|
||||
<span>{getModeIcon(mode)}</span>
|
||||
<span>{getModeLabel(mode)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 完整四情境對照表 */}
|
||||
<div className="bg-white rounded-lg p-4">
|
||||
<div className="text-blue-700 font-medium mb-3">📚 智能複習四情境對照表</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200">
|
||||
<th className="text-left p-2 font-medium text-gray-700">情境類型</th>
|
||||
<th className="text-left p-2 font-medium text-gray-700">建議複習方式</th>
|
||||
<th className="text-left p-2 font-medium text-gray-700">學習目的</th>
|
||||
<th className="text-left p-2 font-medium text-gray-700">判斷條件</th>
|
||||
<th className="text-center p-2 font-medium text-gray-700">狀態</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{currentCard && (() => {
|
||||
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
|
||||
const wordCEFR = currentCard.difficultyLevel || 'A2';
|
||||
const tableData = generateContextTable(userCEFR, wordCEFR);
|
||||
|
||||
return tableData.map((row, index) => (
|
||||
<tr key={index} className={`border-b border-gray-100 ${row.isCurrent ? 'bg-blue-50 ring-1 ring-blue-200' : ''}`}>
|
||||
<td className="p-2">
|
||||
<span className="flex items-center gap-1 font-medium">
|
||||
{row.icon} {row.type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-2">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{row.reviewTypes.map((type, idx) => (
|
||||
<span key={idx} className="text-xs whitespace-nowrap">{type}</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2 text-gray-600">{row.purpose}</td>
|
||||
<td className="p-2 text-gray-500 text-xs">{row.condition}</td>
|
||||
<td className="p-2 text-center">
|
||||
{row.isCurrent && <span className="text-blue-600 font-medium">← 目前</span>}
|
||||
</td>
|
||||
</tr>
|
||||
));
|
||||
})()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Card Mastery Level */}
|
||||
{currentCard?.baseMasteryLevel && currentCard?.lastReviewDate && (
|
||||
<div className="mb-4">
|
||||
<MasteryIndicator
|
||||
level={calculateCurrentMastery(currentCard.baseMasteryLevel, currentCard.lastReviewDate)}
|
||||
baseMasteryLevel={currentCard.baseMasteryLevel}
|
||||
size="medium"
|
||||
showPercentage={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'flip-memory' ? (
|
||||
/* Flip Card Mode */
|
||||
|
|
@ -1885,6 +2097,111 @@ export default function LearnPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Task List Modal */}
|
||||
{showTaskListModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-2xl max-w-4xl w-full mx-4 max-h-[80vh] overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
|
||||
📚 學習任務清單
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowTaskListModal(false)}
|
||||
className="text-gray-400 hover:text-gray-600 text-2xl"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 overflow-y-auto max-h-[60vh]">
|
||||
{/* 進度統計 */}
|
||||
<div className="mb-6 bg-blue-50 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-blue-900 font-medium">
|
||||
測驗進度: {completedTests} / {totalTests} ({totalTests > 0 ? Math.round((completedTests / totalTests) * 100) : 0}%)
|
||||
</span>
|
||||
<div className="flex items-center gap-4 text-blue-800">
|
||||
<span>✅ 已完成: {testItems.filter(item => item.isCompleted).length}</span>
|
||||
<span>⏳ 進行中: {testItems.filter(item => item.isCurrent).length}</span>
|
||||
<span>⚪ 待完成: {testItems.filter(item => !item.isCompleted && !item.isCurrent).length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 w-full bg-blue-200 rounded-full h-2">
|
||||
<div
|
||||
className="bg-blue-600 h-2 rounded-full transition-all"
|
||||
style={{ width: `${totalTests > 0 ? (completedTests / totalTests) * 100 : 0}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 任務清單 */}
|
||||
<div className="space-y-4">
|
||||
{groupTestItemsByCard(testItems).map((cardGroup, cardIndex) => (
|
||||
<div key={cardGroup.cardId} className="border border-gray-200 rounded-lg p-4 bg-white shadow-sm">
|
||||
{/* 詞卡標題 */}
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="font-medium text-gray-900">
|
||||
詞卡{cardIndex + 1}: {cardGroup.word}
|
||||
</span>
|
||||
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded">
|
||||
{cardGroup.context}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{cardGroup.tests.length}個測驗
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 測驗項目 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
{cardGroup.tests.map(test => (
|
||||
<div
|
||||
key={test.id}
|
||||
className={`flex items-center gap-3 p-3 rounded-lg transition-all ${
|
||||
test.isCompleted
|
||||
? 'bg-green-50 border border-green-200'
|
||||
: test.isCurrent
|
||||
? 'bg-blue-50 border border-blue-300 shadow-sm'
|
||||
: 'bg-gray-50 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
{/* 狀態圖標 */}
|
||||
<span className="text-lg">
|
||||
{test.isCompleted ? '✅' : test.isCurrent ? '⏳' : '⚪'}
|
||||
</span>
|
||||
|
||||
{/* 測驗資訊 */}
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">
|
||||
{test.order}. {test.testName}
|
||||
</div>
|
||||
<div className={`text-xs ${
|
||||
test.isCompleted ? 'text-green-600' :
|
||||
test.isCurrent ? 'text-blue-600' : 'text-gray-500'
|
||||
}`}>
|
||||
{test.isCompleted ? '已完成' :
|
||||
test.isCurrent ? '進行中' : '待完成'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{testItems.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<div className="text-4xl mb-2">📚</div>
|
||||
<p>還沒有生成任務清單</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Complete Modal */}
|
||||
{showComplete && (
|
||||
<LearningComplete
|
||||
|
|
@ -1942,6 +2259,7 @@ export default function LearnPage() {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
|
|
|
|||
179
冗餘數值欄位移除完成報告.md
179
冗餘數值欄位移除完成報告.md
|
|
@ -1,179 +0,0 @@
|
|||
# 冗餘UserLevel/WordLevel欄位移除完成報告
|
||||
|
||||
## 📋 **執行總結**
|
||||
**執行時間**: 2025-09-26
|
||||
**狀態**: ✅ **完全成功**
|
||||
**架構**: 純CEFR字符串架構
|
||||
**前端**: http://localhost:3002
|
||||
**後端**: http://localhost:5008
|
||||
|
||||
---
|
||||
|
||||
## 🎯 **移除目標達成**
|
||||
|
||||
### **✅ 消除資料重複問題**
|
||||
```sql
|
||||
-- 移除前:重複存儲
|
||||
users.english_level: "A2" (主要)
|
||||
flashcards.UserLevel: 50 (冗餘) ← 已移除
|
||||
flashcards.difficulty_level: "A2" (主要)
|
||||
flashcards.WordLevel: 35 (冗餘) ← 已移除
|
||||
|
||||
-- 移除後:純CEFR架構
|
||||
users.english_level: "A2" (唯一來源)
|
||||
flashcards.difficulty_level: "A2" (唯一來源)
|
||||
```
|
||||
|
||||
### **✅ 程式碼簡化成果**
|
||||
- **FlashcardsController**: 移除數值欄位初始化邏輯
|
||||
- **SpacedRepetitionService**: 移除批量初始化程式碼
|
||||
- **前端接口**: 移除數值欄位映射
|
||||
- **資料庫模型**: 移除冗餘屬性定義
|
||||
|
||||
---
|
||||
|
||||
## 🔧 **具體實施成果**
|
||||
|
||||
### **Phase 1: 後端資料庫清理** ✅ **完成**
|
||||
|
||||
#### **1. 資料庫遷移執行**
|
||||
```bash
|
||||
✅ 創建遷移: dotnet ef migrations add RemoveRedundantLevelFields
|
||||
✅ 執行遷移: dotnet ef database update
|
||||
✅ 欄位移除: UserLevel, WordLevel從flashcards表移除
|
||||
```
|
||||
|
||||
#### **2. Flashcard模型更新**
|
||||
```csharp
|
||||
// 移除前:
|
||||
public int UserLevel { get; set; } = 50;
|
||||
public int WordLevel { get; set; } = 50;
|
||||
|
||||
// 移除後:
|
||||
// UserLevel和WordLevel已移除 - 改用即時CEFR轉換
|
||||
```
|
||||
|
||||
#### **3. Controller邏輯清理**
|
||||
```csharp
|
||||
// 移除前:數值欄位初始化
|
||||
if (nextCard.UserLevel == 0) nextCard.UserLevel = ...;
|
||||
if (nextCard.WordLevel == 0) nextCard.WordLevel = ...;
|
||||
|
||||
// 移除後:
|
||||
// UserLevel和WordLevel欄位已移除 - 改用即時CEFR轉換
|
||||
```
|
||||
|
||||
### **Phase 2: 前端接口適配** ✅ **完成**
|
||||
|
||||
#### **1. API服務層更新**
|
||||
```typescript
|
||||
// 移除前:包含數值欄位映射
|
||||
userLevel: card.userLevel || 50,
|
||||
wordLevel: card.wordLevel || 50,
|
||||
|
||||
// 移除後:
|
||||
// 智能複習擴展欄位 (數值欄位已移除,改用即時CEFR轉換)
|
||||
```
|
||||
|
||||
#### **2. 接口定義簡化**
|
||||
```typescript
|
||||
// 移除前:
|
||||
interface ExtendedFlashcard {
|
||||
userLevel?: number;
|
||||
wordLevel?: number;
|
||||
}
|
||||
|
||||
// 移除後:
|
||||
interface ExtendedFlashcard {
|
||||
// 注意:userLevel和wordLevel已移除,改用即時CEFR轉換
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 **功能驗證結果**
|
||||
|
||||
### **✅ API測試通過**
|
||||
```bash
|
||||
✅ GET /flashcards/due:
|
||||
- success: true, count: 5
|
||||
- hasUserLevel: false, hasWordLevel: false
|
||||
- 確認數值欄位已完全移除
|
||||
|
||||
✅ POST /flashcards/{id}/optimal-review-mode:
|
||||
- userCEFR: "A2" → 智能選擇: "sentence-reorder"
|
||||
- adaptationContext: "適中詞彙"
|
||||
- 純CEFR字符串智能選擇100%正常
|
||||
```
|
||||
|
||||
### **✅ 即時轉換驗證**
|
||||
```csharp
|
||||
// 後端日誌確認:
|
||||
CEFR converted to levels: A2→35, A2→35
|
||||
Selected mode: sentence-reorder, context: 適中詞彙
|
||||
```
|
||||
|
||||
### **✅ 前端功能正常**
|
||||
- 學習頁面載入正常
|
||||
- 四情境對照表顯示正確
|
||||
- 智能適配完全正常
|
||||
- 播放按鈕統一設計正常
|
||||
|
||||
---
|
||||
|
||||
## 📊 **架構優化成果**
|
||||
|
||||
### **資料庫優化** ✅
|
||||
- **移除冗餘欄位**: UserLevel, WordLevel
|
||||
- **減少存儲空間**: 每張詞卡節省8 bytes
|
||||
- **消除同步負擔**: 不需要維護數值和CEFR同步
|
||||
- **符合正規化**: 遵循資料庫設計最佳實踐
|
||||
|
||||
### **程式碼品質提升** ✅
|
||||
- **移除重複邏輯**: 約50行冗餘程式碼
|
||||
- **統一CEFR處理**: 全系統使用標準CEFR術語
|
||||
- **降低複雜度**: 不需要管理雙欄位邏輯
|
||||
- **提升可維護性**: 單一資料來源原則
|
||||
|
||||
### **架構純化** ✅
|
||||
- **純CEFR標準**: 完全符合國際語言學習標準
|
||||
- **即時轉換**: CEFRMappingService高效轉換(< 1ms)
|
||||
- **無性能影響**: 轉換開銷微乎其微
|
||||
- **標準化API**: 前後端統一使用CEFR術語
|
||||
|
||||
---
|
||||
|
||||
## 🎉 **最終成果**
|
||||
|
||||
### **✅ 技術債務清理完成**
|
||||
- 徹底解決資料重複問題
|
||||
- 消除維護負擔和同步風險
|
||||
- 提升系統架構純度
|
||||
|
||||
### **✅ CEFR標準化達成**
|
||||
- 全系統統一使用標準CEFR等級
|
||||
- 符合國際語言學習慣例
|
||||
- 提升專業度和可信度
|
||||
|
||||
### **✅ 系統性能優化**
|
||||
- 移除冗餘資料存儲
|
||||
- 簡化資料庫結構
|
||||
- 降低記憶體使用
|
||||
|
||||
### **🚀 系統現狀**
|
||||
- **資料庫**: 純CEFR字符串,無冗餘欄位
|
||||
- **後端**: 即時轉換邏輯,高效能計算
|
||||
- **前端**: 純CEFR顯示,統一播放按鈕
|
||||
- **功能**: 智能複習系統100%正常運作
|
||||
|
||||
**冗餘數值欄位移除計劃圓滿完成!智能複習系統現已達到純CEFR標準化架構!** 🎯✨
|
||||
|
||||
---
|
||||
|
||||
**技術架構優化前後對比**:
|
||||
```
|
||||
優化前: CEFR字符串 + 數值欄位 (重複資料)
|
||||
優化後: 純CEFR字符串 + 即時轉換 (標準化)
|
||||
```
|
||||
|
||||
**系統已準備投入生產使用,架構純淨、標準、高效!** 🚀📚
|
||||
272
移除冗餘數值欄位計劃.md
272
移除冗餘數值欄位計劃.md
|
|
@ -1,272 +0,0 @@
|
|||
# 移除冗餘UserLevel/WordLevel欄位和程式碼計劃
|
||||
|
||||
## 🎯 **目標**
|
||||
徹底移除冗餘的數值欄位,簡化資料庫結構,實現純CEFR字符串架構,消除資料重複問題。
|
||||
|
||||
---
|
||||
|
||||
## 📊 **現況分析**
|
||||
|
||||
### **重複資料確認**
|
||||
```sql
|
||||
-- 用戶程度重複存儲 ❌
|
||||
users.english_level: "A2" (主要,標準CEFR)
|
||||
flashcards.UserLevel: 50 (冗餘,數值緩存)
|
||||
|
||||
-- 詞彙難度重複存儲 ❌
|
||||
flashcards.difficulty_level: "A2" (主要,標準CEFR)
|
||||
flashcards.WordLevel: 35 (冗餘,數值緩存)
|
||||
```
|
||||
|
||||
### **冗餘程度評估**
|
||||
- **智能選擇**: ✅ 已改為即時CEFR轉換,不使用存儲數值
|
||||
- **四情境判斷**: ✅ 使用即時轉換的數值進行運算
|
||||
- **API回應**: ⚠️ 仍包含數值欄位(僅為前端相容)
|
||||
- **舊資料處理**: ⚠️ 防止數值為0的初始化邏輯
|
||||
|
||||
---
|
||||
|
||||
## 📋 **詳細移除計劃**
|
||||
|
||||
### **Phase 1: 後端資料庫和模型清理** ⏱️ 1天
|
||||
|
||||
#### **1.1 創建資料庫遷移**
|
||||
```bash
|
||||
cd backend/DramaLing.Api
|
||||
dotnet ef migrations add RemoveRedundantLevelFields
|
||||
```
|
||||
|
||||
**Migration內容**:
|
||||
```sql
|
||||
ALTER TABLE flashcards DROP COLUMN UserLevel;
|
||||
ALTER TABLE flashcards DROP COLUMN WordLevel;
|
||||
```
|
||||
|
||||
#### **1.2 更新Flashcard模型**
|
||||
**檔案**: `Models/Entities/Flashcard.cs`
|
||||
```csharp
|
||||
// 移除這兩個屬性:
|
||||
// [Range(1, 100)]
|
||||
// public int UserLevel { get; set; } = 50;
|
||||
//
|
||||
// [Range(1, 100)]
|
||||
// public int WordLevel { get; set; } = 50;
|
||||
```
|
||||
|
||||
#### **1.3 清理配置選項**
|
||||
**檔案**: `appsettings.json`
|
||||
```json
|
||||
{
|
||||
"SpacedRepetition": {
|
||||
// 移除 "DefaultUserLevel": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**檔案**: `Models/Configuration/SpacedRepetitionOptions.cs`
|
||||
```csharp
|
||||
// 移除 DefaultUserLevel 屬性
|
||||
```
|
||||
|
||||
### **Phase 2: 後端API和服務清理** ⏱️ 1天
|
||||
|
||||
#### **2.1 清理FlashcardsController**
|
||||
**檔案**: `Controllers/FlashcardsController.cs`
|
||||
|
||||
**移除數值欄位初始化**:
|
||||
```csharp
|
||||
// 移除 lines 508-512:
|
||||
// if (nextCard.UserLevel == 0)
|
||||
// nextCard.UserLevel = CEFRMappingService.GetDefaultUserLevel();
|
||||
// if (nextCard.WordLevel == 0)
|
||||
// nextCard.WordLevel = CEFRMappingService.GetWordLevel(nextCard.DifficultyLevel);
|
||||
```
|
||||
|
||||
**簡化API回應**:
|
||||
```csharp
|
||||
var response = new
|
||||
{
|
||||
// 移除 nextCard.UserLevel, nextCard.WordLevel
|
||||
// 保留 nextCard.DifficultyLevel (CEFR字符串)
|
||||
};
|
||||
```
|
||||
|
||||
#### **2.2 清理SpacedRepetitionService**
|
||||
**檔案**: `Services/SpacedRepetitionService.cs`
|
||||
|
||||
**移除批量初始化邏輯**:
|
||||
```csharp
|
||||
// 移除 lines 141-149:
|
||||
// foreach (var card in dueCards.Where(c => c.WordLevel == 0))
|
||||
// {
|
||||
// card.WordLevel = CEFRMappingService.GetWordLevel(card.DifficultyLevel);
|
||||
// if (card.UserLevel == 0)
|
||||
// card.UserLevel = _options.DefaultUserLevel;
|
||||
// }
|
||||
```
|
||||
|
||||
#### **2.3 清理QuestionGeneratorService**
|
||||
**檔案**: `Services/QuestionGeneratorService.cs`
|
||||
|
||||
**移除數值版本的方法** (如果存在):
|
||||
```csharp
|
||||
// 檢查並移除任何直接使用數值參數的方法
|
||||
```
|
||||
|
||||
### **Phase 3: 前端適配調整** ⏱️ 0.5天
|
||||
|
||||
#### **3.1 更新前端API服務**
|
||||
**檔案**: `frontend/lib/services/flashcards.ts`
|
||||
|
||||
**移除數值欄位映射**:
|
||||
```typescript
|
||||
const flashcards = response.data.map((card: any) => ({
|
||||
// 移除這兩行:
|
||||
// userLevel: card.userLevel || 50,
|
||||
// wordLevel: card.wordLevel || 50,
|
||||
}));
|
||||
```
|
||||
|
||||
#### **3.2 更新前端接口定義**
|
||||
**檔案**: `frontend/app/learn/page.tsx`
|
||||
|
||||
**簡化ExtendedFlashcard**:
|
||||
```typescript
|
||||
interface ExtendedFlashcard extends Omit<Flashcard, 'nextReviewDate'> {
|
||||
// 移除:
|
||||
// userLevel?: number;
|
||||
// wordLevel?: number;
|
||||
|
||||
nextReviewDate?: string;
|
||||
// ...其他實際需要的欄位
|
||||
}
|
||||
```
|
||||
|
||||
#### **3.3 更新前端顯示邏輯**
|
||||
**全部改為CEFR字符串邏輯**:
|
||||
```typescript
|
||||
// 不再使用 currentCard.userLevel, currentCard.wordLevel
|
||||
// 改為:
|
||||
const userCEFR = localStorage.getItem('userEnglishLevel') || 'A2';
|
||||
const wordCEFR = currentCard.difficultyLevel || 'A2';
|
||||
```
|
||||
|
||||
### **Phase 4: API接口純化** ⏱️ 0.5天
|
||||
|
||||
#### **4.1 移除API回應中的數值欄位**
|
||||
**所有相關API端點**:
|
||||
- `GET /flashcards/due`
|
||||
- `GET /flashcards/next-review`
|
||||
- `GET /flashcards/{id}`
|
||||
|
||||
**移除回應中的**:
|
||||
```json
|
||||
{
|
||||
// 移除 "userLevel": 50,
|
||||
// 移除 "wordLevel": 35,
|
||||
"difficultyLevel": "A2" // 保留CEFR字符串
|
||||
}
|
||||
```
|
||||
|
||||
#### **4.2 更新API文檔**
|
||||
- 移除數值欄位的相關描述
|
||||
- 更新為純CEFR架構文檔
|
||||
|
||||
### **Phase 5: 測試和驗證** ⏱️ 0.5天
|
||||
|
||||
#### **5.1 功能測試清單**
|
||||
- [ ] 智能複習選擇功能正常
|
||||
- [ ] 四情境判斷邏輯正確
|
||||
- [ ] 前端顯示完全正常
|
||||
- [ ] CEFR等級轉換準確
|
||||
- [ ] 新詞卡創建和更新正常
|
||||
|
||||
#### **5.2 性能測試**
|
||||
- [ ] API回應時間無明顯變化
|
||||
- [ ] 智能選擇速度正常
|
||||
- [ ] 前端載入速度正常
|
||||
|
||||
#### **5.3 回歸測試**
|
||||
- [ ] 學習頁面完整流程
|
||||
- [ ] 詞卡管理功能正常
|
||||
- [ ] 所有播放按鈕正常
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ **檔案修改清單**
|
||||
|
||||
### **後端檔案** (5個主要檔案)
|
||||
1. `Models/Entities/Flashcard.cs` - 移除數值屬性
|
||||
2. `Controllers/FlashcardsController.cs` - 移除初始化和回應邏輯
|
||||
3. `Services/SpacedRepetitionService.cs` - 移除批量初始化
|
||||
4. `Models/Configuration/SpacedRepetitionOptions.cs` - 移除配置
|
||||
5. `appsettings.json` - 移除配置項
|
||||
|
||||
### **前端檔案** (3個主要檔案)
|
||||
1. `lib/services/flashcards.ts` - 移除數值映射
|
||||
2. `app/learn/page.tsx` - 更新接口和邏輯
|
||||
3. `components/review/ReviewTypeIndicator.tsx` - 移除數值依賴
|
||||
|
||||
### **資料庫遷移** (1個檔案)
|
||||
1. 新的migration檔案 - DROP COLUMN指令
|
||||
|
||||
---
|
||||
|
||||
## 📈 **預期效益**
|
||||
|
||||
### **資料庫優化**
|
||||
- ✅ 移除2個冗餘INT欄位
|
||||
- ✅ 消除資料同步負擔
|
||||
- ✅ 減少儲存空間使用
|
||||
- ✅ 簡化資料庫結構
|
||||
|
||||
### **程式碼簡化**
|
||||
- ✅ 移除~50行冗餘程式碼
|
||||
- ✅ 消除資料同步邏輯
|
||||
- ✅ 統一CEFR處理流程
|
||||
- ✅ 提升程式碼可讀性
|
||||
|
||||
### **架構純化**
|
||||
- ✅ 純CEFR標準架構
|
||||
- ✅ 符合資料庫正規化原則
|
||||
- ✅ 消除資料重複問題
|
||||
- ✅ 降低維護複雜度
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ **風險管理**
|
||||
|
||||
### **風險等級**: 🟢 **低風險**
|
||||
- CEFR轉換邏輯已穩定運行
|
||||
- 即時轉換性能優異
|
||||
- 不影響用戶體驗
|
||||
|
||||
### **緩解措施**
|
||||
- 保留migration回滾腳本
|
||||
- 分階段實施,逐步驗證
|
||||
- 保持完整測試覆蓋
|
||||
- 監控性能指標
|
||||
|
||||
### **回滾計劃**
|
||||
如有問題可快速回滾:
|
||||
```sql
|
||||
-- 回滾migration恢復欄位
|
||||
dotnet ef database update PreviousMigration
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 **實施建議**
|
||||
|
||||
### **推薦立即實施**
|
||||
1. **技術債務清理**: 消除設計上的冗餘
|
||||
2. **標準化架構**: 完全符合CEFR國際標準
|
||||
3. **長期維護**: 降低未來開發和維護成本
|
||||
4. **代碼品質**: 提升整體架構清潔度
|
||||
|
||||
### **實施順序**
|
||||
1. **後端清理** → **前端適配** → **測試驗證**
|
||||
2. 可隨時暫停,每個階段都有明確的檢查點
|
||||
3. 出現問題立即回滾,影響可控
|
||||
|
||||
**建議:開始實施此清理計劃,徹底解決資料重複問題!** 🎯
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
# 複習系統後端驅動架構重構計劃
|
||||
|
||||
## 專案概述
|
||||
|
||||
### 問題現狀
|
||||
1. **邏輯錯誤**:完成一個測驗就標記詞卡完成,但實際還有其他測驗未完成
|
||||
2. **架構問題**:前端管理複習會話狀態,容易出現數據不一致
|
||||
3. **UI問題**:雙進度條視覺效果不佳,用戶希望整合為分段式進度條
|
||||
4. **維護困難**:複習邏輯散落在前端各處,調試和維護困難
|
||||
|
||||
### 解決方案
|
||||
將複習會話狀態管理移至後端,實現真正的後端驅動架構,同時優化進度條UI設計。
|
||||
|
||||
## 技術架構設計
|
||||
|
||||
### 後端架構
|
||||
|
||||
#### 1. 數據模型設計
|
||||
|
||||
```csharp
|
||||
// 學習會話實體
|
||||
public class StudySession
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid UserId { get; set; }
|
||||
public DateTime StartedAt { get; set; }
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
public SessionStatus Status { get; set; }
|
||||
public List<StudyCard> Cards { get; set; } = new();
|
||||
public int CurrentCardIndex { get; set; }
|
||||
public string? CurrentTestType { get; set; }
|
||||
public int TotalTests { get; set; }
|
||||
public int CompletedTests { get; set; }
|
||||
}
|
||||
|
||||
// 詞卡學習進度
|
||||
public class StudyCard
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid StudySessionId { get; set; }
|
||||
public Guid FlashcardId { get; set; }
|
||||
public string Word { get; set; } = string.Empty;
|
||||
public List<string> PlannedTests { get; set; } = new();
|
||||
public List<TestResult> CompletedTests { get; set; } = new();
|
||||
public bool IsCompleted => CompletedTests.Count >= PlannedTests.Count;
|
||||
public int Order { get; set; }
|
||||
|
||||
// 導航屬性
|
||||
public StudySession StudySession { get; set; }
|
||||
public Flashcard Flashcard { get; set; }
|
||||
}
|
||||
|
||||
// 測驗結果實體
|
||||
public class TestResult
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid StudyCardId { get; set; }
|
||||
public string TestType { get; set; } = string.Empty;
|
||||
public bool IsCorrect { get; set; }
|
||||
public string? UserAnswer { get; set; }
|
||||
public int? ConfidenceLevel { get; set; } // 1-5, 用於翻卡記憶
|
||||
public int ResponseTimeMs { get; set; }
|
||||
public DateTime CompletedAt { get; set; }
|
||||
|
||||
// 導航屬性
|
||||
public StudyCard StudyCard { get; set; }
|
||||
}
|
||||
|
||||
// 會話狀態枚舉
|
||||
public enum SessionStatus
|
||||
{
|
||||
Active, // 進行中
|
||||
Completed, // 已完成
|
||||
Paused, // 暫停
|
||||
Abandoned // 放棄
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 服務層設計
|
||||
|
||||
```csharp
|
||||
// 學習會話服務介面
|
||||
public interface IStudySessionService
|
||||
{
|
||||
Task<StudySessionDto> StartSessionAsync(Guid userId);
|
||||
Task<CurrentTestDto> GetCurrentTestAsync(Guid sessionId);
|
||||
Task<SubmitTestResponseDto> SubmitTestAsync(Guid sessionId, SubmitTestRequestDto request);
|
||||
Task<NextTestDto> GetNextTestAsync(Guid sessionId);
|
||||
Task<ProgressDto> GetProgressAsync(Guid sessionId);
|
||||
Task<CompleteSessionResponseDto> CompleteSessionAsync(Guid sessionId);
|
||||
}
|
||||
|
||||
// 測驗模式選擇服務
|
||||
public interface IReviewModeSelector
|
||||
{
|
||||
List<string> GetPlannedTests(string userCEFRLevel, string wordCEFRLevel);
|
||||
string GetNextTestType(StudyCard card);
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. API端點設計
|
||||
|
||||
```csharp
|
||||
[Route("api/study/sessions")]
|
||||
public class StudySessionController : ControllerBase
|
||||
{
|
||||
// 開始學習會話
|
||||
[HttpPost("start")]
|
||||
public async Task<ActionResult<StudySessionDto>> StartSession()
|
||||
|
||||
// 獲取當前測驗
|
||||
[HttpGet("{sessionId}/current-test")]
|
||||
public async Task<ActionResult<CurrentTestDto>> GetCurrentTest(Guid sessionId)
|
||||
|
||||
// 提交測驗結果
|
||||
[HttpPost("{sessionId}/submit-test")]
|
||||
public async Task<ActionResult<SubmitTestResponseDto>> SubmitTest(Guid sessionId, SubmitTestRequestDto request)
|
||||
|
||||
// 獲取下一個測驗
|
||||
[HttpGet("{sessionId}/next-test")]
|
||||
public async Task<ActionResult<NextTestDto>> GetNextTest(Guid sessionId)
|
||||
|
||||
// 獲取詳細進度
|
||||
[HttpGet("{sessionId}/progress")]
|
||||
public async Task<ActionResult<ProgressDto>> GetProgress(Guid sessionId)
|
||||
|
||||
// 結束會話
|
||||
[HttpPut("{sessionId}/complete")]
|
||||
public async Task<ActionResult<CompleteSessionResponseDto>> CompleteSession(Guid sessionId)
|
||||
}
|
||||
```
|
||||
|
||||
### 前端架構
|
||||
|
||||
#### 1. 服務層重構
|
||||
|
||||
```typescript
|
||||
// 學習會話服務
|
||||
class StudySessionService {
|
||||
async startSession(): Promise<StudySessionResponse> {
|
||||
return await this.post('/api/study/sessions/start');
|
||||
}
|
||||
|
||||
async getCurrentTest(sessionId: string): Promise<CurrentTestResponse> {
|
||||
return await this.get(`/api/study/sessions/${sessionId}/current-test`);
|
||||
}
|
||||
|
||||
async submitTest(sessionId: string, result: TestResult): Promise<SubmitTestResponse> {
|
||||
return await this.post(`/api/study/sessions/${sessionId}/submit-test`, result);
|
||||
}
|
||||
|
||||
async getNextTest(sessionId: string): Promise<NextTestResponse> {
|
||||
return await this.get(`/api/study/sessions/${sessionId}/next-test`);
|
||||
}
|
||||
|
||||
async getProgress(sessionId: string): Promise<ProgressResponse> {
|
||||
return await this.get(`/api/study/sessions/${sessionId}/progress`);
|
||||
}
|
||||
|
||||
async completeSession(sessionId: string): Promise<CompleteSessionResponse> {
|
||||
return await this.put(`/api/study/sessions/${sessionId}/complete`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. React組件簡化
|
||||
|
||||
```typescript
|
||||
// 簡化的 LearnPage 組件
|
||||
function LearnPage() {
|
||||
const [session, setSession] = useState<StudySession | null>(null);
|
||||
const [currentTest, setCurrentTest] = useState<CurrentTest | null>(null);
|
||||
const [progress, setProgress] = useState<Progress | null>(null);
|
||||
|
||||
// 簡化的狀態管理 - 只保留UI相關狀態
|
||||
const [selectedAnswer, setSelectedAnswer] = useState<string | null>(null);
|
||||
const [showResult, setShowResult] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// 簡化的邏輯
|
||||
const handleStartSession = async () => {
|
||||
const newSession = await studyService.startSession();
|
||||
setSession(newSession);
|
||||
loadCurrentTest(newSession.id);
|
||||
};
|
||||
|
||||
const handleSubmitAnswer = async (result: TestResult) => {
|
||||
await studyService.submitTest(session.id, result);
|
||||
loadNextTest();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 實施計劃
|
||||
|
||||
### 階段一:後端擴展 (預計2-3天)
|
||||
|
||||
#### Day 1: 數據模型和遷移
|
||||
- [ ] 創建 StudySession, StudyCard, TestResult 實體
|
||||
- [ ] 創建資料庫遷移
|
||||
- [ ] 更新 DbContext
|
||||
|
||||
#### Day 2: 服務層實現
|
||||
- [ ] 實現 StudySessionService
|
||||
- [ ] 實現 ReviewModeSelector
|
||||
- [ ] 單元測試
|
||||
|
||||
#### Day 3: API控制器
|
||||
- [ ] 實現 StudySessionController
|
||||
- [ ] API集成測試
|
||||
- [ ] Swagger文檔更新
|
||||
|
||||
### 階段二:前端重構 (預計2天)
|
||||
|
||||
#### Day 4: 服務層和狀態管理
|
||||
- [ ] 創建 StudySessionService
|
||||
- [ ] 重構 LearnPage 組件
|
||||
- [ ] 移除複雜的本地狀態
|
||||
|
||||
#### Day 5: UI組件優化
|
||||
- [ ] 簡化測驗組件
|
||||
- [ ] 更新導航邏輯
|
||||
- [ ] 錯誤處理優化
|
||||
|
||||
### 階段三:進度條美化 (預計1天)
|
||||
|
||||
#### Day 6: 分段式進度條
|
||||
- [ ] 設計 SegmentedProgressBar 組件
|
||||
- [ ] 實現詞卡分段顯示
|
||||
- [ ] 添加hover tooltip功能
|
||||
- [ ] 響應式布局優化
|
||||
|
||||
### 階段四:測試與部署 (預計1天)
|
||||
|
||||
#### Day 7: 完整測試
|
||||
- [ ] 端到端學習流程測試
|
||||
- [ ] 進度追蹤準確性驗證
|
||||
- [ ] 性能測試
|
||||
- [ ] 用戶體驗測試
|
||||
|
||||
## 數據流程圖
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[用戶開始學習] --> B[POST /sessions/start]
|
||||
B --> C[後端創建StudySession]
|
||||
C --> D[後端規劃詞卡測驗]
|
||||
D --> E[返回會話信息]
|
||||
E --> F[GET /sessions/{id}/current-test]
|
||||
F --> G[後端返回當前測驗]
|
||||
G --> H[前端顯示測驗UI]
|
||||
H --> I[用戶完成測驗]
|
||||
I --> J[POST /sessions/{id}/submit-test]
|
||||
J --> K[後端記錄結果]
|
||||
K --> L{該詞卡測驗完成?}
|
||||
L -->|否| M[GET /sessions/{id}/next-test]
|
||||
L -->|是| N[後端計算SM2並更新]
|
||||
N --> O{所有詞卡完成?}
|
||||
O -->|否| M
|
||||
O -->|是| P[PUT /sessions/{id}/complete]
|
||||
M --> G
|
||||
P --> Q[學習完成]
|
||||
```
|
||||
|
||||
## 關鍵優勢
|
||||
|
||||
### 可靠性提升
|
||||
- ✅ 數據一致性:狀態存在資料庫,不怕頁面刷新
|
||||
- ✅ 錯誤恢復:會話可暫停和恢復
|
||||
- ✅ 邏輯正確:只有完成所有測驗才標記詞卡完成
|
||||
|
||||
### 維護性改善
|
||||
- ✅ 業務邏輯集中:複習邏輯在後端統一管理
|
||||
- ✅ 前端簡化:專注UI渲染和用戶互動
|
||||
- ✅ 測試友好:API可獨立測試
|
||||
|
||||
### 用戶體驗優化
|
||||
- ✅ 進度條美化:分段式設計更直觀
|
||||
- ✅ 響應速度:減少前端複雜計算
|
||||
- ✅ 數據准確:實時同步學習進度
|
||||
|
||||
## 風險評估與應對
|
||||
|
||||
### 風險點
|
||||
1. **數據遷移風險**:現有學習記錄可能需要轉換
|
||||
2. **API性能風險**:頻繁API調用可能影響響應速度
|
||||
3. **向下兼容風險**:可能影響現有功能
|
||||
|
||||
### 應對措施
|
||||
1. **分階段部署**:先在測試環境驗證,再逐步上線
|
||||
2. **數據備份**:重構前完整備份現有數據
|
||||
3. **性能監控**:實施API性能監控和優化
|
||||
4. **回滾方案**:保留舊版本代碼,必要時快速回滾
|
||||
|
||||
## 成功標準
|
||||
|
||||
### 功能標準
|
||||
- [ ] 詞卡必須完成所有預定測驗才能標記為完成
|
||||
- [ ] 學習進度準確追蹤和顯示
|
||||
- [ ] 支持會話暫停和恢復
|
||||
- [ ] 分段式進度條正確顯示詞卡分佈
|
||||
|
||||
### 性能標準
|
||||
- [ ] API響應時間 < 500ms
|
||||
- [ ] 頁面載入時間 < 2s
|
||||
- [ ] 學習流程無明顯卡頓
|
||||
|
||||
### 用戶體驗標準
|
||||
- [ ] 學習流程直觀流暢
|
||||
- [ ] 進度顯示清晰準確
|
||||
- [ ] 錯誤處理友好
|
||||
|
||||
---
|
||||
|
||||
**創建時間**: 2025-09-26
|
||||
**負責人**: Claude Code
|
||||
**預計完成**: 2025-10-03 (7個工作天)
|
||||
Loading…
Reference in New Issue