创建一个简单的记忆翻牌游戏,包含以下功能:
- 6对卡片随机排列
- 点击翻牌效果和匹配音效
- 键盘操作支持(Tab聚焦/Enter选择)
- 屏幕阅读器友好提示
- 胜利动画和音效
XML
import { useState, useEffect, useCallback } from 'react';
import { Volume2, VolumeX, Trophy } from 'lucide-react';
const MemoryGame = () => {
const [cards, setCards] = useState([]);
const [flipped, setFlipped] = useState([]);
const [matched, setMatched] = useState([]);
const [moves, setMoves] = useState(0);
const [isSoundOn, setIsSoundOn] = useState(true);
const [gameWon, setGameWon] = useState(false);
// 初始化卡片数据
const initGame = useCallback(() => {
const emojis = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊'];
const cardPairs = [...emojis, ...emojis];
const shuffled = cardPairs
.sort(() => Math.random() - 0.5)
.map((emoji, index) => ({ id: index, emoji, flipped: false }));
setCards(shuffled);
setFlipped([]);
setMatched([]);
setMoves(0);
setGameWon(false);
}, []);
useEffect(() => {
initGame();
}, [initGame]);
// 播放音效
const playSound = (soundType) => {
if (!isSoundOn) return;
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
if (soundType === 'flip') {
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(440, audioContext.currentTime);
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
} else if (soundType === 'match') {
oscillator.type = 'triangle';
oscillator.frequency.setValueAtTime(660, audioContext.currentTime);
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
} else if (soundType === 'win') {
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(880, audioContext.currentTime);
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 1);
}
oscillator.start();
oscillator.stop(audioContext.currentTime + (soundType === 'win' ? 1 : 0.5));
};
// 处理卡片点击
const handleCardClick = (id) => {
if (flipped.length >= 2 || flipped.includes(id) || matched.includes(id) || gameWon) {
return;
}
const newFlipped = [...flipped, id];
setFlipped(newFlipped);
playSound('flip');
if (newFlipped.length === 2) {
setMoves(moves + 1);
const [firstId, secondId] = newFlipped;
const firstCard = cards.find(card => card.id === firstId);
const secondCard = cards.find(card => card.id === secondId);
if (firstCard.emoji === secondCard.emoji) {
setMatched([...matched, firstId, secondId]);
playSound('match');
// 检查是否胜利
if (matched.length + 2 === cards.length) {
setTimeout(() => {
setGameWon(true);
playSound('win');
}, 500);
}
} else {
setTimeout(() => {
setFlipped([]);
}, 1000);
}
}
};
// 键盘导航
useEffect(() => {
const handleKeyDown = (e) => {
if (gameWon) return;
const focusableCards = document.querySelectorAll('.card:not([aria-disabled="true"])');
const currentFocus = document.activeElement;
const currentIndex = Array.from(focusableCards).indexOf(currentFocus);
if (e.key === 'Tab') {
// 默认Tab行为已由浏览器处理
} else if (e.key === 'Enter' && currentFocus.classList.contains('card')) {
const id = parseInt(currentFocus.getAttribute('data-id'));
handleCardClick(id);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
if (currentIndex < focusableCards.length - 1) {
focusableCards[currentIndex + 1].focus();
} else {
focusableCards[0].focus();
}
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
if (currentIndex > 0) {
focusableCards[currentIndex - 1].focus();
} else {
focusableCards[focusableCards.length - 1].focus();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [flipped, matched, gameWon, cards]);
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-100 to-purple-100 flex flex-col items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-xl p-8 w-full max-w-2xl">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-indigo-800">记忆翻牌游戏</h1>
<div className="flex items-center gap-4">
<div className="bg-indigo-100 px-4 py-2 rounded-full flex items-center gap-2">
<span className="font-medium">步数:</span>
<span className="font-bold">{moves}</span>
</div>
<button
onClick={() => setIsSoundOn(!isSoundOn)}
className={`p-2 rounded-full ${isSoundOn ? 'bg-green-100 text-green-600' : 'bg-red-100 text-red-600'}`}
aria-label={isSoundOn ? '关闭声音' : '开启声音'}
>
{isSoundOn ? <Volume2 size={20} /> : <VolumeX size={20} />}
</button>
</div>
</div>
{gameWon && (
<div className="mb-6 p-4 bg-gradient-to-r from-green-100 to-emerald-100 rounded-xl border border-green-200 flex items-center justify-center gap-2 animate-bounce">
<Trophy className="text-yellow-500" size={24} />
<span className="text-xl font-bold text-green-800">恭喜获胜!</span>
</div>
)}
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
{cards.map((card) => {
const isFlipped = flipped.includes(card.id) || matched.includes(card.id);
const isDisabled = matched.includes(card.id) || gameWon;
return (
<button
key={card.id}
data-id={card.id}
onClick={() => handleCardClick(card.id)}
disabled={isDisabled}
className={`card relative aspect-square rounded-xl transition-all duration-300 transform ${
isFlipped
? 'bg-white rotate-y-180 scale-105 shadow-md'
: 'bg-gradient-to-br from-indigo-500 to-purple-600 rotate-y-0 hover:scale-105'
} ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
tabIndex={isDisabled ? -1 : 0}
aria-disabled={isDisabled}
aria-label={isFlipped ? `已翻开的卡片,图案是${card.emoji}` : '未翻开的卡片'}
>
<div className={`absolute inset-0 flex items-center justify-center transition-transform duration-500 transform-style-preserve-3d ${
isFlipped ? 'rotate-y-180' : ''
}`}>
<div className="back-face absolute inset-0 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center">
<span className="text-white text-4xl">?</span>
</div>
<div className="front-face absolute inset-0 bg-white rounded-xl flex items-center justify-center rotate-y-180">
<span className="text-4xl">{card.emoji}</span>
</div>
</div>
</button>
);
})}
</div>
<div className="mt-8 text-center">
<button
onClick={initGame}
className="px-6 py-3 bg-gradient-to-r from-indigo-500 to-purple-600 text-white rounded-full font-medium hover:from-indigo-600 hover:to-purple-700 transition-all shadow-md hover:shadow-lg transform hover:-translate-y-0.5"
>
重新开始
</button>
<div className="mt-4 text-sm text-gray-600">
<p className="mb-1">操作说明:</p>
<ul className="list-disc list-inside text-left">
<li>使用鼠标点击或键盘方向键导航</li>
<li>按Enter键翻开卡片</li>
<li>找到所有匹配的卡片对</li>
</ul>
</div>
</div>
</div>
<style jsx>{`
.transform-style-preserve-3d {
transform-style: preserve-3d;
}
.rotate-y-0 {
transform: rotateY(0deg);
}
.rotate-y-180 {
transform: rotateY(180deg);
}
.card:focus-visible {
outline: 3px solid #4f46e5;
outline-offset: 2px;
}
`}</style>
</div>
);
};
export default MemoryGame;