بنقرة واحدة
travel-itinerary
交互式生成自驾旅行路书 HTML 页面。地图 + 时间轴双栏布局,内嵌天气预报、雨天 Plan B、文字路书面板、途经点标记、移动端底部抽屉。支持国内外目的地。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
交互式生成自驾旅行路书 HTML 页面。地图 + 时间轴双栏布局,内嵌天气预报、雨天 Plan B、文字路书面板、途经点标记、移动端底部抽屉。支持国内外目的地。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | travel-itinerary |
| description | 交互式生成自驾旅行路书 HTML 页面。地图 + 时间轴双栏布局,内嵌天气预报、雨天 Plan B、文字路书面板、途经点标记、移动端底部抽屉。支持国内外目的地。 |
| user-invokable | true |
| args | [{"name":"destination","description":"目的地描述,如\"华东自驾10天\"(可选,也可在对话中说明)","required":false}] |
生成单文件自包含旅行路书 HTML,带交互地图、窄时间轴 + 地图双栏布局、天气预报、雨天备案、文字路书面板、移动端底部抽屉。
自驾行程需询问:
"每天计划几点出发、几点结束?住酒店还是睡车上(服务区)?"
常见模式:
中国大陆目的地:
"需要地图显示路线,你有高德地图 JS API Key(和安全密钥)吗?没有用免费通用地图。"
海外目的地:
"你有 Google Maps API Key 吗?没有用免费的 OpenStreetMap。"
"页面风格偏好?(旅行杂志感 / 极简 / 深色冒险风 / 随你发挥)"
无偏好则根据目的地气质自由发挥,每次生成都要有所不同。
body { display: grid; grid-template-columns: 148px 1fr; height: 100vh; overflow: hidden; }
#timeline { width: 148px; overflow-y: auto; padding: 12px 0; background: var(--white); border-right: 1px solid var(--bd); }
#map-wrap { position: relative; overflow: hidden; }
#map { height: 100vh; width: 100%; }
时间轴每项(.tl-item)点击后触发地图飞入 + InfoWindow:
function selectCity(i) {
if (isMobile()) showMobileSheet(i);
else openInfoWindow(i);
}
InfoWindow 防漂移(高德地图必须):
function openInfoWindow(i) {
const s = stops[i];
const zoomLevel = map.getZoom();
// 地图中心北移,避免高窗口把标记推入海/遮挡
const latShift = zoomLevel >= 12 ? 0.12 : 0.7;
map.setZoomAndCenter(zoomLevel < 10 ? 10 : zoomLevel,
[s.lng, s.lat + latShift], false, 400);
const iw = new AMap.InfoWindow({
content: buildInfoHtml(s),
offset: new AMap.Pixel(0, -38),
autoMove: false, // ← 必须关掉,否则焦点漂移到海上
closeWhenClickMap: true
});
iw.open(map, [s.lng, s.lat]);
}
<script>
const AMAP_KEY = 'YOUR_KEY';
const AMAP_SECRET = 'YOUR_SECRET';
window._AMapSecurityConfig = { securityJsCode: AMAP_SECRET };
const sc = document.createElement('script');
sc.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}&plugin=AMap.Driving`;
sc.onload = () => {
const map = new AMap.Map('map', {
zoom: 5, center: [115, 33],
mapStyle: 'amap://styles/whitesmoke'
});
initMap(map);
};
document.head.appendChild(sc);
</script>
路线:AMap.Driving,多站用 waypoints 单次调用(比 N-1 次更可靠)。
注意:Key 必须是 Web端(JS API)类型,在高德控制台加域名白名单锁定到部署域名。
function initGoogleMap() {
const map = new google.maps.Map(document.getElementById('map'), {
zoom: 11, center: { lat: CENTER_LAT, lng: CENTER_LNG },
disableDefaultUI: true, zoomControl: true
});
initMap(map);
}
const map = L.map('map').setView([CENTER_LAT, CENTER_LNG], 10);
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
attribution: '© OpenStreetMap © CARTO', maxZoom: 19
}).addTo(map);
// 路线:OSRM 免费(无需 Key)
async function planRoute(coords, color = '#7060d0') {
const str = coords.map(c => `${c[1]},${c[0]}`).join(';');
try {
const res = await fetch(`https://router.project-osrm.org/route/v1/driving/${str}?overview=full&geometries=geojson`);
const data = await res.json();
if (data.routes?.[0])
return L.geoJSON(data.routes[0].geometry, { style: { color, weight: 4, opacity: 0.85 } }).addTo(map);
} catch {}
return L.polyline(coords, { color, weight: 3, opacity: 0.6, dashArray: '8 6' }).addTo(map);
}
const stops = [
{
d: "APR 25", // 日期标签,与 weather/planB 对应
name: "城市名",
lat: 39.904, lng: 116.407,
type: "start", // start | end | star | holiday | normal
title: "副标题",
brief: "一句话概述",
places: [ // 城内景点
{ name: "景点", lat: 0, lng: 0, desc: "介绍" }
],
spots: [ // 贴士/美食/注意/补能
{ tag: "food", name: "名称", desc: "说明" } // food|tip|warn|must
],
history: "历史文化背景故事(InfoWindow 底部展示)",
stay: true // 是否在此过夜(可选)
}
];
用于标记晚饭停留、服务区过夜等非主站点,打在路线上:
const waypoints = [
{ name: "全椒服务区", icon: "🛏️", label: "全椒 · 过夜", lng: 118.178, lat: 32.082 },
{ name: "滕州", icon: "🍗", label: "滕州 · 晚饭", lng: 117.157, lat: 34.984 },
];
// 渲染
waypoints.forEach(wp => {
const wrapEl = document.createElement('div');
wrapEl.className = 'mk-stop-wrap';
wrapEl.innerHTML = `<div class="mk-stop">${wp.icon}</div><div class="mk-stop-label">${wp.label}</div>`;
new AMap.Marker({ position:[wp.lng, wp.lat], content:wrapEl,
offset:new AMap.Pixel(-11, -11), map, zIndex: 85, bubble:true });
});
const weather = {
"APR 25": { icon:"🌤️", desc:"多云", hi:22, lo:10, wind:"南风3级", rain:0, risk:null, note:"" },
// risk: null | 'high' | 'low'
};
查询方式(Python,批量):
import requests
url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": LAT, "longitude": LNG,
"daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,windspeed_10m_max",
"timezone": "Asia/Shanghai",
"start_date": "2026-04-25", "end_date": "2026-05-05"
}
r = requests.get(url, params=params, timeout=15)
注意:Open-Meteo 仅提供 16 天预报,出发前 3 天复查。
const planB = {
"APR 28": {
title: "雨天备案",
items: ["武夷山博物馆", "茶艺体验馆", "民宿棋牌"]
}
};
const rbDays = [
{ d:"APR 25", city:"北京 → 全椒服务区", km:"~1050km",
weather: weather["APR 25"],
entries:[
{ time:"09:00", cls:"drive", action:"满电出发,京沪高速南下", note:"确认满电再走" },
{ time:"17:30", cls:"key", action:"滕州东下高速 · 菜煎饼+辣子鸡", note:"人均40元" },
{ time:"23:00", cls:"key", action:"全椒服务区 · 慢充过夜", note:"大站有淋浴" },
]
}
];
// cls: "drive"(行驶)| "key"(关键节点)| ""(普通)
stops.forEach(s => {
const w = weather[s.d];
if (!w) return;
// 天气芯片(城市 Marker 正上方)
const wEl = document.createElement('div');
wEl.className = 'wmk';
wEl.innerHTML = `
<div class="wmk-row">
<span>${w.icon}</span>
<span class="thi">${w.hi}°</span>/<span class="tlo">${w.lo}°</span>
</div>
<div class="wmk-date">${s.d}</div>`;
new AMap.Marker({ position:[s.lng,s.lat], content:wEl,
offset:new AMap.Pixel(-48,-78), map, zIndex:90, bubble:true });
// 高风险天气警告标签
if (w.risk === 'high') {
const warnEl = document.createElement('div');
warnEl.className = 'rain-warn';
warnEl.textContent = '⚠️ 雨天 Plan B';
new AMap.Marker({ position:[s.lng,s.lat], content:warnEl,
offset:new AMap.Pixel(0,-108), map, zIndex:110, bubble:true });
}
});
function buildInfoHtml(s) {
const w = weather[s.d] || {};
const pb = planB[s.d];
// 天气条
const weatherStrip = w.icon ? `
<div class="iw-weather ${w.risk==='high'?'risk':''}">
${w.icon} ${w.desc} ${w.hi}°/${w.lo}° · ${w.wind}
${w.rain > 0 ? ` · 降水${w.rain}mm` : ''}
${w.note ? `<br><span style="font-size:10px">${w.note}</span>` : ''}
</div>` : '';
// 雨天 Plan B 框
const planBHtml = pb ? `
<div class="iw-planb">
<div class="iw-planb-title">☂️ ${pb.title}</div>
${pb.items.map(it => `<div class="iw-planb-item">· ${it}</div>`).join('')}
</div>` : '';
// 景点 + 美食 + 贴士
const spotsHtml = s.spots.map(sp => `
<div class="spot spot-${sp.tag}">
<div class="spot-name">${sp.name}</div>
<div class="spot-desc">${sp.desc}</div>
</div>`).join('');
return `
<div class="iw">
<div class="iw-head">
<div class="iw-date">${s.d}</div>
<div class="iw-title">${s.title}</div>
</div>
${weatherStrip}
<div class="iw-brief">${s.brief}</div>
${planBHtml}
${spotsHtml}
${s.history ? `<div class="iw-history">${s.history}</div>` : ''}
</div>`;
}
右上角固定按钮,点击弹出左侧滑动面板,每天包含:时间轴条目 + 景点 + 美食详情。
<!-- 按钮(放在 #map-wrap 内) -->
<div id="rb-btn" style="position:fixed;top:16px;right:16px;z-index:200">
<div class="rb-trigger" id="rb-open">📖 文字路书</div>
</div>
<!-- 面板 -->
<div id="rb-overlay"></div>
<div id="rb-panel">
<div id="rb-header">
<span>文字路书</span>
<button id="rb-close">✕</button>
</div>
<div id="rb-body"></div>
</div>
function buildRoadbook() {
document.getElementById('rb-body').innerHTML = rbDays.map(day => {
const stop = stops.find(s => s.d === day.d);
const foodSpots = stop ? stop.spots.filter(s => s.tag === 'food') : [];
const placeList = stop ? (stop.places || []) : [];
const entries = day.entries.map(e => `
<div class="rb-entry ${e.cls||''}">
<div class="rb-time">${e.time}</div>
<div class="rb-entry-right">
<div class="rb-action">${e.action}</div>
${e.note ? `<div class="rb-note">${e.note}</div>` : ''}
</div>
</div>`).join('');
const spotsHtml = (foodSpots.length || placeList.length) ? `
<div class="rb-spots">
${placeList.length ? '<div class="rb-spots-title">📍 景点</div>' +
placeList.map(p => `<div class="rb-spot-item"><b>${p.name}</b><div class="rb-note">${p.desc||''}</div></div>`).join('') : ''}
${foodSpots.length ? '<div class="rb-spots-title">🍽️ 美食</div>' +
foodSpots.map(f => `<div class="rb-spot-item"><b>${f.name}</b><div class="rb-note">${f.desc||''}</div></div>`).join('') : ''}
</div>` : '';
return `
<div class="rb-day">
<div class="rb-day-head">
<span class="rb-day-date">${day.d}</span>
<span class="rb-day-city">${day.city}</span>
<span class="rb-day-km">${day.km}</span>
</div>
${day.weather?.icon ? `<div class="rb-day-weather">${day.weather.icon} ${day.weather.desc} ${day.weather.hi}°/${day.weather.lo}°</div>` : ''}
<div class="rb-entries">${entries}</div>
${spotsHtml}
</div>`;
}).join('');
}
@media (max-width: 767px) {
body { grid-template-columns: 1fr; }
#timeline {
position: fixed; left: 0; right: 0; bottom: 0;
width: 100%; height: 85vh;
border-radius: 18px 18px 0 0;
transform: translateY(calc(85vh - 72px));
transition: transform .38s cubic-bezier(.32,0,.15,1);
z-index: 500; overflow-y: auto;
}
#timeline[data-state="half"] { transform: translateY(calc(85vh - 45vh)); }
#timeline[data-state="full"] { transform: translateY(0); }
/* 高德 InfoWindow 在移动端隐藏,改用底部抽屉 */
.amap-info-outer { display: none !important; }
}
function isMobile() { return window.innerWidth <= 767; }
function setDrawer(s) { document.getElementById('timeline').dataset.state = s; }
// 点击城市时:移动端展开抽屉,桌面端开 InfoWindow
function selectCity(i) {
if (isMobile()) {
showMobileSheet(i);
setDrawer('half');
} else {
openInfoWindow(i);
}
}
Nginx 路径结构(一个域名多次旅行):
travel.mediaprogram.cn/
2026-04-25/index.html ← 本次行程
2026-12-01/index.html ← 下次行程
server {
server_name travel.mediaprogram.cn;
location /2026-04-25/ {
alias /usr/share/nginx/html/travel/2026-04-25/;
index index.html;
add_header Cache-Control "no-cache";
}
}
SSL 证书用 certbot:
certbot --nginx -d travel.mediaprogram.cn --non-interactive --agree-tos -m YOUR_EMAIL
| 目的地类型 | 配色 | 字体 | 气质 |
|---|---|---|---|
| 中国古都 / 人文 | 宣纸米 + 朱砂红 | Noto Serif SC | 古典沉静 |
| 热带城市 | 深丛林绿 + 椰白 | 现代无衬线 | 清爽活力 |
| 日系城市 | 浅樱粉 + 深炭灰 | 精细线条 | 克制精致 |
| 欧洲历史 | 石灰白 + 赭石金 | 衬线古典 | 优雅厚重 |
| 国内自驾公路 | 暖米 + 琥珀橙 + 青绿 | DM Sans + Noto Sans SC | 公路旅行感 |
| 问题 | 解决 |
|---|---|
| 手机媒体查询不生效 | 加 <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 高德路线空白 | Key + Secret 缺一不可,控制台看报错 |
| InfoWindow 焦点漂移到海上 | autoMove: false + 手动 latShift 北移地图中心 |
| 高德 Key 报 USERKEY_PLAT_NOMATCH | Key 类型错误(用了 Web服务 Key 而非 JS API Key) |
| OSRM 路线失败 | 加虚线直连 fallback |
| 谷歌地图国内无法访问 | 国内目的地不用谷歌方案 |
| 天气预报超出范围 | Open-Meteo 最多 16 天,出发前重新查 |
| 移动端 InfoWindow 遮挡地图 | 隐藏 .amap-info-outer,改用底部抽屉 |