Complete Course Design Project
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.env
|
||||
.venv/
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.13
|
||||
47
README.md
Normal 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
@ -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
|
After Width: | Height: | Size: 2.3 MiB |
BIN
images/1.png
Normal file
|
After Width: | Height: | Size: 3.8 MiB |
BIN
images/2.png
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
BIN
images/3.png
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
images/4.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
images/5.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
images/6.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
images/7.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
images/8.png
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
BIN
images/9.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
67
index.html
Normal 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
@ -0,0 +1,6 @@
|
||||
def main():
|
||||
print("Hello from english-learning-uv!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
pyproject.toml
Normal 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
@ -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
@ -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;
|
||||
}
|
||||
}
|
||||