ux: 優化學習模式的用戶體驗設計
- 簡化語音播放器,移除口音切換和音量控制 - 修正選擇題邏輯:根據英文定義選擇詞彙 - 修復播放按鈕事件冒泡問題,避免誤觸翻卡 - 優化翻卡背面設計,使用灰色區塊和自適應高度 - 統一語音播放為美式發音,提供一致體驗 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0bfb08a97b
commit
da85bb8f42
|
|
@ -73,12 +73,12 @@ export default function LearnPage() {
|
||||||
|
|
||||||
// Quiz mode options - dynamically generate from current cards
|
// Quiz mode options - dynamically generate from current cards
|
||||||
const quizOptions = [
|
const quizOptions = [
|
||||||
cards[currentCardIndex].translation,
|
cards[currentCardIndex].word,
|
||||||
...cards
|
...cards
|
||||||
.filter((_, idx) => idx !== currentCardIndex)
|
.filter((_, idx) => idx !== currentCardIndex)
|
||||||
.map(card => card.translation)
|
.map(card => card.word)
|
||||||
.slice(0, 2),
|
.slice(0, 2),
|
||||||
'建議、提議' // additional wrong option
|
'negotiate' // additional wrong option
|
||||||
].sort(() => Math.random() - 0.5) // shuffle options
|
].sort(() => Math.random() - 0.5) // shuffle options
|
||||||
|
|
||||||
const handleFlip = () => {
|
const handleFlip = () => {
|
||||||
|
|
@ -130,7 +130,7 @@ export default function LearnPage() {
|
||||||
const handleQuizAnswer = (answer: string) => {
|
const handleQuizAnswer = (answer: string) => {
|
||||||
setSelectedAnswer(answer)
|
setSelectedAnswer(answer)
|
||||||
setShowResult(true)
|
setShowResult(true)
|
||||||
if (answer === currentCard.translation) {
|
if (answer === currentCard.word) {
|
||||||
setScore({ ...score, correct: score.correct + 1, total: score.total + 1 })
|
setScore({ ...score, correct: score.correct + 1, total: score.total + 1 })
|
||||||
} else {
|
} else {
|
||||||
setScore({ ...score, total: score.total + 1 })
|
setScore({ ...score, total: score.total + 1 })
|
||||||
|
|
@ -281,12 +281,12 @@ export default function LearnPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="relative w-full h-96 cursor-pointer"
|
className="relative w-full min-h-96 cursor-pointer"
|
||||||
onClick={handleFlip}
|
onClick={handleFlip}
|
||||||
style={{ perspective: '1000px' }}
|
style={{ perspective: '1000px' }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute w-full h-full transition-transform duration-600 ${
|
className={`w-full transition-transform duration-600 ${
|
||||||
isFlipped ? 'rotate-y-180' : ''
|
isFlipped ? 'rotate-y-180' : ''
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -296,8 +296,11 @@ export default function LearnPage() {
|
||||||
>
|
>
|
||||||
{/* Front of card */}
|
{/* Front of card */}
|
||||||
<div
|
<div
|
||||||
className="absolute w-full h-full bg-white rounded-2xl shadow-xl p-8 flex flex-col items-center justify-center"
|
className="w-full min-h-96 bg-white rounded-2xl shadow-xl p-8 flex flex-col items-center justify-center"
|
||||||
style={{ backfaceVisibility: 'hidden' }}
|
style={{
|
||||||
|
backfaceVisibility: 'hidden',
|
||||||
|
display: isFlipped ? 'none' : 'flex'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-4xl font-bold text-gray-900 mb-4">
|
<div className="text-4xl font-bold text-gray-900 mb-4">
|
||||||
{currentCard.word}
|
{currentCard.word}
|
||||||
|
|
@ -311,10 +314,6 @@ export default function LearnPage() {
|
||||||
</div>
|
</div>
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
text={currentCard.word}
|
text={currentCard.word}
|
||||||
accent="us"
|
|
||||||
speed={1.0}
|
|
||||||
showAccentSelector={false}
|
|
||||||
showSpeedControl={false}
|
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -325,39 +324,36 @@ export default function LearnPage() {
|
||||||
|
|
||||||
{/* Back of card */}
|
{/* Back of card */}
|
||||||
<div
|
<div
|
||||||
className="absolute w-full h-full bg-white rounded-2xl shadow-xl p-8"
|
className="w-full bg-white rounded-2xl shadow-xl p-6"
|
||||||
style={{
|
style={{
|
||||||
backfaceVisibility: 'hidden',
|
backfaceVisibility: 'hidden',
|
||||||
transform: 'rotateY(180deg)'
|
transform: 'rotateY(180deg)',
|
||||||
|
display: isFlipped ? 'block' : 'none'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div>
|
<div className="p-3 bg-gray-50 rounded-lg">
|
||||||
<div className="text-sm font-semibold text-gray-700 mb-1">翻譯</div>
|
<div className="text-sm font-semibold text-gray-700 mb-1">翻譯</div>
|
||||||
<div className="text-2xl font-bold text-gray-900">{currentCard.translation}</div>
|
<div className="text-xl font-bold text-gray-900">{currentCard.translation}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="p-3 bg-gray-50 rounded-lg">
|
||||||
<div className="text-sm font-semibold text-gray-700 mb-1">定義</div>
|
<div className="text-sm font-semibold text-gray-700 mb-1">定義</div>
|
||||||
<div className="text-gray-600">{currentCard.definition}</div>
|
<div className="text-gray-600 text-sm">{currentCard.definition}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="p-3 bg-gray-50 rounded-lg">
|
||||||
<div className="text-sm font-semibold text-gray-700 mb-1">例句</div>
|
<div className="text-sm font-semibold text-gray-700 mb-1">例句</div>
|
||||||
<div className="text-gray-600 mb-2">{currentCard.example}</div>
|
<div className="text-gray-600 text-sm mb-2">{currentCard.example}</div>
|
||||||
<div className="text-gray-500 text-sm mb-3">{currentCard.exampleTranslation}</div>
|
<div className="text-gray-500 text-xs mb-2">{currentCard.exampleTranslation}</div>
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
text={currentCard.example}
|
text={currentCard.example}
|
||||||
accent="us"
|
className="mt-1"
|
||||||
speed={0.8}
|
|
||||||
showAccentSelector={true}
|
|
||||||
showSpeedControl={true}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="p-3 bg-gray-50 rounded-lg">
|
||||||
<div className="text-sm font-semibold text-gray-700 mb-1">同義詞</div>
|
<div className="text-sm font-semibold text-gray-700 mb-1">同義詞</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{currentCard.synonyms.map((syn, idx) => (
|
{currentCard.synonyms.map((syn, idx) => (
|
||||||
<span key={idx} className="px-3 py-1 bg-gray-100 rounded-full text-sm">
|
<span key={idx} className="px-2 py-1 bg-gray-100 rounded-full text-xs">
|
||||||
{syn}
|
{syn}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
|
|
@ -419,21 +415,19 @@ export default function LearnPage() {
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-xl p-8">
|
<div className="bg-white rounded-2xl shadow-xl p-8">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="text-sm text-gray-600 mb-2">根據定義選擇正確的中文翻譯</div>
|
<div className="text-sm text-gray-600 mb-4">根據英文定義選擇正確的英文詞彙</div>
|
||||||
<div className="text-xl text-gray-800 leading-relaxed mb-3">
|
<div className="p-4 bg-gray-50 rounded-lg">
|
||||||
{currentCard.definition}
|
<div className="text-xl text-gray-800 leading-relaxed mb-3">
|
||||||
|
{currentCard.definition}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 mb-3">
|
||||||
|
({currentCard.partOfSpeech})
|
||||||
|
</div>
|
||||||
|
<AudioPlayer
|
||||||
|
text={currentCard.definition}
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500 mb-3">
|
|
||||||
({currentCard.partOfSpeech})
|
|
||||||
</div>
|
|
||||||
<AudioPlayer
|
|
||||||
text={currentCard.definition}
|
|
||||||
accent="us"
|
|
||||||
speed={0.9}
|
|
||||||
showAccentSelector={false}
|
|
||||||
showSpeedControl={true}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -443,9 +437,9 @@ export default function LearnPage() {
|
||||||
onClick={() => !showResult && handleQuizAnswer(option)}
|
onClick={() => !showResult && handleQuizAnswer(option)}
|
||||||
disabled={showResult}
|
disabled={showResult}
|
||||||
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
|
className={`w-full p-4 text-left rounded-lg border-2 transition-all ${
|
||||||
showResult && option === currentCard.translation
|
showResult && option === currentCard.word
|
||||||
? 'border-green-500 bg-green-50'
|
? 'border-green-500 bg-green-50'
|
||||||
: showResult && option === selectedAnswer && option !== currentCard.translation
|
: showResult && option === selectedAnswer && option !== currentCard.word
|
||||||
? 'border-red-500 bg-red-50'
|
? 'border-red-500 bg-red-50'
|
||||||
: selectedAnswer === option
|
: selectedAnswer === option
|
||||||
? 'border-primary bg-primary-light'
|
? 'border-primary bg-primary-light'
|
||||||
|
|
@ -454,12 +448,12 @@ export default function LearnPage() {
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-medium">{option}</span>
|
<span className="font-medium">{option}</span>
|
||||||
{showResult && option === currentCard.translation && (
|
{showResult && option === currentCard.word && (
|
||||||
<svg className="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
{showResult && option === selectedAnswer && option !== currentCard.translation && (
|
{showResult && option === selectedAnswer && option !== currentCard.word && (
|
||||||
<svg className="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -598,10 +592,6 @@ export default function LearnPage() {
|
||||||
<div className="text-gray-500 mb-3">{currentCard.exampleTranslation}</div>
|
<div className="text-gray-500 mb-3">{currentCard.exampleTranslation}</div>
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
text={currentCard.example}
|
text={currentCard.example}
|
||||||
accent="us"
|
|
||||||
speed={0.8}
|
|
||||||
showAccentSelector={false}
|
|
||||||
showSpeedControl={true}
|
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -637,10 +627,6 @@ export default function LearnPage() {
|
||||||
<div className="flex flex-col items-center mb-6">
|
<div className="flex flex-col items-center mb-6">
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
text={currentCard.word}
|
text={currentCard.word}
|
||||||
accent="us"
|
|
||||||
speed={1.0}
|
|
||||||
showAccentSelector={true}
|
|
||||||
showSpeedControl={true}
|
|
||||||
className="mb-4"
|
className="mb-4"
|
||||||
/>
|
/>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
|
|
@ -674,7 +660,6 @@ export default function LearnPage() {
|
||||||
{/* Result Display */}
|
{/* Result Display */}
|
||||||
{showResult && (
|
{showResult && (
|
||||||
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
||||||
<div className="text-sm font-semibold text-gray-700 mb-2">單字詳情</div>
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div>
|
<div>
|
||||||
<span className="font-semibold">{currentCard.word}</span> - {currentCard.translation}
|
<span className="font-semibold">{currentCard.word}</span> - {currentCard.translation}
|
||||||
|
|
@ -728,10 +713,6 @@ export default function LearnPage() {
|
||||||
<span className="text-gray-500">{currentCard.pronunciation}</span>
|
<span className="text-gray-500">{currentCard.pronunciation}</span>
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
text={currentCard.word}
|
text={currentCard.word}
|
||||||
accent="us"
|
|
||||||
speed={1.0}
|
|
||||||
showAccentSelector={false}
|
|
||||||
showSpeedControl={false}
|
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -739,10 +720,6 @@ export default function LearnPage() {
|
||||||
<div className="text-sm text-gray-600 mb-2">完整例句發音:</div>
|
<div className="text-sm text-gray-600 mb-2">完整例句發音:</div>
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
text={currentCard.example}
|
text={currentCard.example}
|
||||||
accent="us"
|
|
||||||
speed={0.8}
|
|
||||||
showAccentSelector={true}
|
|
||||||
showSpeedControl={true}
|
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,7 @@ import { Play, Pause, Volume2, VolumeX, Settings } from 'lucide-react';
|
||||||
export interface AudioPlayerProps {
|
export interface AudioPlayerProps {
|
||||||
text: string;
|
text: string;
|
||||||
audioUrl?: string;
|
audioUrl?: string;
|
||||||
accent?: 'us' | 'uk';
|
|
||||||
speed?: number;
|
|
||||||
autoPlay?: boolean;
|
autoPlay?: boolean;
|
||||||
showAccentSelector?: boolean;
|
|
||||||
showSpeedControl?: boolean;
|
|
||||||
onPlayStart?: () => void;
|
onPlayStart?: () => void;
|
||||||
onPlayEnd?: () => void;
|
onPlayEnd?: () => void;
|
||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void;
|
||||||
|
|
@ -27,11 +23,7 @@ export interface TTSResponse {
|
||||||
export default function AudioPlayer({
|
export default function AudioPlayer({
|
||||||
text,
|
text,
|
||||||
audioUrl: providedAudioUrl,
|
audioUrl: providedAudioUrl,
|
||||||
accent = 'us',
|
|
||||||
speed = 1.0,
|
|
||||||
autoPlay = false,
|
autoPlay = false,
|
||||||
showAccentSelector = true,
|
|
||||||
showSpeedControl = true,
|
|
||||||
onPlayStart,
|
onPlayStart,
|
||||||
onPlayEnd,
|
onPlayEnd,
|
||||||
onError,
|
onError,
|
||||||
|
|
@ -39,18 +31,13 @@ export default function AudioPlayer({
|
||||||
}: AudioPlayerProps) {
|
}: AudioPlayerProps) {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isMuted, setIsMuted] = useState(false);
|
|
||||||
const [volume, setVolume] = useState(1);
|
|
||||||
const [currentAccent, setCurrentAccent] = useState<'us' | 'uk'>(accent);
|
|
||||||
const [currentSpeed, setCurrentSpeed] = useState(speed);
|
|
||||||
const [audioUrl, setAudioUrl] = useState<string | null>(providedAudioUrl || null);
|
const [audioUrl, setAudioUrl] = useState<string | null>(providedAudioUrl || null);
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
|
|
||||||
// 生成音頻
|
// 生成音頻
|
||||||
const generateAudio = async (textToSpeak: string, accent: 'us' | 'uk', speed: number) => {
|
const generateAudio = async (textToSpeak: string) => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
@ -63,8 +50,8 @@ export default function AudioPlayer({
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
text: textToSpeak,
|
text: textToSpeak,
|
||||||
accent: accent,
|
accent: 'us',
|
||||||
speed: speed,
|
speed: 1.0,
|
||||||
voice: ''
|
voice: ''
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
@ -103,7 +90,7 @@ export default function AudioPlayer({
|
||||||
|
|
||||||
// 如果沒有音頻 URL,先生成
|
// 如果沒有音頻 URL,先生成
|
||||||
if (!urlToPlay) {
|
if (!urlToPlay) {
|
||||||
urlToPlay = await generateAudio(text, currentAccent, currentSpeed);
|
urlToPlay = await generateAudio(text);
|
||||||
if (!urlToPlay) return;
|
if (!urlToPlay) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,8 +98,6 @@ export default function AudioPlayer({
|
||||||
if (!audio) return;
|
if (!audio) return;
|
||||||
|
|
||||||
audio.src = urlToPlay;
|
audio.src = urlToPlay;
|
||||||
audio.playbackRate = currentSpeed;
|
|
||||||
audio.volume = isMuted ? 0 : volume;
|
|
||||||
|
|
||||||
await audio.play();
|
await audio.play();
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
|
|
@ -134,7 +119,8 @@ export default function AudioPlayer({
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切換播放/暫停
|
// 切換播放/暫停
|
||||||
const togglePlayPause = () => {
|
const togglePlayPause = (e?: React.MouseEvent) => {
|
||||||
|
e?.stopPropagation(); // 阻止事件冒泡
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
pauseAudio();
|
pauseAudio();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -155,58 +141,10 @@ export default function AudioPlayer({
|
||||||
onError?.(errorMessage);
|
onError?.(errorMessage);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切換口音
|
|
||||||
const handleAccentChange = async (newAccent: 'us' | 'uk') => {
|
|
||||||
if (newAccent === currentAccent) return;
|
|
||||||
|
|
||||||
setCurrentAccent(newAccent);
|
|
||||||
setAudioUrl(null); // 清除現有音頻,強制重新生成
|
|
||||||
|
|
||||||
// 如果正在播放,停止並重新生成
|
|
||||||
if (isPlaying) {
|
|
||||||
pauseAudio();
|
|
||||||
await generateAudio(text, newAccent, currentSpeed);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 切換速度
|
|
||||||
const handleSpeedChange = async (newSpeed: number) => {
|
|
||||||
if (newSpeed === currentSpeed) return;
|
|
||||||
|
|
||||||
setCurrentSpeed(newSpeed);
|
|
||||||
|
|
||||||
// 如果音頻正在播放,直接調整播放速度
|
|
||||||
const audio = audioRef.current;
|
|
||||||
if (audio && isPlaying) {
|
|
||||||
audio.playbackRate = newSpeed;
|
|
||||||
} else {
|
|
||||||
// 否則清除音頻,重新生成
|
|
||||||
setAudioUrl(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 音量控制
|
|
||||||
const handleVolumeChange = (newVolume: number) => {
|
|
||||||
setVolume(newVolume);
|
|
||||||
const audio = audioRef.current;
|
|
||||||
if (audio) {
|
|
||||||
audio.volume = isMuted ? 0 : newVolume;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleMute = () => {
|
|
||||||
const newMuted = !isMuted;
|
|
||||||
setIsMuted(newMuted);
|
|
||||||
const audio = audioRef.current;
|
|
||||||
if (audio) {
|
|
||||||
audio.volume = newMuted ? 0 : volume;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 自動播放
|
// 自動播放
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoPlay && text && !audioUrl) {
|
if (autoPlay && text && !audioUrl) {
|
||||||
generateAudio(text, currentAccent, currentSpeed);
|
generateAudio(text);
|
||||||
}
|
}
|
||||||
}, [autoPlay, text]);
|
}, [autoPlay, text]);
|
||||||
|
|
||||||
|
|
@ -242,75 +180,6 @@ export default function AudioPlayer({
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 口音選擇器 */}
|
|
||||||
{showAccentSelector && (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<button
|
|
||||||
onClick={() => handleAccentChange('us')}
|
|
||||||
className={`
|
|
||||||
px-2 py-1 text-xs rounded transition-colors
|
|
||||||
${currentAccent === 'us'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
US
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleAccentChange('uk')}
|
|
||||||
className={`
|
|
||||||
px-2 py-1 text-xs rounded transition-colors
|
|
||||||
${currentAccent === 'uk'
|
|
||||||
? 'bg-blue-600 text-white'
|
|
||||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
UK
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 速度控制 */}
|
|
||||||
{showSpeedControl && (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span className="text-xs text-gray-600">Speed:</span>
|
|
||||||
<select
|
|
||||||
value={currentSpeed}
|
|
||||||
onChange={(e) => handleSpeedChange(parseFloat(e.target.value))}
|
|
||||||
className="text-xs border border-gray-300 rounded px-1 py-0.5"
|
|
||||||
>
|
|
||||||
<option value={0.5}>0.5x</option>
|
|
||||||
<option value={0.75}>0.75x</option>
|
|
||||||
<option value={1.0}>1x</option>
|
|
||||||
<option value={1.25}>1.25x</option>
|
|
||||||
<option value={1.5}>1.5x</option>
|
|
||||||
<option value={2.0}>2x</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 音量控制 */}
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<button
|
|
||||||
onClick={toggleMute}
|
|
||||||
className="p-1 text-gray-600 hover:text-gray-800"
|
|
||||||
title={isMuted ? 'Unmute' : 'Mute'}
|
|
||||||
>
|
|
||||||
{isMuted ? <VolumeX size={16} /> : <Volume2 size={16} />}
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.1}
|
|
||||||
value={isMuted ? 0 : volume}
|
|
||||||
onChange={(e) => handleVolumeChange(parseFloat(e.target.value))}
|
|
||||||
className="w-16 h-1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 錯誤顯示 */}
|
{/* 錯誤顯示 */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-xs text-red-600 bg-red-50 px-2 py-1 rounded">
|
<div className="text-xs text-red-600 bg-red-50 px-2 py-1 rounded">
|
||||||
|
|
|
||||||
|
|
@ -168,10 +168,6 @@ export function FlashcardForm({ cardSets, initialData, isEdit = false, onSuccess
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<AudioPlayer
|
<AudioPlayer
|
||||||
text={formData.english}
|
text={formData.english}
|
||||||
accent="us"
|
|
||||||
speed={1.0}
|
|
||||||
showAccentSelector={true}
|
|
||||||
showSpeedControl={false}
|
|
||||||
className="w-auto"
|
className="w-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue