diff --git a/backend/DramaLing.Api/Controllers/CardSetsController.cs b/backend/DramaLing.Api/Controllers/CardSetsController.cs index e648834..5be8260 100644 --- a/backend/DramaLing.Api/Controllers/CardSetsController.cs +++ b/backend/DramaLing.Api/Controllers/CardSetsController.cs @@ -30,6 +30,30 @@ public class CardSetsController : ControllerBase throw new UnauthorizedAccessException("Invalid user ID"); } + private async Task EnsureDefaultCardSetAsync(Guid userId) + { + // 檢查用戶是否已有預設卡組 + var hasDefaultCardSet = await _context.CardSets + .AnyAsync(cs => cs.UserId == userId && cs.IsDefault); + + if (!hasDefaultCardSet) + { + // 創建預設「未分類」卡組 + var defaultCardSet = new CardSet + { + Id = Guid.NewGuid(), + UserId = userId, + Name = "未分類", + Description = "系統預設卡組,用於存放尚未分類的詞卡", + Color = "bg-slate-700", + IsDefault = true + }; + + _context.CardSets.Add(defaultCardSet); + await _context.SaveChangesAsync(); + } + } + [HttpGet] public async Task GetCardSets() { @@ -37,9 +61,13 @@ public class CardSetsController : ControllerBase { var userId = GetUserId(); + // 確保用戶有預設卡組 + await EnsureDefaultCardSetAsync(userId); + var cardSets = await _context.CardSets .Where(cs => cs.UserId == userId) - .OrderByDescending(cs => cs.CreatedAt) + .OrderBy(cs => cs.IsDefault ? 0 : 1) // 預設卡組排在最前面 + .ThenByDescending(cs => cs.CreatedAt) .Select(cs => new { cs.Id, @@ -49,6 +77,7 @@ public class CardSetsController : ControllerBase cs.CardCount, cs.CreatedAt, cs.UpdatedAt, + cs.IsDefault, // 計算進度 (簡化版) Progress = cs.CardCount > 0 ? _context.Flashcards @@ -186,6 +215,10 @@ public class CardSetsController : ControllerBase if (cardSet == null) return NotFound(new { Success = false, Error = "Card set not found" }); + // 防止刪除預設卡組 + if (cardSet.IsDefault) + return BadRequest(new { Success = false, Error = "Cannot delete default card set" }); + _context.CardSets.Remove(cardSet); await _context.SaveChangesAsync(); @@ -209,6 +242,43 @@ public class CardSetsController : ControllerBase }); } } + + [HttpPost("ensure-default")] + public async Task EnsureDefaultCardSet() + { + try + { + var userId = GetUserId(); + await EnsureDefaultCardSetAsync(userId); + + // 返回預設卡組 + var defaultCardSet = await _context.CardSets + .FirstOrDefaultAsync(cs => cs.UserId == userId && cs.IsDefault); + + if (defaultCardSet == null) + return StatusCode(500, new { Success = false, Error = "Failed to create default card set" }); + + return Ok(new + { + Success = true, + Data = defaultCardSet, + Message = "Default card set ensured" + }); + } + catch (UnauthorizedAccessException) + { + return Unauthorized(new { Success = false, Error = "Unauthorized" }); + } + catch (Exception ex) + { + return StatusCode(500, new + { + Success = false, + Error = "Failed to ensure default card set", + Timestamp = DateTime.UtcNow + }); + } + } } // Request DTOs diff --git a/backend/DramaLing.Api/Controllers/FlashcardsController.cs b/backend/DramaLing.Api/Controllers/FlashcardsController.cs index 2ddbd6a..236249f 100644 --- a/backend/DramaLing.Api/Controllers/FlashcardsController.cs +++ b/backend/DramaLing.Api/Controllers/FlashcardsController.cs @@ -30,6 +30,31 @@ public class FlashcardsController : ControllerBase throw new UnauthorizedAccessException("Invalid user ID"); } + private async Task GetOrCreateDefaultCardSetAsync(Guid userId) + { + // 嘗試找到預設卡組 + var defaultCardSet = await _context.CardSets + .FirstOrDefaultAsync(cs => cs.UserId == userId && cs.IsDefault); + + if (defaultCardSet != null) + return defaultCardSet.Id; + + // 如果沒有預設卡組,創建一個 + var newDefaultCardSet = new CardSet + { + Id = Guid.NewGuid(), + UserId = userId, + Name = "未分類", + Description = "系統預設卡組,用於存放尚未分類的詞卡", + Color = "bg-slate-700", + IsDefault = true + }; + + _context.CardSets.Add(newDefaultCardSet); + await _context.SaveChangesAsync(); + return newDefaultCardSet.Id; + } + [HttpGet] public async Task GetFlashcards( [FromQuery] Guid? setId, @@ -116,18 +141,30 @@ public class FlashcardsController : ControllerBase { var userId = GetUserId(); - // 驗證卡組是否屬於用戶 - var cardSet = await _context.CardSets - .FirstOrDefaultAsync(cs => cs.Id == request.CardSetId && cs.UserId == userId); + // 確定要使用的卡組ID + Guid cardSetId; + if (request.CardSetId.HasValue) + { + // 如果指定了卡組,驗證是否屬於用戶 + var cardSet = await _context.CardSets + .FirstOrDefaultAsync(cs => cs.Id == request.CardSetId.Value && cs.UserId == userId); - if (cardSet == null) - return NotFound(new { Success = false, Error = "Card set not found" }); + if (cardSet == null) + return NotFound(new { Success = false, Error = "Card set not found" }); + + cardSetId = request.CardSetId.Value; + } + else + { + // 如果沒有指定卡組,使用或創建預設卡組 + cardSetId = await GetOrCreateDefaultCardSetAsync(userId); + } var flashcard = new Flashcard { Id = Guid.NewGuid(), UserId = userId, - CardSetId = request.CardSetId, + CardSetId = cardSetId, Word = request.Word.Trim(), Translation = request.Translation.Trim(), Definition = request.Definition.Trim(), @@ -290,7 +327,7 @@ public class FlashcardsController : ControllerBase // DTOs public class CreateFlashcardRequest { - public Guid CardSetId { get; set; } + public Guid? CardSetId { get; set; } public string Word { get; set; } = string.Empty; public string Translation { get; set; } = string.Empty; public string Definition { get; set; } = string.Empty; diff --git a/backend/DramaLing.Api/Models/Entities/CardSet.cs b/backend/DramaLing.Api/Models/Entities/CardSet.cs index 127cbd0..56b5c85 100644 --- a/backend/DramaLing.Api/Models/Entities/CardSet.cs +++ b/backend/DramaLing.Api/Models/Entities/CardSet.cs @@ -19,6 +19,8 @@ public class CardSet public int CardCount { get; set; } = 0; + public bool IsDefault { get; set; } = false; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; diff --git a/frontend/app/flashcards/page.tsx b/frontend/app/flashcards/page.tsx index 83ae8e9..7150c52 100644 --- a/frontend/app/flashcards/page.tsx +++ b/frontend/app/flashcards/page.tsx @@ -32,7 +32,23 @@ function FlashcardsContent() { try { const result = await flashcardsService.getCardSets() if (result.success && result.data) { - setCardSets(result.data.sets) + if (result.data.sets.length === 0) { + // 如果沒有卡組,確保創建預設卡組 + const ensureResult = await flashcardsService.ensureDefaultCardSet() + if (ensureResult.success) { + // 重新載入卡組 + const retryResult = await flashcardsService.getCardSets() + if (retryResult.success && retryResult.data) { + setCardSets(retryResult.data.sets) + } else { + setError('Failed to load card sets after creating default') + } + } else { + setError(ensureResult.error || 'Failed to create default card set') + } + } else { + setCardSets(result.data.sets) + } } else { setError(result.error || 'Failed to load card sets') } @@ -175,6 +191,22 @@ function FlashcardsContent() { > 所有詞卡 ({filteredCards.length}) + {/* Search */}
@@ -216,14 +248,22 @@ function FlashcardsContent() { {filteredSets.map(set => (
{ setSelectedSet(set.id) setActiveTab('all-cards') }} > -
-

{set.name}

+
+
+ {set.isDefault && 📂} +

+ {set.name} + {set.isDefault && (預設)} +

+

{set.description}

@@ -254,6 +294,26 @@ function FlashcardsContent() { )}
+ {/* 未分類提醒 */} + {selectedSet && cardSets.find(set => set.id === selectedSet)?.isDefault && filteredCards.length > 15 && ( +
+
+ 💡 +
+

+ 您有 {filteredCards.length} 個未分類詞卡,建議整理到不同主題的卡組中,有助於更好地組織學習內容。 +

+
+ +
+
+ )} + {filteredCards.length === 0 ? (

沒有找到詞卡

diff --git a/frontend/components/FlashcardForm.tsx b/frontend/components/FlashcardForm.tsx index 4cb3495..b35aa1f 100644 --- a/frontend/components/FlashcardForm.tsx +++ b/frontend/components/FlashcardForm.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect } from 'react' +import React, { useState, useEffect } from 'react' import { flashcardsService, type CreateFlashcardRequest, type CardSet } from '@/lib/services/flashcards' interface FlashcardFormProps { @@ -12,8 +12,23 @@ interface FlashcardFormProps { } export function FlashcardForm({ cardSets, initialData, isEdit = false, onSuccess, onCancel }: FlashcardFormProps) { + // 找到預設卡組或第一個卡組 + const getDefaultCardSetId = () => { + if (initialData?.cardSetId) return initialData.cardSetId + + // 優先選擇預設卡組 + const defaultCardSet = cardSets.find(set => set.isDefault) + if (defaultCardSet) return defaultCardSet.id + + // 如果沒有預設卡組,選擇第一個卡組 + if (cardSets.length > 0) return cardSets[0].id + + // 如果沒有任何卡組,返回空字串 + return '' + } + const [formData, setFormData] = useState({ - cardSetId: initialData?.cardSetId || (cardSets[0]?.id || ''), + cardSetId: getDefaultCardSetId(), english: initialData?.english || '', chinese: initialData?.chinese || '', pronunciation: initialData?.pronunciation || '', @@ -21,6 +36,16 @@ export function FlashcardForm({ cardSets, initialData, isEdit = false, onSuccess example: initialData?.example || '', }) + // 當 cardSets 改變時,重新設定 cardSetId(處理初始載入的情況) + React.useEffect(() => { + if (!formData.cardSetId && cardSets.length > 0) { + const defaultId = getDefaultCardSetId() + if (defaultId) { + setFormData(prev => ({ ...prev, cardSetId: defaultId })) + } + } + }, [cardSets]) + const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -87,18 +112,41 @@ export function FlashcardForm({ cardSets, initialData, isEdit = false, onSuccess - + {cardSets.length === 0 ? ( +
+ 載入卡組中... +
+ ) : ( + + )}
{/* 英文單字 */} @@ -190,10 +238,12 @@ export function FlashcardForm({ cardSets, initialData, isEdit = false, onSuccess
diff --git a/frontend/lib/services/flashcards.ts b/frontend/lib/services/flashcards.ts index 3f87f26..63a057c 100644 --- a/frontend/lib/services/flashcards.ts +++ b/frontend/lib/services/flashcards.ts @@ -8,6 +8,7 @@ export interface CardSet { cardCount: number; createdAt: string; updatedAt: string; + isDefault: boolean; progress: number; lastStudied: string; tags: string[]; @@ -40,7 +41,7 @@ export interface CreateCardSetRequest { } export interface CreateFlashcardRequest { - cardSetId: string; + cardSetId?: string; english: string; chinese: string; pronunciation: string; @@ -192,6 +193,19 @@ class FlashcardsService { }; } } + + async ensureDefaultCardSet(): Promise> { + try { + return await this.makeRequest>('/cardsets/ensure-default', { + method: 'POST', + }); + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to ensure default card set', + }; + } + } } export const flashcardsService = new FlashcardsService(); \ No newline at end of file