// VOC 报告 inline JS · PPT 式 deck 驱动 // 特性: // 1) IntersectionObserver 驱动的 fade-up / rise-in / headline-anim // 2) 键盘导航(↑↓ PgUp PgDn Home End j k)逐张切换 // 3) slide counter (n/N + 进度条) // 4) ambient 光晕跟随当前 slide 偏移 // 5) 首次滚动后 5s 自动隐藏键盘提示 // 6) ECharts 各图按需初始化(首次进入视口时才渲染) module.exports = ` (function(){ 'use strict'; var slides = []; var currentSlide = 0; var firstScrolled = false; // —— 1) 分片标题动画:把每个 .headline-anim 文本切成 .char 逐字入场 —— function splitHeadlines(){ var els = document.querySelectorAll('.headline-anim'); els.forEach(function(el){ if (el.dataset.split === '1') return; el.dataset.split = '1'; var text = el.textContent; el.textContent = ''; var chars = Array.from(text); chars.forEach(function(ch, i){ var span = document.createElement('span'); span.className = 'char'; span.textContent = ch === ' ' ? '\\u00A0' : ch; span.style.transitionDelay = (i * 28) + 'ms'; el.appendChild(span); }); }); } // —— 2) Fade / Rise / Headline 统一的 IntersectionObserver —— function initReveal(){ var selectors = ['.fade-up', '.rise-in', '.headline-anim']; var els = document.querySelectorAll(selectors.join(',')); if (!('IntersectionObserver' in window)){ els.forEach(function(el){ el.classList.add('is-visible'); }); return; } var io = new IntersectionObserver(function(entries){ entries.forEach(function(e){ if (e.isIntersecting){ e.target.classList.add('is-visible'); io.unobserve(e.target); } }); }, { threshold: 0.08, rootMargin: '0px 0px -5% 0px' }); els.forEach(function(el){ io.observe(el); }); // 首屏兜底 setTimeout(function(){ els.forEach(function(el){ var r = el.getBoundingClientRect(); if (r.top < window.innerHeight && r.bottom > 0) el.classList.add('is-visible'); }); }, 100); // 终极兜底 setTimeout(function(){ document.querySelectorAll('.fade-up:not(.is-visible),.rise-in:not(.is-visible),.headline-anim:not(.is-visible)').forEach(function(el){ el.classList.add('is-visible'); }); }, 4500); } // —— 3) 数字计数动画 —— function initCountUp(){ var els = document.querySelectorAll('.metric-value[data-count]'); if (!('IntersectionObserver' in window)){ els.forEach(function(el){ el.textContent = el.dataset.count; }); return; } var io = new IntersectionObserver(function(entries){ entries.forEach(function(e){ if (!e.isIntersecting) return; var el = e.target; var target = parseFloat(el.dataset.count); if (isNaN(target)) return; var isFloat = !Number.isInteger(target); var start = 0, duration = 1400, t0 = performance.now(); function tick(now){ var p = Math.min(1, (now - t0) / duration); var eased = 1 - Math.pow(1 - p, 3); var v = start + (target - start) * eased; el.textContent = isFloat ? v.toFixed(1) : Math.floor(v).toLocaleString('en-US'); if (p < 1) requestAnimationFrame(tick); else el.textContent = isFloat ? target.toFixed(1) : target.toLocaleString('en-US'); } requestAnimationFrame(tick); io.unobserve(el); }); }, { threshold: 0.4 }); els.forEach(function(el){ io.observe(el); }); } // —— 4) Slide 进度 · nav 高亮 · ambient 光晕跟随 · slide counter —— function initDeck(){ slides = Array.prototype.slice.call(document.querySelectorAll('section.report-section')); var progressLine = document.getElementById('progress-line'); var navDots = document.querySelectorAll('.nav-dot'); var counter = document.querySelector('.slide-counter'); var counterNow = counter && counter.querySelector('.now'); var counterTotal = counter && counter.querySelector('.total'); var counterBar = counter && counter.querySelector('.bar'); if (counterTotal) counterTotal.textContent = String(slides.length).padStart(2, '0'); // 预设每张 slide 的 ambient 偏移(用 CSS 变量驱动) var ambientPresets = [ { x: '15%', y: '25%' }, { x: '75%', y: '30%' }, { x: '25%', y: '70%' }, { x: '80%', y: '65%' }, { x: '45%', y: '20%' }, { x: '55%', y: '75%' }, { x: '20%', y: '50%' }, { x: '85%', y: '50%' }, { x: '50%', y: '40%' }, { x: '30%', y: '30%' }, { x: '70%', y: '70%' }, { x: '40%', y: '60%' } ]; function setActive(idx){ if (idx < 0) idx = 0; if (idx >= slides.length) idx = slides.length - 1; currentSlide = idx; slides.forEach(function(s, i){ if (i === idx) s.classList.add('in-view'); else s.classList.remove('in-view'); }); for (var j = 0; j < navDots.length; j++){ if (j === idx) navDots[j].classList.add('active'); else navDots[j].classList.remove('active'); } if (counterNow) counterNow.textContent = String(idx + 1).padStart(2, '0'); if (counterBar) { var pct = slides.length > 1 ? ((idx + 1) / slides.length) * 100 : 100; counterBar.style.setProperty('--pct', pct.toFixed(1) + '%'); } // 根据当前 slide 切换 ambient 位置 var p = ambientPresets[idx % ambientPresets.length]; document.body.style.setProperty('--amb-x', p.x); document.body.style.setProperty('--amb-y', p.y); } function onScroll(){ if (!firstScrolled && window.scrollY > 20){ firstScrolled = true; setTimeout(function(){ var hint = document.querySelector('.kbd-hint'); if (hint) hint.classList.add('fade'); }, 4000); } var h = document.documentElement.scrollHeight - window.innerHeight; var p = h > 0 ? (window.scrollY / h) * 100 : 0; if (progressLine) progressLine.style.width = p + '%'; // 找出离视口中心最近的 slide var mid = window.innerHeight * 0.45; var bestIdx = 0, bestDist = Infinity; for (var i = 0; i < slides.length; i++){ var r = slides[i].getBoundingClientRect(); var center = (r.top + r.bottom) / 2; var d = Math.abs(center - mid); if (d < bestDist){ bestDist = d; bestIdx = i; } } if (bestIdx !== currentSlide) setActive(bestIdx); } window.addEventListener('scroll', onScroll, { passive: true }); onScroll(); navDots.forEach(function(d, i){ d.addEventListener('click', function(e){ e.preventDefault(); var tid = d.dataset.target; var t = tid ? document.getElementById(tid) : slides[i]; if (t) t.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); }); // —— 键盘导航 —— function goto(idx){ idx = Math.max(0, Math.min(slides.length - 1, idx)); slides[idx].scrollIntoView({ behavior: 'smooth', block: 'start' }); } window.addEventListener('keydown', function(e){ // 不在输入框里 var tag = (e.target && e.target.tagName || '').toLowerCase(); if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return; if (e.key === 'ArrowDown' || e.key === 'PageDown' || e.key === 'j'){ e.preventDefault(); goto(currentSlide + 1); } else if (e.key === 'ArrowUp' || e.key === 'PageUp' || e.key === 'k'){ e.preventDefault(); goto(currentSlide - 1); } else if (e.key === 'Home'){ e.preventDefault(); goto(0); } else if (e.key === 'End'){ e.preventDefault(); goto(slides.length - 1); } else if (e.key === ' '){ e.preventDefault(); goto(currentSlide + (e.shiftKey ? -1 : 1)); } }); } // —— 5) ECharts 懒加载(slide 进入视口才渲染,避免首屏卡顿)—— var COMMON = { backgroundColor: 'transparent', textStyle: { fontFamily: 'Inter, Noto Sans SC, system-ui', color: '#A8A8A8' }, grid: { left: '3%', right: '4%', bottom: '3%', top: '12%', containLabel: true } }; var AMBER='#F5A623', GREEN='#00DC82', PURPLE='#8B5CF6', ROSE='#FF4D8D', BLUE='#3B82F6'; var PALETTE = [AMBER, GREEN, PURPLE, ROSE, BLUE, '#EF4444', '#06B6D4', '#F59E0B']; var CHART_REGISTRY = []; function initChart(id, cb){ var el = document.getElementById(id); if (!el) return; CHART_REGISTRY.push({ id: id, el: el, cb: cb, rendered: false }); } function renderChart(item){ if (item.rendered || !window.echarts) return; var chart = window.echarts.init(item.el, null, { renderer: 'canvas' }); try { item.cb(chart); item.rendered = true; } catch(e){ console.error(item.id, e); } window.addEventListener('resize', function(){ chart.resize(); }); } function bootCharts(){ if (!window.echarts) { setTimeout(bootCharts, 300); return; } if (!('IntersectionObserver' in window)){ CHART_REGISTRY.forEach(renderChart); return; } var io = new IntersectionObserver(function(entries){ entries.forEach(function(e){ if (!e.isIntersecting) return; var item = CHART_REGISTRY.find(function(x){ return x.el === e.target; }); if (item) renderChart(item); io.unobserve(e.target); }); }, { threshold: 0.1, rootMargin: '200px 0px' }); CHART_REGISTRY.forEach(function(item){ io.observe(item.el); }); } function initCharts(){ var D = window.__CHART_DATA__ || {}; // 样本分布 initChart('chartSample', function(chart){ var data = (D.noteHeatmap || []).slice(0, 5).map(function(n, i){ return { value: (n.liked || 0) + (n.comments || 0) + (n.collected || 0), name: (n.title || '').slice(0, 14) || '笔记' + (i + 1), itemStyle: { color: PALETTE[i % PALETTE.length] } }; }); chart.setOption(Object.assign({}, COMMON, { tooltip: { trigger: 'item', backgroundColor: '#1A1A1A', borderColor: '#252525', textStyle: { color: '#F5F5F5' } }, legend: { bottom: 0, textStyle: { color: '#A8A8A8', fontSize: 11 } }, animationDuration: 1000, animationEasing: 'cubicOut', series: [{ type: 'pie', radius: ['52%', '76%'], center: ['50%', '45%'], label: { color: '#F5F5F5', fontSize: 11, formatter: '{b}\\n{c}' }, itemStyle: { borderColor: '#0A0A0A', borderWidth: 2 }, data: data }] })); }); // IP 地域 initChart('chartGeo', function(chart){ var ips = (D.topIPs || []).slice(0, 10).reverse(); chart.setOption(Object.assign({}, COMMON, { tooltip: { trigger: 'axis', backgroundColor: '#1A1A1A', borderColor: '#252525', textStyle: { color: '#F5F5F5' } }, animationDuration: 1200, animationEasing: 'cubicOut', xAxis: { type: 'value', axisLine: { lineStyle: { color: '#252525' } }, splitLine: { lineStyle: { color: '#1A1A1A' } }, axisLabel: { color: '#6B6B6B' } }, yAxis: { type: 'category', data: ips.map(function(x){return x[0];}), axisLine: { lineStyle: { color: '#252525' } }, axisLabel: { color: '#A8A8A8', fontSize: 12 } }, series: [{ type: 'bar', data: ips.map(function(x){return x[1];}), itemStyle: { color: AMBER, borderRadius: [0, 3, 3, 0] }, barWidth: '60%', label: { show: true, position: 'right', color: '#F5A623', fontFamily: 'JetBrains Mono', fontSize: 11 } }] })); }); // 笔记热力散点 initChart('chartNoteHeatmap', function(chart){ chart.setOption(Object.assign({}, COMMON, { tooltip: { trigger: 'item', backgroundColor: '#1A1A1A', borderColor: '#252525', textStyle: { color: '#F5F5F5' }, formatter: function(p){ return '