feat: 建立開發/正式版本分離的進度組件架構

## 組件重組
- 創建 QuizProgressDev.tsx:給 /review-dev 使用,保留完整開發功能
- 修改 QuizProgress.tsx:給 /review 使用,移除開發測試資訊
- 頁面獨立:兩個頁面使用不同組件,互不影響

## TypeScript 修復
- 完善 CardState interface 類型兼容性
- 修復 primaryImageUrl, updatedAt, exampleTranslation 類型匹配
- 確保所有必需屬性都有預設值

## 頁面功能
- /review: 使用簡化版 QuizProgress,適合正式使用
- /review-dev: 使用完整版 QuizProgressDev,保留所有調試功能

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-10-07 02:21:13 +08:00
parent 473ecf4508
commit d0b6e9e757
5 changed files with 82 additions and 27 deletions

View File

@ -3,7 +3,7 @@
import { Navigation } from '@/components/shared/Navigation'
import { FlipMemory } from '@/components/review/quiz/FlipMemory'
import { VocabChoiceQuiz } from '@/components/review/quiz/VocabChoiceQuiz'
import { QuizProgress } from '@/components/review/ui/QuizProgress'
import { QuizProgressDev } from '@/components/review/ui/QuizProgressDev'
import { QuizResult } from '@/components/review/quiz/QuizResult'
import { useReviewSession } from '@/hooks/review/useReviewSession'
@ -115,7 +115,7 @@ export default function ReviewDevPage() {
<div className="py-8">
<div className="max-w-4xl mx-auto px-4">
{/* 使用修改後的 SimpleProgress 組件 */}
<QuizProgress
<QuizProgressDev
currentQuizItem={currentQuizItem}
totalQuizItems={totalQuizItems}
completedQuizItems={completedQuizItems}

View File

@ -114,13 +114,12 @@ export default function ReviewPage() {
<div className="py-8">
<div className="max-w-4xl mx-auto px-4">
{/* 使用修改後的 SimpleProgress 組件 */}
{/* 使用簡化的 ReviewProgress 組件 */}
<QuizProgress
currentQuizItem={currentQuizItem}
totalQuizItems={totalQuizItems}
completedQuizItems={completedQuizItems}
score={score}
quizItems={quizItems}
/>
{/* 根據當前測驗項目類型渲染對應組件 */}

View File

@ -22,23 +22,7 @@ export function QuizProgress({ currentQuizItem, totalQuizItems, completedQuizIte
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-3">
<div>
<span className="text-sm font-medium text-gray-600"></span>
{currentQuizItem && (
<div className="flex items-center mt-1">
<span className="text-lg mr-2">
{currentQuizItem.quizType === 'flip-card' ? '🔄' : '🎯'}
</span>
<span className="text-sm text-gray-500">
{currentQuizItem.quizType === 'flip-card' ? '翻卡記憶' : '詞彙選擇'} {currentQuizItem.cardData.word}
</span>
</div>
)}
</div>
<div className="flex items-center gap-4 text-sm text-right">
<span className="text-gray-600">
{completedQuizItems}/{totalQuizItems}
</span>
{score.total > 0 && (
<span className="text-green-600 font-medium">
{accuracy}%

View File

@ -0,0 +1,67 @@
import { QuizItem } from '@/lib/data/reviewSimpleData'
interface ReviewProgressProps {
currentQuizItem?: QuizItem
totalQuizItems: number
completedQuizItems: number
score: { correct: number; total: number }
}
export function QuizProgressDev({ currentQuizItem, totalQuizItems, completedQuizItems, score }: ReviewProgressProps) {
const progress = (completedQuizItems / totalQuizItems) * 100
const accuracy = score.total > 0 ? Math.round((score.correct / score.total) * 100) : 0
return (
<div className="mb-8">
<div className="flex justify-between items-center mb-3">
<div>
<span className="text-sm font-medium text-gray-600"></span>
{currentQuizItem && (
<div className="flex items-center mt-1">
<span className="text-lg mr-2">
{currentQuizItem.quizType === 'flip-card' ? '🔄' : '🎯'}
</span>
<span className="text-sm text-gray-500">
{currentQuizItem.quizType === 'flip-card' ? '翻卡記憶' : '詞彙選擇'} {currentQuizItem.cardData.word}
</span>
</div>
)}
</div>
<div className="flex items-center gap-4 text-sm text-right">
<span className="text-gray-600">
{completedQuizItems}/{totalQuizItems}
</span>
{score.total > 0 && (
<span className="text-green-600 font-medium">
{accuracy}%
</span>
)}
</div>
</div>
{/* 進度條 */}
<div className="w-full bg-gray-200 rounded-full h-3">
<div
className="bg-blue-500 h-3 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
{/* 基本統計 */}
<div className="flex justify-center gap-4 mt-3 text-sm">
{score.total > 0 && (
<>
<div className="flex items-center gap-1">
<span className="w-2 h-2 bg-green-500 rounded-full"></span>
<span className="text-green-700"> {score.correct}</span>
</div>
<div className="flex items-center gap-1">
<span className="w-2 h-2 bg-red-500 rounded-full"></span>
<span className="text-red-700"> {score.total - score.correct}</span>
</div>
</>
)}
</div>
</div>
)
}

View File

@ -1,14 +1,17 @@
import { useReducer, useEffect, useMemo, useState } from 'react'
import { flashcardsService, Flashcard } from '@/lib/services/flashcards'
// 重新定義所需的介面
// 重新定義所需的介面,確保與 reviewSimpleData.CardState 完全兼容
interface CardState extends Flashcard {
skipCount: number
wrongCount: number
isCompleted: boolean
originalOrder: number
synonyms?: string[]
difficultyLevelNumeric: number // 添加缺少的屬性
synonyms: string[] // 改為必需的數組
difficultyLevelNumeric: number // 必需屬性
exampleTranslation: string // 覆蓋 Flashcard 的可選定義,改為必需
updatedAt: string // 覆蓋 Flashcard 的可選定義,改為必需
primaryImageUrl: string | null // 確保類型匹配null 而非 undefined
}
interface QuizItem {
@ -89,16 +92,18 @@ const generateQuizItemsFromFlashcards = (flashcards: Flashcard[]): QuizItem[] =>
let order = 0
flashcards.forEach((card) => {
// 轉換 Flashcard 為 CardState 格式
// 轉換 Flashcard 為 CardState 格式,確保所有屬性都有值
const cardState: CardState = {
...card,
exampleTranslation: card.exampleTranslation || '', // 確保 exampleTranslation 不為 undefined
exampleTranslation: card.exampleTranslation || '', // 確保為 string不是 undefined
updatedAt: card.updatedAt || card.createdAt, // 確保 updatedAt 為 string
primaryImageUrl: card.primaryImageUrl || null, // 確保為 null 而非 undefined
skipCount: 0,
wrongCount: 0,
isCompleted: false,
originalOrder: order / 2, // 原始詞卡的順序
synonyms: [],
difficultyLevelNumeric: card.masteryLevel || 1 // 添加缺少的屬性
synonyms: [], // 確保為空數組而非 undefined
difficultyLevelNumeric: card.masteryLevel || 1 // 使用 masteryLevel 或預設值 1
}
// 為每張詞卡生成兩種測驗模式