📖 项目简介
快乐记单词 是一款专为儿童设计的互动式英语拼写学习游戏。通过有趣的游戏化体验,让孩子们在轻松愉快的氛围中掌握英语单词的拼写,提高英语学习兴趣和效果。
✨ 核心特色
🎮 游戏化学习体验
- 互动拼写:通过点击字母按钮完成单词拼写
- 即时反馈:实时验证拼写正确性,给予鼓励性反馈
- 进度追踪:记录学习进度和正确率统计
- 成就系统:根据表现给予不同的鼓励语
🎨 精美视觉设计
- 可爱界面:采用粉色和蓝色为主的温馨配色
- 动画效果:字母按钮悬停动画、倒计时动画等
- 装饰元素:底部可爱的水果和动物表情装饰
- 响应式设计:完美适配手机、平板和电脑
🔊 多媒体功能
- 语音播放:点击中文释义可播放英文发音
- 发音标准:使用浏览器原生语音合成技术
- 交互反馈:音效和视觉反馈相结合
🎯 目标用户
- 学龄前儿童:3-6 岁英语启蒙阶段
- 小学低年级学生:1-3 年级英语学习
- 英语初学者:任何年龄的英语入门学习者
- 家长和老师:用于教学和辅导
📚 丰富的单词库
🍎 生活主题词汇
- 水果类:apple, banana, orange, grape, strawberry 等
- 动物类:cat, dog, elephant, tiger, lion 等
- 颜色类:red, blue, green, yellow, purple 等
- 数字类:one, two, three, four, five 等
👨👩👧👦 家庭和社交
- 家庭成员:father, mother, brother, sister 等
- 食物类:bread, rice, noodles, egg, milk 等
- 学校用品:book, pen, pencil, eraser, ruler 等
📖 系统化学习
- Unit 1-4:按教学单元组织的词汇
- 循序渐进:从简单到复杂的词汇安排
- 实用性强:贴近日常生活的常用词汇
🚀 技术特色
- React 18:最新的 React 框架
- Vite:快速的构建工具
- Tailwind CSS:现代化的样式框架
- 响应式设计:完美适配各种设备
- 流畅动画:60fps 的流畅交互体验
- 智能反馈:根据学习表现提供个性化鼓励
- 无障碍设计:支持键盘操作和屏幕阅读器
- 离线可用:纯前端应用,无需网络连接
🎮 游戏玩法
- 选择单词库:右上角下拉菜单选择学习主题
- 查看中文释义:点击中文释义可播放英文发音
- 拼写单词:点击字母按钮按顺序拼写单词
- 验证答案:点击”验证”按钮检查拼写是否正确
- 继续学习:点击”下一个”进入下一个单词
- 正确反馈:拼写正确时显示绿色对勾和鼓励语
- 错误提示:拼写错误时显示红色叉号,可重新尝试
- 进度统计:实时显示正确率和完成进度
- 成就奖励:完成一轮后根据表现给予不同等级的鼓励
📱 使用场景
- 亲子互动:家长陪伴孩子一起学习
- 自主学习:孩子独立使用,培养学习习惯
- 复习巩固:课后复习和巩固课堂所学
- 课堂辅助:老师用于课堂教学演示
- 课后作业:布置给学生的家庭作业
- 兴趣小组:英语兴趣小组的活动工具
📊 学习效果
- 词汇量提升:系统掌握常用英语词汇
- 拼写能力:提高英语单词拼写准确率
- 学习兴趣:培养英语学习兴趣和自信心
- 自主学习:培养独立学习能力
源码
import React, { useState } from "react";
import { wordLibraries } from "../data/wordLibraries";
// 洗牌算法
function shuffle(arr) {
// 洗牌算法
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
export default function CutWatermelonGame() {
const [bankKey, setBankKey] = useState(Object.keys(wordLibraries)[0]);
const [currentIdx, setCurrentIdx] = useState(0);
const [order, setOrder] = useState(
shuffle(
Array.from({ length: wordLibraries[bankKey].words.length }, (_, i) => i)
)
);
const [input, setInput] = useState([]);
const [feedback, setFeedback] = useState("");
const [shuffled, setShuffled] = useState([]);
const [countdown, setCountdown] = useState(0);
const [showCountdown, setShowCountdown] = useState(false);
const [showFinish, setShowFinish] = useState(false);
const [correctCount, setCorrectCount] = useState(0);
const [totalCount, setTotalCount] = useState(0);
const [hasAnswered, setHasAnswered] = useState(false);
const words = wordLibraries[bankKey].words;
const currentWord = words[order[currentIdx]];
// 初始化打乱字母和顺序(切换库时重洗顺序)
React.useEffect(() => {
setInput([]);
setFeedback("");
setShuffled(shuffle(currentWord.english.split("")));
}, [bankKey, currentIdx, order]);
// 切换库时重置顺序
React.useEffect(() => {
setOrder(shuffle(Array.from({ length: words.length }, (_, i) => i)));
setCurrentIdx(0);
setHasAnswered(false);
}, [bankKey]);
// 点击字母拼接
const handleLetterClick = (letter, idx) => {
setInput([...input, { letter, idx }]);
};
// 提交答案
const handleSubmit = () => {
if (hasAnswered) return;
const answer = input.map((i) => i.letter).join("");
if (answer === currentWord.english) {
setFeedback("✅ 恭喜你,拼写正确!");
setCountdown(3);
setShowCountdown(true);
setCorrectCount((c) => c + 1);
} else {
setFeedback("❌ 拼写不对,再试试!");
}
setTotalCount((t) => t + 1);
setHasAnswered(true);
};
// 下一题
const handleNext = () => {
if (!hasAnswered) {
setTotalCount((t) => t + 1);
setHasAnswered(true);
}
if (currentIdx + 1 < words.length) {
setCurrentIdx((prev) => prev + 1);
} else {
// 全部做完,弹窗鼓励
setShowFinish(true);
}
setInput([]);
setFeedback("");
setHasAnswered(false);
};
// 重置本库
const handleReset = () => {
setOrder(shuffle(Array.from({ length: words.length }, (_, i) => i)));
setCurrentIdx(0);
setInput([]);
setFeedback("");
setShowFinish(false);
setCorrectCount(0);
setTotalCount(0);
};
// 切换单词库
const handleBankChange = (e) => {
setBankKey(e.target.value);
setCurrentIdx(0);
setInput([]);
setFeedback("");
};
// 判断字母是否已被用
const usedIdxs = input.map((i) => i.idx);
// 倒计时副作用
React.useEffect(() => {
if (showCountdown && countdown > 0) {
const timer = setTimeout(() => {
setCountdown(countdown - 1);
}, 1000);
return () => clearTimeout(timer);
} else if (showCountdown && countdown === 0) {
setShowCountdown(false);
handleNext();
}
// eslint-disable-next-line
}, [showCountdown, countdown]);
// 鼓励语函数
function getFinishTitle() {
if (totalCount === 0) return "太棒了!本轮单词全部完成!";
const rate = correctCount / totalCount;
if (rate === 1) return "完美!全部答对!";
if (rate >= 0.8) return "很棒!继续努力!";
if (rate >= 0.5) return "不错哦!再接再厉!";
return "加油!下次会更好!";
}
function getFinishDesc() {
if (totalCount === 0) return "宝贝真棒,继续加油哦!";
const rate = correctCount / totalCount;
if (rate === 1) return "宝贝太厉害了,全部拼写正确!";
if (rate >= 0.8) return "宝贝很棒,拼写正确率很高!";
if (rate >= 0.5) return "有进步空间,继续练习会更棒!";
return "不要灰心,多练习就会进步!";
}
return (
<div className="min-h-screen from-blue-100 to-pink-100 flex flex-col items-center justify-center relative px-2">
{/* 右上角单词库切换 */}
<div className="absolute top-4 right-4 z-10">
<select
className="rounded-lg border-2 border-pink-300 bg-white px-3 py-1 text-lg font-bold text-pink-600 shadow focus:outline-none focus:ring-2 focus:ring-pink-400"
value={bankKey}
onChange={handleBankChange}
>
{Object.entries(wordLibraries).map(([key, lib]) => (
<option key={key} value={key}>
{lib.name || key}
</option>
))}
</select>
</div>
{/* 标题 */}
<h2 className="text-3xl md:text-4xl font-extrabold text-pink-500 drop-shadow mb-2 mt-2 select-none">
快乐记单词
</h2>
{/* 中文意思,点击发音 */}
<div
className="text-2xl md:text-3xl font-bold text-blue-700 bg-yellow-100 rounded-xl px-6 py-3 mb-4 shadow cursor-pointer hover:bg-yellow-200 transition select-none mt-10 flex items-center gap-3 justify-center"
title="点击播放英文发音"
onClick={() => {
if (window.speechSynthesis) {
const utter = new window.SpeechSynthesisUtterance(
currentWord.english
);
utter.lang = "en-US";
window.speechSynthesis.speak(utter);
}
}}
>
{/* 扬声器图标 */}
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-7 h-7 md:w-8 md:h-8 text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 9v6h4l5 5V4l-5 5H9z"
/>
</svg>
<span>“{currentWord.chinese}”</span>
</div>
{/* 拼写区 */}
<div className="flex justify-center min-h-[48px] mb-8">
{input.length > 0 && (
<span
className="bg-gradient-to-r from-pink-200 to-blue-200 text-pink-700 font-extrabold rounded-xl px-8 py-2 text-2xl shadow select-none tracking-wider cursor-pointer hover:bg-pink-300 transition-all"
title="点击可清空"
onClick={() => setInput([])}
>
{input.map((i) => i.letter).join("")}
</span>
)}
</div>
{/* 字母按钮区 */}
<div className="flex flex-wrap justify-center gap-4 mb-10">
{shuffled.map((letter, idx) => (
<button
key={idx}
onClick={() =>
!usedIdxs.includes(idx) && handleLetterClick(letter, idx)
}
disabled={usedIdxs.includes(idx)}
className={`rounded-full px-5 py-3 text-2xl font-extrabold shadow-lg border-2 border-blue-200 transition-all duration-150 select-none
${
usedIdxs.includes(idx)
? "bg-gray-200 text-gray-400 border-gray-200 cursor-not-allowed"
: "bg-blue-200 text-blue-700 hover:bg-blue-300 hover:scale-110 cursor-pointer"
}
`}
>
{letter}
</button>
))}
</div>
{/* 操作按钮区 */}
<div className="flex gap-8 mb-8">
<button
onClick={handleSubmit}
className="bg-green-400 hover:bg-green-500 text-white font-bold py-2 px-8 rounded-xl text-lg shadow transition-all disabled:bg-gray-300 disabled:cursor-not-allowed"
disabled={input.length !== currentWord.english.length}
>
验证
</button>
<button
onClick={handleNext}
className="bg-yellow-400 hover:bg-yellow-500 text-white font-bold py-2 px-8 rounded-xl text-lg shadow transition-all"
>
下一个
</button>
</div>
{/* 反馈信息 */}
<div className="min-h-[32px] text-xl font-bold text-center">
{feedback && (
<span
className={
feedback.includes("恭喜")
? "text-green-600 animate-sparkle"
: "text-red-500 animate-shake"
}
>
{feedback}
</span>
)}
</div>
{/* 底部可爱装饰 */}
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 flex gap-2 opacity-70 pointer-events-none select-none">
<span className="text-3xl animate-float">🍓</span>
<span className="text-3xl animate-float">🍉</span>
<span className="text-3xl animate-float">🍌</span>
<span className="text-3xl animate-float">🦁</span>
<span className="text-3xl animate-float">🐯</span>
<span className="text-3xl animate-float">🐘</span>
</div>
{/* 倒计时弹窗 */}
{showCountdown && (
<div className="fixed inset-0 flex items-center justify-center z-50">
<div className="relative flex items-center justify-center">
{/* 转圈动画圆 */}
<svg
className="absolute w-40 h-40 animate-spin-slow"
viewBox="0 0 160 160"
>
<circle
cx="80"
cy="80"
r="70"
fill="none"
stroke="#2226"
strokeWidth="18"
strokeDasharray="330"
strokeDashoffset="80"
strokeLinecap="round"
/>
</svg>
{/* 半透明黑色圆背景 */}
<div className="w-36 h-36 rounded-full bg-black bg-opacity-70 flex items-center justify-center">
<span
className="text-white text-7xl font-extrabold select-none"
style={{ fontVariantNumeric: "tabular-nums" }}
>
{countdown}
</span>
</div>
</div>
</div>
)}
{/* 结束弹窗 */}
{showFinish && (
<div className="fixed inset-0 flex items-center justify-center z-50 bg-black bg-opacity-40">
<div className="bg-white rounded-2xl shadow-2xl px-10 py-10 flex flex-col items-center">
<div className="text-6xl mb-4">👏🎉</div>
<div className="text-2xl font-bold text-pink-500 mb-2">
{getFinishTitle()}
</div>
<div className="text-lg text-blue-600 mb-2">
本轮正确率:
{totalCount > 0
? Math.round((correctCount / totalCount) * 100)
: 100}%
</div>
<div className="text-lg text-blue-600 mb-6">{getFinishDesc()}</div>
<button
onClick={handleReset}
className="bg-pink-400 hover:bg-pink-500 text-white font-bold py-2 px-8 rounded-xl text-lg shadow transition-all"
>
再来一轮
</button>
</div>
</div>
)}
</div>
);
}