从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:访问阿里云百炼平台(bailian.console.aliyun.com/?tab=app#/a…) 或魔搭平台(modelscope.cn/models) 来开通服务和获取 API Key。(以阿里云百炼平台为例)
(2)安装Node.js 版本:必须安装 Node.js 20 或更高版本。
访问Node.js — Download Node.js® 官方网站的下载页面,选择平台为 Windows 并选择下载并安装(推荐 LTS 版本)
**安装验证:**安装完成后,打开命令提示符(CMD)并运行以下命令,以确认 Node.js 和 npm(Node.js 包管理器)已成功安装。
node -v
npm -v
(3)安装Qwen-Code:
打开命令提示符(CMD)并运行以下命令:
npm install -g @qwen-code/qwen-code
安装完成后,通过以下命令验证安装是否成功:
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)并运行以下命令(先到指定文件夹,再启动):
cd 路径
qwen
4.2 第二步 配置高德MCP
(1)访问高德开放平台获取你自己的API密钥。
(2)根据以下配置MCP:
{
"mcpServers": {
"gaode": {
"command": "cmd",
"args": [
"/c",
"npx",
"@wopal/mcp-gaode-maps"
],
"env": {
"GAODE_API_KEY": "您的高德地图API密钥"
}
}
}
}
4.3 实战开发数字文旅——构建国庆山西游
我们以“5日游:大同—太原—平遥”为主线,组合规划、路线、天气与POI功能,产出可运行原型页面。
以下为用于驱动 Qwen3-Coder 的首轮提示词(内含示例 Key 字段,按需替换):
我想设计一个项目用高德MCP 助力数字文旅建造——国庆山西游:
背景需求:
- 国庆的来临,欢迎大家来山西游
要求:
- 帮我规划一条山西5日游路线,包括大同,太原,平遥等,并提供可视化地图,考虑交通时间和各景点的游览时长
- 请查询各个风景区的开放时间、门票价格
- 请推荐各个风景区附近评分最高的餐厅
- 请查询山西那5日的天气情况,并给出适合的旅游活动建议
- 每个对方加入可以直接位置导航
界面与体验要求:
- 可视化地图使用内嵌,并且展示各景点之间的顺序以及路线,"GAODE_API_KEY": "示例 Key "
- 卡通风格:界面色彩明亮,字体圆润可爱,符合大众审美。
- 在网站最下面:山西国庆5日游旅行助手 © 2025 制作者:LucianaiB
- 无需联网,纯本地运行(HTML + CSS + JS 单文件)
技术要求:
- 使用 HTML、CSS、JavaScript 单文件实现
- 调用高德MCP作为地点路线支持
- 不依赖任何外部库或服务器
请提供一个开箱即用的解决方案,包含全部代码和使用指南。
提示词投喂与结果观察:
开始漫长的测试与优化中...
效果展示:
5.全文总结
5.1 用户旅程与交互流
journey
title 山西5日游用户旅程
section 出行前
搜集目的地信息: 3: 游客
生成初版行程: 4: Qwen3-Coder
section 出行中
路线导航与换乘: 4: 高德MCP
附近餐饮推荐: 3: 高德MCP
天气联动调整: 3: 系统
section 出行后
评价与复盘: 2: 游客
数据沉淀与优化: 4: 系统
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.完整代码
<!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;
});
}
// 完善的美食推荐功能
function showFoodRecommendations() {
// 创建美食推荐弹窗
✅ 美食推荐功能初始化完成');
});
</script>
</body>
</html>