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;
|
namespace DramaLing.Api.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/flashcards-simple")]
|
[Route("api/flashcards")]
|
||||||
[AllowAnonymous] // 暫時移除認證要求,修復網路錯誤
|
[AllowAnonymous] // 暫時移除認證要求,修復網路錯誤
|
||||||
public class SimplifiedFlashcardsController : ControllerBase
|
public class FlashcardsController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly DramaLingDbContext _context;
|
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;
|
_context = context;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
@ -105,7 +105,7 @@ public class SimplifiedFlashcardsController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<ActionResult> CreateFlashcard([FromBody] CreateSimpleFlashcardRequest request)
|
public async Task<ActionResult> CreateFlashcard([FromBody] CreateFlashcardRequest request)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -250,7 +250,7 @@ public class SimplifiedFlashcardsController : ControllerBase
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
public async Task<ActionResult> UpdateFlashcard(Guid id, [FromBody] CreateSimpleFlashcardRequest request)
|
public async Task<ActionResult> UpdateFlashcard(Guid id, [FromBody] CreateFlashcardRequest request)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -359,8 +359,8 @@ public class SimplifiedFlashcardsController : ControllerBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 簡化的請求 DTO,移除 CardSetId
|
// 請求 DTO
|
||||||
public class CreateSimpleFlashcardRequest
|
public class CreateFlashcardRequest
|
||||||
{
|
{
|
||||||
public string Word { get; set; } = string.Empty;
|
public string Word { get; set; } = string.Empty;
|
||||||
public string Translation { 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 { useRouter } from 'next/navigation'
|
||||||
import { Navigation } from '@/components/Navigation'
|
import { Navigation } from '@/components/Navigation'
|
||||||
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||||
import { flashcardsService, type Flashcard } from '@/lib/services/flashcards'
|
import { flashcardsService, type Flashcard } from '@/lib/services/simplifiedFlashcards'
|
||||||
|
|
||||||
interface FlashcardDetailPageProps {
|
interface FlashcardDetailPageProps {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||||
import { Navigation } from '@/components/Navigation'
|
import { Navigation } from '@/components/Navigation'
|
||||||
import { FlashcardForm } from '@/components/FlashcardForm'
|
import { FlashcardForm } from '@/components/FlashcardForm'
|
||||||
// import { flashcardsService, type CardSet, type Flashcard } from '@/lib/services/flashcards'
|
// 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 類型
|
// 暫時為了兼容性定義 CardSet 類型
|
||||||
type CardSet = {
|
type CardSet = {
|
||||||
|
|
@ -16,7 +16,7 @@ type CardSet = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用簡化的 Flashcard 類型
|
// 使用簡化的 Flashcard 類型
|
||||||
type Flashcard = SimpleFlashcard
|
type Flashcard = Flashcard
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
function FlashcardsContent() {
|
function FlashcardsContent() {
|
||||||
|
|
@ -74,8 +74,8 @@ function FlashcardsContent() {
|
||||||
const [showForm, setShowForm] = useState(false)
|
const [showForm, setShowForm] = useState(false)
|
||||||
const [editingCard, setEditingCard] = useState<Flashcard | null>(null)
|
const [editingCard, setEditingCard] = useState<Flashcard | null>(null)
|
||||||
|
|
||||||
// 添加假資料用於展示CEFR效果 (更新為 SimpleFlashcard 格式)
|
// 添加假資料用於展示CEFR效果 (更新為 Flashcard 格式)
|
||||||
const mockFlashcards: SimpleFlashcard[] = [
|
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: '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: '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' },
|
{ 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 {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null) // 清除之前的錯誤
|
setError(null) // 清除之前的錯誤
|
||||||
const result = await simplifiedFlashcardsService.getFlashcards()
|
const result = await flashcardsService.getFlashcards()
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setFlashcards(result.data.flashcards)
|
setFlashcards(result.data.flashcards)
|
||||||
console.log('✅ 詞卡載入成功:', result.data.flashcards.length, '個詞卡')
|
console.log('✅ 詞卡載入成功:', result.data.flashcards.length, '個詞卡')
|
||||||
|
|
@ -140,7 +140,7 @@ function FlashcardsContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await simplifiedFlashcardsService.deleteFlashcard(card.id)
|
const result = await flashcardsService.deleteFlashcard(card.id)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
loadFlashcards()
|
loadFlashcards()
|
||||||
alert(`詞卡「${card.word}」已刪除`)
|
alert(`詞卡「${card.word}」已刪除`)
|
||||||
|
|
@ -162,7 +162,7 @@ function FlashcardsContent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 真實API調用
|
// 真實API調用
|
||||||
const result = await simplifiedFlashcardsService.toggleFavorite(card.id)
|
const result = await flashcardsService.toggleFavorite(card.id)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
loadFlashcards()
|
loadFlashcards()
|
||||||
alert(`${card.isFavorite ? '已取消收藏' : '已加入收藏'}「${card.word}」`)
|
alert(`${card.isFavorite ? '已取消收藏' : '已加入收藏'}「${card.word}」`)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { useState, useMemo, useCallback } from 'react'
|
||||||
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
||||||
import { Navigation } from '@/components/Navigation'
|
import { Navigation } from '@/components/Navigation'
|
||||||
import { ClickableTextV2 } from '@/components/ClickableTextV2'
|
import { ClickableTextV2 } from '@/components/ClickableTextV2'
|
||||||
import { simplifiedFlashcardsService } from '@/lib/services/simplifiedFlashcards'
|
import { flashcardsService } from '@/lib/services/flashcards'
|
||||||
import { Play } from 'lucide-react'
|
import { Play } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
|
@ -215,7 +215,7 @@ function GenerateContent() {
|
||||||
example: `Example sentence with ${word}.` // 提供預設例句
|
example: `Example sentence with ${word}.` // 提供預設例句
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await simplifiedFlashcardsService.createFlashcard(cardData)
|
const response = await flashcardsService.createFlashcard(cardData)
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log(`✅ 已將「${word}」保存到詞卡!`)
|
console.log(`✅ 已將「${word}」保存到詞卡!`)
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,34 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { flashcardsService, type CreateFlashcardRequest, type CardSet } from '@/lib/services/flashcards'
|
import { flashcardsService, type CreateFlashcardRequest, type Flashcard } from '@/lib/services/flashcards'
|
||||||
import AudioPlayer from './AudioPlayer'
|
import AudioPlayer from './AudioPlayer'
|
||||||
|
|
||||||
interface FlashcardFormProps {
|
interface FlashcardFormProps {
|
||||||
cardSets: CardSet[]
|
initialData?: Partial<Flashcard>
|
||||||
initialData?: Partial<CreateFlashcardRequest & { id: string }>
|
|
||||||
isEdit?: boolean
|
isEdit?: boolean
|
||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlashcardForm({ cardSets, initialData, isEdit = false, onSuccess, onCancel }: FlashcardFormProps) {
|
export function FlashcardForm({ 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<CreateFlashcardRequest>({
|
const [formData, setFormData] = useState<CreateFlashcardRequest>({
|
||||||
cardSetId: getDefaultCardSetId(),
|
|
||||||
word: initialData?.word || '',
|
word: initialData?.word || '',
|
||||||
translation: initialData?.translation || '',
|
translation: initialData?.translation || '',
|
||||||
definition: initialData?.definition || '',
|
definition: initialData?.definition || '',
|
||||||
pronunciation: initialData?.pronunciation || '',
|
pronunciation: initialData?.pronunciation || '',
|
||||||
partOfSpeech: initialData?.partOfSpeech || '名詞',
|
partOfSpeech: initialData?.partOfSpeech || '名詞',
|
||||||
example: initialData?.example || '',
|
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 [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
@ -71,196 +46,162 @@ export function FlashcardForm({ cardSets, initialData, isEdit = false, onSuccess
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
onSuccess()
|
onSuccess()
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || '操作失敗')
|
setError(result.error || `Failed to ${isEdit ? 'update' : 'create'} flashcard`)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
setError('操作失敗,請重試')
|
setError(error instanceof Error ? error.message : 'An unexpected error occurred')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (field: keyof CreateFlashcardRequest, value: string) => {
|
|
||||||
setFormData(prev => ({ ...prev, [field]: value }))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
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>
|
|
||||||
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* 詞卡集合選擇 */}
|
{error && (
|
||||||
<div>
|
<div className="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-lg">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
{error}
|
||||||
詞卡集合 *
|
|
||||||
</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>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<select
|
|
||||||
value={formData.cardSetId}
|
<div>
|
||||||
onChange={(e) => handleChange('cardSetId', e.target.value)}
|
<label htmlFor="word" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
|
單字 *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="word"
|
||||||
|
name="word"
|
||||||
|
value={formData.word}
|
||||||
|
onChange={handleChange}
|
||||||
required
|
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="請輸入單字"
|
||||||
{!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>
|
||||||
|
|
||||||
{/* 英文單字 */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<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>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={formData.word}
|
id="pronunciation"
|
||||||
onChange={(e) => handleChange('word', e.target.value)}
|
name="pronunciation"
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
|
value={formData.pronunciation}
|
||||||
placeholder="例如:negotiate"
|
onChange={handleChange}
|
||||||
required
|
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.word && (
|
{formData.pronunciation && (
|
||||||
<div className="flex-shrink-0">
|
<AudioPlayer text={formData.word} />
|
||||||
<AudioPlayer
|
|
||||||
text={formData.word}
|
|
||||||
className="w-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 中文翻譯 */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="partOfSpeech" 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>
|
</label>
|
||||||
<select
|
<select
|
||||||
|
id="partOfSpeech"
|
||||||
|
name="partOfSpeech"
|
||||||
value={formData.partOfSpeech}
|
value={formData.partOfSpeech}
|
||||||
onChange={(e) => handleChange('partOfSpeech', e.target.value)}
|
onChange={handleChange}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
|
|
||||||
required
|
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"
|
||||||
>
|
>
|
||||||
{partOfSpeechOptions.map(option => (
|
<option value="名詞">名詞 (noun)</option>
|
||||||
<option key={option} value={option}>
|
<option value="動詞">動詞 (verb)</option>
|
||||||
{option}
|
<option value="形容詞">形容詞 (adjective)</option>
|
||||||
</option>
|
<option value="副詞">副詞 (adverb)</option>
|
||||||
))}
|
<option value="介詞">介詞 (preposition)</option>
|
||||||
|
<option value="連詞">連詞 (conjunction)</option>
|
||||||
|
<option value="感歎詞">感歎詞 (interjection)</option>
|
||||||
|
<option value="片語">片語 (phrase)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label htmlFor="example" 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>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
|
id="example"
|
||||||
|
name="example"
|
||||||
value={formData.example}
|
value={formData.example}
|
||||||
onChange={(e) => handleChange('example', e.target.value)}
|
onChange={handleChange}
|
||||||
rows={3}
|
required
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent"
|
rows={2}
|
||||||
placeholder="例如:We need to negotiate the contract terms."
|
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>
|
||||||
|
|
||||||
{/* 操作按鈕 */}
|
<div>
|
||||||
<div className="flex justify-end space-x-4 pt-4">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
|
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>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue