| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- // ==============================================================================
- // 肝纯片 v2 · Ch2 竞品正反面 · 全面分析(会议 req C)
- // ==============================================================================
- // 2.1 6 大竞品正反面全景(优点+缺点)
- // 2.2 竞品差评痛点 → 肝纯片机会窗口
- // 2.3 三轴定位图谱 · 肝纯片无人地带
- // ==============================================================================
- const LA = require('./liver-analyze.js');
- const C = require('./liver-components.js');
- const { BRAND, esc, renderSectionHead, renderVocCard, renderVocGrid,
- renderStatStrip, renderMatrix2x2, renderCompareTable, renderInsightHero,
- renderFindingCard, renderDecisionList, isEnglish, getTranslation, LIVER_CONTEXT_RE } = C;
- // ============================================================
- // 2.1 6 大竞品正反面(从 VOC 提取真实好评/差评)
- // ============================================================
- function renderSub1({ chapter: c, subIdx, sub, slideNum, slideTotal, data, meta }) {
- const items = data?.items || [];
- // 正面/负面内容匹配模式(因 sentiment 推断偏弱,用内容 regex 补充)
- // negRe 增加中文否定句式:不怎么管用/不太好/不太行
- const negRe = /没有?用(?!过)|没效|没感觉|no effect|didn.?t work|副作用|拉肚子|胃不舒服|难吃|太贵|智商税|骗|坑|差评|上当|头晕|不降反升|不推荐|难受|不怎么管用|不太好用|不太行|不管用|不好用|无效|过敏|side.?effect|stomach|伤肝|别吃/i;
- const posRe = /有效|管用|好用|推荐|不错|下来了|降了|好转|有用的|真有用|确实有用|效果好|缓解|改善|靠谱|信赖|放心|好评|好转|值得/i;
- const competitors = [
- { name: 'Swisse 护肝片', product: 'Swisse 护肝片', color: '#003087', seed: 201,
- alsoMatch: /swisse|澳洲护肝|澳佳宝/i },
- { name: '易善复', product: '易善复', color: '#1A5276', seed: 202,
- alsoMatch: /易善复|多烯磷脂酰胆碱|磷脂/i },
- { name: '葵花护肝片', product: '葵花护肝片', color: '#F39C12', seed: 203,
- alsoMatch: /葵花|护肝片.*葵花/i },
- { name: '片仔癀', product: '片仔癀护肝', color: '#C0392B', seed: 204,
- alsoMatch: /片仔癀/i },
- { name: '海王金樽', product: '海王金樽', color: '#2E86C1', seed: 205,
- alsoMatch: /海王|金樽|解酒液/i },
- { name: '水飞蓟/奶蓟草', product: '水飞蓟成分', color: '#6C3483', seed: 206,
- alsoMatch: /水飞蓟|奶蓟|silymarin|milk thistle/i },
- ];
- // 为每个竞品从 VOC 中提取正面和负面原声
- const cpData = competitors.map(cp => {
- // 匹配该竞品的所有 VOC(内容必须同时提到护肝相关语境,排除片仔癀牙痛/皮肤等无关帖)
- const cpItems = items.filter(it => {
- const matched = (it.product || '') === cp.product || cp.alsoMatch.test(`${it.content || ''} ${it.keyword || ''}`);
- if (!matched) return false;
- return LIVER_CONTEXT_RE.test(it.content || '');
- });
- // 按内容 regex 分正负面
- const posItems = cpItems.filter(it => posRe.test(it.content || '') && !negRe.test(it.content || ''));
- const negItems = cpItems.filter(it => negRe.test(it.content || ''));
- // 取代表性原声(cpItems 已经通过 cp.alsoMatch + LIVER_CONTEXT_RE 双重过滤,不再重复 contentMatch)
- const posEvidence = LA.getEvidence(posItems, { top: 2, seed: cp.seed, minChars: 15 });
- const negEvidence = LA.getEvidence(negItems, { top: 2, seed: cp.seed + 50, minChars: 15 });
- return { ...cp, total: cpItems.length, posCount: posItems.length, negCount: negItems.length, posEvidence, negEvidence };
- });
- return `<section class="report-section" id="${c.key}-${subIdx}" data-tone="${c.tone}" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- ${renderSectionHead({
- eyebrow: `${c.num}.${subIdx} · COMPETITOR PROS & CONS`,
- title: sub,
- subtitle: '江旭要求:"各个竞品,除了缺点还有哪些优点,全面来分析一下。"——以下优缺点均从真实 VOC 提取。',
- color: c.color,
- })}
- ${renderStatStrip([
- { num: cpData.reduce((s, cp) => s + cp.total, 0), label: '竞品相关 VOC 总量', color: c.color },
- { num: cpData.reduce((s, cp) => s + cp.posCount, 0), label: '正面评价', color: BRAND.JIANG_GREEN },
- { num: cpData.reduce((s, cp) => s + cp.negCount, 0), label: '负面评价', color: BRAND.CORAL },
- { num: cpData.length, label: '对标竞品', color: BRAND.PURPLE },
- ])}
- <div style="display:grid; grid-template-columns:repeat(2, 1fr); gap:18px; margin-top:20px;">
- ${cpData.map(cp => `
- <div style="padding:20px; background:var(--bg-1); border:1px solid var(--border); border-top:3px solid ${cp.color}; border-radius:var(--radius-md);">
- <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
- <div class="text-mono text-xs" style="color:${cp.color}; font-weight:700;">${esc(cp.name)}</div>
- <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>
- </div>
- <div style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
- <div>
- <div style="color:${BRAND.JIANG_GREEN}; font-size:0.72rem; font-weight:700; margin-bottom:6px;">✅ 用户好评原声</div>
- ${cp.posEvidence.length > 0
- ? cp.posEvidence.map(ev => {
- const raw = (ev.content || '').slice(0, 90);
- const trans = isEnglish(raw) ? getTranslation(ev.content) : null;
- const display = trans ? trans.slice(0, 90) : raw;
- 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>' : '');
- 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;">
- "${esc(display)}"${badge}
- <div style="color:var(--text-3); font-size:0.64rem; margin-top:3px;">—${esc(ev.nickname || ev.platform)} · ❤️${ev.likes || 0}</div>
- </div>`;
- }).join('')
- : `<div style="color:var(--text-3); font-size:0.74rem; padding:8px; background:var(--bg-2); border-radius:4px;">正面 VOC 较少(${cp.posCount} 条)</div>`
- }
- </div>
- <div>
- <div style="color:${BRAND.CORAL}; font-size:0.72rem; font-weight:700; margin-bottom:6px;">❌ 用户差评原声</div>
- ${cp.negEvidence.length > 0
- ? cp.negEvidence.map(ev => {
- const raw = (ev.content || '').slice(0, 90);
- const trans = isEnglish(raw) ? getTranslation(ev.content) : null;
- const display = trans ? trans.slice(0, 90) : raw;
- 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>' : '');
- 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;">
- "${esc(display)}"${badge}
- <div style="color:var(--text-3); font-size:0.64rem; margin-top:3px;">—${esc(ev.nickname || ev.platform)} · ❤️${ev.likes || 0}</div>
- </div>`;
- }).join('')
- : `<div style="color:var(--text-3); font-size:0.74rem; padding:8px; background:var(--bg-2); border-radius:4px;">负面 VOC 较少(${cp.negCount} 条)</div>`
- }
- </div>
- </div>
- </div>`).join('')}
- </div>
- ${renderFindingCard({
- icon: '📊',
- title: 'VOC 驱动的竞品全景:正反面都来自真实用户声音',
- body: `6 大竞品共 <strong>${cpData.reduce((s, cp) => s + cp.total, 0)}</strong> 条 VOC。正面评价集中在"医生推荐/品牌信任/有效降指标",负面集中在"<strong style="color:${BRAND.CORAL};">没效果/副作用/太贵/智商税</strong>"——这正是肝纯片的机会窗口。`,
- accent: c.color,
- })}
- </div>
- </section>`;
- }
- // ============================================================
- // 2.2 竞品差评 → 机会窗口
- // ============================================================
- function renderSub2({ chapter: c, subIdx, sub, slideNum, slideTotal, data, meta }) {
- const items = data?.items || [];
- const negItems = items.filter(it => it.sentiment === 'negative');
- const painToOpp = [
- { pain: '吸收差/没效果', re: /没用|没效|没感觉|no effect|didn.?t work|吸收差/i, opp: '藤茶 DMY 总黄酮 98%·真实科学力', score: 95, color: BRAND.TENGCHA_GREEN },
- { pain: '智商税/广告过度', re: /智商税|骗人|广告|忽悠|scam/i, opp: '千年食饮史·不做硬广做口碑', score: 90, color: BRAND.JIANG_GREEN },
- { pain: '副作用/胃不舒服', re: /副作用|胃不舒服|拉肚子|过敏|side effect/i, opp: '藤茶 = 食饮千年·天然温和', score: 88, color: BRAND.SAGE },
- { pain: '价格贵/不值', re: /贵|不值|expensive|价格/i, opp: 'vs Swisse 便宜 15-30%·OTC 定价', score: 80, color: BRAND.WARM_ORANGE },
- { pain: '包装药盒感/无礼品感', re: /药盒|包装|不好看|礼|面子/i, opp: '新中式礼盒·东方养生美学', score: 85, color: BRAND.GOLD },
- { pain: '处方依赖/不能自购', re: /处方|医嘱|不能自己|医生才能/i, opp: 'OTC 非处方·药店自选', score: 82, color: BRAND.TECH_BLUE },
- ];
- const painStats = painToOpp.map(p => ({
- ...p, count: negItems.filter(it => p.re.test(it.content || '')).length,
- })).sort((a, b) => b.count - a.count);
- const negEvidence = LA.getEvidence(negItems, { top: 4, seed: 61, minChars: 15, contentMatch: LIVER_CONTEXT_RE });
- return `<section class="report-section" id="${c.key}-${subIdx}" data-tone="${c.tone}" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- ${renderSectionHead({
- eyebrow: `${c.num}.${subIdx} · PAIN → OPPORTUNITY`,
- title: sub,
- subtitle: '竞品的每个缺点都是我们的机会窗口。',
- color: c.color,
- })}
- ${renderCompareTable({
- title: '竞品差评痛点 × 肝纯片应答',
- headers: ['竞品痛点', 'VOC 数', '肝纯片应答', '匹配度'],
- rows: painStats.map(p => [
- { html: `<strong>${esc(p.pain)}</strong>`, bold: true },
- `${p.count} 条`,
- { html: `<span style="color:${p.color};">${esc(p.opp)}</span>`, color: p.color },
- { html: `<strong style="color:${p.score >= 85 ? BRAND.JIANG_GREEN : BRAND.WARM_ORANGE};">${p.score}%</strong>` },
- ]),
- accent: c.color,
- })}
- ${negEvidence.length > 0 ? `
- <div style="margin-top:24px;">
- <div class="text-mono text-xs" style="color:${BRAND.CORAL}; letter-spacing:0.15em; margin-bottom:14px; font-weight:700;">竞品差评原声 · 机会证据</div>
- ${renderVocGrid(negEvidence, { columns: 2, accent: BRAND.CORAL, compact: true })}
- </div>` : ''}
- ${renderFindingCard({
- icon: '🎯',
- title: '机会窗口结论:藤茶 DMY 可以同时回应 6 个竞品痛点',
- 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 自选。`,
- accent: c.color,
- })}
- </div>
- </section>`;
- }
- // ============================================================
- // 2.3 三轴定位图谱
- // ============================================================
- function renderSub3({ chapter: c, subIdx, sub, slideNum, slideTotal, data, meta }) {
- return `<section class="report-section" id="${c.key}-${subIdx}" data-tone="${c.tone}" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- ${renderSectionHead({
- eyebrow: `${c.num}.${subIdx} · THREE-AXIS MAP`,
- title: sub,
- subtitle: '保健品 ↔ OTC 药 ↔ 食品三轴定位图谱 · 肝纯片的无人地带坐标',
- color: c.color,
- })}
- ${renderMatrix2x2({
- xLabel: '产品形态',
- yLabel: '信任层级',
- xLow: '←药品形态',
- xHigh: '食品形态→',
- yLow: '低信任(食品级)',
- yHigh: '高信任(医疗级)',
- quadrants: {
- q1: {
- title: '高信任·食品形态',
- color: BRAND.JIANG_GREEN,
- items: [
- '<strong>★ 肝纯片目标位</strong>',
- '保健品信任 + 食品便携',
- '藤茶千年食饮史·天然',
- '新中式礼盒·送得出手',
- ],
- note: '→ 无人地带·品类创新',
- },
- q2: {
- title: '高信任·药品形态',
- color: BRAND.TECH_BLUE,
- items: [
- '易善复(处方药)',
- '葵花护肝片(OTC)',
- '水飞蓟片(OTC)',
- ],
- note: '信任高但体验差',
- },
- q3: {
- title: '低信任·药品形态',
- color: '#888',
- items: [
- '不知名护肝片',
- '微商保肝产品',
- ],
- note: '→ 避免区',
- },
- q4: {
- title: '低信任·食品形态',
- color: BRAND.WARM_ORANGE,
- items: [
- 'Swisse 护肝片(保健品)',
- '海王金樽(食品饮料)',
- '各类解酒糖果',
- ],
- note: '"智商税"争议最多',
- },
- },
- accent: c.color,
- })}
- ${renderInsightHero({
- eyebrow: 'POSITIONING GAP',
- title: '肝纯片的无人地带 = "高信任 + 食品体验"象限',
- subtitle: `
- 左上(易善复/葵花):<strong>高信任但体验差</strong>(药盒感/副作用/处方依赖)<br/>
- 右下(Swisse/海王金樽):<strong>好体验但低信任</strong>(智商税/没效果)<br/>
- 右上(肝纯片目标):<strong style="color:${BRAND.JIANG_GREEN};">高信任 + 好体验</strong> = 藤茶千年食饮安全 × OTC 品质背书 × 新中式包装美学 → <strong>品类空白</strong>
- `,
- color: c.color,
- })}
- </div>
- </section>`;
- }
- module.exports = { renderSub1, renderSub2, renderSub3 };
|