refactor: 完成前端組件重命名,統一使用flashcards服務
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9bb63c4ce3
commit
fd58f43b9b
|
|
@ -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;
|
||||
|
|
@ -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<{
|
||||
|
|
|
|||
|
|
@ -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}」`)
|
||||
|
|
|
|||
|
|
@ -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}」保存到詞卡!`)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue