// ==============================================================================
// <品类名> · 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 八大假设,从用户真实原声反向推演<金额目标>增长路径。
${Object.keys(m.platforms || {}).length}
覆盖平台
${(m.products || []).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 };