dramaling-vocab-learning/note/智能複習_old/詞彙學習-UI設計規範.md

35 KiB
Raw Blame History

詞彙學習 - UI設計規範

文件版本: 1.0 建立日期: 2025-09-27 目標讀者: 前端實作工程師、UI設計師 用途: HTML/CSS實作、視覺設計、組件規範 配合文檔: 智能複習系統-開發指南.md - 系統架構和業務邏輯 來源: 從備份檔案 page-v1-original.tsx 提取的完整UI設計規格


📋 總體設計架構

核心測驗類型

該系統包含6種主要測驗類型

  1. 翻卡記憶 (flip-memory)
  2. 詞彙選擇 (vocab-choice)
  3. 例句填空 (sentence-fill)
  4. 例句重組 (sentence-reorder)
  5. 聽力測驗 (listening - vocab-listening, sentence-listening)
  6. 口說測驗 (speaking - sentence-speaking)

共同設計元素

通用容器結構

<div className="relative">
  {/* 錯誤回報按鈕 */}
  <div className="flex justify-end mb-2">
    <button className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900">
      🚩 回報錯誤
    </button>
  </div>

  {/* 主要內容區 */}
  <div className="bg-white rounded-xl shadow-lg p-8">
    {/* 測驗內容 */}
  </div>

  {/* 導航按鈕 */}
  <div className="flex gap-4 mt-6">
    <button className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium">
      上一張
    </button>
    <button className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium">
      下一張/完成
    </button>
  </div>
</div>

通用標題區

<div className="flex justify-between items-start mb-6">
  <h2 className="text-2xl font-bold text-gray-900">
    {測驗名稱}
  </h2>
  <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
    {currentCard.difficultyLevel}
  </span>
</div>

配色方案

  • 主色調: bg-primary, text-white, hover:bg-primary-dark
  • 次要色調: bg-gray-500, hover:bg-gray-600
  • 成功狀態: bg-green-50, border-green-200, text-green-700
  • 錯誤狀態: bg-red-50, border-red-200, text-red-700
  • 提示狀態: bg-yellow-50, border-yellow-200, text-yellow-800
  • 信息狀態: bg-blue-50, border-blue-200, text-blue-700

🔄 1. 翻卡記憶 (flip-memory)

核心特色

  • 3D翻卡動畫效果
  • 正面顯示單字,背面顯示詳細資訊
  • 信心等級評估系統

HTML結構

<div
  className="card-container"
  onClick={handleFlip}
  style={{ height: `${cardHeight}px` }}
>
  <div className={`card ${isFlipped ? 'flipped' : ''}`}>
    {/* 正面 */}
    <div className="card-front">
      <div className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow p-8">
        {/* 標題區 */}
        <div className="flex justify-between items-start mb-6">
          <h2 className="text-2xl font-bold text-gray-900">翻卡記憶</h2>
          <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
            {difficultyLevel}
          </span>
        </div>

        {/* 指示文字 */}
        <p className="text-lg text-gray-700 mb-6 text-left">
          點擊卡片翻面根據你對單字的熟悉程度進行自我評估
        </p>

        {/* 單字顯示區 */}
        <div className="flex-1 flex items-center justify-center mt-6">
          <div className="bg-gray-50 rounded-lg p-8 w-full text-center">
            <h3 className="text-4xl font-bold text-gray-900 mb-6">{word}</h3>
            <div className="flex items-center justify-center gap-3">
              <span className="text-lg text-gray-500">{pronunciation}</span>
              <AudioPlayer text={word} />
            </div>
          </div>
        </div>
      </div>
    </div>

    {/* 背面 */}
    <div className="card-back">
      <div className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow">
        <div className="space-y-4">
          {/* 定義區塊 */}
          <div className="bg-gray-50 rounded-lg p-4">
            <h3 className="font-semibold text-gray-900 mb-2 text-left">定義</h3>
            <p className="text-gray-700 text-left">{definition}</p>
          </div>

          {/* 例句區塊 */}
          <div className="bg-gray-50 rounded-lg p-4">
            <h3 className="font-semibold text-gray-900 mb-2 text-left">例句</h3>
            <div className="relative">
              <p className="text-gray-700 italic mb-2 text-left pr-12">{example}</p>
              <div className="absolute bottom-0 right-0">
                <AudioPlayer text={example} />
              </div>
            </div>
            <p className="text-gray-600 text-sm text-left">{exampleTranslation}</p>
          </div>

          {/* 同義詞區塊 */}
          <div className="bg-gray-50 rounded-lg p-4">
            <h3 className="font-semibold text-gray-900 mb-2 text-left">同義詞</h3>
            <div className="flex flex-wrap gap-2">
              {synonyms.map(synonym => (
                <span className="bg-white text-gray-700 px-3 py-1 rounded-full text-sm border border-gray-200">
                  {synonym}
                </span>
              ))}
            </div>
          </div>

          {/* 信心等級評估區 */}
          <div className="bg-blue-50 rounded-lg p-4">
            <h3 className="font-semibold text-blue-900 mb-3 text-left">你對這個單字的熟悉程度</h3>
            <div className="grid grid-cols-5 gap-2">
              {[1, 2, 3, 4, 5].map(level => (
                <button
                  key={level}
                  onClick={() => handleConfidenceLevel(level)}
                  className="py-3 px-4 bg-white border border-blue-200 text-blue-700 rounded-lg hover:bg-blue-100 hover:border-blue-300 transition-all text-center"
                >
                  <div className="font-bold text-lg">{level}</div>
                  <div className="text-xs">
                    {level === 1 ? '完全不懂' :
                     level === 2 ? '模糊' :
                     level === 3 ? '一般' :
                     level === 4 ? '熟悉' : '非常熟悉'}
                  </div>
                </button>
              ))}
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

CSS動畫樣式

.card-container {
  perspective: 1000px;
  transition: height 0.3s ease;
  overflow: visible;
  position: relative;
}

.card {
  position: relative;
  width: 100%;
  height: 100%;
  text-align: center;
  transition: transform 0.6s ease;
  transform-style: preserve-3d;
}

.card.flipped {
  transform: rotateY(180deg);
}

.card-front, .card-back {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
  display: flex;
  align-items: stretch;
  justify-content: center;
  top: 0;
  left: 0;
}

.card-back {
  transform: rotateY(180deg);
}

🎯 2. 詞彙選擇 (vocab-choice)

核心特色

  • 四選一選擇題
  • 顯示詞彙定義,選擇對應單字
  • 即時反饋系統

HTML結構

<div className="bg-white rounded-xl shadow-lg p-8">
  {/* 標題區 */}
  <div className="flex justify-between items-start mb-6">
    <h2 className="text-2xl font-bold text-gray-900">詞彙選擇</h2>
    <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
      {difficultyLevel}
    </span>
  </div>

  {/* 指示文字 */}
  <p className="text-lg text-gray-700 mb-6 text-left">
    請選擇符合上述定義的英文詞彙
  </p>

  {/* 定義顯示區 */}
  <div className="text-center mb-8">
    <div className="bg-gray-50 rounded-lg p-4 mb-6">
      <h3 className="font-semibold text-gray-900 mb-2 text-left">定義</h3>
      <p className="text-gray-700 text-left">{definition}</p>
    </div>
  </div>

  {/* 選項區域 */}
  <div className="space-y-3 mb-6">
    {quizOptions.map((option, idx) => (
      <button
        key={idx}
        onClick={() => !showResult && handleQuizAnswer(option)}
        disabled={showResult}
        className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
          showResult
            ? option === correctAnswer
              ? 'border-green-500 bg-green-50 text-green-700'
              : option === selectedAnswer
              ? 'border-red-500 bg-red-50 text-red-700'
              : 'border-gray-200 bg-gray-50 text-gray-500'
            : 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
        }`}
      >
        {option}
      </button>
    ))}
  </div>

  {/* 結果反饋區 */}
  {showResult && (
    <div className={`p-6 rounded-lg w-full mb-6 ${
      selectedAnswer === correctAnswer
        ? 'bg-green-50 border border-green-200'
        : 'bg-red-50 border border-red-200'
    }`}>
      <p className={`font-semibold text-left text-xl mb-4 ${
        selectedAnswer === correctAnswer ? 'text-green-700' : 'text-red-700'
      }`}>
        {selectedAnswer === correctAnswer ? '正確!' : '錯誤!'}
      </p>

      {selectedAnswer !== correctAnswer && (
        <div className="mb-4">
          <p className="text-gray-700 text-left">
            正確答案是<strong className="text-lg">{correctAnswer}</strong>
          </p>
        </div>
      )}

      <div className="space-y-3">
        <div className="text-left">
          <div className="flex items-center text-gray-600">
            <span className="mx-2">{pronunciation}</span>
            <AudioPlayer text={correctAnswer} />
          </div>
        </div>

        <div className="text-left">
          <p className="text-gray-600">
            <strong>例句</strong>{example}
          </p>
          <p className="text-gray-500 text-sm">
            <strong>翻譯</strong>{exampleTranslation}
          </p>
        </div>
      </div>
    </div>
  )}
</div>

✏️ 3. 例句填空 (sentence-fill)

核心特色

  • 內嵌式輸入框設計
  • 動態寬度調整
  • 提示系統
  • 支援圖片顯示

HTML結構

<div className="bg-white rounded-xl shadow-lg p-8">
  {/* 標題區 */}
  <div className="flex justify-between items-start mb-6">
    <h2 className="text-2xl font-bold text-gray-900">例句填空</h2>
    <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
      {difficultyLevel}
    </span>
  </div>

  {/* 圖片區(如果有) */}
  {exampleImage && (
    <div className="mb-6">
      <div className="bg-gray-50 rounded-lg p-4">
        <h3 className="font-semibold text-gray-900 mb-2 text-left">圖片提示</h3>
        <img
          src={exampleImage}
          alt="Example illustration"
          className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
          onClick={() => setModalImage(exampleImage)}
        />
      </div>
    </div>
  )}

  {/* 指示文字 */}
  <p className="text-lg text-gray-700 mb-6 text-left">
    請點擊例句中的空白處輸入正確的單字
  </p>

  {/* 填空句子區域 */}
  <div className="mb-6">
    <div className="bg-gray-50 rounded-lg p-6">
      <div className="text-lg text-gray-700 leading-relaxed">
        {/* 動態生成填空句子 */}
        {sentence.split(targetWord).map((part, index) => {
          const isTargetWord = index > 0;
          return (
            <React.Fragment key={index}>
              <span>{part}</span>
              {isTargetWord && (
                <span className="relative inline-block mx-1">
                  <input
                    type="text"
                    value={fillAnswer}
                    onChange={(e) => setFillAnswer(e.target.value)}
                    onKeyDown={(e) => {
                      if (e.key === 'Enter' && !showResult && fillAnswer.trim()) {
                        handleFillAnswer()
                      }
                    }}
                    placeholder=""
                    disabled={showResult}
                    className={`inline-block px-2 py-1 text-center bg-transparent focus:outline-none disabled:bg-gray-100 ${
                      fillAnswer
                        ? 'border-b-2 border-blue-500'
                        : 'border-b-2 border-dashed border-gray-400 focus:border-blue-400 focus:border-solid'
                    }`}
                    style={{
                      width: `${Math.max(100, Math.max(targetWord.length * 12, fillAnswer.length * 12 + 20))}px`
                    }}
                  />
                  {!fillAnswer && (
                    <span
                      className="absolute inset-0 flex items-center justify-center text-gray-400 pointer-events-none"
                      style={{ paddingBottom: '8px' }}
                    >
                      ____
                    </span>
                  )}
                </span>
              )}
            </React.Fragment>
          )
        })}
      </div>
    </div>
  </div>

  {/* 操作按鈕 */}
  <div className="flex gap-3 mb-4">
    {!showResult && fillAnswer.trim() && (
      <button
        onClick={handleFillAnswer}
        className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
      >
        確認答案
      </button>
    )}
    <button
      onClick={() => setShowHint(!showHint)}
      className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
    >
      {showHint ? '隱藏提示' : '顯示提示'}
    </button>
  </div>

  {/* 提示區域 */}
  {showHint && (
    <div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
      <h4 className="font-semibold text-yellow-800 mb-2">詞彙定義</h4>
      <p className="text-yellow-800">{definition}</p>
    </div>
  )}

  {/* 結果反饋區 */}
  {showResult && (
    <div className={`mt-6 p-6 rounded-lg w-full ${
      fillAnswer.toLowerCase().trim() === targetWord.toLowerCase()
        ? 'bg-green-50 border border-green-200'
        : 'bg-red-50 border border-red-200'
    }`}>
      <p className={`font-semibold text-left text-xl mb-4 ${
        fillAnswer.toLowerCase().trim() === targetWord.toLowerCase()
          ? 'text-green-700'
          : 'text-red-700'
      }`}>
        {fillAnswer.toLowerCase().trim() === targetWord.toLowerCase() ? '正確!' : '錯誤!'}
      </p>

      {fillAnswer.toLowerCase().trim() !== targetWord.toLowerCase() && (
        <div className="mb-4">
          <p className="text-gray-700 text-left">
            正確答案是<strong className="text-lg">{targetWord}</strong>
          </p>
        </div>
      )}

      <div className="space-y-3">
        <div className="text-left">
          <p className="text-gray-600">
            <strong>發音</strong>
            <span className="mx-2">{pronunciation}</span>
            <AudioPlayer text={targetWord} />
          </p>
        </div>

        <div className="text-left">
          <p className="text-gray-600">
            <strong>完整例句</strong>"{completeExample}"
          </p>
          <p className="text-gray-500 text-sm">
            <strong>翻譯</strong>"{exampleTranslation}"
          </p>
        </div>
      </div>
    </div>
  )}
</div>

🧩 4. 例句重組 (sentence-reorder)

核心特色

  • 拖拽式單字重組
  • 雙區域設計(可用單字區 + 重組區域)
  • 即時狀態反饋

HTML結構

<div className="bg-white rounded-xl shadow-lg p-8">
  {/* 標題區 */}
  <div className="flex justify-between items-start mb-6">
    <h2 className="text-2xl font-bold text-gray-900">例句重組</h2>
    <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
      {difficultyLevel}
    </span>
  </div>

  {/* 圖片區(如果有) */}
  {exampleImage && (
    <div className="mb-6">
      <div className="bg-gray-50 rounded-lg p-4">
        <h3 className="font-semibold text-gray-900 mb-2 text-left">圖片提示</h3>
        <img
          src={exampleImage}
          alt="Example illustration"
          className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
          onClick={() => setModalImage(exampleImage)}
        />
      </div>
    </div>
  )}

  {/* 重組區域 */}
  <div className="mb-6">
    <h3 className="text-lg font-semibold text-gray-900 mb-3 text-left">重組區域</h3>
    <div className="relative min-h-[120px] bg-gray-50 rounded-lg p-4 border-2 border-dashed border-gray-300">
      {arrangedWords.length === 0 ? (
        <div className="absolute inset-0 flex items-center justify-center text-gray-400 text-lg">
          將下方單字拖拽到這裡組成完整句子
        </div>
      ) : (
        <div className="flex flex-wrap gap-2">
          {arrangedWords.map((word, index) => (
            <div
              key={`arranged-${index}`}
              className="inline-flex items-center bg-blue-100 text-blue-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-blue-200 transition-colors"
              onClick={() => handleRemoveFromArranged(word)}
            >
              {word}
              <span className="ml-2 text-blue-600 hover:text-blue-800">×</span>
            </div>
          ))}
        </div>
      )}
    </div>
  </div>

  {/* 指示文字 */}
  <p className="text-lg text-gray-700 mb-6 text-left">
    點擊下方單字依序重組成正確的句子
  </p>

  {/* 可用單字區域 */}
  <div className="mb-6">
    <h3 className="text-lg font-semibold text-gray-900 mb-3 text-left">可用單字</h3>
    <div className="bg-white border border-gray-200 rounded-lg p-4 min-h-[80px]">
      {shuffledWords.length === 0 ? (
        <div className="text-center text-gray-400">
          所有單字都已使用
        </div>
      ) : (
        <div className="flex flex-wrap gap-2">
          {shuffledWords.map((word, index) => (
            <button
              key={`shuffled-${index}`}
              onClick={() => handleWordClick(word)}
              className="bg-gray-100 text-gray-800 px-3 py-2 rounded-full text-lg font-medium cursor-pointer hover:bg-gray-200 active:bg-gray-300 transition-colors select-none"
            >
              {word}
            </button>
          ))}
        </div>
      )}
    </div>
  </div>

  {/* 控制按鈕 */}
  <div className="flex gap-3 mb-6">
    {arrangedWords.length > 0 && (
      <button
        onClick={handleCheckReorderAnswer}
        className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors"
      >
        檢查答案
      </button>
    )}
    <button
      onClick={handleResetReorder}
      className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
    >
      重新開始
    </button>
  </div>

  {/* 結果反饋區 */}
  {reorderResult !== null && (
    <div className={`p-6 rounded-lg w-full mb-6 ${
      reorderResult
        ? 'bg-green-50 border border-green-200'
        : 'bg-red-50 border border-red-200'
    }`}>
      <p className={`font-semibold text-left text-xl mb-4 ${
        reorderResult ? 'text-green-700' : 'text-red-700'
      }`}>
        {reorderResult ? '正確!' : '錯誤!'}
      </p>

      {!reorderResult && (
        <div className="mb-4">
          <p className="text-gray-700 text-left">
            正確答案是<strong className="text-lg">{correctSentence}</strong>
          </p>
        </div>
      )}

      <div className="space-y-3">
        <div className="text-left">
          <div className="flex items-center text-gray-600">
            <AudioPlayer text={correctSentence} />
          </div>
        </div>

        <div className="text-left">
          <p className="text-gray-600">
            {translation}
          </p>
        </div>
      </div>
    </div>
  )}
</div>

🎧 5. 聽力測驗 (listening)

5.1 詞彙聽力 (vocab-listening)

<div className="bg-white rounded-xl shadow-lg p-8">
  {/* 標題區 */}
  <div className="flex justify-between items-start mb-6">
    <h2 className="text-2xl font-bold text-gray-900">詞彙聽力</h2>
    <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
      {difficultyLevel}
    </span>
  </div>

  {/* 指示文字 */}
  <p className="text-lg text-gray-700 mb-6 text-left">
    請聽發音並選擇正確的英文單字
  </p>

  {/* 音頻播放區 */}
  <div className="space-y-4 mb-8">
    <div className="bg-gray-50 rounded-lg p-4">
      <h3 className="font-semibold text-gray-900 mb-2 text-left">發音</h3>
      <div className="flex items-center gap-3">
        <span className="text-gray-700">{pronunciation}</span>
        <AudioPlayer text={word} />
      </div>
    </div>
  </div>

  {/* 選項區域 - 2x2網格布局 */}
  <div className="grid grid-cols-2 gap-3 mb-6">
    {options.map((word) => (
      <button
        key={word}
        onClick={() => !showResult && handleListeningAnswer(word)}
        disabled={showResult}
        className={`p-4 text-center rounded-lg border-2 transition-all ${
          showResult
            ? word === correctAnswer
              ? 'border-green-500 bg-green-50 text-green-700'
              : word === selectedAnswer
              ? 'border-red-500 bg-red-50 text-red-700'
              : 'border-gray-200 bg-gray-50 text-gray-500'
            : 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
        }`}
      >
        <div className="text-lg font-medium">{word}</div>
        <div className="text-sm text-gray-500 mt-1">{pronunciation}</div>
      </button>
    ))}
  </div>

  {/* 結果反饋區 */}
  {showResult && (
    <div className={`p-6 rounded-lg w-full mb-6 ${
      selectedAnswer === correctAnswer
        ? 'bg-green-50 border border-green-200'
        : 'bg-red-50 border border-red-200'
    }`}>
      <p className={`font-semibold text-left text-xl mb-4 ${
        selectedAnswer === correctAnswer ? 'text-green-700' : 'text-red-700'
      }`}>
        {selectedAnswer === correctAnswer ? '正確!' : '錯誤!'}
      </p>

      <div className="space-y-3">
        <div className="text-left">
          <p className="text-gray-700">
            <strong>正確單字</strong>{correctAnswer}
          </p>
          <p className="text-gray-600">
            <strong>定義</strong>{definition}
          </p>
        </div>
      </div>
    </div>
  )}
</div>

5.2 例句聽力 (sentence-listening)

<div className="bg-white rounded-xl shadow-lg p-8">
  {/* 標題區 */}
  <div className="flex justify-between items-start mb-6">
    <h2 className="text-2xl font-bold text-gray-900">例句聽力</h2>
    <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
      {difficultyLevel}
    </span>
  </div>

  {/* 指示文字 */}
  <p className="text-lg text-gray-700 mb-6 text-left">
    請聽例句並選擇正確的選項
  </p>

  {/* 音頻播放區 */}
  <div className="text-center mb-8">
    <div className="mb-6">
      <AudioPlayer text={example} />
      <p className="text-sm text-gray-500 mt-2">
        點擊播放聽例句
      </p>
    </div>
  </div>

  {/* 選項區域 - 垂直列表布局 */}
  <div className="grid grid-cols-1 gap-3 mb-6">
    {sentenceOptions.map((sentence, idx) => (
      <button
        key={idx}
        onClick={() => !showResult && handleSentenceListeningAnswer(sentence)}
        disabled={showResult}
        className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
          showResult
            ? sentence === correctSentence
              ? 'border-green-500 bg-green-50 text-green-700'
              : sentence === selectedAnswer
              ? 'border-red-500 bg-red-50 text-red-700'
              : 'border-gray-200 bg-gray-50 text-gray-500'
            : 'border-gray-200 hover:border-blue-300 hover:bg-blue-50'
        }`}
      >
        <div className="text-sm text-gray-600 mb-1">選項 {String.fromCharCode(65 + idx)}:</div>
        <div className="text-base">{sentence}</div>
      </button>
    ))}
  </div>

  {/* 結果反饋區 */}
  {showResult && (
    <div className={`p-6 rounded-lg w-full mb-6 ${
      selectedAnswer === correctSentence
        ? 'bg-green-50 border border-green-200'
        : 'bg-red-50 border border-red-200'
    }`}>
      <p className={`font-semibold text-left text-xl mb-4 ${
        selectedAnswer === correctSentence ? 'text-green-700' : 'text-red-700'
      }`}>
        {selectedAnswer === correctSentence ? '正確!' : '錯誤!'}
      </p>

      <div className="space-y-3">
        <div className="text-left">
          <p className="text-gray-700">
            {correctSentence}
          </p>
          <p className="text-gray-600">
            {translation}
          </p>
        </div>
      </div>
    </div>
  )}
</div>

🎤 6. 口說測驗 (sentence-speaking)

核心特色

  • 整合 VoiceRecorder 組件
  • 語音識別評估
  • 即時反饋系統

HTML結構

<div className="bg-white rounded-xl shadow-lg p-8">
  {/* 標題區 */}
  <div className="flex justify-between items-start mb-6">
    <h2 className="text-2xl font-bold text-gray-900">例句口說</h2>
    <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
      {difficultyLevel}
    </span>
  </div>

  {/* VoiceRecorder 組件區域 */}
  <div className="w-full">
    <VoiceRecorder
      targetText={example}
      targetTranslation={exampleTranslation}
      exampleImage={exampleImage}
      instructionText="請看例句圖片並大聲說出完整的例句:"
      onRecordingComplete={() => {
        handleSpeakingAnswer(example)
      }}
    />
  </div>

  {/* 結果反饋區 */}
  {showResult && (
    <div className="mt-6 p-6 rounded-lg bg-blue-50 border border-blue-200 w-full">
      <p className="text-blue-700 text-left text-xl font-semibold mb-2">
        錄音完成
      </p>
      <p className="text-gray-600 text-left">
        系統正在評估你的發音...
      </p>
    </div>
  )}
</div>

📊 進度條系統設計

雙層進度條

<div className="mb-8">
  <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="flex items-center gap-2 text-sm text-gray-600">
        <span>詞卡: {completedCards}/{totalCards}</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="點擊查看詳細進度"
      >
        測驗: {completedTests}/{totalTests}
        <span className="text-xs ml-1">📋</span>
      </button>
    </div>
  </div>

  {/* 詞卡進度條 */}
  <div className="flex items-center gap-2 mb-2">
    <span className="text-xs text-gray-500 w-12">詞卡</span>
    <div className="flex-1 bg-gray-200 rounded-full h-2">
      <div
        className="bg-green-500 h-2 rounded-full transition-all"
        style={{ width: `${(completedCards / totalCards) * 100}%` }}
      />
    </div>
    <span className="text-xs text-gray-500 w-8">{completedCards}/{totalCards}</span>
  </div>

  {/* 測驗進度條 */}
  <div className="flex items-center gap-2">
    <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: `${(completedTests / totalTests) * 100}%` }}
      />
    </div>
    <span className="text-xs text-gray-500 w-8">{completedTests}/{totalTests}</span>
  </div>
</div>

🎨 通用Modal設計

圖片放大Modal

<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50" onClick={() => setModalImage(null)}>
  <div className="relative max-w-4xl max-h-[90vh] mx-4">
    <img
      src={modalImage}
      alt="放大圖片"
      className="max-w-full max-h-full rounded-lg"
    />
    <button
      onClick={() => setModalImage(null)}
      className="absolute top-4 right-4 bg-black bg-opacity-50 text-white p-2 rounded-full hover:bg-opacity-75"
    >
      
    </button>
  </div>
</div>

任務清單Modal

<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 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} ({progressPercentage}%)
          </span>
          <div className="flex items-center gap-4 text-blue-800">
            <span> 已完成: {completedCount}</span>
            <span> 進行中: {currentCount}</span>
            <span> 待完成: {pendingCount}</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: `${progressPercentage}%` }}
          />
        </div>
      </div>

      {/* 測驗清單 */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
        {testItems.map((item) => (
          <div
            key={item.id}
            className={`flex items-center gap-3 p-3 rounded-lg transition-all ${
              item.isCompleted
                ? 'bg-green-50 border border-green-200'
                : item.isCurrent
                ? 'bg-blue-50 border border-blue-300 shadow-sm'
                : 'bg-gray-50 border border-gray-200'
            }`}
          >
            <span className="text-lg">
              {item.isCompleted ? '✅' : item.isCurrent ? '⏳' : '⚪'}
            </span>
            <div className="flex-1">
              <div className="font-medium text-sm">
                {item.order}. {item.word} - {item.testName}
              </div>
              <div className={`text-xs ${
                item.isCompleted ? 'text-green-600' :
                item.isCurrent ? 'text-blue-600' : 'text-gray-500'
              }`}>
                {item.isCompleted ? '已完成' :
                 item.isCurrent ? '進行中' : '待完成'}
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>

    {/* Footer */}
    <div className="px-6 py-4 border-t border-gray-200 bg-gray-50">
      <button className="w-full py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors">
        關閉
      </button>
    </div>
  </div>
</div>

🎮 導航控制設計

智能導航按鈕

<div className="flex gap-4 mt-6">
  {/* 上一張按鈕 */}
  <button
    onClick={handlePrevious}
    disabled={currentCardIndex === 0}
    className="flex-1 py-3 bg-gray-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-600 transition-colors font-medium"
  >
    上一張
  </button>

  {/* 動態下一張按鈕 */}
  <button
    onClick={handleNext}
    className="flex-1 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors font-medium"
  >
    {currentCardIndex < totalCards - 1 ? '下一張' : '完成學習'}
  </button>
</div>

跳過功能按鈕

{/* 跳過按鈕(答題前狀態) */}
{!showResult && (
  <div className="flex justify-center mb-4">
    <button
      onClick={handleSkip}
      className="px-6 py-2 bg-yellow-500 text-white rounded-lg hover:bg-yellow-600 transition-colors"
    >
      ⏭️ 暫時跳過
    </button>
  </div>
)}

{/* 繼續按鈕(答題後狀態) */}
{showResult && (
  <div className="flex justify-center mt-6">
    <button
      onClick={handleNext}
      className="px-8 py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors font-medium"
    >
      繼續下一個 
    </button>
  </div>
)}

🎨 設計特色總結

1. 一致性設計

  • 所有測驗類型都採用相同的容器結構 (bg-white rounded-xl shadow-lg p-8)
  • 統一的配色方案和視覺層次
  • 標準化的按鈕樣式和hover效果

2. 響應式布局

  • 使用 Grid 和 Flexbox 布局
  • 適配不同螢幕尺寸 (md:grid-cols-2 lg:grid-cols-3)
  • 移動端友好的觸控設計

3. 互動動畫

  • 翻卡3D效果 (transform: rotateY(180deg))
  • Hover狀態轉換 (transition-all, hover:bg-blue-50)
  • 按鈕點擊反饋 (active:bg-gray-300)

4. 無障礙設計

  • 鍵盤導航支援 (onKeyDown Enter 鍵提交)
  • 清晰的狀態反饋 (disabled 狀態顯示)
  • 適當的對比度和字體大小

5. 視覺層次

  • 合理的間距系統 (mb-6, p-4, gap-3)
  • 字體大小層次 (text-4xl, text-2xl, text-lg, text-sm)
  • 顏色對比設計 (text-gray-900, text-gray-700, text-gray-500)

6. 即時反饋

  • 答題後的即時視覺反饋(綠色成功,紅色錯誤)
  • 動態進度條更新
  • 狀態圖標顯示 (, , , )

📝 實作注意事項

必需依賴組件

  1. AudioPlayer - 音頻播放功能
  2. VoiceRecorder - 語音錄製功能
  3. MasteryIndicator - 熟悉度顯示
  4. ReviewTypeIndicator - 測驗類型指示器

關鍵狀態管理

  • isFlipped - 翻卡狀態
  • showResult - 顯示答題結果
  • selectedAnswer - 用戶選擇的答案
  • fillAnswer - 填空題答案
  • arrangedWords / shuffledWords - 重組題狀態

動畫效果實作

  • 使用 CSS transition-all 實現平滑過渡
  • 翻卡動畫需要 transform-style: preserve-3d
  • 進度條動畫使用 transition-all 配合動態 width 樣式

這份文件保留了原始設計的所有精華和用戶體驗細節,可作為重新實作的完整參考。