liver-v2-ch2-competitor.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. // ==============================================================================
  2. // 肝纯片 v2 · Ch2 竞品正反面 · 全面分析(会议 req C)
  3. // ==============================================================================
  4. // 2.1 6 大竞品正反面全景(优点+缺点)
  5. // 2.2 竞品差评痛点 → 肝纯片机会窗口
  6. // 2.3 三轴定位图谱 · 肝纯片无人地带
  7. // ==============================================================================
  8. const LA = require('./liver-analyze.js');
  9. const C = require('./liver-components.js');
  10. const { BRAND, esc, renderSectionHead, renderVocCard, renderVocGrid,
  11. renderStatStrip, renderMatrix2x2, renderCompareTable, renderInsightHero,
  12. renderFindingCard, renderDecisionList, isEnglish, getTranslation, LIVER_CONTEXT_RE } = C;
  13. // ============================================================
  14. // 2.1 6 大竞品正反面(从 VOC 提取真实好评/差评)
  15. // ============================================================
  16. function renderSub1({ chapter: c, subIdx, sub, slideNum, slideTotal, data, meta }) {
  17. const items = data?.items || [];
  18. // 正面/负面内容匹配模式(因 sentiment 推断偏弱,用内容 regex 补充)
  19. // negRe 增加中文否定句式:不怎么管用/不太好/不太行
  20. const negRe = /没有?用(?!过)|没效|没感觉|no effect|didn.?t work|副作用|拉肚子|胃不舒服|难吃|太贵|智商税|骗|坑|差评|上当|头晕|不降反升|不推荐|难受|不怎么管用|不太好用|不太行|不管用|不好用|无效|过敏|side.?effect|stomach|伤肝|别吃/i;
  21. const posRe = /有效|管用|好用|推荐|不错|下来了|降了|好转|有用的|真有用|确实有用|效果好|缓解|改善|靠谱|信赖|放心|好评|好转|值得/i;
  22. const competitors = [
  23. { name: 'Swisse 护肝片', product: 'Swisse 护肝片', color: '#003087', seed: 201,
  24. alsoMatch: /swisse|澳洲护肝|澳佳宝/i },
  25. { name: '易善复', product: '易善复', color: '#1A5276', seed: 202,
  26. alsoMatch: /易善复|多烯磷脂酰胆碱|磷脂/i },
  27. { name: '葵花护肝片', product: '葵花护肝片', color: '#F39C12', seed: 203,
  28. alsoMatch: /葵花|护肝片.*葵花/i },
  29. { name: '片仔癀', product: '片仔癀护肝', color: '#C0392B', seed: 204,
  30. alsoMatch: /片仔癀/i },
  31. { name: '海王金樽', product: '海王金樽', color: '#2E86C1', seed: 205,
  32. alsoMatch: /海王|金樽|解酒液/i },
  33. { name: '水飞蓟/奶蓟草', product: '水飞蓟成分', color: '#6C3483', seed: 206,
  34. alsoMatch: /水飞蓟|奶蓟|silymarin|milk thistle/i },
  35. ];
  36. // 为每个竞品从 VOC 中提取正面和负面原声
  37. const cpData = competitors.map(cp => {
  38. // 匹配该竞品的所有 VOC(内容必须同时提到护肝相关语境,排除片仔癀牙痛/皮肤等无关帖)
  39. const cpItems = items.filter(it => {
  40. const matched = (it.product || '') === cp.product || cp.alsoMatch.test(`${it.content || ''} ${it.keyword || ''}`);
  41. if (!matched) return false;
  42. return LIVER_CONTEXT_RE.test(it.content || '');
  43. });
  44. // 按内容 regex 分正负面
  45. const posItems = cpItems.filter(it => posRe.test(it.content || '') && !negRe.test(it.content || ''));
  46. const negItems = cpItems.filter(it => negRe.test(it.content || ''));
  47. // 取代表性原声(cpItems 已经通过 cp.alsoMatch + LIVER_CONTEXT_RE 双重过滤,不再重复 contentMatch)
  48. const posEvidence = LA.getEvidence(posItems, { top: 2, seed: cp.seed, minChars: 15 });
  49. const negEvidence = LA.getEvidence(negItems, { top: 2, seed: cp.seed + 50, minChars: 15 });
  50. return { ...cp, total: cpItems.length, posCount: posItems.length, negCount: negItems.length, posEvidence, negEvidence };
  51. });
  52. return `<section class="report-section" id="${c.key}-${subIdx}" data-tone="${c.tone}" data-slide-num="${slideNum}/${slideTotal}">
  53. <div class="section-inner">
  54. ${renderSectionHead({
  55. eyebrow: `${c.num}.${subIdx} · COMPETITOR PROS & CONS`,
  56. title: sub,
  57. subtitle: '江旭要求:"各个竞品,除了缺点还有哪些优点,全面来分析一下。"——以下优缺点均从真实 VOC 提取。',
  58. color: c.color,
  59. })}
  60. ${renderStatStrip([
  61. { num: cpData.reduce((s, cp) => s + cp.total, 0), label: '竞品相关 VOC 总量', color: c.color },
  62. { num: cpData.reduce((s, cp) => s + cp.posCount, 0), label: '正面评价', color: BRAND.JIANG_GREEN },
  63. { num: cpData.reduce((s, cp) => s + cp.negCount, 0), label: '负面评价', color: BRAND.CORAL },
  64. { num: cpData.length, label: '对标竞品', color: BRAND.PURPLE },
  65. ])}
  66. <div style="display:grid; grid-template-columns:repeat(2, 1fr); gap:18px; margin-top:20px;">
  67. ${cpData.map(cp => `
  68. <div style="padding:20px; background:var(--bg-1); border:1px solid var(--border); border-top:3px solid ${cp.color}; border-radius:var(--radius-md);">
  69. <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
  70. <div class="text-mono text-xs" style="color:${cp.color}; font-weight:700;">${esc(cp.name)}</div>
  71. <div style="font-size:0.68rem; color:var(--text-3);">${cp.total} 条 VOC · <span style="color:${BRAND.JIANG_GREEN};">${cp.posCount} 正</span> / <span style="color:${BRAND.CORAL};">${cp.negCount} 负</span></div>
  72. </div>
  73. <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
  74. <div>
  75. <div style="color:${BRAND.JIANG_GREEN}; font-size:0.72rem; font-weight:700; margin-bottom:6px;">✅ 用户好评原声</div>
  76. ${cp.posEvidence.length > 0
  77. ? cp.posEvidence.map(ev => {
  78. const raw = (ev.content || '').slice(0, 90);
  79. const trans = isEnglish(raw) ? getTranslation(ev.content) : null;
  80. const display = trans ? trans.slice(0, 90) : raw;
  81. const badge = trans ? ' <span style="color:' + BRAND.WARM_ORANGE + ';font-size:0.56rem;">🔄已翻译</span>' : (isEnglish(raw) ? ' <span style="color:' + BRAND.WARM_ORANGE + ';font-size:0.56rem;">EN</span>' : '');
  82. return `<div style="padding:8px 10px; background:${BRAND.JIANG_GREEN}06; border-left:2px solid ${BRAND.JIANG_GREEN}44; border-radius:4px; margin-bottom:6px; font-size:0.74rem; color:var(--text-1); line-height:1.5;">
  83. "${esc(display)}"${badge}
  84. <div style="color:var(--text-3); font-size:0.64rem; margin-top:3px;">—${esc(ev.nickname || ev.platform)} · ❤️${ev.likes || 0}</div>
  85. </div>`;
  86. }).join('')
  87. : `<div style="color:var(--text-3); font-size:0.74rem; padding:8px; background:var(--bg-2); border-radius:4px;">正面 VOC 较少(${cp.posCount} 条)</div>`
  88. }
  89. </div>
  90. <div>
  91. <div style="color:${BRAND.CORAL}; font-size:0.72rem; font-weight:700; margin-bottom:6px;">❌ 用户差评原声</div>
  92. ${cp.negEvidence.length > 0
  93. ? cp.negEvidence.map(ev => {
  94. const raw = (ev.content || '').slice(0, 90);
  95. const trans = isEnglish(raw) ? getTranslation(ev.content) : null;
  96. const display = trans ? trans.slice(0, 90) : raw;
  97. const badge = trans ? ' <span style="color:' + BRAND.WARM_ORANGE + ';font-size:0.56rem;">🔄已翻译</span>' : (isEnglish(raw) ? ' <span style="color:' + BRAND.WARM_ORANGE + ';font-size:0.56rem;">EN</span>' : '');
  98. return `<div style="padding:8px 10px; background:${BRAND.CORAL}06; border-left:2px solid ${BRAND.CORAL}44; border-radius:4px; margin-bottom:6px; font-size:0.74rem; color:var(--text-1); line-height:1.5;">
  99. "${esc(display)}"${badge}
  100. <div style="color:var(--text-3); font-size:0.64rem; margin-top:3px;">—${esc(ev.nickname || ev.platform)} · ❤️${ev.likes || 0}</div>
  101. </div>`;
  102. }).join('')
  103. : `<div style="color:var(--text-3); font-size:0.74rem; padding:8px; background:var(--bg-2); border-radius:4px;">负面 VOC 较少(${cp.negCount} 条)</div>`
  104. }
  105. </div>
  106. </div>
  107. </div>`).join('')}
  108. </div>
  109. ${renderFindingCard({
  110. icon: '📊',
  111. title: 'VOC 驱动的竞品全景:正反面都来自真实用户声音',
  112. body: `6 大竞品共 <strong>${cpData.reduce((s, cp) => s + cp.total, 0)}</strong> 条 VOC。正面评价集中在"医生推荐/品牌信任/有效降指标",负面集中在"<strong style="color:${BRAND.CORAL};">没效果/副作用/太贵/智商税</strong>"——这正是肝纯片的机会窗口。`,
  113. accent: c.color,
  114. })}
  115. </div>
  116. </section>`;
  117. }
  118. // ============================================================
  119. // 2.2 竞品差评 → 机会窗口
  120. // ============================================================
  121. function renderSub2({ chapter: c, subIdx, sub, slideNum, slideTotal, data, meta }) {
  122. const items = data?.items || [];
  123. const negItems = items.filter(it => it.sentiment === 'negative');
  124. const painToOpp = [
  125. { pain: '吸收差/没效果', re: /没用|没效|没感觉|no effect|didn.?t work|吸收差/i, opp: '藤茶 DMY 总黄酮 98%·真实科学力', score: 95, color: BRAND.TENGCHA_GREEN },
  126. { pain: '智商税/广告过度', re: /智商税|骗人|广告|忽悠|scam/i, opp: '千年食饮史·不做硬广做口碑', score: 90, color: BRAND.JIANG_GREEN },
  127. { pain: '副作用/胃不舒服', re: /副作用|胃不舒服|拉肚子|过敏|side effect/i, opp: '藤茶 = 食饮千年·天然温和', score: 88, color: BRAND.SAGE },
  128. { pain: '价格贵/不值', re: /贵|不值|expensive|价格/i, opp: 'vs Swisse 便宜 15-30%·OTC 定价', score: 80, color: BRAND.WARM_ORANGE },
  129. { pain: '包装药盒感/无礼品感', re: /药盒|包装|不好看|礼|面子/i, opp: '新中式礼盒·东方养生美学', score: 85, color: BRAND.GOLD },
  130. { pain: '处方依赖/不能自购', re: /处方|医嘱|不能自己|医生才能/i, opp: 'OTC 非处方·药店自选', score: 82, color: BRAND.TECH_BLUE },
  131. ];
  132. const painStats = painToOpp.map(p => ({
  133. ...p, count: negItems.filter(it => p.re.test(it.content || '')).length,
  134. })).sort((a, b) => b.count - a.count);
  135. const negEvidence = LA.getEvidence(negItems, { top: 4, seed: 61, minChars: 15, contentMatch: LIVER_CONTEXT_RE });
  136. return `<section class="report-section" id="${c.key}-${subIdx}" data-tone="${c.tone}" data-slide-num="${slideNum}/${slideTotal}">
  137. <div class="section-inner">
  138. ${renderSectionHead({
  139. eyebrow: `${c.num}.${subIdx} · PAIN → OPPORTUNITY`,
  140. title: sub,
  141. subtitle: '竞品的每个缺点都是我们的机会窗口。',
  142. color: c.color,
  143. })}
  144. ${renderCompareTable({
  145. title: '竞品差评痛点 × 肝纯片应答',
  146. headers: ['竞品痛点', 'VOC 数', '肝纯片应答', '匹配度'],
  147. rows: painStats.map(p => [
  148. { html: `<strong>${esc(p.pain)}</strong>`, bold: true },
  149. `${p.count} 条`,
  150. { html: `<span style="color:${p.color};">${esc(p.opp)}</span>`, color: p.color },
  151. { html: `<strong style="color:${p.score >= 85 ? BRAND.JIANG_GREEN : BRAND.WARM_ORANGE};">${p.score}%</strong>` },
  152. ]),
  153. accent: c.color,
  154. })}
  155. ${negEvidence.length > 0 ? `
  156. <div style="margin-top:24px;">
  157. <div class="text-mono text-xs" style="color:${BRAND.CORAL}; letter-spacing:0.15em; margin-bottom:14px; font-weight:700;">竞品差评原声 · 机会证据</div>
  158. ${renderVocGrid(negEvidence, { columns: 2, accent: BRAND.CORAL, compact: true })}
  159. </div>` : ''}
  160. ${renderFindingCard({
  161. icon: '🎯',
  162. title: '机会窗口结论:藤茶 DMY 可以同时回应 6 个竞品痛点',
  163. body: `"<strong style="color:${BRAND.TENGCHA_GREEN};">吸收差</strong>"→ DMY 98% 真科学力 · "<strong style="color:${BRAND.JIANG_GREEN};">智商税</strong>"→ 千年食饮不做硬广 · "<strong style="color:${BRAND.SAGE};">副作用</strong>"→ 天然温和 · "<strong style="color:${BRAND.WARM_ORANGE};">价格贵</strong>"→ OTC 定价合理 · "<strong style="color:${BRAND.GOLD};">无礼品感</strong>"→ 新中式礼盒 · "<strong style="color:${BRAND.TECH_BLUE};">处方依赖</strong>"→ OTC 自选。`,
  164. accent: c.color,
  165. })}
  166. </div>
  167. </section>`;
  168. }
  169. // ============================================================
  170. // 2.3 三轴定位图谱
  171. // ============================================================
  172. function renderSub3({ chapter: c, subIdx, sub, slideNum, slideTotal, data, meta }) {
  173. return `<section class="report-section" id="${c.key}-${subIdx}" data-tone="${c.tone}" data-slide-num="${slideNum}/${slideTotal}">
  174. <div class="section-inner">
  175. ${renderSectionHead({
  176. eyebrow: `${c.num}.${subIdx} · THREE-AXIS MAP`,
  177. title: sub,
  178. subtitle: '保健品 ↔ OTC 药 ↔ 食品三轴定位图谱 · 肝纯片的无人地带坐标',
  179. color: c.color,
  180. })}
  181. ${renderMatrix2x2({
  182. xLabel: '产品形态',
  183. yLabel: '信任层级',
  184. xLow: '←药品形态',
  185. xHigh: '食品形态→',
  186. yLow: '低信任(食品级)',
  187. yHigh: '高信任(医疗级)',
  188. quadrants: {
  189. q1: {
  190. title: '高信任·食品形态',
  191. color: BRAND.JIANG_GREEN,
  192. items: [
  193. '<strong>★ 肝纯片目标位</strong>',
  194. '保健品信任 + 食品便携',
  195. '藤茶千年食饮史·天然',
  196. '新中式礼盒·送得出手',
  197. ],
  198. note: '→ 无人地带·品类创新',
  199. },
  200. q2: {
  201. title: '高信任·药品形态',
  202. color: BRAND.TECH_BLUE,
  203. items: [
  204. '易善复(处方药)',
  205. '葵花护肝片(OTC)',
  206. '水飞蓟片(OTC)',
  207. ],
  208. note: '信任高但体验差',
  209. },
  210. q3: {
  211. title: '低信任·药品形态',
  212. color: '#888',
  213. items: [
  214. '不知名护肝片',
  215. '微商保肝产品',
  216. ],
  217. note: '→ 避免区',
  218. },
  219. q4: {
  220. title: '低信任·食品形态',
  221. color: BRAND.WARM_ORANGE,
  222. items: [
  223. 'Swisse 护肝片(保健品)',
  224. '海王金樽(食品饮料)',
  225. '各类解酒糖果',
  226. ],
  227. note: '"智商税"争议最多',
  228. },
  229. },
  230. accent: c.color,
  231. })}
  232. ${renderInsightHero({
  233. eyebrow: 'POSITIONING GAP',
  234. title: '肝纯片的无人地带 = "高信任 + 食品体验"象限',
  235. subtitle: `
  236. 左上(易善复/葵花):<strong>高信任但体验差</strong>(药盒感/副作用/处方依赖)<br/>
  237. 右下(Swisse/海王金樽):<strong>好体验但低信任</strong>(智商税/没效果)<br/>
  238. 右上(肝纯片目标):<strong style="color:${BRAND.JIANG_GREEN};">高信任 + 好体验</strong> = 藤茶千年食饮安全 × OTC 品质背书 × 新中式包装美学 → <strong>品类空白</strong>
  239. `,
  240. color: c.color,
  241. })}
  242. </div>
  243. </section>`;
  244. }
  245. module.exports = { renderSub1, renderSub2, renderSub3 };