ux: 實現翻卡模式動態高度適配與版面優化
- 新增動態高度計算系統,解決翻轉時卡片大小變化問題 - 實現正面背面高度自動匹配,確保翻轉體驗一致 - 優化翻卡模式設計:移除CEFR標籤、簡化正面佈局 - 改善背面內容組織:灰底區分、左對齊文字、移除冗餘元素 - 修復背面底部空白問題,提升視覺整潔度 - 添加平滑高度過渡動畫,增強用戶體驗 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
31e3fe9fa8
commit
c1e296c860
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef, useLayoutEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Navigation } from '@/components/Navigation'
|
||||
import AudioPlayer from '@/components/AudioPlayer'
|
||||
|
|
@ -24,6 +24,12 @@ export default function LearnPage() {
|
|||
const [reportingCard, setReportingCard] = useState<any>(null)
|
||||
const [showComplete, setShowComplete] = useState(false)
|
||||
const [quizOptions, setQuizOptions] = useState<string[]>(['brought', 'determine', 'achieve', 'consider'])
|
||||
const [cardHeight, setCardHeight] = useState<number>(400)
|
||||
|
||||
// Refs for measuring card content heights
|
||||
const cardFrontRef = useRef<HTMLDivElement>(null)
|
||||
const cardBackRef = useRef<HTMLDivElement>(null)
|
||||
const cardContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Mock data with real example images
|
||||
const cards = [
|
||||
|
|
@ -70,6 +76,37 @@ export default function LearnPage() {
|
|||
|
||||
const currentCard = cards[currentCardIndex]
|
||||
|
||||
// Calculate optimal card height based on content (only when card changes)
|
||||
const calculateCardHeight = () => {
|
||||
if (!cardFrontRef.current || !cardBackRef.current) return 400;
|
||||
|
||||
// Get the scroll heights to measure actual content
|
||||
const frontHeight = cardFrontRef.current.scrollHeight;
|
||||
const backHeight = cardBackRef.current.scrollHeight;
|
||||
|
||||
console.log('Heights calculated:', { frontHeight, backHeight }); // Debug log
|
||||
|
||||
// Use the maximum height with padding
|
||||
const maxHeight = Math.max(frontHeight, backHeight);
|
||||
const paddedHeight = maxHeight + 40; // Add padding for visual spacing
|
||||
|
||||
// Ensure minimum height for visual consistency
|
||||
return Math.max(paddedHeight, 450);
|
||||
};
|
||||
|
||||
// Update card height only when card content changes (not on flip)
|
||||
useLayoutEffect(() => {
|
||||
if (mounted && cardFrontRef.current && cardBackRef.current) {
|
||||
// Wait for DOM to be fully rendered
|
||||
const timer = setTimeout(() => {
|
||||
const newHeight = calculateCardHeight();
|
||||
setCardHeight(newHeight);
|
||||
}, 50);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [currentCardIndex, mounted]);
|
||||
|
||||
// Client-side mounting and quiz options generation
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
|
|
@ -102,6 +139,7 @@ export default function LearnPage() {
|
|||
setShowResult(false)
|
||||
setFillAnswer('')
|
||||
setShowHint(false)
|
||||
// Height will be recalculated in useLayoutEffect
|
||||
} else {
|
||||
setShowComplete(true)
|
||||
}
|
||||
|
|
@ -115,6 +153,7 @@ export default function LearnPage() {
|
|||
setShowResult(false)
|
||||
setFillAnswer('')
|
||||
setShowHint(false)
|
||||
// Height will be recalculated in useLayoutEffect
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -305,95 +344,64 @@ export default function LearnPage() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className="card-container" onClick={handleFlip}>
|
||||
<div
|
||||
ref={cardContainerRef}
|
||||
className="card-container"
|
||||
onClick={handleFlip}
|
||||
style={{ height: `${cardHeight}px` }}
|
||||
>
|
||||
<div className={`card ${isFlipped ? 'flipped' : ''}`}>
|
||||
{/* Front */}
|
||||
<div className="card-front">
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 text-center cursor-pointer hover:shadow-xl transition-shadow">
|
||||
<div className="mb-4">
|
||||
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{currentCard.difficulty}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
<div
|
||||
ref={cardFrontRef}
|
||||
className="bg-white rounded-xl shadow-lg text-center cursor-pointer hover:shadow-xl transition-shadow"
|
||||
>
|
||||
<h2 className="text-4xl font-bold text-gray-900 mb-6">
|
||||
{currentCard.word}
|
||||
</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{currentCard.partOfSpeech} {currentCard.pronunciation}
|
||||
</p>
|
||||
<AudioPlayer text={currentCard.word} />
|
||||
<p className="text-sm text-gray-500 mt-4">
|
||||
點擊查看翻譯
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<span className="text-lg text-gray-500">
|
||||
{currentCard.pronunciation}
|
||||
</span>
|
||||
<AudioPlayer text={currentCard.word} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Back */}
|
||||
<div className="card-back">
|
||||
<div className="bg-white rounded-xl shadow-lg p-8 cursor-pointer hover:shadow-xl transition-shadow">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Left Column - Text Content */}
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<span className="inline-block bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
|
||||
{currentCard.difficulty}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-2">
|
||||
{currentCard.word}
|
||||
</h2>
|
||||
<p className="text-lg text-blue-600 font-semibold mb-4">
|
||||
{currentCard.translation}
|
||||
</p>
|
||||
<p className="text-gray-600 mb-4">
|
||||
{currentCard.partOfSpeech} {currentCard.pronunciation}
|
||||
</p>
|
||||
|
||||
<div className="mb-4">
|
||||
<AudioPlayer text={currentCard.word} />
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-2">定義</h3>
|
||||
<p className="text-gray-700">{currentCard.definition}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-2">例句</h3>
|
||||
<p className="text-gray-700 italic mb-1">"{currentCard.example}"</p>
|
||||
<p className="text-gray-600 text-sm">"{currentCard.exampleTranslation}"</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900 mb-2">同義詞</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{currentCard.synonyms.map((synonym, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-gray-100 text-gray-700 px-2 py-1 rounded text-sm"
|
||||
>
|
||||
{synonym}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={cardBackRef}
|
||||
className="bg-white rounded-xl shadow-lg cursor-pointer hover:shadow-xl transition-shadow"
|
||||
>
|
||||
{/* Content Sections */}
|
||||
<div className="space-y-4">
|
||||
{/* Definition */}
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-2 text-left">定義</h3>
|
||||
<p className="text-gray-700 text-left">{currentCard.definition}</p>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Image */}
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={currentCard.exampleImage}
|
||||
alt={`Example for ${currentCard.word}`}
|
||||
className="rounded-lg shadow-md max-w-full h-auto cursor-pointer hover:shadow-lg transition-shadow"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setModalImage(currentCard.exampleImage)
|
||||
}}
|
||||
/>
|
||||
<div className="absolute top-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
|
||||
點擊放大
|
||||
</div>
|
||||
{/* Example */}
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-2 text-left">例句</h3>
|
||||
<p className="text-gray-700 italic mb-2 text-left">"{currentCard.example}"</p>
|
||||
<p className="text-gray-600 text-sm text-left">"{currentCard.exampleTranslation}"</p>
|
||||
</div>
|
||||
|
||||
{/* Synonyms */}
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-2 text-left">同義詞</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{currentCard.synonyms.map((synonym, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="bg-white text-gray-700 px-3 py-1 rounded-full text-sm border border-gray-200"
|
||||
>
|
||||
{synonym}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -935,7 +943,9 @@ export default function LearnPage() {
|
|||
<style jsx>{`
|
||||
.card-container {
|
||||
perspective: 1000px;
|
||||
height: 400px;
|
||||
transition: height 0.3s ease;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card {
|
||||
|
|
@ -943,7 +953,7 @@ export default function LearnPage() {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
transition: transform 0.6s;
|
||||
transition: transform 0.6s ease;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
|
|
@ -957,18 +967,34 @@ export default function LearnPage() {
|
|||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.card-back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.card-front > div, .card-back > div {
|
||||
.card-front > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card-back > div {
|
||||
width: 100%;
|
||||
padding: 1.5rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue