/* Injected prototype: render schedule, countdown, SVG overview, and interactions Uses DEFAULT_DATA embedded in index.html (partial provided by user). Adds: - Top countdown with timezone-aware display - SVG hand-drawn style overview (placeholder paths) - Per-day timeline rendering - Google Maps & Xiaohongshu links for places - To-do list rendering with localStorage-based check state */ (function(){ // Attempt to read DEFAULT_DATA from the page (user provided it inline earlier) try{ if(typeof DEFAULT_DATA === 'undefined' || !DEFAULT_DATA) return console.log('No DEFAULT_DATA present for prototype injection'); }catch(e){return} const L = window.location; // Utility: format a Date object into local string with timezone short function fmtLocal(dt){ try{ return new Intl.DateTimeFormat(undefined,{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit',timeZoneName:'short'}).format(dt); }catch(e){ return dt.toString(); } } // ---------- Top countdown: find next upcoming event from DEFAULT_DATA function nextEvent(){ // scan todos -> but better to scan per-day events if present; fallback to todos var candidates = []; // try days (if exists) if(DEFAULT_DATA.days && Array.isArray(DEFAULT_DATA.days)){ DEFAULT_DATA.days.forEach((d,di)=>{ if(d.items && Array.isArray(d.items)){ d.items.forEach(it=>{ if(it.time){ // assume ISO-like; try Date parse const dt = new Date(it.time); if(!isNaN(dt)) candidates.push({dt, title:it.title||it.name||it.text||'活动', obj:it}); } }) } }) } // fallback todos with tags that include date-like substring (best-effort) if(candidates.length===0 && DEFAULT_DATA.todos){ DEFAULT_DATA.todos.forEach(t=>{ // naive: look for yyyy or mm.dd pattern — user provided times likely not here; skip }) } candidates.sort((a,b)=>a.dt - b.dt); return candidates.length?candidates[0]:null; } function renderCountdown(){ const card = document.getElementById('countdownCard'); if(!card) return; const row = card.querySelector('#countdownRow'); // prefer nextEvent const ev = nextEvent(); const labelEl = card.querySelector('.cd-title'); const subEl = card.querySelector('.cd-sub'); // if no event found, fallback to first todo urgent let targetDate = null; let label = ''; if(ev){ targetDate = ev.dt; label = ev.title; } else if(DEFAULT_DATA.todos && DEFAULT_DATA.todos.length){ label = DEFAULT_DATA.todos[0].text; } else { label = '无近期事件'; } if(labelEl) labelEl.textContent = label; if(subEl){ if(targetDate) subEl.textContent = fmtLocal(targetDate); else subEl.textContent = ''; } if(!targetDate){ row.innerHTML = '无可计算的倒计时目标'; return } function tick(){ const now = Date.now(); const diff = targetDate.getTime() - now; if(diff<=0){ row.innerHTML = '✈️ 已开始 / 已过时'; return } const d = Math.floor(diff/86400000); const h = Math.floor((diff%86400000)/3600000); const m = Math.floor((diff%3600000)/60000); const s = Math.floor((diff%60000)/1000); if(d>0){ row.innerHTML = ''+d+' ' + ''+String(h).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')+''+ ''; }else{ row.innerHTML = ''+String(h).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(s).padStart(2,'0')+''+ '后出发'; } } tick(); setInterval(tick,1000); } // ---------- SVG overview (hand-drawn placeholder) ---------- function renderSVGOverview(){ const ov = document.getElementById('ov'); if(!ov) return; // container const svgNS = 'http://www.w3.org/2000/svg'; const w = 440, h = 160; const svg = document.createElementNS(svgNS,'svg'); svg.setAttribute('viewBox','0 0 '+w+' '+h); svg.style.width='100%';svg.style.height='160px';svg.style.display='block'; // background paper const bg = document.createElementNS(svgNS,'rect'); bg.setAttribute('x',0);bg.setAttribute('y',0);bg.setAttribute('width',w);bg.setAttribute('height',h);bg.setAttribute('fill','#FFF8F5');bg.setAttribute('rx',12); svg.appendChild(bg); // generate placeholder points from days const days = DEFAULT_DATA.days && DEFAULT_DATA.days.length?DEFAULT_DATA.days.length:6; const colors = ['#C75B54','#D89A3D','#4E7FB2','#C07B4A','#B57BB0','#6C6CBB']; const points = []; for(let i=0;i{ const item = document.createElement('div'); item.className='todo-item'+(doneState[t.id]?' is-done':''); const chk = document.createElement('button'); chk.className='todo-check'; chk.setAttribute('aria-label','完成'); chk.addEventListener('click', function(){ // toggle local state only doneState[t.id] = !doneState[t.id]; localStorage.setItem(doneKey, JSON.stringify(doneState)); renderTodos(); }); const main = document.createElement('div'); main.className='todo-main'; const text = document.createElement('div'); text.className='todo-text'; text.textContent = t.text; const tag = document.createElement('div'); tag.className='todo-tag'; tag.textContent = t.tag || ''; main.appendChild(text); main.appendChild(tag); if(t.urgent){ const urg = document.createElement('div'); urg.className='todo-urgent'; urg.textContent='紧急'; urg.style.marginLeft='8px'; main.insertBefore(urg, main.firstChild); } item.appendChild(chk); item.appendChild(main); body.appendChild(item); }); // update counts const total = todos.length; const done = Object.keys(doneState).filter(k=>doneState[k]).length; el.querySelector('#todoNum').textContent = done + ' / ' + total; const pct = Math.round((done/total)*100); el.querySelector('#todoBar i').style.width = pct+'%'; } // ---------- Render timeline from DEFAULT_DATA.days (if present) ---------- function renderTimeline(){ const container = document.getElementById('tl'); if(!container) return; container.innerHTML=''; const days = DEFAULT_DATA.days || []; // if no days, try to synthesize from todos if(days.length===0){ const placeholder = document.createElement('div'); placeholder.className='tl-end'; placeholder.textContent='暂无逐日行程(请导入或编辑)'; container.appendChild(placeholder); return } days.forEach((d,di)=>{ const dayHeader = document.createElement('div'); dayHeader.className='overview'; dayHeader.style.marginTop='10px'; dayHeader.innerHTML = '
Day '+(di+1)+'
'+(d.theme||('Day '+(di+1)))+'
编辑
'; container.appendChild(dayHeader); if(d.items && d.items.length){ d.items.forEach(it=>{ const row = document.createElement('div'); row.className='tl-row'; const time = document.createElement('div'); time.className='tl-time'; time.textContent = it.time? (new Date(it.time)).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : ''; const rail = document.createElement('div'); rail.className='tl-rail'; rail.innerHTML = '
'; const card = document.createElement('div'); card.className='tl-card'; const top = document.createElement('div'); top.className='tl-top'; const info = document.createElement('div'); info.className='tl-info'; const title = document.createElement('div'); title.className='tl-title'; title.textContent = it.title || it.name || it.text || '活动'; const loc = document.createElement('div'); loc.className='tl-loc'; loc.textContent = it.loc || it.place || ''; if(it.needBooking) { const b = document.createElement('span'); b.textContent='需预约'; b.style.color='var(--red)'; b.style.marginLeft='8px'; title.appendChild(b); } info.appendChild(title); info.appendChild(loc); // actions: google maps + xiaohongshu const acts = document.createElement('div'); acts.className='tl-acts'; const gbtn = document.createElement('button'); gbtn.className='mini-btn'; gbtn.textContent='导航'; gbtn.addEventListener('click', ()=>{ const q = encodeURIComponent(it.geo || it.loc || it.title || ''); const url = 'https://www.google.com/maps/dir/?api=1&destination='+q+'&travelmode=driving'; window.open(url,'_blank'); }); const xbtn = document.createElement('button'); xbtn.className='mini-btn'; xbtn.textContent='📕 小红书'; xbtn.addEventListener('click', ()=>{ const q = encodeURIComponent(it.title || it.loc || it.name || ''); const url = 'https://www.xiaohongshu.com/search/result?keyword='+q; window.open(url,'_blank'); }); acts.appendChild(gbtn); acts.appendChild(xbtn); top.appendChild(info); top.appendChild(acts); card.appendChild(top); if(it.note){ const note = document.createElement('div'); note.className='tl-note'; note.textContent = it.note; card.appendChild(note); } row.appendChild(time); row.appendChild(rail); row.appendChild(card); container.appendChild(row); }) } }) } // ---------- Init render renderCountdown(); renderSVGOverview(); renderTodos(); renderTimeline(); })();