dramaling-vocab-learning/note/done/FRONTEND_FLASHCARD_DATA_FLO...

14 KiB
Raw Blame History

前端詞卡管理資料流程圖

📋 文檔概覽

本文檔詳細說明前端詞卡管理功能如何取得詞卡及其例句圖片,並顯示在用戶介面上的完整資料流程。


🏗️ 整體架構圖

graph TB
    A[用戶訪問 /flashcards] --> B[FlashcardsContent 組件初始化]
    B --> C[useEffect 觸發資料載入]
    C --> D[flashcardsService.getFlashcards()]
    D --> E[HTTP GET /api/flashcards]
    E --> F[FlashcardsController.GetFlashcards()]
    F --> G[EF Core 查詢 + Include 圖片關聯]
    G --> H[資料庫查詢 flashcards + example_images]
    H --> I[IImageStorageService.GetImageUrlAsync()]
    I --> J[組裝回應資料]
    J --> K[前端接收 flashcards 陣列]
    K --> L[狀態更新 setFlashcards()]
    L --> M[UI 重新渲染]
    M --> N[FlashcardItem 組件渲染]
    N --> O[圖片顯示邏輯判斷]
    O --> P{有例句圖片?}
    P -->|Yes| Q[顯示圖片 <img>]
    P -->|No| R[顯示新增按鈕]
    Q --> S[響應式圖片縮放]
    R --> T[點擊觸發 handleGenerateExampleImage]

🔄 詳細資料流程

第1階段頁面初始化

1.1 組件載入

// /frontend/app/flashcards/page.tsx
export default function FlashcardsPage() {
  return (
    <ProtectedRoute>
      <FlashcardsContent />
    </ProtectedRoute>
  )
}

function FlashcardsContent() {
  const [searchState, searchActions] = useFlashcardSearch(activeTab)

  useEffect(() => {
    loadTotalCounts()  // 初始化資料載入
  }, [])
}

1.2 資料載入觸發

sequenceDiagram
    participant UC as 用戶
    participant FC as FlashcardsContent
    participant FS as flashcardsService
    participant API as Backend API

    UC->>FC: 訪問 /flashcards
    FC->>FC: useEffect 觸發
    FC->>FS: searchActions.refresh()
    FS->>API: GET /api/flashcards

第2階段後端資料處理

2.1 API 端點處理

// FlashcardsController.GetFlashcards()
var query = _context.Flashcards
    .Include(f => f.FlashcardExampleImages)      // 載入圖片關聯
        .ThenInclude(fei => fei.ExampleImage)   // 載入圖片詳情
    .Where(f => f.UserId == userId && !f.IsArchived)
    .AsQueryable();

2.2 資料庫查詢流程

graph LR
    A[Flashcards Table] --> B[FlashcardExampleImages Table]
    B --> C[ExampleImages Table]
    A --> D[User Filter]
    A --> E[Search Filter]
    A --> F[CEFR Filter]
    C --> G[Image URL Generation]
    G --> H[完整 JSON 回應]

2.3 圖片資料組裝

// 每個 flashcard 處理圖片關聯
foreach (var flashcardImage in flashcard.FlashcardExampleImages)
{
    var imageUrl = await _imageStorageService.GetImageUrlAsync(
        flashcardImage.ExampleImage.RelativePath
    );

    exampleImages.Add(new ExampleImageDto
    {
        Id = flashcardImage.ExampleImage.Id.ToString(),
        ImageUrl = imageUrl,  // 完整的 HTTP URL
        IsPrimary = flashcardImage.IsPrimary,
        QualityScore = flashcardImage.ExampleImage.QualityScore
    });
}

第3階段前端資料接收與處理

3.1 API 回應結構

{
  "success": true,
  "data": {
    "flashcards": [
      {
        "id": "94c32b17-53a7-4de5-9bfc-f6d4f2dc1368",
        "word": "up",
        "translation": "出",
        "example": "He brought the issue up in the meeting.",

        // 新增的圖片相關欄位
        "exampleImages": [
          {
            "id": "d96d3330-7814-45e1-9ac6-801c8ca32ee7",
            "imageUrl": "https://localhost:5008/images/examples/xxx.png",
            "isPrimary": true,
            "qualityScore": 0.95,
            "fileSize": 190000
          }
        ],
        "hasExampleImage": true,
        "primaryImageUrl": "https://localhost:5008/images/examples/xxx.png"
      }
    ]
  }
}

3.2 前端狀態更新流程

graph TD
    A[API 回應接收] --> B[解構 flashcards 陣列]
    B --> C[更新 React 狀態]
    C --> D[觸發組件重新渲染]
    D --> E[FlashcardItem 組件 map 渲染]
    E --> F[個別詞卡資料傳入]
    F --> G[圖片顯示邏輯判斷]

第4階段UI 渲染與圖片顯示

4.1 詞卡項目渲染

// FlashcardItem 組件
function FlashcardItem({ card, ... }) {
  return (
    <div className="bg-white border rounded-lg">
      {/* 圖片區域 - 響應式設計 */}
      <div className="w-32 h-20 sm:w-40 sm:h-24 md:w-48 md:h-32">
        {hasExampleImage(card) ? (
          // 顯示圖片
          <img
            src={getExampleImage(card)}
            alt={`${card.word} example`}
            className="w-full h-full object-cover"
          />
        ) : (
          // 顯示新增按鈕
          <div onClick={() => onGenerateExampleImage(card)}>
            <span>新增例句圖</span>
          </div>
        )}
      </div>

      {/* 詞卡資訊 */}
      <div className="flex-1">
        <h3>{card.word}</h3>
        <span>{card.translation}</span>
      </div>
    </div>
  )
}

4.2 圖片顯示判斷邏輯

flowchart TD
    A[FlashcardItem 渲染] --> B{檢查 card.hasExampleImage}
    B -->|true| C[取得 card.primaryImageUrl]
    B -->|false| D[顯示新增例句圖按鈕]
    C --> E[設定 img src 屬性]
    E --> F[瀏覽器載入圖片]
    F --> G{圖片載入成功?}
    G -->|成功| H[顯示 512x512 圖片]
    G -->|失敗| I[顯示錯誤提示]
    D --> J[用戶點擊生成按鈕]
    J --> K[觸發 handleGenerateExampleImage]
    H --> L[CSS 響應式縮放顯示]

🔧 技術實現細節

前端服務層

// /frontend/lib/services/flashcards.ts
export const flashcardsService = {
  async getFlashcards(): Promise<FlashcardsResponse> {
    const response = await fetch(`${API_URL}/api/flashcards`, {
      headers: {
        'Authorization': `Bearer ${getToken()}`,
        'Content-Type': 'application/json'
      }
    })

    return response.json()
  }
}

// 回應介面定義
interface Flashcard {
  id: string
  word: string
  translation: string
  example: string

  // 圖片相關欄位
  exampleImages: ExampleImage[]
  hasExampleImage: boolean
  primaryImageUrl?: string
}

interface ExampleImage {
  id: string
  imageUrl: string
  isPrimary: boolean
  qualityScore?: number
  fileSize?: number
}

圖片顯示邏輯

// 當前實現 (將被取代)
const getExampleImage = (card: Flashcard): string | null => {
  // 硬編碼映射 (舊方式)
  const imageMap: {[key: string]: string} = {
    'evidence': '/images/examples/bring_up.png',
  }
  return imageMap[card.word?.toLowerCase()] || null
}

// 新實現 (基於 API 資料)
const getExampleImage = (card: Flashcard): string | null => {
  return card.primaryImageUrl || null
}

const hasExampleImage = (card: Flashcard): boolean => {
  return card.hasExampleImage
}

🖼️ 圖片載入和顯示流程

圖片 URL 生成過程

sequenceDiagram
    participant FE as 前端
    participant BE as 後端 API
    participant DB as 資料庫
    participant FS as 檔案系統

    FE->>BE: GET /api/flashcards
    BE->>DB: 查詢 flashcards + images
    DB-->>BE: 返回關聯資料
    BE->>FS: 檢查圖片檔案存在
    FS-->>BE: 確認檔案路徑
    BE->>BE: 生成完整 HTTP URL
    BE-->>FE: 回應包含 imageUrl
    FE->>FS: 瀏覽器請求圖片
    FS-->>FE: 返回 512x512 PNG 圖片

響應式圖片顯示

/* 圖片容器響應式尺寸 */
.example-image-container {
  /* 手機 */
  width: 128px;  /* w-32 */
  height: 80px;  /* h-20 */
}

@media (min-width: 640px) {
  .example-image-container {
    /* 平板 */
    width: 160px;  /* sm:w-40 */
    height: 96px;  /* sm:h-24 */
  }
}

@media (min-width: 768px) {
  .example-image-container {
    /* 桌面 */
    width: 192px;  /* md:w-48 */
    height: 128px; /* md:h-32 */
  }
}

/* 圖片本身處理 */
.example-image {
  width: 100%;
  height: 100%;
  object-fit: cover;  /* 保持比例,裁切適應容器 */
  border-radius: 8px;
}

效能優化策略

前端優化

// 圖片懶載入
<img
  src={card.primaryImageUrl}
  loading="lazy"  // 瀏覽器原生懶載入
  alt={`${card.word} example`}
/>

// 錯誤處理
<img
  src={card.primaryImageUrl}
  onError={(e) => {
    e.target.style.display = 'none'
    // 顯示備用內容
  }}
/>

後端優化

// 查詢優化
var flashcards = await query
    .AsNoTracking()  // 只讀查詢優化
    .OrderByDescending(f => f.CreatedAt)
    .ToListAsync();

// 圖片 URL 快取 (未來實現)
private readonly IMemoryCache _urlCache;

🎮 用戶互動流程

圖片生成流程

flowchart TD
    A[用戶看到詞卡] --> B{是否有圖片?}
    B -->|有| C[顯示 512x512 圖片]
    B -->|無| D[顯示新增例句圖按鈕]
    D --> E[用戶點擊按鈕]
    E --> F[觸發 handleGenerateExampleImage]
    F --> G[調用圖片生成 API]
    G --> H[顯示生成進度]
    H --> I[等待 2-3 分鐘]
    I --> J[生成完成]
    J --> K[自動刷新詞卡列表]
    K --> L[新圖片顯示在詞卡中]

生成進度顯示

// 生成狀態管理
const [generatingCards, setGeneratingCards] = useState<Set<string>>(new Set())

const handleGenerateExampleImage = async (card: Flashcard) => {
  // 1. 標記為生成中
  setGeneratingCards(prev => new Set([...prev, card.id]))

  try {
    // 2. 調用生成 API
    const result = await imageGenerationService.generateImage(card.id)

    // 3. 輪詢進度
    await imageGenerationService.pollUntilComplete(result.requestId)

    // 4. 刷新資料
    await searchActions.refresh()

    // 5. 顯示成功訊息
    toast.success(`「${card.word}」的例句圖片生成完成!`)
  } catch (error) {
    toast.error(`圖片生成失敗: ${error.message}`)
  } finally {
    // 6. 移除生成中狀態
    setGeneratingCards(prev => {
      const newSet = new Set(prev)
      newSet.delete(card.id)
      return newSet
    })
  }
}

📊 資料流轉換表

階段 資料格式 位置 範例
資料庫 關聯表格 flashcard_example_images {flashcard_id, example_image_id, is_primary}
EF Core 實體物件 Flashcard.FlashcardExampleImages List<FlashcardExampleImage>
後端 API JSON 回應 HTTP Response {hasExampleImage: true, primaryImageUrl: "https://..."}
前端狀態 TypeScript 物件 React State flashcards: Flashcard[]
UI 組件 JSX 元素 React Component <img src={card.primaryImageUrl} />
瀏覽器 實際圖片 DOM 512x512 PNG 圖片顯示

🔍 錯誤處理流程

API 層級錯誤

graph TD
    A[API 調用] --> B{網路狀態}
    B -->|成功| C[解析 JSON]
    B -->|失敗| D[顯示網路錯誤]
    C --> E{success: true?}
    E -->|Yes| F[正常資料流程]
    E -->|No| G[顯示 API 錯誤訊息]
    D --> H[重試機制]
    G --> H
    H --> I[用戶手動重新整理]

圖片載入錯誤

// 圖片載入失敗處理
const handleImageError = (e: React.SyntheticEvent<HTMLImageElement>) => {
  const target = e.target as HTMLImageElement
  target.style.display = 'none'

  // 顯示備用內容
  target.parentElement!.innerHTML = `
    <div class="text-gray-400 text-xs text-center">
      <svg class="w-6 h-6 mx-auto mb-1">
        <!-- 錯誤圖示 -->
      </svg>
      圖片載入失敗
    </div>
  `
}

🎯 實際運作範例

情境1有圖片的詞卡 (deal)

1. 用戶訪問詞卡頁面
2. API 返回: hasExampleImage: true, primaryImageUrl: "https://localhost:5008/..."
3. React 渲染: <img src="https://localhost:5008/..." />
4. 瀏覽器載入: 512x512 PNG 圖片 (約190KB)
5. CSS 處理: 響應式縮放顯示在詞卡中

情境2無圖片的詞卡 (up)

1. 用戶訪問詞卡頁面
2. API 返回: hasExampleImage: false, primaryImageUrl: null
3. React 渲染: 新增例句圖按鈕
4. 用戶點擊: 觸發圖片生成流程
5. 生成完成: 自動刷新並顯示新圖片

🔮 未來擴展規劃

前端增強功能

  • 圖片預覽: 點擊圖片查看大圖
  • 多圖片支援: 輪播顯示多張例句圖
  • 圖片編輯: 刪除、重新生成功能
  • 批量生成: 一次為多個詞卡生成圖片

效能優化

  • 圖片 CDN: 雲端加速分發
  • WebP 格式: 更小的檔案大小
  • 預載入: 預先載入即將顯示的圖片
  • 虛擬化: 大量詞卡的效能優化

📈 監控指標

前端效能

  • 頁面載入時間: 目標 < 2 秒
  • 圖片載入時間: 目標 < 1 秒
  • API 回應時間: 目標 < 500ms

用戶體驗

  • 圖片顯示成功率: 目標 > 95%
  • 生成成功率: 目標 > 90%
  • 用戶滿意度: 目標 > 4.5/5

文檔版本: v1.0 建立日期: 2025-09-24 最後更新: 2025-09-24 相關文檔: 前後端整合計劃