// ============================================================================== // <品类名> · VOC 报告主渲染器(模板) // ============================================================================== // 职责: // - 定义 CHAPTERS 元信息(章 × 子章 × tone) // - 装配 Cover + Agenda + Divider + Sections + Coming Soon // - 生成完整 HTML(含 CSS + 导航 JS) // ============================================================================== const LA = require('./<品类>-analyze.js'); const C = require('./<品类>-components.js'); const { BRAND, esc, fmtLikes } = C; // ========================================================== // 1. 章节元信息 // TODO: 按 2.VOC深度思路.md 的章节骨架填写 // ========================================================== const CHAPTERS = [ { num: 1, key: 'challenge', title: '诘问起点', tone: 'urgent', subs: ['1-1', '1-2', '1-3'], heroLine: '<章 1 一句话总结>', }, { num: 2, key: 'ch2', title: '<赛道 1> VOC', tone: 'critical', subs: ['2-1', '2-2', '2-3', '2-4'], heroLine: '<章 2 一句话总结>', }, { num: 3, key: 'ch3', title: '<赛道 2> VOC', tone: 'analytical', subs: ['3-1', '3-2', '3-3', '3-4'], heroLine: '<章 3 一句话总结>', }, { num: 4, key: 'kano', title: 'KANO × JTBD', tone: 'insight', subs: ['4-1', '4-2', '4-3', '4-4', '4-5'], heroLine: '<章 4 一句话总结>', }, { num: 5, key: 'scene', title: '场景四元素地图', tone: 'empathy', subs: ['5-1', '5-2', '5-3', '5-4'], heroLine: '<章 5 一句话总结>', }, { num: 6, key: 'competitor', title: '三轴 × 无人地带', tone: 'strategic', subs: ['6-1', '6-2', '6-3'], heroLine: '<章 6 一句话总结>', }, { num: 7, key: 'opportunity', title: '新机会 × 新人群', tone: 'forward', subs: ['7-1', '7-2', '7-3'], heroLine: '<章 7 一句话总结>', }, { num: 8, key: 'blueprint', title: '4P 配称蓝图', tone: 'blueprint', subs: ['8-1', '8-2'], heroLine: '<章 8 一句话总结>', }, ]; // ========================================================== // 2. Slide 渲染器集合(从各章节模块导入) // ========================================================== const SLIDE_RENDERERS = { ...require('./<品类>-ch1-challenge.js'), ...require('./<品类>-ch2-xxx.js'), ...require('./<品类>-ch3-xxx.js'), ...require('./<品类>-ch4-kano.js'), ...require('./<品类>-ch5-scene.js'), ...require('./<品类>-ch6-competitor.js'), ...require('./<品类>-ch7-opportunity.js'), ...require('./<品类>-ch8-blueprint.js'), }; // ========================================================== // 3. 封面 // ========================================================== function renderCover(data) { const m = data.meta || {}; const total = m.count || (data.items?.length || 0); const platforms = Object.entries(m.platforms || {}) .map(([k, v]) => `${k}: ${v}`) .join(' · '); return `
VOC DEEP INSIGHT · 反向定位

<品类名>
VOC 深度洞察报告

基于${total} 条真实 VOC × 多平台 × H1-H8 八大假设,从用户真实原声反向推演<金额目标>增长路径。
${total}
真实 VOC 条数
${Object.keys(m.platforms || {}).length}
覆盖平台
${(m.products || []).length}
产品维度
${CHAPTERS.length}
深度章节
${platforms}
采集时间: ${new Date(m.collected_at || Date.now()).toISOString().slice(0, 10)}
`; } // ========================================================== // 4. 目录 // ========================================================== function renderAgenda() { return `
📋 AGENDA · 章节导航

8 章 · ${CHAPTERS.reduce((s, c) => s + c.subs.length, 0)} 子章

`; } // ========================================================== // 5. 章分隔 // ========================================================== function renderDivider(chapter) { return `
CHAPTER ${chapter.num.toString().padStart(2, '0')}

${esc(chapter.title)}

${chapter.heroLine || ''}
`; } // ========================================================== // 6. Coming Soon 占位 // ========================================================== function renderComingSoon(chapter, subIdx) { return `
COMING IN BATCH

${chapter.num}.${subIdx} · 建设中

此子章正在填充中
`; } // ========================================================== // 7. CSS // ========================================================== const CSS = ` :root { --bg-0: #0A0A0A; --bg-1: #111111; --bg-2: #1A1A1A; --bg-3: #252525; --border: rgba(255,255,255,0.08); --border-strong: rgba(255,255,255,0.16); --text-1: #F5F5F5; --text-2: #A8A8A8; --text-3: #6B6B6B; --font-head: 'Inter','Noto Sans SC','PingFang SC','Microsoft YaHei',system-ui,sans-serif; --font-mono: 'JetBrains Mono','SFMono-Regular',Consolas,monospace; --font-body: 'Inter','Noto Sans SC',system-ui,sans-serif; --radius-sm: 4px; --radius-md: 8px; --radius-lg: 12px; } * { box-sizing: border-box; } body { font-family: var(--font-body); background: var(--bg-0); color: var(--text-1); margin: 0; line-height: 1.6; } .text-mono { font-family: var(--font-mono); } .text-xs { font-size: 0.72rem; } .grid { display: grid; } h1,h2,h3 { margin-top: 0; } .report-section { min-height: 100vh; padding: 60px 48px; border-bottom: 1px solid var(--border); scroll-margin-top: 20px; } .section-inner { max-width: 1280px; margin: 0 auto; } .cover { background: linear-gradient(180deg, #0A0A0A 0%, #111 100%); } .divider { background: linear-gradient(180deg, #0F0F0F 0%, #000 100%); display:flex; align-items:center; } a { color: inherit; } a:hover { opacity: 0.85; } `; // ========================================================== // 8. 导航 JS // ========================================================== const NAV_JS = ` (function() { const sections = document.querySelectorAll('.report-section'); let idx = 0; function go(i) { idx = Math.max(0, Math.min(sections.length - 1, i)); sections[idx].scrollIntoView({ behavior: 'smooth' }); if (sections[idx].id) history.replaceState(null, '', '#' + sections[idx].id); } document.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight' || e.key === ' ') { e.preventDefault(); go(idx + 1); } if (e.key === 'ArrowLeft') { e.preventDefault(); go(idx - 1); } if (e.key === 'Home') go(0); if (e.key === 'End') go(sections.length - 1); }); if (location.hash) { const i = [...sections].findIndex((s) => '#' + s.id === location.hash); if (i >= 0) idx = i; } })(); `; // ========================================================== // 9. 主装配 // ========================================================== function buildHTML(data) { const slides = []; slides.push(renderCover(data)); slides.push(renderAgenda()); const slideTotal = CHAPTERS.reduce((s, c) => s + c.subs.length, 0) + 2; let slideNum = 3; for (const chapter of CHAPTERS) { slides.push(renderDivider(chapter)); for (const sub of chapter.subs) { const subIdx = sub.split('-')[1]; const key = `${chapter.key}-${subIdx}`; const renderer = SLIDE_RENDERERS[key]; if (!renderer) { slides.push(renderComingSoon(chapter, subIdx)); } else { slides.push(renderer({ chapter, subIdx, sub, slideNum, slideTotal, data })); } slideNum++; } } return ` <品类名> · VOC 深度洞察报告 ${slides.join('\n')} `; } module.exports = { buildHTML, CHAPTERS };