lactic-v2-render.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. // ==============================================================================
  2. // 江中乳酸菌素片儿童版 · VOC 深度洞察报告 v2 · 主渲染器
  3. // ==============================================================================
  4. // 叙事线:王总 5 步分析路径
  5. // Ch0 品类全景 · 肠道用药大盘(Step 1) 3 子章
  6. // Ch1 成人乳酸菌素片 · 品类机会(Step 2) 3 子章
  7. // Ch2 成人→儿童 · 机会迁移(Step 3) 3 子章
  8. // Ch3 儿童微生态 · 交叉验证(Step 4) 3 子章
  9. // Ch4 品类定位 · 里斯×特劳特(Step 5) 4 子章
  10. // Ch5 落地行动 3 子章
  11. // 总计 27 slides(cover + agenda + 6 divider + 19 子章)
  12. // ==============================================================================
  13. const STYLES = require('./voc-styles.js');
  14. const SCRIPTS = require('./voc-scripts.js');
  15. const LA = require('./lactic-analyze.js');
  16. const C = require('./lactic-components.js');
  17. const { BRAND, esc, fmtLikes, renderSectionHead, renderVocCard, renderVocGrid,
  18. renderMatrix2x2, renderStatStrip, renderBarChart, renderCompareTable,
  19. renderFindingCard, renderInsightHero, renderDecisionList, renderTagCloud,
  20. renderTheoryBox, renderHypothesisBadge } = C;
  21. // ============================================================
  22. // 6 章元信息(对应王总 5 步 + 落地)
  23. // ============================================================
  24. const CHAPTERS = [
  25. { id: 0, num: '0', key: 'category', title: '品类全景 · 肠道用药大盘', en: 'CATEGORY LANDSCAPE', tone: 'blue', color: BRAND.TECH_BLUE, icon: '🔭',
  26. summary: '微生态制剂 × 肠炎止泻 × 润肠通便 全量扫描 · TAM→SAM→SOM' },
  27. { id: 1, num: '1', key: 'adult', title: '成人乳酸菌素片 · 品类机会', en: 'ADULT OPPORTUNITY', tone: 'amber', color: BRAND.WARM_ORANGE, icon: '💊',
  28. summary: '成人 VOC 全景 · 品类机会点 · 竞品差异化空白' },
  29. { id: 2, num: '2', key: 'migrate', title: '成人→儿童 · 机会迁移', en: 'MIGRATION', tone: 'rose', color: BRAND.BERRY_PINK, icon: '🔄',
  30. summary: '成人 VOC 中的儿童信号 · 家长决策路径 · 可迁移机会矩阵' },
  31. { id: 3, num: '3', key: 'cross', title: '儿童微生态 · 交叉验证', en: 'CROSS VALIDATION', tone: 'purple', color: BRAND.PURPLE, icon: '🔬',
  32. summary: '儿童产品全量扫描 · 竞品空白 × 我们能接住的交叉点' },
  33. { id: 4, num: '4', key: 'position', title: '品类定位 · 里斯×特劳特', en: 'POSITIONING', tone: 'green', color: BRAND.JIANG_GREEN, icon: '🎯',
  34. summary: '里斯品类战略 · 特劳特一句话定位 · 三条机会 VOC 验证' },
  35. { id: 5, num: '5', key: 'action', title: '落地行动', en: 'ACTION PLAN', tone: 'green', color: BRAND.LACTIC_MINT, icon: '🚀',
  36. summary: '产品策略 · 渠道策略 · P0/P1 行动清单' },
  37. ];
  38. // ============================================================
  39. // 各章节 subsections 清单
  40. // ============================================================
  41. const CHAPTER_SUBSECTIONS = {
  42. 'category': [
  43. '微生态制剂 × 肠炎止泻 × 润肠通便 · 三大品类扫描',
  44. '各品类人群需求标签 × 解决问题矩阵',
  45. 'TAM→SAM→SOM · 市场漏斗与乳酸菌素片区间',
  46. ],
  47. 'adult': [
  48. '成人乳酸菌素片 VOC 全景 · 平台×情感×主题',
  49. '成人品类机会点 · 竞品差异化空白',
  50. '品类核心机会提炼 · 可向儿童迁移的信号',
  51. ],
  52. 'migrate': [
  53. '成人 VOC 中的儿童信号扫描 · "妈妈/小孩/儿童"',
  54. '家长购买决策路径 · 双决策者模型(家长买×孩子吃)',
  55. '可迁移 vs 不可迁移 · 机会矩阵',
  56. ],
  57. 'cross': [
  58. '儿童微生态制剂全量扫描 · 妈咪爱/亿活/合生元等',
  59. '交叉验证 · 竞品没满足 + 我们能满足的空白',
  60. '竞品正反面精简分析 · 药品 vs 食品 vs 营养品',
  61. ],
  62. 'position': [
  63. '里斯品类战略 · 我们在哪个品类?',
  64. '特劳特定位 · 一句话定位方案',
  65. '三条细分机会 VOC 验证 · 肠道调理/免疫力/营养吸收',
  66. '风险评估 · 功效扩展 vs 原有心智冲突',
  67. ],
  68. 'action': [
  69. '产品策略 · 包装/剂型/话术',
  70. '渠道策略 · 分年龄段打法',
  71. 'P0/P1 行动清单 × 时间线',
  72. ],
  73. };
  74. // ============================================================
  75. // 每章 hero line
  76. // ============================================================
  77. const CHAPTER_HERO_LINES = {
  78. category: `王总明确要求:<strong style="color:${BRAND.TECH_BLUE};">"从整个肠道品类入手,不是从乳酸菌素片入手"</strong>。本章从 <strong>微生态制剂(益生菌+乳酸菌素片)、肠炎止泻药、润肠通便药</strong> 三大品类出发,扫描 <strong style="color:${BRAND.LACTIC_MINT};">100+ 亿肠道市场</strong> 的人群需求、标签、解决问题,建立 TAM→SAM→SOM 漏斗。`,
  79. adult: `王总第二步:<strong style="color:${BRAND.WARM_ORANGE};">"聚焦到乳酸菌素片这个品类,先做成人的完整 VOC 分析"</strong>。成人乳酸菌素片的市场机会是什么?与竞品(整肠生/培菲康/妈咪爱成人版)的差异在哪里?哪些机会可以向儿童迁移?`,
  80. migrate: `王总第三步:<strong style="color:${BRAND.BERRY_PINK};">"从成人机会迁移到儿童"</strong>——成人 VOC 中能否扒拉出儿童相关的?成人的需求能否迁移到儿童?<strong>妈妈买给孩子用的这部分 VOC</strong> 是最关键的桥梁。`,
  81. cross: `王总第四步:<strong style="color:${BRAND.PURPLE};">"反向交叉验证"</strong>——用微生态制剂大品类中的"儿童"部分,与 Ch2 得到的儿童机会做交叉,找到<strong style="color:${BRAND.JIANG_GREEN};">"竞品没满足的 + 我们能满足的"</strong>机会。`,
  82. position: `王总第五步:<strong style="color:${BRAND.JIANG_GREEN};">"用里斯品类战略 + 特劳特定位收敛"</strong>——我在哪个品类?怎么定位?<strong>容易传播的、能写在盒子上的一句话</strong>。三条细分机会(肠道调理/免疫力/营养吸收)逐一 VOC 验证,评估功效扩展与原有心智的冲突风险。`,
  83. action: `基于 Ch0-Ch4 的层层收敛,输出 <strong style="color:${BRAND.LACTIC_MINT};">可落地的行动清单</strong>:产品策略(包装/剂型/话术)、渠道策略(分年龄段打法)、P0/P1 优先级时间线。从 <strong style="color:${BRAND.JIANG_GREEN};">6000 万到 10 亿的三年路径</strong>。`,
  84. };
  85. // ============================================================
  86. // utils
  87. // ============================================================
  88. function sn(n) { return String(n).padStart(2, '0'); }
  89. // ============================================================
  90. // 共用组件
  91. // ============================================================
  92. function renderNav() {
  93. const dots = [
  94. { tip: '封面', href: '#s0' },
  95. { tip: '目录', href: '#agenda' },
  96. ...CHAPTERS.map((c) => ({ tip: `${c.num} ${c.title}`, href: `#${c.key}` })),
  97. ];
  98. return `<nav class="nav-rail" aria-label="章节导航">
  99. ${dots.map((d, i) => `<a class="nav-dot${i === 0 ? ' active' : ''}" href="${d.href}" data-tip="${esc(d.tip)}" data-idx="${i}"></a>`).join('')}
  100. </nav>
  101. <div class="top-bar">
  102. <div class="brand-logo">江中<span class="dot">·</span>产品开发部 / 乳酸菌素片儿童版 VOC 深度洞察 v2.0</div>
  103. <div class="text-mono text-xs" style="color:var(--text-3);">王总 5 步路径 / 品类→成人→迁移→交叉→定位</div>
  104. </div>`;
  105. }
  106. function renderKbdHint() {
  107. return `<div class="kbd-hint" id="kbdHint">
  108. <span>键盘切换</span>
  109. <kbd>↑</kbd><kbd>↓</kbd>
  110. <span>或</span>
  111. <kbd>PgUp</kbd><kbd>PgDn</kbd>
  112. </div>`;
  113. }
  114. // ============================================================
  115. // Cover
  116. // ============================================================
  117. function renderCover({ product, collectedAt, slideNum, slideTotal, meta }) {
  118. const totalComments = meta?.comments || 0;
  119. const platformCount = Object.values(meta?.platforms || {}).filter((n) => n > 0).length;
  120. const commentsDisplay = totalComments ? totalComments.toLocaleString() : '待采集';
  121. return `<section class="report-section" id="s0" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}" style="min-height:100vh;padding:0;">
  122. <div class="cover-hero">
  123. <div class="aurora" aria-hidden="true"><span></span><span></span><span></span></div>
  124. <div class="cover-grid-bg"></div>
  125. <div class="cover-content">
  126. <div class="cover-meta rise-in" style="display:flex; justify-content:space-between; align-items:center; gap:16px; flex-wrap:wrap;">
  127. <span>江中 / 产品开发部 / 2026 · ${esc(product)} · VOC 深度洞察 v2</span>
  128. <span style="display:inline-flex; align-items:center; gap:6px; padding:4px 12px; background:${BRAND.JIANG_GREEN}15; color:${BRAND.JIANG_GREEN}; border:1px solid ${BRAND.JIANG_GREEN}55; border-radius:16px; font-family:var(--font-mono); font-size:0.72rem; letter-spacing:0.12em; font-weight:600;">
  129. <span style="font-size:0.9rem;">✓</span>王总 5 步路径 · 结构重构版
  130. </span>
  131. </div>
  132. <div class="cover-title-stack">
  133. <p class="text-mono rise-in" style="color:${BRAND.LACTIC_MINT}; font-size:0.9rem;letter-spacing:0.2em;margin-bottom:18px;">品类全景 → 成人机会 → 儿童迁移 → 交叉验证 → 里斯×特劳特定位</p>
  134. <h1 class="cover-title rise-in">${esc(product)}<br/><span style="color:${BRAND.LACTIC_MINT}; -webkit-text-fill-color:${BRAND.LACTIC_MINT}; background:none; font-size:0.66em;">VOC 深度洞察报告 v2</span></h1>
  135. <p class="text-poster rise-in" style="margin-top:20px;color:var(--text-2); font-size:1.42rem;">
  136. 严格遵循<strong style="color:${BRAND.LACTIC_MINT};">王总 5 步分析路径</strong> · 从品类入手而非产品入手
  137. </p>
  138. </div>
  139. <p class="cover-sub fade-up">
  140. 基于 <strong style="color:${BRAND.LACTIC_MINT};">0420 会议王总核心分析路径</strong>重构:
  141. Step1 <strong style="color:${BRAND.TECH_BLUE};">肠道品类全景</strong> →
  142. Step2 <strong style="color:${BRAND.WARM_ORANGE};">成人乳酸菌素片品类机会</strong> →
  143. Step3 <strong style="color:${BRAND.BERRY_PINK};">成人→儿童机会迁移</strong> →
  144. Step4 <strong style="color:${BRAND.PURPLE};">儿童微生态交叉验证</strong> →
  145. Step5 <strong style="color:${BRAND.JIANG_GREEN};">里斯品类战略 × 特劳特定位收敛</strong>。
  146. 以 <strong style="color:${BRAND.LACTIC_MINT};">${commentsDisplay}</strong> 条真实家长 VOC 为证据底座。
  147. </p>
  148. <div class="cover-stats fade-up" style="grid-template-columns:repeat(5, 1fr); max-width:960px;">
  149. <div class="cover-stat"><div class="num">5 步</div><div class="lbl">王总分析路径</div></div>
  150. <div class="cover-stat"><div class="num">${commentsDisplay}</div><div class="lbl">真实家长 VOC</div></div>
  151. <div class="cover-stat"><div class="num">${platformCount}</div><div class="lbl">采集平台</div></div>
  152. <div class="cover-stat"><div class="num">6</div><div class="lbl">章节</div></div>
  153. <div class="cover-stat"><div class="num">${slideTotal}</div><div class="lbl">slides</div></div>
  154. </div>
  155. <p class="text-mono text-xs fade-up" style="margin-top:40px; color:var(--text-3);">SCROLL DOWN · 采集截止 ${esc(collectedAt || '')} · ${commentsDisplay} 条真实 VOC</p>
  156. </div>
  157. </div>
  158. </section>`;
  159. }
  160. // ============================================================
  161. // Agenda
  162. // ============================================================
  163. function renderAgenda({ slideNum, slideTotal }) {
  164. return `<section class="report-section" id="agenda" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}">
  165. <div class="section-inner">
  166. <div class="section-head rise-in">
  167. <div class="section-eyebrow" style="color:${BRAND.LACTIC_MINT};">CONTENTS · 王总 5 步路径 + 落地</div>
  168. <h2 class="text-h1"><span class="headline-anim">从品类入手 · 层层收敛到一句话定位</span></h2>
  169. <p class="section-sub">严格遵循王总要求:<strong style="color:${BRAND.LACTIC_MINT};">品类全景→成人机会→儿童迁移→交叉验证→定位收敛→落地</strong></p>
  170. </div>
  171. <div class="grid" style="grid-template-columns:repeat(2, 1fr); gap:22px; margin-top:20px;">
  172. ${CHAPTERS.map((c, i) => `
  173. <a href="#${c.key}" class="fade-up" style="text-decoration:none; display:block; background:var(--bg-1); border:1px solid var(--border); border-left:4px solid ${c.color}; border-radius:var(--radius-md); padding:24px; transition:all .35s cubic-bezier(.2,.7,.2,1); position:relative; overflow:hidden;" onmouseover="this.style.transform='translateY(-4px)'; this.style.borderColor='${c.color}'; this.style.boxShadow='0 20px 60px -20px ${c.color}55'" onmouseout="this.style.transform='translateY(0)'; this.style.borderColor='var(--border)'; this.style.boxShadow='none'">
  174. <div style="position:absolute; top:-18px; right:-10px; font-family:var(--font-head); font-weight:900; font-size:8rem; line-height:1; color:${c.color}; opacity:.08; letter-spacing:-0.06em; pointer-events:none;">${c.num}</div>
  175. <div style="display:flex; justify-content:space-between; align-items:start; margin-bottom:14px;">
  176. <div class="text-mono text-xs" style="color:${c.color}; letter-spacing:0.18em; font-weight:700;">CH ${c.num} · ${c.en} ${i < 5 ? `· STEP ${i + 1}` : ''}</div>
  177. <div style="font-size:1.6rem; filter:grayscale(20%);">${c.icon}</div>
  178. </div>
  179. <h3 class="text-h2" style="margin:0 0 10px; color:var(--text-1); font-size:1.38rem;">${esc(c.title)}</h3>
  180. <p style="color:var(--text-2); font-size:0.88rem; line-height:1.6; margin-bottom:12px;">${esc(c.summary)}</p>
  181. <div style="display:flex; flex-wrap:wrap; gap:5px; margin-bottom:12px;">
  182. ${(CHAPTER_SUBSECTIONS[c.key] || []).slice(0, 5).map((sub, j) => `<span style="padding:3px 8px; background:${c.color}15; color:${c.color}; border-radius:3px; font-size:0.66rem; font-weight:600;">${c.num}.${j + 1} ${esc(sub.split('·')[0])}</span>`).join('')}
  183. </div>
  184. </a>`).join('')}
  185. </div>
  186. <div class="fade-up" style="margin-top:44px; padding:22px 26px; background:linear-gradient(135deg, ${BRAND.LACTIC_MINT}10, ${BRAND.JIANG_GREEN}06); border:1px solid ${BRAND.LACTIC_MINT}22; border-radius:var(--radius-md);">
  187. <div class="text-mono text-xs" style="color:${BRAND.LACTIC_MINT}; letter-spacing:0.15em; margin-bottom:10px;">METHODOLOGY · 王总分析路径</div>
  188. <div style="color:var(--text-1); font-size:0.98rem; line-height:1.72;">
  189. 本报告 v2 版严格遵循 <strong style="color:${BRAND.LACTIC_MINT};">0420 会议王总 5 步分析路径</strong>重构,解决 v1 版"<strong style="color:${BRAND.CORAL};">太散太乱,不知道怎么看怎么用</strong>"的问题:<br/>
  190. ① 品类全景入手 → ② 成人品类机会 → ③ 机会迁移到儿童 → ④ 反向交叉验证 → ⑤ 里斯品类战略 + 特劳特定位收敛。<br/>
  191. 使用客户熟悉的 <strong style="color:${BRAND.JIANG_GREEN};">里斯 + 特劳特</strong> 框架,而非 KANO/混沌等不适用模型。
  192. </div>
  193. </div>
  194. </div>
  195. </section>`;
  196. }
  197. // ============================================================
  198. // Chapter Divider
  199. // ============================================================
  200. function renderChapterDivider({ chapter, slideNum, slideTotal }) {
  201. const c = chapter;
  202. const heroLine = CHAPTER_HERO_LINES[c.key] || c.summary;
  203. const subs = CHAPTER_SUBSECTIONS[c.key] || [];
  204. const stepLabel = c.id < 5 ? `STEP ${c.id + 1} · ` : '';
  205. return `<section class="report-section" id="${c.key}" data-tone="${c.tone}" data-slide-num="${slideNum}/${slideTotal}" style="justify-content:center;">
  206. <div class="section-inner">
  207. <div class="rise-in" style="text-align:center; margin-bottom:40px;">
  208. <div style="font-family:var(--font-head); font-weight:900; font-size:clamp(5rem, 14vw, 11rem); line-height:0.9; color:${c.color}; opacity:0.12; letter-spacing:-0.06em;">0${c.num}</div>
  209. <div class="text-mono text-xs" style="color:${c.color}; letter-spacing:0.3em; margin-top:-2rem; font-weight:700;">${stepLabel}CHAPTER ${c.num} · ${c.en}</div>
  210. </div>
  211. <div class="rise-in" style="text-align:center; margin-bottom:36px;">
  212. <div style="font-size:3.2rem; margin-bottom:14px;">${c.icon}</div>
  213. <h2 class="text-poster" style="font-size:clamp(2.4rem, 5.2vw, 4.2rem); color:var(--text-1); letter-spacing:-0.025em;">${esc(c.title)}</h2>
  214. <p style="color:${c.color}; font-family:var(--font-mono); font-size:0.92rem; letter-spacing:0.18em; margin-top:12px; font-weight:600;">${c.en}</p>
  215. </div>
  216. <div class="fade-up" style="max-width:960px; margin:0 auto; text-align:center; padding:28px 32px; background:linear-gradient(135deg, ${c.color}0C, transparent); border-left:3px solid ${c.color}; border-radius:var(--radius-md);">
  217. <p style="color:var(--text-1); font-size:1.08rem; line-height:1.82;">${heroLine}</p>
  218. </div>
  219. <div class="fade-up" style="margin-top:40px; display:grid; grid-template-columns:repeat(${Math.min(subs.length, 4)}, 1fr); gap:14px; max-width:1100px; margin-left:auto; margin-right:auto;">
  220. ${subs.map((sub, i) => `
  221. <div style="padding:16px 14px; background:var(--bg-1); border:1px solid ${c.color}22; border-radius:var(--radius-sm); text-align:center;">
  222. <div class="text-mono text-xs" style="color:${c.color}; letter-spacing:0.15em; margin-bottom:6px; font-weight:700;">${c.num}.${i + 1}</div>
  223. <div style="color:var(--text-1); font-size:0.86rem; line-height:1.5;">${esc(sub)}</div>
  224. </div>`).join('')}
  225. </div>
  226. <div class="fade-up" style="margin-top:48px; text-align:center;">
  227. <div class="text-mono text-xs" style="color:var(--text-3); letter-spacing:0.3em;">SCROLL DOWN · ${subs.length} 个子章</div>
  228. </div>
  229. </div>
  230. </section>`;
  231. }
  232. // ============================================================
  233. // 子章渲染器注册
  234. // ============================================================
  235. function tryRequire(modPath) {
  236. try { return require(modPath); } catch (e) { return null; }
  237. }
  238. const CH0 = tryRequire('./lactic-v2-ch0-category.js');
  239. const CH1 = tryRequire('./lactic-v2-ch1-adult.js');
  240. const CH2 = tryRequire('./lactic-v2-ch2-migrate.js');
  241. const CH3 = tryRequire('./lactic-v2-ch3-cross.js');
  242. const CH4 = tryRequire('./lactic-v2-ch4-position.js');
  243. const CH5 = tryRequire('./lactic-v2-ch5-action.js');
  244. // ============================================================
  245. // buildHTML · 主装配
  246. // ============================================================
  247. function buildHTML({ product, collectedAt } = {}) {
  248. product = product || '江中乳酸菌素片儿童版';
  249. collectedAt = collectedAt || new Date().toISOString().slice(0, 10);
  250. const data = LA.loadMerged();
  251. const meta = LA.getMeta(data);
  252. // slide 总数
  253. let slideTotal = 2; // cover + agenda
  254. for (const c of CHAPTERS) {
  255. slideTotal += 1 + (CHAPTER_SUBSECTIONS[c.key] || []).length;
  256. }
  257. const slides = [];
  258. let num = 1;
  259. slides.push(renderCover({ product, collectedAt, slideNum: sn(num), slideTotal: sn(slideTotal), meta }));
  260. num++;
  261. slides.push(renderAgenda({ slideNum: sn(num), slideTotal: sn(slideTotal) }));
  262. num++;
  263. const chModules = [CH0, CH1, CH2, CH3, CH4, CH5];
  264. for (let ci = 0; ci < CHAPTERS.length; ci++) {
  265. const c = CHAPTERS[ci];
  266. slides.push(renderChapterDivider({ chapter: c, slideNum: sn(num), slideTotal: sn(slideTotal) }));
  267. num++;
  268. const subs = CHAPTER_SUBSECTIONS[c.key] || [];
  269. const mod = chModules[ci];
  270. for (let i = 0; i < subs.length; i++) {
  271. const subIdx = i + 1;
  272. const renderFn = mod && mod[`renderSub${subIdx}`];
  273. const ctx = {
  274. chapter: c,
  275. subIdx,
  276. sub: subs[i],
  277. slideNum: sn(num),
  278. slideTotal: sn(slideTotal),
  279. data,
  280. meta,
  281. };
  282. if (renderFn) {
  283. slides.push(renderFn(ctx));
  284. } else {
  285. slides.push(renderPlaceholder(ctx));
  286. }
  287. num++;
  288. }
  289. }
  290. return `<!DOCTYPE html>
  291. <html lang="zh-CN">
  292. <head>
  293. <meta charset="UTF-8"/>
  294. <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  295. <title>${esc(product)} · VOC 深度洞察报告 v2 · 王总 5 步路径</title>
  296. ${STYLES}
  297. </head>
  298. <body>
  299. ${renderNav()}
  300. ${renderKbdHint()}
  301. <div class="progress-line" id="progressLine"></div>
  302. <main class="deck">
  303. ${slides.join('\n')}
  304. </main>
  305. <script>${SCRIPTS}</script>
  306. </body>
  307. </html>`;
  308. }
  309. function renderPlaceholder(ctx) {
  310. const { chapter: c, subIdx, sub, slideNum, slideTotal } = ctx;
  311. return `<section class="report-section" id="${c.key}-${subIdx}" data-tone="${c.tone}" data-slide-num="${slideNum}/${slideTotal}">
  312. <div class="section-inner">
  313. ${renderSectionHead({
  314. eyebrow: `${c.num}.${subIdx} · ${c.en}`,
  315. title: sub || 'Coming Soon',
  316. subtitle: '此子章将在后续批次填充。',
  317. color: c.color,
  318. })}
  319. <div style="margin-top:40px; padding:40px; background:${c.color}08; border:1px dashed ${c.color}55; border-radius:var(--radius-lg); text-align:center;">
  320. <div style="font-size:3.4rem; margin-bottom:18px;">🚧</div>
  321. <div class="text-h2" style="color:${c.color};">待填充</div>
  322. </div>
  323. </div>
  324. </section>`;
  325. }
  326. module.exports = {
  327. buildHTML,
  328. BRAND,
  329. CHAPTERS,
  330. CHAPTER_SUBSECTIONS,
  331. CHAPTER_HERO_LINES,
  332. };