
从0到1:用 Qwen3-Coder 和 高德MCP 助力数字文旅建造------国庆山西游
1. 背景
"技术不是替代旅行,而是让旅途更有把握,让每一次选择更符合你的期待。"
随着大模型与地图服务能力的成熟,围绕旅游场景的"智能行程助理"成为低门槛、强体验的实践方向。Qwen3-Coder 负责理解需求、生成代码与自动化操作,高德MCP 则作为地理数据能力的桥梁,承接地理编码、路径规划、天气与POI等服务。本文以"国庆山西游"原型为例,提供从0到1的构建路径,帮助读者快速完成一个可本地运行、无需外部依赖的演示级系统。
2.效果展示
首页展示:

地图导览:

行程规划:

天气查询:
3. 相关介绍
3.1 数字文旅
数字文旅强调以数据与智能驱动旅行全流程,包括目的地信息聚合、行程生成、动态调整与复盘沉淀。目标不只是"把信息摆齐",而是通过算法与工具让决策更高效、体验更顺滑。
3.2 Qwen3-Coder
- Qwen3-Coder-Plus:作为"理解-生成-优化"的智能中枢,擅长在复杂需求中做任务拆解与自动化拼装。
- Qwen-Code CLI:面向开发/办公的命令行"驾驶舱",用自然语言与模型对话,驱动任务执行,降低实施门槛。
3.3 高德MCP
高德MCP将高德开放平台能力以 MCP 协议方式提供,统一了模型侧的调用姿势,实现地点搜索、地理编码、路线规划、天气与POI检索等,便于与大模型工作流拼装。
4.分步指南(超详细)
4.1 第一步 激活Qwen3-Coder
(1)获取 API Key:访问阿里云百炼平台(https://bailian.console.aliyun.com/?tab=app#/app-market/newTemplate) 或魔搭平台(https://modelscope.cn/models) 来开通服务和获取 API Key。(以阿里云百炼平台为例)

(2)安装Node.js 版本:必须安装 Node.js 20 或更高版本。
访问Node.js --- Download Node.js® 官方网站的下载页面,选择平台为 Windows 并选择下载并安装(推荐 LTS 版本)
**安装验证:**安装完成后,打开命令提示符(CMD)并运行以下命令,以确认 Node.js 和 npm(Node.js 包管理器)已成功安装。
cmd
node -v
npm -v

(3)安装Qwen-Code:
打开命令提示符(CMD)并运行以下命令:
cmd
npm install -g @qwen-code/qwen-code
安装完成后,通过以下命令验证安装是否成功:
cmd
qwen --version

(4)系统级环境变量:打开命令提示符(CMD)并运行以下命令
setx OPENAI_API_KEY "YOUR_API_KEY_HERE"
setx OPENAI_BASE_URL "YOUR_REGIONAL_BASE_URL"
setx OPENAI_MODEL "qwen3-coder-plus"
中国内地用户:
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
国际用户:
OPENAI_BASE_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1

(5)启动Qwen3-Coder:
打开命令提示符(CMD)并运行以下命令(先到指定文件夹,再启动):
cmd
cd 路径
qwen

4.2 第二步 配置高德MCP
(1)访问高德开放平台获取你自己的API密钥。

(2)根据以下配置MCP:
json
{
"mcpServers": {
"gaode": {
"command": "cmd",
"args": [
"/c",
"npx",
"@wopal/mcp-gaode-maps"
],
"env": {
"GAODE_API_KEY": "您的高德地图API密钥"
}
}
}
}
4.3 实战开发数字文旅------构建国庆山西游
我们以"5日游:大同---太原---平遥"为主线,组合规划、路线、天气与POI功能,产出可运行原型页面。
以下为用于驱动 Qwen3-Coder 的首轮提示词(内含示例 Key 字段,按需替换):
md
我想设计一个项目用高德MCP 助力数字文旅建造------国庆山西游:
背景需求:
- 国庆的来临,欢迎大家来山西游
要求:
- 帮我规划一条山西5日游路线,包括大同,太原,平遥等,并提供可视化地图,考虑交通时间和各景点的游览时长
- 请查询各个风景区的开放时间、门票价格
- 请推荐各个风景区附近评分最高的餐厅
- 请查询山西那5日的天气情况,并给出适合的旅游活动建议
- 每个对方加入可以直接位置导航
界面与体验要求:
- 可视化地图使用内嵌,并且展示各景点之间的顺序以及路线,"GAODE_API_KEY": "示例 Key "
- 卡通风格:界面色彩明亮,字体圆润可爱,符合大众审美。
- 在网站最下面:山西国庆5日游旅行助手 © 2025 制作者:LucianaiB
- 无需联网,纯本地运行(HTML + CSS + JS 单文件)
技术要求:
- 使用 HTML、CSS、JavaScript 单文件实现
- 调用高德MCP作为地点路线支持
- 不依赖任何外部库或服务器
请提供一个开箱即用的解决方案,包含全部代码和使用指南。
提示词投喂与结果观察:

开始漫长的测试与优化中...

效果展示:

5.全文总结
5.1 用户旅程与交互流
Qwen3-Coder 游客 系统 高德MCP 出行前 出行前 游客 搜集目的地信息 搜集目的地信息 Qwen3-Coder 生成初版行程 生成初版行程 出行中 出行中 高德MCP 路线导航与换乘 路线导航与换乘 高德MCP 附近餐饮推荐 附近餐饮推荐 系统 天气联动调整 天气联动调整 出行后 出行后 游客 评价与复盘 评价与复盘 系统 数据沉淀与优化 数据沉淀与优化 山西5日游用户旅程
5.2模块职责与数据流(对照表)
模块/组件 | 主要职责 | 关键输入 | 关键输出 | 典型风险 |
---|---|---|---|---|
Qwen3-Coder | 需求理解、代码/文案生成、任务分解 | 自然语言、上下文、示例 | HTML/JS片段、提示词、脚本 | 幻觉、指令不明确 |
高德MCP | 地理编码/路径/天气/POI | 经纬度、关键词、API Key | 路线polyline、POI列表、天气数据 | 配额、鉴权失败 |
前端页面 | 地图渲染、交互、可视化 | API响应、行程数据 | 标注、连线、行程卡片 | 兼容性、性能 |
配置与密钥 | 环境变量、安全管理 | OPENAI_*、GAODE_API_KEY | 安全调用上下文 | 泄露、误配 |
5.3 总结
本原型以"国庆山西游"为故事线,目标是用尽量少的工程成本,验证"一套大模型+地图服务"的可行性与可用性。Qwen3-Coder 在这里承担了两个关键角色:其一是把自然语言需求转为可执行方案(例如拆出页面结构、脚本逻辑与接口参数),其二是在多轮调试中快速重构与迁移,实现"人---机共创"的高效闭环。高德MCP 则起到"能力联通器"的作用,向上以统一的 MCP 协议与模型对接,向下聚合地理编码、路径规划、天气与 POI 等核心能力,使得我们不必在各种零碎 SDK 之间反复切换。二者结合的结果,是把传统需要多人协作、分工较细的工作流,压缩为一人可控的小闭环,尤其适合中小团队或教学与演示场景。
从实施路径看,本文遵循"先能跑、再好看、最后稳"的节奏:先保证 API Key、MCP 适配与页面基础能力可运行;随后完善地图可视化与行程结构化表达;再通过日志回放与抽样核验,逐步抬高准确性与稳定性。为了避免"好看不耐用",我们引入了量化评测指标,分别从准确性、响应速度、成本效益、易用性与稳定性给出权重与打分标准,鼓励在后续版本中进行 A/B 对比与数据驱动的优化。风险方面,重点提示密钥管理(统一走环境变量)、调用配额(限流与重试)与隐私定位(授权与最小化采集)等问题,确保方案不仅"能演示",也具备延展到生产级的基本素养。
更重要的是,可复用性。本文提供的提示词框架、模块职责对照与旅程图,可以直接迁移到其他目的地或主题(例如"研学游""亲子游""红色文化游"),仅需替换城市与兴趣点,即可组合新的原型。倘若后续接入更多数据源(如票务、住宿与评价),或引入前端组件库与服务端缓存,整体体验还可以在不改动核心架构的前提下平滑进化。综上,这套"Qwen3-Coder + 高德MCP"的组合为数字文旅提供了一条具备性价比与可复制性的落地路径:既能快速亮相,也能循序渐进地走向成熟。
"把复杂留给系统,把选择还给用户。"
"不是要替你做决定,而是把更好的选项摆在你面前。"
6.完整代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>山西5日游旅行助手 - 发现三晋之美</title>
<script src="https://webapi.amap.com/maps?v=2.0&key=fdda8428fc9485f355b24b1c76f6f147"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: auto;
color: #333;
overflow-x: hidden;
}
/* 🎨 动态渐变背景动画 */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(45deg, #667eea, #764ba2, #f093fb, #f5576c, #4facfe, #00f2fe);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
z-index: -2;
}
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* 🌟 鼠标跟随光晕效果 */
.cursor-glow {
position: fixed;
width: 20px;
height: 20px;
background: radial-gradient(circle, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0.2) 50%, transparent 100%);
border-radius: 50%;
pointer-events: none;
z-index: 9999;
transition: transform 0.1s ease;
mix-blend-mode: screen;
}
/* ✨ 页面切换动画 */
.page-transition {
opacity: 0;
transform: translateY(30px);
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.page-transition.active {
opacity: 1;
transform: translateY(0);
}
.fade-in {
animation: fadeIn 0.8s ease-out forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 🎯 按钮波纹效果 */
.ripple-effect {
position: relative;
overflow: hidden;
}
.ripple-effect::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.ripple-effect:active::before {
width: 300px;
height: 300px;
}
/* 💫 卡片悬浮3D效果 */
.card-3d {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform-style: preserve-3d;
}
.card-3d:hover {
transform: translateY(-10px) rotateX(5deg) rotateY(5deg);
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
}
/* 💓 心跳动画 */
.heartbeat {
animation: heartbeat 2s ease-in-out infinite;
}
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
14% { transform: scale(1.1); }
28% { transform: scale(1); }
42% { transform: scale(1.1); }
70% { transform: scale(1); }
}
/* ⌨️ 打字机效果 */
.typewriter {
overflow: hidden;
border-right: 2px solid rgba(255,255,255,0.75);
white-space: nowrap;
animation: typing 3.5s steps(40, end), blink-caret 0.75s step-end infinite;
}
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
@keyframes blink-caret {
from, to { border-color: transparent; }
50% { border-color: rgba(255,255,255,0.75); }
}
/* 🔢 数字计数动画 */
.counter {
transition: all 0.3s ease;
}
/* 📱 触摸手势支持 */
.swipe-container {
touch-action: pan-x;
position: relative;
}
/* 🎪 元素依次出现动画 */
.stagger-animation {
opacity: 0;
transform: translateY(30px);
animation: staggerIn 0.6s ease-out forwards;
}
.stagger-animation:nth-child(1) { animation-delay: 0.1s; }
.stagger-animation:nth-child(2) { animation-delay: 0.2s; }
.stagger-animation:nth-child(3) { animation-delay: 0.3s; }
.stagger-animation:nth-child(4) { animation-delay: 0.4s; }
.stagger-animation:nth-child(5) { animation-delay: 0.5s; }
@keyframes staggerIn {
to {
opacity: 1;
transform: translateY(0);
}
}
/* 🌊 滚动隐藏导航栏 */
.header {
transition: transform 0.3s ease-in-out;
}
.header.hidden {
transform: translateY(-100%);
}
/* 🎨 增强现有动画 */
.nav-item {
position: relative;
overflow: hidden;
}
.nav-item::after {
content: '';
position: absolute;
bottom: 0;
left: -100%;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent);
transition: left 0.5s ease;
}
.nav-item:hover::after {
left: 100%;
}
/* 🎭 按钮增强动画 */
.enhanced-button {
position: relative;
overflow: hidden;
transform: perspective(1px) translateZ(0);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.enhanced-button:hover {
transform: scale(1.05) translateZ(0);
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.enhanced-button:active {
transform: scale(0.98) translateZ(0);
}
/* 🌈 彩虹边框动画 */
.rainbow-border {
position: relative;
background: linear-gradient(45deg, #ff0000, #ff7300, #fffb00, #48ff00, #00ffd5, #002bff, #7a00ff, #ff00c8, #ff0000);
background-size: 400%;
border-radius: 15px;
padding: 2px;
animation: rainbow 3s linear infinite;
}
@keyframes rainbow {
0% { background-position: 0% 50%; }
100% { background-position: 400% 50%; }
}
.rainbow-border > * {
background: white;
border-radius: 13px;
}
/* 🎪 旋转加载动画增强 */
.loading-enhanced {
position: relative;
width: 60px;
height: 60px;
}
.loading-enhanced::before,
.loading-enhanced::after {
content: '';
position: absolute;
border-radius: 50%;
animation: spin 1.5s linear infinite;
}
.loading-enhanced::before {
width: 60px;
height: 60px;
border: 3px solid transparent;
border-top: 3px solid var(--primary-orange);
border-right: 3px solid var(--primary-blue);
}
.loading-enhanced::after {
width: 40px;
height: 40px;
top: 10px;
left: 10px;
border: 3px solid transparent;
border-bottom: 3px solid var(--accent-yellow);
border-left: 3px solid var(--accent-pink);
animation-direction: reverse;
animation-duration: 1s;
}
/* 🎨 滑动切换动画 */
.slide-left {
animation: slideLeft 0.5s ease-out;
}
.slide-right {
animation: slideRight 0.5s ease-out;
}
@keyframes slideLeft {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideRight {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* 🌟 粒子效果背景 */
.particles {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
background: rgba(255, 255, 255, 0.5);
border-radius: 50%;
animation: float 6s ease-in-out infinite;
}
.particle:nth-child(odd) {
animation-delay: -2s;
animation-duration: 8s;
}
.particle:nth-child(even) {
animation-delay: -4s;
animation-duration: 10s;
}
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg);
opacity: 0;
}
10%, 90% {
opacity: 1;
}
50% {
transform: translateY(-100px) rotate(180deg);
}
}
/* 卡通风格主题色彩 */
:root {
--primary-orange: #FF6B35;
--primary-blue: #4ECDC4;
--accent-yellow: #FFE66D;
--accent-pink: #FF8B94;
--accent-green: #95E1D3;
--bg-white: #FFFFFF;
--text-dark: #2C3E50;
--text-light: #7F8C8D;
}
/* 顶部导航栏 */
.header {
background: linear-gradient(90deg, var(--primary-orange), var(--primary-blue));
padding: 15px 0;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="20" cy="20" r="2" fill="rgba(255,255,255,0.1)"/><circle cx="80" cy="40" r="1.5" fill="rgba(255,255,255,0.1)"/><circle cx="40" cy="80" r="1" fill="rgba(255,255,255,0.1)"/></svg>');
animation: float 20s infinite linear;
}
@keyframes float {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
position: relative;
z-index: 2;
}
.logo {
display: flex;
align-items: center;
color: white;
font-size: 24px;
font-weight: bold;
text-decoration: none;
}
.logo::before {
content: '🏯';
font-size: 32px;
margin-right: 10px;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
60% { transform: translateY(-5px); }
}
.nav-menu {
display: flex;
gap: 20px;
list-style: none;
}
.nav-item {
background: rgba(255,255,255,0.2);
border-radius: 25px;
padding: 10px 20px;
cursor: pointer;
transition: all 0.3s ease;
color: white;
font-weight: 500;
backdrop-filter: blur(10px);
}
.nav-item:hover {
background: rgba(255,255,255,0.3);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* 主要内容区域 */
.main-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
display: grid;
grid-template-columns: 300px 1fr;
gap: 20px;
min-height: auto;
}
/* 侧边栏 */
.sidebar {
background: var(--bg-white);
border-radius: 20px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
height: fit-content;
position: sticky;
top: 20px;
}
.sidebar-title {
color: var(--primary-orange);
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
display: flex;
align-items: center;
}
.sidebar-title::before {
content: '🗺️';
margin-right: 8px;
font-size: 24px;
}
.route-days {
display: flex;
flex-direction: column;
gap: 12px;
}
.day-button {
background: linear-gradient(135deg, var(--accent-yellow), var(--accent-pink));
border: none;
border-radius: 15px;
padding: 15px;
cursor: pointer;
transition: all 0.3s ease;
color: var(--text-dark);
font-weight: 600;
font-size: 14px;
position: relative;
overflow: hidden;
}
.day-button:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.day-button.active {
background: linear-gradient(135deg, var(--primary-orange), var(--primary-blue));
color: white;
}
.day-button::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
transition: left 0.5s;
}
.day-button:hover::before {
left: 100%;
}
/* 功能按钮区域 */
.function-buttons {
margin-top: 25px;
display: flex;
flex-direction: column;
gap: 12px;
}
.function-btn {
background: var(--bg-white);
border: 2px solid var(--primary-blue);
border-radius: 12px;
padding: 12px 16px;
cursor: pointer;
transition: all 0.3s ease;
color: var(--primary-blue);
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.function-btn:hover {
background: var(--primary-blue);
color: white;
transform: scale(1.05);
}
/* 主内容区域 */
.content-area {
background: var(--bg-white);
min-height: auto;
height: fit-content;
padding: 0;
margin: 0;
min-height: auto;
padding: 0;
margin: 0;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
position: relative;
}
/* 欢迎页面 */
.welcome-section {
padding: 40px;
height: fit-content;
min-height: auto;
margin-bottom: 0;
height: fit-content;
min-height: auto;
text-align: center;
background: linear-gradient(135deg, var(--accent-yellow), var(--accent-pink));
color: var(--text-dark);
}
.welcome-title {
font-size: 36px;
font-weight: bold;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
}
.welcome-subtitle {
font-size: 18px;
margin-bottom: 30px;
opacity: 0.8;
}
.start-journey-btn {
background: var(--primary-orange);
color: white;
border: none;
padding: 15px 30px;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(255,107,53,0.3);
}
.start-journey-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(255,107,53,0.4);
}
/* 地图区域 */
.map-section {
display: none;
height: 600px;
position: relative;
}
#map-container {
width: 100%;
height: 100%;
border-radius: 0 0 20px 20px;
}
/* 路线详情区域 */
.route-section {
display: none;
padding: 30px;
}
.panel-title {
color: var(--primary-orange);
font-size: 24px;
font-weight: bold;
margin-bottom: 25px;
display: flex;
align-items: center;
}
.panel-title::before {
content: '📍';
margin-right: 10px;
font-size: 28px;
}
.route-timeline {
display: flex;
flex-direction: column;
gap: 20px;
}
.timeline-item {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
border-radius: 15px;
padding: 20px;
position: relative;
border-left: 5px solid var(--primary-blue);
transition: all 0.3s ease;
}
.timeline-item:hover {
transform: translateX(5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.timeline-day {
background: var(--primary-orange);
color: white;
padding: 5px 15px;
border-radius: 20px;
font-weight: bold;
display: inline-block;
margin-bottom: 15px;
}
.timeline-content h3 {
color: var(--text-dark);
margin-bottom: 10px;
}
.timeline-attractions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.attraction-tag {
background: var(--accent-yellow);
color: var(--text-dark);
padding: 5px 12px;
border-radius: 15px;
font-size: 12px;
font-weight: 500;
}
/* 天气区域 */
.weather-section {
display: none;
padding: 30px;
}
.weather-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.weather-card {
background: linear-gradient(135deg, var(--primary-blue), var(--accent-green));
color: white;
padding: 20px;
border-radius: 15px;
text-align: center;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.weather-city {
font-size: 18px;
font-weight: bold;
margin-bottom: 10px;
}
.weather-temp {
font-size: 32px;
font-weight: bold;
margin: 10px 0;
}
.weather-desc {
.weather-desc {
opacity: 0.9;
font-size: 14px;
}
.weather-icon {
font-size: 48px;
margin: 10px 0;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
}
.weather-range {
font-size: 14px;
opacity: 0.8;
margin-bottom: 12px;
}
.weather-details {
display: grid;
gap: 8px;
margin-top: 15px;
}
.weather-detail-item {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
opacity: 0.9;
padding: 6px 12px;
background: rgba(255,255,255,0.15);
border-radius: 8px;
backdrop-filter: blur(10px);
}
.weather-detail-item span:last-child {
font-weight: bold;
}
.forecast-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #eee;
}
.forecast-item:last-child {
border-bottom: none;
}
.forecast-date {
font-weight: bold;
color: var(--text-dark);
min-width: 80px;
}
.forecast-weather {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
justify-content: center;
}
.forecast-icon {
font-size: 24px;
}
.forecast-desc {
color: var(--text-light);
font-size: 14px;
}
.forecast-temp {
font-weight: bold;
color: var(--primary-orange);
min-width: 80px;
text-align: right;
}
}
.weather-details {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 12px;
position: relative;
z-index: 2;
}
.weather-detail-item {
background: rgba(255,255,255,0.2);
padding: 8px;
border-radius: 8px;
text-align: center;
font-size: 12px;
}
.weather-detail-label {
opacity: 0.8;
margin-bottom: 4px;
}
.weather-detail-value {
font-weight: bold;
}
/* 5日预报样式 */
.forecast-container {
margin-top: 25px;
background: #f8f9fa;
padding: 20px;
border-radius: 15px;
}
.forecast-title {
color: var(--primary-orange);
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
display: flex;
align-items: center;
}
.forecast-title::before {
content: '📅';
margin-right: 8px;
}
.forecast-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.forecast-item {
background: white;
padding: 15px;
border-radius: 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s ease;
}
.forecast-item:hover {
transform: translateY(-2px);
}
.forecast-date {
font-weight: bold;
color: var(--text-dark);
margin-bottom: 8px;
font-size: 14px;
}
.forecast-icon {
font-size: 24px;
margin: 8px 0;
}
.forecast-desc {
font-size: 12px;
color: var(--text-light);
margin-bottom: 8px;
}
.forecast-temp {
font-weight: bold;
color: var(--primary-blue);
font-size: 13px;
}
/* 旅游指数样式 */
.travel-index-container {
margin-top: 25px;
background: linear-gradient(135deg, #e3f2fd, #f3e5f5);
padding: 20px;
border-radius: 15px;
}
.travel-index-title {
color: var(--primary-blue);
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
display: flex;
align-items: center;
}
.travel-index-title::before {
content: '🎯';
margin-right: 8px;
}
.travel-index-grid {
display: grid;
gap: 10px;
}
.travel-index-item {
background: white;
padding: 12px 15px;
border-radius: 10px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
/* 天气预警样式 */
.weather-alerts {
margin-top: 25px;
background: linear-gradient(135deg, #fff3e0, #fce4ec);
padding: 20px;
border-radius: 15px;
border-left: 4px solid #ff9800;
}
.alert-title {
color: #ff9800;
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
display: flex;
align-items: center;
}
.alert-title::before {
content: '⚠️';
margin-right: 8px;
}
.alert-content {
color: var(--text-dark);
line-height: 1.5;
font-size: 14px;
}
/* 天气更新时间 */
.weather-update {
text-align: center;
margin-top: 20px;
color: var(--text-light);
font-size: 12px;
}
.refresh-weather-btn {
background: var(--primary-blue);
color: white;
border: none;
padding: 8px 16px;
border-radius: 20px;
font-size: 12px;
cursor: pointer;
margin-left: 10px;
transition: all 0.3s ease;
}
.refresh-weather-btn:hover {
background: var(--primary-orange);
transform: scale(1.05);
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-container {
grid-template-columns: 1fr;
gap: 15px;
padding: 15px;
}
.sidebar {
position: static;
order: 2;
}
.nav-menu {
display: none;
}
.welcome-title {
font-size: 28px;
}
.map-section {
height: 400px;
}
}
/* 加载动画 */
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.loading::after {
content: '';
width: 40px;
height: 40px;
border: 4px solid var(--accent-yellow);
border-top: 4px solid var(--primary-orange);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 浮动按钮样式 */
.floating-buttons {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.floating-btn {
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.floating-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 1);
}
</style>
</head>
<body>
<!-- 顶部导航栏 -->
<header class="header">
<div class="nav-container">
<a href="#" class="logo">山西5日游助手</a>
<nav class="nav-menu">
<div class="nav-item" data-section="welcome">首页</div>
<div class="nav-item" data-section="map">地图导览</div>
<div class="nav-item" data-section="route">行程规划</div>
<div class="nav-item" data-section="weather">天气查询</div>
</nav>
</div>
</header>
<!-- 主要内容区域 -->
<main class="main-container">
<!-- 侧边栏 -->
<aside class="sidebar">
<div class="sidebar-title">5日游行程</div>
<div class="route-days">
<button class="day-button" data-day="1">
第1天 - 太原市区游<br>
<small>晋祠 → 山西博物院 → 柳巷</small>
</button>
<button class="day-button" data-day="2">
第2天 - 太原-平遥<br>
<small>乔家大院 → 平遥古城</small>
</button>
<button class="day-button" data-day="3">
第3天 - 平遥古城<br>
<small>古城墙 → 日升昌 → 县衙</small>
</button>
<button class="day-button" data-day="4">
第4天 - 平遥-大同<br>
<small>王家大院 → 云冈石窟</small>
</button>
<button class="day-button" data-day="5">
第5天 - 大同-返程<br>
<small>悬空寺 → 华严寺</small>
</button>
</div>
<div class="function-buttons">
</div>
</aside>
<!-- 主内容区域 -->
<section class="content-area">
<!-- 欢迎页面 -->
<div id="welcome" class="welcome-section">
<h1 class="welcome-title">🏯 探索三晋文化之旅</h1>
<p class="welcome-subtitle">
精心规划的山西5日游路线,带您领略千年古韵与现代魅力
</p>
<button class="start-journey-btn" data-section="map">
🚀 开始我的山西之旅
</button>
<div style="margin-top: 40px; margin-bottom: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
<div style="background: rgba(255,255,255,0.9); padding: 20px; border-radius: 15px;">
<h3 style="color: var(--primary-orange); margin-bottom: 10px;">🏛️ 历史文化</h3>
<p>探访晋祠、平遥古城等千年古迹</p>
</div>
<div style="background: rgba(255,255,255,0.9); padding: 20px; border-radius: 15px;">
<h3 style="color: var(--primary-blue); margin-bottom: 10px;">🎨 石窟艺术</h3>
<p>欣赏云冈石窟的佛教艺术瑰宝</p>
</div>
<div style="background: rgba(255,255,255,0.9); padding: 20px; border-radius: 15px;">
<h3 style="color: var(--accent-pink); margin-bottom: 10px;">🍜 特色美食</h3>
<p>品尝刀削面、平遥牛肉等地道美味</p>
</div>
</div>
</div>
<!-- 地图区域 -->
<div id="map" class="map-section">
<div id="map-container"></div>
</div>
<!-- 路线详情区域 -->
<div id="route" class="route-section">
<div class="panel-title">山西5日游完整行程</div>
<div id="route-content" class="route-timeline">
<!-- 路线内容将通过JavaScript动态生成 -->
</div>
</div>
<!-- 天气区域 -->
<div id="weather" class="weather-section">
<div class="panel-title">🌤️ 各城市天气预报</div>
<!-- 天气更新时间 -->
<div style="text-align: center; margin-bottom: 20px; color: var(--text-light); font-size: 14px;">
<span id="weather-update-time">最后更新: --</span>
<button onclick="updateWeather()" style="margin-left: 15px; background: var(--primary-blue); color: white; border: none; padding: 5px 12px; border-radius: 8px; cursor: pointer; font-size: 12px;">🔄 刷新</button>
</div>
<div class="weather-cards">
<div class="weather-card" data-city="taiyuan">
<div class="weather-city">太原</div>
<div class="weather-icon">☀️</div>
<div class="weather-temp">22°C</div>
<div class="weather-range">15°C ~ 25°C</div>
<div class="weather-desc">晴转多云 适宜出行</div>
<div class="weather-details">
<div class="weather-detail-item">
<span>💨 风力</span>
<span class="wind-level">2级</span>
</div>
<div class="weather-detail-item">
<span>💧 湿度</span>
<span class="humidity">65%</span>
</div>
<div class="weather-detail-item">
<span>👁️ 能见度</span>
<span class="visibility">15km</span>
</div>
</div>
</div>
<div class="weather-card" data-city="pingyao">
<div class="weather-city">平遥</div>
<div class="weather-icon">⛅</div>
<div class="weather-temp">20°C</div>
<div class="weather-range">13°C ~ 23°C</div>
<div class="weather-desc">多云 微风</div>
<div class="weather-details">
<div class="weather-detail-item">
<span>💨 风力</span>
<span class="wind-level">1级</span>
</div>
<div class="weather-detail-item">
<span>💧 湿度</span>
<span class="humidity">58%</span>
</div>
<div class="weather-detail-item">
<span>👁️ 能见度</span>
<span class="visibility">12km</span>
</div>
</div>
</div>
<div class="weather-card" data-city="datong">
<div class="weather-city">大同</div>
<div class="weather-icon">🌤️</div>
<div class="weather-temp">18°C</div>
<div class="weather-range">10°C ~ 21°C</div>
<div class="weather-desc">晴朗 空气清新</div>
<div class="weather-details">
<div class="weather-detail-item">
<span>💨 风力</span>
<span class="wind-level">3级</span>
</div>
<div class="weather-detail-item">
<span>💧 湿度</span>
<span class="humidity">45%</span>
</div>
<div class="weather-detail-item">
<span>👁️ 能见度</span>
<span class="visibility">20km</span>
</div>
</div>
</div>
</div>
<!-- 5日天气预报 -->
<div style="margin-top: 30px;">
<h3 style="color: var(--primary-orange); margin-bottom: 20px; display: flex; align-items: center;">
<span style="margin-right: 8px;">📅</span>
5日天气趋势
</h3>
<div id="weather-forecast" style="background: white; border-radius: 15px; padding: 20px; box-shadow: 0 4px 15px rgba(0,0,0,0.08);">
<!-- 5日预报内容将通过JavaScript生成 -->
</div>
</div>
<!-- 穿衣建议和旅游指数 -->
<div style="margin-top: 30px; display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<div style="background: linear-gradient(135deg, #fff, #f8f9fa); padding: 20px; border-radius: 15px; border-left: 4px solid var(--primary-orange);">
<h3 style="color: var(--primary-orange); margin-bottom: 15px; display: flex; align-items: center;">
<span style="margin-right: 8px;">🧥</span>
穿衣建议
</h3>
<div id="clothing-advice" style="line-height: 1.6; color: var(--text-dark);">
山西秋季昼夜温差较大,建议携带薄外套。白天可穿长袖衬衫,晚上需要加件外套保暖。
舒适的运动鞋是必备,因为会有较多步行游览。
</div>
</div>
<div style="background: linear-gradient(135deg, #fff, #f8f9fa); padding: 20px; border-radius: 15px; border-left: 4px solid var(--primary-blue);">
<h3 style="color: var(--primary-blue); margin-bottom: 15px; display: flex; align-items: center;">
<span style="margin-right: 8px;">🎯</span>
旅游指数
</h3>
<div id="travel-index" style="display: grid; gap: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>🚶 舒适度指数</span>
<span style="background: #4CAF50; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px;">优</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>📸 拍照指数</span>
<span style="background: #2196F3; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px;">良</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>🌬️ 空气质量</span>
<span style="background: #4CAF50; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px;">优</span>
</div>
</div>
</div>
</div>
<!-- 天气预警 -->
<div id="weather-alerts" style="margin-top: 20px; display: none;">
<div style="background: linear-gradient(135deg, #fff3cd, #ffeaa7); padding: 15px; border-radius: 12px; border-left: 4px solid #f39c12;">
<h4 style="color: #d68910; margin: 0 0 10px 0; display: flex; align-items: center;">
<span style="margin-right: 8px;">⚠️</span>
天气预警
</h4>
<div id="alert-content" style="color: #7d6608; font-size: 14px; line-height: 1.5;"></div>
</div>
</div>
</div>
</section>
</main>
<script>
// 全局变量
let map = null;
let markers = [];
let polylines = [];
let currentDay = 0;
// 山西5日游完整路线数据
const travelRoute = [
{
day: 1,
city: "太原",
title: "太原市区游",
attractions: [
{
name: "晋祠",
location: [112.434468, 37.708991],
openTime: "08:00-18:00",
price: "80元",
duration: "2-3小时",
description: "中国现存最早的皇家园林,三晋文化的发源地",
visitTime: "09:00-12:00"
},
{
name: "山西博物院",
location: [112.563958, 37.857014],
openTime: "09:00-17:00",
price: "免费",
duration: "2-3小时",
description: "了解山西历史文化的最佳场所",
visitTime: "14:00-17:00"
},
{
name: "柳巷商业街",
location: [112.565308, 37.857014],
openTime: "全天开放",
price: "免费",
duration: "1-2小时",
description: "太原最繁华的商业街,品尝当地美食",
visitTime: "18:00-20:00"
}
],
accommodation: "太原市区酒店",
transport: "市内公交/地铁"
},
{
day: 2,
city: "太原-平遥",
title: "太原-平遥",
attractions: [
{
name: "乔家大院",
location: [112.232593, 37.386844],
openTime: "08:00-18:30",
price: "138元",
duration: "2-3小时",
description: "清代北方民居建筑的典型代表",
visitTime: "10:00-13:00"
},
{
name: "平遥古城",
location: [112.190369, 37.195001],
openTime: "全天开放",
price: "125元",
duration: "半天",
description: "保存最完整的明清古城之一",
visitTime: "15:00-18:00"
}
],
accommodation: "平遥古城内客栈",
transport: "高铁/大巴 (约2小时)"
},
{
day: 3,
city: "平遥",
title: "平遥古城深度游",
attractions: [
{
name: "平遥古城墙",
location: [112.190369, 37.195001],
openTime: "08:00-18:00",
price: "包含在古城票内",
duration: "1-2小时",
description: "明代古城墙,俯瞰古城全貌",
visitTime: "08:30-10:30"
},
{
name: "日升昌票号",
location: [112.189369, 37.194001],
openTime: "08:00-18:00",
price: "包含在古城票内",
duration: "1小时",
description: "中国第一家票号,了解古代金融业",
visitTime: "11:00-12:00"
},
{
name: "县衙署",
location: [112.191369, 37.196001],
openTime: "08:00-18:00",
price: "包含在古城票内",
duration: "1小时",
description: "明清县衙建筑群,体验古代官府文化",
visitTime: "14:00-15:00"
}
],
accommodation: "平遥古城内客栈",
transport: "步行游览"
},
{
day: 4,
city: "平遥-大同",
title: "平遥-大同",
attractions: [
{
name: "王家大院",
location: [111.833333, 37.133333],
openTime: "08:00-18:30",
price: "66元",
duration: "2-3小时",
description: "被誉为'华夏民居第一宅'",
visitTime: "09:00-12:00"
},
{
name: "云冈石窟",
location: [113.132415, 40.109589],
openTime: "08:30-17:30",
price: "120元",
duration: "3-4小时",
description: "中国四大石窟之一,世界文化遗产",
visitTime: "15:00-18:00"
}
],
accommodation: "大同市区酒店",
transport: "高铁 (约3小时)"
},
{
day: 5,
city: "大同",
title: "大同-返程",
attractions: [
{
name: "悬空寺",
location: [113.718468, 39.665325],
openTime: "08:00-18:00",
price: "130元",
duration: "2小时",
description: "建在悬崖峭壁上的千年古寺",
visitTime: "09:00-11:00"
},
{
name: "华严寺",
location: [113.295415, 40.076589],
openTime: "08:00-18:00",
price: "65元",
duration: "1-2小时",
description: "辽金时期佛教建筑精品",
visitTime: "13:00-15:00"
}
],
accommodation: "返程",
transport: "高铁/飞机返程"
}
];
// 餐厅推荐数据
const restaurants = {
taiyuan: [
{ name: "老太原面馆", rating: 4.8, cuisine: "面食", distance: "500m", price: "人均30元" },
{ name: "认一力饭庄", rating: 4.7, cuisine: "晋菜", distance: "800m", price: "人均80元" },
{ name: "六味斋", rating: 4.6, cuisine: "熟食", distance: "300m", price: "人均25元" }
],
pingyao: [
{ name: "德居源", rating: 4.9, cuisine: "晋菜", distance: "200m", price: "人均60元" },
{ name: "天元奎饭店", rating: 4.7, cuisine: "传统菜", distance: "150m", price: "人均45元" },
{ name: "洪武记饭店", rating: 4.8, cuisine: "平遥牛肉", distance: "100m", price: "人均35元" }
],
datong: [
{ name: "凤临阁", rating: 4.8, cuisine: "大同菜", distance: "600m", price: "人均70元" },
{ name: "老大同刀削面", rating: 4.6, cuisine: "面食", distance: "400m", price: "人均20元" },
{ name: "同和居", rating: 4.7, cuisine: "传统菜", distance: "500m", price: "人均50元" }
]
};
// 🎨 动画效果管理器
class AnimationManager {
constructor() {
this.currentPage = 0;
this.totalPages = 4;
this.isAnimating = false;
this.touchStartX = 0;
this.touchEndX = 0;
this.lastScrollY = 0;
this.init();
}
init() {
this.createCursorGlow();
this.createParticles();
this.setupKeyboardShortcuts();
this.setupTouchGestures();
this.setupScrollHideNav();
this.addRippleEffects();
this.startTypewriterEffect();
this.enhanceButtons();
this.addStaggerAnimations();
}
// 🌟 创建鼠标跟随光晕
createCursorGlow() {
const glow = document.createElement('div');
glow.className = 'cursor-glow';
document.body.appendChild(glow);
document.addEventListener('mousemove', (e) => {
glow.style.left = e.clientX - 10 + 'px';
glow.style.top = e.clientY - 10 + 'px';
});
document.addEventListener('mousedown', () => {
glow.style.transform = 'scale(1.5)';
});
document.addEventListener('mouseup', () => {
glow.style.transform = 'scale(1)';
});
}
// ✨ 创建粒子效果
createParticles() {
const particlesContainer = document.createElement('div');
particlesContainer.className = 'particles';
document.body.appendChild(particlesContainer);
for (let i = 0; i < 50; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = Math.random() * 100 + '%';
particle.style.top = Math.random() * 100 + '%';
particle.style.animationDelay = Math.random() * 6 + 's';
particlesContainer.appendChild(particle);
}
}
// ⌨️ 键盘快捷键
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (this.isAnimating) return;
switch(e.key) {
case '1':
this.switchToPage('welcome');
break;
case '2':
this.switchToPage('map');
break;
case '3':
this.switchToPage('route');
break;
case '4':
this.switchToPage('weather');
break;
case 'Escape':
this.switchToPage('welcome');
break;
case 'ArrowLeft':
this.previousPage();
break;
case 'ArrowRight':
this.nextPage();
break;
}
});
}
// 📱 触摸手势支持
setupTouchGestures() {
const container = document.querySelector('.content-area');
if (!container) return;
container.addEventListener('touchstart', (e) => {
this.touchStartX = e.changedTouches[0].screenX;
}, { passive: true });
container.addEventListener('touchend', (e) => {
this.touchEndX = e.changedTouches[0].screenX;
this.handleSwipe();
}, { passive: true });
}
handleSwipe() {
const swipeThreshold = 50;
const diff = this.touchStartX - this.touchEndX;
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0) {
this.nextPage(); // 向左滑动,下一页
} else {
this.previousPage(); // 向右滑动,上一页
}
}
}
// 🌊 滚动隐藏导航栏
setupScrollHideNav() {
window.addEventListener('scroll', () => {
const currentScrollY = window.scrollY;
const header = document.querySelector('.header');
if (currentScrollY > this.lastScrollY && currentScrollY > 100) {
header.classList.add('hidden');
} else {
header.classList.remove('hidden');
}
this.lastScrollY = currentScrollY;
});
}
// 🎯 添加波纹效果
addRippleEffects() {
const buttons = document.querySelectorAll('.day-button, .function-btn, .start-journey-btn, .nav-item');
buttons.forEach(button => {
button.classList.add('ripple-effect');
});
}
// ⌨️ 打字机效果
startTypewriterEffect() {
const title = document.querySelector('.welcome-title');
if (title) {
title.classList.add('typewriter');
}
}
// 🎨 增强按钮效果
enhanceButtons() {
const buttons = document.querySelectorAll('.day-button, .function-btn, .start-journey-btn');
buttons.forEach(button => {
button.classList.add('enhanced-button');
// 添加心跳动画给重要按钮
if (button.classList.contains('start-journey-btn')) {
button.classList.add('heartbeat');
}
});
// 添加3D卡片效果
const cards = document.querySelectorAll('.weather-card, .timeline-item, .sidebar');
cards.forEach(card => {
card.classList.add('card-3d');
});
}
// 🎪 添加渐进式加载动画
addStaggerAnimations() {
const elements = document.querySelectorAll('.route-days .day-button');
elements.forEach((el, index) => {
el.classList.add('stagger-animation');
el.style.animationDelay = (index * 0.1) + 's';
});
}
// 📄 页面切换动画
switchToPage(page) {
if (this.isAnimating) return;
this.isAnimating = true;
const currentSection = document.querySelector('.welcome-section[style*="block"], .map-section[style*="block"], .route-section[style*="block"], .weather-section[style*="block"]');
const targetSection = document.getElementById(page);
if (currentSection) {
currentSection.style.animation = 'fadeOut 0.3s ease-out forwards';
setTimeout(() => {
currentSection.style.display = 'none';
currentSection.style.animation = '';
if (targetSection) {
targetSection.style.display = 'block';
targetSection.classList.add('fade-in');
setTimeout(() => {
targetSection.classList.remove('fade-in');
this.isAnimating = false;
}, 800);
}
}, 300);
} else {
showSection(page);
this.isAnimating = false;
}
}
nextPage() {
const pages = ['welcome', 'map', 'route', 'weather'];
this.currentPage = (this.currentPage + 1) % pages.length;
this.switchToPage(pages[this.currentPage]);
}
previousPage() {
const pages = ['welcome', 'map', 'route', 'weather'];
this.currentPage = (this.currentPage - 1 + pages.length) % pages.length;
this.switchToPage(pages[this.currentPage]);
}
// 🔢 数字计数动画
animateCounter(element, start, end, duration = 2000) {
const startTime = performance.now();
const startValue = parseInt(start);
const endValue = parseInt(end);
const difference = endValue - startValue;
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
const currentValue = Math.round(startValue + (difference * easeOutQuart));
element.textContent = currentValue + '°C';
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
}
}
// 🎨 创建动画管理器实例
let animationManager;
// 初始化页面
document.addEventListener('DOMContentLoaded', function() {
console.log('页面加载完成,开始初始化...');
// 初始化动画管理器
animationManager = new AnimationManager();
showSection('welcome');
generateRouteContent();
setupEventListeners();
// 添加页面加载完成的淡入效果
document.body.style.opacity = '0';
setTimeout(() => {
document.body.style.transition = 'opacity 1s ease-in-out';
document.body.style.opacity = '1';
}, 100);
});
// 设置事件监听器
function setupEventListeners() {
console.log('设置事件监听器...');
// 导航菜单事件
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', function() {
const section = this.getAttribute('data-section');
showSection(section);
console.log('导航点击:', section);
showSection(section);
});
});
// 功能按钮事件
document.querySelectorAll('.function-btn').forEach(btn => {
btn.addEventListener('click', function() {
const action = this.getAttribute('data-action');
console.log('功能按钮点击:', action);
switch(action) {
case 'map':
showSection('map');
break;
case 'food':
showAttractions();
break;
case 'weather':
showSection('weather');
break;
case 'route':
showAllRoute();
break;
}
});
});
// 天数按钮事件
document.querySelectorAll('.day-button').forEach(btn => {
btn.addEventListener('click', function() {
const day = parseInt(this.getAttribute('data-day'));
console.log('天数按钮点击:', day);
showRouteDetail(day);
});
});
// 开始旅程按钮事件
document.querySelector('.start-journey-btn').addEventListener('click', function() {
const section = this.getAttribute('data-section');
showSection(section);
});
console.log('所有事件监听器设置完成');
}
// 显示不同区域
function showSection(section) {
console.log('切换到区域:', section);
// 隐藏所有区域
document.querySelectorAll('.welcome-section, .map-section, .route-section, .weather-section').forEach(el => {
el.style.display = 'none';
});
// 特殊处理美食推荐页面
if (section === 'food') {
showFoodSection();
return;
}
// 显示对应区域
const sectionElement = document.getElementById(section);
if (sectionElement) {
sectionElement.style.display = 'block';
}
// 特殊处理美食推荐页面
if (section === 'food') {
showFoodSection();
return;
}
// 特殊处理美食推荐页面
if (section === 'food') {
showFoodSection();
return;
}
// 隐藏所有区域
document.querySelectorAll('.welcome-section, .map-section, .route-section, .weather-section').forEach(el => {
el.style.display = 'none';
});
// 显示指定区域
const sectionToShow = document.getElementById(section);
if (sectionToShow) {
sectionToShow.style.display = 'block';
console.log('成功显示区域:', section);
} else {
console.error('找不到区域:', section);
return;
}
// 特殊处理
if (section === 'map') {
console.log('准备初始化地图...');
// 确保地图容器已显示后再初始化
setTimeout(() => {
if (typeof AMap !== 'undefined') {
initMap();
} else {
console.log('AMap未加载,等待加载完成...');
waitForAMap(() => initMap());
}
}, 200);
} else if (section === 'route') {
generateRouteContent();
} else if (section === 'weather') {
updateWeather();
}
}
// 地图初始化重试计数
let mapInitRetryCount = 0;
const maxRetries = 5;
// 初始化高德地图
function initMap() {
console.log('开始初始化地图,尝试次数:', mapInitRetryCount + 1);
try {
if (map) {
map.destroy();
map = null;
}
// 检查AMap是否已加载
if (typeof AMap === 'undefined') {
console.log('AMap未定义,等待加载...');
document.getElementById('map-container').innerHTML =
'<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #666; font-size: 16px; gap: 10px;">' +
'<div class="loading"></div>' +
'<div>🗺️ 地图API加载中,请稍候...</div>' +
'</div>';
// 重试机制
if (mapInitRetryCount < maxRetries) {
mapInitRetryCount++;
setTimeout(() => initMap(), 2000);
} else {
showMapError('地图API加载超时,请检查网络连接');
}
return;
}
console.log('AMap已加载,开始创建地图实例');
// 创建地图实例
map = new AMap.Map('map-container', {
zoom: 7,
center: [112.549248, 37.857014],
mapStyle: 'amap://styles/normal',
resizeEnable: true,
rotateEnable: true,
pitchEnable: true,
zoomEnable: true,
dragEnable: true
});
// 监听地图加载完成事件
map.on('complete', function() {
console.log('地图加载完成');
// 添加地图控件
try {
map.addControl(new AMap.Scale({
position: 'LB'
}));
map.addControl(new AMap.ToolBar({
position: 'RT'
}));
console.log('地图控件添加成功');
} catch (e) {
console.warn('地图控件添加失败:', e);
}
// 清除之前的标记
clearMapMarkers();
// 添加景点标记
addAllAttractions();
// 绘制路线
drawCompleteRoute();
console.log('✅ 地图初始化完全成功');
mapInitRetryCount = 0; // 重置重试计数
});
// 监听地图加载错误
map.on('error', function(e) {
console.error('地图加载错误:', e);
showMapError('地图加载出错: ' + (e.message || '未知错误'));
});
} catch (error) {
console.error('地图初始化异常:', error);
if (mapInitRetryCount < maxRetries) {
mapInitRetryCount++;
console.log('重试地图初始化,第', mapInitRetryCount, '次');
setTimeout(() => initMap(), 1000);
} else {
showMapError('地图初始化失败: ' + error.message);
}
}
}
// 显示地图错误信息
function showMapError(message) {
document.getElementById('map-container').innerHTML =
'<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #e74c3c; font-size: 16px; gap: 15px; padding: 20px; text-align: center;">' +
'<div style="font-size: 48px;">❌</div>' +
'<div style="font-weight: bold;">地图加载失败</div>' +
'<div style="font-size: 14px; color: #666; line-height: 1.5;">' + message + '</div>' +
'<button onclick="retryMapInit()" style="background: var(--primary-orange); color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px;">🔄 重新加载</button>' +
'</div>';
}
// 重新初始化地图
function retryMapInit() {
mapInitRetryCount = 0;
initMap();
}
// 等待AMap加载的函数
function waitForAMap(callback, timeout = 10000) {
const startTime = Date.now();
function check() {
if (typeof AMap !== 'undefined') {
callback();
} else if (Date.now() - startTime < timeout) {
setTimeout(check, 100);
} else {
showMapError('地图API加载超时,请检查网络连接或API密钥');
}
}
check();
}
// 清除地图标记
function clearMapMarkers() {
if (map) {
markers.forEach(marker => map.remove(marker));
polylines.forEach(polyline => map.remove(polyline));
}
markers = [];
polylines = [];
}
// 添加景点标记
function addAllAttractions() {
const colors = ['#FF6B35', '#4ECDC4', '#FFE66D', '#FF8B94', '#95E1D3'];
travelRoute.forEach((dayRoute, dayIndex) => {
dayRoute.attractions.forEach(attraction => {
const marker = new AMap.Marker({
position: attraction.location,
title: attraction.name,
icon: new AMap.Icon({
size: new AMap.Size(32, 32),
image: `data:image/svg+xml;base64,${btoa(` <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"> <circle cx="16" cy="16" r="14" fill="${colors[dayIndex]}" stroke="white" stroke-width="2"/> <text x="16" y="20" text-anchor="middle" fill="white" font-size="12" font-weight="bold">${dayIndex + 1}</text> </svg> `)}`,
imageSize: new AMap.Size(32, 32)
})
});
const infoWindow = new AMap.InfoWindow({
content: `
<div style="padding: 15px; min-width: 250px; font-family: 'Microsoft YaHei';">
<h3 style="color: ${colors[dayIndex]}; margin: 0 0 12px 0; font-size: 16px;">${attraction.name}</h3>
<div style="display: grid; gap: 8px; font-size: 13px;">
<p style="margin: 0;"><strong>🕒 开放时间:</strong> ${attraction.openTime}</p>
<p style="margin: 0;"><strong>💰 门票价格:</strong> ${attraction.price}</p>
<p style="margin: 0;"><strong>⏱️ 建议游览:</strong> ${attraction.duration}</p>
<p style="margin: 0;"><strong>📅 推荐时间:</strong> ${attraction.visitTime}</p>
<p style="margin: 8px 0 0 0; color: #666; font-size: 12px; line-height: 1.4;">${attraction.description}</p>
</div>
</div>
`,
offset: new AMap.Pixel(0, -30)
});
marker.on('click', () => infoWindow.open(map, marker.getPosition()));
markers.push(marker);
map.add(marker);
});
});
}
// 绘制路线
function drawCompleteRoute() {
const colors = ['#FF6B35', '#4ECDC4', '#FFE66D', '#FF8B94', '#95E1D3'];
// 绘制每天内部的路线
travelRoute.forEach((dayRoute, dayIndex) => {
if (dayRoute.attractions.length > 1) {
const path = dayRoute.attractions.map(attr => attr.location);
const polyline = new AMap.Polyline({
path: path,
strokeColor: colors[dayIndex],
strokeWeight: 4,
strokeOpacity: 0.8,
strokeStyle: 'solid'
});
polylines.push(polyline);
map.add(polyline);
}
});
// 连接不同天的路线
for (let i = 0; i < travelRoute.length - 1; i++) {
const currentDay = travelRoute[i];
const nextDay = travelRoute[i + 1];
if (currentDay.attractions.length > 0 && nextDay.attractions.length > 0) {
const lastAttraction = currentDay.attractions[currentDay.attractions.length - 1];
const firstAttraction = nextDay.attractions[0];
const connectLine = new AMap.Polyline({
path: [lastAttraction.location, firstAttraction.location],
strokeColor: '#999999',
strokeWeight: 2,
strokeOpacity: 0.6,
strokeStyle: 'dashed'
});
polylines.push(connectLine);
map.add(connectLine);
}
}
}
// 显示路线详情
function showRouteDetail(day) {
console.log('显示第', day, '天路线详情');
document.querySelectorAll('.day-button').forEach(btn => btn.classList.remove('active'));
const clickedButton = document.querySelector(`[data-day="${day}"]`);
if (clickedButton) clickedButton.classList.add('active');
currentDay = day;
showSection('route');
generateDayRouteContent(day);
// 如果地图已初始化,聚焦到该天的景点
if (map && travelRoute[day - 1]) {
const dayRoute = travelRoute[day - 1];
if (dayRoute.attractions.length > 0) {
const firstAttraction = dayRoute.attractions[0];
map.setCenter(firstAttraction.location);
map.setZoom(12);
}
}
}
// 生成路线内容
function generateRouteContent() {
const routeContent = document.getElementById('route-content');
if (!routeContent) return;
let html = '';
travelRoute.forEach(dayRoute => {
html += `
<div class="timeline-item">
<div class="timeline-day">第${dayRoute.day}天</div>
<div class="timeline-content">
<h3>${dayRoute.title}</h3>
<p><strong>🏨 住宿:</strong> ${dayRoute.accommodation}</p>
<p><strong>🚗 交通:</strong> ${dayRoute.transport}</p>
<div class="timeline-attractions">
${dayRoute.attractions.map(attr =>
`<span class="attraction-tag">${attr.name}</span>`
).join('')}
</div>
</div>
</div>
`;
});
routeContent.innerHTML = html;
}
// 生成单天详细内容
function generateDayRouteContent(day) {
const dayRoute = travelRoute[day - 1];
if (!dayRoute) return;
const routeContent = document.getElementById('route-content');
if (!routeContent) return;
let html = `
<div class="timeline-item" style="border-left: 5px solid var(--primary-orange); background: linear-gradient(135deg, #fff, #f8f9fa);">
<div class="timeline-day">第${dayRoute.day}天 - ${dayRoute.title}</div>
<div class="timeline-content">
<p style="color: var(--text-light); margin-bottom: 20px;">
<strong>🏨 住宿:</strong> ${dayRoute.accommodation} |
<strong>🚗 交通:</strong> ${dayRoute.transport}
</p>
</div>
</div>
`;
dayRoute.attractions.forEach((attraction, index) => {
html += `
<div class="timeline-item" style="margin-left: 20px; border-left: 3px solid var(--primary-blue);">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
<h3 style="color: var(--primary-blue); margin: 0; font-size: 18px;">${attraction.name}</h3>
<span style="background: var(--accent-yellow); color: var(--text-dark); padding: 6px 12px; border-radius: 12px; font-size: 12px; font-weight: bold; white-space: nowrap;">
${attraction.visitTime}
</span>
</div>
<p style="margin: 8px 0 15px 0; color: var(--text-dark); line-height: 1.5;">${attraction.description}</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px;">
<div style="background: #f1f3f4; padding: 12px; border-radius: 10px; text-align: center;">
<div style="font-size: 11px; color: var(--text-light); margin-bottom: 4px;">🕒 开放时间</div>
<div style="font-weight: bold; font-size: 13px;">${attraction.openTime}</div>
</div>
<div style="background: #f1f3f4; padding: 12px; border-radius: 10px; text-align: center;">
<div style="font-size: 11px; color: var(--text-light); margin-bottom: 4px;">💰 门票价格</div>
<div style="font-weight: bold; font-size: 13px; color: var(--primary-orange);">${attraction.price}</div>
</div>
<div style="background: #f1f3f4; padding: 12px; border-radius: 10px; text-align: center;">
<div style="font-size: 11px; color: var(--text-light); margin-bottom: 4px;">⏱️ 建议游览</div>
<div style="font-weight: bold; font-size: 13px;">${attraction.duration}</div>
</div>
</div>
</div>
`;
});
routeContent.innerHTML = html;
}
// 显示美食推荐
function showAttractions() {
const cities = ['taiyuan', 'pingyao', 'datong'];
const cityNames = ['太原', '平遥', '大同'];
let html = '<div class="panel-title">🍜 美食推荐</div>';
cities.forEach((city, index) => {
html += `
<div style="margin-bottom: 35px;">
<h3 style="color: var(--primary-orange); margin-bottom: 20px; font-size: 20px; display: flex; align-items: center;">
<span style="margin-right: 8px;">🏙️</span>
${cityNames[index]}美食推荐
</h3>
<div style="display: grid; gap: 18px;">
`;
restaurants[city].forEach(restaurant => {
html += `
<div style="background: linear-gradient(135deg, #fff, #f8f9fa); padding: 25px; border-radius: 18px; border-left: 4px solid var(--primary-blue); box-shadow: 0 4px 15px rgba(0,0,0,0.08); transition: transform 0.3s ease;" onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
<h4 style="margin: 0; color: var(--text-dark); font-size: 16px;">${restaurant.name}</h4>
<div style="display: flex; align-items: center; gap: 6px; background: rgba(255,165,0,0.1); padding: 4px 8px; border-radius: 8px;">
<span style="color: #ffa500; font-size: 14px;">⭐</span>
<span style="font-weight: bold; color: var(--primary-orange); font-size: 14px;">${restaurant.rating}</span>
</div>
</div>
<div style="display: flex; gap: 20px; font-size: 14px; color: var(--text-light);">
<span style="display: flex; align-items: center; gap: 4px;">🍽️ ${restaurant.cuisine}</span>
<span style="display: flex; align-items: center; gap: 4px;">📍 ${restaurant.distance}</span>
<span style="display: flex; align-items: center; gap: 4px;">💰 ${restaurant.price}</span>
</div>
</div>
`;
});
html += '</div></div>';
});
document.getElementById('route-content').innerHTML = html;
showSection('route');
}
// 显示完整路线
function showAllRoute() {
showSection('route');
generateRouteContent();
}
// 🎨 增强的动画效果函数
function addAdvancedAnimations() {
// 添加右下角浮动按钮
const floatingButtons = `
<div class="floating-buttons">
<button class="floating-btn" data-section="map" title="查看地图" onclick="showSection('map')">🗺️</button>
<button class="floating-btn" data-section="weather" title="天气预报" onclick="showSection('weather')">🌤️</button>
<button class="floating-btn" data-section="route" title="完整路线" onclick="showSection('itinerary')">📍</button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', floatingButtons);
// 为浮动按钮添加点击事件
document.querySelectorAll('.floating-btn').forEach(btn => {
btn.addEventListener('click', function() {
const section = this.getAttribute('data-section');
const action = this.getAttribute('data-action');
if (section) {
showSection(section);
} else if (action === 'food') {
showFoodSection();
}
});
});
// 🌈 为重要元素添加彩虹边框(更低调的样式)
const importantElements = document.querySelectorAll('.start-journey-btn');
importantElements.forEach(el => {
// 移除过于显眼的彩虹边框和心跳动画
el.classList.remove('heartbeat');
el.style.animation = 'none';
});
// 💫 添加悬浮动画到天气卡片
const weatherCards = document.querySelectorAll('.weather-card');
weatherCards.forEach((card, index) => {
card.style.animationDelay = (index * 0.2) + 's';
card.classList.add('stagger-animation');
// 添加温度数字动画
const tempElement = card.querySelector('.weather-temp');
if (tempElement) {
tempElement.addEventListener('mouseenter', () => {
const currentTemp = parseInt(tempElement.textContent);
animationManager.animateCounter(tempElement, currentTemp - 5, currentTemp, 1000);
});
}
});
// 🎪 为导航项添加更多动画效果
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach((item, index) => {
item.addEventListener('mouseenter', () => {
item.style.transform = 'translateY(-2px) scale(1.05)';
item.style.boxShadow = '0 8px 25px rgba(0,0,0,0.2)';
});
item.addEventListener('mouseleave', () => {
item.style.transform = 'translateY(0) scale(1)';
item.style.boxShadow = '0 5px 15px rgba(0,0,0,0.2)';
});
});
// 🎯 为功能按钮添加点击反馈
const functionBtns = document.querySelectorAll('.function-btn');
functionBtns.forEach(btn => {
btn.addEventListener('click', () => {
btn.style.transform = 'scale(0.95)';
setTimeout(() => {
btn.style.transform = 'scale(1.05)';
setTimeout(() => {
btn.style.transform = 'scale(1)';
}, 150);
}, 100);
});
});
// 🌟 为时间轴项目添加滑入动画
const timelineItems = document.querySelectorAll('.timeline-item');
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.animation = 'slideInFromLeft 0.6s ease-out forwards';
entry.target.style.opacity = '1';
}
});
}, observerOptions);
timelineItems.forEach(item => {
item.style.opacity = '0';
observer.observe(item);
});
}
// 🎨 添加滑入动画样式
const slideInAnimation = `
@keyframes slideInFromLeft {
from {
transform: translateX(-50px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeOut {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(-20px);
opacity: 0;
}
}
`;
// 动态添加样式
const styleSheet = document.createElement('style');
styleSheet.textContent = slideInAnimation;
document.head.appendChild(styleSheet);
// 更新天气
// 天气数据模拟
const weatherIcons = {
sunny: '☀️',
cloudy: '⛅',
partlyCloudy: '🌤️',
rainy: '🌧️',
snowy: '❄️',
foggy: '🌫️'
};
const weatherConditions = [
{ condition: 'sunny', desc: '晴朗', icon: '☀️' },
{ condition: 'cloudy', desc: '多云', icon: '⛅' },
{ condition: 'partlyCloudy', desc: '晴转多云', icon: '🌤️' },
{ condition: 'rainy', desc: '小雨', icon: '🌧️' },
{ condition: 'foggy', desc: '雾霾', icon: '🌫️' }
];
// 🌤️ 增强的天气更新函数
function updateWeather() {
console.log('🌤️ 更新天气信息...');
const cities = ['taiyuan', 'pingyao', 'datong'];
const cityNames = ['太原', '平遥', '大同'];
// 添加加载动画
const weatherSection = document.getElementById('weather');
if (weatherSection && weatherSection.style.display === 'block') {
const loadingDiv = document.createElement('div');
loadingDiv.className = 'loading-enhanced';
loadingDiv.style.position = 'fixed';
loadingDiv.style.top = '50%';
loadingDiv.style.left = '50%';
loadingDiv.style.transform = 'translate(-50%, -50%)';
loadingDiv.style.zIndex = '9999';
document.body.appendChild(loadingDiv);
setTimeout(() => {
document.body.removeChild(loadingDiv);
}, 1500);
}
// 更新当前天气(带动画)
document.querySelectorAll('.weather-card').forEach((card, index) => {
const cityData = generateWeatherData(cities[index]);
// 温度更新动画
const tempElement = card.querySelector('.weather-temp');
if (tempElement) {
const oldTemp = parseInt(tempElement.textContent) || cityData.temp;
animationManager.animateCounter(tempElement, oldTemp, cityData.temp, 1500);
}
// 其他元素的淡入更新
setTimeout(() => {
const rangeElement = card.querySelector('.weather-range');
if (rangeElement) {
rangeElement.style.opacity = '0';
setTimeout(() => {
rangeElement.textContent = `${cityData.minTemp}°C ~ ${cityData.maxTemp}°C`;
rangeElement.style.transition = 'opacity 0.5s ease';
rangeElement.style.opacity = '1';
}, 200);
}
const descElement = card.querySelector('.weather-desc');
if (descElement) {
descElement.style.opacity = '0';
setTimeout(() => {
descElement.textContent = cityData.desc + ' 适宜出行';
descElement.style.transition = 'opacity 0.5s ease';
descElement.style.opacity = '1';
}, 400);
}
const iconElement = card.querySelector('.weather-icon');
if (iconElement) {
iconElement.style.transform = 'scale(0)';
setTimeout(() => {
iconElement.textContent = cityData.icon;
iconElement.style.transition = 'transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55)';
iconElement.style.transform = 'scale(1)';
}, 600);
}
}, index * 200);
// 更新详细信息
const windElement = card.querySelector('.wind-level');
if (windElement) windElement.textContent = cityData.windLevel + '级';
const humidityElement = card.querySelector('.humidity');
if (humidityElement) humidityElement.textContent = cityData.humidity + '%';
const visibilityElement = card.querySelector('.visibility');
if (visibilityElement) visibilityElement.textContent = cityData.visibility + 'km';
});
// 更新5日预报
generate5DayForecast();
// 更新穿衣建议
updateClothingAdvice();
// 更新旅游指数
updateTravelIndex();
// 更新时间戳(带打字机效果)
const now = new Date();
const timeString = now.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
const updateTimeElement = document.getElementById('weather-update-time');
if (updateTimeElement) {
updateTimeElement.style.opacity = '0';
setTimeout(() => {
updateTimeElement.textContent = `最后更新: ${timeString}`;
updateTimeElement.style.transition = 'opacity 0.5s ease';
updateTimeElement.style.opacity = '1';
}, 800);
}
// 检查天气预警
checkWeatherAlerts();
console.log('✅ 天气信息更新完成');
}
// 生成天气数据
function generateWeatherData(city) {
const baseTemp = city === 'taiyuan' ? 20 : city === 'pingyao' ? 18 : 16;
const condition = weatherConditions[Math.floor(Math.random() * weatherConditions.length)];
const temp = baseTemp + Math.floor(Math.random() * 8) - 2;
const minTemp = temp - Math.floor(Math.random() * 5) - 3;
const maxTemp = temp + Math.floor(Math.random() * 5) + 2;
return {
temp: temp,
minTemp: minTemp,
maxTemp: maxTemp,
desc: condition.desc,
icon: condition.icon,
windLevel: Math.floor(Math.random() * 4) + 1,
humidity: Math.floor(Math.random() * 30) + 45,
visibility: Math.floor(Math.random() * 10) + 10,
condition: condition.condition
};
}
// 生成5日天气预报
// 国庆5日天气预报数据
const nationalDayWeather = [
{
date: '10月1日',
day: '国庆节',
taiyuan: { icon: '☀️', desc: '晴朗', temp: 22, minTemp: 12, maxTemp: 24, wind: '北风2级', humidity: 45 },
pingyao: { icon: '🌤️', desc: '晴转多云', temp: 20, minTemp: 10, maxTemp: 22, wind: '东北风2级', humidity: 50 },
datong: { icon: '☀️', desc: '晴朗', temp: 18, minTemp: 8, maxTemp: 20, wind: '北风3级', humidity: 40 }
},
{
date: '10月2日',
day: '国庆假期',
taiyuan: { icon: '⛅', desc: '多云', temp: 21, minTemp: 11, maxTemp: 23, wind: '东风2级', humidity: 55 },
pingyao: { icon: '⛅', desc: '多云', temp: 19, minTemp: 9, maxTemp: 21, wind: '东风2级', humidity: 58 },
datong: { icon: '🌤️', desc: '晴转多云', temp: 17, minTemp: 7, maxTemp: 19, wind: '东北风2级', humidity: 48 }
},
{
date: '10月3日',
day: '国庆假期',
taiyuan: { icon: '🌤️', desc: '晴转多云', temp: 23, minTemp: 13, maxTemp: 25, wind: '南风2级', humidity: 50 },
pingyao: { icon: '☀️', desc: '晴朗', temp: 21, minTemp: 11, maxTemp: 23, wind: '南风2级', humidity: 45 },
datong: { icon: '⛅', desc: '多云', temp: 19, minTemp: 9, maxTemp: 21, wind: '南风2级', humidity: 52 }
},
{
date: '10月4日',
day: '国庆假期',
taiyuan: { icon: '☀️', desc: '晴朗', temp: 24, minTemp: 14, maxTemp: 26, wind: '西南风2级', humidity: 42 },
pingyao: { icon: '☀️', desc: '晴朗', temp: 22, minTemp: 12, maxTemp: 24, wind: '西南风2级', humidity: 40 },
datong: { icon: '🌤️', desc: '晴转多云', temp: 20, minTemp: 10, maxTemp: 22, wind: '西风2级', humidity: 46 }
},
{
date: '10月5日',
day: '国庆假期',
taiyuan: { icon: '⛅', desc: '多云转晴', temp: 22, minTemp: 12, maxTemp: 24, wind: '北风2级', humidity: 48 },
pingyao: { icon: '⛅', desc: '多云', temp: 20, minTemp: 10, maxTemp: 22, wind: '北风2级', humidity: 52 },
datong: { icon: '☀️', desc: '晴朗', temp: 18, minTemp: 8, maxTemp: 20, wind: '北风3级', humidity: 44 }
}
];
// 生成国庆5日天气预报
function generate5DayForecast() {
const forecastContainer = document.getElementById('weather-forecast');
if (!forecastContainer) return;
let html = '';
nationalDayWeather.forEach((dayWeather, index) => {
html += `
<div class="forecast-day-container" style="margin-bottom: 25px;">
<div class="forecast-day-header" style="background: linear-gradient(135deg, var(--primary-orange), var(--primary-blue)); color: white; padding: 15px; border-radius: 12px 12px 0 0; text-align: center;">
<h4 style="margin: 0; font-size: 16px;">${dayWeather.date} (${dayWeather.day})</h4>
</div>
<div class="forecast-cities" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; background: white; border-radius: 0 0 12px 12px; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.1);">
<div class="city-forecast" style="padding: 20px; border-right: 1px solid #eee; text-align: center;">
<div style="font-weight: bold; color: var(--primary-orange); margin-bottom: 10px;">太原</div>
<div style="font-size: 32px; margin: 8px 0;">${dayWeather.taiyuan.icon}</div>
<div style="color: var(--text-dark); margin-bottom: 8px;">${dayWeather.taiyuan.desc}</div>
<div style="font-size: 18px; font-weight: bold; color: var(--primary-blue); margin-bottom: 8px;">${dayWeather.taiyuan.temp}°C</div>
<div style="font-size: 12px; color: var(--text-light);">${dayWeather.taiyuan.minTemp}°C ~ ${dayWeather.taiyuan.maxTemp}°C</div>
<div style="font-size: 11px; color: var(--text-light); margin-top: 5px;">${dayWeather.taiyuan.wind} | 湿度${dayWeather.taiyuan.humidity}%</div>
</div>
<div class="city-forecast" style="padding: 20px; border-right: 1px solid #eee; text-align: center;">
<div style="font-weight: bold; color: var(--primary-orange); margin-bottom: 10px;">平遥</div>
<div style="font-size: 32px; margin: 8px 0;">${dayWeather.pingyao.icon}</div>
<div style="color: var(--text-dark); margin-bottom: 8px;">${dayWeather.pingyao.desc}</div>
<div style="font-size: 18px; font-weight: bold; color: var(--primary-blue); margin-bottom: 8px;">${dayWeather.pingyao.temp}°C</div>
<div style="font-size: 12px; color: var(--text-light);">${dayWeather.pingyao.minTemp}°C ~ ${dayWeather.pingyao.maxTemp}°C</div>
<div style="font-size: 11px; color: var(--text-light); margin-top: 5px;">${dayWeather.pingyao.wind} | 湿度${dayWeather.pingyao.humidity}%</div>
</div>
<div class="city-forecast" style="padding: 20px; text-align: center;">
<div style="font-weight: bold; color: var(--primary-orange); margin-bottom: 10px;">大同</div>
<div style="font-size: 32px; margin: 8px 0;">${dayWeather.datong.icon}</div>
<div style="color: var(--text-dark); margin-bottom: 8px;">${dayWeather.datong.desc}</div>
<div style="font-size: 18px; font-weight: bold; color: var(--primary-blue); margin-bottom: 8px;">${dayWeather.datong.temp}°C</div>
<div style="font-size: 12px; color: var(--text-light);">${dayWeather.datong.minTemp}°C ~ ${dayWeather.datong.maxTemp}°C</div>
<div style="font-size: 11px; color: var(--text-light); margin-top: 5px;">${dayWeather.datong.wind} | 湿度${dayWeather.datong.humidity}%</div>
</div>
</div>
</div>
`;
});
forecastContainer.innerHTML = html;
}
// 更新穿衣建议
function updateClothingAdvice() {
const adviceElement = document.getElementById('clothing-advice');
if (!adviceElement) return;
const avgTemp = 19; // 平均温度
let advice = '';
if (avgTemp >= 25) {
advice = '天气较热,建议穿轻薄的短袖衣物,注意防晒。携带遮阳帽和太阳镜,多补充水分。';
} else if (avgTemp >= 20) {
advice = '天气温和,建议穿长袖衬衫或薄外套。早晚温差较大,可准备一件薄外套备用。';
} else if (avgTemp >= 15) {
advice = '天气偏凉,建议穿长袖衣物配薄外套。山西秋季昼夜温差大,晚上需要加件外套保暖。';
} else if (avgTemp >= 10) {
advice = '天气较冷,建议穿厚外套或轻薄羽绒服。注意保暖,特别是在户外景点游览时。';
} else {
advice = '天气寒冷,建议穿厚重的冬装,包括羽绒服、毛衣等。注意头部和手脚保暖。';
}
advice += ' 舒适的运动鞋是必备,因为会有较多步行游览。';
adviceElement.textContent = advice;
}
// 更新旅游指数
function updateTravelIndex() {
const indexElement = document.getElementById('travel-index');
if (!indexElement) return;
// 根据天气条件计算指数
const avgTemp = 19;
const humidity = 55;
const visibility = 15;
// 舒适度指数
let comfortLevel = avgTemp >= 15 && avgTemp <= 25 && humidity <= 70 ? '优' :
avgTemp >= 10 && avgTemp <= 30 && humidity <= 80 ? '良' : '一般';
let comfortColor = comfortLevel === '优' ? '#4CAF50' :
comfortLevel === '良' ? '#2196F3' : '#FF9800';
// 拍照指数
let photoLevel = visibility >= 15 ? '优' : visibility >= 10 ? '良' : '一般';
let photoColor = photoLevel === '优' ? '#4CAF50' :
photoLevel === '良' ? '#2196F3' : '#FF9800';
// 空气质量
let airLevel = visibility >= 15 ? '优' : visibility >= 10 ? '良' : '轻度污染';
let airColor = airLevel === '优' ? '#4CAF50' :
airLevel === '良' ? '#2196F3' : '#FF9800';
indexElement.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>🚶 舒适度指数</span>
<span style="background: ${comfortColor}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px;">${comfortLevel}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>📸 拍照指数</span>
<span style="background: ${photoColor}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px;">${photoLevel}</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>🌬️ 空气质量</span>
<span style="background: ${airColor}; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px;">${airLevel}</span>
</div>
`;
}
// 检查天气预警
function checkWeatherAlerts() {
const alertsElement = document.getElementById('weather-alerts');
const alertContent = document.getElementById('alert-content');
if (!alertsElement || !alertContent) return;
// 模拟天气预警检查
const hasAlert = Math.random() < 0.3; // 30% 概率有预警
if (hasAlert) {
const alerts = [
'大风蓝色预警:预计今日下午到夜间,部分地区有4-5级偏北风,阵风6-7级,请注意防范。',
'霜冻黄色预警:预计明晨最低气温将降至2°C以下,请注意添衣保暖。',
'大雾黄色预警:预计今夜到明晨有雾,能见度不足500米,请注意交通安全。'
];
const randomAlert = alerts[Math.floor(Math.random() * alerts.length)];
alertContent.textContent = randomAlert;
alertsElement.style.display = 'block';
} else {
alertsElement.style.display = 'none';
}
}
// 定时更新天气
setTimeout(updateWeather, 2000);
setInterval(updateWeather, 30 * 60 * 1000);
// 添加全局错误处理
window.addEventListener('error', function(e) {
console.error('JavaScript错误:', e.error);
});
// 🎨 增强现有函数的动画效果
const originalShowSection = showSection;
showSection = function(section) {
console.log('🎬 切换到区域:', section, '(带动画效果)');
// 如果动画管理器存在,使用动画切换
if (animationManager && !animationManager.isAnimating) {
animationManager.switchToPage(section);
return;
}
// 否则使用原始函数
originalShowSection(section);
};
// 🎯 增强按钮点击效果
const originalSetupEventListeners = setupEventListeners;
setupEventListeners = function() {
originalSetupEventListeners();
// 为所有按钮添加点击反馈效果
document.querySelectorAll('button, .nav-item, .day-button').forEach(element => {
element.addEventListener('click', function(e) {
// 创建点击波纹效果
const ripple = document.createElement('span');
const rect = this.getBoundingClientRect();
const size = Math.max(rect.width, rect.height);
const x = e.clientX - rect.left - size / 2;
const y = e.clientY - rect.top - size / 2;
ripple.style.cssText = `
position: absolute;
width: ${size}px;
height: ${size}px;
left: ${x}px;
top: ${y}px;
background: rgba(255, 255, 255, 0.6);
border-radius: 50%;
transform: scale(0);
animation: ripple-animation 0.6s linear;
pointer-events: none;
z-index: 1000;
`;
this.style.position = 'relative';
this.style.overflow = 'hidden';
this.appendChild(ripple);
setTimeout(() => {
if (ripple.parentNode) {
ripple.parentNode.removeChild(ripple);
}
}, 600);
});
});
// 添加键盘提示
const keyboardHint = document.createElement('div');
keyboardHint.innerHTML = `
<div style="position: fixed; bottom: 20px; right: 20px; background: rgba(0,0,0,0.8); color: white; padding: 15px; border-radius: 10px; font-size: 12px; z-index: 1000; backdrop-filter: blur(10px);">
<div style="margin-bottom: 8px; font-weight: bold;">⌨️ 快捷键</div>
<div>1-4: 切换页面</div>
<div>ESC: 返回首页</div>
<div>←→: 上下页面</div>
<div style="margin-top: 8px; font-size: 10px; opacity: 0.7;">📱 支持左右滑动</div>
</div>
`;
document.body.appendChild(keyboardHint);
// 3秒后自动隐藏提示
setTimeout(() => {
keyboardHint.style.transition = 'opacity 1s ease';
keyboardHint.style.opacity = '0';
setTimeout(() => {
if (keyboardHint.parentNode) {
keyboardHint.parentNode.removeChild(keyboardHint);
}
}, 1000);
}, 3000);
};
// 🎪 添加波纹动画样式
const rippleStyle = document.createElement('style');
rippleStyle.textContent = `
@keyframes ripple-animation {
to {
transform: scale(4);
opacity: 0;
}
}
/* 🎨 性能优化:暂停不可见动画 */
.particles {
animation-play-state: running;
}
body.page-hidden .particles {
animation-play-state: paused;
}
/* 📱 移动端优化 */
@media (max-width: 768px) {
.cursor-glow {
display: none;
}
.particles .particle {
animation-duration: 12s;
}
.card-3d:hover {
transform: translateY(-5px);
}
}
/* 🎯 无障碍支持 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.typewriter {
animation: none;
border-right: none;
}
.particles {
display: none;
}
}
`;
document.head.appendChild(rippleStyle);
// 🎨 页面可见性API优化性能
document.addEventListener('visibilitychange', function() {
if (document.hidden) {
document.body.classList.add('page-hidden');
} else {
document.body.classList.remove('page-hidden');
}
});
// 🌟 添加页面加载进度条
function showLoadingProgress() {
const progressBar = document.createElement('div');
progressBar.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 0%;
height: 3px;
background: linear-gradient(90deg, var(--primary-orange), var(--primary-blue));
z-index: 10000;
transition: width 0.3s ease;
`;
document.body.appendChild(progressBar);
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 15;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
setTimeout(() => {
progressBar.style.opacity = '0';
setTimeout(() => {
if (progressBar.parentNode) {
progressBar.parentNode.removeChild(progressBar);
}
}, 300);
}, 200);
}
progressBar.style.width = progress + '%';
}, 100);
}
// 页面完全加载后的最终检查
window.addEventListener('load', function() {
console.log('🎉 山西5日游旅行助手加载完成!');
console.log('📊 数据统计:');
console.log('- 路线天数:', travelRoute.length);
console.log('- 景点总数:', travelRoute.reduce((total, day) => total + day.attractions.length, 0));
console.log('- 餐厅推荐:', Object.values(restaurants).reduce((total, city) => total + city.length, 0));
console.log('✨ 动画效果已启用:');
console.log(' - 页面切换动画 ✓');
console.log(' - 按钮波纹效果 ✓');
console.log(' - 鼠标跟随光晕 ✓');
console.log(' - 动态渐变背景 ✓');
console.log(' - 数字计数动画 ✓');
console.log(' - 键盘快捷键支持 ✓');
console.log(' - 触摸手势支持 ✓');
console.log(' - 滚动隐藏导航 ✓');
console.log('✅ 所有功能已就绪,可以开始使用!');
// 启动高级动画效果
setTimeout(() => {
addAdvancedAnimations();
}, 1000);
// 显示欢迎消息
setTimeout(() => {
const welcomeToast = document.createElement('div');
welcomeToast.style.cssText = `
position: fixed;
top: 100px;
right: 20px;
background: linear-gradient(135deg, var(--primary-orange), var(--primary-blue));
color: white;
padding: 15px 20px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
z-index: 10000;
transform: translateX(400px);
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
`;
welcomeToast.innerHTML = `
<div style="font-weight: bold; margin-bottom: 5px;">🎉 欢迎使用山西旅行助手!</div>
<div style="font-size: 12px; opacity: 0.9;">所有动画效果已启用,尽情探索吧!</div>
`;
document.body.appendChild(welcomeToast);
setTimeout(() => {
welcomeToast.style.transform = 'translateX(0)';
}, 100);
setTimeout(() => {
welcomeToast.style.transform = 'translateX(400px)';
setTimeout(() => {
if (welcomeToast.parentNode) {
welcomeToast.parentNode.removeChild(welcomeToast);
}
}, 500);
}, 4000);
}, 2000);
});
// 🚀 启动加载进度条
showLoadingProgress();
</script>
<script>
// 修复浮动按钮功能的JavaScript代码
document.addEventListener('DOMContentLoaded', function() {
// 添加浮动按钮样式
const style = document.createElement('style');
style.textContent = `
/* 浮动按钮样式 */
.floating-buttons {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.floating-btn {
width: 50px;
height: 50px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.floating-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
background: rgba(255, 255, 255, 1);
}
/* 让首页样式更低调 */
.start-journey-btn {
animation: none !important;
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.2) !important;
}
.start-journey-btn:hover {
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3) !important;
transform: translateY(-1px) !important;
}
.rainbow-border {
display: none !important;
}
/* 美食推荐弹窗动画 */
/* 美食推荐弹窗动画 */
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
/* 美食筛选按钮样式 */
.food-filter-btn:hover {
background: var(--primary-orange) !important;
color: white !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
}
.food-filter-btn.active {
background: var(--primary-orange) !important;
color: white !important;
}
/* 美食卡片悬停效果 */
.food-card-item {
transition: all 0.3s ease;
}
.food-card-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
/* 评分星星悬停效果 */
.food-rating span:hover {
transform: scale(1.2);
transition: transform 0.2s ease;
}
/* 收藏按钮悬停效果 */
button[onclick*="toggleFoodFavorite"]:hover {
transform: scale(1.2);
transition: transform 0.2s ease;
}
@keyframes slideUp {
from { transform: translateY(50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
`;
document.head.appendChild(style);
// 添加浮动按钮HTML
const floatingButtons = document.createElement('div');
floatingButtons.className = 'floating-buttons';
floatingButtons.innerHTML = `
<button class="floating-btn" data-section="map" title="查看地图">🗺️</button>
<button class="floating-btn" data-section="weather" title="天气预报">🌤️</button>
<button class="floating-btn" data-section="route" title="完整路线">📍</button>
`;
document.body.appendChild(floatingButtons);
// 为浮动按钮添加点击事件
document.querySelectorAll('.floating-btn').forEach(btn => {
btn.addEventListener('click', function() {
const section = this.getAttribute('data-section');
const action = this.getAttribute('data-action');
// 添加点击反馈
this.style.transform = 'scale(0.9)';
setTimeout(() => {
this.style.transform = '';
}, 150);
if (section) {
// 调用现有的showSection函数
if (typeof showSection === 'function') {
showSection(section);
} else {
// 如果showSection不存在,手动切换
document.querySelectorAll('.section').forEach(s => s.style.display = 'none');
const pageSection = document.getElementById(section);
if (pageSection) {
pageSection.style.display = 'block';
}
}
} else if (action === 'food') {
// 美食推荐功能
showSection('food');
}
});
});
// 移除过于显眼的动画效果
setTimeout(() => {
const startBtn = document.querySelector('.start-journey-btn');
if (startBtn) {
startBtn.classList.remove('heartbeat');
startBtn.style.animation = 'none';
}
// 移除彩虹边框
const rainbowBorders = document.querySelectorAll('.rainbow-border');
rainbowBorders.forEach(border => {
const child = border.firstElementChild;
if (child) {
border.parentNode.insertBefore(child, border);
border.remove();
}
});
}, 100);
// 优化地图加载性能
const originalInitMap = window.initMap;
if (originalInitMap) {
window.initMap = function() {
// 添加防抖处理
if (window.mapInitTimeout) {
clearTimeout(window.mapInitTimeout);
}
window.mapInitTimeout = setTimeout(() => {
originalInitMap.call(this);
}, 100);
};
}
});
// 完整的美食推荐功能
function showFoodMessage() {
showFoodSection();
}
// 显示美食推荐页面
function showFoodSection() {
// 隐藏所有其他区域
document.querySelectorAll('.section').forEach(section => {
section.style.display = 'none';
});
// 显示美食推荐区域
let foodSection = document.getElementById('food-section');
if (!foodSection) {
foodSection = createFoodSection();
document.querySelector('.container').appendChild(foodSection);
}
foodSection.style.display = 'block';
// 更新导航状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
}
// 创建美食推荐区域
function createFoodSection() {
const foodSection = document.createElement('div');
foodSection.id = 'food-section';
foodSection.className = 'section';
foodSection.style.display = 'none';
const foodData = {
taiyuan: {
name: '太原',
foods: [
{ name: '刀削面', desc: '山西最著名的面食,面条粗细不均,口感筋道', price: '15-25元', location: '老太原面馆' },
{ name: '过油肉', desc: '山西传统名菜,肉质鲜嫩,色泽金黄', price: '35-45元', location: '晋菜馆' },
{ name: '头脑', desc: '太原特色早餐,营养丰富的羊肉汤', price: '20-30元', location: '清和元' },
{ name: '羊杂割', desc: '山西传统小吃,汤鲜味美', price: '18-28元', location: '老字号羊杂店' }
]
},
pingyao: {
name: '平遥',
foods: [
{ name: '平遥牛肉', desc: '中华老字号,色泽红润,香味浓郁', price: '50-80元/斤', location: '冠云牛肉店' },
{ name: '碗托', desc: '平遥特色小吃,口感爽滑', price: '8-12元', location: '古城内小摊' },
{ name: '莜面栲栳栳', desc: '山西特色面食,形似蜂窝', price: '25-35元', location: '德居源' },
{ name: '平遥豆腐脑', desc: '嫩滑香甜,配菜丰富', price: '10-15元', location: '早餐摊点' }
]
},
datong: {
name: '大同',
foods: [
{ name: '大同刀削面', desc: '大同版本的刀削面,汤头更浓郁', price: '18-28元', location: '老大同面馆' },
{ name: '浑源凉粉', desc: '大同特色凉粉,清爽解腻', price: '12-18元', location: '浑源凉粉店' },
{ name: '大同烧麦', desc: '皮薄馅大,鲜美可口', price: '20-30元', location: '老字号烧麦店' },
{ name: '油炸糕', desc: '外酥内软,香甜可口', price: '5-8元/个', location: '街边小摊' }
]
}
};
foodSection.innerHTML = `
<div class="panel-title">🍜 山西美食推荐</div>
<!-- 美食筛选按钮 -->
<div class="food-filters" style="display: flex; justify-content: center; gap: 10px; margin-bottom: 20px; flex-wrap: wrap;">
<button class="food-filter-btn active" data-filter="all" style="padding: 8px 16px; border: 2px solid var(--primary-orange); background: var(--primary-orange); color: white; border-radius: 20px; cursor: pointer; font-size: 14px; transition: all 0.3s;">全部</button>
<button class="food-filter-btn" data-filter="noodles" style="padding: 8px 16px; border: 2px solid var(--primary-orange); background: transparent; color: var(--primary-orange); border-radius: 20px; cursor: pointer; font-size: 14px; transition: all 0.3s;">面食类</button>
<button class="food-filter-btn" data-filter="meat" style="padding: 8px 16px; border: 2px solid var(--primary-orange); background: transparent; color: var(--primary-orange); border-radius: 20px; cursor: pointer; font-size: 14px; transition: all 0.3s;">肉食类</button>
<button class="food-filter-btn" data-filter="snacks" style="padding: 8px 16px; border: 2px solid var(--primary-orange); background: transparent; color: var(--primary-orange); border-radius: 20px; cursor: pointer; font-size: 14px; transition: all 0.3s;">小食类</button>
<button class="food-filter-btn" data-filter="specialty" style="padding: 8px 16px; border: 2px solid var(--primary-orange); background: transparent; color: var(--primary-orange); border-radius: 20px; cursor: pointer; font-size: 14px; transition: all 0.3s;">特产类</button>
</div>
<div class="food-intro" style="background: rgba(255,255,255,0.9); padding: 20px; border-radius: 15px; margin-bottom: 20px; text-align: center;">
<h3 style="color: var(--accent-pink); margin-bottom: 15px;">🎯 美食攻略</h3>
<p style="color: #666; line-height: 1.6; margin: 0;">
山西素有"面食之乡"的美誉,这里的美食文化源远流长。从太原的刀削面到平遥的牛肉,
每一道菜都承载着深厚的历史文化底蕴。让我们一起品味三晋大地的经典美味!
</p>
</div>
<div class="food-cities">
${Object.entries(foodData).map(([cityKey, cityData]) => ` <div class="food-city-card" style="background: rgba(255,255,255,0.95); border-radius: 20px; padding: 25px; margin-bottom: 25px; box-shadow: 0 8px 32px rgba(0,0,0,0.1);"> <h3 style="color: var(--primary-blue); margin-bottom: 20px; font-size: 24px; text-align: center;"> 🏙️ ${cityData.name}特色美食 </h3> <div class="food-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 15px;"> ${cityData.foods.map(food => ` <div class="food-item" style=" background: linear-gradient(135deg, #f8f9ff, #fff); border: 2px solid #e8f2ff; border-radius: 15px; padding: 20px; transition: all 0.3s ease; cursor: pointer; " onmouseover="this.style.transform='translateY(-5px)'; this.style.boxShadow='0 10px 30px rgba(0,0,0,0.15)'" onmouseout="this.style.transform=''; this.style.boxShadow=''"> <h4 style="color: var(--accent-pink); margin: 0 0 10px 0; font-size: 18px;">🍽️ ${food.name}</h4> <p style="color: #666; font-size: 14px; line-height: 1.5; margin: 0 0 10px 0;">${food.desc}</p> <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px;"> <span style="background: var(--primary-orange); color: white; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;">💰 ${food.price}</span> <span style="color: var(--primary-blue); font-size: 12px;">📍 ${food.location}</span> </div> </div> `).join('')} </div> </div> `).join('')}
</div>
<div class="food-tips" style="background: linear-gradient(135deg, #fff8e1, #fff3c4); border-radius: 15px; padding: 20px; margin-top: 20px; border-left: 5px solid var(--primary-orange);">
<h4 style="color: var(--primary-orange); margin: 0 0 15px 0; font-size: 18px;">💡 美食小贴士</h4>
<ul style="color: #666; line-height: 1.8; margin: 0; padding-left: 20px;">
<li>建议在当地老字号餐厅品尝正宗美食</li>
<li>山西面食种类繁多,可以多尝试几种</li>
<li>平遥牛肉可以买一些作为特产带回家</li>
<li>用餐时间避开高峰期,体验更佳</li>
<li>可以向当地人询问隐藏的美食小店</li>
</ul>
</div>
`;
return foodSection;
}
// 显示美食推荐页面
function showFoodSection() {
// 隐藏所有其他区域
document.querySelectorAll('.section').forEach(section => {
section.style.display = 'none';
});
// 显示美食推荐区域
let foodSection = document.getElementById('food-section');
if (!foodSection) {
foodSection = createFoodSection();
document.querySelector('.container').appendChild(foodSection);
}
foodSection.style.display = 'block';
// 更新导航状态
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
});
}
// 创建美食推荐区域
function createFoodSection() {
const foodSection = document.createElement('div');
foodSection.id = 'food-section';
foodSection.className = 'section';
foodSection.style.display = 'none';
const foodData = {
taiyuan: {
name: '太原',
foods: [
{ name: '刀削面', desc: '山西最著名的面食,面条粗细不均,口感筋道', price: '15-25元', location: '老太原面馆' },
{ name: '过油肉', desc: '山西传统名菜,肉质鲜嫩,色泽金黄', price: '35-45元', location: '晋菜馆' },
{ name: '头脑', desc: '太原特色早餐,营养丰富的羊肉汤', price: '20-30元', location: '清和元' },
{ name: '羊杂割', desc: '山西传统小吃,汤鲜味美', price: '18-28元', location: '老字号羊杂店' }
]
},
pingyao: {
name: '平遥',
foods: [
{ name: '平遥牛肉', desc: '中华老字号,色泽红润,香味浓郁', price: '50-80元/斤', location: '冠云牛肉店' },
{ name: '碗托', desc: '平遥特色小吃,口感爽滑', price: '8-12元', location: '古城内小摊' },
{ name: '莜面栲栳栳', desc: '山西特色面食,形似蜂窝', price: '25-35元', location: '德居源' },
{ name: '平遥豆腐脑', desc: '嫩滑香甜,配菜丰富', price: '10-15元', location: '早餐摊点' }
]
},
datong: {
name: '大同',
foods: [
{ name: '大同刀削面', desc: '大同版本的刀削面,汤头更浓郁', price: '18-28元', location: '老大同面馆' },
{ name: '浑源凉粉', desc: '大同特色凉粉,清爽解腻', price: '12-18元', location: '浑源凉粉店' },
{ name: '大同烧麦', desc: '皮薄馅大,鲜美可口', price: '20-30元', location: '老字号烧麦店' },
{ name: '油炸糕', desc: '外酥内软,香甜可口', price: '5-8元/个', location: '街边小摊' }
]
}
};
foodSection.innerHTML = `
<div class="panel-title">🍜 山西美食推荐</div>
<div class="food-intro" style="background: rgba(255,255,255,0.9); padding: 20px; border-radius: 15px; margin-bottom: 20px; text-align: center;">
<h3 style="color: var(--accent-pink); margin-bottom: 15px;">🎯 美食攻略</h3>
<p style="color: #666; line-height: 1.6; margin: 0;">
山西素有"面食之乡"的美誉,这里的美食文化源远流长。从太原的刀削面到平遥的牛肉,
每一道菜都承载着深厚的历史文化底蕴。让我们一起品味三晋大地的经典美味!
</p>
</div>
<div class="food-cities">
${Object.entries(foodData).map(([cityKey, cityData]) => ` <div class="food-city-card" style="background: rgba(255,255,255,0.95); border-radius: 20px; padding: 25px; margin-bottom: 25px; box-shadow: 0 8px 32px rgba(0,0,0,0.1);"> <h3 style="color: var(--primary-blue); margin-bottom: 20px; font-size: 24px; text-align: center;"> 🏙️ ${cityData.name}特色美食 </h3> <div class="food-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 15px;"> ${cityData.foods.map(food => ` <div class="food-item" style=" background: linear-gradient(135deg, #f8f9ff, #fff); border: 2px solid #e8f2ff; border-radius: 15px; padding: 20px; transition: all 0.3s ease; cursor: pointer; " onmouseover="this.style.transform='translateY(-5px)'; this.style.boxShadow='0 10px 30px rgba(0,0,0,0.15)'" onmouseout="this.style.transform=''; this.style.boxShadow=''"> <h4 style="color: var(--accent-pink); margin: 0 0 10px 0; font-size: 18px;">🍽️ ${food.name}</h4> <p style="color: #666; font-size: 14px; line-height: 1.5; margin: 0 0 10px 0;">${food.desc}</p> <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px;"> <span style="background: var(--primary-orange); color: white; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600;">💰 ${food.price}</span> <span style="color: var(--primary-blue); font-size: 12px;">📍 ${food.location}</span> </div> </div> `).join('')} </div> </div> `).join('')}
</div>
<div class="food-tips" style="background: linear-gradient(135deg, #fff8e1, #fff3c4); border-radius: 15px; padding: 20px; margin-top: 20px; border-left: 5px solid var(--primary-orange);">
<h4 style="color: var(--primary-orange); margin: 0 0 15px 0; font-size: 18px;">💡 美食小贴士</h4>
<ul style="color: #666; line-height: 1.8; margin: 0; padding-left: 20px;">
<li>建议在当地老字号餐厅品尝正宗美食</li>
<li>山西面食种类繁多,可以多尝试几种</li>
<li>平遥牛肉可以买一些作为特产带回家</li>
<li>用餐时间避开高峰期,体验更佳</li>
<li>可以向当地人询问隐藏的美食小店</li>
</ul>
</div>
`;
return foodSection;
}
// 完善的美食推荐功能
// 美食收藏功能
function toggleFoodFavorite(foodName, element) {
let favorites = JSON.parse(localStorage.getItem('foodFavorites') || '[]');
if (favorites.includes(foodName)) {
// 取消收藏
favorites = favorites.filter(name => name !== foodName);
element.innerHTML = '🤍';
element.title = '添加到收藏';
showToast('已取消收藏 ' + foodName, 'info');
} else {
// 添加收藏
favorites.push(foodName);
element.innerHTML = '❤️';
element.title = '取消收藏';
showToast('已收藏 ' + foodName, 'success');
}
localStorage.setItem('foodFavorites', JSON.stringify(favorites));
}
// 美食评分功能
function rateFoodItem(element, rating) {
const ratingContainer = element.parentElement;
const stars = ratingContainer.querySelectorAll('span');
// 更新星星显示
stars.forEach((star, index) => {
if (index < rating) {
star.style.color = '#ffc107';
star.innerHTML = '⭐';
} else {
star.style.color = '#ddd';
star.innerHTML = '☆';
}
});
// 保存评分到本地存储
const foodCard = element.closest('.food-card-item');
const foodName = foodCard.querySelector('h3').textContent;
let ratings = JSON.parse(localStorage.getItem('foodRatings') || '{}');
ratings[foodName] = rating;
localStorage.setItem('foodRatings', JSON.stringify(ratings));
showToast(`已为 ${foodName} 评分 ${rating} 星`, 'success');
}
// 初始化美食筛选功能
function initFoodFilters() {
const filterButtons = document.querySelectorAll('.food-filter-btn');
filterButtons.forEach(btn => {
btn.addEventListener('click', function() {
// 移除所有按钮的激活状态
filterButtons.forEach(b => {
b.classList.remove('active');
b.style.background = 'transparent';
b.style.color = 'var(--primary-orange)';
});
// 激活当前按钮
this.classList.add('active');
this.style.background = 'var(--primary-orange)';
this.style.color = 'white';
const filterType = this.dataset.filter;
filterFoodCards(filterType);
});
});
}
// 筛选美食卡片
function filterFoodCards(filterType) {
const foodCards = document.querySelectorAll('.food-card-item');
foodCards.forEach(card => {
const tags = card.querySelectorAll('span');
let shouldShow = filterType === 'all';
if (!shouldShow) {
tags.forEach(tag => {
const tagText = tag.textContent;
if ((filterType === 'noodles' && tagText.includes('面食类')) ||
(filterType === 'meat' && tagText.includes('肉食类')) ||
(filterType === 'snacks' && tagText.includes('小食类')) ||
(filterType === 'specialty' && tagText.includes('特产类'))) {
shouldShow = true;
}
});
}
if (shouldShow) {
card.style.display = 'flex';
card.style.animation = 'fadeInUp 0.5s ease-out';
} else {
card.style.display = 'none';
}
});
}
// 显示提示消息
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#4CAF50' : type === 'info' ? '#2196F3' : '#ff9800'};
color: white;
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
font-size: 14px;
animation: slideInRight 0.3s ease-out;
max-width: 300px;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOutRight 0.3s ease-out';
setTimeout(() => {
if (document.body.contains(toast)) {
document.body.removeChild(toast);
}
}, 300);
}, 2000);
}
// 加载用户的收藏和评分
function loadUserPreferences() {
const favorites = JSON.parse(localStorage.getItem('foodFavorites') || '[]');
const ratings = JSON.parse(localStorage.getItem('foodRatings') || '{}');
// 恢复收藏状态
document.querySelectorAll('[onclick*="toggleFoodFavorite"]').forEach(btn => {
const foodName = btn.getAttribute('onclick').match(/'([^']+)'/)[1];
if (favorites.includes(foodName)) {
btn.innerHTML = '❤️';
btn.title = '取消收藏';
}
});
// 恢复评分状态
Object.keys(ratings).forEach(foodName => {
const rating = ratings[foodName];
const foodCards = document.querySelectorAll('.food-card-item');
foodCards.forEach(card => {
const cardTitle = card.querySelector('h3').textContent;
if (cardTitle === foodName) {
const stars = card.querySelectorAll('.food-rating span');
stars.forEach((star, index) => {
if (index < rating) {
star.style.color = '#ffc107';
star.innerHTML = '⭐';
} else {
star.style.color = '#ddd';
star.innerHTML = '☆';
}
});
}
});
});
}
// 完善的美食推荐功能
function showFoodRecommendations() {
// 创建美食推荐弹窗
const foodModal = document.createElement('div');
foodModal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease;
`;
const foodContent = document.createElement('div');
foodContent.style.cssText = `
background: white;
border-radius: 15px;
padding: 30px;
max-width: 650px;
max-height: 85vh;
overflow-y: auto;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
position: relative;
animation: slideUp 0.4s ease;
margin: 20px;
`;
foodContent.innerHTML = `
<div style="text-align: center; margin-bottom: 25px;">
<h2 style="color: #333; margin: 0 0 10px 0; font-size: 28px; font-weight: 600;">🍜 山西美食推荐</h2>
<p style="color: #666; margin: 0; font-size: 16px;">品味三晋大地的经典美味</p>
</div>
<div style="display: grid; gap: 20px;">
<div class="food-item" style="display: flex; gap: 15px; padding: 20px; border-radius: 12px; background: linear-gradient(135deg, #fff5f5, #fff0f0); border: 1px solid #ffe0e0; transition: all 0.3s ease; cursor: pointer;">
<div style="font-size: 50px; flex-shrink: 0; filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1));">🍜</div>
<div style="flex: 1;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px;">
<h3 style="margin: 0; color: #d32f2f; font-size: 20px; font-weight: 600;">刀削面</h3>
<div style="display: flex; gap: 8px; align-items: center;">
<div class="food-rating" style="display: flex; gap: 2px;">
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 1)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 2)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 3)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 4)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 5)">⭐</span>
</div>
<button style="background: none; border: none; font-size: 18px; cursor: pointer;" onclick="toggleFoodFavorite('刀削面', this)" title="添加到收藏">🤍</button>
</div>
</div>
<p style="margin: 0 0 12px 0; color: #555; font-size: 15px; line-height: 1.5;">山西最著名的面食,面条粗细不均,三角形状,口感筋道爽滑。师傅手持特制刀具,将面团削成面条直接入锅,技艺精湛。</p>
<div style="display: flex; gap: 15px; font-size: 13px;">
<span style="background: #e3f2fd; color: #1976d2; padding: 4px 8px; border-radius: 12px;">💰 15-25元</span>
<span style="background: #f3e5f5; color: #7b1fa2; padding: 4px 8px; border-radius: 12px;">📍 老太原面馆</span>
<span style="background: #e8f5e8; color: #2e7d32; padding: 4px 8px; border-radius: 12px;">🍜 面食类</span>
</div>
</div>
</div>
<div class="food-item" style="display: flex; gap: 15px; padding: 20px; border-radius: 12px; background: linear-gradient(135deg, #fff8e1, #fff3c4); border: 1px solid #ffecb3; transition: all 0.3s ease; cursor: pointer;">
<div style="font-size: 50px; flex-shrink: 0; filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1));">🥩</div>
<div style="flex: 1;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px;">
<h3 style="margin: 0; color: #f57c00; font-size: 20px; font-weight: 600;">过油肉</h3>
<div style="display: flex; gap: 8px; align-items: center;">
<div class="food-rating" style="display: flex; gap: 2px;">
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 1)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 2)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 3)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 4)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 5)">⭐</span>
</div>
<button style="background: none; border: none; font-size: 18px; cursor: pointer;" onclick="toggleFoodFavorite('过油肉', this)" title="添加到收藏">🤍</button>
</div>
</div>
<p style="margin: 0 0 12px 0; color: #555; font-size: 15px; line-height: 1.5;">山西传统名菜,选用优质猪肉片,配以青椒、木耳等,经过油炸后炒制,肉片鲜嫩,酸甜适中,是山西人的家常菜。</p>
<div style="display: flex; gap: 15px; font-size: 13px;">
<span style="background: #e3f2fd; color: #1976d2; padding: 4px 8px; border-radius: 12px;">💰 35-45元</span>
<span style="background: #f3e5f5; color: #7b1fa2; padding: 4px 8px; border-radius: 12px;">📍 晋菜馆</span>
<span style="background: #fff3e0; color: #f57c00; padding: 4px 8px; border-radius: 12px;">🥩 肉食类</span>
</div>
</div>
</div>
<div class="food-item" style="display: flex; gap: 15px; padding: 20px; border-radius: 12px; background: linear-gradient(135deg, #f3e5f5, #e1bee7); border: 1px solid #ce93d8; transition: all 0.3s ease; cursor: pointer;">
<div style="font-size: 50px; flex-shrink: 0; filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1));">🥩</div>
<div style="flex: 1;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px;">
<h3 style="margin: 0; color: #7b1fa2; font-size: 20px; font-weight: 600;">平遥牛肉</h3>
<div style="display: flex; gap: 8px; align-items: center;">
<div class="food-rating" style="display: flex; gap: 2px;">
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 1)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 2)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 3)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 4)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 5)">⭐</span>
</div>
<button style="background: none; border: none; font-size: 18px; cursor: pointer;" onclick="toggleFoodFavorite('平遥牛肉', this)" title="添加到收藏">🤍</button>
</div>
</div>
<p style="margin: 0 0 12px 0; color: #555; font-size: 15px; line-height: 1.5;">平遥古城特产,选用优质黄牛肉,采用传统工艺腌制风干,色泽红润,肉质鲜美,是绝佳的旅游纪念品和下酒菜。</p>
<div style="display: flex; gap: 15px; font-size: 13px;">
<span style="background: #e3f2fd; color: #1976d2; padding: 4px 8px; border-radius: 12px;">💰 50-80元/斤</span>
<span style="background: #f3e5f5; color: #7b1fa2; padding: 4px 8px; border-radius: 12px;">📍 冠云牛肉店</span>
<span style="background: #fce4ec; color: #c2185b; padding: 4px 8px; border-radius: 12px;">🎁 特产类</span>
</div>
</div>
</div>
<div class="food-item" style="display: flex; gap: 15px; padding: 20px; border-radius: 12px; background: linear-gradient(135deg, #e8f5e8, #c8e6c9); border: 1px solid #a5d6a7; transition: all 0.3s ease; cursor: pointer;">
<div style="font-size: 50px; flex-shrink: 0; filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1));">🍲</div>
<div style="flex: 1;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px;">
<h3 style="margin: 0; color: #ef6c00; font-size: 20px; font-weight: 600;">山西老陈醋</h3>
<div style="display: flex; gap: 8px; align-items: center;">
<div class="food-rating" style="display: flex; gap: 2px;">
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 1)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 2)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 3)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 4)">⭐</span>
<span style="color: #ffc107; cursor: pointer;" onclick="rateFoodItem(this, 5)">⭐</span>
</div>
<button style="background: none; border: none; font-size: 18px; cursor: pointer;" onclick="toggleFoodFavorite('山西老陈醋', this)" title="添加到收藏">🤍</button>
</div>
</div>
<p style="margin: 0 0 12px 0; color: #555; font-size: 15px; line-height: 1.5;">中国四大名醋之首,采用传统工艺酿造,酸味纯正,香气浓郁,具有开胃消食的功效,是山西人餐桌必备调料。</p>
<div style="display: flex; gap: 15px; font-size: 13px;">
<span style="background: #e3f2fd; color: #1976d2; padding: 4px 8px; border-radius: 12px;">💰 20-50元/瓶</span>
<span style="background: #f3e5f5; color: #7b1fa2; padding: 4px 8px; border-radius: 12px;">📍 东湖醋园</span>
<span style="background: #fce4ec; color: #c2185b; padding: 4px 8px; border-radius: 12px;">🎁 特产类</span>
</div>
</div>
</div>
<div class="food-item" style="display: flex; gap: 15px; padding: 20px; border-radius: 12px; background: linear-gradient(135deg, #e3f2fd, #bbdefb); border: 1px solid #90caf9; transition: all 0.3s ease; cursor: pointer;">
<div style="font-size: 50px; flex-shrink: 0; filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1));">🥟</div>
<div style="flex: 1;">
<h3 style="margin: 0 0 8px 0; color: #1976d2; font-size: 20px; font-weight: 600;">莜面栲栳栳</h3>
<p style="margin: 0 0 12px 0; color: #555; font-size: 15px; line-height: 1.5;">忻州特色面食,用莜面制作,形似蜂窝,需要特殊技巧搓制。蘸料丰富多样,口感独特,营养价值高。</p>
<div style="display: flex; gap: 15px; font-size: 13px;">
<span style="color: #ff6b6b; background: rgba(255,107,107,0.1); padding: 4px 8px; border-radius: 12px;">📍 忻州、五台山周边</span>
<span style="color: #4caf50; background: rgba(76,175,80,0.1); padding: 4px 8px; border-radius: 12px;">💰 18-28元</span>
</div>
</div>
</div>
<div class="food-item" style="display: flex; gap: 15px; padding: 20px; border-radius: 12px; background: linear-gradient(135deg, #fff3e0, #ffe0b2); border: 1px solid #ffcc02; transition: all 0.3s ease; cursor: pointer;">
<div style="font-size: 50px; flex-shrink: 0; filter: drop-shadow(2px 2px 4px rgba(0,0,0,0.1));">🍯</div>
<div style="flex: 1;">
<h3 style="margin: 0 0 8px 0; color: #ef6c00; font-size: 20px; font-weight: 600;">山西老陈醋</h3>
<p style="margin: 0 0 12px 0; color: #555; font-size: 15px; line-height: 1.5;">中国四大名醋之首,采用传统工艺酿造,酸味纯正,香气浓郁,具有开胃消食的功效,是山西人餐桌必备调料。</p>
<div style="display: flex; gap: 15px; font-size: 13px;">
<span style="color: #ff6b6b; background: rgba(255,107,107,0.1); padding: 4px 8px; border-radius: 12px;">📍 东湖、水塔、紫林品牌</span>
<span style="color: #4caf50; background: rgba(76,175,80,0.1); padding: 4px 8px; border-radius: 12px;">💰 15-50元/瓶</span>
</div>
</div>
</div>
</div>
<div style="margin-top: 30px; padding-top: 25px; border-top: 2px solid #f0f0f0; text-align: center;">
<div style="margin-bottom: 20px;">
<h4 style="color: #333; margin: 0 0 10px 0; font-size: 16px;">🎯 美食攻略小贴士</h4>
<p style="color: #666; font-size: 14px; line-height: 1.5; margin: 0;">
• 建议上午品尝太原头脑,下午体验刀削面<br>
• 平遥牛肉可作为伴手礼带回家<br>
• 山西老陈醋搭配任何面食都很棒
</p>
</div>
<button onclick="this.closest('.food-modal').remove()" style="
background: linear-gradient(135deg, #ff6b6b, #ff8e8e);
color: white;
border: none;
padding: 15px 40px;
border-radius: 30px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.3);
letter-spacing: 1px;
" onmouseover="this.style.transform='translateY(-3px)'; this.style.boxShadow='0 8px 25px rgba(255, 107, 107, 0.4)'" onmouseout="this.style.transform=''; this.style.boxShadow='0 6px 20px rgba(255, 107, 107, 0.3)'">
关闭美食推荐
</button>
</div>
`;
foodModal.className = 'food-modal';
foodModal.appendChild(foodContent);
document.body.appendChild(foodModal);
// 点击背景关闭
foodModal.addEventListener('click', function(e) {
if (e.target === foodModal) {
foodModal.style.animation = 'fadeIn 0.3s ease reverse';
setTimeout(() => foodModal.remove(), 300);
}
});
// 添加食物项目悬停效果
setTimeout(() => {
const foodItems = foodModal.querySelectorAll('.food-item');
foodItems.forEach(item => {
item.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-5px) scale(1.02)';
this.style.boxShadow = '0 10px 30px rgba(0, 0, 0, 0.15)';
});
item.addEventListener('mouseleave', function() {
this.style.transform = '';
this.style.boxShadow = '';
});
});
}, 100);
// 阻止滚动穿透
document.body.style.overflow = 'hidden';
foodModal.addEventListener('remove', () => {
document.body.style.overflow = '';
});
}
// 重新定义美食推荐功能,覆盖之前的重复定义
window.showFoodSection = function() {
// 隐藏所有其他区域
document.querySelectorAll('.section').forEach(section => {
section.style.display = 'none';
});
// 创建美食推荐页面内容
const mainContent = document.querySelector('.container-fluid');
let foodSection = document.getElementById('food-section');
if (!foodSection) {
foodSection = document.createElement('div');
foodSection.id = 'food-section';
foodSection.className = 'section';
foodSection.innerHTML = `
<div class="row">
<div class="col-12">
<h2 class="text-center mb-4">山西特色美食推荐</h2>
<!-- 美食筛选 -->
<div class="row mb-4">
<div class="col-md-6">
<select class="form-select" id="foodTypeFilter">
<option value="">全部美食</option>
<option value="面食">面食</option>
<option value="肉类">肉类</option>
<option value="小吃">小吃</option>
<option value="汤品">汤品</option>
</select>
</div>
<div class="col-md-6">
<input type="text" class="form-control" id="foodSearch" placeholder="搜索美食...">
</div>
</div>
<!-- 美食列表 -->
<div class="row" id="foodList">
<div class="col-md-6 col-lg-4 mb-4" data-type="面食">
<div class="card h-100">
<img src="https://via.placeholder.com/300x200?text=刀削面" class="card-img-top" alt="刀削面">
<div class="card-body">
<h5 class="card-title">刀削面</h5>
<p class="card-text">山西最著名的面食,面条粗细不均,口感筋道,配以各种浇头。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-primary">面食</span>
<div class="rating">
<span class="text-warning">★★★★★</span>
<small class="text-muted">(4.8)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="肉类">
<div class="card h-100">
<img src="https://via.placeholder.com/300x200?text=过油肉" class="card-img-top" alt="过油肉">
<div class="card-body">
<h5 class="card-title">过油肉</h5>
<p class="card-text">山西传统名菜,肉片嫩滑,配菜丰富,口味鲜美。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-success">肉类</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.6)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="小吃">
<div class="card h-100">
<img src="https://via.placeholder.com/300x200?text=平遥牛肉" class="card-img-top" alt="平遥牛肉">
<div class="card-body">
<h5 class="card-title">平遥牛肉</h5>
<p class="card-text">平遥古城特产,色泽红润,肉质鲜嫩,是著名的地方小吃。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-info">小吃</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.5)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="面食">
<div class="card h-100">
<img src="https://via.placeholder.com/300x200?text=猫耳朵" class="card-img-top" alt="猫耳朵">
<div class="card-body">
<h5 class="card-title">猫耳朵</h5>
<p class="card-text">形似猫耳的面食,口感Q弹,是山西特色面食之一。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-primary">面食</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.4)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="汤品">
<div class="card h-100">
<img src="https://via.placeholder.com/300x200?text=羊杂割" class="card-img-top" alt="羊杂割">
<div class="card-body">
<h5 class="card-title">羊杂割</h5>
<p class="card-text">山西传统汤品,营养丰富,味道鲜美,是冬日暖身佳品。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-warning">汤品</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.3)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="小吃">
<div class="card h-100">
<img src="https://via.placeholder.com/300x200?text=太原头脑" class="card-img-top" alt="太原头脑">
<div class="card-body">
<h5 class="card-title">太原头脑</h5>
<p class="card-text">太原特色小吃,由羊肉、藕根、山药等熬制而成,营养丰富。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-info">小吃</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.2)</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
mainContent.appendChild(foodSection);
}
// 显示美食推荐页面
foodSection.style.display = 'block';
// 添加筛选功能
const typeFilter = document.getElementById('foodTypeFilter');
const searchInput = document.getElementById('foodSearch');
function filterFood() {
const selectedType = typeFilter.value;
const searchTerm = searchInput.value.toLowerCase();
const foodItems = document.querySelectorAll('#foodList .col-md-6');
foodItems.forEach(item => {
const type = item.getAttribute('data-type');
const title = item.querySelector('.card-title').textContent.toLowerCase();
const description = item.querySelector('.card-text').textContent.toLowerCase();
const typeMatch = !selectedType || type === selectedType;
const searchMatch = !searchTerm || title.includes(searchTerm) || description.includes(searchTerm);
if (typeMatch && searchMatch) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
if (typeFilter && searchInput) {
typeFilter.addEventListener('change', filterFood);
searchInput.addEventListener('input', filterFood);
}
}
// 强制覆盖所有重复的函数定义
window.showFoodSection = function() {
console.log('执行美食推荐功能...');
// 隐藏所有其他区域
document.querySelectorAll('.section').forEach(section => {
section.style.display = 'none';
});
// 隐藏首页内容
const homeSection = document.getElementById('home');
if (homeSection) homeSection.style.display = 'none';
// 创建美食推荐页面内容
const mainContent = document.querySelector('.container-fluid');
let foodSection = document.getElementById('food-section');
if (!foodSection) {
foodSection = document.createElement('div');
foodSection.id = 'food-section';
foodSection.className = 'section';
foodSection.style.display = 'block';
foodSection.innerHTML = `
<div class="row">
<div class="col-12">
<h2 class="text-center mb-4" style="color: #d4a574;">🍜 山西特色美食推荐</h2>
<!-- 美食筛选 -->
<div class="row mb-4">
<div class="col-md-6">
<select class="form-select" id="foodTypeFilter">
<option value="">全部美食</option>
<option value="面食">面食</option>
<option value="肉类">肉类</option>
<option value="小吃">小吃</option>
<option value="汤品">汤品</option>
</select>
</div>
<div class="col-md-6">
<input type="text" class="form-control" id="foodSearch" placeholder="搜索美食...">
</div>
</div>
<!-- 美食列表 -->
<div class="row" id="foodList">
<div class="col-md-6 col-lg-4 mb-4" data-type="面食">
<div class="card h-100 shadow-sm">
<img src="https://via.placeholder.com/300x200/d4a574/ffffff?text=刀削面" class="card-img-top" alt="刀削面">
<div class="card-body">
<h5 class="card-title">刀削面</h5>
<p class="card-text">山西最著名的面食,面条粗细不均,口感筋道,配以各种浇头。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-primary">面食</span>
<div class="rating">
<span class="text-warning">★★★★★</span>
<small class="text-muted">(4.8)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="肉类">
<div class="card h-100 shadow-sm">
<img src="https://via.placeholder.com/300x200/d4a574/ffffff?text=过油肉" class="card-img-top" alt="过油肉">
<div class="card-body">
<h5 class="card-title">过油肉</h5>
<p class="card-text">山西传统名菜,肉片嫩滑,配菜丰富,口味鲜美。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-success">肉类</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.6)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="小吃">
<div class="card h-100 shadow-sm">
<img src="https://via.placeholder.com/300x200/d4a574/ffffff?text=平遥牛肉" class="card-img-top" alt="平遥牛肉">
<div class="card-body">
<h5 class="card-title">平遥牛肉</h5>
<p class="card-text">平遥古城特产,色泽红润,肉质鲜嫩,是著名的地方小吃。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-info">小吃</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.5)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="面食">
<div class="card h-100 shadow-sm">
<img src="https://via.placeholder.com/300x200/d4a574/ffffff?text=猫耳朵" class="card-img-top" alt="猫耳朵">
<div class="card-body">
<h5 class="card-title">猫耳朵</h5>
<p class="card-text">形似猫耳的面食,口感Q弹,是山西特色面食之一。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-primary">面食</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.4)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="汤品">
<div class="card h-100 shadow-sm">
<img src="https://via.placeholder.com/300x200/d4a574/ffffff?text=羊杂割" class="card-img-top" alt="羊杂割">
<div class="card-body">
<h5 class="card-title">羊杂割</h5>
<p class="card-text">山西传统汤品,营养丰富,味道鲜美,是冬日暖身佳品。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-warning">汤品</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.3)</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4" data-type="小吃">
<div class="card h-100 shadow-sm">
<img src="https://via.placeholder.com/300x200/d4a574/ffffff?text=太原头脑" class="card-img-top" alt="太原头脑">
<div class="card-body">
<h5 class="card-title">太原头脑</h5>
<p class="card-text">太原特色小吃,由羊肉、藕根、山药等熬制而成,营养丰富。</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-info">小吃</span>
<div class="rating">
<span class="text-warning">★★★★☆</span>
<small class="text-muted">(4.2)</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
mainContent.appendChild(foodSection);
// 添加筛选功能
setTimeout(() => {
const typeFilter = document.getElementById('foodTypeFilter');
const searchInput = document.getElementById('foodSearch');
function filterFood() {
const selectedType = typeFilter ? typeFilter.value : '';
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
const foodItems = document.querySelectorAll('#foodList .col-md-6');
foodItems.forEach(item => {
const type = item.getAttribute('data-type');
const title = item.querySelector('.card-title').textContent.toLowerCase();
const description = item.querySelector('.card-text').textContent.toLowerCase();
const typeMatch = !selectedType || type === selectedType;
const searchMatch = !searchTerm || title.includes(searchTerm) || description.includes(searchTerm);
if (typeMatch && searchMatch) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
if (typeFilter) typeFilter.addEventListener('change', filterFood);
if (searchInput) searchInput.addEventListener('input', filterFood);
}, 100);
} else {
foodSection.style.display = 'block';
}
console.log('✅ 美食推荐页面已显示');
};
// 页面加载完成后的初始化
$(document).ready(function() {
console.log('页面加载完成,初始化美食推荐功能...');
// 重新绑定所有美食推荐相关的点击事件
$('a[href="#food"], .nav-link[href="#food"], [onclick*="showSection(\'food\')"]').off('click').on('click', function(e) {
e.preventDefault();
console.log('点击美食推荐按钮');
window.showFoodSection();
});
// 也绑定导航栏中的美食推荐链接
$('.navbar-nav a').each(function() {
if ($(this).text().includes('美食推荐')) {
$(this).off('click').on('click', function(e) {
e.preventDefault();
console.log('点击导航栏美食推荐');
window.showFoodSection();
});
}
});
console.log('✅ 美食推荐功能初始化完成');
});
</script>
</body>
</html>