travel-page-framework
行程摘要

Overview

整体概览

Hotels

酒店安排

Metro Coverage

地铁覆盖目标

Timeline

每日行程

点击卡片可查看当日景点详情。

Side Trips

周边城市侧游

Tips

实用提醒

Attractions

景点灵感

* * * Reads trip-data.json and populates all DOM sections defined in the HTML skeleton. * Theme colors come from TripThemes[themeName] (loaded via themes/light.js etc). */ ;(function (global) { 'use strict'; /* ── helpers ─────────────────────────────────────────── */ let _theme = null; let _metroColorMap = {}; function theme() { return _theme || (global.TripThemes && global.TripThemes.light) || {}; } function badgeClass(status) { const t = theme(); if (t.badge && t.badge[status]) return t.badge[status]; return (t.badge && t.badge._default) || ''; } /** Build metro color map on first use from metroCoverage data. */ function buildMetroColors(lines) { const palette = (theme().metroPalette) || []; lines.forEach(function (line, i) { if (!_metroColorMap[line.name] && palette[i % palette.length]) { var p = palette[i % palette.length]; _metroColorMap[line.name] = p.bg + ' ' + p.text + ' ' + p.border; } }); } function metroClass(line) { return _metroColorMap[line] || (theme().metro && theme().metro._default) || ''; } function listItems(items) { return items.map(function (item) { return '
  • ' + esc(item) + '
  • '; }).join(''); } function esc(s) { if (typeof s !== 'string') return ''; return s.replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } /* ── modal ──────────────────────────────────────────── */ function openDayModal(day) { var modal = document.getElementById('day-modal'); if (!modal) return; document.body.classList.add('modal-open'); modal.classList.remove('hidden'); modal.classList.add('flex'); setText('modal-meta', day.day + ' · ' + day.date + ' · ' + day.city); setText('modal-title', day.theme); setText('modal-note', '备注:' + day.note); setHTML('modal-morning', listItems(day.segments.morning)); setHTML('modal-afternoon', listItems(day.segments.afternoon)); setHTML('modal-evening', listItems(day.segments.evening && day.segments.evening.length ? day.segments.evening : ['自由安排 / 休整'])); var attractions = day.attractions || []; setHTML('modal-gallery', attractions.length ? attractions.map(function (a) { return '' + esc(a.name) + ''; }).join('') : '
    暂无景点图。
    '); setHTML('modal-attractions', attractions.length ? attractions.map(function (a) { return '
    ' + '
    ' + esc(a.name) + '
    ' + '

    ' + esc(a.description) + '

    ' + (a.reason ? '
    安排理由:' + esc(a.reason) + '
    ' : '') + '
    '; }).join('') : '
    这一天没有单独展开的景点说明。
    '); } function closeDayModal() { var modal = document.getElementById('day-modal'); if (!modal) return; modal.classList.add('hidden'); modal.classList.remove('flex'); document.body.classList.remove('modal-open'); } function bindModalEvents() { var close = document.getElementById('modal-close'); var modal = document.getElementById('day-modal'); if (close) close.addEventListener('click', closeDayModal); if (modal) modal.addEventListener('click', function (e) { if (e.target === modal) closeDayModal(); }); } /* ── tiny DOM helpers ───────────────────────────────── */ function setText(id, val) { var el = document.getElementById(id); if (el) el.textContent = val || ''; } function setHTML(id, val) { var el = document.getElementById(id); if (el) el.innerHTML = val || ''; } /* ── main render ────────────────────────────────────── */ function render(data) { // Build metro palette from data if (data.metroCoverage && data.metroCoverage.lines) { buildMetroColors(data.metroCoverage.lines); } // Title document.title = data.meta.title; // Hero setText('hero-title', data.hero.title); setText('hero-subtitle', data.hero.subtitle); setText('hero-date', data.hero.dateRange); setText('hero-summary', data.hero.summary); var heroBg = document.getElementById('hero-bg'); if (heroBg && data.hero.heroImage) { heroBg.style.backgroundImage = "url('" + data.hero.heroImage + "')"; } setHTML('hero-tags', (data.hero.tags || []).map(function (tag) { return '' + esc(tag) + ''; }).join('')); setHTML('hero-quick', data.stats.slice(0, 4).map(function (s) { return '
    ' + '
    ' + esc(s.label) + '
    ' + '
    ' + esc(s.value) + '
    '; }).join('')); // Stats setHTML('stats-grid', data.stats.map(function (s) { return '
    ' + '
    ' + esc(s.label) + '
    ' + '
    ' + esc(s.value) + '
    '; }).join('')); // Hotels setHTML('hotel-grid', data.hotels.map(function (h) { return '
    ' + '' + esc(h.name) + '' + '
    ' + '
    ' + esc(h.phase) + '
    ' + '

    ' + esc(h.name) + '

    ' + '' + esc(h.status) + '
    ' + '
    ' + '
    日期:' + esc(h.dateRange) + '
    区域:' + esc(h.station) + '
    ' + '
    价格:' + esc(h.price) + '
    地铁:' + esc(h.distanceToMetro) + '
    ' + '
    '; }).join('')); // Metro setText('metro-goal', data.metroCoverage.goal); setHTML('metro-lines', data.metroCoverage.lines.map(function (l) { return '
    ' + '
    ' + esc(l.name) + '
    ' + '
    ' + esc(l.day) + '
    '; }).join('')); // Days setHTML('day-list', data.days.map(function (day, idx) { var metroTags = day.metroLines.length ? day.metroLines.map(function (l) { return '' + esc(l) + ''; }).join('') : '当日无主补线任务'; var attrTags = (day.attractions || []).map(function (a) { return '' + esc(a.name) + ''; }).join(''); return '
    ' + '
    ' + '
    ' + esc(day.day) + ' · ' + esc(day.date) + '
    ' + '

    ' + esc(day.theme) + '

    ' + '
    城市:' + esc(day.city) + ' · 酒店:' + esc(day.hotel || '无') + '
    ' + '
    查看详情
    ' + '
    ' + metroTags + '
    ' + '
    ' + '
    上午
      ' + listItems(day.segments.morning) + '
    ' + '
    下午
      ' + listItems(day.segments.afternoon) + '
    ' + '
    晚上
      ' + listItems(day.segments.evening && day.segments.evening.length ? day.segments.evening : ['自由安排 / 休整']) + '
    ' + (attrTags ? '
    ' + attrTags + '
    ' : '') + '
    '; }).join('')); // Bind day card clicks document.querySelectorAll('.day-card').forEach(function (card) { card.addEventListener('click', function () { openDayModal(data.days[Number(card.dataset.dayIndex)]); }); }); // Side trips setHTML('side-trips-list', data.sideTrips.map(function (s) { return '
    ' + '' + esc(s.name) + '' + '
    ' + esc(s.date) + '
    ' + '

    ' + esc(s.name) + '

    ' + '
    ' + esc(s.role) + '
    ' + '

    ' + esc(s.description) + '

    '; }).join('')); // Tips setHTML('tips-panel', data.tips.map(function (tip) { return '
    ' + esc(tip) + '
    '; }).join('')); // Attractions setHTML('attraction-grid', data.attractions.map(function (a) { return '
    ' + '' + esc(a.name) + '' + '
    ' + esc(a.city) + ' · ' + esc(a.type) + '
    ' + '

    ' + esc(a.name) + '

    ' + '

    ' + esc(a.description) + '

    ' + '
    ' + a.bestFor.map(function (b) { return '' + esc(b) + ''; }).join('') + '
    '; }).join('')); } /* ── public API ─────────────────────────────────────── */ global.TripRenderer = { /** * @param {Object} opts * @param {string} opts.dataUrl - path to trip-data.json (default: './data/trip-data.json') * @param {string} opts.theme - theme name (default: 'light') * @param {Object} opts.data - pass data directly instead of fetching */ init: function (opts) { opts = opts || {}; var themeName = opts.theme || 'light'; _theme = (global.TripThemes && global.TripThemes[themeName]) || null; // Apply tailwind config if theme provides it if (_theme && _theme.tailwind && global.tailwind) { global.tailwind.config = { theme: { extend: { colors: _theme.tailwind, boxShadow: _theme.shadows || {} } } }; } bindModalEvents(); if (opts.data) { render(opts.data); } else { var url = opts.dataUrl || './data/trip-data.json'; fetch(url) .then(function (r) { return r.json(); }) .then(render) .catch(function (e) { console.error('[TripRenderer] Failed to load data:', e); }); } }, /** Expose for external use */ render: render, metroClass: metroClass, badgeClass: badgeClass }; })(window);