dramaling-vocab-learning/FRONTEND_FLASHCARD_DATA_FLO...

533 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 前端詞卡管理資料流程圖
## 📋 **文檔概覽**
本文檔詳細說明前端詞卡管理功能如何取得詞卡及其例句圖片,並顯示在用戶介面上的完整資料流程。
---
## 🏗️ **整體架構圖**
```mermaid
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 組件載入**
```typescript
// /frontend/app/flashcards/page.tsx
export default function FlashcardsPage() {
return (
<ProtectedRoute>
<FlashcardsContent />
</ProtectedRoute>
)
}
function FlashcardsContent() {
const [searchState, searchActions] = useFlashcardSearch(activeTab)
useEffect(() => {
loadTotalCounts() // 初始化資料載入
}, [])
}
```
#### **1.2 資料載入觸發**
```mermaid
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 端點處理**
```csharp
// FlashcardsController.GetFlashcards()
var query = _context.Flashcards
.Include(f => f.FlashcardExampleImages) // 載入圖片關聯
.ThenInclude(fei => fei.ExampleImage) // 載入圖片詳情
.Where(f => f.UserId == userId && !f.IsArchived)
.AsQueryable();
```
#### **2.2 資料庫查詢流程**
```mermaid
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 圖片資料組裝**
```csharp
// 每個 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 回應結構**
```json
{
"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 前端狀態更新流程**
```mermaid
graph TD
A[API 回應接收] --> B[解構 flashcards 陣列]
B --> C[更新 React 狀態]
C --> D[觸發組件重新渲染]
D --> E[FlashcardItem 組件 map 渲染]
E --> F[個別詞卡資料傳入]
F --> G[圖片顯示邏輯判斷]
```
---
### **第4階段UI 渲染與圖片顯示**
#### **4.1 詞卡項目渲染**
```typescript
// 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 圖片顯示判斷邏輯**
```mermaid
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 響應式縮放顯示]
```
---
## 🔧 **技術實現細節**
### **前端服務層**
```typescript
// /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
}
```
### **圖片顯示邏輯**
```typescript
// 當前實現 (將被取代)
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 生成過程**
```mermaid
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 圖片
```
### **響應式圖片顯示**
```css
/* 圖片容器響應式尺寸 */
.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;
}
```
---
## ⚡ **效能優化策略**
### **前端優化**
```typescript
// 圖片懶載入
<img
src={card.primaryImageUrl}
loading="lazy" // 瀏覽器原生懶載入
alt={`${card.word} example`}
/>
// 錯誤處理
<img
src={card.primaryImageUrl}
onError={(e) => {
e.target.style.display = 'none'
// 顯示備用內容
}}
/>
```
### **後端優化**
```csharp
// 查詢優化
var flashcards = await query
.AsNoTracking() // 只讀查詢優化
.OrderByDescending(f => f.CreatedAt)
.ToListAsync();
// 圖片 URL 快取 (未來實現)
private readonly IMemoryCache _urlCache;
```
---
## 🎮 **用戶互動流程**
### **圖片生成流程**
```mermaid
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[新圖片顯示在詞卡中]
```
### **生成進度顯示**
```typescript
// 生成狀態管理
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 層級錯誤**
```mermaid
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[用戶手動重新整理]
```
### **圖片載入錯誤**
```typescript
// 圖片載入失敗處理
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
**相關文檔**: [前後端整合計劃](./EXAMPLE_IMAGE_FRONTEND_BACKEND_INTEGRATION_PLAN.md)