feat: 完成CardSet功能清理和測試資料優化
## 主要改動 ### 後端 CardSet 功能完全移除 - 刪除 CardSet.cs 實體模型 - 移除 Flashcard 中的 CardSetId 欄位和導航屬性 - 清理 User 實體中的 CardSets 導航屬性 - 更新 DbContext 移除 CardSet 相關配置 - 修復 FlashcardsController、StatsController、StudyController 中的 CardSet 引用 - 創建和執行資料庫 migration 移除 CardSet 表和相關約束 ### API 功能修復和優化 - 修復 FlashcardsController GetFlashcards 方法的 500 錯誤 - 恢復例句圖片處理功能 (FlashcardExampleImages) - 增強錯誤日誌和調試資訊 - 簡化後重新添加完整圖片處理邏輯 ### 前端測試資料完善 - 轉換 CSV 為完整的 API 響應格式 - 為所有詞彙添加圖片資料結構和URL - 修正 exampleTranslation 為 example 的正確中文翻譯 - 更新 review-design 頁面支援動態卡片切換 - 移除 cardSetId 相關欄位 ### 系統架構簡化 - 移除不使用的 CardSet 功能,專注核心 Flashcard 學習 - 統一資料格式,提升前後端一致性 - 完善測試環境的假資料支援 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4ec3fd1bc9
commit
589a22b89d
|
|
@ -19,6 +19,7 @@ public class FlashcardsController : ControllerBase
|
|||
private readonly DramaLingDbContext _context;
|
||||
private readonly ILogger<FlashcardsController> _logger;
|
||||
private readonly IImageStorageService _imageStorageService;
|
||||
private readonly IAuthService _authService;
|
||||
// 🆕 智能複習服務依賴
|
||||
private readonly ISpacedRepetitionService _spacedRepetitionService;
|
||||
private readonly IReviewTypeSelectorService _reviewTypeSelectorService;
|
||||
|
|
@ -28,6 +29,7 @@ public class FlashcardsController : ControllerBase
|
|||
DramaLingDbContext context,
|
||||
ILogger<FlashcardsController> logger,
|
||||
IImageStorageService imageStorageService,
|
||||
IAuthService authService,
|
||||
ISpacedRepetitionService spacedRepetitionService,
|
||||
IReviewTypeSelectorService reviewTypeSelectorService,
|
||||
IQuestionGeneratorService questionGeneratorService)
|
||||
|
|
@ -35,6 +37,7 @@ public class FlashcardsController : ControllerBase
|
|||
_context = context;
|
||||
_logger = logger;
|
||||
_imageStorageService = imageStorageService;
|
||||
_authService = authService;
|
||||
_spacedRepetitionService = spacedRepetitionService;
|
||||
_reviewTypeSelectorService = reviewTypeSelectorService;
|
||||
_questionGeneratorService = questionGeneratorService;
|
||||
|
|
@ -66,6 +69,7 @@ public class FlashcardsController : ControllerBase
|
|||
try
|
||||
{
|
||||
var userId = GetUserId();
|
||||
_logger.LogInformation("GetFlashcards called for user: {UserId}", userId);
|
||||
|
||||
var query = _context.Flashcards
|
||||
.Include(f => f.FlashcardExampleImages)
|
||||
|
|
@ -73,6 +77,8 @@ public class FlashcardsController : ControllerBase
|
|||
.Where(f => f.UserId == userId && !f.IsArchived)
|
||||
.AsQueryable();
|
||||
|
||||
_logger.LogInformation("Base query created successfully");
|
||||
|
||||
// 搜尋篩選 (擴展支援例句內容)
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
{
|
||||
|
|
@ -119,11 +125,14 @@ public class FlashcardsController : ControllerBase
|
|||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Executing database query...");
|
||||
var flashcards = await query
|
||||
.AsNoTracking() // 效能優化:只讀查詢
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
_logger.LogInformation("Found {Count} flashcards", flashcards.Count);
|
||||
|
||||
// 生成圖片資訊
|
||||
var flashcardDtos = new List<object>();
|
||||
foreach (var flashcard in flashcards)
|
||||
|
|
@ -177,8 +186,9 @@ public class FlashcardsController : ControllerBase
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting flashcards for user");
|
||||
return StatusCode(500, new { Success = false, Error = "Failed to load flashcards" });
|
||||
_logger.LogError(ex, "Error getting flashcards for user: {Message}", ex.Message);
|
||||
_logger.LogError("Stack trace: {StackTrace}", ex.StackTrace);
|
||||
return StatusCode(500, new { Success = false, Error = "Failed to load flashcards", Details = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,7 +249,6 @@ public class FlashcardsController : ControllerBase
|
|||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
CardSetId = null, // 暫時不使用 CardSet
|
||||
Word = request.Word,
|
||||
Translation = request.Translation,
|
||||
Definition = request.Definition ?? "",
|
||||
|
|
|
|||
|
|
@ -39,22 +39,6 @@ public class StatsController : ControllerBase
|
|||
|
||||
// 並行獲取統計數據
|
||||
var totalWordsTask = _context.Flashcards.CountAsync(f => f.UserId == userId);
|
||||
var cardSetsTask = _context.CardSets
|
||||
.Where(cs => cs.UserId == userId)
|
||||
.OrderByDescending(cs => cs.CreatedAt)
|
||||
.Take(5)
|
||||
.Select(cs => new
|
||||
{
|
||||
cs.Id,
|
||||
cs.Name,
|
||||
Count = cs.CardCount,
|
||||
Progress = cs.CardCount > 0 ?
|
||||
_context.Flashcards
|
||||
.Where(f => f.CardSetId == cs.Id)
|
||||
.Average(f => (double?)f.MasteryLevel) ?? 0 : 0,
|
||||
LastStudied = cs.CreatedAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var recentCardsTask = _context.Flashcards
|
||||
.Where(f => f.UserId == userId)
|
||||
|
|
@ -73,10 +57,9 @@ public class StatsController : ControllerBase
|
|||
.FirstOrDefaultAsync(ds => ds.UserId == userId && ds.Date == today);
|
||||
|
||||
// 等待所有查詢完成
|
||||
await Task.WhenAll(totalWordsTask, cardSetsTask, recentCardsTask, todayStatsTask);
|
||||
await Task.WhenAll(totalWordsTask, recentCardsTask, todayStatsTask);
|
||||
|
||||
var totalWords = await totalWordsTask;
|
||||
var cardSets = await cardSetsTask;
|
||||
var recentCards = await recentCardsTask;
|
||||
var todayStats = await todayStatsTask;
|
||||
|
||||
|
|
@ -107,7 +90,7 @@ public class StatsController : ControllerBase
|
|||
new { Word = "perspective", Translation = "觀點", Status = "new" },
|
||||
new { Word = "substantial", Translation = "大量的", Status = "learned" }
|
||||
},
|
||||
CardSets = cardSets
|
||||
CardSets = new object[0] // 移除 CardSet 功能
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ public class StudyController : ControllerBase
|
|||
|
||||
var today = DateTime.Today;
|
||||
var query = _context.Flashcards
|
||||
.Include(f => f.CardSet)
|
||||
.Where(f => f.UserId == userId);
|
||||
|
||||
// 篩選到期和新詞卡
|
||||
|
|
@ -88,8 +87,8 @@ public class StudyController : ControllerBase
|
|||
x.Card.DifficultyLevel,
|
||||
CardSet = new
|
||||
{
|
||||
x.Card.CardSet.Name,
|
||||
x.Card.CardSet.Color
|
||||
Name = "Default",
|
||||
Color = "bg-blue-500"
|
||||
},
|
||||
x.Priority,
|
||||
x.IsDue,
|
||||
|
|
@ -187,7 +186,6 @@ public class StudyController : ControllerBase
|
|||
|
||||
// 獲取詞卡詳細資訊
|
||||
var cards = await _context.Flashcards
|
||||
.Include(f => f.CardSet)
|
||||
.Where(f => f.UserId == userId && request.CardIds.Contains(f.Id))
|
||||
.ToListAsync();
|
||||
|
||||
|
|
@ -217,7 +215,7 @@ public class StudyController : ControllerBase
|
|||
c.MasteryLevel,
|
||||
c.EasinessFactor,
|
||||
c.Repetitions,
|
||||
CardSet = new { c.CardSet.Name, c.CardSet.Color }
|
||||
CardSet = new { Name = "Default", Color = "bg-blue-500" }
|
||||
}),
|
||||
TotalCards = orderedCards.Count,
|
||||
StartedAt = session.StartedAt
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ public class DramaLingDbContext : DbContext
|
|||
// DbSets
|
||||
public DbSet<User> Users { get; set; }
|
||||
public DbSet<UserSettings> UserSettings { get; set; }
|
||||
public DbSet<CardSet> CardSets { get; set; }
|
||||
public DbSet<Flashcard> Flashcards { get; set; }
|
||||
public DbSet<Tag> Tags { get; set; }
|
||||
public DbSet<FlashcardTag> FlashcardTags { get; set; }
|
||||
|
|
@ -39,7 +38,6 @@ public class DramaLingDbContext : DbContext
|
|||
// 設定表名稱 (與 Supabase 一致)
|
||||
modelBuilder.Entity<User>().ToTable("user_profiles");
|
||||
modelBuilder.Entity<UserSettings>().ToTable("user_settings");
|
||||
modelBuilder.Entity<CardSet>().ToTable("card_sets");
|
||||
modelBuilder.Entity<Flashcard>().ToTable("flashcards");
|
||||
modelBuilder.Entity<Tag>().ToTable("tags");
|
||||
modelBuilder.Entity<FlashcardTag>().ToTable("flashcard_tags");
|
||||
|
|
@ -114,7 +112,6 @@ public class DramaLingDbContext : DbContext
|
|||
{
|
||||
var flashcardEntity = modelBuilder.Entity<Flashcard>();
|
||||
flashcardEntity.Property(f => f.UserId).HasColumnName("user_id");
|
||||
flashcardEntity.Property(f => f.CardSetId).HasColumnName("card_set_id");
|
||||
flashcardEntity.Property(f => f.PartOfSpeech).HasColumnName("part_of_speech");
|
||||
flashcardEntity.Property(f => f.ExampleTranslation).HasColumnName("example_translation");
|
||||
flashcardEntity.Property(f => f.EasinessFactor).HasColumnName("easiness_factor");
|
||||
|
|
@ -200,31 +197,13 @@ public class DramaLingDbContext : DbContext
|
|||
|
||||
private void ConfigureRelationships(ModelBuilder modelBuilder)
|
||||
{
|
||||
// CardSet 配置 - 手動 GUID 生成
|
||||
modelBuilder.Entity<CardSet>()
|
||||
.Property(cs => cs.Id)
|
||||
.ValueGeneratedNever(); // 關閉自動生成,允許手動設定 GUID
|
||||
|
||||
// User relationships
|
||||
modelBuilder.Entity<CardSet>()
|
||||
.HasOne(cs => cs.User)
|
||||
.WithMany(u => u.CardSets)
|
||||
.HasForeignKey(cs => cs.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Flashcard>()
|
||||
.HasOne(f => f.User)
|
||||
.WithMany(u => u.Flashcards)
|
||||
.HasForeignKey(f => f.UserId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<Flashcard>()
|
||||
.HasOne(f => f.CardSet)
|
||||
.WithMany(cs => cs.Flashcards)
|
||||
.HasForeignKey(f => f.CardSetId)
|
||||
.IsRequired(false) // 允許 CardSetId 為 null
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// Study relationships
|
||||
modelBuilder.Entity<StudySession>()
|
||||
.HasOne(ss => ss.User)
|
||||
|
|
|
|||
1496
backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.Designer.cs
generated
Normal file
1496
backend/DramaLing.Api/Migrations/20250927144350_RemoveCardSetFeature.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DramaLing.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveCardSetFeature : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_flashcards_card_sets_card_set_id",
|
||||
table: "flashcards");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "card_sets");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_flashcards_card_set_id",
|
||||
table: "flashcards");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "card_set_id",
|
||||
table: "flashcards");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "card_set_id",
|
||||
table: "flashcards",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "card_sets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
CardCount = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
Color = table.Column<string>(type: "TEXT", maxLength: 50, nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
Description = table.Column<string>(type: "TEXT", nullable: true),
|
||||
IsDefault = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_card_sets", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_card_sets_user_profiles_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "user_profiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_flashcards_card_set_id",
|
||||
table: "flashcards",
|
||||
column: "card_set_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_card_sets_UserId",
|
||||
table: "card_sets",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_flashcards_card_sets_card_set_id",
|
||||
table: "flashcards",
|
||||
column: "card_set_id",
|
||||
principalTable: "card_sets",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,46 +82,6 @@ namespace DramaLing.Api.Migrations
|
|||
b.ToTable("audio_cache", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.CardSet", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CardCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Color")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("card_sets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
|
@ -341,10 +301,6 @@ namespace DramaLing.Api.Migrations
|
|||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("CardSetId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("card_set_id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
|
@ -439,8 +395,6 @@ namespace DramaLing.Api.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CardSetId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("flashcards", (string)null);
|
||||
|
|
@ -1239,17 +1193,6 @@ namespace DramaLing.Api.Migrations
|
|||
b.ToTable("WordQueryUsageStats");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.CardSet", b =>
|
||||
{
|
||||
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
|
||||
.WithMany("CardSets")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.DailyStats", b =>
|
||||
{
|
||||
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
|
||||
|
|
@ -1289,19 +1232,12 @@ namespace DramaLing.Api.Migrations
|
|||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.Flashcard", b =>
|
||||
{
|
||||
b.HasOne("DramaLing.Api.Models.Entities.CardSet", "CardSet")
|
||||
.WithMany("Flashcards")
|
||||
.HasForeignKey("CardSetId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("DramaLing.Api.Models.Entities.User", "User")
|
||||
.WithMany("Flashcards")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("CardSet");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
|
|
@ -1506,11 +1442,6 @@ namespace DramaLing.Api.Migrations
|
|||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.CardSet", b =>
|
||||
{
|
||||
b.Navigation("Flashcards");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.ExampleImage", b =>
|
||||
{
|
||||
b.Navigation("FlashcardExampleImages");
|
||||
|
|
@ -1546,8 +1477,6 @@ namespace DramaLing.Api.Migrations
|
|||
|
||||
modelBuilder.Entity("DramaLing.Api.Models.Entities.User", b =>
|
||||
{
|
||||
b.Navigation("CardSets");
|
||||
|
||||
b.Navigation("DailyStats");
|
||||
|
||||
b.Navigation("ErrorReports");
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ public class Flashcard
|
|||
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
public Guid? CardSetId { get; set; }
|
||||
|
||||
// 詞卡內容
|
||||
[Required]
|
||||
[MaxLength(255)]
|
||||
|
|
@ -71,7 +69,6 @@ public class Flashcard
|
|||
|
||||
// Navigation Properties
|
||||
public virtual User User { get; set; } = null!;
|
||||
public virtual CardSet? CardSet { get; set; }
|
||||
public virtual ICollection<StudyRecord> StudyRecords { get; set; } = new List<StudyRecord>();
|
||||
public virtual ICollection<FlashcardTag> FlashcardTags { get; set; } = new List<FlashcardTag>();
|
||||
public virtual ICollection<ErrorReport> ErrorReports { get; set; } = new List<ErrorReport>();
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ public class User
|
|||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
// Navigation Properties
|
||||
public virtual ICollection<CardSet> CardSets { get; set; } = new List<CardSet>();
|
||||
public virtual ICollection<Flashcard> Flashcards { get; set; } = new List<Flashcard>();
|
||||
public virtual UserSettings? Settings { get; set; }
|
||||
public virtual ICollection<StudySession> StudySessions { get; set; } = new List<StudySession>();
|
||||
|
|
|
|||
|
|
@ -127,7 +127,6 @@ public class StudySessionService : IStudySessionService
|
|||
}
|
||||
|
||||
var flashcard = await _context.Flashcards
|
||||
.Include(f => f.CardSet)
|
||||
.FirstOrDefaultAsync(f => f.Id == currentCard.FlashcardId);
|
||||
|
||||
return new CurrentTestDto
|
||||
|
|
|
|||
|
|
@ -1,112 +1,314 @@
|
|||
[
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "1",
|
||||
"id": "580f7a9c-b6cd-4b08-a554-d5f96c2087f9",
|
||||
"userId": "00000000-0000-0000-0000-000000000001",
|
||||
"word": "warrants",
|
||||
"translation": "逮捕令,許可證",
|
||||
"definition": "official papers that allow the police to do something, like search a house or arrest someone.",
|
||||
"partOfSpeech": "noun",
|
||||
"pronunciation": "/ˈwɒrənts/",
|
||||
"example": "The police obtained warrants to search the building for evidence.",
|
||||
"exampleTranslation": "we're gonna be signing our own death warrants.",
|
||||
"exampleTranslation": "警方取得了搜查大樓尋找證據的許可證。",
|
||||
"easinessFactor": 2.5,
|
||||
"repetitions": 0,
|
||||
"intervalDays": 1,
|
||||
"nextReviewDate": "2025-09-27T00:00:00",
|
||||
"masteryLevel": 0,
|
||||
"timesReviewed": 0,
|
||||
"timesCorrect": 0,
|
||||
"lastReviewedAt": null,
|
||||
"isFavorite": false,
|
||||
"isArchived": false,
|
||||
"difficultyLevel": "B2",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=1TFuEhblYQ8CxXkKQRvrwOmsJuQKLcHtD",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["papers", "documents", "permits", "orders"]
|
||||
},
|
||||
"reviewHistory": null,
|
||||
"lastQuestionType": null,
|
||||
"createdAt": "2025-09-27T13:02:32.13951",
|
||||
"updatedAt": "2025-09-27T13:02:32.139524",
|
||||
"user": null,
|
||||
"studyRecords": [],
|
||||
"flashcardTags": [],
|
||||
"errorReports": [],
|
||||
"flashcardExampleImages": [
|
||||
{
|
||||
"id": "2",
|
||||
"word": "brought this thing up",
|
||||
"definition": "To mention or introduce a topic.",
|
||||
"example": "He brought this thing up during our meeting, and now we have to deal with it.",
|
||||
"exampleTranslation": "you're the one that brought this thing up.",
|
||||
"difficultyLevel": "B2",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=1zWPKI1XWfNbz_n2OvBEp_a2ppQzNz5fT",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["talked about it", "said it", "mentioned it"]
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"word": "instincts",
|
||||
"definition": "Instincts are natural tendencies or feelings that guide behavior without conscious thought.",
|
||||
"example": "Animals use their instincts to find food and stay safe.",
|
||||
"exampleTranslation": "now you're telling me not to trust my instincts?",
|
||||
"difficultyLevel": "B2",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=1vRoTYS--kuXER1AzXKaJddGT4nL8C2v4",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["feelings", "sense", "gut feeling"]
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"word": "cut some slack",
|
||||
"definition": "To not be too critical of someone or to allow them some leeway, especially because they are having difficulties.",
|
||||
"example": "Since he's new to the job, we should cut him some slack.",
|
||||
"exampleTranslation": "You're the one who said we should cut him as much slack as he needs.",
|
||||
"difficultyLevel": "B2",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=1xLOJbkEa5HcYLLKZLJnjmDQ3NZHleeT5",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["be nice", "be kind", "forgive", "understand"]
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"word": "ashamed",
|
||||
"definition": "Feeling sorry and embarrassed because you did something wrong.",
|
||||
"example": "She felt ashamed of her mistake and apologized.",
|
||||
"exampleTranslation": "He gave me that look because he's ashamed.",
|
||||
"difficultyLevel": "B1",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=1_c4HhwYHGAlZyXbdXrwlYm7jOjyquECe",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["sorry", "bad", "embarrassed", "guilty"]
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"word": "tragedy",
|
||||
"definition": "A very sad event or situation that causes great suffering.",
|
||||
"example": "The earthquake was a great tragedy for the small town.",
|
||||
"exampleTranslation": "The real tragedy is I have to get drinks with Louis.",
|
||||
"difficultyLevel": "B2",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=19AfxmUfTuuI2DHURBaXJFFCTPb2wvmRd",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["disaster", "sad event", "bad thing", "accident"]
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"word": "criticize",
|
||||
"definition": "To say what you think is bad about someone or something.",
|
||||
"example": "It's not helpful to criticize someone without offering constructive advice.",
|
||||
"exampleTranslation": "JUST TO CRITICIZE WHERE I LIVE, OR... THAT'S A SIDE BENEFIT.",
|
||||
"difficultyLevel": "B2",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=1xE9i6bc_qtbeb-xowqjwlLTjct1UQxx2",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["blame", "say bad things", "complain", "judge"]
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"word": "condemned",
|
||||
"definition": "To say strongly that you do not approve of something or someone.",
|
||||
"example": "The building was condemned after the earthquake due to structural damage.",
|
||||
"exampleTranslation": "HOW LONG AGO WAS IT CONDEMNED?",
|
||||
"difficultyLevel": "B2",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=1jIBsvySXFTVN73oxDFRBjKlw0KMBWcaW",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["said no", "disagreed", "blamed", "refused"]
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"word": "blackmail",
|
||||
"definition": "To get money from someone by saying you will tell a secret about them.",
|
||||
"example": "The corrupt official tried to blackmail the businessman into paying him money.",
|
||||
"exampleTranslation": "WHEN YOU CAME FIVE YEARS AGO TO BLACKMAIL ME, I WAS FURIOUS.",
|
||||
"difficultyLevel": "B2",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=16x7gyLMYGI4WJAoxPNm2EYG8QkCeSXyb",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["threaten", "force", "make someone do"]
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"word": "furious",
|
||||
"definition": "Feeling or showing extreme anger.",
|
||||
"example": "She was furious when she found out her flight was delayed.",
|
||||
"exampleTranslation": "WHEN YOU CAME FIVE YEARS AGO TO BLACKMAIL ME, I WAS FURIOUS.",
|
||||
"difficultyLevel": "B2",
|
||||
"imageUrl": "https://drive.google.com/uc?export=view&id=1kUn3vDm_YKruuWrh3nq7g3WQ8E7yIkSY",
|
||||
"pronunciation": "",
|
||||
"synonyms": ["angry", "mad", "very angry", "upset"]
|
||||
"flashcardId": "",
|
||||
"exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"isPrimary": true,
|
||||
"exampleImage": {
|
||||
"id": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png",
|
||||
"qualityScore": 95,
|
||||
"fileSize": 2048576,
|
||||
"createdAt": "2025-09-27T13:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "c7d8e9f0-a1b2-3456-7890-abcdef123456",
|
||||
"userId": "00000000-0000-0000-0000-000000000001",
|
||||
"word": "ashamed",
|
||||
"translation": "羞恥的,慚愧的",
|
||||
"definition": "Feeling sorry and embarrassed because you did something wrong.",
|
||||
"partOfSpeech": "adjective",
|
||||
"pronunciation": "/əˈʃeɪmd/",
|
||||
"example": "She felt ashamed of her mistake and apologized.",
|
||||
"exampleTranslation": "她為自己的錯誤感到羞愧並道歉。",
|
||||
"easinessFactor": 2.5,
|
||||
"repetitions": 0,
|
||||
"intervalDays": 1,
|
||||
"nextReviewDate": "2025-09-27T00:00:00",
|
||||
"masteryLevel": 0,
|
||||
"timesReviewed": 0,
|
||||
"timesCorrect": 0,
|
||||
"lastReviewedAt": null,
|
||||
"isFavorite": false,
|
||||
"isArchived": false,
|
||||
"difficultyLevel": "B1",
|
||||
"reviewHistory": null,
|
||||
"lastQuestionType": null,
|
||||
"createdAt": "2025-09-27T13:06:39.29807",
|
||||
"updatedAt": "2025-09-27T13:06:39.29807",
|
||||
"user": null,
|
||||
"studyRecords": [],
|
||||
"flashcardTags": [],
|
||||
"errorReports": [],
|
||||
"flashcardExampleImages": [
|
||||
{
|
||||
"flashcardId": "",
|
||||
"exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"isPrimary": true,
|
||||
"exampleImage": {
|
||||
"id": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png",
|
||||
"qualityScore": 95,
|
||||
"fileSize": 2048576,
|
||||
"createdAt": "2025-09-27T13:00:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d9e0f1a2-b3c4-5678-9012-cdef12345678",
|
||||
"userId": "00000000-0000-0000-0000-000000000001",
|
||||
"word": "tragedy",
|
||||
"translation": "悲劇,慘事",
|
||||
"definition": "A very sad event or situation that causes great suffering.",
|
||||
"partOfSpeech": "noun",
|
||||
"pronunciation": "/ˈtrædʒədi/",
|
||||
"example": "The earthquake was a great tragedy for the small town.",
|
||||
"exampleTranslation": "地震對這個小鎮來說是一場巨大的悲劇。",
|
||||
"easinessFactor": 2.5,
|
||||
"repetitions": 0,
|
||||
"intervalDays": 1,
|
||||
"nextReviewDate": "2025-09-27T00:00:00",
|
||||
"masteryLevel": 0,
|
||||
"timesReviewed": 0,
|
||||
"timesCorrect": 0,
|
||||
"lastReviewedAt": null,
|
||||
"isFavorite": false,
|
||||
"isArchived": false,
|
||||
"difficultyLevel": "B2",
|
||||
"reviewHistory": null,
|
||||
"lastQuestionType": null,
|
||||
"createdAt": "2025-09-27T13:07:39.29807",
|
||||
"updatedAt": "2025-09-27T13:07:39.29807",
|
||||
"user": null,
|
||||
"studyRecords": [],
|
||||
"flashcardTags": [],
|
||||
"errorReports": [],
|
||||
"flashcardExampleImages": [
|
||||
{
|
||||
"flashcardId": "",
|
||||
"exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"isPrimary": true,
|
||||
"exampleImage": {
|
||||
"id": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png",
|
||||
"qualityScore": 95,
|
||||
"fileSize": 2048576,
|
||||
"createdAt": "2025-09-27T13:00:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e1f2a3b4-c5d6-7890-1234-def123456789",
|
||||
"userId": "00000000-0000-0000-0000-000000000001",
|
||||
"word": "criticize",
|
||||
"translation": "批評,指責",
|
||||
"definition": "To say what you think is bad about someone or something.",
|
||||
"partOfSpeech": "verb",
|
||||
"pronunciation": "/ˈkrɪtɪsaɪz/",
|
||||
"example": "It's not helpful to criticize someone without offering constructive advice.",
|
||||
"exampleTranslation": "批評別人而不提供建設性建議是沒有幫助的。",
|
||||
"easinessFactor": 2.5,
|
||||
"repetitions": 0,
|
||||
"intervalDays": 1,
|
||||
"nextReviewDate": "2025-09-27T00:00:00",
|
||||
"masteryLevel": 0,
|
||||
"timesReviewed": 0,
|
||||
"timesCorrect": 0,
|
||||
"lastReviewedAt": null,
|
||||
"isFavorite": false,
|
||||
"isArchived": false,
|
||||
"difficultyLevel": "B2",
|
||||
"reviewHistory": null,
|
||||
"lastQuestionType": null,
|
||||
"createdAt": "2025-09-27T13:08:39.29807",
|
||||
"updatedAt": "2025-09-27T13:08:39.29807",
|
||||
"user": null,
|
||||
"studyRecords": [],
|
||||
"flashcardTags": [],
|
||||
"errorReports": [],
|
||||
"flashcardExampleImages": [
|
||||
{
|
||||
"flashcardId": "",
|
||||
"exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"isPrimary": true,
|
||||
"exampleImage": {
|
||||
"id": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png",
|
||||
"qualityScore": 95,
|
||||
"fileSize": 2048576,
|
||||
"createdAt": "2025-09-27T13:00:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f3a4b5c6-d7e8-9012-3456-f123456789ab",
|
||||
"userId": "00000000-0000-0000-0000-000000000001",
|
||||
"word": "condemned",
|
||||
"translation": "譴責,定罪",
|
||||
"definition": "To say strongly that you do not approve of something or someone.",
|
||||
"partOfSpeech": "verb",
|
||||
"pronunciation": "/kənˈdemd/",
|
||||
"example": "The building was condemned after the earthquake due to structural damage.",
|
||||
"exampleTranslation": "地震後由於結構損壞,這棟建築被宣告不適用。",
|
||||
"easinessFactor": 2.5,
|
||||
"repetitions": 0,
|
||||
"intervalDays": 1,
|
||||
"nextReviewDate": "2025-09-27T00:00:00",
|
||||
"masteryLevel": 0,
|
||||
"timesReviewed": 0,
|
||||
"timesCorrect": 0,
|
||||
"lastReviewedAt": null,
|
||||
"isFavorite": false,
|
||||
"isArchived": false,
|
||||
"difficultyLevel": "B2",
|
||||
"reviewHistory": null,
|
||||
"lastQuestionType": null,
|
||||
"createdAt": "2025-09-27T13:09:39.29807",
|
||||
"updatedAt": "2025-09-27T13:09:39.29807",
|
||||
"user": null,
|
||||
"studyRecords": [],
|
||||
"flashcardTags": [],
|
||||
"errorReports": [],
|
||||
"flashcardExampleImages": [
|
||||
{
|
||||
"flashcardId": "",
|
||||
"exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"isPrimary": true,
|
||||
"exampleImage": {
|
||||
"id": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png",
|
||||
"qualityScore": 95,
|
||||
"fileSize": 2048576,
|
||||
"createdAt": "2025-09-27T13:00:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "a5b6c7d8-e9f0-1234-5678-123456789abc",
|
||||
"userId": "00000000-0000-0000-0000-000000000001",
|
||||
"word": "blackmail",
|
||||
"translation": "勒索,要脅",
|
||||
"definition": "To get money from someone by saying you will tell a secret about them.",
|
||||
"partOfSpeech": "verb",
|
||||
"pronunciation": "/ˈblækmeɪl/",
|
||||
"example": "The corrupt official tried to blackmail the businessman into paying him money.",
|
||||
"exampleTranslation": "腐敗的官員試圖勒索商人要他付錢。",
|
||||
"easinessFactor": 2.5,
|
||||
"repetitions": 0,
|
||||
"intervalDays": 1,
|
||||
"nextReviewDate": "2025-09-27T00:00:00",
|
||||
"masteryLevel": 0,
|
||||
"timesReviewed": 0,
|
||||
"timesCorrect": 0,
|
||||
"lastReviewedAt": null,
|
||||
"isFavorite": false,
|
||||
"isArchived": false,
|
||||
"difficultyLevel": "B2",
|
||||
"reviewHistory": null,
|
||||
"lastQuestionType": null,
|
||||
"createdAt": "2025-09-27T13:10:39.29807",
|
||||
"updatedAt": "2025-09-27T13:10:39.29807",
|
||||
"user": null,
|
||||
"studyRecords": [],
|
||||
"flashcardTags": [],
|
||||
"errorReports": [],
|
||||
"flashcardExampleImages": [
|
||||
{
|
||||
"flashcardId": "",
|
||||
"exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"isPrimary": true,
|
||||
"exampleImage": {
|
||||
"id": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png",
|
||||
"qualityScore": 95,
|
||||
"fileSize": 2048576,
|
||||
"createdAt": "2025-09-27T13:00:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b7c8d9e0-f1a2-3456-7890-23456789abcd",
|
||||
"userId": "00000000-0000-0000-0000-000000000001",
|
||||
"word": "furious",
|
||||
"translation": "憤怒的,狂怒的",
|
||||
"definition": "Feeling or showing extreme anger.",
|
||||
"partOfSpeech": "adjective",
|
||||
"pronunciation": "/ˈfjʊəriəs/",
|
||||
"example": "She was furious when she found out her flight was delayed.",
|
||||
"exampleTranslation": "她發現航班延誤時非常憤怒。",
|
||||
"easinessFactor": 2.5,
|
||||
"repetitions": 0,
|
||||
"intervalDays": 1,
|
||||
"nextReviewDate": "2025-09-27T00:00:00",
|
||||
"masteryLevel": 0,
|
||||
"timesReviewed": 0,
|
||||
"timesCorrect": 0,
|
||||
"lastReviewedAt": null,
|
||||
"isFavorite": false,
|
||||
"isArchived": false,
|
||||
"difficultyLevel": "B2",
|
||||
"reviewHistory": null,
|
||||
"lastQuestionType": null,
|
||||
"createdAt": "2025-09-27T13:11:39.29807",
|
||||
"updatedAt": "2025-09-27T13:11:39.29807",
|
||||
"user": null,
|
||||
"studyRecords": [],
|
||||
"flashcardTags": [],
|
||||
"errorReports": [],
|
||||
"flashcardExampleImages": [
|
||||
{
|
||||
"flashcardId": "",
|
||||
"exampleImageId": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"isPrimary": true,
|
||||
"exampleImage": {
|
||||
"id": "bb389dab-582f-405f-8225-5c2749eaceee",
|
||||
"relativePath": "cdb73809-8dfd-4a68-acc1-ba59c62e8e76_bb389dab-582f-405f-8225-5c2749eaceee.png",
|
||||
"qualityScore": 95,
|
||||
"fileSize": 2048576,
|
||||
"createdAt": "2025-09-27T13:00:00"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"count": 10
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Navigation } from '@/components/Navigation'
|
||||
import {
|
||||
FlipMemoryTest,
|
||||
|
|
@ -11,10 +11,12 @@ import {
|
|||
SentenceListeningTest,
|
||||
SentenceSpeakingTest
|
||||
} from '@/components/review/review-tests'
|
||||
import exampleData from './example-data.json'
|
||||
|
||||
export default function ReviewTestsPage() {
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
const [activeTab, setActiveTab] = useState('FlipMemoryTest')
|
||||
const [currentCardIndex, setCurrentCardIndex] = useState(0)
|
||||
|
||||
// 測驗組件清單
|
||||
const testComponents = [
|
||||
|
|
@ -33,20 +35,41 @@ export default function ReviewTestsPage() {
|
|||
setLogs(prev => [`[${activeTab}] [${timestamp}] ${message}`, ...prev.slice(0, 9)])
|
||||
}
|
||||
|
||||
// 模擬資料
|
||||
const mockCardData = {
|
||||
word: "elaborate",
|
||||
definition: "To explain something in more detail; to develop or present a theory, policy, or system in further detail",
|
||||
example: "Could you elaborate on your proposal for the new marketing strategy?",
|
||||
exampleTranslation: "你能詳細說明一下你對新行銷策略的提案嗎?",
|
||||
pronunciation: "/ɪˈlæbərət/",
|
||||
synonyms: ["explain", "detail", "expand", "clarify"],
|
||||
difficultyLevel: "B2",
|
||||
exampleImage: "https://via.placeholder.com/400x200?text=Marketing+Strategy"
|
||||
// 從 API 響應格式獲取當前卡片資料
|
||||
const flashcardsData = exampleData.data || []
|
||||
const currentCard = flashcardsData[currentCardIndex] || flashcardsData[0]
|
||||
|
||||
// 轉換為組件所需格式
|
||||
const mockCardData = currentCard ? {
|
||||
word: currentCard.word,
|
||||
definition: currentCard.definition,
|
||||
example: currentCard.example,
|
||||
exampleTranslation: currentCard.exampleTranslation,
|
||||
pronunciation: currentCard.pronunciation,
|
||||
difficultyLevel: currentCard.difficultyLevel,
|
||||
translation: currentCard.translation
|
||||
} : {
|
||||
word: "loading...",
|
||||
definition: "Loading...",
|
||||
example: "Loading...",
|
||||
exampleTranslation: "載入中...",
|
||||
pronunciation: "",
|
||||
difficultyLevel: "A1",
|
||||
translation: "載入中"
|
||||
}
|
||||
|
||||
// 選項題選項
|
||||
const vocabChoiceOptions = ["elaborate", "celebrate", "collaborate", "deliberate"]
|
||||
// 選項題選項 - 從資料中生成
|
||||
const generateVocabChoiceOptions = () => {
|
||||
if (!currentCard) return ["loading"]
|
||||
const correctAnswer = currentCard.word
|
||||
const otherWords = flashcardsData
|
||||
.filter(card => card.word !== correctAnswer)
|
||||
.slice(0, 3)
|
||||
.map(card => card.word)
|
||||
return [correctAnswer, ...otherWords].sort(() => Math.random() - 0.5)
|
||||
}
|
||||
|
||||
const vocabChoiceOptions = generateVocabChoiceOptions()
|
||||
|
||||
// 回調函數
|
||||
const handleConfidenceSubmit = (level: number) => {
|
||||
|
|
@ -74,6 +97,27 @@ export default function ReviewTestsPage() {
|
|||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Review 組件設計</h1>
|
||||
<p className="text-gray-600">所有 review-tests 組件的 UI 設計頁面</p>
|
||||
|
||||
{/* 卡片切換控制 */}
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setCurrentCardIndex(Math.max(0, currentCardIndex - 1))}
|
||||
disabled={currentCardIndex === 0}
|
||||
className="px-3 py-1 bg-gray-500 text-white rounded disabled:opacity-50"
|
||||
>
|
||||
上一張
|
||||
</button>
|
||||
<span className="text-sm text-gray-600">
|
||||
卡片 {currentCardIndex + 1} / {flashcardsData.length} - {currentCard?.word || 'loading'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setCurrentCardIndex(Math.min(flashcardsData.length - 1, currentCardIndex + 1))}
|
||||
disabled={currentCardIndex >= flashcardsData.length - 1}
|
||||
className="px-3 py-1 bg-gray-500 text-white rounded disabled:opacity-50"
|
||||
>
|
||||
下一張
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab 導航 */}
|
||||
|
|
@ -113,7 +157,7 @@ export default function ReviewTestsPage() {
|
|||
example={mockCardData.example}
|
||||
exampleTranslation={mockCardData.exampleTranslation}
|
||||
pronunciation={mockCardData.pronunciation}
|
||||
synonyms={mockCardData.synonyms}
|
||||
synonyms={[]}
|
||||
difficultyLevel={mockCardData.difficultyLevel}
|
||||
onConfidenceSubmit={handleConfidenceSubmit}
|
||||
onReportError={handleReportError}
|
||||
|
|
@ -142,7 +186,7 @@ export default function ReviewTestsPage() {
|
|||
exampleTranslation={mockCardData.exampleTranslation}
|
||||
pronunciation={mockCardData.pronunciation}
|
||||
difficultyLevel={mockCardData.difficultyLevel}
|
||||
exampleImage={mockCardData.exampleImage}
|
||||
exampleImage="https://via.placeholder.com/400x200?text=Example+Image"
|
||||
onAnswer={handleAnswer}
|
||||
onReportError={handleReportError}
|
||||
onImageClick={handleImageClick}
|
||||
|
|
@ -191,7 +235,7 @@ export default function ReviewTestsPage() {
|
|||
example={mockCardData.example}
|
||||
exampleTranslation={mockCardData.exampleTranslation}
|
||||
difficultyLevel={mockCardData.difficultyLevel}
|
||||
exampleImage={mockCardData.exampleImage}
|
||||
exampleImage="https://via.placeholder.com/400x200?text=Speaking+Test"
|
||||
onAnswer={handleAnswer}
|
||||
onReportError={handleReportError}
|
||||
onImageClick={handleImageClick}
|
||||
|
|
|
|||
Loading…
Reference in New Issue