| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566 |
- // VOC 报告 HTML 渲染器 · v2
- const { esc, fmtNum, fmtMoney, cleanText } = require('./voc-analyze');
- const STYLES = require('./voc-styles');
- const SCRIPTS = require('./voc-scripts');
- function renderNav(product, hasDouyin, hasTikTok) {
- const sections = [
- { id: 's0', tip: '封面' }, { id: 's1', tip: '执行摘要' },
- { id: 's2', tip: '核心洞察' }, { id: 's3', tip: '爆款笔记' },
- { id: 's4', tip: '用户原声' }, { id: 's5', tip: 'KOL 分析' },
- { id: 's6', tip: 'Amazon 对标' },
- ...(hasTikTok ? [{ id: 'sttk', tip: 'TikTok 海外' }] : []),
- ...(hasDouyin ? [{ id: 'sdy', tip: '抖音阵地' }] : []),
- { id: 's7', tip: '痛点亮点' },
- { id: 's8', tip: '机会矩阵' }, { id: 's9', tip: '行动建议' },
- ];
- return `<div class="progress-line" id="progress-line"></div>
- <div class="top-bar">
- <div class="brand-logo">BRAIN<span class="dot">·</span>HACK / VOC</div>
- <div class="text-mono text-xs">${esc(product)} — 洞察报告 v2.0</div>
- </div>
- <nav class="nav-rail" id="navRail">
- ${sections.map((s, i) => `<a class="nav-dot${i === 0 ? ' active' : ''}" data-tip="${s.tip}" data-target="${s.id}" href="#${s.id}"></a>`).join('\n ')}
- </nav>`;
- }
- function renderS0Cover({ product, category, collectedAt, A, slideNum, slideTotal }) {
- return `<section class="report-section" id="s0" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}" style="min-height:100vh;padding:0;">
- <div class="cover-hero">
- <div class="aurora" aria-hidden="true"><span></span><span></span><span></span></div>
- <div class="cover-grid-bg"></div>
- <div class="cover-content">
- <div class="cover-meta rise-in"><span>VOC / 2026 / 中国市场 · Amazon 对标</span></div>
- <div class="cover-title-stack">
- <p class="text-mono text-amber rise-in" style="font-size:0.9rem;letter-spacing:0.2em;margin-bottom:18px;">${esc(category)}</p>
- <h1 class="cover-title rise-in">${esc(product)}</h1>
- <p class="text-poster rise-in" style="margin-top:20px;color:var(--text-2);">用户洞察 · VOC 报告</p>
- </div>
- <p class="cover-sub fade-up">
- 基于小红书 <strong class="text-amber">${A.metrics.notes}</strong> 篇相关笔记 ·
- <strong class="text-amber">${A.metrics.sampleComments}</strong> 条真实评论 ·
- <strong class="text-amber">${A.kols.length}</strong> 位作者画像 ·
- ${A.amazonProducts.length > 0 ? `叠加 Amazon <strong class="text-amber">${A.amazonProducts.length}</strong> 款国际对标产品` : '国际品类对标中'},
- 解构用户真实需求、场景与未被满足的机会。
- </p>
- <div class="cover-stats fade-up">
- <div class="cover-stat"><div class="num">${fmtNum(A.metrics.totalLikes)}</div><div class="lbl">累计点赞</div></div>
- <div class="cover-stat"><div class="num">${fmtNum(A.metrics.totalComments)}</div><div class="lbl">累计评论</div></div>
- <div class="cover-stat"><div class="num">${fmtNum(A.metrics.totalCollected)}</div><div class="lbl">累计收藏</div></div>
- <div class="cover-stat"><div class="num">${fmtNum(A.metrics.totalShared)}</div><div class="lbl">累计分享</div></div>
- </div>
- <p class="text-mono text-xs fade-up" style="margin-top:64px;">SCROLL DOWN · 采集时间 ${collectedAt}</p>
- </div>
- </div>
- </section>`;
- }
- function renderS1Summary({ A, slideNum, slideTotal }) {
- const engRate = A.metrics.totalLikes > 0 ? ((A.metrics.totalComments / A.metrics.totalLikes) * 100).toFixed(1) : '—';
- return `<section class="report-section" id="s1" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">01 · Executive Summary</div>
- <h2 class="text-h1"><span class="headline-anim">执行摘要</span></h2>
- <p class="section-sub">核心数据一览,洞察始于规模。</p>
- </div>
- <div class="grid grid-4">
- <div class="metric fade-up">
- <div class="metric-label">笔记样本</div>
- <div class="metric-value" data-count="${A.metrics.notes}">${A.metrics.notes}</div>
- <div class="metric-sub">关键词覆盖 ${Object.keys(A.meta.xiaohongshu?.by_keyword || {}).length} 个维度</div>
- </div>
- <div class="metric fade-up">
- <div class="metric-label">评论原声</div>
- <div class="metric-value" data-count="${A.metrics.sampleComments}">${A.metrics.sampleComments}</div>
- <div class="metric-sub">来自 Top 笔记深度抓取</div>
- </div>
- <div class="metric fade-up">
- <div class="metric-label">作者画像</div>
- <div class="metric-value" data-count="${A.kols.length}">${A.kols.length}</div>
- <div class="metric-sub">头部 ${A.kolTier.head.length} · 腰部 ${A.kolTier.mid.length} · KOC ${A.kolTier.tail.length}</div>
- </div>
- <div class="metric fade-up">
- <div class="metric-label">评论/点赞比</div>
- <div class="metric-value">${engRate}<span class="metric-unit"> %</span></div>
- <div class="metric-sub">用户参与深度指标</div>
- </div>
- </div>
- <div class="grid grid-2" style="margin-top:40px;">
- <div class="card fade-up">
- <div class="text-mono text-xs" style="color:var(--amber);">SAMPLE BREAKDOWN</div>
- <h3 class="text-h2" style="margin-top:10px;">样本规模分布</h3>
- <div id="chartSample" class="chart-container" style="margin-top:20px;"></div>
- </div>
- <div class="card fade-up">
- <div class="text-mono text-xs" style="color:var(--amber);">GEO DISTRIBUTION</div>
- <h3 class="text-h2" style="margin-top:10px;">用户地域 TOP 10</h3>
- <div id="chartGeo" class="chart-container" style="margin-top:20px;"></div>
- </div>
- </div>
- </div>
- </section>`;
- }
- function renderS2Insights({ A, themes, slideNum, slideTotal }) {
- const topPain = themes.pain[0] || { tag: '—', count: 0 };
- const topBright = themes.bright[0] || { tag: '—', count: 0 };
- const topScenario = themes.scenarios[0] || { tag: '—', count: 0 };
- const hotNote = A.topNotes[0] || {};
- const topComment = A.topComments[0] || {};
- return `<section class="report-section" id="s2" data-tone="rose" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">02 · Core Insights</div>
- <h2 class="text-h1"><span class="headline-anim">三条核心洞察</span></h2>
- <p class="section-sub">从数据里提炼出的不言而喻的真相。</p>
- </div>
- <div class="grid" style="grid-template-columns:1fr;gap:18px;">
- <div class="insight-banner fade-up" data-num="I">
- <h3>用户对此品类的第一反应是 <strong>${esc(topPain.tag)}</strong><br>而非功效或品牌。</h3>
- <p class="insight-body">在 ${A.metrics.sampleComments} 条评论中,"${esc(topPain.tag)}"被反复提及 <strong class="text-amber">${topPain.count}</strong> 次,超过任何品牌词。这是品类级的心智障碍,比产品本身更值得投资解决。</p>
- <div class="insight-data">
- <div class="dp"><span class="num">${topPain.count}</span><span class="lbl">提及次数</span></div>
- <div class="dp"><span class="num">${topBright.count}</span><span class="lbl">正向对照</span></div>
- <div class="dp"><span class="num text-rose">${((topPain.count / Math.max(1, A.metrics.sampleComments)) * 100).toFixed(1)}%</span><span class="lbl">覆盖率</span></div>
- </div>
- </div>
- <div class="insight-banner fade-up" data-num="II">
- <h3>最高赞笔记 <strong>${fmtNum(hotNote.liked_count || 0)}</strong> 赞,<br>主题围绕 <strong>${esc(topScenario.tag)}</strong>。</h3>
- <p class="insight-body">"<em>${esc(cleanText(hotNote.title || '').slice(0, 40))}</em>" — 证明 <strong class="text-amber">${esc(topScenario.tag)}</strong> 是此品类最强的种草切入点。内容方向 / 产品卖点 / 投流素材都应围绕这一场景聚焦。</p>
- <div class="insight-data">
- <div class="dp"><span class="num">${fmtNum(hotNote.liked_count || 0)}</span><span class="lbl">最高赞</span></div>
- <div class="dp"><span class="num">${fmtNum(hotNote.comments_count || 0)}</span><span class="lbl">评论</span></div>
- <div class="dp"><span class="num">${fmtNum(hotNote.collected_count || 0)}</span><span class="lbl">收藏</span></div>
- </div>
- </div>
- <div class="insight-banner fade-up" data-num="III">
- <h3>一条 <strong>${fmtNum(topComment.like_count || 0)}</strong> 赞的评论,<br>说明 <strong>${esc(topBright.tag)}</strong> 是消费者自发认同的卖点。</h3>
- <p class="insight-body">"<em>${esc(cleanText(topComment.content || '').slice(0, 70))}</em>" — 这条评论的点赞数体现消费者的<strong class="text-amber">共识形成</strong>。这个表达本身就是可直接挪用的最佳营销文案。</p>
- <div class="insight-data">
- <div class="dp"><span class="num">${fmtNum(topComment.like_count || 0)}</span><span class="lbl">评论获赞</span></div>
- <div class="dp"><span class="num">${topBright.count}</span><span class="lbl">亮点提及</span></div>
- <div class="dp"><span class="num">${topScenario.count}</span><span class="lbl">场景覆盖</span></div>
- </div>
- </div>
- </div>
- </div>
- </section>`;
- }
- function renderS3Notes({ A, slideNum, slideTotal }) {
- const top12 = A.topNotes.slice(0, 12);
- return `<section class="report-section" id="s3" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">03 · Top Notes</div>
- <h2 class="text-h1"><span class="headline-anim">爆款笔记 · 种草全景</span></h2>
- <p class="section-sub">按点赞数排序的 Top 12 笔记 · 标题 / 互动结构 / 发布人画像</p>
- </div>
- <div class="grid grid-3">
- ${top12.map((n, i) => `<div class="note-card fade-up">
- <div class="note-rank">#${String(i + 1).padStart(2, '0')}</div>
- <div class="note-title">${esc(cleanText(n.title || ''))}</div>
- <div style="display:flex;gap:8px;flex-wrap:wrap;">
- <span class="tag gray">${n.type === 'video' ? '视频' : '图文'}</span>
- ${n.user?.red_official_verified ? '<span class="tag">官方认证</span>' : ''}
- </div>
- <div class="note-stats">
- <div class="st"><span class="n">${fmtNum(n.liked_count)}</span><span class="l">LIKES</span></div>
- <div class="st"><span class="n">${fmtNum(n.comments_count)}</span><span class="l">COMMENTS</span></div>
- <div class="st"><span class="n">${fmtNum(n.collected_count)}</span><span class="l">SAVES</span></div>
- </div>
- <div class="text-xs" style="color:var(--text-3);">@${esc(n.user?.nickname || '未知作者')}</div>
- </div>`).join('\n ')}
- </div>
- <div class="card fade-up" style="margin-top:32px;">
- <div class="text-mono text-xs text-amber">NOTE HEATMAP</div>
- <h3 class="text-h2" style="margin-top:10px;">笔记互动热力图</h3>
- <p class="text-sm" style="margin:8px 0 20px;">X 轴=点赞 · Y 轴=评论 · 气泡大小=收藏 · 找出低评论高收藏的"干货潜力"笔记</p>
- <div id="chartNoteHeatmap" class="chart-container" style="min-height:440px;"></div>
- </div>
- </div>
- </section>`;
- }
- function renderS4Voices({ A, slideNum, slideTotal }) {
- const voices = A.topComments.slice(0, 15);
- return `<section class="report-section" id="s4" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">04 · Voice Wall</div>
- <h2 class="text-h1"><span class="headline-anim">用户真实原声</span></h2>
- <p class="section-sub">高赞评论 Top 15 · 未经任何编辑的 VOC 原始语料</p>
- </div>
- <div class="voice-wall">
- ${voices.map((c, i) => `<div class="voice-card ${i < 3 ? 'hot' : ''} fade-up">
- <p class="voice-content">${esc(cleanText(c.content))}</p>
- <div class="voice-meta">
- <span>${esc((c.user_info?.nickname || '匿名').slice(0, 14))}${c.ip_location ? ' · ' + esc(c.ip_location) : ''}</span>
- <span class="voice-likes">♥ ${fmtNum(c.like_count || 0)}</span>
- </div>
- </div>`).join('\n ')}
- </div>
- </div>
- </section>`;
- }
- function renderS5KOL({ A, slideNum, slideTotal }) {
- const T = A.kolTier;
- const totalQualified = A.kols.length;
- const filtered = A.metrics.kolsFiltered || 0;
- const hasAnyKol = totalQualified > 0;
- const hasTopTier = T.head.length + T.mid.length > 0;
- return `<section class="report-section" id="s5" data-tone="purple" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">05 · KOL Landscape</div>
- <h2 class="text-h1"><span class="headline-anim">作者生态分层</span></h2>
- <p class="section-sub">仅展示 ≥ 1000 粉且真实活跃的作者 · 已过滤 ${filtered} 位低粉低互动账号</p>
- </div>
- <div class="grid grid-4" style="margin-bottom:32px;">
- <div class="metric fade-up">
- <div class="metric-label">头部 KOL (50w+)</div>
- <div class="metric-value" style="color:var(--amber);">${T.head.length}</div>
- <div class="metric-sub">品牌背书 · 单篇合作单价高</div>
- </div>
- <div class="metric fade-up">
- <div class="metric-label">腰部达人 (5w-50w)</div>
- <div class="metric-value" style="color:var(--green);">${T.mid.length}</div>
- <div class="metric-sub">性价比最高 · 批量合作</div>
- </div>
- <div class="metric fade-up">
- <div class="metric-label">尾部 (5k-5w)</div>
- <div class="metric-value" style="color:var(--blue);">${T.tail.length}</div>
- <div class="metric-sub">垂类精准 · 素材铺量</div>
- </div>
- <div class="metric fade-up">
- <div class="metric-label">KOC (1k-5k)</div>
- <div class="metric-value" style="color:var(--purple);">${T.koc.length}</div>
- <div class="metric-sub">真实口碑 · 低成本试水</div>
- </div>
- </div>
- ${!hasAnyKol ? `
- <div class="insight-banner fade-up" data-num="!" style="border-left-color:var(--rose);">
- <h3>本次采样<strong class="text-rose">未发现 ≥1000 粉合格作者</strong>,<br>说明此品类在小红书尚未形成规模化 KOL 生态。</h3>
- <p class="insight-body">有效样本仅 ${filtered} 个微型账号(粉丝多在百位数),无稳定流量池。这是<strong class="text-amber">品类空白信号</strong>:
- <br>· 策略上 — 用 <strong>品牌自营矩阵</strong> 抢占心智(而非依赖达人合作)<br>· 执行上 — 做 <strong>大号跨品类合作</strong>(健康/养生/母婴领域头部)切入垂类
- <br>· 测试上 — <strong>站外抖音 + 微信私域</strong> 成本可能更低效果更高</p>
- </div>` : `
- <h3 class="text-h2 fade-up" style="margin-top:24px;margin-bottom:20px;">合格作者 Top ${Math.min(totalQualified, 9)} · 按粉丝排序</h3>
- <div class="grid grid-3">
- ${A.kols.slice(0, 9).map((k) => `<div class="kol-card fade-up">
- ${k.images ? `<img class="kol-avatar" src="${esc(k.images)}" alt="" loading="lazy" onerror="this.style.display='none'">` : '<div class="kol-avatar"></div>'}
- <div class="kol-info">
- <div class="kol-name">${esc(k.nickname || '')}${k.red_official_verified ? ' <span class="tag" style="font-size:0.62rem;padding:2px 6px;">官方</span>' : ''}</div>
- <div class="kol-desc">${esc(cleanText(k.desc || '暂无简介').slice(0, 70))}</div>
- <div class="kol-stats">
- <span><span class="amber">${fmtNum(k.fans)}</span> 粉丝</span>
- <span>${fmtNum(k.interaction || k.likes)} 赞藏</span>
- ${k.ip_location ? `<span>${esc(k.ip_location)}</span>` : ''}
- </div>
- </div>
- </div>`).join('\n ')}
- </div>
- ${!hasTopTier ? `
- <div class="card fade-up" style="margin-top:28px;border-left:3px solid var(--amber);">
- <div class="text-mono text-xs text-amber">STRATEGIC NOTE</div>
- <h3 class="text-h2" style="margin-top:10px;">头部/腰部缺失的含义</h3>
- <p class="text-sm" style="margin-top:8px;color:var(--text-2);line-height:1.7;">
- 目前合格样本全部集中在「尾部 + KOC」区间,这说明:<br>
- 1. 此品类属于 <strong class="text-amber">真实口碑驱动型</strong>,靠数十位 KOC 铺量比签一位头部更有效;<br>
- 2. 建议 <strong>优先批量投放 ${T.tail.length + T.koc.length} 位尾部/KOC 作者</strong>,以真实体验 × 素材密度驱动种草;<br>
- 3. 未来如需引爆,再考虑跨品类头部合作(如 母婴 / 健康科普 / 生活方式 赛道的 5w+ 达人)。
- </p>
- </div>` : ''}
- `}
- </div>
- </section>`;
- }
- function renderS6Amazon({ A, slideNum, slideTotal }) {
- const amz = A.amazon;
- if (!amz || amz.empty) {
- return `<section class="report-section" id="s6" data-tone="green" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">06 · Amazon Benchmark</div>
- <h2 class="text-h1"><span class="headline-anim">Amazon 国际对标</span></h2>
- <p class="section-sub">此品类在 Sorftime 未返回产品,需补充英文关键词或确认 domain 映射。</p>
- </div>
- <div class="card fade-up"><p style="color:var(--text-2);">数据未采集或关键词无匹配产品。建议补充英文关键词后重跑采集脚本。</p></div>
- </div>
- </section>`;
- }
- const hasReviews = Object.keys(amz.reviewsSummary || {}).length > 0;
- const hasDetails = (amz.detailProducts || []).length > 0;
- // ═════════════════════════════════════════════
- // 6.0 · 核心发现 & 机会评估(分析层 · hero)
- // ═════════════════════════════════════════════
- const op = amz.opportunity;
- const findings = amz.keyFindings || [];
- const toneColor = (t) => ({
- green: 'var(--green)', amber: 'var(--amber)', rose: 'var(--rose)',
- blue: 'var(--blue)', purple: 'var(--purple)',
- }[t] || 'var(--amber)');
- const executiveLayer = op ? `
- <h3 class="text-h2 fade-up" style="margin-top:12px;margin-bottom:16px;">6.0 · 核心发现 & 机会评估</h3>
- <p class="section-sub fade-up" style="margin-bottom:22px;">基于 ${amz.total} 款产品 + ${op.dimensions.length} 维度诊断的判断层 · 每个结论都是可以直接拍板的行动锚点</p>
- <div class="grid" style="grid-template-columns:1.1fr 2fr;gap:22px;margin-bottom:28px;">
- <div class="card card-glow fade-up" style="text-align:center;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:32px 24px;border-left:4px solid ${toneColor(op.gradeColor)};">
- <div class="text-mono text-xs" style="color:var(--text-3);letter-spacing:0.18em;margin-bottom:10px;">CATEGORY OPPORTUNITY</div>
- <div style="font-size:3.2rem;font-weight:900;font-family:'JetBrains Mono';color:${toneColor(op.gradeColor)};line-height:1;margin-bottom:8px;">${esc(op.grade.split(' · ')[0])}</div>
- <div class="text-sm" style="color:var(--text-2);font-weight:600;margin-bottom:14px;">${esc(op.grade.split(' · ')[1] || '')}</div>
- <div style="background:rgba(255,255,255,0.04);border-radius:999px;padding:6px 16px;font-family:'JetBrains Mono';font-weight:700;font-size:0.9rem;">
- <span style="color:${toneColor(op.gradeColor)};">${op.totalScore}</span><span style="color:var(--text-3);"> / ${op.maxScore}</span>
- </div>
- <div class="text-xs" style="margin-top:14px;color:var(--text-3);">甜点价位:<strong style="color:var(--amber);">${esc(op.sweetSpot)}</strong></div>
- </div>
- <div style="display:flex;flex-direction:column;gap:10px;">
- ${op.dimensions.map((d) => `<div class="fade-up" style="display:flex;align-items:center;gap:14px;padding:12px 16px;background:rgba(255,255,255,0.02);border-radius:10px;border:1px solid rgba(255,255,255,0.05);">
- <div style="font-size:1.3rem;flex-shrink:0;width:36px;text-align:center;">${d.icon}</div>
- <div style="flex:1;min-width:0;">
- <div style="display:flex;gap:10px;align-items:center;margin-bottom:4px;flex-wrap:wrap;">
- <span style="font-weight:700;font-size:0.86rem;">${esc(d.name)}</span>
- <span class="text-mono" style="font-size:0.68rem;padding:2px 8px;border-radius:999px;background:rgba(245,166,35,0.12);color:var(--amber);letter-spacing:0.1em;">${esc(d.level)}</span>
- <span style="margin-left:auto;display:flex;gap:3px;">${[1,2,3,4,5].map((i) => `<span style="width:6px;height:6px;border-radius:50%;background:${i <= d.score ? 'var(--amber)' : 'rgba(255,255,255,0.1)'};"></span>`).join('')}</span>
- </div>
- <div class="text-xs" style="color:var(--text-2);line-height:1.5;">${d.note.replace(/\*\*(.+?)\*\*/g, '<strong style="color:var(--amber);">$1</strong>')}</div>
- </div>
- </div>`).join('')}
- </div>
- </div>
- <div class="grid grid-2" style="margin-bottom:40px;gap:16px;">
- ${findings.map((f, i) => `<div class="fade-up" style="position:relative;padding:20px 22px 20px 26px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);border-radius:12px;border-left:4px solid ${toneColor(f.tone)};">
- <div style="position:absolute;top:16px;right:20px;font-family:'JetBrains Mono';color:var(--text-3);font-size:0.7rem;letter-spacing:0.1em;">FINDING ${String(i + 1).padStart(2, '0')}</div>
- <div style="display:flex;gap:10px;align-items:flex-start;margin-bottom:8px;">
- <span style="font-size:1.2rem;flex-shrink:0;">${f.icon}</span>
- <h4 style="font-size:1rem;font-weight:700;color:${toneColor(f.tone)};line-height:1.4;">${esc(f.title)}</h4>
- </div>
- <p class="text-sm" style="color:var(--text-2);line-height:1.65;">${f.body}</p>
- </div>`).join('')}
- </div>` : '';
- // 6.1 品类全景
- const overview = `
- <h3 class="text-h2 fade-up" style="margin-top:12px;margin-bottom:16px;">6.1 · 品类全景</h3>
- <div class="grid grid-4" style="margin-bottom:28px;">
- <div class="metric fade-up">
- <div class="metric-label">产品样本</div>
- <div class="metric-value" data-count="${amz.total}">${amz.total}</div>
- <div class="metric-sub">${amz.keywords.length} 组关键词 × 3 页</div>
- </div>
- <div class="metric fade-up">
- <div class="metric-label">均价 / 中位</div>
- <div class="metric-value">$${amz.priceStats.mean.toFixed(1)}</div>
- <div class="metric-sub">中位 $${amz.priceStats.median.toFixed(2)} · $${amz.priceStats.min.toFixed(1)}–$${amz.priceStats.max.toFixed(0)}</div>
- </div>
- <div class="metric fade-up">
- <div class="metric-label">平均评分</div>
- <div class="metric-value" style="color:var(--green);">${amz.avgRating.toFixed(2)}</div>
- <div class="metric-sub">用户质量期望 · 中位 ${amz.medianRating.toFixed(1)}</div>
- </div>
- <div class="metric fade-up">
- <div class="metric-label">月销总额</div>
- <div class="metric-value" style="color:var(--amber);">${fmtMoney(amz.totalMonthlyRevenue)}</div>
- <div class="metric-sub">CR5 ${amz.top5Share.toFixed(1)}% · HHI ${Math.round(amz.hhi)}</div>
- </div>
- </div>
- <div class="grid grid-2" style="margin-bottom:40px;">
- <div class="card fade-up">
- <div class="text-mono text-xs text-amber">PRICE BAND</div>
- <h3 class="text-h2" style="margin-top:10px;">价格带分布</h3>
- <div id="chartPrice" class="chart-container" style="margin-top:20px;min-height:280px;"></div>
- </div>
- <div class="card fade-up">
- <div class="text-mono text-xs text-green">RATING DISTRIBUTION</div>
- <h3 class="text-h2" style="margin-top:10px;">评分分布</h3>
- <div id="chartRatingDist" class="chart-container" style="margin-top:20px;min-height:280px;"></div>
- </div>
- </div>`;
- // 6.2 品牌格局
- const top10Brands = amz.topBrands.slice(0, 10);
- const brandStructure = `
- <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:16px;">6.2 · 品牌格局</h3>
- <div class="grid grid-2" style="margin-bottom:32px;">
- <div class="card fade-up">
- <div class="text-mono text-xs text-amber">BRAND SHARE</div>
- <h3 class="text-h2" style="margin-top:10px;">品牌月销份额</h3>
- <div id="chartBrandShare" class="chart-container" style="margin-top:20px;min-height:320px;"></div>
- </div>
- <div class="card fade-up">
- <div class="text-mono text-xs text-amber">TOP 10 BRANDS</div>
- <h3 class="text-h2" style="margin-top:10px;">Top 10 品牌表</h3>
- <div class="table-wrap" style="margin-top:16px;">
- <table class="data-tbl">
- <thead><tr><th style="width:40px;">#</th><th>品牌</th><th>SKU</th><th>份额</th><th>月销额</th></tr></thead>
- <tbody>
- ${top10Brands.map((b, i) => `<tr>
- <td class="tb-num">${String(i + 1).padStart(2, '0')}</td>
- <td style="font-weight:600;">${esc(b.brand)}</td>
- <td class="tb-num">${b.count}</td>
- <td class="tb-num" style="color:var(--amber);">${b.share.toFixed(1)}%</td>
- <td class="tb-num">${fmtMoney(b.sales)}</td>
- </tr>`).join('')}
- </tbody>
- </table>
- </div>
- <div style="margin-top:14px;padding:10px 14px;background:rgba(255,77,141,.08);border-left:3px solid var(--rose);border-radius:0 8px 8px 0;font-size:0.82rem;color:var(--text-2);">
- <strong class="text-rose">⚠ 市场集中度:</strong>CR5 = <strong>${amz.top5Share.toFixed(1)}%</strong>, HHI = <strong>${Math.round(amz.hhi)}</strong>
- ${amz.top5Share >= 60 ? '(<span style="color:var(--rose);">高度集中,头部垄断</span>)' : amz.top5Share >= 40 ? '(<span style="color:var(--amber);">中度集中,需差异化切入</span>)' : '(<span style="color:var(--green);">竞争充分,进入门槛低</span>)'}
- </div>
- </div>
- </div>`;
- // ═════════════════════════════════════
- // 6.2+ · Top 5 竞品深度画像(分析层 · 紧跟 6.2 品牌格局)
- // ═════════════════════════════════════
- const dossiers = amz.competitorDossiers || [];
- const dossiersSection = dossiers.length ? `
- <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:12px;">6.2+ · Top ${dossiers.length} 竞品深度画像</h3>
- <p class="section-sub fade-up" style="margin-bottom:22px;">不是看品牌数据,而是看<strong class="text-amber">它在市场中的位置</strong>、<strong class="text-green">它外部的优势</strong>、<strong class="text-rose">它给新玩家留下的缝</strong></p>
- <div class="grid grid-2" style="margin-bottom:40px;gap:20px;">
- ${dossiers.map((d, i) => `<div class="card fade-up" style="border-left:4px solid ${toneColor(d.positionColor)};padding:22px 22px 20px;">
- <div style="display:flex;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap;">
- <div style="display:flex;align-items:baseline;gap:10px;">
- <span class="text-mono" style="color:var(--text-3);font-size:0.7rem;">#${String(i + 1).padStart(2, '0')}</span>
- <h4 style="font-size:1.05rem;font-weight:700;">${esc(d.brand)}</h4>
- </div>
- <span class="text-mono" style="font-size:0.72rem;padding:4px 10px;border-radius:999px;background:rgba(255,255,255,0.05);color:${toneColor(d.positionColor)};border:1px solid ${toneColor(d.positionColor)};letter-spacing:0.06em;white-space:nowrap;">${esc(d.positioning)}</span>
- </div>
- <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px;padding:10px 12px;background:rgba(255,255,255,0.02);border-radius:8px;">
- <div><div class="text-xs" style="color:var(--text-3);">份额</div><div style="font-weight:700;color:var(--amber);font-family:'JetBrains Mono';font-size:0.95rem;">${d.share.toFixed(1)}%</div></div>
- <div><div class="text-xs" style="color:var(--text-3);">SKU</div><div style="font-weight:700;font-family:'JetBrains Mono';font-size:0.95rem;">${d.skuCount}</div></div>
- <div><div class="text-xs" style="color:var(--text-3);">均价</div><div style="font-weight:700;font-family:'JetBrains Mono';font-size:0.95rem;">$${d.avgPrice.toFixed(1)}</div></div>
- <div><div class="text-xs" style="color:var(--text-3);">评分</div><div style="font-weight:700;color:var(--green);font-family:'JetBrains Mono';font-size:0.95rem;">★ ${d.avgRating.toFixed(2)}</div></div>
- </div>
- ${d.topProduct ? `<div style="padding:10px 12px;background:rgba(245,166,35,0.04);border-left:2px solid var(--amber);border-radius:0 6px 6px 0;margin-bottom:12px;">
- <div class="text-xs" style="color:var(--amber);font-family:'JetBrains Mono';margin-bottom:4px;letter-spacing:0.08em;">TOP PRODUCT · ${esc(d.topProduct.asin)}</div>
- <div class="text-sm" style="line-height:1.4;color:var(--text-1);margin-bottom:6px;">${esc((d.topProduct.title || '').slice(0, 70))}</div>
- <div class="text-xs text-mono" style="color:var(--text-3);display:flex;gap:12px;flex-wrap:wrap;">
- <span>$${d.topProduct.price.toFixed(2)}</span>
- <span style="color:var(--green);">★ ${d.topProduct.rating.toFixed(1)}</span>
- <span>${fmtNum(d.topProduct.reviewCount)} 评</span>
- <span style="color:var(--amber);">${fmtNum(d.topProduct.monthlySales)}/月</span>
- </div>
- </div>` : ''}
- <div class="grid grid-2" style="gap:10px;font-size:0.8rem;">
- <div>
- <div class="text-xs" style="color:var(--green);font-weight:700;margin-bottom:6px;letter-spacing:0.08em;">✅ STRENGTHS</div>
- ${d.strengths.length ? d.strengths.map((s) => `<div style="padding:4px 0;display:flex;justify-content:space-between;gap:8px;color:var(--text-2);"><span>${esc(s.label)}</span><span class="text-mono" style="color:var(--green);">×${s.n}</span></div>`).join('') : '<div class="text-xs" style="color:var(--text-3);padding:4px 0;">评论样本少,未提炼出明显优势</div>'}
- </div>
- <div>
- <div class="text-xs" style="color:var(--rose);font-weight:700;margin-bottom:6px;letter-spacing:0.08em;">⚠ WEAKNESSES</div>
- ${d.weaknesses.length ? d.weaknesses.map((w) => `<div style="padding:4px 0;display:flex;justify-content:space-between;gap:8px;color:var(--text-2);"><span>${esc(w.label)}</span><span class="text-mono" style="color:var(--rose);">×${w.n}</span></div>`).join('') : '<div class="text-xs" style="color:var(--text-3);padding:4px 0;">差评样本少,未捉到明显缝</div>'}
- </div>
- </div>
- <div style="margin-top:12px;padding-top:12px;border-top:1px dashed rgba(255,255,255,0.08);display:flex;gap:10px;font-size:0.7rem;color:var(--text-3);">
- <span>价格差 ${d.priceVsCategory > 0 ? '+' : ''}$${d.priceVsCategory} · 评分差 ${d.ratingVsCategory > 0 ? '+' : ''}${d.ratingVsCategory}</span>
- <span style="margin-left:auto;">${d.totalReviews ? fmtNum(d.totalReviews) + ' 评论' : ''}</span>
- </div>
- </div>`).join('')}
- </div>` : '';
- // 6.3 Top 产品矩阵
- const topBySalesRows = amz.topBySales.slice(0, 8).map((p, i) => `<tr>
- <td class="tb-num">${String(i + 1).padStart(2, '0')}</td>
- <td><div style="font-weight:600;">${esc(p.Brand || 'N/A')}</div><div class="text-xs" style="margin-top:3px;color:var(--text-3);">${esc((p.Title || '').slice(0, 80))}</div></td>
- <td class="tb-num">$${((p.SalesPrice || p.Price || 0) / 100).toFixed(2)}</td>
- <td class="tb-num" style="color:var(--amber);">${fmtNum(p.ListingSalesVolumeOfMonth || 0)}</td>
- <td>${p.Ratings ? `★ ${p.Ratings}` : '—'}<br><span class="text-xs text-mono" style="color:var(--text-3);">${fmtNum(p.RatingsCount || 0)} 评</span></td>
- <td class="text-mono text-xs" style="color:var(--text-3);">${esc(p.Asin || '—')}</td>
- </tr>`).join('');
- const topByReviewsRows = amz.topByReviews.slice(0, 8).map((p, i) => `<tr>
- <td class="tb-num">${String(i + 1).padStart(2, '0')}</td>
- <td><div style="font-weight:600;">${esc(p.Brand || 'N/A')}</div><div class="text-xs" style="margin-top:3px;color:var(--text-3);">${esc((p.Title || '').slice(0, 80))}</div></td>
- <td style="color:var(--green);font-weight:600;">★ ${p.Ratings || '—'}</td>
- <td class="tb-num" style="color:var(--amber);">${fmtNum(p.RatingsCount || 0)}</td>
- <td class="tb-num">$${((p.SalesPrice || p.Price || 0) / 100).toFixed(2)}</td>
- </tr>`).join('');
- const topMatrix = `
- <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:16px;">6.3 · Top 产品矩阵</h3>
- <div class="grid grid-2" style="margin-bottom:32px;">
- <div class="card fade-up">
- <div class="text-mono text-xs text-amber">MONTHLY SALES LEADERS</div>
- <h3 class="text-h2" style="margin-top:10px;">月销量 Top 8</h3>
- <div class="table-wrap" style="margin-top:16px;">
- <table class="data-tbl">
- <thead><tr><th>#</th><th>品牌 / 产品</th><th>售价</th><th>月销</th><th>评分</th><th>ASIN</th></tr></thead>
- <tbody>${topBySalesRows}</tbody>
- </table>
- </div>
- </div>
- <div class="card fade-up">
- <div class="text-mono text-xs text-green">REVIEW VOLUME LEADERS</div>
- <h3 class="text-h2" style="margin-top:10px;">评论数 Top 8 · 用户口碑沉淀</h3>
- <div class="table-wrap" style="margin-top:16px;">
- <table class="data-tbl">
- <thead><tr><th>#</th><th>品牌 / 产品</th><th>评分</th><th>评论数</th><th>售价</th></tr></thead>
- <tbody>${topByReviewsRows}</tbody>
- </table>
- </div>
- </div>
- </div>
- <div class="card fade-up" style="margin-bottom:40px;">
- <div class="text-mono text-xs text-purple">REVIEW COUNT DISTRIBUTION</div>
- <h3 class="text-h2" style="margin-top:10px;">评论数分布 · 口碑沉淀结构</h3>
- <div id="chartReviewCount" class="chart-container" style="margin-top:20px;min-height:260px;"></div>
- </div>`;
- // 6.4 深度产品画像
- const detailSection = hasDetails ? `
- <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:16px;">6.4 · 深度产品画像</h3>
- <p class="section-sub fade-up" style="margin-bottom:24px;">对 Top ${amz.detailProducts.length} 款拉取完整 ProductRequest · 含 BSR / 上线日 / Buybox 卖家 / 变体数</p>
- <div class="grid grid-2" style="margin-bottom:40px;">
- ${amz.detailProducts.slice(0, 6).map((p) => {
- const photo = (typeof p.photo === 'string' ? p.photo : p.photo?.Url) || '';
- return `<div class="card fade-up" style="display:flex;gap:16px;">
- ${photo ? `<img src="${esc(photo)}" alt="" loading="lazy" onerror="this.style.display='none'" style="width:96px;height:96px;object-fit:cover;border-radius:8px;flex-shrink:0;">` : '<div style="width:96px;height:96px;background:rgba(255,255,255,0.04);border-radius:8px;flex-shrink:0;"></div>'}
- <div style="flex:1;min-width:0;">
- <div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;">
- <span class="tag" style="background:rgba(245,166,35,.12);color:var(--amber);">${esc(p.brand || 'N/A')}</span>
- ${p.hasVideo ? '<span class="tag">有视频</span>' : ''}
- ${p.aplus ? '<span class="tag">A+</span>' : ''}
- ${p.brandStore ? '<span class="tag">品牌店</span>' : ''}
- ${p.variantCount > 1 ? `<span class="tag">${p.variantCount} 变体</span>` : ''}
- </div>
- <div style="font-weight:600;font-size:0.88rem;margin-bottom:8px;line-height:1.4;">${esc((p.title || '').slice(0, 78))}</div>
- <div class="text-xs text-mono" style="color:var(--text-3);margin-bottom:10px;">ASIN · ${esc(p.asin)}</div>
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;font-size:0.78rem;">
- <div><div style="color:var(--text-3);">售价</div><div style="color:var(--amber);font-weight:700;font-family:'JetBrains Mono';">${fmtMoney(p.price)}</div></div>
- <div><div style="color:var(--text-3);">评分</div><div style="color:var(--green);font-weight:700;">★ ${p.rating || '—'} · ${fmtNum(p.ratingsCount || 0)}</div></div>
- <div><div style="color:var(--text-3);">月销</div><div style="font-weight:700;">${fmtNum(p.monthlySales || 0)}</div></div>
- <div><div style="color:var(--text-3);">BSR</div><div style="font-weight:700;">${p.bsrRank ? '#' + fmtNum(p.bsrRank) : '—'}</div></div>
- <div><div style="color:var(--text-3);">上线天数</div><div style="font-weight:700;">${p.onlineDays ? p.onlineDays + 'd' : '—'}</div></div>
- <div><div style="color:var(--text-3);">Buybox 卖家</div><div style="font-weight:700;font-size:0.7rem;">${esc((p.buyboxSeller || '—').slice(0, 16))}</div></div>
- </div>
- </div>
- </div>`;
- }).join('')}
- </div>
- ${amz.avgOnlineDays > 0 ? `
- <div class="card fade-up" style="margin-bottom:40px;border-left:3px solid var(--blue);">
- <div class="text-mono text-xs" style="color:var(--blue);">MARKET MATURITY</div>
- <h3 class="text-h2" style="margin-top:10px;">品类成熟度:平均上线 ${amz.avgOnlineDays} 天 (${(amz.avgOnlineDays / 365).toFixed(1)} 年)</h3>
- <p class="text-sm" style="margin-top:10px;color:var(--text-2);line-height:1.7;">
- Top ${amz.detailProducts.length} 深度样本中 · <strong style="color:var(--green);">${amz.newProducts} 款新品(<180d)</strong> · <strong style="color:var(--amber);">${amz.matureProducts} 款成熟品(≥1年)</strong>
- ${amz.matureProducts > amz.newProducts ? '· 市场结构稳固,新玩家需强差异化' : '· 新品频出,市场动态,有迭代窗口'}
- </p>
- </div>` : ''}` : '';
- // 6.5 评论深度
- const reviewSamples = hasReviews ? Object.values(amz.reviewsSummary) : [];
- const reviewSection = hasReviews ? `
- <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:16px;">6.5 · 真实评论抽样</h3>
- <p class="section-sub fade-up" style="margin-bottom:24px;">对 Top ${reviewSamples.length} 款抓取真实评论 · 按星级分布 + 正负面抽样</p>
- <div class="grid grid-1" style="margin-bottom:32px;">
- ${reviewSamples.map((r) => {
- const posSample = r.positiveReviews[0];
- const negSample = r.negativeReviews[0];
- const starStr = [5, 4, 3, 2, 1].map((s) => `${'★'.repeat(s)} ${r.starDist[s] || 0}`).join(' ');
- return `<div class="card fade-up" style="border-left:3px solid var(--amber);">
- <div style="display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:14px;">
- <div>
- <div class="text-mono text-xs" style="color:var(--amber);">${esc(r.asin)}</div>
- <h4 style="font-size:0.95rem;margin-top:4px;">${esc(r.brand || 'N/A')} · ${esc((r.title || '').slice(0, 50))}</h4>
- </div>
- <div style="text-align:right;flex-shrink:0;">
- <div style="color:var(--green);font-weight:700;font-size:1.1rem;">★ ${r.averageRating.toFixed(2)}</div>
- <div class="text-xs" style="color:var(--text-3);">${r.total} 条抽样</div>
- </div>
- </div>
- <div class="text-xs text-mono" style="color:var(--text-3);margin-bottom:12px;">${starStr}</div>
- <div class="grid grid-2" style="gap:12px;">
- ${posSample ? `<div style="padding:12px;background:rgba(0,220,130,.06);border-left:2px solid var(--green);border-radius:0 6px 6px 0;">
- <div class="text-xs" style="color:var(--green);margin-bottom:6px;">★ ${posSample.Star ?? posSample.Rating ?? posSample.rating} · ${esc(String(posSample.Title || posSample.title || '好评').slice(0, 40))}${posSample.ConsumerName ? ' · ' + esc(String(posSample.ConsumerName).slice(0, 16)) : ''}${posSample.IsVP ? ' <span class="text-mono" style="color:var(--amber);">✓VP</span>' : ''}</div>
- <div class="text-sm" style="color:var(--text-2);line-height:1.5;">${esc(cleanText(posSample.Content || posSample.content || '').slice(0, 240))}</div>
- </div>` : '<div></div>'}
- ${negSample ? `<div style="padding:12px;background:rgba(255,77,141,.06);border-left:2px solid var(--rose);border-radius:0 6px 6px 0;">
- <div class="text-xs" style="color:var(--rose);margin-bottom:6px;">★ ${negSample.Star ?? negSample.Rating ?? negSample.rating} · ${esc(String(negSample.Title || negSample.title || '差评').slice(0, 40))}${negSample.ConsumerName ? ' · ' + esc(String(negSample.ConsumerName).slice(0, 16)) : ''}${negSample.IsVP ? ' <span class="text-mono" style="color:var(--amber);">✓VP</span>' : ''}</div>
- <div class="text-sm" style="color:var(--text-2);line-height:1.5;">${esc(cleanText(negSample.Content || negSample.content || '').slice(0, 240))}</div>
- </div>` : '<div></div>'}
- </div>
- </div>`;
- }).join('')}
- </div>
- <div class="card fade-up">
- <div class="text-mono text-xs text-amber">STAR DISTRIBUTION</div>
- <h3 class="text-h2" style="margin-top:10px;">各产品 5 星分布对比</h3>
- <div id="chartStarDist" class="chart-container" style="margin-top:20px;min-height:320px;"></div>
- </div>` : '';
- // ═════════════════════════════════════
- // 6.5+ · VOC 用户原声(分析层 · 紧跟 6.5 评论抽样)
- // ═════════════════════════════════════
- const vt = amz.amazonThemes;
- const renderBarRow = (items, color, totalRef) => items.map((t) => {
- const pct = totalRef ? Math.min(100, (t.count / totalRef * 100)) : 0;
- return `<div class="fade-up" style="margin-bottom:12px;">
- <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:5px;gap:8px;">
- <div style="display:flex;gap:8px;align-items:center;min-width:0;">
- <span style="font-size:1rem;flex-shrink:0;">${t.emoji || t.icon || '•'}</span>
- <span style="font-weight:600;font-size:0.82rem;overflow:hidden;text-overflow:ellipsis;">${esc(t.tag.split(' · ')[1] || t.tag)}</span>
- </div>
- <span class="text-mono" style="font-size:0.78rem;color:${color};flex-shrink:0;">${t.count} · ${t.pct.toFixed(1)}%</span>
- </div>
- <div style="height:6px;background:rgba(255,255,255,0.05);border-radius:3px;overflow:hidden;">
- <div style="height:100%;background:${color};width:${pct.toFixed(1)}%;border-radius:3px;transition:width 1.2s cubic-bezier(.16,.84,.33,1);"></div>
- </div>
- ${t.examples?.[0] ? `<div class="text-xs" style="margin-top:6px;color:var(--text-3);line-height:1.5;padding-left:24px;font-style:italic;">"${esc(t.examples[0].text.slice(0, 90))}${t.examples[0].text.length > 90 ? '…' : ''}"${t.examples[0].brand ? ` · <span style="color:var(--text-2);">${esc(t.examples[0].brand)}</span>` : ''}</div>` : ''}
- </div>`;
- }).join('');
- const vocThemesSection = vt && (vt.pain.length || vt.bright.length || vt.scenarios.length) ? `
- <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:12px;">6.5+ · VOC 用户原声主题</h3>
- <p class="section-sub fade-up" style="margin-bottom:22px;">从 <strong class="text-amber">${vt.totalReviews}</strong> 条真实评论中聚类 · <strong class="text-rose">${vt.negReviews}</strong> 条差评 × <strong class="text-green">${vt.posReviews}</strong> 条好评 · 高频词聚类后得出的结构性主题</p>
- <div class="grid grid-3" style="margin-bottom:40px;gap:20px;">
- <div class="card fade-up" style="border-left:3px solid var(--rose);">
- <div class="text-mono text-xs" style="color:var(--rose);">PAIN POINTS · 痛点</div>
- <h3 class="text-h3" style="margin-top:8px;margin-bottom:16px;">🔴 用户核心痛点 Top ${vt.pain.length}</h3>
- ${vt.pain.length ? renderBarRow(vt.pain, 'var(--rose)', vt.negReviews || 1) : '<div class="text-sm" style="color:var(--text-3);">差评样本不足以提炼共同痛点</div>'}
- </div>
- <div class="card fade-up" style="border-left:3px solid var(--green);">
- <div class="text-mono text-xs" style="color:var(--green);">BRIGHT SPOTS · 亮点</div>
- <h3 class="text-h3" style="margin-top:8px;margin-bottom:16px;">🟢 用户最爱亮点 Top ${vt.bright.length}</h3>
- ${vt.bright.length ? renderBarRow(vt.bright, 'var(--green)', vt.posReviews || 1) : '<div class="text-sm" style="color:var(--text-3);">好评样本不足</div>'}
- </div>
- <div class="card fade-up" style="border-left:3px solid var(--blue);">
- <div class="text-mono text-xs" style="color:var(--blue);">USAGE SCENARIOS · 场景</div>
- <h3 class="text-h3" style="margin-top:8px;margin-bottom:16px;">📍 使用场景分布</h3>
- ${vt.scenarios.length ? renderBarRow(vt.scenarios, 'var(--blue)', vt.totalReviews || 1) : '<div class="text-sm" style="color:var(--text-3);">评论中未明显捉到场景信号</div>'}
- </div>
- </div>` : '';
- // ═════════════════════════════════════
- // 6.6 · 战略建议(分析层 · 收尾)
- // ═════════════════════════════════════
- const st = amz.strategy;
- const renderTrack = (track, accent, eyebrow) => `<div class="card card-accent fade-up" style="border-left-color:${accent};">
- <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;">
- <div class="text-mono text-xs" style="color:${accent};">${eyebrow}</div>
- <span class="text-mono" style="font-size:0.64rem;padding:3px 10px;border-radius:999px;background:${accent}20;color:${accent};letter-spacing:0.1em;font-weight:700;">${track.badge}</span>
- </div>
- <h3 class="text-h2" style="margin-top:10px;margin-bottom:14px;">${esc(track.title)}</h3>
- <ul style="list-style:none;display:flex;flex-direction:column;gap:10px;color:var(--text-2);font-size:0.88rem;line-height:1.6;">
- ${track.items.map((x) => `<li style="padding-left:20px;position:relative;"><span style="position:absolute;left:0;top:2px;color:${accent};">✓</span>${x}</li>`).join('')}
- </ul>
- </div>`;
- const strategySection = st ? `
- <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:12px;">6.6 · 从数据到行动 · 三马车战略建议</h3>
- <p class="section-sub fade-up" style="margin-bottom:22px;">基于以上所有分析维度(机会等级 <strong style="color:${toneColor(op?.gradeColor || 'amber')};">${esc(op?.grade || '—')}</strong>)导出的分阶段可执行动作</p>
- <div class="grid grid-3" style="margin-bottom:20px;gap:18px;">
- ${renderTrack(st.short, 'var(--amber)', 'TRACK 1 · 0-30 DAYS')}
- ${renderTrack(st.mid, 'var(--green)', 'TRACK 2 · 30-90 DAYS')}
- ${renderTrack(st.long, 'var(--purple)', 'TRACK 3 · 90+ DAYS')}
- </div>` : '';
- return `<section class="report-section" id="s6" data-tone="green" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">06 · Amazon Benchmark</div>
- <h2 class="text-h1"><span class="headline-anim">Amazon 国际对标</span></h2>
- <p class="section-sub">${amz.keywords.length} 组英文关键词 × 3 页 · ${amz.total} 款产品 · Top ${amz.detailProducts.length} 深度详情 · Top ${Object.keys(amz.reviewsSummary).length} 真实评论</p>
- </div>
- ${executiveLayer}
- ${overview}
- ${brandStructure}
- ${dossiersSection}
- ${topMatrix}
- ${detailSection}
- ${reviewSection}
- ${vocThemesSection}
- ${strategySection}
- </div>
- </section>`;
- }
- function renderSDouyin({ A, slideNum, slideTotal }) {
- const dy = A.douyin;
- if (!dy || dy.empty) return '';
- const v = dy.video;
- const u = dy.userProfile;
- const topCmts = dy.topComments || [];
- const topPosts = dy.topPosts || [];
- const userPosts = dy.userPosts || [];
- const searches = dy.searches || {};
- const shareType = dy.shareType || 'video';
- const accountCards = dy.accountCards || [];
- const agg = dy.aggMetrics;
- // ---------- 账号矩阵(主号 + 竞品号 + 垂类号,多卡一排)----------
- // 只在有 2+ 账号时才展示矩阵,单账号走 heroUser 旧样式
- const roleToneMap = {
- '官方号': { bg: 'rgba(255,0,80,.14)', fg: '#FF3E75', border: 'rgba(255,0,80,.3)' },
- '品牌竞品': { bg: 'rgba(245,166,35,.14)', fg: '#F5A623', border: 'rgba(245,166,35,.3)' },
- '品牌竞品号': { bg: 'rgba(245,166,35,.14)', fg: '#F5A623', border: 'rgba(245,166,35,.3)' },
- '垂类博主': { bg: 'rgba(139,92,246,.14)', fg: '#A78BFA', border: 'rgba(139,92,246,.3)' },
- '垂类博主号': { bg: 'rgba(139,92,246,.14)', fg: '#A78BFA', border: 'rgba(139,92,246,.3)' },
- '补充号': { bg: 'rgba(0,220,130,.14)', fg: '#22C581', border: 'rgba(0,220,130,.3)' },
- };
- const accountMatrix = accountCards.length >= 2 ? `
- <div class="section-sub rise-in" style="margin-top:12px;margin-bottom:14px;color:var(--text-3);">
- <strong style="color:#FF3E75;">抖音账号矩阵 · ${accountCards.length} 个账号</strong>
- ${agg ? `· 累计粉丝 <strong style="color:var(--amber);">${fmtNum(agg.total_followers)}</strong>
- · 采样作品 <strong>${agg.total_posts_sampled}</strong>
- · 认证账号 <strong style="color:var(--green);">${agg.verified_count}/${agg.total_accounts}</strong>` : ''}
- </div>
- <div class="grid grid-${Math.min(accountCards.length, 3)}" style="margin-bottom:32px;gap:18px;">
- ${accountCards.map((c) => {
- const tone = roleToneMap[c.role] || roleToneMap['官方号'];
- const hasData = !c.error && c.nickname !== '—';
- if (!hasData) {
- return `<div class="card fade-up" style="border-left:3px solid ${tone.fg};opacity:.6;">
- <div class="text-mono text-xs" style="color:${tone.fg};letter-spacing:.08em;">${esc(c.role)}</div>
- <h4 class="text-h3" style="margin-top:8px;">${esc(c.label || c.role)}</h4>
- <div class="text-sm" style="color:var(--text-3);margin-top:10px;line-height:1.5;">
- ⚠ ${esc((c.error || '数据不可用').slice(0, 80))}
- </div>
- <div class="text-xs text-mono" style="margin-top:10px;color:var(--text-3);word-break:break-all;">${esc(c.share_url || '')}</div>
- </div>`;
- }
- return `<div class="card fade-up" style="border-left:3px solid ${tone.fg};">
- <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:12px;">
- <span class="tag" style="background:${tone.bg};color:${tone.fg};border-color:${tone.border};">${esc(c.role)}</span>
- ${c.custom_verify ? `<span class="tag" style="background:rgba(245,166,35,.12);color:var(--amber);font-size:.68rem;">✓ ${esc(c.custom_verify.slice(0, 14))}</span>` : ''}
- </div>
- <div style="display:flex;gap:12px;align-items:center;margin-bottom:12px;">
- ${c.avatar ? `<img src="${esc(c.avatar)}" alt="" loading="lazy" onerror="this.style.display='none'" style="width:56px;height:56px;border-radius:50%;border:1.5px solid ${tone.border};object-fit:cover;flex-shrink:0;">` : `<div style="width:56px;height:56px;border-radius:50%;background:var(--bg-3);flex-shrink:0;"></div>`}
- <div style="flex:1;min-width:0;">
- <div class="text-h3" style="font-size:.95rem;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc((c.nickname || '—').slice(0, 20))}</div>
- <div class="text-xs" style="color:var(--text-3);margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc((c.signature || '').slice(0, 40))}</div>
- </div>
- </div>
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:12px;">
- <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
- <div style="color:${tone.fg};font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(c.follower_count)}</div>
- <div style="color:var(--text-3);font-size:.62rem;letter-spacing:.06em;margin-top:2px;">粉丝</div>
- </div>
- <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
- <div style="font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(c.aweme_count)}</div>
- <div style="color:var(--text-3);font-size:.62rem;letter-spacing:.06em;margin-top:2px;">作品</div>
- </div>
- <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
- <div style="color:var(--green);font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(c.total_favorited)}</div>
- <div style="color:var(--text-3);font-size:.62rem;letter-spacing:.06em;margin-top:2px;">获赞</div>
- </div>
- </div>
- ${c.top_post ? `<div style="padding:10px;background:rgba(255,255,255,.02);border-radius:6px;border-left:2px solid ${tone.border};">
- <div class="text-xs" style="color:var(--text-3);letter-spacing:.08em;font-family:'JetBrains Mono';margin-bottom:6px;">TOP POST</div>
- <div class="text-xs" style="line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:2.2em;">${esc(cleanText(c.top_post.desc || '').slice(0, 90))}</div>
- <div class="text-xs text-mono" style="margin-top:6px;color:var(--text-3);display:flex;gap:10px;">
- <span style="color:var(--amber);">♥ ${fmtNum(c.top_post.digg)}</span>
- <span style="color:var(--green);">💬 ${fmtNum(c.top_post.comment)}</span>
- ${c.top_post.play > 0 ? `<span>▶ ${fmtNum(c.top_post.play)}</span>` : ''}
- </div>
- </div>` : `<div class="text-xs" style="color:var(--text-3);padding:10px;background:rgba(255,255,255,.02);border-radius:6px;">暂无作品数据</div>`}
- </div>`;
- }).join('')}
- </div>` : '';
- // ---------- Hero:作者画像卡 + 作品节奏 ----------
- const heroUser = u ? `
- <div class="grid grid-2" style="margin-bottom:36px;gap:24px;">
- <div class="card card-glow fade-up" style="display:flex;gap:18px;align-items:flex-start;">
- ${u.avatar ? `<img src="${esc(u.avatar)}" alt="" loading="lazy" onerror="this.style.display='none'" style="width:96px;height:96px;border-radius:50%;border:2px solid rgba(245,166,35,0.4);flex-shrink:0;object-fit:cover;">` : '<div style="width:96px;height:96px;border-radius:50%;background:var(--bg-3);flex-shrink:0;"></div>'}
- <div style="flex:1;min-width:0;">
- <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;">
- <span class="tag" style="background:rgba(255,0,80,.15);color:#FF3E75;border-color:rgba(255,0,80,.3);">抖音 · ${shareType === 'user' ? '品牌旗舰店' : '视频'}</span>
- ${u.custom_verify ? `<span class="tag" style="background:rgba(245,166,35,.15);color:var(--amber);">${esc(u.custom_verify.slice(0, 20))}</span>` : ''}
- ${u.ip_location ? `<span class="tag gray">${esc(u.ip_location)}</span>` : ''}
- </div>
- <h3 class="text-h2" style="margin-bottom:8px;">${esc(u.nickname || '—')}</h3>
- <div class="text-sm" style="margin-bottom:14px;color:var(--text-2);line-height:1.6;">${esc((u.signature || '暂无简介').slice(0, 120))}</div>
- <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;font-size:0.78rem;">
- <div><div style="color:var(--text-3);">粉丝</div><div style="color:var(--amber);font-weight:800;font-family:'JetBrains Mono';font-size:1.1rem;">${fmtNum(u.follower_count || 0)}</div></div>
- <div><div style="color:var(--text-3);">作品</div><div style="font-weight:800;font-family:'JetBrains Mono';font-size:1.1rem;">${fmtNum(u.aweme_count || 0)}</div></div>
- <div><div style="color:var(--text-3);">获赞</div><div style="color:var(--green);font-weight:800;font-family:'JetBrains Mono';font-size:1.1rem;">${fmtNum(u.total_favorited || 0)}</div></div>
- <div><div style="color:var(--text-3);">关注</div><div style="font-weight:800;font-family:'JetBrains Mono';font-size:1.1rem;">${fmtNum(u.following_count || 0)}</div></div>
- </div>
- </div>
- </div>
- <div class="card fade-up">
- <div class="text-mono text-xs" style="color:#FF3E75;">PRODUCTION RHYTHM</div>
- <h3 class="text-h2" style="margin-top:8px;">作品节奏均值</h3>
- ${dy.authorStats ? `<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:18px;">
- <div style="text-align:center;padding:14px;background:rgba(245,166,35,0.06);border-radius:10px;">
- <div style="color:var(--amber);font-weight:800;font-size:1.4rem;font-family:'JetBrains Mono';">${fmtNum(Math.round(dy.authorStats.avgDigg))}</div>
- <div style="color:var(--text-3);font-size:0.7rem;margin-top:4px;letter-spacing:0.08em;">AVG ♥ LIKES</div>
- </div>
- <div style="text-align:center;padding:14px;background:rgba(0,220,130,0.06);border-radius:10px;">
- <div style="color:var(--green);font-weight:800;font-size:1.4rem;font-family:'JetBrains Mono';">${fmtNum(Math.round(dy.authorStats.avgComment))}</div>
- <div style="color:var(--text-3);font-size:0.7rem;margin-top:4px;letter-spacing:0.08em;">AVG 💬 COMMENT</div>
- </div>
- <div style="text-align:center;padding:14px;background:rgba(139,92,246,0.06);border-radius:10px;">
- <div style="color:var(--purple);font-weight:800;font-size:1.4rem;font-family:'JetBrains Mono';">${fmtNum(Math.round(dy.authorStats.avgPlay))}</div>
- <div style="color:var(--text-3);font-size:0.7rem;margin-top:4px;letter-spacing:0.08em;">AVG ▶ PLAY</div>
- </div>
- </div>
- <div style="margin-top:16px;padding:10px 14px;background:rgba(255,255,255,.03);border-left:2px solid #FF3E75;border-radius:0 6px 6px 0;font-size:0.8rem;color:var(--text-2);line-height:1.6;">
- ${(u.follower_count || 0) > 500000
- ? `<strong style="color:#FF3E75;">头部品牌号</strong>:${fmtNum(u.follower_count)} 粉丝 + 高频发布 (${u.aweme_count} 作品) 已建立稳定内容资产,适合做 KOL 型种草。`
- : (u.follower_count || 0) > 10000
- ? `<strong style="color:var(--amber);">成长期品牌号</strong>:粉丝 ${fmtNum(u.follower_count)},内容储备 ${u.aweme_count} 支,应加大高转化爆款内容产出。`
- : `<strong style="color:var(--text-2);">起步品牌号</strong>:粉丝仅 ${fmtNum(u.follower_count)},首要任务是建立内容基线和选定垂类话题。`}
- </div>` : '<p class="text-sm" style="color:var(--text-3);margin-top:14px;">暂无作品均值数据。</p>'}
- </div>
- </div>` : '';
- // ---------- Hot video 卡片(仅 video-type 分享链)----------
- const videoCard = v ? `
- <div class="card fade-up" style="margin-bottom:32px;display:flex;gap:18px;align-items:flex-start;">
- ${v.cover ? `<img src="${esc(v.cover)}" alt="" loading="lazy" onerror="this.style.display='none'" style="width:140px;height:190px;object-fit:cover;border-radius:10px;flex-shrink:0;">` : ''}
- <div style="flex:1;min-width:0;">
- <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;">
- <span class="tag" style="background:rgba(255,0,80,.12);color:#FF3E75;border-color:rgba(255,0,80,.25);">品牌视频</span>
- ${(v.text_extra || []).slice(0, 3).map((t) => `<span class="tag">#${esc(t)}</span>`).join('')}
- </div>
- <div style="font-weight:600;font-size:0.94rem;line-height:1.5;margin-bottom:12px;">${esc(cleanText(v.desc || '').slice(0, 160))}</div>
- <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;font-size:0.78rem;margin-bottom:10px;">
- <div><div style="color:var(--text-3);">点赞</div><div style="color:var(--amber);font-weight:700;font-family:'JetBrains Mono';font-size:1rem;">${fmtNum(v.statistics?.digg_count || 0)}</div></div>
- <div><div style="color:var(--text-3);">评论</div><div style="color:var(--green);font-weight:700;font-family:'JetBrains Mono';font-size:1rem;">${fmtNum(v.statistics?.comment_count || 0)}</div></div>
- <div><div style="color:var(--text-3);">分享</div><div style="font-weight:700;font-family:'JetBrains Mono';font-size:1rem;">${fmtNum(v.statistics?.share_count || 0)}</div></div>
- <div><div style="color:var(--text-3);">播放</div><div style="font-weight:700;font-family:'JetBrains Mono';font-size:1rem;">${fmtNum(v.statistics?.play_count || 0)}</div></div>
- </div>
- </div>
- </div>` : '';
- // ---------- Top Posts 散点 + 卡片网格 ----------
- const topPostsSection = topPosts.length ? `
- <h3 class="text-h2 rise-in" style="margin-top:8px;margin-bottom:18px;">品牌号作品热度图 · Top ${topPosts.length}</h3>
- <div class="grid grid-2" style="margin-bottom:32px;gap:24px;">
- <div class="card fade-up">
- <div class="text-mono text-xs" style="color:#FF3E75;">POST SCATTER</div>
- <h4 class="text-h3" style="margin-top:8px;margin-bottom:16px;">X=点赞 · Y=评论 · 气泡=转发</h4>
- <div id="chartDyPosts" class="chart-container" style="min-height:380px;"></div>
- </div>
- <div class="card fade-up">
- <div class="text-mono text-xs" style="color:var(--amber);">TOP ${Math.min(topPosts.length, 5)} POSTS</div>
- <h4 class="text-h3" style="margin-top:8px;margin-bottom:16px;">爆款作品清单</h4>
- <div style="display:flex;flex-direction:column;gap:12px;">
- ${topPosts.slice(0, 5).map((p, i) => `<div style="display:flex;gap:12px;padding:10px;background:rgba(255,255,255,0.02);border-radius:8px;">
- ${p.cover ? `<img src="${esc(p.cover)}" alt="" loading="lazy" onerror="this.style.display='none'" style="width:56px;height:72px;object-fit:cover;border-radius:6px;flex-shrink:0;">` : '<div style="width:56px;height:72px;background:var(--bg-3);border-radius:6px;flex-shrink:0;"></div>'}
- <div style="flex:1;min-width:0;">
- <div class="text-xs" style="color:var(--amber);font-family:'JetBrains Mono';margin-bottom:3px;">#${String(i + 1).padStart(2, '0')}</div>
- <div class="text-sm" style="line-height:1.4;margin-bottom:6px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${esc(cleanText(p.desc || '').slice(0, 80))}</div>
- <div class="text-xs text-mono" style="color:var(--text-3);display:flex;gap:10px;">
- <span style="color:var(--amber);">♥ ${fmtNum(p.digg)}</span>
- <span style="color:var(--green);">💬 ${fmtNum(p.comment)}</span>
- <span>▶ ${fmtNum(p.play)}</span>
- </div>
- </div>
- </div>`).join('')}
- </div>
- </div>
- </div>` : '';
- // ---------- 评论墙 ----------
- const commentWall = topCmts.length ? `
- <h3 class="text-h2 rise-in" style="margin-top:8px;margin-bottom:18px;">抖音评论墙 · Top ${Math.min(topCmts.length, 12)}</h3>
- <div class="voice-wall" style="margin-bottom:32px;">
- ${topCmts.slice(0, 12).map((c, i) => `<div class="voice-card ${i < 3 ? 'hot' : ''} fade-up">
- <p class="voice-content">${esc(cleanText(c.text || ''))}</p>
- <div class="voice-meta">
- <span>${esc((c.user?.nickname || '匿名').slice(0, 14))}${c.ip_label ? ' · ' + esc(c.ip_label) : ''}</span>
- <span class="voice-likes">♥ ${fmtNum(c.digg_count || 0)}</span>
- </div>
- </div>`).join('')}
- </div>` : '';
- // ---------- 完整作品表 ----------
- const userPostsTable = userPosts.length ? `
- <h3 class="text-h2 rise-in" style="margin-top:8px;margin-bottom:18px;">品牌号完整作品流 · ${userPosts.length} 条</h3>
- <div class="table-wrap fade-up" style="margin-bottom:32px;">
- <table class="data-tbl">
- <thead><tr><th>#</th><th>内容</th><th>点赞</th><th>评论</th><th>播放</th><th>分享</th><th>收藏</th></tr></thead>
- <tbody>
- ${userPosts.slice().sort((a, b) => (b.statistics?.digg_count || 0) - (a.statistics?.digg_count || 0)).slice(0, 12).map((p, i) => `<tr>
- <td class="tb-num">${String(i + 1).padStart(2, '0')}</td>
- <td style="font-size:0.86rem;max-width:360px;">${esc(cleanText(p.desc || '').slice(0, 72))}</td>
- <td class="tb-num" style="color:var(--amber);">${fmtNum(p.statistics?.digg_count || 0)}</td>
- <td class="tb-num" style="color:var(--green);">${fmtNum(p.statistics?.comment_count || 0)}</td>
- <td class="tb-num">${fmtNum(p.statistics?.play_count || 0)}</td>
- <td class="tb-num">${fmtNum(p.statistics?.share_count || 0)}</td>
- <td class="tb-num">${fmtNum(p.statistics?.collect_count || 0)}</td>
- </tr>`).join('')}
- </tbody>
- </table>
- </div>` : '';
- // ---------- 关键词搜索(若有 cookie)----------
- const searchSection = Object.keys(searches).length ? `
- <h3 class="text-h2 rise-in" style="margin-top:8px;margin-bottom:18px;">品类关键词热度</h3>
- <div class="grid grid-${Math.min(Object.keys(searches).length, 3)}" style="margin-bottom:32px;">
- ${Object.entries(searches).map(([kw, info]) => {
- if (!info || info._error) return `<div class="card fade-up"><div class="text-mono text-xs text-rose">"${esc(kw)}"</div><div class="text-sm" style="margin-top:10px;color:var(--text-3);">⚠ ${esc(String(info?._error || '无数据').slice(0, 60))}</div></div>`;
- if (!info.count) return `<div class="card fade-up"><div class="text-mono text-xs text-amber">KEYWORD "${esc(kw)}"</div><div class="text-sm" style="margin-top:10px;color:var(--text-3);">当前品类曝光低,搜索结果为 0 条 — 说明此品类在抖音尚未形成内容沉淀,是<strong class="text-amber">蓝海机会</strong>。</div></div>`;
- return `<div class="card fade-up">
- <div class="text-mono text-xs text-amber">KEYWORD "${esc(kw)}"</div>
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px;font-size:0.8rem;">
- <div><div style="color:var(--text-3);">视频数</div><div style="font-weight:700;font-size:1.05rem;">${info.count}</div></div>
- <div><div style="color:var(--text-3);">总点赞</div><div style="color:var(--amber);font-weight:700;font-size:1.05rem;">${fmtNum(info.totalDigg)}</div></div>
- </div>
- <div style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
- ${(info.top || []).slice(0, 3).map((t) => `<div class="text-xs" style="color:var(--text-2);padding:6px 8px;background:rgba(255,255,255,0.03);border-radius:4px;line-height:1.4;">
- <span class="text-mono" style="color:var(--amber);margin-right:6px;">${fmtNum(t.digg)}❤</span>${esc((t.desc || '').slice(0, 50))}
- </div>`).join('')}
- </div>
- </div>`;
- }).join('')}
- </div>` : '';
- return `<section class="report-section" id="sdy" data-tone="rose" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow" style="color:#FF3E75;">DY · Douyin Presence</div>
- <h2 class="text-h1"><span class="headline-anim">抖音阵地深度</span></h2>
- <p class="section-sub">${accountCards.length >= 2 ? `多账号矩阵采样:官方号 + 品牌竞品 + 垂类博主 · 主号深度画像如下` : (shareType === 'user' ? '通过品牌旗舰店分享链直抓 · 账号画像 / 作品流 / 评论 / 品类搜索' : '通过品牌视频分享链直抓 · 视频/评论/作者主页/品类搜索')}</p>
- </div>
- ${renderDouyinTrendsBlock({ A })}
- ${accountMatrix}
- ${heroUser}
- ${videoCard}
- ${topPostsSection}
- ${commentWall}
- ${userPostsTable}
- ${searchSection}
- </div>
- </section>`;
- }
- function renderS7Themes({ themes, slideNum, slideTotal }) {
- const renderGroup = (list, cls, title, eyebrow, color) => `
- <div>
- <div class="section-eyebrow fade-up" style="color:${color};">${eyebrow}</div>
- <h3 class="text-h2 fade-up" style="margin-bottom:18px;">${title}</h3>
- <div class="grid grid-3">
- ${list.slice(0, 6).map((t) => `<div class="theme-poster ${cls} fade-up">
- <h4>${esc(t.tag)}</h4>
- <div class="count"><span class="num">${t.count}</span>次提及</div>
- <ul class="examples">
- ${(t.examples || []).slice(0, 2).map((e) => `<li>"${esc(cleanText(e).slice(0, 80))}"</li>`).join('')}
- </ul>
- </div>`).join('\n ')}
- </div>
- </div>`;
- return `<section class="report-section" id="s7" data-tone="blue" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">07 · Themes Cluster</div>
- <h2 class="text-h1"><span class="headline-anim">痛点 · 亮点 · 场景</span></h2>
- <p class="section-sub">从真实语料中提炼出的 VOC 主题 · 标注频次与典型原声</p>
- </div>
- <div style="display:flex;flex-direction:column;gap:56px;">
- ${renderGroup(themes.pain, 'pain', '痛点主题', '07.1 · PAIN POINTS', 'var(--rose)')}
- ${renderGroup(themes.bright, 'bright', '亮点主题', '07.2 · BRIGHT SPOTS', 'var(--green)')}
- ${renderGroup(themes.scenarios, 'scene', '场景主题', '07.3 · USAGE SCENARIOS', 'var(--blue)')}
- </div>
- </div>
- </section>`;
- }
- function renderS8Matrix({ themes, slideNum, slideTotal }) {
- const q1 = themes.pain.filter((t) => ['口感差', '效果不显'].includes(t.tag)).slice(0, 2);
- const q2 = themes.bright.slice(0, 3);
- const q3 = themes.pain.filter((t) => ['信任危机', '价格敏感', '副作用担忧'].includes(t.tag)).slice(0, 2);
- const q4 = themes.scenarios.slice(0, 2);
- return `<section class="report-section" id="s8" data-tone="purple" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">08 · Opportunity Matrix</div>
- <h2 class="text-h1"><span class="headline-anim">品类机会矩阵</span></h2>
- <p class="section-sub">四象限 · 产品可解决性 × 用户痛点强度</p>
- </div>
- <div class="matrix-2x2 fade-up">
- <div class="axis-y">痛点强度 LOW → HIGH</div>
- <div class="matrix-cell q1">
- <div class="cell-label text-amber">QUADRANT I · 产品机会</div>
- <h5>高痛点 · 产品可优化解决</h5>
- <p>${q1.map((t) => `<strong class="text-amber">${esc(t.tag)}</strong>(${t.count})`).join(' / ') || '尚无强信号'}。这些痛点可通过产品配方、剂型、包装直接优化 — 直接迭代 ROI 最高。</p>
- </div>
- <div class="matrix-cell q2">
- <div class="cell-label text-green">QUADRANT II · 强化既有</div>
- <h5>低痛点 · 产品可强化放大</h5>
- <p>${q2.map((t) => `<strong class="text-green">${esc(t.tag)}</strong>(${t.count})`).join(' / ') || '尚无强信号'}。这些是消费者已认同的优势,内容营销直接放大。</p>
- </div>
- <div class="matrix-cell q3">
- <div class="cell-label text-rose">QUADRANT III · 教育必投</div>
- <h5>高痛点 · 非产品端</h5>
- <p>${q3.map((t) => `<strong class="text-rose">${esc(t.tag)}</strong>(${t.count})`).join(' / ') || '尚无强信号'}。这些是品类心智问题,需要 PR / 教育内容 / 品牌信任建设长期解决。</p>
- </div>
- <div class="matrix-cell q4">
- <div class="cell-label text-purple">QUADRANT IV · 场景机会</div>
- <h5>潜在使用场景 · 可扩展</h5>
- <p>${q4.map((t) => `<strong class="text-purple">${esc(t.tag)}</strong>(${t.count})`).join(' / ') || '尚无强信号'}。这些场景是新增长入口,可通过 campaign / 联名 / KOC 铺量打开。</p>
- </div>
- <div class="axis-x">产品可解决性 LOW → HIGH</div>
- </div>
- </div>
- </section>`;
- }
- function renderS9Action({ A, themes, slideNum, slideTotal }) {
- const topPain = themes.pain[0]?.tag || '核心痛点';
- const topScene = themes.scenarios[0]?.tag || '场景';
- const topKol = A.kols[0];
- return `<section class="report-section" id="s9" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow">09 · Action Plan</div>
- <h2 class="text-h1"><span class="headline-anim">30 天行动建议</span></h2>
- <p class="section-sub">产品 / 内容 / 渠道 三路并进,从 VOC 到落地。</p>
- </div>
- <div class="grid grid-3" style="margin-bottom:40px;">
- <div class="card card-accent fade-up">
- <div class="text-mono text-xs text-amber">TRACK A · PRODUCT</div>
- <h3 class="text-h2" style="margin-top:10px;">产品端</h3>
- <ul style="margin-top:16px;list-style:none;display:flex;flex-direction:column;gap:10px;color:var(--text-2);font-size:0.9rem;">
- <li>✓ 针对「${esc(topPain)}」推出改良版 SKU / 新剂型</li>
- <li>✓ 包装文案直接借用高赞评论原文</li>
- <li>✓ 上市前用同样 VOC 链路做 A/B 预测试</li>
- </ul>
- </div>
- <div class="card card-accent fade-up" style="border-left-color:var(--green);">
- <div class="text-mono text-xs text-green">TRACK B · CONTENT</div>
- <h3 class="text-h2" style="margin-top:10px;">内容端</h3>
- <ul style="margin-top:16px;list-style:none;display:flex;flex-direction:column;gap:10px;color:var(--text-2);font-size:0.9rem;">
- <li>✓ 切入「${esc(topScene)}」场景做系列种草</li>
- <li>✓ 复用 Top 爆款笔记标题公式批量产出</li>
- <li>✓ 用高赞评论作为视频字幕 / 金句 BGM</li>
- </ul>
- </div>
- <div class="card card-accent fade-up" style="border-left-color:var(--purple);">
- <div class="text-mono text-xs text-purple">TRACK C · CHANNEL</div>
- <h3 class="text-h2" style="margin-top:10px;">渠道端</h3>
- <ul style="margin-top:16px;list-style:none;display:flex;flex-direction:column;gap:10px;color:var(--text-2);font-size:0.9rem;">
- <li>✓ 头部 ${A.kolTier.head.length} 位优先合作${topKol ? `(如 ${esc(topKol.nickname)})` : ''}</li>
- <li>✓ 腰部 ${A.kolTier.mid.length} 位批量铺垫品类心智</li>
- <li>✓ KOC ${A.kolTier.tail.length} 位素材铺量支撑投流</li>
- </ul>
- </div>
- </div>
- <h3 class="text-h2 fade-up" style="margin-bottom:18px;">30 天时间线</h3>
- <div class="tl fade-up">
- <div class="tl-item">
- <div class="tl-time">DAY 01 — 07 · 对齐</div>
- <h5>对齐核心 VOC 发现 + 快速小样测试</h5>
- <p>把本报告的 Top 3 洞察 / Top 痛点 / Top 爆款笔记,在内部做 1 次品牌 / 产品 / 运营 / 投放四方对齐会。选 1-2 个高赞评论金句,做小样社交投流测试。</p>
- </div>
- <div class="tl-item">
- <div class="tl-time">DAY 08 — 14 · 内容</div>
- <h5>复用爆款结构,启动 KOC 铺量</h5>
- <p>以「${esc(topScene)}」为切入点,量产 20-30 条 KOC 笔记;每条笔记用不同的高赞评论做卡点。配合 ${A.kolTier.mid.length} 位腰部达人同步发种草。</p>
- </div>
- <div class="tl-item">
- <div class="tl-time">DAY 15 — 21 · 放大</div>
- <h5>基于数据优选,头部合作上场</h5>
- <p>根据 Day 08-14 的测试数据,锁定 2-3 个最强素材方向,启动头部 KOL 合作。同步通过 Amazon 国际对标数据测试是否跨境放量。</p>
- </div>
- <div class="tl-item">
- <div class="tl-time">DAY 22 — 30 · 复盘</div>
- <h5>收集新一轮 VOC,迭代下次投放</h5>
- <p>重新跑 VOC 采集脚本,对比 30 天前后的痛点词频变化。把"新涌现的痛点"作为下一迭代的产品优化 input,形成闭环。</p>
- </div>
- </div>
- </div>
- </section>`;
- }
- function renderFooter() {
- return `<footer class="report-footer">
- <div>Generated by <strong class="text-amber">BRAIN·HACK / VOC</strong> · Powered by TikHub + Sorftime + OpenClaw Skills</div>
- <div class="meta">Data sources: Xiaohongshu · Amazon (via Sorftime) · Douyin (via TikHub)</div>
- </footer>`;
- }
- // ============================================================
- // TikTok · Amazon 的内容端对标(独立 slide)
- // ============================================================
- function renderSTikTok({ A, slideNum, slideTotal }) {
- const tt = A.tiktok;
- if (!tt || tt.empty) return '';
- const m = tt.metrics || {};
- const topVids = tt.topVideos || [];
- const topAuthors = tt.topAuthors || [];
- const topCmts = tt.topComments || [];
- const topTags = tt.topHashtags || [];
- const topRegions = tt.topRegions || [];
- const byKw = tt.byKeywordStats || {};
- const keywords = tt.keywords_used || Object.keys(byKw);
- const topCatUsers = tt.topCategoryUsers || [];
- const TT_CYAN = '#25F4EE';
- const TT_PINK = '#FE2C55';
- // --- Hero metrics 四大数字 + 认证率 + 触达粉丝 ---
- const heroMetrics = `
- <div class="grid grid-4" style="margin-bottom:28px;gap:16px;">
- <div class="card card-glow fade-up" style="border-left:3px solid ${TT_CYAN};">
- <div class="text-mono text-xs" style="color:${TT_CYAN};letter-spacing:.12em;">VIDEOS</div>
- <div style="font-family:'JetBrains Mono';font-size:2.4rem;font-weight:900;color:${TT_CYAN};margin-top:8px;">${fmtNum(m.totalVideos)}</div>
- <div class="text-xs" style="color:var(--text-3);margin-top:4px;">跨 ${keywords.length} 个英文关键词</div>
- </div>
- <div class="card fade-up" style="border-left:3px solid var(--amber);">
- <div class="text-mono text-xs text-amber" style="letter-spacing:.12em;">TOTAL PLAYS</div>
- <div style="font-family:'JetBrains Mono';font-size:2.4rem;font-weight:900;color:var(--amber);margin-top:8px;">${fmtNum(m.totalPlay)}</div>
- <div class="text-xs" style="color:var(--text-3);margin-top:4px;">平均 ER ${(m.avgEngagementRate * 100).toFixed(1)}%</div>
- </div>
- <div class="card fade-up" style="border-left:3px solid ${TT_PINK};">
- <div class="text-mono text-xs" style="color:${TT_PINK};letter-spacing:.12em;">TOTAL LIKES</div>
- <div style="font-family:'JetBrains Mono';font-size:2.4rem;font-weight:900;color:${TT_PINK};margin-top:8px;">${fmtNum(m.totalDigg)}</div>
- <div class="text-xs" style="color:var(--text-3);margin-top:4px;">评论 ${fmtNum(m.totalComment)} · 分享 ${fmtNum(m.totalShare)}</div>
- </div>
- <div class="card fade-up" style="border-left:3px solid var(--green);">
- <div class="text-mono text-xs text-green" style="letter-spacing:.12em;">CREATORS</div>
- <div style="font-family:'JetBrains Mono';font-size:2.4rem;font-weight:900;color:var(--green);margin-top:8px;">${m.totalAuthors}</div>
- <div class="text-xs" style="color:var(--text-3);margin-top:4px;">认证 ${m.verifiedAuthors} · 触达粉丝 ${fmtNum(m.totalFollowersReached)}</div>
- </div>
- </div>`;
- // --- 关键词覆盖 bar row ---
- const maxKwDigg = Math.max(1, ...Object.values(byKw).map((v) => v.totalDigg || 0));
- const keywordCoverage = Object.keys(byKw).length ? `
- <div class="card fade-up" style="margin-bottom:28px;border-left:3px solid ${TT_CYAN};">
- <div class="text-mono text-xs" style="color:${TT_CYAN};letter-spacing:.12em;">KEYWORD COVERAGE</div>
- <h3 class="text-h3" style="margin-top:8px;margin-bottom:14px;">英文关键词覆盖深度 · TikTok 搜索反馈</h3>
- <div style="display:flex;flex-direction:column;gap:10px;">
- ${Object.entries(byKw).map(([kw, s]) => {
- const pct = Math.round((s.totalDigg / maxKwDigg) * 100);
- return `<div style="display:grid;grid-template-columns:220px 1fr auto;gap:14px;align-items:center;">
- <div class="text-sm" style="font-family:'JetBrains Mono';color:var(--text-2);">${esc(kw)}</div>
- <div style="background:rgba(255,255,255,.06);border-radius:4px;height:14px;position:relative;overflow:hidden;">
- <div style="width:${pct}%;height:100%;background:linear-gradient(90deg, ${TT_CYAN}, ${TT_PINK});border-radius:4px;transition:width .8s;"></div>
- </div>
- <div class="text-xs text-mono" style="color:var(--text-2);min-width:160px;text-align:right;">${s.videos} 视频 · <strong style="color:${TT_CYAN};">${fmtNum(s.totalDigg)}</strong> 赞 · <strong>${fmtNum(s.totalPlay)}</strong> 播</div>
- </div>`;
- }).join('')}
- </div>
- </div>` : '';
- // --- Top 作者卡 ---
- const authorsBlock = topAuthors.length ? `
- <h3 class="text-h2 rise-in" style="margin-top:12px;margin-bottom:18px;">Top ${topAuthors.length} 海外创作者矩阵 <span class="text-xs" style="color:var(--text-3);font-weight:400;margin-left:10px;">按粉丝排序</span></h3>
- <div class="grid grid-${Math.min(topAuthors.length, 3)}" style="margin-bottom:32px;gap:18px;">
- ${topAuthors.map((a) => `<div class="card fade-up" style="border-left:3px solid ${TT_CYAN};">
- <div style="display:flex;gap:12px;align-items:center;margin-bottom:12px;">
- ${a.avatar ? `<img src="${esc(a.avatar)}" alt="" loading="lazy" onerror="this.style.display='none'" style="width:56px;height:56px;border-radius:50%;border:1.5px solid ${TT_CYAN};flex-shrink:0;object-fit:cover;">` : `<div style="width:56px;height:56px;border-radius:50%;background:var(--bg-3);flex-shrink:0;"></div>`}
- <div style="flex:1;min-width:0;">
- <div class="text-h3" style="font-size:.95rem;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc((a.nickname || '—').slice(0, 20))}</div>
- <div class="text-xs text-mono" style="color:${TT_CYAN};margin-top:3px;">@${esc(a.unique_id || '')}${a.region ? ' · ' + esc(a.region) : ''}</div>
- </div>
- ${a.custom_verify ? `<span class="tag" style="background:rgba(37,244,238,.15);color:${TT_CYAN};border-color:rgba(37,244,238,.3);font-size:.65rem;flex-shrink:0;">✓ ${esc(String(a.custom_verify).slice(0, 8))}</span>` : ''}
- </div>
- <div class="text-xs" style="color:var(--text-3);line-height:1.5;margin-bottom:12px;min-height:2.2em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${esc(a.signature || '(no bio)')}</div>
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:10px;">
- <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
- <div style="color:${TT_CYAN};font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(a.follower_count)}</div>
- <div style="color:var(--text-3);font-size:.62rem;margin-top:2px;letter-spacing:.06em;">FOLLOWERS</div>
- </div>
- <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
- <div style="font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(a.aweme_count)}</div>
- <div style="color:var(--text-3);font-size:.62rem;margin-top:2px;letter-spacing:.06em;">VIDEOS</div>
- </div>
- <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
- <div style="color:var(--amber);font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(a.total_favorited)}</div>
- <div style="color:var(--text-3);font-size:.62rem;margin-top:2px;letter-spacing:.06em;">LIKES</div>
- </div>
- </div>
- ${a.best_post ? `<div style="padding:10px;background:rgba(255,255,255,.02);border-radius:6px;border-left:2px solid ${TT_CYAN};">
- <div class="text-xs" style="color:var(--text-3);letter-spacing:.06em;font-family:'JetBrains Mono';margin-bottom:6px;">BEST POST</div>
- <div class="text-xs" style="line-height:1.5;min-height:2.2em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">${esc(cleanText(a.best_post.desc || '').slice(0, 90))}</div>
- <div class="text-xs text-mono" style="margin-top:6px;color:var(--text-3);display:flex;gap:10px;">
- <span style="color:${TT_PINK};">♥ ${fmtNum(a.best_post.digg)}</span>
- <span>▶ ${fmtNum(a.best_post.play)}</span>
- </div>
- </div>` : `<div class="text-xs" style="color:var(--text-3);padding:10px;background:rgba(255,255,255,.02);border-radius:6px;">暂无 Best Post 数据</div>`}
- </div>`).join('')}
- </div>` : '';
- // --- Top Videos 网格 ---
- const topVideosBlock = topVids.length ? `
- <h3 class="text-h2 rise-in" style="margin-top:12px;margin-bottom:18px;">爆款视频 Top ${Math.min(topVids.length, 9)} <span class="text-xs" style="color:var(--text-3);font-weight:400;margin-left:10px;">按点赞排序 · 品类流量锚点</span></h3>
- <div class="grid grid-3" style="margin-bottom:32px;gap:16px;">
- ${topVids.slice(0, 9).map((v, i) => `<div class="card fade-up" style="border-left:3px solid ${i < 3 ? TT_PINK : TT_CYAN};">
- <div style="display:flex;gap:10px;margin-bottom:10px;">
- ${v.cover ? `<img src="${esc(v.cover)}" alt="" loading="lazy" onerror="this.style.display='none'" style="width:64px;height:84px;border-radius:6px;object-fit:cover;flex-shrink:0;">` : `<div style="width:64px;height:84px;background:var(--bg-3);border-radius:6px;flex-shrink:0;"></div>`}
- <div style="flex:1;min-width:0;">
- <div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;">
- <span class="text-mono text-xs" style="color:${i < 3 ? TT_PINK : TT_CYAN};font-weight:800;">#${String(i + 1).padStart(2, '0')}</span>
- ${v._from_keyword ? `<span class="text-xs" style="color:var(--text-3);opacity:.7;">· ${esc(v._from_keyword)}</span>` : ''}
- </div>
- <div class="text-xs" style="line-height:1.5;color:var(--text-2);min-height:3em;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;">${esc(cleanText(v.desc || '').slice(0, 140))}</div>
- </div>
- </div>
- <div class="text-xs text-mono" style="color:var(--text-3);border-top:1px solid rgba(255,255,255,.05);padding-top:8px;display:flex;gap:10px;flex-wrap:wrap;">
- <span style="color:${TT_PINK};">♥ ${fmtNum(v.digg)}</span>
- <span style="color:var(--green);">💬 ${fmtNum(v.comment)}</span>
- <span>▶ ${fmtNum(v.play)}</span>
- <span style="color:var(--text-3);">↗ ${fmtNum(v.share)}</span>
- </div>
- ${v.author ? `<div class="text-xs" style="color:var(--text-3);margin-top:6px;display:flex;align-items:center;gap:6px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;">
- <span style="color:${TT_CYAN};">@${esc(v.author.unique_id || v.author.nickname || '')}</span>
- <span>· ${fmtNum(v.author.follower_count || 0)}粉</span>
- ${v.author.custom_verify ? `<span style="color:var(--amber);">✓</span>` : ''}
- </div>` : ''}
- ${(v.hashtags || []).length ? `<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px;">
- ${(v.hashtags || []).slice(0, 4).map((h) => `<span class="text-xs" style="color:${TT_CYAN};opacity:.8;">#${esc(h)}</span>`).join('')}
- </div>` : ''}
- </div>`).join('')}
- </div>` : '';
- // --- 评论墙 ---
- const commentBlock = topCmts.length ? `
- <h3 class="text-h2 rise-in" style="margin-top:12px;margin-bottom:18px;">📣 海外用户原声 Top ${Math.min(topCmts.length, 12)}</h3>
- <div class="voice-wall" style="margin-bottom:32px;">
- ${topCmts.slice(0, 12).map((c, i) => `<div class="voice-card ${i < 3 ? 'hot' : ''} fade-up">
- <p class="voice-content">${esc(cleanText(c.text || ''))}</p>
- <div class="voice-meta">
- <span>${esc((c.user?.nickname || c.user?.unique_id || 'user').slice(0, 20))}${c.user?.region ? ' · ' + esc(c.user.region) : ''}</span>
- <span class="voice-likes">♥ ${fmtNum(c.digg_count || 0)}</span>
- </div>
- </div>`).join('')}
- </div>` : '';
- // --- Hashtag 词云(Top List) ---
- const hashtagBlock = topTags.length ? `
- <div class="grid grid-2" style="margin-bottom:32px;gap:24px;">
- <div class="card fade-up" style="border-left:3px solid ${TT_CYAN};">
- <div class="text-mono text-xs" style="color:${TT_CYAN};letter-spacing:.12em;">HASHTAG FREQUENCY</div>
- <h3 class="text-h3" style="margin-top:8px;margin-bottom:14px;">Top ${Math.min(topTags.length, 16)} 热门标签</h3>
- <div style="display:flex;flex-wrap:wrap;gap:8px;">
- ${topTags.slice(0, 16).map((t, i) => {
- const size = i < 3 ? '1.1rem' : i < 8 ? '.95rem' : '.85rem';
- const opacity = i < 3 ? 1 : 0.85;
- const color = i < 3 ? TT_PINK : TT_CYAN;
- return `<span style="background:rgba(37,244,238,.08);border:1px solid rgba(37,244,238,.25);color:${color};padding:6px 12px;border-radius:20px;font-size:${size};font-family:'JetBrains Mono';font-weight:${i < 3 ? 800 : 600};opacity:${opacity};">
- #${esc(t.tag)} <span style="color:var(--text-3);font-size:.7rem;margin-left:4px;">${t.count}</span>
- </span>`;
- }).join('')}
- </div>
- </div>
- <div class="card fade-up" style="border-left:3px solid var(--amber);">
- <div class="text-mono text-xs text-amber" style="letter-spacing:.12em;">REGION MIX</div>
- <h3 class="text-h3" style="margin-top:8px;margin-bottom:14px;">创作者地区分布</h3>
- ${topRegions.length ? `<div style="display:flex;flex-direction:column;gap:8px;">
- ${topRegions.map(([r, c]) => {
- const pct = Math.round((c / (m.totalVideos || 1)) * 100);
- return `<div style="display:grid;grid-template-columns:90px 1fr auto;gap:10px;align-items:center;">
- <div class="text-xs text-mono" style="color:var(--text-2);">${esc(r)}</div>
- <div style="background:rgba(255,255,255,.06);border-radius:4px;height:10px;overflow:hidden;">
- <div style="width:${pct}%;height:100%;background:${TT_CYAN};border-radius:4px;"></div>
- </div>
- <div class="text-xs text-mono" style="color:var(--text-3);min-width:60px;text-align:right;">${c} · ${pct}%</div>
- </div>`;
- }).join('')}
- </div>` : `<p class="text-sm" style="color:var(--text-3);">无地区数据</p>`}
- </div>
- </div>` : '';
- // --- 品类头部账号(从 user search 获取) ---
- const catUserBlock = topCatUsers.length ? `
- <h3 class="text-h2 rise-in" style="margin-top:12px;margin-bottom:18px;">🏷 品类头部账号 <span class="text-xs" style="color:var(--text-3);font-weight:400;margin-left:10px;">来自 TikTok 用户搜索 · 可作为 B 侧合作 / 学习对象</span></h3>
- <div class="grid grid-3" style="margin-bottom:32px;gap:12px;">
- ${topCatUsers.map((u) => `<div class="card fade-up" style="padding:14px;">
- <div style="display:flex;gap:10px;align-items:center;">
- ${u.avatar ? `<img src="${esc(u.avatar)}" alt="" loading="lazy" onerror="this.style.display='none'" style="width:40px;height:40px;border-radius:50%;object-fit:cover;flex-shrink:0;">` : `<div style="width:40px;height:40px;border-radius:50%;background:var(--bg-3);flex-shrink:0;"></div>`}
- <div style="flex:1;min-width:0;">
- <div class="text-sm" style="font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc((u.nickname || '').slice(0, 18))}${u.custom_verify ? ' <span style="color:var(--amber);">✓</span>' : ''}</div>
- <div class="text-xs text-mono" style="color:${TT_CYAN};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">@${esc(u.unique_id || '')}</div>
- </div>
- <div style="text-align:right;flex-shrink:0;">
- <div class="text-xs text-mono" style="color:${TT_CYAN};font-weight:800;">${fmtNum(u.follower_count || 0)}</div>
- <div class="text-xs" style="color:var(--text-3);font-size:.65rem;">粉丝</div>
- </div>
- </div>
- </div>`).join('')}
- </div>` : '';
- return `<section class="report-section" id="sttk" data-tone="cyan" data-slide-num="${slideNum}/${slideTotal}">
- <div class="section-inner">
- <div class="section-head rise-in">
- <div class="section-eyebrow" style="color:${TT_CYAN};">TT · TikTok Presence</div>
- <h2 class="text-h1"><span class="headline-anim">TikTok 海外阵地</span></h2>
- <p class="section-sub">Amazon 的内容端对标 · <strong style="color:${TT_CYAN};">${keywords.map((k) => esc(k)).join(' / ')}</strong> · 不需 cookie 直通海外版</p>
- </div>
- ${heroMetrics}
- ${keywordCoverage}
- ${authorsBlock}
- ${topVideosBlock}
- ${commentBlock}
- ${hashtagBlock}
- ${catUserBlock}
- ${renderTTAdsBlock({ A })}
- </div>
- </section>`;
- }
- // ============================================================
- // Market Intel · TikTok Ads 官方情报 block(注入 TikTok slide 尾部)
- // ============================================================
- function renderTTAdsBlock({ A }) {
- const mi = A.marketIntel;
- if (!mi || mi.empty || !mi.ttAds?.hasData) return '';
- const { insights, relatedKeywords, relatedFromKeyword } = mi.ttAds;
- const TT_CYAN = '#25F4EE';
- const TT_PINK = '#FE2C55';
- // Insight 卡:每个 keyword 1 张
- const insightsCards = insights.length ? `
- <div class="grid grid-${Math.min(insights.length, 3)}" style="margin-bottom:24px;gap:18px;">
- ${insights.map((ins, i) => {
- const postChange = ins.post_change || 0;
- const trendColor = postChange > 50 ? '#22c55e' : (postChange > 0 ? TT_CYAN : '#f97316');
- const trendArrow = postChange > 0 ? '▲' : (postChange < 0 ? '▼' : '●');
- return `<div class="card card-glow fade-up" style="border-left:3px solid ${TT_PINK};">
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
- <div class="text-mono text-xs" style="color:${TT_PINK};letter-spacing:.12em;">AD KEYWORD #${i + 1}</div>
- <span class="tag" style="background:rgba(254,44,85,.15);color:${TT_PINK};border-color:rgba(254,44,85,.3);font-size:.7rem;">TikTok Creative Center</span>
- </div>
- <h3 class="text-h3" style="margin:0 0 14px;font-family:'JetBrains Mono';font-size:1.1rem;color:${TT_CYAN};">"${esc(ins.keyword)}"</h3>
- <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:14px;">
- <div style="padding:10px;background:rgba(255,255,255,.03);border-radius:6px;">
- <div class="text-xs text-mono" style="color:var(--text-3);letter-spacing:.06em;">IMPRESSIONS</div>
- <div style="font-family:'JetBrains Mono';font-size:1.4rem;font-weight:900;color:${TT_CYAN};margin-top:4px;">${fmtNum(ins.impression)}</div>
- </div>
- <div style="padding:10px;background:rgba(255,255,255,.03);border-radius:6px;">
- <div class="text-xs text-mono" style="color:var(--text-3);letter-spacing:.06em;">POST CHANGE</div>
- <div style="font-family:'JetBrains Mono';font-size:1.4rem;font-weight:900;color:${trendColor};margin-top:4px;">${trendArrow} ${(postChange || 0).toFixed(1)}%</div>
- </div>
- <div style="padding:10px;background:rgba(255,255,255,.03);border-radius:6px;">
- <div class="text-xs text-mono" style="color:var(--text-3);letter-spacing:.06em;">CTR</div>
- <div style="font-family:'JetBrains Mono';font-size:1.15rem;font-weight:800;color:var(--amber);margin-top:4px;">${(ins.ctr || 0).toFixed(2)}%</div>
- </div>
- <div style="padding:10px;background:rgba(255,255,255,.03);border-radius:6px;">
- <div class="text-xs text-mono" style="color:var(--text-3);letter-spacing:.06em;">CVR</div>
- <div style="font-family:'JetBrains Mono';font-size:1.15rem;font-weight:800;color:var(--amber);margin-top:4px;">${(ins.cvr || 0).toFixed(2)}%</div>
- </div>
- </div>
- <div class="text-xs text-mono" style="color:var(--text-3);display:flex;gap:12px;flex-wrap:wrap;padding-top:10px;border-top:1px solid rgba(255,255,255,.06);">
- <span>📝 ${fmtNum(ins.post)} posts</span>
- <span style="color:${TT_PINK};">♥ ${fmtNum(ins.like)}</span>
- <span>💬 ${fmtNum(ins.comment)}</span>
- <span>↗ ${fmtNum(ins.share)}</span>
- <span>💰 ${fmtNum(ins.cost)}</span>
- </div>
- </div>`;
- }).join('')}
- </div>` : '';
- // Related keywords 词云:按 score 渐变字号
- const relatedBlock = relatedKeywords.length ? `
- <div class="card fade-up" style="margin-bottom:24px;border-left:3px solid ${TT_CYAN};">
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
- <div class="text-mono text-xs" style="color:${TT_CYAN};letter-spacing:.12em;">RELATED KEYWORD NETWORK</div>
- <span class="text-xs" style="color:var(--text-3);">Top ${relatedKeywords.length} · 来源: "${esc(relatedFromKeyword)}"</span>
- </div>
- <h3 class="text-h3" style="margin:0 0 14px;">TikTok Ads 官方相关词图谱</h3>
- <div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
- ${relatedKeywords.map((r, i) => {
- const pct = (r.score || 0) / 100;
- const fontSize = (0.8 + pct * 0.6).toFixed(2); // 0.8~1.4rem
- const color = i < 3 ? TT_PINK : (i < 10 ? TT_CYAN : 'var(--text-2)');
- const opacity = (0.6 + pct * 0.4).toFixed(2);
- return `<span style="background:rgba(37,244,238,.07);border:1px solid rgba(37,244,238,.2);color:${color};padding:5px 12px;border-radius:20px;font-size:${fontSize}rem;font-family:'JetBrains Mono';font-weight:${i < 5 ? 800 : 600};opacity:${opacity};">
- ${esc(r.name)} <span style="color:var(--text-3);font-size:.65rem;margin-left:4px;">${r.score}</span>
- </span>`;
- }).join('')}
- </div>
- </div>` : '';
- return `
- <div style="margin-top:32px;padding-top:24px;border-top:1px solid rgba(37,244,238,.15);">
- <div class="section-eyebrow rise-in" style="color:${TT_PINK};">TT · OFFICIAL MARKET INTEL</div>
- <h3 class="text-h2 rise-in" style="margin:6px 0 20px;">TikTok Creative Center 官方情报 <span class="text-xs" style="color:var(--text-3);font-weight:400;margin-left:10px;">第一方数据 · 广告主视角的市场真相</span></h3>
- ${insightsCards}
- ${relatedBlock}
- </div>`;
- }
- // ============================================================
- // Market Intel · Douyin 全站热榜基线 block(注入 Douyin slide 顶部)
- // ============================================================
- function renderDouyinTrendsBlock({ A }) {
- const mi = A.marketIntel;
- if (!mi || mi.empty || !mi.douyinGlobal?.hasGlobal) return '';
- const { hotWords, webHotSearch, hotTopics, categoryHitTotal, filterKeywords } = mi.douyinGlobal;
- const DY_PINK = '#FF3E75';
- const hotWordsBlock = hotWords.length ? `
- <div class="card fade-up" style="border-left:3px solid ${DY_PINK};">
- <div class="text-mono text-xs" style="color:${DY_PINK};letter-spacing:.12em;">HOT WORD BILLBOARD</div>
- <div class="text-xs" style="color:var(--text-3);margin-top:4px;">抖音星图 · 近 24h 热词 Top 6</div>
- <div style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
- ${hotWords.map((w, i) => `<div style="display:grid;grid-template-columns:24px 1fr auto;gap:10px;align-items:center;padding:6px 0;border-bottom:1px dashed rgba(255,255,255,.05);">
- <span class="text-mono" style="color:${i < 3 ? DY_PINK : 'var(--text-3)'};font-weight:800;font-size:.8rem;">#${String(i + 1).padStart(2, '0')}</span>
- <span class="text-sm" style="color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(w.title)}</span>
- <span class="text-xs text-mono" style="color:${DY_PINK};font-weight:700;">${fmtNum(w.score)}</span>
- </div>`).join('')}
- </div>
- </div>` : '';
- const webHotBlock = webHotSearch.length ? `
- <div class="card fade-up" style="border-left:3px solid var(--amber);">
- <div class="text-mono text-xs text-amber" style="letter-spacing:.12em;">WEB HOT SEARCH</div>
- <div class="text-xs" style="color:var(--text-3);margin-top:4px;">抖音 Web 首页热搜 Top 6</div>
- <div style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
- ${webHotSearch.map((w, i) => `<div style="display:grid;grid-template-columns:24px 1fr auto;gap:10px;align-items:center;padding:6px 0;border-bottom:1px dashed rgba(255,255,255,.05);">
- <span class="text-mono" style="color:${i < 3 ? 'var(--amber)' : 'var(--text-3)'};font-weight:800;font-size:.8rem;">#${String(i + 1).padStart(2, '0')}</span>
- <span class="text-sm" style="color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(w.word)}</span>
- <span class="text-xs text-mono" style="color:var(--amber);font-weight:700;">${fmtNum(w.view_count)}</span>
- </div>`).join('')}
- </div>
- </div>` : '';
- const hotTopicsBlock = hotTopics.length ? `
- <div class="card fade-up" style="border-left:3px solid var(--green);">
- <div class="text-mono text-xs text-green" style="letter-spacing:.12em;">HOT TOPIC CHALLENGE</div>
- <div class="text-xs" style="color:var(--text-3);margin-top:4px;">抖音热挑战 Top 5</div>
- <div style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
- ${hotTopics.map((t, i) => `<div style="padding:8px;background:rgba(255,255,255,.02);border-radius:6px;">
- <div style="display:flex;justify-content:space-between;gap:8px;align-items:center;">
- <span class="text-sm" style="color:var(--text-2);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">#${esc(t.challenge_name)}</span>
- <span class="text-mono" style="color:var(--green);font-weight:800;font-size:.75rem;white-space:nowrap;">▶ ${fmtNum(t.play_cnt)}</span>
- </div>
- <div class="text-xs text-mono" style="color:var(--text-3);margin-top:3px;">📝 ${fmtNum(t.publish_cnt)} 篇参与</div>
- </div>`).join('')}
- </div>
- </div>` : '';
- const categoryNote = filterKeywords.length ? `
- <div class="text-xs" style="color:var(--text-3);margin-top:12px;padding:10px;background:rgba(255,62,117,.04);border-left:2px solid ${DY_PINK};border-radius:4px;line-height:1.6;">
- <strong style="color:${DY_PINK};">品类过滤命中:</strong> 用关键词 <span class="text-mono">[${filterKeywords.slice(0, 6).map((k) => esc(k)).join(' / ')}]</span> 过滤以上 5 份站级热榜,共命中 <strong style="color:${categoryHitTotal ? 'var(--amber)' : DY_PINK};">${categoryHitTotal}</strong> 条相关内容 —
- ${categoryHitTotal === 0 ? '说明本品类在抖音全站 Top 榜未形成内容沉淀,是<strong class="text-amber">品牌蓝海</strong>:现有头部创作者尚未进入健康/滋补赛道。' : '说明本品类已有热点渗透,可顺势挂载热话题做内容承接。'}
- </div>` : '';
- return `
- <div style="margin-bottom:28px;padding:20px;background:linear-gradient(135deg,rgba(255,62,117,.04),rgba(255,255,255,.01));border-radius:12px;border:1px solid rgba(255,62,117,.12);">
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
- <div>
- <div class="section-eyebrow" style="color:${DY_PINK};">DOUYIN · CONTENT ECOSYSTEM BASELINE</div>
- <h3 class="text-h2" style="margin:4px 0 0;">抖音站级热榜信号 <span class="text-xs" style="color:var(--text-3);font-weight:400;margin-left:10px;">中国内容生态基线 · 决定品牌内容投放语境</span></h3>
- </div>
- <span class="tag" style="background:rgba(255,62,117,.15);color:${DY_PINK};border-color:rgba(255,62,117,.3);font-size:.7rem;">Douyin Billboard API</span>
- </div>
- <div class="grid grid-3" style="gap:14px;">
- ${hotWordsBlock}
- ${webHotBlock}
- ${hotTopicsBlock}
- </div>
- ${categoryNote}
- </div>`;
- }
- function buildHTML({ product, category, collectedAt, A, themes }) {
- const amz = A.amazon || {};
- const hasDouyin = A.douyin && !A.douyin.empty;
- const hasTikTok = A.tiktok && !A.tiktok.empty;
- // 鲜活的星级分布:把每款产品的 5 星分布转成 ECharts 友好结构
- const starDistProducts = Object.values(amz.reviewsSummary || {}).map((r) => ({
- asin: r.asin,
- brand: r.brand || 'N/A',
- title: (r.title || '').slice(0, 28),
- stars: [5, 4, 3, 2, 1].map((s) => r.starDist[s] || 0),
- }));
- // 抖音作品散点
- const douyinPosts = hasDouyin ? (A.douyin.topPosts || []).concat(
- (A.douyin.userPosts || []).slice(0, 20).map((v) => ({
- aweme_id: v.aweme_id, desc: v.desc,
- digg: v.statistics?.digg_count || 0,
- comment: v.statistics?.comment_count || 0,
- share: v.statistics?.share_count || 0,
- play: v.statistics?.play_count || 0,
- }))
- ).filter((v, i, arr) => arr.findIndex((x) => x.aweme_id === v.aweme_id) === i).slice(0, 30) : [];
- const chartData = {
- priceBuckets: amz.priceBuckets || A.priceBuckets || {},
- topBrands: (A.topBrands || []).slice(0, 8),
- topBrandShares: (amz.topBrands || []).slice(0, 8).map((b) => ({
- name: b.brand, value: +b.share.toFixed(1), sales: b.sales, count: b.count,
- })),
- ratingBuckets: amz.ratingBuckets || {},
- reviewCountBuckets: amz.reviewCountBuckets || {},
- starDistProducts,
- topIPs: A.topIPs || [],
- noteHeatmap: (A.topNotes || []).slice(0, 20).map((n) => ({
- title: cleanText(n.title || '').slice(0, 22),
- liked: n.liked_count || 0,
- comments: n.comments_count || 0,
- collected: n.collected_count || 0,
- })),
- douyinPosts,
- };
- // Slide 编号(基础 10 张 + TikTok/抖音 各占一张)
- const slideTotal = 10 + (hasTikTok ? 1 : 0) + (hasDouyin ? 1 : 0);
- const sn = (i) => String(i).padStart(2, '0');
- let n = 1;
- const s0 = sn(n++), s1 = sn(n++), s2 = sn(n++), s3 = sn(n++), s4 = sn(n++);
- const s5 = sn(n++), s6 = sn(n++);
- const sttk = hasTikTok ? sn(n++) : '';
- const sdy = hasDouyin ? sn(n++) : '';
- const s7 = sn(n++), s8 = sn(n++), s9 = sn(n++);
- const ST = String(slideTotal).padStart(2, '0');
- return `<!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width,initial-scale=1.0">
- <title>${esc(product)} · VOC 洞察报告</title>
- <script>document.documentElement.classList.add('js');</script>
- <!-- 本地离线字体(同目录 assets/fonts/) · 双击 HTML 即可用,无需网络 -->
- <link href="./assets/fonts/fonts.css" rel="stylesheet">
- <!-- 在线 fallback:仅当本地字体缺失时异步加载(不阻塞首屏) -->
- <link href="https://fonts.loli.net/css2?family=Inter:wght@400;600;700;800;900&family=Noto+Sans+SC:wght@400;500;700;900&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
- <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
- ${STYLES}
- </head>
- <body>
- ${renderNav(product, hasDouyin, hasTikTok)}
- <div class="slide-counter" id="slideCounter">
- <span class="now">${s0}</span>
- <span>/</span>
- <span class="total">${ST}</span>
- <span class="bar"></span>
- </div>
- <div class="kbd-hint" id="kbdHint">
- <kbd>↓</kbd> / <kbd>Space</kbd>
- <span style="margin:0 4px;opacity:.5;">|</span>
- <span style="opacity:.7;">滚动或按键切换</span>
- </div>
- <main class="deck">
- ${renderS0Cover({ product, category, collectedAt, A, slideNum: s0, slideTotal: ST })}
- ${renderS1Summary({ A, slideNum: s1, slideTotal: ST })}
- ${renderS2Insights({ A, themes, slideNum: s2, slideTotal: ST })}
- ${renderS3Notes({ A, slideNum: s3, slideTotal: ST })}
- ${renderS4Voices({ A, slideNum: s4, slideTotal: ST })}
- ${renderS5KOL({ A, slideNum: s5, slideTotal: ST })}
- ${renderS6Amazon({ A, slideNum: s6, slideTotal: ST })}
- ${hasTikTok ? renderSTikTok({ A, slideNum: sttk, slideTotal: ST }) : ''}
- ${hasDouyin ? renderSDouyin({ A, slideNum: sdy, slideTotal: ST }) : ''}
- ${renderS7Themes({ themes, slideNum: s7, slideTotal: ST })}
- ${renderS8Matrix({ themes, slideNum: s8, slideTotal: ST })}
- ${renderS9Action({ A, themes, slideNum: s9, slideTotal: ST })}
- </main>
- ${renderFooter()}
- <script>
- window.__CHART_DATA__ = ${JSON.stringify(chartData)};
- ${SCRIPTS}
- </script>
- </body>
- </html>`;
- }
- module.exports = { buildHTML };
|