|
|
|
|
@ -1,8 +1,15 @@
|
|
|
|
|
// 导出可交互的 HTML 行程文件
|
|
|
|
|
// 导出可交互的 HTML 行程文件(包含地图和站点交互,样式与工作台一致)
|
|
|
|
|
export function exportToHtml(scheme, filename = null) {
|
|
|
|
|
if (!scheme) return
|
|
|
|
|
|
|
|
|
|
const html = generateHtml(scheme)
|
|
|
|
|
// 使用完整的 points 数据(包含 schedule, foods, waypoints, gallery 等)
|
|
|
|
|
const points = scheme.points || []
|
|
|
|
|
if (!points || points.length === 0) {
|
|
|
|
|
console.error('导出失败:没有站点数据')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const html = generateHtml(scheme, points)
|
|
|
|
|
const blob = new Blob([html], { type: 'text/html;charset=utf-8' })
|
|
|
|
|
const url = URL.createObjectURL(blob)
|
|
|
|
|
const link = document.createElement('a')
|
|
|
|
|
@ -14,25 +21,102 @@ export function exportToHtml(scheme, filename = null) {
|
|
|
|
|
URL.revokeObjectURL(url)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateHtml(scheme) {
|
|
|
|
|
const daysDetail = scheme.daysDetail || []
|
|
|
|
|
const points = []
|
|
|
|
|
|
|
|
|
|
// 从 daysDetail 中提取站点信息
|
|
|
|
|
daysDetail.forEach((day, idx) => {
|
|
|
|
|
points.push({
|
|
|
|
|
name: day.location || `Day ${idx + 1}`,
|
|
|
|
|
day: `Day ${idx + 1}`,
|
|
|
|
|
desc: day.desc || '',
|
|
|
|
|
km: day.km || '—',
|
|
|
|
|
driveTime: day.driveTime || '—',
|
|
|
|
|
schedule: day.schedule || [],
|
|
|
|
|
foods: day.foods || [],
|
|
|
|
|
waypoints: day.waypoints || [],
|
|
|
|
|
hotel: day.hotel || ''
|
|
|
|
|
// 检查 URL 是否安全
|
|
|
|
|
function isSafeUrl(url) {
|
|
|
|
|
if (!url) return false
|
|
|
|
|
return url.startsWith('http://') || url.startsWith('https://')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 过滤安全的图片 URL
|
|
|
|
|
function getSafeGallery(gallery) {
|
|
|
|
|
return (gallery || []).filter(g => isSafeUrl(g.url))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成站点详情 HTML(用于初始化页面)
|
|
|
|
|
function generatePointDetail(p) {
|
|
|
|
|
let html = ''
|
|
|
|
|
|
|
|
|
|
// 统计信息
|
|
|
|
|
html += `<div class="wm-stats">
|
|
|
|
|
<div class="wm-stat"><div class="ws-val">${p.km || '—'}</div><div class="ws-lbl">行驶里程</div></div>
|
|
|
|
|
<div class="wm-stat"><div class="ws-val">${p.driveTime || '—'}</div><div class="ws-lbl">驾驶时间</div></div>
|
|
|
|
|
<div class="wm-stat"><div class="ws-val">${p.day || 'Day 1'}</div><div class="ws-lbl">第几天</div></div>
|
|
|
|
|
</div>`
|
|
|
|
|
|
|
|
|
|
// 描述
|
|
|
|
|
if (p.desc) {
|
|
|
|
|
html += `<div class="wm-sec"><p style="font-size:13px;color:#636e72;line-height:1.5;">${p.desc}</p></div>`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 照片画廊
|
|
|
|
|
const safeGallery = getSafeGallery(p.gallery);
|
|
|
|
|
if (safeGallery.length > 0) {
|
|
|
|
|
html += '<div class="wm-sec photo-gallery"><h4>📸 风景照片</h4><div class="gallery-grid">'
|
|
|
|
|
safeGallery.forEach(g => {
|
|
|
|
|
html += `<div class="gallery-item"><img src="${g.url}" alt="${g.title}" loading="lazy" crossorigin="anonymous" /><div class="gallery-caption">${g.title}</div></div>`
|
|
|
|
|
})
|
|
|
|
|
html += '</div></div>'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 途径推荐
|
|
|
|
|
if (p.waypoints && p.waypoints.length > 0) {
|
|
|
|
|
html += '<div class="wm-sec"><h4>🗺️ 途径推荐</h4><div class="waypoint-list">'
|
|
|
|
|
p.waypoints.forEach(wp => {
|
|
|
|
|
html += `<div class="waypoint-item">
|
|
|
|
|
<div class="wp-icon">${wp.icon || '📍'}</div>
|
|
|
|
|
<div class="wp-info"><div class="wp-name">${wp.name}</div><div class="wp-desc">${wp.desc || ''}</div></div>
|
|
|
|
|
</div>`
|
|
|
|
|
})
|
|
|
|
|
html += '</div></div>'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 行程安排
|
|
|
|
|
if (p.schedule && p.schedule.length > 0) {
|
|
|
|
|
html += '<div class="wm-sec"><h4>📅 行程安排</h4>'
|
|
|
|
|
p.schedule.forEach(s => {
|
|
|
|
|
html += `<div class="wm-si">
|
|
|
|
|
<span class="si-time">${s.time}</span>
|
|
|
|
|
<div class="si-content">
|
|
|
|
|
<div class="si-title">${s.title || s.content || ''}</div>
|
|
|
|
|
${s.desc ? `<div class="si-desc">${s.desc}</div>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>`
|
|
|
|
|
})
|
|
|
|
|
html += '</div>'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 美食推荐
|
|
|
|
|
if (p.foods && p.foods.length > 0) {
|
|
|
|
|
html += '<div class="wm-sec"><h4>🍽️ 美食推荐</h4><div class="foods-grid">'
|
|
|
|
|
p.foods.forEach(f => {
|
|
|
|
|
const name = typeof f === 'string' ? f : (f.name || '')
|
|
|
|
|
const icon = typeof f === 'object' ? (f.icon || '🍜') : '🍜'
|
|
|
|
|
const desc = typeof f === 'object' ? (f.desc || '') : ''
|
|
|
|
|
html += `<div class="food-item">
|
|
|
|
|
<div class="food-icon">${icon}</div>
|
|
|
|
|
<div class="food-info">
|
|
|
|
|
<div class="food-name">${name}</div>
|
|
|
|
|
${desc ? `<div class="food-desc">${desc}</div>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
</div>`
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
html += '</div></div>'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 住宿推荐
|
|
|
|
|
if (p.hotel) {
|
|
|
|
|
html += `<div class="wm-sec"><h4>🏨 住宿推荐</h4><div class="hotel-info">${p.hotel}</div></div>`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 注意事项
|
|
|
|
|
if (p.tips) {
|
|
|
|
|
html += `<div class="wm-sec"><h4>💡 注意事项</h4><div class="wm-tips">${p.tips}</div></div>`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return html
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateHtml(scheme, points) {
|
|
|
|
|
return `<!DOCTYPE html>
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
<head>
|
|
|
|
|
@ -41,153 +125,467 @@ function generateHtml(scheme) {
|
|
|
|
|
<title>${scheme.name || '行程规划'}</title>
|
|
|
|
|
<style>
|
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f5f7fa; color: #2d3436; line-height: 1.6; }
|
|
|
|
|
.container { max-width: 900px; margin: 0 auto; padding: 24px; }
|
|
|
|
|
.header { background: linear-gradient(135deg, #6c5ce7, #a29bfe); color: #fff; padding: 32px; border-radius: 16px; margin-bottom: 24px; }
|
|
|
|
|
.header h1 { font-size: 24px; margin-bottom: 12px; }
|
|
|
|
|
.meta { display: flex; gap: 16px; flex-wrap: wrap; font-size: 14px; opacity: 0.9; }
|
|
|
|
|
.meta span { background: rgba(255,255,255,0.2); padding: 4px 12px; border-radius: 6px; }
|
|
|
|
|
.highlights { background: #fff; padding: 20px; border-radius: 12px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
|
|
|
|
|
.highlights h2 { font-size: 18px; margin-bottom: 12px; color: #6c5ce7; }
|
|
|
|
|
.highlights ul { padding-left: 20px; }
|
|
|
|
|
.highlights li { margin-bottom: 6px; }
|
|
|
|
|
.tabs { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; }
|
|
|
|
|
.tab { padding: 10px 20px; background: #fff; border: 2px solid #eee; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.2s; }
|
|
|
|
|
.tab:hover { border-color: #6c5ce7; color: #6c5ce7; }
|
|
|
|
|
.tab.active { background: #6c5ce7; color: #fff; border-color: #6c5ce7; }
|
|
|
|
|
.day-content { background: #fff; padding: 24px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); display: none; }
|
|
|
|
|
.day-content.active { display: block; }
|
|
|
|
|
.day-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #eee; }
|
|
|
|
|
.day-badge { background: #6c5ce7; color: #fff; padding: 4px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; }
|
|
|
|
|
.day-title { font-size: 18px; font-weight: 700; }
|
|
|
|
|
.day-km { margin-left: auto; font-size: 13px; color: #999; }
|
|
|
|
|
.schedule { margin-bottom: 20px; }
|
|
|
|
|
.schedule-item { display: flex; gap: 12px; margin-bottom: 12px; padding: 12px; background: #f8f9fa; border-radius: 8px; }
|
|
|
|
|
.schedule-time { font-size: 13px; font-weight: 600; color: #6c5ce7; min-width: 60px; }
|
|
|
|
|
.schedule-content { flex: 1; }
|
|
|
|
|
.schedule-content strong { display: block; margin-bottom: 4px; }
|
|
|
|
|
.schedule-content p { font-size: 14px; color: #636e72; margin: 0; }
|
|
|
|
|
.waypoints { margin-bottom: 20px; }
|
|
|
|
|
.waypoints h3 { font-size: 15px; margin-bottom: 10px; color: #636e72; }
|
|
|
|
|
.waypoint-list { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
|
|
|
.waypoint-tag { background: #fff; border: 1px solid #dfe6e9; padding: 6px 12px; border-radius: 8px; font-size: 13px; }
|
|
|
|
|
.foods { margin-bottom: 20px; }
|
|
|
|
|
.foods h3 { font-size: 15px; margin-bottom: 10px; color: #636e72; }
|
|
|
|
|
.food-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
|
|
|
|
|
.food-item { display: flex; align-items: center; gap: 8px; padding: 10px; background: #f8f9fa; border-radius: 8px; }
|
|
|
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f5f7fa; color: #2d3436; height: 100vh; overflow: hidden; }
|
|
|
|
|
|
|
|
|
|
/* 工作台三栏布局 */
|
|
|
|
|
.wb-layout { display: flex; height: 100vh; }
|
|
|
|
|
|
|
|
|
|
/* 左侧时间线 */
|
|
|
|
|
.wb-left { width: 220px; background: #2d3436; color: #fff; padding: 16px 0; overflow-y: auto; flex-shrink: 0; }
|
|
|
|
|
.wb-tl-header { font-size: 13px; padding: 8px 16px; color: #b2bec3; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; }
|
|
|
|
|
.wb-tl-item { padding: 10px 16px; cursor: pointer; display: flex; gap: 12px; transition: all 0.2s; position: relative; }
|
|
|
|
|
.wb-tl-item:hover { transform: translateX(4px); background: rgba(108,92,231,0.2); }
|
|
|
|
|
.wb-tl-item.active { background: rgba(108,92,231,0.4); }
|
|
|
|
|
.wb-tl-item.active::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: #6c5ce7; }
|
|
|
|
|
.wb-tl-dot { width: 12px; height: 12px; border-radius: 50%; border: 3px solid #6c5ce7; background: #fff; margin-top: 2px; flex-shrink: 0; position: relative; transition: all 0.3s; }
|
|
|
|
|
.wb-tl-item.active .wb-tl-dot { transform: scale(1.3); box-shadow: 0 0 0 4px rgba(108,92,231,0.3); background: #6c5ce7; }
|
|
|
|
|
.wb-tl-item.completed .wb-tl-dot { background: #6c5ce7; border-color: #6c5ce7; }
|
|
|
|
|
.wb-tl-item.completed .wb-tl-dot::after { content: "✓"; color: #fff; font-size: 8px; display: flex; align-items: center; justify-content: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
|
|
|
|
.wb-tl-info { flex: 1; min-width: 0; }
|
|
|
|
|
.wb-tl-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
|
|
|
.wb-tl-day { font-size: 11px; color: #b2bec3; margin-top: 2px; }
|
|
|
|
|
.wb-tl-item.active .wb-tl-name { color: #6c5ce7; }
|
|
|
|
|
|
|
|
|
|
/* 中间详情面板 */
|
|
|
|
|
.wb-mid { width: 340px; background: #fff; overflow-y: auto; border-right: 1px solid #e9ecef; flex-shrink: 0; }
|
|
|
|
|
.wm-header { padding: 16px; border-bottom: 1px solid #e9ecef; background: #f8f9fa; }
|
|
|
|
|
.wm-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
|
|
|
|
|
.wm-subtitle { font-size: 12px; color: #636e72; }
|
|
|
|
|
.wm-body { padding: 16px; }
|
|
|
|
|
.wm-sec { margin-bottom: 20px; }
|
|
|
|
|
.wm-sec h4 { font-size: 14px; color: #6c5ce7; margin-bottom: 10px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
|
|
|
|
|
.wm-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; }
|
|
|
|
|
.wm-stat { background: #f8f9fa; padding: 10px; border-radius: 8px; text-align: center; }
|
|
|
|
|
.ws-val { font-size: 16px; font-weight: 700; color: #6c5ce7; }
|
|
|
|
|
.ws-lbl { font-size: 11px; color: #636e72; margin-top: 2px; }
|
|
|
|
|
|
|
|
|
|
/* 日程安排 */
|
|
|
|
|
.wm-si { display: flex; gap: 12px; margin-bottom: 10px; padding: 10px; background: #f8f9fa; border-radius: 8px; }
|
|
|
|
|
.si-time { font-size: 12px; font-weight: 600; color: #6c5ce7; min-width: 45px; }
|
|
|
|
|
.si-content { flex: 1; }
|
|
|
|
|
.si-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
|
|
|
|
.si-desc { font-size: 13px; color: #636e72; line-height: 1.4; }
|
|
|
|
|
|
|
|
|
|
/* 途径推荐 */
|
|
|
|
|
.waypoint-list { display: flex; flex-direction: column; gap: 8px; }
|
|
|
|
|
.waypoint-item { display: flex; gap: 10px; padding: 10px; background: #f8f9fa; border-radius: 8px; }
|
|
|
|
|
.wp-icon { font-size: 20px; flex-shrink: 0; }
|
|
|
|
|
.wp-info { flex: 1; }
|
|
|
|
|
.wp-name { font-size: 14px; font-weight: 600; margin-bottom: 2px; }
|
|
|
|
|
.wp-desc { font-size: 12px; color: #636e72; }
|
|
|
|
|
|
|
|
|
|
/* 美食推荐 */
|
|
|
|
|
.foods-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
|
|
|
.food-item { display: flex; gap: 8px; padding: 10px; background: #f8f9fa; border-radius: 8px; align-items: center; }
|
|
|
|
|
.food-icon { font-size: 20px; }
|
|
|
|
|
.hotel { background: #f0f4ff; padding: 14px; border-radius: 8px; border-left: 3px solid #6c5ce7; font-size: 14px; color: #636e72; }
|
|
|
|
|
.footer { text-align: center; margin-top: 32px; padding: 20px; color: #999; font-size: 12px; }
|
|
|
|
|
@media (max-width: 600px) {
|
|
|
|
|
.container { padding: 16px; }
|
|
|
|
|
.header { padding: 20px; }
|
|
|
|
|
.header h1 { font-size: 20px; }
|
|
|
|
|
.meta { flex-direction: column; gap: 8px; }
|
|
|
|
|
.tabs { gap: 6px; }
|
|
|
|
|
.tab { padding: 8px 14px; font-size: 13px; }
|
|
|
|
|
.day-content { padding: 16px; }
|
|
|
|
|
.food-list { grid-template-columns: 1fr; }
|
|
|
|
|
.food-info { flex: 1; }
|
|
|
|
|
.food-name { font-size: 13px; font-weight: 600; }
|
|
|
|
|
.food-desc { font-size: 12px; color: #636e72; margin-top: 2px; }
|
|
|
|
|
|
|
|
|
|
/* 照片画廊 */
|
|
|
|
|
.photo-gallery { margin-bottom: 16px; }
|
|
|
|
|
.photo-gallery h4 { font-size: 14px; color: #6c5ce7; margin-bottom: 10px; }
|
|
|
|
|
.gallery-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
|
|
|
.gallery-item { border-radius: 8px; overflow: hidden; position: relative; aspect-ratio: 4/3; background: linear-gradient(135deg, #e8e4ff, #d4c8ff); }
|
|
|
|
|
.gallery-item img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.2s; }
|
|
|
|
|
.gallery-item:hover img { transform: scale(1.05); }
|
|
|
|
|
.gallery-caption { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,0.7)); color: #fff; padding: 6px 8px; font-size: 11px; }
|
|
|
|
|
|
|
|
|
|
/* 住宿和提示 */
|
|
|
|
|
.hotel-info { background: #e8e4ff; padding: 10px; border-radius: 8px; border-left: 3px solid #6c5ce7; font-size: 13px; color: #6c5ce7; }
|
|
|
|
|
.wm-tips { background: #fff8e1; padding: 10px; border-radius: 8px; border-left: 3px solid #f39c12; font-size: 13px; color: #636e72; line-height: 1.5; }
|
|
|
|
|
|
|
|
|
|
.empty-state { text-align: center; padding: 40px 20px; color: #b2bec3; }
|
|
|
|
|
.empty-icon { font-size: 48px; display: block; margin-bottom: 10px; }
|
|
|
|
|
|
|
|
|
|
/* 右侧地图 */
|
|
|
|
|
.wb-right { flex: 1; position: relative; }
|
|
|
|
|
#map-container { width: 100%; height: 100%; position: relative; overflow: hidden; background: linear-gradient(135deg, #e8e4ff 0%, #d4c8ff 100%); }
|
|
|
|
|
#static-map { width: 100%; height: 100%; object-fit: contain; display: block; }
|
|
|
|
|
#map-markers { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
|
|
|
|
|
#map-markers > * { pointer-events: auto; }
|
|
|
|
|
|
|
|
|
|
/* 地图控制 */
|
|
|
|
|
.map-controls { position: absolute; top: 12px; right: 12px; z-index: 1000; display: flex; flex-direction: column; gap: 8px; }
|
|
|
|
|
.route-toggle { background: #fff; border: 1px solid #e9ecef; padding: 8px 14px; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 13px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: all 0.2s; }
|
|
|
|
|
.route-toggle:hover { border-color: #6c5ce7; color: #6c5ce7; }
|
|
|
|
|
.route-toggle.active { background: #6c5ce7; color: #fff; border-color: #6c5ce7; }
|
|
|
|
|
|
|
|
|
|
/* 自定义标记 */
|
|
|
|
|
.marker-pin { width: 36px; height: 36px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); display: flex; align-items: center; justify-content: center; box-shadow: 0 3px 8px rgba(0,0,0,.25); cursor: pointer; transition: transform .2s; }
|
|
|
|
|
.marker-pin:hover { transform: rotate(-45deg) scale(1.15); }
|
|
|
|
|
.marker-pin span { transform: rotate(45deg); color: #fff; font-weight: 800; font-size: 13px; }
|
|
|
|
|
${points.map((p, i) => `.marker-pin.d${i} { background: ${['#6c757d', '#2196F3', '#4CAF50', '#FF9800', '#E91E63', '#9C27B0', '#00BCD4', '#FFC107', '#795548', '#607D8B'][i % 10]}; }`).join('')}
|
|
|
|
|
.marker-label { background: #fff; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; box-shadow: 0 1px 4px rgba(0,0,0,.15); white-space: nowrap; margin-top: 6px; }
|
|
|
|
|
|
|
|
|
|
/* 汽车标记 */
|
|
|
|
|
.car-marker { z-index: 1000 !important; }
|
|
|
|
|
.car-marker .car-inner { font-size: 28px; filter: drop-shadow(0 2px 4px rgba(0,0,0,.3)); animation: carBounce 1.5s ease infinite; }
|
|
|
|
|
@keyframes carBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
|
|
|
|
|
|
|
|
|
|
/* 加载遮罩 */
|
|
|
|
|
.loading-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; background: #2d3436; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; transition: opacity .5s; }
|
|
|
|
|
.loading-overlay.hidden { opacity: 0; pointer-events: none; }
|
|
|
|
|
.loading-spinner { width: 40px; height: 40px; border: 3px solid rgba(255,255,255,.2); border-top-color: #6c5ce7; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 16px; }
|
|
|
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
|
|
|
|
|
|
|
|
.popup-name { font-weight: 700; font-size: 14px; margin-bottom: 4px; color: #2d3436; }
|
|
|
|
|
.popup-day { color: #6c5ce7; font-size: 12px; font-weight: 600; }
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1000px) {
|
|
|
|
|
.wb-left { width: 180px; }
|
|
|
|
|
.wb-mid { width: 280px; }
|
|
|
|
|
}
|
|
|
|
|
@media (max-width: 800px) {
|
|
|
|
|
.wb-layout { flex-direction: column; }
|
|
|
|
|
.wb-left { width: 100%; height: 80px; display: flex; overflow-x: auto; overflow-y: hidden; padding: 8px; }
|
|
|
|
|
.wb-tl-header { display: none; }
|
|
|
|
|
.wb-tl-item { flex-direction: column; align-items: center; min-width: 70px; }
|
|
|
|
|
.wb-mid { width: 100%; height: 200px; border-right: none; border-top: 1px solid #e9ecef; }
|
|
|
|
|
.wb-right { height: calc(100vh - 280px); }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div class="container">
|
|
|
|
|
<div class="header">
|
|
|
|
|
<h1>${scheme.name || '行程规划'}</h1>
|
|
|
|
|
<div class="meta">
|
|
|
|
|
<span>📍 ${scheme.route || '—'}</span>
|
|
|
|
|
<span>📅 ${scheme.days || '—'}天</span>
|
|
|
|
|
<span>🚗 ~${scheme.totalKm || '—'}km</span>
|
|
|
|
|
<span>💰 ${scheme.budget || '—'}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="loading-overlay" id="loadingOverlay">
|
|
|
|
|
<div class="loading-spinner"></div>
|
|
|
|
|
<div style="font-size:18px;margin-bottom:8px">正在加载地图...</div>
|
|
|
|
|
<div style="font-size:13px;opacity:.7">请稍候</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${scheme.highlights && scheme.highlights.length > 0 ? `
|
|
|
|
|
<div class="highlights">
|
|
|
|
|
<h2>✨ 行程亮点</h2>
|
|
|
|
|
<ul>
|
|
|
|
|
${scheme.highlights.map(h => `<li>${h}</li>`).join('')}
|
|
|
|
|
</ul>
|
|
|
|
|
<div class="wb-layout">
|
|
|
|
|
<!-- 左侧时间线 -->
|
|
|
|
|
<div class="wb-left">
|
|
|
|
|
<div class="wb-tl-header">行程时间线</div>
|
|
|
|
|
<div id="timeline"></div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
<div class="tabs">
|
|
|
|
|
${points.map((p, i) => `
|
|
|
|
|
<div class="tab ${i === 0 ? 'active' : ''}" onclick="showDay(${i})">Day ${i + 1}</div>
|
|
|
|
|
`).join('')}
|
|
|
|
|
<!-- 中间详情面板 -->
|
|
|
|
|
<div class="wb-mid">
|
|
|
|
|
<div class="wm-header">
|
|
|
|
|
<div class="wm-title" id="detail-title">${points[0]?.name || ''}</div>
|
|
|
|
|
<div class="wm-subtitle" id="detail-subtitle">${points[0]?.day || 'Day 1'} · ${points[0]?.km || '—'}km · 驾车${points[0]?.driveTime || '—'}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="wm-body" id="detail-body">
|
|
|
|
|
${generatePointDetail(points[0])}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${points.map((p, i) => `
|
|
|
|
|
<div class="day-content ${i === 0 ? 'active' : ''}" id="day-${i}">
|
|
|
|
|
<div class="day-header">
|
|
|
|
|
<span class="day-badge">Day ${i + 1}</span>
|
|
|
|
|
<span class="day-title">${p.name}</span>
|
|
|
|
|
<span class="day-km">${p.km}km · 驾车${p.driveTime}h</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
${p.desc ? `<p style="margin-bottom:16px;color:#636e72;">${p.desc}</p>` : ''}
|
|
|
|
|
|
|
|
|
|
${p.schedule && p.schedule.length > 0 ? `
|
|
|
|
|
<div class="schedule">
|
|
|
|
|
<h3 style="margin-bottom:12px;">📅 日程安排</h3>
|
|
|
|
|
${p.schedule.map(s => `
|
|
|
|
|
<div class="schedule-item">
|
|
|
|
|
<div class="schedule-time">${s.time || '—'}</div>
|
|
|
|
|
<div class="schedule-content">
|
|
|
|
|
<strong>${s.title || s.content || '—'}</strong>
|
|
|
|
|
<p>${s.desc || ''}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${p.waypoints && p.waypoints.length > 0 ? `
|
|
|
|
|
<div class="waypoints">
|
|
|
|
|
<h3>🗺️ 途径推荐</h3>
|
|
|
|
|
<div class="waypoint-list">
|
|
|
|
|
${p.waypoints.map(wp => `
|
|
|
|
|
<div class="waypoint-tag">${wp.icon || '📍'} ${wp.name}</div>
|
|
|
|
|
`).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${p.foods && p.foods.length > 0 ? `
|
|
|
|
|
<div class="foods">
|
|
|
|
|
<h3>🍽️ 美食推荐</h3>
|
|
|
|
|
<div class="food-list">
|
|
|
|
|
${p.foods.map(f => {
|
|
|
|
|
const name = typeof f === 'string' ? f : (f.name || f)
|
|
|
|
|
const icon = typeof f === 'object' ? (f.icon || '🍜') : '🍜'
|
|
|
|
|
return `<div class="food-item"><span class="food-icon">${icon}</span><span>${name}</span></div>`
|
|
|
|
|
}).join('')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
${p.hotel ? `
|
|
|
|
|
<div class="hotel">🏨 住宿推荐:${p.hotel}</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
<!-- 右侧地图 -->
|
|
|
|
|
<div class="wb-right">
|
|
|
|
|
<div id="map-container">
|
|
|
|
|
<img id="static-map" src="" alt="行程地图" />
|
|
|
|
|
<div id="map-markers"></div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
|
|
|
|
${scheme.tips ? `
|
|
|
|
|
<div style="background:#fff8e1;padding:20px;border-radius:12px;margin-top:20px;border-left:3px solid #f39c12;">
|
|
|
|
|
<h3 style="margin-bottom:8px;">💡 旅行贴士</h3>
|
|
|
|
|
<p>${scheme.tips}</p>
|
|
|
|
|
<div class="map-controls">
|
|
|
|
|
<button class="route-toggle" id="route-toggle" onclick="toggleRoute()">
|
|
|
|
|
🛣️ 显示路线
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
|
|
<div class="footer">由智能行程规划系统生成</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
function showDay(idx) {
|
|
|
|
|
document.querySelectorAll('.day-content').forEach(el => el.classList.remove('active'));
|
|
|
|
|
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
|
|
|
|
|
document.getElementById('day-' + idx).classList.add('active');
|
|
|
|
|
document.querySelectorAll('.tab')[idx].classList.add('active');
|
|
|
|
|
const points = ${JSON.stringify(points.map(p => ({
|
|
|
|
|
name: p.name,
|
|
|
|
|
day: p.day || 'Day 1',
|
|
|
|
|
lat: p.lat || 0,
|
|
|
|
|
lng: p.lng || 0,
|
|
|
|
|
km: p.km || '—',
|
|
|
|
|
driveTime: p.driveTime || '—',
|
|
|
|
|
desc: p.desc || '',
|
|
|
|
|
title: p.title || '',
|
|
|
|
|
content: p.content || '',
|
|
|
|
|
schedule: (p.schedule || []).map(s => ({
|
|
|
|
|
time: s.time || '',
|
|
|
|
|
title: s.title || s.content || '',
|
|
|
|
|
content: s.content || '',
|
|
|
|
|
desc: s.desc || ''
|
|
|
|
|
})),
|
|
|
|
|
waypoints: (p.waypoints || []).map(wp => ({
|
|
|
|
|
name: wp.name || '',
|
|
|
|
|
desc: wp.desc || '',
|
|
|
|
|
icon: wp.icon || '📍',
|
|
|
|
|
km: wp.km || '',
|
|
|
|
|
driveTime: wp.driveTime || ''
|
|
|
|
|
})),
|
|
|
|
|
foods: (p.foods || []).map(f => typeof f === 'string' ? { name: f, icon: '🍜', desc: '' } : {
|
|
|
|
|
name: f.name || '',
|
|
|
|
|
icon: f.icon || '🍜',
|
|
|
|
|
desc: f.desc || ''
|
|
|
|
|
}),
|
|
|
|
|
gallery: (p.gallery || []).map(g => ({
|
|
|
|
|
url: g.url || '',
|
|
|
|
|
title: g.title || ''
|
|
|
|
|
})),
|
|
|
|
|
hotel: p.hotel || '',
|
|
|
|
|
tips: p.tips || ''
|
|
|
|
|
})))};
|
|
|
|
|
|
|
|
|
|
let currentStep = 0;
|
|
|
|
|
let routeVisible = false;
|
|
|
|
|
let routeSvg = null;
|
|
|
|
|
let mapImageLoaded = false;
|
|
|
|
|
|
|
|
|
|
// 计算地图边界
|
|
|
|
|
function calcMapBounds() {
|
|
|
|
|
const validPoints = points.filter(p => p.lat && p.lng);
|
|
|
|
|
if (validPoints.length === 0) return { minLat: 25, maxLat: 25, minLng: 102, maxLng: 102 };
|
|
|
|
|
|
|
|
|
|
let minLat = validPoints[0].lat, maxLat = validPoints[0].lat;
|
|
|
|
|
let minLng = validPoints[0].lng, maxLng = validPoints[0].lng;
|
|
|
|
|
|
|
|
|
|
validPoints.forEach(p => {
|
|
|
|
|
if (p.lat < minLat) minLat = p.lat;
|
|
|
|
|
if (p.lat > maxLat) maxLat = p.lat;
|
|
|
|
|
if (p.lng < minLng) minLng = p.lng;
|
|
|
|
|
if (p.lng > maxLng) maxLng = p.lng;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 添加 10% 边距
|
|
|
|
|
const latPadding = (maxLat - minLat) * 0.1 || 0.5;
|
|
|
|
|
const lngPadding = (maxLng - minLng) * 0.1 || 0.5;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
minLat: minLat - latPadding,
|
|
|
|
|
maxLat: maxLat + latPadding,
|
|
|
|
|
minLng: minLng - lngPadding,
|
|
|
|
|
maxLng: maxLng + lngPadding
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将经纬度转换为图片坐标
|
|
|
|
|
function latLngToPixel(lat, lng, imgWidth, imgHeight, bounds) {
|
|
|
|
|
const x = ((lng - bounds.minLng) / (bounds.maxLng - bounds.minLng)) * imgWidth;
|
|
|
|
|
const y = ((bounds.maxLat - lat) / (bounds.maxLat - bounds.minLat)) * imgHeight;
|
|
|
|
|
return { x, y };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成静态地图 URL(使用静态地图 API)
|
|
|
|
|
function generateStaticMapUrl() {
|
|
|
|
|
const bounds = calcMapBounds();
|
|
|
|
|
const centerLat = (bounds.minLat + bounds.maxLat) / 2;
|
|
|
|
|
const centerLng = (bounds.minLng + bounds.maxLng) / 2;
|
|
|
|
|
|
|
|
|
|
// 计算缩放级别
|
|
|
|
|
const latDiff = bounds.maxLat - bounds.minLat;
|
|
|
|
|
const lngDiff = bounds.maxLng - bounds.minLng;
|
|
|
|
|
const maxDiff = Math.max(latDiff, lngDiff);
|
|
|
|
|
|
|
|
|
|
let zoom = 8;
|
|
|
|
|
if (maxDiff > 10) zoom = 6;
|
|
|
|
|
else if (maxDiff > 5) zoom = 7;
|
|
|
|
|
else if (maxDiff > 2) zoom = 8;
|
|
|
|
|
else if (maxDiff > 1) zoom = 9;
|
|
|
|
|
else zoom = 10;
|
|
|
|
|
|
|
|
|
|
// 使用静态地图 API (HTTPS)
|
|
|
|
|
return 'https://staticmap.openstreetmap.de/staticmap.php?' +
|
|
|
|
|
'center=' + centerLat.toFixed(4) + ',' + centerLng.toFixed(4) +
|
|
|
|
|
'&zoom=' + zoom +
|
|
|
|
|
'&size=800x600' +
|
|
|
|
|
'&maptype=mapnik';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查 URL 是否安全
|
|
|
|
|
function isSafeUrl(url) {
|
|
|
|
|
if (!url) return false;
|
|
|
|
|
// 只允许 http/https 协议的 URL
|
|
|
|
|
return url.startsWith('http://') || url.startsWith('https://');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 过滤安全的图片 URL
|
|
|
|
|
function getSafeGallery(gallery) {
|
|
|
|
|
return (gallery || []).filter(g => isSafeUrl(g.url));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染地图标记
|
|
|
|
|
function renderMarkers() {
|
|
|
|
|
const container = document.getElementById('map-container');
|
|
|
|
|
const img = document.getElementById('static-map');
|
|
|
|
|
const markersDiv = document.getElementById('map-markers');
|
|
|
|
|
|
|
|
|
|
if (!mapImageLoaded) return;
|
|
|
|
|
|
|
|
|
|
const imgWidth = img.naturalWidth || img.offsetWidth;
|
|
|
|
|
const imgHeight = img.naturalHeight || img.offsetHeight;
|
|
|
|
|
|
|
|
|
|
if (imgWidth === 0 || imgHeight === 0) return;
|
|
|
|
|
|
|
|
|
|
const bounds = calcMapBounds();
|
|
|
|
|
markersDiv.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
points.forEach((p, i) => {
|
|
|
|
|
if (!p.lat || !p.lng) return;
|
|
|
|
|
|
|
|
|
|
const pos = latLngToPixel(p.lat, p.lng, imgWidth, imgHeight, bounds);
|
|
|
|
|
|
|
|
|
|
const marker = document.createElement('div');
|
|
|
|
|
marker.className = 'marker-pin d' + i;
|
|
|
|
|
marker.style.position = 'absolute';
|
|
|
|
|
marker.style.left = (pos.x - 18) + 'px';
|
|
|
|
|
marker.style.top = (pos.y - 36) + 'px';
|
|
|
|
|
marker.style.cursor = 'pointer';
|
|
|
|
|
marker.style.zIndex = '100';
|
|
|
|
|
marker.innerHTML = '<span>' + i + '</span>';
|
|
|
|
|
marker.onclick = function() { selectPoint(i); };
|
|
|
|
|
|
|
|
|
|
const label = document.createElement('div');
|
|
|
|
|
label.className = 'marker-label';
|
|
|
|
|
label.style.position = 'absolute';
|
|
|
|
|
label.style.left = (pos.x - 30) + 'px';
|
|
|
|
|
label.style.top = (pos.y + 5) + 'px';
|
|
|
|
|
label.style.textAlign = 'center';
|
|
|
|
|
label.textContent = p.name;
|
|
|
|
|
|
|
|
|
|
markersDiv.appendChild(marker);
|
|
|
|
|
markersDiv.appendChild(label);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 绘制路线
|
|
|
|
|
function drawRoute() {
|
|
|
|
|
const container = document.getElementById('map-container');
|
|
|
|
|
const img = document.getElementById('static-map');
|
|
|
|
|
|
|
|
|
|
if (!mapImageLoaded) return;
|
|
|
|
|
|
|
|
|
|
const imgWidth = img.naturalWidth || img.offsetWidth;
|
|
|
|
|
const imgHeight = img.naturalHeight || img.offsetHeight;
|
|
|
|
|
|
|
|
|
|
if (imgWidth === 0 || imgHeight === 0) return;
|
|
|
|
|
|
|
|
|
|
const bounds = calcMapBounds();
|
|
|
|
|
const validPoints = points.filter(p => p.lat && p.lng);
|
|
|
|
|
|
|
|
|
|
if (validPoints.length < 2) return;
|
|
|
|
|
|
|
|
|
|
// 移除旧路线
|
|
|
|
|
if (routeSvg) routeSvg.remove();
|
|
|
|
|
|
|
|
|
|
// 创建 SVG 覆盖层
|
|
|
|
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
|
|
|
svg.style.position = 'absolute';
|
|
|
|
|
svg.style.top = '0';
|
|
|
|
|
svg.style.left = '0';
|
|
|
|
|
svg.style.width = '100%';
|
|
|
|
|
svg.style.height = '100%';
|
|
|
|
|
svg.style.pointerEvents = 'none';
|
|
|
|
|
svg.style.zIndex = '50';
|
|
|
|
|
|
|
|
|
|
// 创建路线 polyline
|
|
|
|
|
const polylinePoints = validPoints.map(p => {
|
|
|
|
|
const pos = latLngToPixel(p.lat, p.lng, imgWidth, imgHeight, bounds);
|
|
|
|
|
return pos.x + ',' + pos.y;
|
|
|
|
|
}).join(' ');
|
|
|
|
|
|
|
|
|
|
const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
|
|
|
polyline.setAttribute('points', polylinePoints);
|
|
|
|
|
polyline.setAttribute('fill', 'none');
|
|
|
|
|
polyline.setAttribute('stroke', '#6c5ce7');
|
|
|
|
|
polyline.setAttribute('stroke-width', '3');
|
|
|
|
|
polyline.setAttribute('stroke-dasharray', '8,6');
|
|
|
|
|
polyline.setAttribute('stroke-opacity', '0.8');
|
|
|
|
|
|
|
|
|
|
svg.appendChild(polyline);
|
|
|
|
|
container.appendChild(svg);
|
|
|
|
|
routeSvg = svg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideLoading() {
|
|
|
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
|
|
|
if (overlay) overlay.classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initMap() {
|
|
|
|
|
const img = document.getElementById('static-map');
|
|
|
|
|
const mapUrl = generateStaticMapUrl();
|
|
|
|
|
|
|
|
|
|
img.onload = function() {
|
|
|
|
|
mapImageLoaded = true;
|
|
|
|
|
renderMarkers();
|
|
|
|
|
drawRoute();
|
|
|
|
|
hideLoading();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
img.onerror = function() {
|
|
|
|
|
// 如果静态地图加载失败,使用备用方案
|
|
|
|
|
const container = document.getElementById('map-container');
|
|
|
|
|
container.innerHTML = '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#e8e4ff,#d4c8ff);color:#6c5ce7;font-size:16px;">地图加载中...</div>';
|
|
|
|
|
hideLoading();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
img.src = mapUrl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hideLoading() {
|
|
|
|
|
const overlay = document.getElementById('loadingOverlay');
|
|
|
|
|
if (overlay) overlay.classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateTimeline() {
|
|
|
|
|
const tl = document.getElementById('timeline');
|
|
|
|
|
let html = '';
|
|
|
|
|
points.forEach(function(loc, i) {
|
|
|
|
|
const cls = i === currentStep ? 'active' : (i < currentStep ? 'completed' : '');
|
|
|
|
|
html += '<div class="wb-tl-item ' + cls + '" data-idx="' + i + '">';
|
|
|
|
|
html += '<div class="wb-tl-dot"></div>';
|
|
|
|
|
html += '<div class="wb-tl-info">';
|
|
|
|
|
html += '<div class="wb-tl-name">' + loc.name + '</div>';
|
|
|
|
|
html += '<div class="wb-tl-day">' + (loc.km || '—') + 'km · ' + (loc.day || 'Day ' + (i + 1)) + '</div>';
|
|
|
|
|
html += '</div></div>';
|
|
|
|
|
});
|
|
|
|
|
tl.innerHTML = html;
|
|
|
|
|
tl.querySelectorAll('.wb-tl-item').forEach(function(el) {
|
|
|
|
|
el.addEventListener('click', function() {
|
|
|
|
|
selectPoint(parseInt(this.getAttribute('data-idx')));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectPoint(idx) {
|
|
|
|
|
currentStep = idx;
|
|
|
|
|
|
|
|
|
|
updateTimeline();
|
|
|
|
|
|
|
|
|
|
const p = points[idx];
|
|
|
|
|
document.getElementById('detail-title').textContent = p.name;
|
|
|
|
|
document.getElementById('detail-subtitle').textContent = p.day + ' · ' + p.km + 'km · 驾车' + p.driveTime;
|
|
|
|
|
document.getElementById('detail-body').innerHTML = generatePointDetail(p);
|
|
|
|
|
|
|
|
|
|
// 高亮对应标记
|
|
|
|
|
document.querySelectorAll('.marker-pin').forEach((m, i) => {
|
|
|
|
|
m.style.transform = i === idx ? 'rotate(-45deg) scale(1.3)' : 'rotate(-45deg)';
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleRoute() {
|
|
|
|
|
routeVisible = !routeVisible;
|
|
|
|
|
const btn = document.getElementById('route-toggle');
|
|
|
|
|
|
|
|
|
|
if (routeVisible) {
|
|
|
|
|
drawRoute();
|
|
|
|
|
btn.innerHTML = '🛣️ 隐藏路线';
|
|
|
|
|
} else {
|
|
|
|
|
if (routeSvg) {
|
|
|
|
|
routeSvg.remove();
|
|
|
|
|
routeSvg = null;
|
|
|
|
|
}
|
|
|
|
|
btn.innerHTML = '🛣️ 显示路线';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.selectPoint = selectPoint;
|
|
|
|
|
window.toggleRoute = toggleRoute;
|
|
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
initMap();
|
|
|
|
|
updateTimeline();
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
initMap();
|
|
|
|
|
updateTimeline();
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
|