chapter.template.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. // ==============================================================================
  2. // <品类名> · Ch<N> · <章主题>(模板)
  3. // ==============================================================================
  4. // 使用方法:
  5. // 1. 复制此文件为 <品类>-ch<N>-<topic>.js
  6. // 2. 替换所有 <...> 占位符
  7. // 3. 按实际子章数填充 renderSubN 函数
  8. // 4. 在 module.exports 里映射 '<key>-<subIdx>': renderSubN
  9. // 5. 在 render.js 的 SLIDE_RENDERERS 里导入此文件
  10. //
  11. // 每个子章必含 5 件套:
  12. // ① SectionHead(eyebrow + title + subtitle)
  13. // ② ≥ 3 张 VOC 证据卡(来自 getEvidence)
  14. // ③ 至少 1 个结构化组件(表格 / 矩阵 / 统计条)
  15. // ④ DecisionList 落地决策
  16. // ⑤ data-slide-num 属性
  17. // ==============================================================================
  18. const LA = require('./<品类>-analyze.js');
  19. const C = require('./<品类>-components.js');
  20. const { BRAND, esc, fmtLikes } = C;
  21. // ==========================================================
  22. // Sub 1 · <子章 1 主题>
  23. // ==========================================================
  24. function renderSub1(ctx) {
  25. const { chapter, subIdx, sub, slideNum, slideTotal, data } = ctx;
  26. const items = data.items || [];
  27. // ① 查询证据
  28. const coreVoc = LA.getEvidence(items, {
  29. keyword: '<本品/竞品名>',
  30. contentMatch: /<相关词 1>|<相关词 2>|<相关词 3>/,
  31. minChars: 25,
  32. top: 4,
  33. seed: 7, // 种子用奇数,避免和其他章冲突
  34. });
  35. const painVoc = LA.getEvidence(items, {
  36. contentMatch: /<痛点词 1>|<痛点词 2>/,
  37. sentiment: 'negative',
  38. minChars: 25,
  39. top: 3,
  40. seed: 11,
  41. });
  42. // ② 渲染 HTML
  43. return `<section class="report-section" id="${chapter.key}-${subIdx}"
  44. data-tone="${chapter.tone}" data-slide-num="${slideNum}/${slideTotal}">
  45. <div class="section-inner">
  46. ${C.renderSectionHead({
  47. eyebrow: `${chapter.num}.${subIdx} · <副标题>`,
  48. title: '<span style="color:' + BRAND.MAIN + ';"><关键词></span>:<主标题>',
  49. subtitle: '<副标题描述 · 30-80 字解释 subchapter 要讲什么>',
  50. color: BRAND.MAIN,
  51. })}
  52. <!-- VOC 证据卡 · 核心 -->
  53. <div class="grid" style="grid-template-columns:1fr 1fr; gap:14px; margin:20px 0;">
  54. ${coreVoc.map((it) => C.renderVocCard(it, { accent: BRAND.MAIN, maxChars: 200 })).join('')}
  55. </div>
  56. <!-- KPI / 统计条 -->
  57. ${C.renderStatStrip([
  58. { num: '<数字 1>', label: '<说明>', color: BRAND.MAIN },
  59. { num: '<数字 2>', label: '<说明>', color: BRAND.JIANG_GREEN },
  60. { num: '<数字 3>', label: '<说明>', color: BRAND.CORAL },
  61. { num: '<数字 4>', label: '<说明>', color: BRAND.PURPLE },
  62. ], { accent: BRAND.MAIN })}
  63. <!-- Insight Hero · 金句 -->
  64. ${C.renderInsightHero({
  65. eyebrow: '💡 KEY INSIGHT · 关键洞察',
  66. title: '<一句话金句观点(25-50 字,带 strong 强调)>',
  67. subtitle: '<进一步解释或证据补充>',
  68. color: BRAND.MAIN,
  69. })}
  70. <!-- 痛点 VOC 二排 -->
  71. <div class="grid" style="grid-template-columns:1fr 1fr 1fr; gap:14px; margin:24px 0;">
  72. ${painVoc.map((it) => C.renderVocCard(it, { accent: BRAND.CORAL, compact: true, maxChars: 140 })).join('')}
  73. </div>
  74. <!-- 落地决策 -->
  75. ${C.renderDecisionList([
  76. `<strong>P0 · 决策 1</strong>:<具体动作>`,
  77. `<strong>P1 · 决策 2</strong>:<具体动作>`,
  78. `<strong>P2 · 决策 3</strong>:<具体动作>`,
  79. `<strong>P3 · 决策 4</strong>:<具体动作>`,
  80. ], { title: '落地决策', accent: BRAND.MAIN, columns: 2 })}
  81. </div>
  82. </section>`;
  83. }
  84. // ==========================================================
  85. // Sub 2 · <子章 2 主题>
  86. // ==========================================================
  87. function renderSub2(ctx) {
  88. const { chapter, subIdx, sub, slideNum, slideTotal, data } = ctx;
  89. const items = data.items || [];
  90. // 以竞品对比为例
  91. const voc = LA.getEvidence(items, {
  92. keyword: '<竞品名>',
  93. top: 4,
  94. seed: 13,
  95. });
  96. return `<section class="report-section" id="${chapter.key}-${subIdx}"
  97. data-tone="${chapter.tone}" data-slide-num="${slideNum}/${slideTotal}">
  98. <div class="section-inner">
  99. ${C.renderSectionHead({
  100. eyebrow: `${chapter.num}.${subIdx} · <子章副标题>`,
  101. title: '<主标题>',
  102. color: BRAND.CORAL,
  103. })}
  104. <!-- 对比表格 -->
  105. ${C.renderCompareTable({
  106. title: 'COMPETITOR COMPARISON',
  107. headers: ['维度', '本品', '<竞品 A>', '<竞品 B>'],
  108. rows: [
  109. [
  110. '核心成分',
  111. { html: '<strong>本品</strong>', color: BRAND.MAIN, bold: true },
  112. { html: '<竞品 A>', color: 'var(--text-2)' },
  113. { html: '<竞品 B>', color: 'var(--text-2)' },
  114. ],
  115. ['价格', '¥<...>', '¥<...>', '¥<...>'],
  116. ['批号', 'OTC', '食品', '保健品'],
  117. ],
  118. accent: BRAND.CORAL,
  119. })}
  120. <!-- VOC 证据 -->
  121. <div class="grid" style="grid-template-columns:1fr 1fr; gap:14px; margin:24px 0;">
  122. ${voc.map((it) => C.renderVocCard(it, { accent: BRAND.CORAL })).join('')}
  123. </div>
  124. ${C.renderDecisionList([
  125. `<strong>决策 1</strong>:<...>`,
  126. `<strong>决策 2</strong>:<...>`,
  127. ], { accent: BRAND.CORAL })}
  128. </div>
  129. </section>`;
  130. }
  131. // ==========================================================
  132. // Sub 3 · <子章 3 主题>(2×2 矩阵示例)
  133. // ==========================================================
  134. function renderSub3(ctx) {
  135. const { chapter, subIdx, sub, slideNum, slideTotal, data } = ctx;
  136. return `<section class="report-section" id="${chapter.key}-${subIdx}"
  137. data-tone="${chapter.tone}" data-slide-num="${slideNum}/${slideTotal}">
  138. <div class="section-inner">
  139. ${C.renderSectionHead({
  140. eyebrow: `${chapter.num}.${subIdx} · 无人地带坐标`,
  141. title: '<主标题>',
  142. color: BRAND.JIANG_GREEN,
  143. })}
  144. ${C.renderMatrix2x2({
  145. xLabel: '<X 轴名>',
  146. yLabel: '<Y 轴名>',
  147. xLow: '<X 低端>',
  148. xHigh: '<X 高端>',
  149. yLow: '<Y 低端>',
  150. yHigh: '<Y 高端>',
  151. accent: BRAND.JIANG_GREEN,
  152. quadrants: {
  153. q1: {
  154. title: '右上 · <象限 1>',
  155. color: BRAND.TECH_BLUE,
  156. items: ['<竞品 A>', '<竞品 B>'],
  157. note: '痛点:<...>',
  158. },
  159. q2: {
  160. title: '左上 · <象限 2>',
  161. color: BRAND.WINE_RED,
  162. items: ['<竞品 C>'],
  163. note: '痛点:<...>',
  164. },
  165. q3: {
  166. title: '左下 · <象限 3>',
  167. color: BRAND.WARM_ORANGE,
  168. items: ['<竞品 D>'],
  169. note: '<...>',
  170. },
  171. q4: {
  172. title: '右下 · <象限 4>(<strong style="color:' + BRAND.JIANG_GREEN + ';">无人地带</strong>)',
  173. color: BRAND.JIANG_GREEN,
  174. items: ['<strong>✨ 本品独占</strong>'],
  175. note: '✅ 机会:<...>',
  176. },
  177. },
  178. })}
  179. ${C.renderDecisionList([
  180. `<strong>锁定象限</strong>:<...>`,
  181. `<strong>瓦解对手</strong>:<...>`,
  182. ], { accent: BRAND.JIANG_GREEN })}
  183. </div>
  184. </section>`;
  185. }
  186. // ==========================================================
  187. // 导出
  188. // TODO: 按实际 subs 数量增减
  189. // ==========================================================
  190. module.exports = {
  191. '<chapter-key>-1': renderSub1,
  192. '<chapter-key>-2': renderSub2,
  193. '<chapter-key>-3': renderSub3,
  194. // '<chapter-key>-4': renderSub4, // 如有更多子章
  195. // '<chapter-key>-5': renderSub5,
  196. };