获取 Demo
源代码请在公众号回复“react单词消消乐” 。


📖 项目简介
英语单词消消乐 是一款专为英语学习设计的互动式记忆游戏。通过经典的消消乐玩法,让用户在轻松愉快的游戏中掌握英语单词,提高词汇量和记忆效果。
🎯 项目目标
- 让英语学习变得有趣且高效
- 通过游戏化方式增强单词记忆
- 提供多样化的单词库选择
- 支持语音朗读功能,提升听说能力
✨ 核心功能特色
🎮 游戏玩法
- 经典消消乐模式:点击英语单词和对应的中文翻译来消除
- 智能匹配系统:自动识别英文-中文配对
- 实时反馈:匹配成功有消失动画,失败有抖动提示
- 进度追踪:实时显示匹配进度和得分
🗣️ 语音功能
- 双语朗读:支持英语和中文语音朗读
- 智能语音:根据卡片类型自动选择语言
- 可调节语速:英语稍慢便于学习,中文正常语速
- 语音开关:用户可自由开启/关闭语音功能
📚 单词库系统
- 多样化主题:水果、动物、颜色、数字等多个主题
- 易于扩展:模块化设计,可轻松添加新单词库
- 实时切换:游戏过程中可随时切换单词库
🎨 用户体验
- 响应式设计:完美适配手机、平板、电脑
- 精美动画:流畅的卡片动画和交互效果
- 直观界面:简洁美观的 UI 设计
- 即时反馈:清晰的操作提示和状态显示
🛠️ 技术栈
- React.js – 现代化的用户界面框架
- Tailwind CSS – 实用优先的 CSS 框架
- JavaScript ES6+ – 现代 JavaScript 语法
- Web Speech API – 浏览器原生语音合成
全部源码
import { useState, useEffect } from "react";
import { wordLibraries } from "../data/wordLibraries";
export default function WordsGame() {
const [currentLibrary, setCurrentLibrary] = useState("fruits");
const [gameWords, setGameWords] = useState([]);
const [selectedCards, setSelectedCards] = useState([]);
const [matchedPairs, setMatchedPairs] = useState([]);
const [score, setScore] = useState(0);
const [gameComplete, setGameComplete] = useState(false);
const [gameStarted, setGameStarted] = useState(false);
const [disappearingCards, setDisappearingCards] = useState([]);
const [shakingCards, setShakingCards] = useState([]);
const [speechSynthesis, setSpeechSynthesis] = useState(null);
const [speechEnabled, setSpeechEnabled] = useState(true);
// 初始化游戏
const initializeGame = () => {
const library = wordLibraries[currentLibrary];
const allWords = [...library.words];
// 随机选择10对单词(20张卡片)
const shuffled = allWords.sort(() => Math.random() - 0.5).slice(0, 10);
// 创建卡片数组,每对单词创建两张卡片
const cards = [];
shuffled.forEach((word, index) => {
cards.push({
id: `english-${index}`,
type: "english",
content: word.english,
pairId: index,
matched: false,
});
cards.push({
id: `chinese-${index}`,
type: "chinese",
content: word.chinese,
pairId: index,
matched: false,
});
});
// 随机打乱卡片顺序
const shuffledCards = cards.sort(() => Math.random() - 0.5);
setGameWords(shuffledCards);
setSelectedCards([]);
setMatchedPairs([]);
setScore(0);
setGameComplete(false);
setGameStarted(true);
};
// 处理卡片点击
const handleCardClick = (card) => {
if (card.matched || selectedCards.length >= 2) return;
// 朗读卡片内容
speakCardContent(card);
const newSelectedCards = [...selectedCards, card];
setSelectedCards(newSelectedCards);
if (newSelectedCards.length === 2) {
const [card1, card2] = newSelectedCards;
// 检查是否匹配
if (card1.pairId === card2.pairId && card1.type !== card2.type) {
// 匹配成功 - 添加温和的消失动画
setDisappearingCards([card1.id, card2.id]);
setTimeout(() => {
setMatchedPairs((prev) => [...prev, card1.pairId]);
setScore((prev) => prev + 10);
setSelectedCards([]);
setDisappearingCards([]);
// 检查游戏是否完成
if (matchedPairs.length + 1 === 10) {
setGameComplete(true);
}
}, 400);
} else {
// 匹配失败 - 添加抖动动画和标红效果
setShakingCards([card1.id, card2.id]);
setTimeout(() => {
setSelectedCards([]);
setShakingCards([]); // 动画结束后复位
}, 1000);
}
}
};
// 切换单词库
const changeLibrary = (libraryKey) => {
setCurrentLibrary(libraryKey);
setGameStarted(false);
};
// 检查卡片是否被选中
const isCardSelected = (card) => {
return selectedCards.some((selected) => selected.id === card.id);
};
// 检查卡片是否已匹配
const isCardMatched = (card) => {
return matchedPairs.includes(card.pairId);
};
// 检查卡片是否正在消失
const isCardDisappearing = (card) => {
return disappearingCards.includes(card.id);
};
// 检查卡片是否正在抖动
const isCardShaking = (card) => {
return shakingCards.includes(card.id);
};
// 朗读卡片内容
const speakCardContent = (card) => {
if (speechEnabled && speechSynthesis && !speechSynthesis.speaking) {
const utterance = new SpeechSynthesisUtterance(card.content);
// 根据卡片类型设置语言
if (card.type === "english") {
utterance.lang = "en-US";
utterance.rate = 0.8; // 稍微慢一点,便于学习
} else {
utterance.lang = "zh-CN";
utterance.rate = 0.9;
}
utterance.volume = 0.8;
utterance.pitch = 1.0;
speechSynthesis.speak(utterance);
}
};
// 测试语音功能
const testSpeech = () => {
if (speechSynthesis) {
const testText = "语音功能测试";
const utterance = new SpeechSynthesisUtterance(testText);
utterance.lang = "zh-CN";
utterance.rate = 0.9;
utterance.volume = 0.8;
speechSynthesis.speak(utterance);
}
};
useEffect(() => {
if (gameStarted) {
initializeGame();
}
}, [currentLibrary]);
// 初始化语音合成
useEffect(() => {
if ("speechSynthesis" in window) {
setSpeechSynthesis(window.speechSynthesis);
}
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 py-4 px-4">
<div className="max-w-6xl mx-auto">
{/* 标题和单词库选择 */}
<div className="flex flex-col lg:flex-row justify-between items-center mb-4 gap-4">
<div className="text-center flex-1">
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold text-indigo-700 mb-1 animate-bounce">
🎮 英语单词消消乐
</h1>
<p className="text-xs md:text-sm lg:text-base text-indigo-600 font-medium">
点击英语单词和对应的中文翻译来消除吧!
</p>
</div>
{/* 单词库选择和语音开关 */}
<div className="flex flex-col sm:flex-row items-center gap-3">
{/* 语音开关 */}
<div className="bg-gradient-to-r from-emerald-100 to-teal-100 rounded-xl shadow-md p-2 md:p-3 border-2 border-emerald-200">
<div className="flex items-center space-x-1 md:space-x-2">
<span className="text-xs md:text-sm font-bold text-emerald-700">
{speechEnabled ? "🔊" : "🔇"}
</span>
<button
onClick={() => setSpeechEnabled(!speechEnabled)}
className={`px-2 md:px-3 py-1 text-xs md:text-sm font-bold rounded-lg border-2 transition-all duration-200 cursor-pointer ${
speechEnabled
? "bg-emerald-500 text-white border-emerald-400"
: "bg-gray-300 text-gray-600 border-gray-400"
}`}
>
{speechEnabled ? "语音开" : "语音关"}
</button>
<button
onClick={testSpeech}
className="px-1 md:px-2 py-1 text-xs font-bold bg-blue-500 text-white rounded border-2 border-blue-400 transition-all duration-200 cursor-pointer hover:bg-blue-600"
>
测试
</button>
</div>
</div>
{/* 单词库选择 */}
<div className="bg-gradient-to-r from-indigo-100 to-purple-100 rounded-xl shadow-md p-2 md:p-3 border-2 border-indigo-200">
<div className="flex items-center">
<span className="text-xs md:text-sm font-bold text-indigo-700 mr-1 md:mr-2">
🌈
</span>
<select
value={currentLibrary}
onChange={(e) => changeLibrary(e.target.value)}
className="px-2 md:px-3 py-1 text-xs md:text-sm font-bold text-indigo-700 bg-white rounded-lg border-2 border-indigo-200 shadow-sm focus:outline-none focus:border-purple-400 transition-all duration-200 cursor-pointer"
>
{Object.entries(wordLibraries).map(([key, library]) => (
<option
key={key}
value={key}
className="text-xs md:text-sm font-bold"
>
{library.name}
</option>
))}
</select>
</div>
</div>
</div>
</div>
{/* 游戏状态 */}
<div className="bg-gradient-to-r from-emerald-100 to-teal-100 rounded-xl shadow-md p-3 mb-4 border-2 border-emerald-200">
<div className="flex justify-between items-center mb-3">
<div className="text-sm md:text-base font-bold text-emerald-700">
🎯 {wordLibraries[currentLibrary].name}
</div>
<div className="text-lg md:text-xl font-bold text-indigo-600">
⭐ {score}
</div>
</div>
{!gameStarted ? (
<button
onClick={initializeGame}
className="w-full bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 text-white font-bold py-2 px-4 rounded-xl text-base md:text-lg shadow-md transform hover:scale-105 transition-all duration-200 border-2 border-emerald-400"
>
🚀 开始游戏
</button>
) : (
<div className="text-center">
<div className="text-sm md:text-base text-emerald-600 mb-2 font-bold">
🎪 {matchedPairs.length} / 10
</div>
<button
onClick={initializeGame}
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-bold py-1 px-3 rounded-lg text-sm md:text-base shadow-md transform hover:scale-105 transition-all duration-200 border-2 border-indigo-400"
>
🔄 重新开始
</button>
</div>
)}
</div>
{/* 游戏完成提示 */}
{gameComplete && (
<div className="bg-gradient-to-r from-emerald-200 to-indigo-200 border-2 border-emerald-400 text-emerald-800 px-4 py-3 rounded-xl mb-4 text-center shadow-md animate-pulse">
<h3 className="text-xl md:text-2xl font-bold mb-2">
🎉 恭喜!游戏完成!🎉
</h3>
<p className="text-lg font-bold">最终得分: ⭐ {score} ⭐</p>
</div>
)}
{/* 游戏卡片区域 */}
{gameStarted && (
<div className="relative">
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-8 lg:grid-cols-10 gap-1 md:gap-2">
{gameWords.map((card) => (
<div
key={card.id}
onClick={() => !isCardMatched(card) && handleCardClick(card)}
className={`
h-16 sm:h-20 md:h-24 rounded-lg md:rounded-xl shadow-md transition-all duration-300 transform
${
isCardMatched(card)
? "opacity-0 pointer-events-none"
: "cursor-pointer hover:scale-105 hover:rotate-1"
}
${
isCardDisappearing(card)
? "animate-disappear pointer-events-none bg-gradient-to-br from-indigo-300 to-purple-300 border-2 border-indigo-500 shadow-lg"
: isCardShaking(card)
? "animate-shake pointer-events-none bg-gradient-to-br from-red-200 to-pink-200 border-2 border-red-400 shadow-md"
: isCardSelected(card)
? "bg-gradient-to-br from-indigo-200 to-purple-200 border-2 border-indigo-400 shadow-md"
: "bg-gradient-to-br from-slate-50 to-gray-100 border-2 border-slate-200 hover:border-indigo-400 hover:shadow-md"
}
`}
>
<div className="flex items-center justify-center h-full p-1 md:p-2 w-full">
<span
className={`
text-xs md:text-sm font-bold text-center word-card-text w-full
${
card.type === "english"
? "text-indigo-700"
: "text-emerald-700"
}
`}
>
{card.content}
</span>
</div>
</div>
))}
</div>
</div>
)}
{/* 游戏说明 */}
<div className="bg-gradient-to-r from-slate-100 to-gray-100 rounded-xl shadow-md p-3 mt-4 border-2 border-slate-200">
<h3 className="text-lg font-bold text-slate-700 mb-3 text-center">
📖 游戏规则
</h3>
<ul className="text-slate-700 space-y-2 text-sm font-medium">
<li className="flex items-center">
🎯 点击英语单词和对应的中文翻译来消除
</li>
<li className="flex items-center">👆 每次只能选择两张卡片</li>
<li className="flex items-center">
⭐ 匹配成功得10分,匹配失败不扣分
</li>
<li className="flex items-center">🏆 消除所有卡片即可完成游戏</li>
<li className="flex items-center">🔄 可以随时切换不同的单词库</li>
<li className="flex items-center">
🔊 点击卡片时会朗读内容,可开关语音功能
</li>
</ul>
</div>
</div>
</div>
);
}
原文链接:https://code.ifrontend.net/archives/729,转载请注明出处。
评论0