fix: 增加导出html的demow文件;修改导出html功能,目前加载地图依然存在问题

refactor
Lxy 7 days ago
parent 9409a2c5e8
commit 27d65ea08f

File diff suppressed because one or more lines are too long

@ -235,12 +235,17 @@ const toggleRoute = () => {
function exportCurrentScheme() { function exportCurrentScheme() {
const idx = store.activeSchemeIndex const idx = store.activeSchemeIndex
let scheme
if (idx >= 0 && idx < store.quickSchemes.length) { if (idx >= 0 && idx < store.quickSchemes.length) {
// // 使 points 使 store.points
exportToHtml(store.quickSchemes[idx], `${store.quickSchemes[idx].name || '行程规划'}.html`) scheme = {
...store.quickSchemes[idx],
points: JSON.parse(JSON.stringify(store.points)) // points
}
} else { } else {
// points // points
const scheme = { scheme = {
name: '我的行程', name: '我的行程',
route: store.points.map(p => p.name).join(' → '), route: store.points.map(p => p.name).join(' → '),
days: store.points.length, days: store.points.length,
@ -248,22 +253,13 @@ function exportCurrentScheme() {
totalDriveTime: store.totalDriveTime, totalDriveTime: store.totalDriveTime,
budget: '—', budget: '—',
highlights: [], highlights: [],
daysDetail: store.points.map((p, i) => ({ points: JSON.parse(JSON.stringify(store.points)) // points
location: p.name,
desc: p.desc,
km: p.km,
driveTime: p.driveTime,
schedule: p.schedule || [],
foods: p.foods || [],
waypoints: p.waypoints || [],
hotel: p.hotel || ''
})),
tips: ''
}
exportToHtml(scheme, '我的行程.html')
} }
} }
exportToHtml(scheme, `${scheme.name || '行程规划'}.html`)
}
// points phase // points phase
watch([() => store.points, () => store.phase], ([newPoints, newPhase]) => { watch([() => store.points, () => store.phase], ([newPoints, newPhase]) => {
console.log('[Workbench] watch 触发 - phase:', newPhase, 'points 长度:', newPoints.length) console.log('[Workbench] watch 触发 - phase:', newPhase, 'points 长度:', newPoints.length)

@ -1,8 +1,15 @@
// 导出可交互的 HTML 行程文件 // 导出可交互的 HTML 行程文件(包含地图和站点交互,样式与工作台一致)
export function exportToHtml(scheme, filename = null) { export function exportToHtml(scheme, filename = null) {
if (!scheme) return 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 blob = new Blob([html], { type: 'text/html;charset=utf-8' })
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
const link = document.createElement('a') const link = document.createElement('a')
@ -14,25 +21,102 @@ export function exportToHtml(scheme, filename = null) {
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
} }
function generateHtml(scheme) { // 检查 URL 是否安全
const daysDetail = scheme.daysDetail || [] function isSafeUrl(url) {
const points = [] if (!url) return false
return url.startsWith('http://') || url.startsWith('https://')
// 从 daysDetail 中提取站点信息 }
daysDetail.forEach((day, idx) => {
points.push({ // 过滤安全的图片 URL
name: day.location || `Day ${idx + 1}`, function getSafeGallery(gallery) {
day: `Day ${idx + 1}`, return (gallery || []).filter(g => isSafeUrl(g.url))
desc: day.desc || '', }
km: day.km || '—',
driveTime: day.driveTime || '—', // 生成站点详情 HTML用于初始化页面
schedule: day.schedule || [], function generatePointDetail(p) {
foods: day.foods || [], let html = ''
waypoints: day.waypoints || [],
hotel: day.hotel || '' // 统计信息
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> return `<!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
@ -41,153 +125,467 @@ function generateHtml(scheme) {
<title>${scheme.name || '行程规划'}</title> <title>${scheme.name || '行程规划'}</title>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { 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; } 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; }
.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; } .wb-layout { display: flex; height: 100vh; }
.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); } .wb-left { width: 220px; background: #2d3436; color: #fff; padding: 16px 0; overflow-y: auto; flex-shrink: 0; }
.highlights h2 { font-size: 18px; margin-bottom: 12px; color: #6c5ce7; } .wb-tl-header { font-size: 13px; padding: 8px 16px; color: #b2bec3; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; }
.highlights ul { padding-left: 20px; } .wb-tl-item { padding: 10px 16px; cursor: pointer; display: flex; gap: 12px; transition: all 0.2s; position: relative; }
.highlights li { margin-bottom: 6px; } .wb-tl-item:hover { transform: translateX(4px); background: rgba(108,92,231,0.2); }
.tabs { display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap; } .wb-tl-item.active { background: rgba(108,92,231,0.4); }
.tab { padding: 10px 20px; background: #fff; border: 2px solid #eee; border-radius: 8px; cursor: pointer; font-weight: 600; transition: all 0.2s; } .wb-tl-item.active::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: #6c5ce7; }
.tab:hover { border-color: #6c5ce7; color: #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; }
.tab.active { background: #6c5ce7; color: #fff; border-color: #6c5ce7; } .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; }
.day-content { background: #fff; padding: 24px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); display: none; } .wb-tl-item.completed .wb-tl-dot { background: #6c5ce7; border-color: #6c5ce7; }
.day-content.active { display: block; } .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%); }
.day-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #eee; } .wb-tl-info { flex: 1; min-width: 0; }
.day-badge { background: #6c5ce7; color: #fff; padding: 4px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; } .wb-tl-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.day-title { font-size: 18px; font-weight: 700; } .wb-tl-day { font-size: 11px; color: #b2bec3; margin-top: 2px; }
.day-km { margin-left: auto; font-size: 13px; color: #999; } .wb-tl-item.active .wb-tl-name { color: #6c5ce7; }
.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; } .wb-mid { width: 340px; background: #fff; overflow-y: auto; border-right: 1px solid #e9ecef; flex-shrink: 0; }
.schedule-content { flex: 1; } .wm-header { padding: 16px; border-bottom: 1px solid #e9ecef; background: #f8f9fa; }
.schedule-content strong { display: block; margin-bottom: 4px; } .wm-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
.schedule-content p { font-size: 14px; color: #636e72; margin: 0; } .wm-subtitle { font-size: 12px; color: #636e72; }
.waypoints { margin-bottom: 20px; } .wm-body { padding: 16px; }
.waypoints h3 { font-size: 15px; margin-bottom: 10px; color: #636e72; } .wm-sec { margin-bottom: 20px; }
.waypoint-list { display: flex; flex-wrap: wrap; gap: 8px; } .wm-sec h4 { font-size: 14px; color: #6c5ce7; margin-bottom: 10px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
.waypoint-tag { background: #fff; border: 1px solid #dfe6e9; padding: 6px 12px; border-radius: 8px; font-size: 13px; } .wm-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 16px; }
.foods { margin-bottom: 20px; } .wm-stat { background: #f8f9fa; padding: 10px; border-radius: 8px; text-align: center; }
.foods h3 { font-size: 15px; margin-bottom: 10px; color: #636e72; } .ws-val { font-size: 16px; font-weight: 700; color: #6c5ce7; }
.food-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; } .ws-lbl { font-size: 11px; color: #636e72; margin-top: 2px; }
.food-item { display: flex; align-items: center; gap: 8px; padding: 10px; background: #f8f9fa; border-radius: 8px; }
/* 日程安排 */
.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; } .food-icon { font-size: 20px; }
.hotel { background: #f0f4ff; padding: 14px; border-radius: 8px; border-left: 3px solid #6c5ce7; font-size: 14px; color: #636e72; } .food-info { flex: 1; }
.footer { text-align: center; margin-top: 32px; padding: 20px; color: #999; font-size: 12px; } .food-name { font-size: 13px; font-weight: 600; }
@media (max-width: 600px) { .food-desc { font-size: 12px; color: #636e72; margin-top: 2px; }
.container { padding: 16px; }
.header { padding: 20px; } /* 照片画廊 */
.header h1 { font-size: 20px; } .photo-gallery { margin-bottom: 16px; }
.meta { flex-direction: column; gap: 8px; } .photo-gallery h4 { font-size: 14px; color: #6c5ce7; margin-bottom: 10px; }
.tabs { gap: 6px; } .gallery-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.tab { padding: 8px 14px; font-size: 13px; } .gallery-item { border-radius: 8px; overflow: hidden; position: relative; aspect-ratio: 4/3; background: linear-gradient(135deg, #e8e4ff, #d4c8ff); }
.day-content { padding: 16px; } .gallery-item img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.2s; }
.food-list { grid-template-columns: 1fr; } .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> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="loading-overlay" id="loadingOverlay">
<div class="header"> <div class="loading-spinner"></div>
<h1>${scheme.name || '行程规划'}</h1> <div style="font-size:18px;margin-bottom:8px">正在加载地图...</div>
<div class="meta"> <div style="font-size:13px;opacity:.7">请稍候</div>
<span>📍 ${scheme.route || '—'}</span>
<span>📅 ${scheme.days || '—'}</span>
<span>🚗 ~${scheme.totalKm || '—'}km</span>
<span>💰 ${scheme.budget || '—'}</span>
</div> </div>
</div>
${scheme.highlights && scheme.highlights.length > 0 ? `
<div class="highlights">
<h2> 行程亮点</h2>
<ul>
${scheme.highlights.map(h => `<li>${h}</li>`).join('')}
</ul>
</div>
` : ''}
<div class="tabs"> <div class="wb-layout">
${points.map((p, i) => ` <!-- 左侧时间线 -->
<div class="tab ${i === 0 ? 'active' : ''}" onclick="showDay(${i})">Day ${i + 1}</div> <div class="wb-left">
`).join('')} <div class="wb-tl-header">行程时间线</div>
<div id="timeline"></div>
</div> </div>
${points.map((p, i) => ` <!-- 中间详情面板 -->
<div class="day-content ${i === 0 ? 'active' : ''}" id="day-${i}"> <div class="wb-mid">
<div class="day-header"> <div class="wm-header">
<span class="day-badge">Day ${i + 1}</span> <div class="wm-title" id="detail-title">${points[0]?.name || ''}</div>
<span class="day-title">${p.name}</span> <div class="wm-subtitle" id="detail-subtitle">${points[0]?.day || 'Day 1'} · ${points[0]?.km || '—'}km · 驾车${points[0]?.driveTime || '—'}</div>
<span class="day-km">${p.km}km · 驾车${p.driveTime}h</span>
</div> </div>
<div class="wm-body" id="detail-body">
${p.desc ? `<p style="margin-bottom:16px;color:#636e72;">${p.desc}</p>` : ''} ${generatePointDetail(points[0])}
${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>
</div> </div>
`).join('')}
</div> <!-- 右侧地图 -->
` : ''} <div class="wb-right">
<div id="map-container">
${p.waypoints && p.waypoints.length > 0 ? ` <img id="static-map" src="" alt="行程地图" />
<div class="waypoints"> <div id="map-markers"></div>
<h3>🗺 途径推荐</h3>
<div class="waypoint-list">
${p.waypoints.map(wp => `
<div class="waypoint-tag">${wp.icon || '📍'} ${wp.name}</div>
`).join('')}
</div> </div>
<div class="map-controls">
<button class="route-toggle" id="route-toggle" onclick="toggleRoute()">
🛣 显示路线
</button>
</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>
</div> </div>
` : ''}
${p.hotel ? ` <script>
<div class="hotel">🏨 住宿推荐${p.hotel}</div> const points = ${JSON.stringify(points.map(p => ({
` : ''} name: p.name,
</div> day: p.day || 'Day 1',
`).join('')} 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 || ''
})))};
${scheme.tips ? ` let currentStep = 0;
<div style="background:#fff8e1;padding:20px;border-radius:12px;margin-top:20px;border-left:3px solid #f39c12;"> let routeVisible = false;
<h3 style="margin-bottom:8px;">💡 旅行贴士</h3> let routeSvg = null;
<p>${scheme.tips}</p> let mapImageLoaded = false;
</div>
` : ''}
<div class="footer">由智能行程规划系统生成</div> // 计算地图边界
</div> function calcMapBounds() {
const validPoints = points.filter(p => p.lat && p.lng);
if (validPoints.length === 0) return { minLat: 25, maxLat: 25, minLng: 102, maxLng: 102 };
<script> let minLat = validPoints[0].lat, maxLat = validPoints[0].lat;
function showDay(idx) { let minLng = validPoints[0].lng, maxLng = validPoints[0].lng;
document.querySelectorAll('.day-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); validPoints.forEach(p => {
document.getElementById('day-' + idx).classList.add('active'); if (p.lat < minLat) minLat = p.lat;
document.querySelectorAll('.tab')[idx].classList.add('active'); 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> </script>
</body> </body>

Loading…
Cancel
Save