ux: 優化學習頁面用戶體驗和互動設計

- 修正翻卡模式卡片翻轉動畫和版面配置
- 改善選擇題模式答案顯示和回饋機制
- 優化語音錄音組件狀態管理
- 加強用戶交互體驗和視覺回饋

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-19 17:44:39 +08:00
parent da85bb8f42
commit 31e3fe9fa8
2 changed files with 646 additions and 535 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
import { useState, useRef, useCallback, useEffect } from 'react'; import { useState, useRef, useCallback, useEffect } from 'react';
import { Mic, Square, Play, Upload } from 'lucide-react'; import { Mic, Square, Play, Upload } from 'lucide-react';
import AudioPlayer from './AudioPlayer';
export interface PronunciationScore { export interface PronunciationScore {
overall: number; overall: number;
@ -21,6 +22,8 @@ export interface PhonemeScore {
export interface VoiceRecorderProps { export interface VoiceRecorderProps {
targetText: string; targetText: string;
targetTranslation?: string;
exampleImage?: string;
onScoreReceived?: (score: PronunciationScore) => void; onScoreReceived?: (score: PronunciationScore) => void;
onRecordingComplete?: (audioBlob: Blob) => void; onRecordingComplete?: (audioBlob: Blob) => void;
maxDuration?: number; maxDuration?: number;
@ -30,6 +33,8 @@ export interface VoiceRecorderProps {
export default function VoiceRecorder({ export default function VoiceRecorder({
targetText, targetText,
targetTranslation,
exampleImage,
onScoreReceived, onScoreReceived,
onRecordingComplete, onRecordingComplete,
maxDuration = 30, // 30 seconds default maxDuration = 30, // 30 seconds default
@ -233,20 +238,44 @@ export default function VoiceRecorder({
}, [audioUrl]); }, [audioUrl]);
return ( return (
<div className={`voice-recorder p-6 border-2 border-dashed border-gray-300 rounded-xl ${className}`}> <div className={`voice-recorder ${className}`}>
{/* 隱藏的音頻元素 */} {/* 隱藏的音頻元素 */}
<audio ref={audioRef} /> <audio ref={audioRef} />
{/* Example Image */}
{exampleImage && (
<div className="mb-4">
<img
src={exampleImage}
alt="Example context"
className="w-full rounded-lg shadow-md cursor-pointer hover:shadow-lg transition-shadow"
style={{ maxHeight: '400px', objectFit: 'contain' }}
/>
<div className="text-xs text-gray-500 mt-2 text-center"></div>
</div>
)}
{/* 目標文字顯示 */} {/* 目標文字顯示 */}
<div className="text-center mb-6"> <div className="mb-6">
<h3 className="text-lg font-semibold mb-2"></h3> <div className="p-4 bg-gray-50 rounded-lg">
<p className="text-2xl font-medium text-gray-800 p-4 bg-blue-50 rounded-lg"> <div className="flex items-start justify-between gap-3">
{targetText} <div className="flex-1">
</p> <div className="text-gray-800 text-lg mb-2">{targetText}</div>
{targetTranslation && (
<div className="text-gray-600 text-base">{targetTranslation}</div>
)}
</div>
<AudioPlayer
text={targetText}
className="flex-shrink-0 mt-1"
/>
</div>
</div>
</div> </div>
{/* 錄音控制區 */} {/* 錄音控制區 */}
<div className="flex flex-col items-center gap-4"> <div className="p-6 border-2 border-dashed border-gray-300 rounded-xl">
<div className="flex flex-col items-center gap-4">
{/* 錄音按鈕 */} {/* 錄音按鈕 */}
<button <button
onClick={isRecording ? stopRecording : startRecording} onClick={isRecording ? stopRecording : startRecording}
@ -360,6 +389,7 @@ export default function VoiceRecorder({
)} )}
</div> </div>
)} )}
</div>
</div> </div>
</div> </div>
); );