refactor: 完成前端組件重命名,統一使用flashcards服務

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-24 01:21:42 +08:00
parent 9bb63c4ce3
commit fd58f43b9b
5 changed files with 173 additions and 232 deletions

View File

@ -8,14 +8,14 @@ using System.Security.Claims;
namespace DramaLing.Api.Controllers;
[ApiController]
[Route("api/flashcards-simple")]
[Route("api/flashcards")]
[AllowAnonymous] // 暫時移除認證要求,修復網路錯誤
public class SimplifiedFlashcardsController : ControllerBase
public class FlashcardsController : ControllerBase
{
private readonly DramaLingDbContext _context;
private readonly ILogger<SimplifiedFlashcardsController> _logger;
private readonly ILogger<FlashcardsController> _logger;
public SimplifiedFlashcardsController(DramaLingDbContext context, ILogger<SimplifiedFlashcardsController> logger)
public FlashcardsController(DramaLingDbContext context, ILogger<FlashcardsController> logger)
{
_context = context;
_logger = logger;
@ -105,7 +105,7 @@ public class SimplifiedFlashcardsController : ControllerBase
}
[HttpPost]
public async Task<ActionResult> CreateFlashcard([FromBody] CreateSimpleFlashcardRequest request)
public async Task<ActionResult> CreateFlashcard([FromBody] CreateFlashcardRequest request)
{
try
{
@ -250,7 +250,7 @@ public class SimplifiedFlashcardsController : ControllerBase
}
[HttpPut("{id}")]
public async Task<ActionResult> UpdateFlashcard(Guid id, [FromBody] CreateSimpleFlashcardRequest request)
public async Task<ActionResult> UpdateFlashcard(Guid id, [FromBody] CreateFlashcardRequest request)
{
try
{
@ -359,8 +359,8 @@ public class SimplifiedFlashcardsController : ControllerBase
}
}
// 簡化的請求 DTO,移除 CardSetId
public class CreateSimpleFlashcardRequest
// 請求 DTO
public class CreateFlashcardRequest
{
public string Word { get; set; } = string.Empty;
public string Translation { get; set; } = string.Empty;

View File

@ -4,7 +4,7 @@ import { useState, useEffect, use } from 'react'
import { useRouter } from 'next/navigation'
import { Navigation } from '@/components/Navigation'
import { ProtectedRoute } from '@/components/ProtectedRoute'
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards'
import { flashcardsService, type Flashcard } from '@/lib/services/simplifiedFlashcards'
interface FlashcardDetailPageProps {
params: Promise<{

View File

@ -6,7 +6,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute'
import { Navigation } from '@/components/Navigation'
import { FlashcardForm } from '@/components/FlashcardForm'
// import { flashcardsService, type CardSet, type Flashcard } from '@/lib/services/flashcards'
import { simplifiedFlashcardsService, type SimpleFlashcard } from '@/lib/services/simplifiedFlashcards'
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards'
// 暫時為了兼容性定義 CardSet 類型
type CardSet = {
@ -16,7 +16,7 @@ type CardSet = {
}
// 使用簡化的 Flashcard 類型
type Flashcard = SimpleFlashcard
type Flashcard = Flashcard
import { useRouter } from 'next/navigation'
function FlashcardsContent() {
@ -74,8 +74,8 @@ function FlashcardsContent() {
const [showForm, setShowForm] = useState(false)
const [editingCard, setEditingCard] = useState<Flashcard | null>(null)
// 添加假資料用於展示CEFR效果 (更新為 SimpleFlashcard 格式)
const mockFlashcards: SimpleFlashcard[] = [
// 添加假資料用於展示CEFR效果 (更新為 Flashcard 格式)
const mockFlashcards: Flashcard[] = [
{ id: 'mock1', word: 'hello', translation: '你好', partOfSpeech: 'interjection', pronunciation: '/həˈloʊ/', masteryLevel: 95, timesReviewed: 15, isFavorite: true, nextReviewDate: '2025-09-21', difficultyLevel: 'A1', definition: 'A greeting word', example: 'Hello, how are you?', createdAt: '2025-09-17', updatedAt: '2025-09-17' },
{ id: 'mock2', word: 'beautiful', translation: '美麗的', partOfSpeech: 'adjective', pronunciation: '/ˈbjuːtɪfəl/', masteryLevel: 78, timesReviewed: 8, isFavorite: false, nextReviewDate: '2025-09-22', difficultyLevel: 'A2', definition: 'Pleasing to look at', example: 'The beautiful sunset', createdAt: '2025-09-16', updatedAt: '2025-09-16' },
{ id: 'mock3', word: 'understand', translation: '理解', partOfSpeech: 'verb', pronunciation: '/ˌʌndərˈstænd/', masteryLevel: 65, timesReviewed: 12, isFavorite: true, nextReviewDate: '2025-09-20', difficultyLevel: 'B1', definition: 'To comprehend', example: 'I understand the concept', createdAt: '2025-09-15', updatedAt: '2025-09-15' },
@ -99,7 +99,7 @@ function FlashcardsContent() {
try {
setLoading(true)
setError(null) // 清除之前的錯誤
const result = await simplifiedFlashcardsService.getFlashcards()
const result = await flashcardsService.getFlashcards()
if (result.success && result.data) {
setFlashcards(result.data.flashcards)
console.log('✅ 詞卡載入成功:', result.data.flashcards.length, '個詞卡')
@ -140,7 +140,7 @@ function FlashcardsContent() {
}
try {
const result = await simplifiedFlashcardsService.deleteFlashcard(card.id)
const result = await flashcardsService.deleteFlashcard(card.id)
if (result.success) {
loadFlashcards()
alert(`詞卡「${card.word}」已刪除`)
@ -162,7 +162,7 @@ function FlashcardsContent() {
}
// 真實API調用
const result = await simplifiedFlashcardsService.toggleFavorite(card.id)
const result = await flashcardsService.toggleFavorite(card.id)
if (result.success) {
loadFlashcards()
alert(`${card.isFavorite ? '已取消收藏' : '已加入收藏'}${card.word}`)

View File

@ -4,7 +4,7 @@ import { useState, useMemo, useCallback } from 'react'
import { ProtectedRoute } from '@/components/ProtectedRoute'
import { Navigation } from '@/components/Navigation'
import { ClickableTextV2 } from '@/components/ClickableTextV2'
import { simplifiedFlashcardsService } from '@/lib/services/simplifiedFlashcards'
import { flashcardsService } from '@/lib/services/flashcards'
import { Play } from 'lucide-react'
import Link from 'next/link'
@ -215,7 +215,7 @@ function GenerateContent() {
example: `Example sentence with ${word}.` // 提供預設例句
}
const response = await simplifiedFlashcardsService.createFlashcard(cardData)
const response = await flashcardsService.createFlashcard(cardData)
if (response.success) {
console.log(`✅ 已將「${word}」保存到詞卡!`)

View File

@ -1,59 +1,34 @@
'use client'
import React, { useState, useEffect } from 'react'
import { flashcardsService, type CreateFlashcardRequest, type CardSet } from '@/lib/services/flashcards'
import React, { useState } from 'react'
import { flashcardsService, type CreateFlashcardRequest, type Flashcard } from '@/lib/services/flashcards'
import AudioPlayer from './AudioPlayer'
interface FlashcardFormProps {
cardSets: CardSet[]
initialData?: Partial<CreateFlashcardRequest & { id: string }>
initialData?: Partial<Flashcard>
isEdit?: boolean
onSuccess: () => void
onCancel: () => void
}
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 ''
}
export function FlashcardForm({ initialData, isEdit = false, onSuccess, onCancel }: FlashcardFormProps) {
const [formData, setFormData] = useState<CreateFlashcardRequest>({
cardSetId: getDefaultCardSetId(),
word: initialData?.word || '',
translation: initialData?.translation || '',
definition: initialData?.definition || '',
pronunciation: initialData?.pronunciation || '',
partOfSpeech: initialData?.partOfSpeech || '名詞',
example: initialData?.example || '',
exampleTranslation: initialData?.exampleTranslation || '',
})
// 當 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<string | null>(null)
const partOfSpeechOptions = [
'名詞', '動詞', '形容詞', '副詞', '介詞', '連詞', '感嘆詞', '代詞', '冠詞'
]
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
@ -71,196 +46,162 @@ export function FlashcardForm({ cardSets, initialData, isEdit = false, onSuccess
if (result.success) {
onSuccess()
} else {
setError(result.error || '操作失敗')
setError(result.error || `Failed to ${isEdit ? 'update' : 'create'} flashcard`)
}
} catch (err) {
setError('操作失敗,請重試')
} catch (error) {
setError(error instanceof Error ? error.message : 'An unexpected error occurred')
} finally {
setLoading(false)
}
}
const handleChange = (field: keyof CreateFlashcardRequest, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">
{isEdit ? '編輯詞卡' : '新增詞卡'}
</h2>
<button
onClick={onCancel}
className="text-gray-400 hover:text-gray-600"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg">
{error}
</div>
)}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<p className="text-red-600 text-sm">{error}</p>
</div>
<div>
<label htmlFor="word" className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
id="word"
name="word"
value={formData.word}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="請輸入單字"
/>
</div>
<div>
<label htmlFor="translation" className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
id="translation"
name="translation"
value={formData.translation}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="請輸入中文翻譯"
/>
</div>
<div>
<label htmlFor="definition" className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<textarea
id="definition"
name="definition"
value={formData.definition}
onChange={handleChange}
required
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="請輸入英文定義"
/>
</div>
<div>
<label htmlFor="pronunciation" className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="flex gap-2">
<input
type="text"
id="pronunciation"
name="pronunciation"
value={formData.pronunciation}
onChange={handleChange}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="例如: /wɜːrd/"
/>
{formData.pronunciation && (
<AudioPlayer text={formData.word} />
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* 詞卡集合選擇 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
{cardSets.length === 0 ? (
<div className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-100 text-gray-500">
...
</div>
) : (
<select
value={formData.cardSetId}
onChange={(e) => handleChange('cardSetId', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
required
>
{/* 如果沒有選中任何卡組,顯示提示 */}
{!formData.cardSetId && (
<option value="" disabled>
</option>
)}
{/* 先顯示預設卡組 */}
{cardSets
.filter(set => set.isDefault)
.map(set => (
<option key={set.id} value={set.id}>
📂 {set.name} ()
</option>
))}
{/* 再顯示其他卡組 */}
{cardSets
.filter(set => !set.isDefault)
.map(set => (
<option key={set.id} value={set.id}>
{set.name}
</option>
))}
</select>
)}
</div>
{/* 英文單字 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<div className="flex gap-2">
<input
type="text"
value={formData.word}
onChange={(e) => handleChange('word', e.target.value)}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="例如negotiate"
required
/>
{formData.word && (
<div className="flex-shrink-0">
<AudioPlayer
text={formData.word}
className="w-auto"
/>
</div>
)}
</div>
</div>
{/* 中文翻譯 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<input
type="text"
value={formData.translation}
onChange={(e) => handleChange('translation', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="例如:談判,協商"
required
/>
</div>
{/* 詞性和發音 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<select
value={formData.partOfSpeech}
onChange={(e) => handleChange('partOfSpeech', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
required
>
{partOfSpeechOptions.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<input
type="text"
value={formData.pronunciation}
onChange={(e) => handleChange('pronunciation', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="例如:/nɪˈɡoʊʃieɪt/"
/>
</div>
</div>
{/* 例句 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<textarea
value={formData.example}
onChange={(e) => handleChange('example', e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
placeholder="例如We need to negotiate the contract terms."
/>
</div>
{/* 操作按鈕 */}
<div className="flex justify-end space-x-4 pt-4">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
>
</button>
<button
type="submit"
disabled={loading || cardSets.length === 0 || !formData.cardSetId}
className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? '處理中...' :
cardSets.length === 0 ? '載入中...' :
(isEdit ? '更新詞卡' : '新增詞卡')}
</button>
</div>
</form>
</div>
</div>
</div>
<div>
<label htmlFor="partOfSpeech" className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<select
id="partOfSpeech"
name="partOfSpeech"
value={formData.partOfSpeech}
onChange={handleChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="名詞"> (noun)</option>
<option value="動詞"> (verb)</option>
<option value="形容詞"> (adjective)</option>
<option value="副詞"> (adverb)</option>
<option value="介詞"> (preposition)</option>
<option value="連詞"> (conjunction)</option>
<option value="感歎詞"> (interjection)</option>
<option value="片語"> (phrase)</option>
</select>
</div>
<div>
<label htmlFor="example" className="block text-sm font-medium text-gray-700 mb-2">
*
</label>
<textarea
id="example"
name="example"
value={formData.example}
onChange={handleChange}
required
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="請輸入例句"
/>
</div>
<div>
<label htmlFor="exampleTranslation" className="block text-sm font-medium text-gray-700 mb-2">
</label>
<textarea
id="exampleTranslation"
name="exampleTranslation"
value={formData.exampleTranslation}
onChange={handleChange}
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="請輸入例句的中文翻譯"
/>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
disabled={loading}
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white px-4 py-2 rounded-lg font-medium transition-colors duration-200"
>
{loading ? (isEdit ? '更新中...' : '創建中...') : (isEdit ? '更新詞卡' : '創建詞卡')}
</button>
<button
type="button"
onClick={onCancel}
disabled={loading}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 disabled:bg-gray-100 transition-colors duration-200"
>
</button>
</div>
</form>
)
}