Complete Course Design Project

This commit is contained in:
Student 2026-01-08 21:14:46 +08:00
commit 7a1f715889
19 changed files with 1545 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.env
.venv/
__pycache__/
.DS_Store

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.13

47
README.md Normal file
View File

@ -0,0 +1,47 @@
# 英语学习日常场景对话练习系统
## 2.1 团队成员与贡献 (必填)
| 姓名 | 学号 | 主要贡献 (具体分工) |
|------|------|-------------------|
| 张扬 | 2411020205 | 核心逻辑开发、Prompt 编写 |
| 刘诣卓 | 2411020209 | 创意提供、网页测试 |
| 顾浩晨 | 2411020108 | 文档撰写、测试与 Bug 修复 |
## 2.2 项目简介 & 运行指南
### 简介
本项目是一个基于网页的英语学习系统,通过模拟真实日常场景对话,提供交互式的选择题练习,帮助学习者提升英语口语表达能力和场景应用能力。
### 如何运行
1. 安装依赖:本项目使用 Python 的 http.server 模块,无需额外安装依赖
2. 进入项目目录:`cd english-learning-uv`
3. 启动服务器:`python -m http.server 8000`
4. 访问网页:在浏览器中输入 `http://localhost:8000`
## 2.3 开发心得
### 选题思考
在英语学习过程中,很多学习者面临着"学了不会用"的痛点。传统的英语学习往往注重词汇和语法的记忆,而缺乏真实场景的应用练习。我们的项目旨在解决这一问题,通过模拟餐厅点餐、超市购物、问路等真实日常场景,让学习者在交互式对话中练习英语,提高实际应用能力。
### AI 协作体验
作为第一次尝试使用 AI 辅助开发的团队,我们的体验可以说是充满惊喜和挑战。
最初使用 AI 写代码时感觉就像是拥有了一个经验丰富的编程助手。只需要描述我们的需求AI 就能快速生成基础代码框架大大提高了开发效率。特别是在处理一些重复性工作时比如创建多个相似的场景数据AI 能够快速生成模板并根据需求进行调整。
让我们印象最深刻的"牛逼"Prompt 是:"帮我创建一个英语学习网页,包含日常场景对话和选择题练习功能,使用 uv 进行包管理"。AI 不仅生成了完整的 HTML、CSS 和 JavaScript 代码,还考虑了用户体验和代码结构的合理性,为我们的项目提供了良好的起点。
当然,也有一些让我们感到挫败的时刻。比如在处理图片加载逻辑时,我们的 Prompt 描述不够精确,导致 AI 生成的代码虽然能运行,但存在性能问题。还有一次,我们要求 AI 实现选项随机排序功能,结果生成的代码打乱了选项但没有正确跟踪正确答案的位置,导致答题逻辑出错。这些经历让我们认识到,与 AI 协作需要清晰、精确的指令,并且需要对生成的代码进行仔细的检查和测试。
### 自我反思
在使用 AI 辅助开发的过程中我们深刻地思考了一个问题AI 时代,程序员的核心竞争力到底是什么?
通过这次项目我们认识到AI 确实能够快速生成基础代码,处理一些常规的编程任务。但它缺乏对项目整体架构的理解,无法把握业务逻辑的本质,也不能创造性地解决复杂问题。作为程序员,我们的核心竞争力应该在于:
1. **问题分析能力**:能够深入理解用户需求,将实际问题转化为清晰的技术方案。
2. **系统设计能力**:能够设计合理的系统架构,考虑代码的可维护性、可扩展性和性能。
3. **创造性思维**:能够创造性地解决复杂问题,提出创新的解决方案。
4. **代码质量意识**:能够编写高质量、可读性强、易于维护的代码。
5. **持续学习能力**:能够不断学习新技术,适应快速变化的技术环境。
AI 是一个强大的工具,但它不能替代程序员的核心能力。相反,它让我们能够从繁琐的重复性工作中解放出来,将更多的精力放在创造性的思考和复杂问题的解决上。在未来的开发工作中,我们应该学会与 AI 协作,充分发挥它的优势,同时不断提升自己的核心竞争力。
通过这次项目,我们不仅完成了一个功能完整的英语学习系统,更重要的是获得了宝贵的 AI 协作经验和对程序员职业发展的深刻思考。这些收获将对我们未来的学习和工作产生积极的影响。

690
data.js Normal file
View File

@ -0,0 +1,690 @@
// 日常英语学习数据
const englishData = {
scenes: [
{
id: 0,
title: "餐厅点餐",
description: "在餐厅点餐的日常对话练习",
image: "", // 更简单的图片导入:
// 方式1将图片放在 images 目录下命名为场景ID+扩展名(推荐)
// 例如0.jpg, 1.png, 2.jpeg 等
// 方式2将图片放在 images 目录下,命名为场景标题拼音+扩展名
// 例如:餐厅点餐 -> cantingdiancan.jpg
// 支持的格式:.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg
// 不需要手动修改此字段,系统会自动查找所有可能的图片
conversation: [
{
speaker: "服务员",
text: "Good afternoon! Welcome to our restaurant. May I take your order?",
type: "other"
},
{
speaker: "我",
text: "I'd like to see the menu first, please.",
type: "user"
},
{
speaker: "服务员",
text: "Sure, here you are. Take your time.",
type: "other"
},
{
speaker: "服务员",
text: "Are you ready to order now?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Yes, I'll have the steak, please.",
"No, I don't like this restaurant.",
"I want to go home.",
"The menu is too expensive."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "服务员",
text: "How would you like your steak cooked?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Medium rare, please.",
"With fries and salad.",
"I want a drink.",
"That's all."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "服务员",
text: "Would you like anything to drink?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"A glass of water, please.",
"No, I'm not hungry.",
"I'll pay now.",
"Thank you."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "服务员",
text: "Okay, your order will be ready in 15 minutes.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Thank you very much.",
"Hurry up!",
"I want it now.",
"That's too long."
],
correctAnswer: 0,
type: "user_question"
}
]
},
{
id: 1,
title: "超市购物",
description: "在超市购物的日常对话练习",
image: "", // 更简单的图片导入:
// 方式1将图片放在 images 目录下命名为场景ID+扩展名(推荐)
// 例如0.jpg, 1.png, 2.jpeg 等
// 方式2将图片放在 images 目录下,命名为场景标题拼音+扩展名
// 例如:餐厅点餐 -> cantingdiancan.jpg
// 支持的格式:.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg
// 不需要手动修改此字段,系统会自动查找所有可能的图片
conversation: [
{
speaker: "顾客",
text: "Excuse me, where can I find the milk?",
type: "user"
},
{
speaker: "超市员工",
text: "It's in aisle 5, next to the bread.",
type: "other"
},
{
speaker: "顾客",
question: "请选择合适的回答:",
options: [
"Thank you very much.",
"I don't like milk.",
"Where is aisle 5?",
"This store is too big."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "超市员工",
text: "You're welcome. Let me know if you need anything else.",
type: "other"
},
{
speaker: "顾客",
text: "Actually, do you have any organic vegetables?",
type: "user"
},
{
speaker: "超市员工",
text: "Yes, they're in the fresh produce section at the back of the store.",
type: "other"
},
{
speaker: "顾客",
question: "请选择合适的回答:",
options: [
"Great, thank you.",
"I don't want organic.",
"That's too far.",
"Why are they there?"
],
correctAnswer: 0,
type: "user_question"
}
]
},
{
id: 2,
title: "问路",
description: "向陌生人问路的日常对话练习",
image: "", // 更简单的图片导入:
// 方式1将图片放在 images 目录下命名为场景ID+扩展名(推荐)
// 例如0.jpg, 1.png, 2.jpeg 等
// 方式2将图片放在 images 目录下,命名为场景标题拼音+扩展名
// 例如:餐厅点餐 -> cantingdiancan.jpg
// 支持的格式:.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg
// 不需要手动修改此字段,系统会自动查找所有可能的图片
conversation: [
{
speaker: "我",
text: "Excuse me, could you tell me how to get to the nearest subway station?",
type: "user"
},
{
speaker: "路人",
text: "Sure! Go straight for two blocks, then turn left at the traffic light. You'll see it on your right.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Is it far from here?",
"I don't want to go there.",
"That's too complicated.",
"I'll take a taxi instead."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "路人",
text: "No, it's only about a 5-minute walk.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Perfect! Thank you so much.",
"5 minutes is too long.",
"I can't walk that far.",
"Do you have a map?"
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "路人",
text: "You're welcome! Have a nice day.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"You too!",
"Bye, I'm in a hurry.",
"I hope I don't get lost.",
"This city is confusing."
],
correctAnswer: 0,
type: "user_question"
}
]
},
{
id: 3,
title: "医院就诊",
description: "在医院看病的日常对话练习",
image: "", // 更简单的图片导入:
// 方式1将图片放在 images 目录下命名为场景ID+扩展名(推荐)
// 例如0.jpg, 1.png, 2.jpeg 等
// 方式2将图片放在 images 目录下,命名为场景标题拼音+扩展名
// 例如:餐厅点餐 -> cantingdiancan.jpg
// 支持的格式:.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg
// 不需要手动修改此字段,系统会自动查找所有可能的图片
conversation: [
{
speaker: "护士",
text: "Good morning, how can I help you today?",
type: "other"
},
{
speaker: "我",
text: "I'm not feeling well. I have a fever and a headache.",
type: "user"
},
{
speaker: "护士",
text: "I'm sorry to hear that. Please fill out this form and take a seat. The doctor will see you shortly.",
type: "other"
},
{
speaker: "医生",
text: "Hello, I'm Dr. Smith. What seems to be the problem?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"I've had a fever and headache for two days.",
"I don't like doctors.",
"How much does this cost?",
"Can I leave now?"
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "医生",
text: "Let me check your temperature and blood pressure.",
type: "other"
},
{
speaker: "医生",
text: "You have a slight fever. I'll prescribe some medicine for you.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"How often should I take it?",
"I don't want any medicine.",
"Is it expensive?",
"When will I get better?"
],
correctAnswer: 0,
type: "user_question"
}
]
},
{
id: 4,
title: "银行办理业务",
description: "在银行办理业务的日常对话练习",
image: "", // 更简单的图片导入:
// 方式1将图片放在 images 目录下命名为场景ID+扩展名(推荐)
// 例如0.jpg, 1.png, 2.jpeg 等
// 方式2将图片放在 images 目录下,命名为场景标题拼音+扩展名
// 例如:餐厅点餐 -> cantingdiancan.jpg
// 支持的格式:.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg
// 不需要手动修改此字段,系统会自动查找所有可能的图片
conversation: [
{
speaker: "银行职员",
text: "Good morning! How can I assist you today?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"I'd like to open a savings account.",
"I want to rob the bank.",
"Your bank is too slow.",
"I need a loan immediately."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "银行职员",
text: "Certainly! Could you please provide your ID and proof of address?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Here you are.",
"I don't have any ID.",
"Why do you need that?",
"This is too麻烦."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "银行职员",
text: "Thank you. Please fill out this form and sign here.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Okay, thank you.",
"I can't read this.",
"This is taking too long.",
"I changed my mind."
],
correctAnswer: 0,
type: "user_question"
}
]
},
{
id: 5,
title: "酒店入住",
description: "酒店入住登记的日常对话练习",
image: "", // 更简单的图片导入:
// 方式1将图片放在 images 目录下命名为场景ID+扩展名(推荐)
// 例如0.jpg, 1.png, 2.jpeg 等
// 方式2将图片放在 images 目录下,命名为场景标题拼音+扩展名
// 例如:餐厅点餐 -> cantingdiancan.jpg
// 支持的格式:.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg
// 不需要手动修改此字段,系统会自动查找所有可能的图片
conversation: [
{
speaker: "前台接待",
text: "Good evening! Welcome to our hotel. Do you have a reservation?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Yes, I have a reservation under the name Zhang.",
"No, I want the best room.",
"How much is a room?",
"I'm here to visit a friend."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "前台接待",
text: "Let me check... Yes, Mr. Zhang. You booked a single room for two nights.",
type: "other"
},
{
speaker: "前台接待",
text: "Could you please show me your ID and credit card?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Here they are.",
"I don't have a credit card.",
"Why do you need that?",
"Can I pay cash?"
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "前台接待",
text: "Thank you. Here's your room key. You're in room 302 on the third floor.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Thank you! Where is the elevator?",
"The room is too small.",
"I want a better view.",
"Can I check out late?"
],
correctAnswer: 0,
type: "user_question"
}
]
},
{
id: 6,
title: "电话预约",
description: "用电话预约的日常对话练习",
image: "", // 更简单的图片导入:
// 方式1将图片放在 images 目录下命名为场景ID+扩展名(推荐)
// 例如0.jpg, 1.png, 2.jpeg 等
// 方式2将图片放在 images 目录下,命名为场景标题拼音+扩展名
// 例如:餐厅点餐 -> cantingdiancan.jpg
// 支持的格式:.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg
// 不需要手动修改此字段,系统会自动查找所有可能的图片
conversation: [
{
speaker: "接听者",
text: "Hello, this is the dental clinic. How can I help you?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"I'd like to make an appointment for a check-up.",
"I have a toothache.",
"Can I speak to Dr. Wang?",
"What are your opening hours?"
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "接听者",
text: "Certainly! When would you like to come in?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Is tomorrow morning available?",
"I want to come now.",
"How much does it cost?",
"I'm scared of dentists."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "接听者",
text: "Yes, we have openings at 9:00 and 10:30. Which one works better for you?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"9:00 would be perfect.",
"Both times are bad.",
"Can I come at noon?",
"I'll call back later."
],
correctAnswer: 0,
type: "user_question"
}
]
},
{
id: 7,
title: "机场登机",
description: "机场办理登机手续的日常对话练习",
image: "", // 更简单的图片导入:
// 方式1将图片放在 images 目录下命名为场景ID+扩展名(推荐)
// 例如0.jpg, 1.png, 2.jpeg 等
// 方式2将图片放在 images 目录下,命名为场景标题拼音+扩展名
// 例如:餐厅点餐 -> cantingdiancan.jpg
// 支持的格式:.jpg, .jpeg, .png, .gif, .webp, .bmp, .svg
// 不需要手动修改此字段,系统会自动查找所有可能的图片
conversation: [
{
speaker: "工作人员",
text: "Good morning! May I see your passport and ticket, please?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Here they are.",
"I lost my ticket.",
"When is boarding time?",
"Where is my gate?"
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "工作人员",
text: "Thank you. Would you like a window seat or an aisle seat?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"A window seat, please.",
"I want the best seat.",
"How much extra?",
"I don't care."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "工作人员",
text: "Here's your boarding pass. Your gate is B12, and boarding starts at 10:30.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Thank you very much.",
"Where is gate B12?",
"Is the flight on time?",
"Can I bring food on board?"
],
correctAnswer: 0,
type: "user_question"
}
]
},
{
id: 8,
title: "公交车让座",
description: "在公交车上让座的日常对话练习",
image: "", // 简单图片导入:将图片放在 images 目录下,可使用以下命名规则之一
// 1. 按场景ID命名8.jpg, 8.png
// 2. 按场景标题命名:公交车让座.jpg, bus.jpg
// 不需要手动修改此字段,系统会自动查找
conversation: [
{
speaker: "老人",
text: "Excuse me, is this seat taken?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"No, please sit here.",
"Yes, it's taken.",
"I'm tired too.",
"Find another seat."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "老人",
text: "Thank you so much, young man/lady.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"You're welcome. It's my pleasure.",
"It's okay.",
"Don't mention it.",
"Sit down quickly."
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "老人",
text: "You're such a kind person.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Thank you for your kind words.",
"It's nothing special.",
"I have to get off soon.",
"I do this every day."
],
correctAnswer: 0,
type: "user_question"
}
]
},
{
id: 9,
title: "图书馆借书",
description: "在图书馆借书的日常对话练习",
image: "", // 简单图片导入:将图片放在 images 目录下,可使用以下命名规则之一
// 1. 按场景ID命名9.jpg, 9.png
// 2. 按场景标题命名:图书馆借书.jpg, library.jpg
// 不需要手动修改此字段,系统会自动查找
conversation: [
{
speaker: "我",
text: "Excuse me, how can I borrow books from the library?",
type: "user"
},
{
speaker: "图书馆职员",
text: "You need a library card. Do you have one?",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"No, how can I get one?",
"I forgot it at home.",
"Can I use my ID instead?",
"Why do I need one?"
],
correctAnswer: 0,
type: "user_question"
},
{
speaker: "图书馆职员",
text: "You can apply for one at the information desk with your ID.",
type: "other"
},
{
speaker: "我",
text: "Great, thank you. And how many books can I borrow at a time?",
type: "user"
},
{
speaker: "图书馆职员",
text: "You can borrow up to 5 books for 3 weeks.",
type: "other"
},
{
speaker: "我",
question: "请选择合适的回答:",
options: [
"Perfect, that's enough for me.",
"That's too few.",
"3 weeks is too short.",
"Can I renew them online?"
],
correctAnswer: 0,
type: "user_question"
}
]
}
]
};

BIN
images/0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

BIN
images/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
images/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
images/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
images/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
images/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
images/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

BIN
images/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

BIN
images/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

67
index.html Normal file
View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>日常英语学习 - 场景对话练习</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header class="header">
<h1>日常英语学习</h1>
<p class="subtitle">场景对话练习</p>
</header>
<div class="status-bar">
<div class="score">
<span id="score">得分: 0</span>
</div>
<div class="progress">
<span id="progress">进度: 1/10</span>
</div>
</div>
<div class="content">
<div class="scene-info">
<h2 id="scene-title">餐厅点餐</h2>
<p id="scene-description">在餐厅点餐的日常对话练习</p>
<div class="scene-image-container">
<img id="scene-image" src="" alt="场景插图" />
</div>
</div>
<div class="conversation-area">
<div id="conversation" class="conversation">
<!-- 对话内容将通过JavaScript动态添加 -->
</div>
</div>
<div id="question-area" class="question-area" style="display: none;">
<div class="question">
<h3 id="question-text">请选择合适的回答:</h3>
<div class="options">
<button class="option" data-index="0">选项1</button>
<button class="option" data-index="1">选项2</button>
<button class="option" data-index="2">选项3</button>
<button class="option" data-index="3">选项4</button>
</div>
</div>
</div>
<div id="feedback" class="feedback" style="display: none;">
<p id="feedback-text">反馈信息</p>
</div>
<div class="controls">
<button id="start-button" class="btn btn-primary">开始学习</button>
<button id="reset-button" class="btn btn-secondary">重新开始</button>
<button id="next-scene-button" class="btn btn-success" disabled>下一个场景</button>
</div>
</div>
</div>
<script src="data.js"></script>
<script src="script.js"></script>
</body>
</html>

6
main.py Normal file
View File

@ -0,0 +1,6 @@
def main():
print("Hello from english-learning-uv!")
if __name__ == "__main__":
main()

7
pyproject.toml Normal file
View File

@ -0,0 +1,7 @@
[project]
name = "english-learning-uv"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = []

399
script.js Normal file
View File

@ -0,0 +1,399 @@
// 英语学习网页主脚本
// 游戏状态
let gameState = {
currentSceneIndex: 0,
currentDialogIndex: 0,
score: 0,
totalQuestions: 0,
correctAnswers: 0
};
// DOM元素
let elements;
// 在DOM加载完成后获取DOM元素
function getDomElements() {
elements = {
scoreElement: document.getElementById('score'),
progressElement: document.getElementById('progress'),
sceneTitle: document.getElementById('scene-title'),
sceneDescription: document.getElementById('scene-description'),
sceneImage: document.getElementById('scene-image'),
conversation: document.getElementById('conversation'),
questionArea: document.getElementById('question-area'),
questionText: document.getElementById('question-text'),
options: document.querySelectorAll('.option'),
feedback: document.getElementById('feedback'),
feedbackText: document.getElementById('feedback-text'),
startButton: document.getElementById('start-button'),
resetButton: document.getElementById('reset-button'),
nextSceneButton: document.getElementById('next-scene-button')
};
}
// 初始化游戏
function initGame() {
resetGameState();
loadScene(gameState.currentSceneIndex);
setupEventListeners();
updateProgress();
}
// 重置游戏状态
function resetGameState() {
gameState = {
currentSceneIndex: 0,
currentDialogIndex: 0,
score: 0,
totalQuestions: 0,
correctAnswers: 0
};
}
// 加载场景
function loadScene(sceneIndex) {
if (sceneIndex >= englishData.scenes.length) {
showGameComplete();
return;
}
const scene = englishData.scenes[sceneIndex];
// 更新场景信息
elements.sceneTitle.textContent = scene.title;
elements.sceneDescription.textContent = scene.description;
// 加载场景图片
// 简化的图片加载逻辑优先使用场景ID作为图片名称
let imagePath = scene.image;
// 确保图片元素存在
if (!elements.sceneImage) {
console.error('场景图片元素不存在');
return;
}
// 重置图片状态
elements.sceneImage.src = '';
elements.sceneImage.alt = scene.title + '场景插图';
elements.sceneImage.style.display = 'none';
// 如果没有指定图片路径则使用场景ID作为图片名称自动查找
if (!imagePath) {
// 简单的命名规则images/[场景ID].[扩展名] 或 images/[场景标题拼音].[扩展名]
const possibleExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
const baseNameId = scene.id.toString();
// 将场景标题转换为拼音风格的文件名(去掉空格和特殊字符,转为小写)
const baseNameTitle = scene.title
.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '') // 保留中英文数字
.toLowerCase();
// 所有可能的图片路径优先ID命名然后标题拼音命名
const allPossiblePaths = [
// 优先使用ID命名
...possibleExtensions.map(ext => `images/${baseNameId}${ext}`),
// 也支持标题拼音命名
...possibleExtensions.map(ext => `images/${baseNameTitle}${ext}`)
];
console.log('尝试查找图片路径:', allPossiblePaths);
// 递归检查图片是否存在
function checkImage(index) {
if (index >= allPossiblePaths.length) {
console.log('未找到任何图片');
return;
}
const potentialPath = allPossiblePaths[index];
console.log('尝试加载图片:', potentialPath);
const img = new Image();
img.onload = function() {
console.log('图片加载成功:', potentialPath);
// 图片存在,设置路径并显示
elements.sceneImage.src = potentialPath;
elements.sceneImage.style.display = 'block';
};
img.onerror = function() {
console.log('图片加载失败:', potentialPath);
// 图片不存在,检查下一个
checkImage(index + 1);
};
img.src = potentialPath;
}
// 开始检查
checkImage(0);
} else {
console.log('使用指定的图片路径:', imagePath);
// 使用指定的图片路径
elements.sceneImage.src = imagePath;
elements.sceneImage.onload = function() {
elements.sceneImage.style.display = 'block';
};
elements.sceneImage.onerror = function() {
console.error('指定的图片路径加载失败:', imagePath);
elements.sceneImage.style.display = 'none';
};
}
// 清空对话区域
elements.conversation.innerHTML = '';
// 重置对话索引
gameState.currentDialogIndex = 0;
// 显示第一个对话
showNextDialog();
// 更新进度
updateProgress();
// 隐藏下一场景按钮
elements.nextSceneButton.disabled = true;
}
// 显示下一个对话
function showNextDialog() {
const scene = englishData.scenes[gameState.currentSceneIndex];
const conversation = scene.conversation;
if (gameState.currentDialogIndex >= conversation.length) {
// 当前场景对话结束
if (gameState.currentSceneIndex < englishData.scenes.length - 1) {
elements.nextSceneButton.disabled = false;
}
return;
}
const dialog = conversation[gameState.currentDialogIndex];
if (dialog.type === 'user_question') {
// 显示选择题
showQuestion(dialog);
} else {
// 显示普通对话
addMessage(dialog);
// 自动显示下一个对话
gameState.currentDialogIndex++;
setTimeout(showNextDialog, 1000);
}
}
// 添加对话消息
function addMessage(dialog) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${dialog.type}`;
const speakerSpan = document.createElement('span');
speakerSpan.className = 'speaker';
speakerSpan.textContent = dialog.speaker + ':';
const textSpan = document.createElement('span');
textSpan.className = 'text';
textSpan.textContent = dialog.text;
messageDiv.appendChild(speakerSpan);
messageDiv.appendChild(textSpan);
elements.conversation.appendChild(messageDiv);
// 滚动到底部
elements.conversation.scrollTop = elements.conversation.scrollHeight;
}
// 显示选择题
function showQuestion(dialog) {
elements.questionText.textContent = dialog.question;
// 复制选项数组并打乱顺序
const originalOptions = [...dialog.options];
const shuffledOptions = [...originalOptions];
// 使用Fisher-Yates算法打乱选项
for (let i = shuffledOptions.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledOptions[i], shuffledOptions[j]] = [shuffledOptions[j], shuffledOptions[i]];
}
// 计算正确答案在打乱后的新位置
const correctAnswerText = originalOptions[dialog.correctAnswer];
const newCorrectIndex = shuffledOptions.indexOf(correctAnswerText);
// 保存原始选项和新的正确答案索引到对话对象中,供后续检查使用
dialog.originalOptions = originalOptions;
dialog.shuffledOptions = shuffledOptions;
dialog.shuffledCorrectAnswer = newCorrectIndex;
// 设置打乱后的选项
elements.options.forEach((option, index) => {
option.textContent = shuffledOptions[index];
option.disabled = false;
option.classList.remove('selected', 'correct', 'incorrect');
});
// 显示选择题区域
elements.questionArea.style.display = 'block';
elements.feedback.style.display = 'none';
}
// 处理选项选择
function handleOptionSelect(selectedOptionIndex) {
const scene = englishData.scenes[gameState.currentSceneIndex];
const dialog = scene.conversation[gameState.currentDialogIndex];
// 使用打乱后的正确答案索引
const correctAnswerIndex = dialog.shuffledCorrectAnswer !== undefined ? dialog.shuffledCorrectAnswer : dialog.correctAnswer;
// 禁用所有选项
elements.options.forEach(option => {
option.disabled = true;
});
// 标记选择的选项
const selectedOption = elements.options[selectedOptionIndex];
selectedOption.classList.add('selected');
// 检查答案
gameState.totalQuestions++;
let isCorrect = false;
if (selectedOptionIndex === correctAnswerIndex) {
// 正确答案
gameState.score += 10;
gameState.correctAnswers++;
selectedOption.classList.add('correct');
showFeedback(true, dialog);
isCorrect = true;
} else {
// 错误答案
selectedOption.classList.add('incorrect');
elements.options[correctAnswerIndex].classList.add('correct');
showFeedback(false, dialog);
}
// 更新得分和进度
updateProgress();
// 添加用户选择的对话
addUserChoiceToConversation(dialog, selectedOptionIndex, isCorrect);
// 延迟后显示下一个对话
setTimeout(() => {
gameState.currentDialogIndex++;
elements.questionArea.style.display = 'none';
elements.feedback.style.display = 'none';
showNextDialog();
}, 3000);
}
// 添加用户选择到对话
function addUserChoiceToConversation(dialog, selectedIndex, isCorrect) {
const userMessage = {
speaker: dialog.speaker,
text: dialog.options[selectedIndex],
type: 'user'
};
addMessage(userMessage);
}
// 显示反馈
function showFeedback(isCorrect, dialog) {
const feedbackTextElement = elements.feedbackText;
if (isCorrect) {
// 正确答案
feedbackTextElement.textContent = '正确! 很好的回答。';
feedbackTextElement.className = 'correct';
elements.feedback.style.borderColor = '#4CAF50';
elements.feedback.style.backgroundColor = '#e8f5e8';
} else {
// 错误答案
// 根据是否有打乱后的选项来选择使用哪个数组
const correctAnswer = dialog.shuffledOptions && dialog.shuffledCorrectAnswer !== undefined
? dialog.shuffledOptions[dialog.shuffledCorrectAnswer]
: dialog.options[dialog.correctAnswer];
feedbackTextElement.textContent = `不正确。正确答案是: ${correctAnswer}`;
feedbackTextElement.className = 'incorrect';
elements.feedback.style.borderColor = '#f44336';
elements.feedback.style.backgroundColor = '#ffebee';
}
elements.feedback.style.display = 'block';
}
// 更新进度
function updateProgress() {
elements.scoreElement.textContent = `得分: ${gameState.score}`;
elements.progressElement.textContent = `进度: ${gameState.currentSceneIndex + 1}/${englishData.scenes.length}`;
}
// 显示游戏完成信息
function showGameComplete() {
elements.conversation.innerHTML = '';
elements.questionArea.style.display = 'none';
const completeMessage = document.createElement('div');
completeMessage.className = 'message other';
completeMessage.innerHTML = `
<span class="speaker">学习完成:</span>
<span class="text">
恭喜您完成了所有场景的学习<br>
总得分: ${gameState.score}<br>
正确题目: ${gameState.correctAnswers}/${gameState.totalQuestions}
</span>
`;
elements.conversation.appendChild(completeMessage);
elements.nextSceneButton.disabled = true;
}
// 设置事件监听器
function setupEventListeners() {
// 选项按钮点击事件
elements.options.forEach((option, index) => {
option.addEventListener('click', () => {
handleOptionSelect(index);
});
});
// 开始按钮点击事件
elements.startButton.addEventListener('click', () => {
initGame();
elements.startButton.style.display = 'none';
});
// 重置按钮点击事件
elements.resetButton.addEventListener('click', () => {
initGame();
elements.startButton.style.display = 'none';
});
// 下一场景按钮点击事件
elements.nextSceneButton.addEventListener('click', () => {
gameState.currentSceneIndex++;
loadScene(gameState.currentSceneIndex);
});
}
// 页面加载完成后初始化游戏
document.addEventListener('DOMContentLoaded', () => {
// 获取DOM元素
getDomElements();
// 确保englishData已加载
if (typeof englishData !== 'undefined') {
initGame();
elements.startButton.style.display = 'block';
} else {
console.error('英语学习数据未加载');
}
});

324
style.css Normal file
View File

@ -0,0 +1,324 @@
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
.header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background-color: #4a90e2;
color: white;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.2em;
opacity: 0.9;
}
/* 状态栏样式 */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 15px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.score, .progress {
font-size: 1.1em;
font-weight: bold;
color: #4a90e2;
}
/* 内容区域样式 */
.content {
background-color: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
}
/* 场景信息样式 */
.scene-info {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e8f0fe;
}
.scene-info h2 {
color: #4a90e2;
margin-bottom: 10px;
font-size: 1.8em;
}
.scene-info p {
color: #666;
font-size: 1.1em;
}
/* 场景图片样式 */
.scene-image-container {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
}
#scene-image {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
max-height: 300px;
object-fit: cover;
}
/* 对话区域样式 */
.conversation-area {
margin-bottom: 30px;
}
.conversation {
max-height: 400px;
overflow-y: auto;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.message {
margin-bottom: 15px;
padding: 10px 15px;
border-radius: 18px;
max-width: 80%;
clear: both;
}
.message.other {
background-color: #e3f2fd;
color: #1565c0;
float: left;
border-bottom-left-radius: 5px;
}
.message.user {
background-color: #4a90e2;
color: white;
float: right;
border-bottom-right-radius: 5px;
}
.message .speaker {
font-weight: bold;
margin-right: 10px;
}
/* 问题区域样式 */
.question-area {
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.question h3 {
color: #4a90e2;
margin-bottom: 20px;
font-size: 1.3em;
}
.options {
display: flex;
flex-direction: column;
gap: 15px;
}
.option {
padding: 15px 20px;
border: 2px solid #e0e0e0;
background-color: white;
border-radius: 8px;
font-size: 1.1em;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
}
.option:hover {
border-color: #4a90e2;
background-color: #f0f8ff;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.option.selected {
border-color: #4a90e2;
background-color: #e3f2fd;
}
.option.correct {
border-color: #4caf50;
background-color: #e8f5e9;
color: #2e7d32;
}
.option.incorrect {
border-color: #f44336;
background-color: #ffebee;
color: #c62828;
}
.option.disabled {
cursor: not-allowed;
opacity: 0.7;
}
.option.disabled:hover {
transform: none;
box-shadow: none;
}
/* 反馈区域样式 */
.feedback {
margin-bottom: 20px;
padding: 15px 20px;
border-radius: 8px;
border: 1px solid #ddd;
text-align: center;
font-weight: bold;
}
.feedback.correct {
background-color: #e8f5e9;
border-color: #4caf50;
color: #2e7d32;
}
.feedback.incorrect {
background-color: #ffebee;
border-color: #f44336;
color: #c62828;
}
/* 控制按钮样式 */
.controls {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1.1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
min-width: 120px;
}
.btn-primary {
background-color: #4a90e2;
color: white;
}
.btn-primary:hover {
background-color: #357abd;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.btn-secondary {
background-color: #90a4ae;
color: white;
}
.btn-secondary:hover {
background-color: #78909c;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.btn-success {
background-color: #4caf50;
color: white;
}
.btn-success:hover {
background-color: #388e3c;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2em;
}
.content {
padding: 20px;
}
.status-bar {
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.message {
max-width: 95%;
}
.controls {
flex-direction: column;
align-items: center;
}
.btn {
width: 100%;
max-width: 300px;
}
}