liver-v2-render.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. // ==============================================================================
  2. // 江中肝纯片 · VOC 深度洞察报告 v2 · 主渲染器
  3. // ==============================================================================
  4. // 会议要求(增量优化):
  5. // A. 新增自身产品分析(肝纯片抖音已卖数年→用户画像)
  6. // B. VOC 分类归拢(7715条→几大类→共性需求)
  7. // C. 竞品正反面全面分析(当前只有缺点→加优点)
  8. // + 特劳特定位 + 参照"右旋"框架
  9. //
  10. // 新结构(6 章):
  11. // Ch0 护肝人群全景 + VOC 归拢 + TAM/SAM/SOM(req B)
  12. // Ch1 肝纯片自身画像(req A · 全新)
  13. // Ch2 竞品正反面全面分析(req C)
  14. // Ch3 品类定位 · 里斯×特劳特
  15. // Ch4 场景×机会(整合旧 Ch5+Ch7)
  16. // Ch5 落地行动
  17. // ==============================================================================
  18. const STYLES = require('./voc-styles.js');
  19. const SCRIPTS = require('./voc-scripts.js');
  20. const LA = require('./liver-analyze.js');
  21. const C = require('./liver-components.js');
  22. const { BRAND, esc } = C;
  23. // Chapter submodules
  24. const ch0 = require('./liver-v2-ch0-landscape.js');
  25. const ch1 = require('./liver-v2-ch1-self.js');
  26. const ch2 = require('./liver-v2-ch2-competitor.js');
  27. const ch3 = require('./liver-v2-ch3-position.js');
  28. const ch4 = require('./liver-v2-ch4-scene.js');
  29. const ch5 = require('./liver-v2-ch5-action.js');
  30. const SUB_MODULES = { landscape: ch0, self: ch1, competitor: ch2, position: ch3, scene: ch4, action: ch5 };
  31. // ============================================================
  32. // 6 章元信息
  33. // ============================================================
  34. const CHAPTERS = [
  35. { id: 0, num: '0', key: 'landscape', title: '护肝人群全景 · VOC 分类归拢', en: 'LIVER LANDSCAPE', tone: 'blue', color: BRAND.TECH_BLUE, icon: '🔍',
  36. summary: '7715 条 VOC 结构化分类:问题类型×人群×需求 + TAM→SAM→SOM 漏斗' },
  37. { id: 1, num: '1', key: 'self', title: '肝纯片自身画像', en: 'SELF PORTRAIT', tone: 'amber', color: BRAND.LIVER_AMBER, icon: '🪞',
  38. summary: '肝纯片抖音已卖数年 · 用户画像 · 满意/不满意 · 价值感知 · DMY 认知' },
  39. { id: 2, num: '2', key: 'competitor', title: '竞品正反面 · 全面分析', en: 'COMPETITOR SWOT', tone: 'rose', color: BRAND.ROSE, icon: '⚔',
  40. summary: '6 大竞品正反面:Swisse / 易善复 / 葵花 / 片仔癀 / 海王金樽 / 熊胆粉' },
  41. { id: 3, num: '3', key: 'position', title: '品类定位 · 里斯×特劳特', en: 'CATEGORY POSITIONING', tone: 'purple', color: BRAND.PURPLE, icon: '🎯',
  42. summary: '里斯品类进化树 + 特劳特心智阶梯 · 藤茶 DMY 差异化锚点 · 一句话定位' },
  43. { id: 4, num: '4', key: 'scene', title: '场景×机会 · 四大独占场景', en: 'SCENE & OPPORTUNITY', tone: 'orange', color: BRAND.WARM_ORANGE, icon: '🗺',
  44. summary: '应酬解酒 / 脂肪肝调理 / 熬夜前后 / 送长辈 · 每个场景带 VOC 证据链' },
  45. { id: 5, num: '5', key: 'action', title: '落地行动', en: 'ACTION PLAN', tone: 'green', color: BRAND.JIANG_GREEN, icon: '🚀',
  46. summary: '4P 配称 + 10 亿路径 + P0/P1 行动清单 + 包装优化方向' },
  47. ];
  48. // ============================================================
  49. // 子章节
  50. // ============================================================
  51. const CHAPTER_SUBSECTIONS = {
  52. landscape: [
  53. '护肝问题分类归拢 · 六大类 + 共性需求',
  54. '人群声量 × 平台分布 × 情感地图',
  55. 'TAM → SAM → SOM · 10 亿目标漏斗',
  56. ],
  57. self: [
  58. '肝纯片自身 VOC · 用户画像',
  59. '满意 vs 不满意 · 双面分析',
  60. '藤茶 DMY 认知度 · 用户感知',
  61. ],
  62. competitor: [
  63. '6 大竞品正反面全景(优点+缺点)',
  64. '竞品差评痛点 → 肝纯片机会窗口',
  65. '三轴定位图谱 · 肝纯片无人地带',
  66. ],
  67. position: [
  68. '里斯品类战略 · 护肝品类进化树',
  69. '特劳特定位 · 一句话方案',
  70. '差异化 MOAT · 藤茶 DMY 锚点',
  71. ],
  72. scene: [
  73. '场景#1 · 应酬解酒 × 液体应急',
  74. '场景#2 · 脂肪肝调理 × 日常养护',
  75. '场景#3 · 熬夜前后 × 便携快取',
  76. '场景#4 · 送长辈 × 孝心礼盒',
  77. ],
  78. action: [
  79. '4P 配称 · 产品+渠道+推广+价格',
  80. '10 亿路径 · 3 阶段 Roadmap',
  81. 'P0/P1 行动清单 + 包装优化',
  82. ],
  83. };
  84. const CHAPTER_HERO_LINES = {
  85. landscape: `7715 条真实 VOC(小红书 + 抖音 + Amazon)结构化分类为 <strong style="color:${BRAND.TECH_BLUE};">6 大护肝问题类型</strong>,从"单点式反馈"升级为"<strong>分类归拢 + 共性需求</strong>"视角。江旭原话:"看能不能让 AI 把这么多条整个梳理分类归拢一下,大概分为哪几大类,然后有哪些共性的需求。"`,
  86. self: `江旭指出"<strong style="color:${BRAND.CORAL};">漏掉了我们自己产品的分析</strong>"——肝纯片已在抖音卖了数年,必须回答:<strong>谁在买?为什么买?满意什么?不满意什么?</strong>本章从自身 VOC 中提炼用户画像,找到产品的真实价值感知和改进方向。`,
  87. competitor: `不再只看缺点。6 大竞品(Swisse / 易善复 / 葵花 / 片仔癀 / 海王金樽 / 熊胆粉)全面正反面分析——<strong style="color:${BRAND.JIANG_GREEN};">优点是我们要学习的</strong>,<strong style="color:${BRAND.CORAL};">缺点是我们的机会窗口</strong>。`,
  88. position: `用客户的 <strong style="color:${BRAND.PURPLE};">组织语言</strong>(里斯品类战略 + 特劳特定位)重新组织内容。王总反复强调:"不用这套语言,内部看不懂。"本章用品类进化树找到位置,用心智阶梯找到定位,用 <strong style="color:${BRAND.TENGCHA_GREEN};">藤茶 DMY</strong> 锚定差异化。`,
  89. scene: `<strong style="color:${BRAND.WARM_ORANGE};">应酬解酒 / 脂肪肝调理 / 熬夜前后 / 送长辈</strong>——四大场景各有 VOC 证据链。场景决定话术、场景决定渠道、场景决定包装形态。每个场景给出人群画像 + 核心话术 + 产品形态建议。`,
  90. action: `从 VOC 洞察反推 <strong style="color:${BRAND.JIANG_GREEN};">4P 配称</strong>,收敛为 <strong>10 亿路径 × 3 阶段</strong>:Y1 药线+电商 5 亿验证 → Y2 礼品+餐饮 3 亿 → Y3 国际+升级 2 亿。附包装优化方向(上版"放蓬塔"偏了,需重新定义)。`,
  91. };
  92. // ============================================================
  93. // 工具函数
  94. // ============================================================
  95. function fmtNum(n) {
  96. if (n >= 10000) return (n / 10000).toFixed(1) + '万';
  97. if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
  98. return String(n);
  99. }
  100. function sn(slideNum, slideTotal) { return `<span style="position:absolute;bottom:14px;right:20px;font-family:var(--font-mono);font-size:0.65rem;color:var(--text-3);">${slideNum} / ${slideTotal}</span>`; }
  101. // ============================================================
  102. // 导航
  103. // ============================================================
  104. function renderNav() {
  105. return `<nav id="report-nav" style="position:fixed;top:0;left:0;right:0;z-index:100;background:rgba(255,255,255,0.92);backdrop-filter:blur(6px);border-bottom:1px solid var(--border);padding:6px 20px;display:flex;align-items:center;gap:12px;font-size:0.72rem;font-family:var(--font-mono);overflow-x:auto;white-space:nowrap;">
  106. <a href="#cover" style="font-weight:800;color:${BRAND.JIANG_GREEN};text-decoration:none;letter-spacing:0.15em;">肝纯片 v2</a>
  107. <span style="color:var(--border);">|</span>
  108. ${CHAPTERS.map(c => `<a href="#ch-${c.key}" style="color:var(--text-2);text-decoration:none;padding:2px 6px;border-radius:3px;transition:background .2s;" onmouseover="this.style.background='${c.color}15'" onmouseout="this.style.background='transparent'">${c.icon} ${c.title.split('·')[0].trim()}</a>`).join('')}
  109. </nav>`;
  110. }
  111. // ============================================================
  112. // Cover
  113. // ============================================================
  114. function renderCover(meta) {
  115. return `<section class="report-section cover-section" id="cover" data-tone="liver" style="min-height:100vh;display:flex;flex-direction:column;justify-content:center;padding:60px 40px;">
  116. <div style="max-width:920px;margin:0 auto;text-align:center;">
  117. <div class="text-mono text-xs" style="color:${BRAND.LIVER_AMBER};letter-spacing:0.3em;margin-bottom:18px;font-weight:700;">JIANGZHONG · VOC DEEP INSIGHT · V2</div>
  118. <h1 style="font-family:var(--font-head);font-size:clamp(2rem,5vw,3.2rem);font-weight:900;line-height:1.2;color:var(--text-1);margin-bottom:16px;">
  119. 江中肝纯片<br/><span style="color:${BRAND.LIVER_AMBER};">VOC 深度洞察报告</span> <span style="color:${BRAND.TENGCHA_GREEN};">v2</span>
  120. </h1>
  121. <div style="color:var(--text-2);font-size:1rem;line-height:1.8;max-width:700px;margin:0 auto 30px;">
  122. 基于 <strong>${fmtNum(meta.comments)}</strong> 条真实 VOC(${Object.entries(meta.platforms).map(([p, n]) => `${({xhs:'小红书',douyin:'抖音',amazon:'Amazon'})[p]||p} ${fmtNum(n)}`).join(' + ')})<br/>
  123. <strong>v2 新增</strong>:自身产品画像 · VOC 分类归拢 · 竞品正反面 · 里斯×特劳特定位
  124. </div>
  125. <div style="display:flex;justify-content:center;gap:24px;flex-wrap:wrap;">
  126. ${CHAPTERS.map(c => `<span style="color:${c.color};font-size:0.78rem;">${c.icon} ${c.title.split('·')[0].trim()}</span>`).join('')}
  127. </div>
  128. <div style="margin-top:40px;color:var(--text-3);font-size:0.72rem;">
  129. 采集阶段 ${meta.stage} · ${meta.collectedAt} · ${meta.keywords} 关键词 · ${Object.keys(meta.platforms).length} 平台
  130. </div>
  131. </div>
  132. </section>`;
  133. }
  134. // ============================================================
  135. // Agenda
  136. // ============================================================
  137. function renderAgenda() {
  138. return `<section class="report-section" id="agenda" data-tone="liver">
  139. <div class="section-inner">
  140. <div class="text-mono text-xs" style="color:${BRAND.LIVER_AMBER};letter-spacing:0.25em;margin-bottom:12px;font-weight:700;">REPORT AGENDA · V2</div>
  141. <h2 style="font-family:var(--font-head);font-size:1.8rem;font-weight:800;color:var(--text-1);margin-bottom:24px;">报告目录</h2>
  142. <div style="display:grid;gap:16px;">
  143. ${CHAPTERS.map(c => {
  144. const subs = CHAPTER_SUBSECTIONS[c.key] || [];
  145. return `<div style="padding:18px 22px;background:${c.color}08;border-left:4px solid ${c.color};border-radius:0 var(--radius-sm) var(--radius-sm) 0;">
  146. <a href="#ch-${c.key}" style="text-decoration:none;display:flex;align-items:center;gap:12px;">
  147. <span style="font-size:1.3rem;">${c.icon}</span>
  148. <div>
  149. <div style="font-family:var(--font-head);font-weight:700;color:${c.color};font-size:1rem;">Ch${c.num} · ${c.title}</div>
  150. <div style="color:var(--text-2);font-size:0.78rem;margin-top:2px;">${subs.map((s,i) => `${c.num}.${i+1} ${s}`).join(' · ')}</div>
  151. </div>
  152. </a>
  153. </div>`;
  154. }).join('')}
  155. </div>
  156. </div>
  157. </section>`;
  158. }
  159. // ============================================================
  160. // Chapter Divider
  161. // ============================================================
  162. function renderChapterDivider(c) {
  163. const heroLine = CHAPTER_HERO_LINES[c.key] || c.summary;
  164. const subs = CHAPTER_SUBSECTIONS[c.key] || [];
  165. return `<section class="report-section chapter-divider" id="ch-${c.key}" data-tone="${c.tone}" style="min-height:80vh;display:flex;flex-direction:column;justify-content:center;padding:60px 40px;">
  166. <div style="max-width:860px;margin:0 auto;">
  167. <div class="text-mono text-xs" style="color:${c.color};letter-spacing:0.3em;margin-bottom:14px;font-weight:700;">CHAPTER ${c.num} · ${c.en}</div>
  168. <h2 style="font-family:var(--font-head);font-size:clamp(1.8rem,4vw,2.8rem);font-weight:900;color:var(--text-1);line-height:1.2;margin-bottom:20px;">${c.icon} ${c.title}</h2>
  169. <div style="color:var(--text-1);font-size:0.94rem;line-height:1.82;max-width:780px;margin-bottom:28px;">${heroLine}</div>
  170. <div style="display:flex;gap:14px;flex-wrap:wrap;">
  171. ${subs.map((s, i) => `<a href="#${c.key}-${i+1}" style="padding:6px 14px;background:${c.color}10;border:1px solid ${c.color}33;border-radius:16px;color:${c.color};font-size:0.78rem;text-decoration:none;font-weight:600;">${c.num}.${i+1} ${s.split('·')[0].trim()}</a>`).join('')}
  172. </div>
  173. </div>
  174. </section>`;
  175. }
  176. // ============================================================
  177. // Placeholder fallback
  178. // ============================================================
  179. function renderPlaceholder(c, subIdx, sub, slideNum, slideTotal) {
  180. return `<section class="report-section" id="${c.key}-${subIdx}" data-tone="${c.tone}" data-slide-num="${slideNum}/${slideTotal}">
  181. <div class="section-inner" style="text-align:center;padding:80px 40px;">
  182. <div style="font-size:2.5rem;margin-bottom:16px;">${c.icon}</div>
  183. <div class="text-mono text-xs" style="color:${c.color};letter-spacing:0.2em;font-weight:700;">${c.num}.${subIdx} · 待填充</div>
  184. <h3 style="font-family:var(--font-head);font-size:1.3rem;color:var(--text-1);margin-top:10px;">${esc(sub)}</h3>
  185. </div>
  186. </section>`;
  187. }
  188. // ============================================================
  189. // buildHTML
  190. // ============================================================
  191. function buildHTML({ product, collectedAt } = {}) {
  192. const data = LA.loadMerged();
  193. const meta = LA.getMeta(data);
  194. const slides = [];
  195. slides.push(renderCover(meta));
  196. slides.push(renderAgenda());
  197. let slideCount = 2;
  198. const totalSubs = Object.values(CHAPTER_SUBSECTIONS).reduce((a, s) => a + s.length, 0);
  199. const slideTotal = 2 + CHAPTERS.length + totalSubs;
  200. for (const c of CHAPTERS) {
  201. slideCount++;
  202. slides.push(renderChapterDivider(c));
  203. const subs = CHAPTER_SUBSECTIONS[c.key] || [];
  204. const mod = SUB_MODULES[c.key];
  205. for (let i = 0; i < subs.length; i++) {
  206. slideCount++;
  207. const subIdx = i + 1;
  208. const fnName = `renderSub${subIdx}`;
  209. if (mod && typeof mod[fnName] === 'function') {
  210. try {
  211. slides.push(mod[fnName]({
  212. chapter: c, subIdx, sub: subs[i],
  213. slideNum: slideCount, slideTotal,
  214. data, meta,
  215. }));
  216. } catch (err) {
  217. console.warn(`⚠ ${c.key}.${fnName} error:`, err.message);
  218. slides.push(renderPlaceholder(c, subIdx, subs[i], slideCount, slideTotal));
  219. }
  220. } else {
  221. slides.push(renderPlaceholder(c, subIdx, subs[i], slideCount, slideTotal));
  222. }
  223. }
  224. }
  225. return `<!DOCTYPE html>
  226. <html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
  227. <title>${product || '江中肝纯片'} · VOC 深度洞察报告 v2</title>
  228. ${STYLES}</head><body>
  229. ${renderNav()}
  230. ${slides.join('\n')}
  231. <script>${SCRIPTS}</script>
  232. </body></html>`;
  233. }
  234. module.exports = { buildHTML, CHAPTERS, CHAPTER_SUBSECTIONS, CHAPTER_HERO_LINES };