voc-render.js 109 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566
  1. // VOC 报告 HTML 渲染器 · v2
  2. const { esc, fmtNum, fmtMoney, cleanText } = require('./voc-analyze');
  3. const STYLES = require('./voc-styles');
  4. const SCRIPTS = require('./voc-scripts');
  5. function renderNav(product, hasDouyin, hasTikTok) {
  6. const sections = [
  7. { id: 's0', tip: '封面' }, { id: 's1', tip: '执行摘要' },
  8. { id: 's2', tip: '核心洞察' }, { id: 's3', tip: '爆款笔记' },
  9. { id: 's4', tip: '用户原声' }, { id: 's5', tip: 'KOL 分析' },
  10. { id: 's6', tip: 'Amazon 对标' },
  11. ...(hasTikTok ? [{ id: 'sttk', tip: 'TikTok 海外' }] : []),
  12. ...(hasDouyin ? [{ id: 'sdy', tip: '抖音阵地' }] : []),
  13. { id: 's7', tip: '痛点亮点' },
  14. { id: 's8', tip: '机会矩阵' }, { id: 's9', tip: '行动建议' },
  15. ];
  16. return `<div class="progress-line" id="progress-line"></div>
  17. <div class="top-bar">
  18. <div class="brand-logo">BRAIN<span class="dot">·</span>HACK / VOC</div>
  19. <div class="text-mono text-xs">${esc(product)} — 洞察报告 v2.0</div>
  20. </div>
  21. <nav class="nav-rail" id="navRail">
  22. ${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 ')}
  23. </nav>`;
  24. }
  25. function renderS0Cover({ product, category, collectedAt, A, slideNum, slideTotal }) {
  26. return `<section class="report-section" id="s0" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}" style="min-height:100vh;padding:0;">
  27. <div class="cover-hero">
  28. <div class="aurora" aria-hidden="true"><span></span><span></span><span></span></div>
  29. <div class="cover-grid-bg"></div>
  30. <div class="cover-content">
  31. <div class="cover-meta rise-in"><span>VOC / 2026 / 中国市场 · Amazon 对标</span></div>
  32. <div class="cover-title-stack">
  33. <p class="text-mono text-amber rise-in" style="font-size:0.9rem;letter-spacing:0.2em;margin-bottom:18px;">${esc(category)}</p>
  34. <h1 class="cover-title rise-in">${esc(product)}</h1>
  35. <p class="text-poster rise-in" style="margin-top:20px;color:var(--text-2);">用户洞察 · VOC 报告</p>
  36. </div>
  37. <p class="cover-sub fade-up">
  38. 基于小红书 <strong class="text-amber">${A.metrics.notes}</strong> 篇相关笔记 ·
  39. <strong class="text-amber">${A.metrics.sampleComments}</strong> 条真实评论 ·
  40. <strong class="text-amber">${A.kols.length}</strong> 位作者画像 ·
  41. ${A.amazonProducts.length > 0 ? `叠加 Amazon <strong class="text-amber">${A.amazonProducts.length}</strong> 款国际对标产品` : '国际品类对标中'},
  42. 解构用户真实需求、场景与未被满足的机会。
  43. </p>
  44. <div class="cover-stats fade-up">
  45. <div class="cover-stat"><div class="num">${fmtNum(A.metrics.totalLikes)}</div><div class="lbl">累计点赞</div></div>
  46. <div class="cover-stat"><div class="num">${fmtNum(A.metrics.totalComments)}</div><div class="lbl">累计评论</div></div>
  47. <div class="cover-stat"><div class="num">${fmtNum(A.metrics.totalCollected)}</div><div class="lbl">累计收藏</div></div>
  48. <div class="cover-stat"><div class="num">${fmtNum(A.metrics.totalShared)}</div><div class="lbl">累计分享</div></div>
  49. </div>
  50. <p class="text-mono text-xs fade-up" style="margin-top:64px;">SCROLL DOWN · 采集时间 ${collectedAt}</p>
  51. </div>
  52. </div>
  53. </section>`;
  54. }
  55. function renderS1Summary({ A, slideNum, slideTotal }) {
  56. const engRate = A.metrics.totalLikes > 0 ? ((A.metrics.totalComments / A.metrics.totalLikes) * 100).toFixed(1) : '—';
  57. return `<section class="report-section" id="s1" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}">
  58. <div class="section-inner">
  59. <div class="section-head rise-in">
  60. <div class="section-eyebrow">01 · Executive Summary</div>
  61. <h2 class="text-h1"><span class="headline-anim">执行摘要</span></h2>
  62. <p class="section-sub">核心数据一览,洞察始于规模。</p>
  63. </div>
  64. <div class="grid grid-4">
  65. <div class="metric fade-up">
  66. <div class="metric-label">笔记样本</div>
  67. <div class="metric-value" data-count="${A.metrics.notes}">${A.metrics.notes}</div>
  68. <div class="metric-sub">关键词覆盖 ${Object.keys(A.meta.xiaohongshu?.by_keyword || {}).length} 个维度</div>
  69. </div>
  70. <div class="metric fade-up">
  71. <div class="metric-label">评论原声</div>
  72. <div class="metric-value" data-count="${A.metrics.sampleComments}">${A.metrics.sampleComments}</div>
  73. <div class="metric-sub">来自 Top 笔记深度抓取</div>
  74. </div>
  75. <div class="metric fade-up">
  76. <div class="metric-label">作者画像</div>
  77. <div class="metric-value" data-count="${A.kols.length}">${A.kols.length}</div>
  78. <div class="metric-sub">头部 ${A.kolTier.head.length} · 腰部 ${A.kolTier.mid.length} · KOC ${A.kolTier.tail.length}</div>
  79. </div>
  80. <div class="metric fade-up">
  81. <div class="metric-label">评论/点赞比</div>
  82. <div class="metric-value">${engRate}<span class="metric-unit"> %</span></div>
  83. <div class="metric-sub">用户参与深度指标</div>
  84. </div>
  85. </div>
  86. <div class="grid grid-2" style="margin-top:40px;">
  87. <div class="card fade-up">
  88. <div class="text-mono text-xs" style="color:var(--amber);">SAMPLE BREAKDOWN</div>
  89. <h3 class="text-h2" style="margin-top:10px;">样本规模分布</h3>
  90. <div id="chartSample" class="chart-container" style="margin-top:20px;"></div>
  91. </div>
  92. <div class="card fade-up">
  93. <div class="text-mono text-xs" style="color:var(--amber);">GEO DISTRIBUTION</div>
  94. <h3 class="text-h2" style="margin-top:10px;">用户地域 TOP 10</h3>
  95. <div id="chartGeo" class="chart-container" style="margin-top:20px;"></div>
  96. </div>
  97. </div>
  98. </div>
  99. </section>`;
  100. }
  101. function renderS2Insights({ A, themes, slideNum, slideTotal }) {
  102. const topPain = themes.pain[0] || { tag: '—', count: 0 };
  103. const topBright = themes.bright[0] || { tag: '—', count: 0 };
  104. const topScenario = themes.scenarios[0] || { tag: '—', count: 0 };
  105. const hotNote = A.topNotes[0] || {};
  106. const topComment = A.topComments[0] || {};
  107. return `<section class="report-section" id="s2" data-tone="rose" data-slide-num="${slideNum}/${slideTotal}">
  108. <div class="section-inner">
  109. <div class="section-head rise-in">
  110. <div class="section-eyebrow">02 · Core Insights</div>
  111. <h2 class="text-h1"><span class="headline-anim">三条核心洞察</span></h2>
  112. <p class="section-sub">从数据里提炼出的不言而喻的真相。</p>
  113. </div>
  114. <div class="grid" style="grid-template-columns:1fr;gap:18px;">
  115. <div class="insight-banner fade-up" data-num="I">
  116. <h3>用户对此品类的第一反应是 <strong>${esc(topPain.tag)}</strong><br>而非功效或品牌。</h3>
  117. <p class="insight-body">在 ${A.metrics.sampleComments} 条评论中,"${esc(topPain.tag)}"被反复提及 <strong class="text-amber">${topPain.count}</strong> 次,超过任何品牌词。这是品类级的心智障碍,比产品本身更值得投资解决。</p>
  118. <div class="insight-data">
  119. <div class="dp"><span class="num">${topPain.count}</span><span class="lbl">提及次数</span></div>
  120. <div class="dp"><span class="num">${topBright.count}</span><span class="lbl">正向对照</span></div>
  121. <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>
  122. </div>
  123. </div>
  124. <div class="insight-banner fade-up" data-num="II">
  125. <h3>最高赞笔记 <strong>${fmtNum(hotNote.liked_count || 0)}</strong> 赞,<br>主题围绕 <strong>${esc(topScenario.tag)}</strong>。</h3>
  126. <p class="insight-body">"<em>${esc(cleanText(hotNote.title || '').slice(0, 40))}</em>" — 证明 <strong class="text-amber">${esc(topScenario.tag)}</strong> 是此品类最强的种草切入点。内容方向 / 产品卖点 / 投流素材都应围绕这一场景聚焦。</p>
  127. <div class="insight-data">
  128. <div class="dp"><span class="num">${fmtNum(hotNote.liked_count || 0)}</span><span class="lbl">最高赞</span></div>
  129. <div class="dp"><span class="num">${fmtNum(hotNote.comments_count || 0)}</span><span class="lbl">评论</span></div>
  130. <div class="dp"><span class="num">${fmtNum(hotNote.collected_count || 0)}</span><span class="lbl">收藏</span></div>
  131. </div>
  132. </div>
  133. <div class="insight-banner fade-up" data-num="III">
  134. <h3>一条 <strong>${fmtNum(topComment.like_count || 0)}</strong> 赞的评论,<br>说明 <strong>${esc(topBright.tag)}</strong> 是消费者自发认同的卖点。</h3>
  135. <p class="insight-body">"<em>${esc(cleanText(topComment.content || '').slice(0, 70))}</em>" — 这条评论的点赞数体现消费者的<strong class="text-amber">共识形成</strong>。这个表达本身就是可直接挪用的最佳营销文案。</p>
  136. <div class="insight-data">
  137. <div class="dp"><span class="num">${fmtNum(topComment.like_count || 0)}</span><span class="lbl">评论获赞</span></div>
  138. <div class="dp"><span class="num">${topBright.count}</span><span class="lbl">亮点提及</span></div>
  139. <div class="dp"><span class="num">${topScenario.count}</span><span class="lbl">场景覆盖</span></div>
  140. </div>
  141. </div>
  142. </div>
  143. </div>
  144. </section>`;
  145. }
  146. function renderS3Notes({ A, slideNum, slideTotal }) {
  147. const top12 = A.topNotes.slice(0, 12);
  148. return `<section class="report-section" id="s3" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}">
  149. <div class="section-inner">
  150. <div class="section-head rise-in">
  151. <div class="section-eyebrow">03 · Top Notes</div>
  152. <h2 class="text-h1"><span class="headline-anim">爆款笔记 · 种草全景</span></h2>
  153. <p class="section-sub">按点赞数排序的 Top 12 笔记 · 标题 / 互动结构 / 发布人画像</p>
  154. </div>
  155. <div class="grid grid-3">
  156. ${top12.map((n, i) => `<div class="note-card fade-up">
  157. <div class="note-rank">#${String(i + 1).padStart(2, '0')}</div>
  158. <div class="note-title">${esc(cleanText(n.title || ''))}</div>
  159. <div style="display:flex;gap:8px;flex-wrap:wrap;">
  160. <span class="tag gray">${n.type === 'video' ? '视频' : '图文'}</span>
  161. ${n.user?.red_official_verified ? '<span class="tag">官方认证</span>' : ''}
  162. </div>
  163. <div class="note-stats">
  164. <div class="st"><span class="n">${fmtNum(n.liked_count)}</span><span class="l">LIKES</span></div>
  165. <div class="st"><span class="n">${fmtNum(n.comments_count)}</span><span class="l">COMMENTS</span></div>
  166. <div class="st"><span class="n">${fmtNum(n.collected_count)}</span><span class="l">SAVES</span></div>
  167. </div>
  168. <div class="text-xs" style="color:var(--text-3);">@${esc(n.user?.nickname || '未知作者')}</div>
  169. </div>`).join('\n ')}
  170. </div>
  171. <div class="card fade-up" style="margin-top:32px;">
  172. <div class="text-mono text-xs text-amber">NOTE HEATMAP</div>
  173. <h3 class="text-h2" style="margin-top:10px;">笔记互动热力图</h3>
  174. <p class="text-sm" style="margin:8px 0 20px;">X 轴=点赞 · Y 轴=评论 · 气泡大小=收藏 · 找出低评论高收藏的"干货潜力"笔记</p>
  175. <div id="chartNoteHeatmap" class="chart-container" style="min-height:440px;"></div>
  176. </div>
  177. </div>
  178. </section>`;
  179. }
  180. function renderS4Voices({ A, slideNum, slideTotal }) {
  181. const voices = A.topComments.slice(0, 15);
  182. return `<section class="report-section" id="s4" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}">
  183. <div class="section-inner">
  184. <div class="section-head rise-in">
  185. <div class="section-eyebrow">04 · Voice Wall</div>
  186. <h2 class="text-h1"><span class="headline-anim">用户真实原声</span></h2>
  187. <p class="section-sub">高赞评论 Top 15 · 未经任何编辑的 VOC 原始语料</p>
  188. </div>
  189. <div class="voice-wall">
  190. ${voices.map((c, i) => `<div class="voice-card ${i < 3 ? 'hot' : ''} fade-up">
  191. <p class="voice-content">${esc(cleanText(c.content))}</p>
  192. <div class="voice-meta">
  193. <span>${esc((c.user_info?.nickname || '匿名').slice(0, 14))}${c.ip_location ? ' · ' + esc(c.ip_location) : ''}</span>
  194. <span class="voice-likes">♥ ${fmtNum(c.like_count || 0)}</span>
  195. </div>
  196. </div>`).join('\n ')}
  197. </div>
  198. </div>
  199. </section>`;
  200. }
  201. function renderS5KOL({ A, slideNum, slideTotal }) {
  202. const T = A.kolTier;
  203. const totalQualified = A.kols.length;
  204. const filtered = A.metrics.kolsFiltered || 0;
  205. const hasAnyKol = totalQualified > 0;
  206. const hasTopTier = T.head.length + T.mid.length > 0;
  207. return `<section class="report-section" id="s5" data-tone="purple" data-slide-num="${slideNum}/${slideTotal}">
  208. <div class="section-inner">
  209. <div class="section-head rise-in">
  210. <div class="section-eyebrow">05 · KOL Landscape</div>
  211. <h2 class="text-h1"><span class="headline-anim">作者生态分层</span></h2>
  212. <p class="section-sub">仅展示 ≥ 1000 粉且真实活跃的作者 · 已过滤 ${filtered} 位低粉低互动账号</p>
  213. </div>
  214. <div class="grid grid-4" style="margin-bottom:32px;">
  215. <div class="metric fade-up">
  216. <div class="metric-label">头部 KOL (50w+)</div>
  217. <div class="metric-value" style="color:var(--amber);">${T.head.length}</div>
  218. <div class="metric-sub">品牌背书 · 单篇合作单价高</div>
  219. </div>
  220. <div class="metric fade-up">
  221. <div class="metric-label">腰部达人 (5w-50w)</div>
  222. <div class="metric-value" style="color:var(--green);">${T.mid.length}</div>
  223. <div class="metric-sub">性价比最高 · 批量合作</div>
  224. </div>
  225. <div class="metric fade-up">
  226. <div class="metric-label">尾部 (5k-5w)</div>
  227. <div class="metric-value" style="color:var(--blue);">${T.tail.length}</div>
  228. <div class="metric-sub">垂类精准 · 素材铺量</div>
  229. </div>
  230. <div class="metric fade-up">
  231. <div class="metric-label">KOC (1k-5k)</div>
  232. <div class="metric-value" style="color:var(--purple);">${T.koc.length}</div>
  233. <div class="metric-sub">真实口碑 · 低成本试水</div>
  234. </div>
  235. </div>
  236. ${!hasAnyKol ? `
  237. <div class="insight-banner fade-up" data-num="!" style="border-left-color:var(--rose);">
  238. <h3>本次采样<strong class="text-rose">未发现 ≥1000 粉合格作者</strong>,<br>说明此品类在小红书尚未形成规模化 KOL 生态。</h3>
  239. <p class="insight-body">有效样本仅 ${filtered} 个微型账号(粉丝多在百位数),无稳定流量池。这是<strong class="text-amber">品类空白信号</strong>:
  240. <br>· 策略上 — 用 <strong>品牌自营矩阵</strong> 抢占心智(而非依赖达人合作)<br>· 执行上 — 做 <strong>大号跨品类合作</strong>(健康/养生/母婴领域头部)切入垂类
  241. <br>· 测试上 — <strong>站外抖音 + 微信私域</strong> 成本可能更低效果更高</p>
  242. </div>` : `
  243. <h3 class="text-h2 fade-up" style="margin-top:24px;margin-bottom:20px;">合格作者 Top ${Math.min(totalQualified, 9)} · 按粉丝排序</h3>
  244. <div class="grid grid-3">
  245. ${A.kols.slice(0, 9).map((k) => `<div class="kol-card fade-up">
  246. ${k.images ? `<img class="kol-avatar" src="${esc(k.images)}" alt="" loading="lazy" onerror="this.style.display='none'">` : '<div class="kol-avatar"></div>'}
  247. <div class="kol-info">
  248. <div class="kol-name">${esc(k.nickname || '')}${k.red_official_verified ? ' <span class="tag" style="font-size:0.62rem;padding:2px 6px;">官方</span>' : ''}</div>
  249. <div class="kol-desc">${esc(cleanText(k.desc || '暂无简介').slice(0, 70))}</div>
  250. <div class="kol-stats">
  251. <span><span class="amber">${fmtNum(k.fans)}</span> 粉丝</span>
  252. <span>${fmtNum(k.interaction || k.likes)} 赞藏</span>
  253. ${k.ip_location ? `<span>${esc(k.ip_location)}</span>` : ''}
  254. </div>
  255. </div>
  256. </div>`).join('\n ')}
  257. </div>
  258. ${!hasTopTier ? `
  259. <div class="card fade-up" style="margin-top:28px;border-left:3px solid var(--amber);">
  260. <div class="text-mono text-xs text-amber">STRATEGIC NOTE</div>
  261. <h3 class="text-h2" style="margin-top:10px;">头部/腰部缺失的含义</h3>
  262. <p class="text-sm" style="margin-top:8px;color:var(--text-2);line-height:1.7;">
  263. 目前合格样本全部集中在「尾部 + KOC」区间,这说明:<br>
  264. 1. 此品类属于 <strong class="text-amber">真实口碑驱动型</strong>,靠数十位 KOC 铺量比签一位头部更有效;<br>
  265. 2. 建议 <strong>优先批量投放 ${T.tail.length + T.koc.length} 位尾部/KOC 作者</strong>,以真实体验 × 素材密度驱动种草;<br>
  266. 3. 未来如需引爆,再考虑跨品类头部合作(如 母婴 / 健康科普 / 生活方式 赛道的 5w+ 达人)。
  267. </p>
  268. </div>` : ''}
  269. `}
  270. </div>
  271. </section>`;
  272. }
  273. function renderS6Amazon({ A, slideNum, slideTotal }) {
  274. const amz = A.amazon;
  275. if (!amz || amz.empty) {
  276. return `<section class="report-section" id="s6" data-tone="green" data-slide-num="${slideNum}/${slideTotal}">
  277. <div class="section-inner">
  278. <div class="section-head rise-in">
  279. <div class="section-eyebrow">06 · Amazon Benchmark</div>
  280. <h2 class="text-h1"><span class="headline-anim">Amazon 国际对标</span></h2>
  281. <p class="section-sub">此品类在 Sorftime 未返回产品,需补充英文关键词或确认 domain 映射。</p>
  282. </div>
  283. <div class="card fade-up"><p style="color:var(--text-2);">数据未采集或关键词无匹配产品。建议补充英文关键词后重跑采集脚本。</p></div>
  284. </div>
  285. </section>`;
  286. }
  287. const hasReviews = Object.keys(amz.reviewsSummary || {}).length > 0;
  288. const hasDetails = (amz.detailProducts || []).length > 0;
  289. // ═════════════════════════════════════════════
  290. // 6.0 · 核心发现 & 机会评估(分析层 · hero)
  291. // ═════════════════════════════════════════════
  292. const op = amz.opportunity;
  293. const findings = amz.keyFindings || [];
  294. const toneColor = (t) => ({
  295. green: 'var(--green)', amber: 'var(--amber)', rose: 'var(--rose)',
  296. blue: 'var(--blue)', purple: 'var(--purple)',
  297. }[t] || 'var(--amber)');
  298. const executiveLayer = op ? `
  299. <h3 class="text-h2 fade-up" style="margin-top:12px;margin-bottom:16px;">6.0 · 核心发现 & 机会评估</h3>
  300. <p class="section-sub fade-up" style="margin-bottom:22px;">基于 ${amz.total} 款产品 + ${op.dimensions.length} 维度诊断的判断层 · 每个结论都是可以直接拍板的行动锚点</p>
  301. <div class="grid" style="grid-template-columns:1.1fr 2fr;gap:22px;margin-bottom:28px;">
  302. <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)};">
  303. <div class="text-mono text-xs" style="color:var(--text-3);letter-spacing:0.18em;margin-bottom:10px;">CATEGORY OPPORTUNITY</div>
  304. <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>
  305. <div class="text-sm" style="color:var(--text-2);font-weight:600;margin-bottom:14px;">${esc(op.grade.split(' · ')[1] || '')}</div>
  306. <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;">
  307. <span style="color:${toneColor(op.gradeColor)};">${op.totalScore}</span><span style="color:var(--text-3);"> / ${op.maxScore}</span>
  308. </div>
  309. <div class="text-xs" style="margin-top:14px;color:var(--text-3);">甜点价位:<strong style="color:var(--amber);">${esc(op.sweetSpot)}</strong></div>
  310. </div>
  311. <div style="display:flex;flex-direction:column;gap:10px;">
  312. ${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);">
  313. <div style="font-size:1.3rem;flex-shrink:0;width:36px;text-align:center;">${d.icon}</div>
  314. <div style="flex:1;min-width:0;">
  315. <div style="display:flex;gap:10px;align-items:center;margin-bottom:4px;flex-wrap:wrap;">
  316. <span style="font-weight:700;font-size:0.86rem;">${esc(d.name)}</span>
  317. <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>
  318. <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>
  319. </div>
  320. <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>
  321. </div>
  322. </div>`).join('')}
  323. </div>
  324. </div>
  325. <div class="grid grid-2" style="margin-bottom:40px;gap:16px;">
  326. ${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)};">
  327. <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>
  328. <div style="display:flex;gap:10px;align-items:flex-start;margin-bottom:8px;">
  329. <span style="font-size:1.2rem;flex-shrink:0;">${f.icon}</span>
  330. <h4 style="font-size:1rem;font-weight:700;color:${toneColor(f.tone)};line-height:1.4;">${esc(f.title)}</h4>
  331. </div>
  332. <p class="text-sm" style="color:var(--text-2);line-height:1.65;">${f.body}</p>
  333. </div>`).join('')}
  334. </div>` : '';
  335. // 6.1 品类全景
  336. const overview = `
  337. <h3 class="text-h2 fade-up" style="margin-top:12px;margin-bottom:16px;">6.1 · 品类全景</h3>
  338. <div class="grid grid-4" style="margin-bottom:28px;">
  339. <div class="metric fade-up">
  340. <div class="metric-label">产品样本</div>
  341. <div class="metric-value" data-count="${amz.total}">${amz.total}</div>
  342. <div class="metric-sub">${amz.keywords.length} 组关键词 × 3 页</div>
  343. </div>
  344. <div class="metric fade-up">
  345. <div class="metric-label">均价 / 中位</div>
  346. <div class="metric-value">$${amz.priceStats.mean.toFixed(1)}</div>
  347. <div class="metric-sub">中位 $${amz.priceStats.median.toFixed(2)} · $${amz.priceStats.min.toFixed(1)}–$${amz.priceStats.max.toFixed(0)}</div>
  348. </div>
  349. <div class="metric fade-up">
  350. <div class="metric-label">平均评分</div>
  351. <div class="metric-value" style="color:var(--green);">${amz.avgRating.toFixed(2)}</div>
  352. <div class="metric-sub">用户质量期望 · 中位 ${amz.medianRating.toFixed(1)}</div>
  353. </div>
  354. <div class="metric fade-up">
  355. <div class="metric-label">月销总额</div>
  356. <div class="metric-value" style="color:var(--amber);">${fmtMoney(amz.totalMonthlyRevenue)}</div>
  357. <div class="metric-sub">CR5 ${amz.top5Share.toFixed(1)}% · HHI ${Math.round(amz.hhi)}</div>
  358. </div>
  359. </div>
  360. <div class="grid grid-2" style="margin-bottom:40px;">
  361. <div class="card fade-up">
  362. <div class="text-mono text-xs text-amber">PRICE BAND</div>
  363. <h3 class="text-h2" style="margin-top:10px;">价格带分布</h3>
  364. <div id="chartPrice" class="chart-container" style="margin-top:20px;min-height:280px;"></div>
  365. </div>
  366. <div class="card fade-up">
  367. <div class="text-mono text-xs text-green">RATING DISTRIBUTION</div>
  368. <h3 class="text-h2" style="margin-top:10px;">评分分布</h3>
  369. <div id="chartRatingDist" class="chart-container" style="margin-top:20px;min-height:280px;"></div>
  370. </div>
  371. </div>`;
  372. // 6.2 品牌格局
  373. const top10Brands = amz.topBrands.slice(0, 10);
  374. const brandStructure = `
  375. <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:16px;">6.2 · 品牌格局</h3>
  376. <div class="grid grid-2" style="margin-bottom:32px;">
  377. <div class="card fade-up">
  378. <div class="text-mono text-xs text-amber">BRAND SHARE</div>
  379. <h3 class="text-h2" style="margin-top:10px;">品牌月销份额</h3>
  380. <div id="chartBrandShare" class="chart-container" style="margin-top:20px;min-height:320px;"></div>
  381. </div>
  382. <div class="card fade-up">
  383. <div class="text-mono text-xs text-amber">TOP 10 BRANDS</div>
  384. <h3 class="text-h2" style="margin-top:10px;">Top 10 品牌表</h3>
  385. <div class="table-wrap" style="margin-top:16px;">
  386. <table class="data-tbl">
  387. <thead><tr><th style="width:40px;">#</th><th>品牌</th><th>SKU</th><th>份额</th><th>月销额</th></tr></thead>
  388. <tbody>
  389. ${top10Brands.map((b, i) => `<tr>
  390. <td class="tb-num">${String(i + 1).padStart(2, '0')}</td>
  391. <td style="font-weight:600;">${esc(b.brand)}</td>
  392. <td class="tb-num">${b.count}</td>
  393. <td class="tb-num" style="color:var(--amber);">${b.share.toFixed(1)}%</td>
  394. <td class="tb-num">${fmtMoney(b.sales)}</td>
  395. </tr>`).join('')}
  396. </tbody>
  397. </table>
  398. </div>
  399. <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);">
  400. <strong class="text-rose">⚠ 市场集中度:</strong>CR5 = <strong>${amz.top5Share.toFixed(1)}%</strong>, HHI = <strong>${Math.round(amz.hhi)}</strong>
  401. ${amz.top5Share >= 60 ? '(<span style="color:var(--rose);">高度集中,头部垄断</span>)' : amz.top5Share >= 40 ? '(<span style="color:var(--amber);">中度集中,需差异化切入</span>)' : '(<span style="color:var(--green);">竞争充分,进入门槛低</span>)'}
  402. </div>
  403. </div>
  404. </div>`;
  405. // ═════════════════════════════════════
  406. // 6.2+ · Top 5 竞品深度画像(分析层 · 紧跟 6.2 品牌格局)
  407. // ═════════════════════════════════════
  408. const dossiers = amz.competitorDossiers || [];
  409. const dossiersSection = dossiers.length ? `
  410. <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:12px;">6.2+ · Top ${dossiers.length} 竞品深度画像</h3>
  411. <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>
  412. <div class="grid grid-2" style="margin-bottom:40px;gap:20px;">
  413. ${dossiers.map((d, i) => `<div class="card fade-up" style="border-left:4px solid ${toneColor(d.positionColor)};padding:22px 22px 20px;">
  414. <div style="display:flex;justify-content:space-between;gap:12px;margin-bottom:12px;flex-wrap:wrap;">
  415. <div style="display:flex;align-items:baseline;gap:10px;">
  416. <span class="text-mono" style="color:var(--text-3);font-size:0.7rem;">#${String(i + 1).padStart(2, '0')}</span>
  417. <h4 style="font-size:1.05rem;font-weight:700;">${esc(d.brand)}</h4>
  418. </div>
  419. <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>
  420. </div>
  421. <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;">
  422. <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>
  423. <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>
  424. <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>
  425. <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>
  426. </div>
  427. ${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;">
  428. <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>
  429. <div class="text-sm" style="line-height:1.4;color:var(--text-1);margin-bottom:6px;">${esc((d.topProduct.title || '').slice(0, 70))}</div>
  430. <div class="text-xs text-mono" style="color:var(--text-3);display:flex;gap:12px;flex-wrap:wrap;">
  431. <span>$${d.topProduct.price.toFixed(2)}</span>
  432. <span style="color:var(--green);">★ ${d.topProduct.rating.toFixed(1)}</span>
  433. <span>${fmtNum(d.topProduct.reviewCount)} 评</span>
  434. <span style="color:var(--amber);">${fmtNum(d.topProduct.monthlySales)}/月</span>
  435. </div>
  436. </div>` : ''}
  437. <div class="grid grid-2" style="gap:10px;font-size:0.8rem;">
  438. <div>
  439. <div class="text-xs" style="color:var(--green);font-weight:700;margin-bottom:6px;letter-spacing:0.08em;">✅ STRENGTHS</div>
  440. ${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>'}
  441. </div>
  442. <div>
  443. <div class="text-xs" style="color:var(--rose);font-weight:700;margin-bottom:6px;letter-spacing:0.08em;">⚠ WEAKNESSES</div>
  444. ${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>'}
  445. </div>
  446. </div>
  447. <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);">
  448. <span>价格差 ${d.priceVsCategory > 0 ? '+' : ''}$${d.priceVsCategory} · 评分差 ${d.ratingVsCategory > 0 ? '+' : ''}${d.ratingVsCategory}</span>
  449. <span style="margin-left:auto;">${d.totalReviews ? fmtNum(d.totalReviews) + ' 评论' : ''}</span>
  450. </div>
  451. </div>`).join('')}
  452. </div>` : '';
  453. // 6.3 Top 产品矩阵
  454. const topBySalesRows = amz.topBySales.slice(0, 8).map((p, i) => `<tr>
  455. <td class="tb-num">${String(i + 1).padStart(2, '0')}</td>
  456. <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>
  457. <td class="tb-num">$${((p.SalesPrice || p.Price || 0) / 100).toFixed(2)}</td>
  458. <td class="tb-num" style="color:var(--amber);">${fmtNum(p.ListingSalesVolumeOfMonth || 0)}</td>
  459. <td>${p.Ratings ? `★ ${p.Ratings}` : '—'}<br><span class="text-xs text-mono" style="color:var(--text-3);">${fmtNum(p.RatingsCount || 0)} 评</span></td>
  460. <td class="text-mono text-xs" style="color:var(--text-3);">${esc(p.Asin || '—')}</td>
  461. </tr>`).join('');
  462. const topByReviewsRows = amz.topByReviews.slice(0, 8).map((p, i) => `<tr>
  463. <td class="tb-num">${String(i + 1).padStart(2, '0')}</td>
  464. <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>
  465. <td style="color:var(--green);font-weight:600;">★ ${p.Ratings || '—'}</td>
  466. <td class="tb-num" style="color:var(--amber);">${fmtNum(p.RatingsCount || 0)}</td>
  467. <td class="tb-num">$${((p.SalesPrice || p.Price || 0) / 100).toFixed(2)}</td>
  468. </tr>`).join('');
  469. const topMatrix = `
  470. <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:16px;">6.3 · Top 产品矩阵</h3>
  471. <div class="grid grid-2" style="margin-bottom:32px;">
  472. <div class="card fade-up">
  473. <div class="text-mono text-xs text-amber">MONTHLY SALES LEADERS</div>
  474. <h3 class="text-h2" style="margin-top:10px;">月销量 Top 8</h3>
  475. <div class="table-wrap" style="margin-top:16px;">
  476. <table class="data-tbl">
  477. <thead><tr><th>#</th><th>品牌 / 产品</th><th>售价</th><th>月销</th><th>评分</th><th>ASIN</th></tr></thead>
  478. <tbody>${topBySalesRows}</tbody>
  479. </table>
  480. </div>
  481. </div>
  482. <div class="card fade-up">
  483. <div class="text-mono text-xs text-green">REVIEW VOLUME LEADERS</div>
  484. <h3 class="text-h2" style="margin-top:10px;">评论数 Top 8 · 用户口碑沉淀</h3>
  485. <div class="table-wrap" style="margin-top:16px;">
  486. <table class="data-tbl">
  487. <thead><tr><th>#</th><th>品牌 / 产品</th><th>评分</th><th>评论数</th><th>售价</th></tr></thead>
  488. <tbody>${topByReviewsRows}</tbody>
  489. </table>
  490. </div>
  491. </div>
  492. </div>
  493. <div class="card fade-up" style="margin-bottom:40px;">
  494. <div class="text-mono text-xs text-purple">REVIEW COUNT DISTRIBUTION</div>
  495. <h3 class="text-h2" style="margin-top:10px;">评论数分布 · 口碑沉淀结构</h3>
  496. <div id="chartReviewCount" class="chart-container" style="margin-top:20px;min-height:260px;"></div>
  497. </div>`;
  498. // 6.4 深度产品画像
  499. const detailSection = hasDetails ? `
  500. <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:16px;">6.4 · 深度产品画像</h3>
  501. <p class="section-sub fade-up" style="margin-bottom:24px;">对 Top ${amz.detailProducts.length} 款拉取完整 ProductRequest · 含 BSR / 上线日 / Buybox 卖家 / 变体数</p>
  502. <div class="grid grid-2" style="margin-bottom:40px;">
  503. ${amz.detailProducts.slice(0, 6).map((p) => {
  504. const photo = (typeof p.photo === 'string' ? p.photo : p.photo?.Url) || '';
  505. return `<div class="card fade-up" style="display:flex;gap:16px;">
  506. ${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>'}
  507. <div style="flex:1;min-width:0;">
  508. <div style="display:flex;gap:6px;margin-bottom:6px;flex-wrap:wrap;">
  509. <span class="tag" style="background:rgba(245,166,35,.12);color:var(--amber);">${esc(p.brand || 'N/A')}</span>
  510. ${p.hasVideo ? '<span class="tag">有视频</span>' : ''}
  511. ${p.aplus ? '<span class="tag">A+</span>' : ''}
  512. ${p.brandStore ? '<span class="tag">品牌店</span>' : ''}
  513. ${p.variantCount > 1 ? `<span class="tag">${p.variantCount} 变体</span>` : ''}
  514. </div>
  515. <div style="font-weight:600;font-size:0.88rem;margin-bottom:8px;line-height:1.4;">${esc((p.title || '').slice(0, 78))}</div>
  516. <div class="text-xs text-mono" style="color:var(--text-3);margin-bottom:10px;">ASIN · ${esc(p.asin)}</div>
  517. <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;font-size:0.78rem;">
  518. <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>
  519. <div><div style="color:var(--text-3);">评分</div><div style="color:var(--green);font-weight:700;">★ ${p.rating || '—'} · ${fmtNum(p.ratingsCount || 0)}</div></div>
  520. <div><div style="color:var(--text-3);">月销</div><div style="font-weight:700;">${fmtNum(p.monthlySales || 0)}</div></div>
  521. <div><div style="color:var(--text-3);">BSR</div><div style="font-weight:700;">${p.bsrRank ? '#' + fmtNum(p.bsrRank) : '—'}</div></div>
  522. <div><div style="color:var(--text-3);">上线天数</div><div style="font-weight:700;">${p.onlineDays ? p.onlineDays + 'd' : '—'}</div></div>
  523. <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>
  524. </div>
  525. </div>
  526. </div>`;
  527. }).join('')}
  528. </div>
  529. ${amz.avgOnlineDays > 0 ? `
  530. <div class="card fade-up" style="margin-bottom:40px;border-left:3px solid var(--blue);">
  531. <div class="text-mono text-xs" style="color:var(--blue);">MARKET MATURITY</div>
  532. <h3 class="text-h2" style="margin-top:10px;">品类成熟度:平均上线 ${amz.avgOnlineDays} 天 (${(amz.avgOnlineDays / 365).toFixed(1)} 年)</h3>
  533. <p class="text-sm" style="margin-top:10px;color:var(--text-2);line-height:1.7;">
  534. Top ${amz.detailProducts.length} 深度样本中 · <strong style="color:var(--green);">${amz.newProducts} 款新品(&lt;180d)</strong> · <strong style="color:var(--amber);">${amz.matureProducts} 款成熟品(≥1年)</strong>
  535. ${amz.matureProducts > amz.newProducts ? '· 市场结构稳固,新玩家需强差异化' : '· 新品频出,市场动态,有迭代窗口'}
  536. </p>
  537. </div>` : ''}` : '';
  538. // 6.5 评论深度
  539. const reviewSamples = hasReviews ? Object.values(amz.reviewsSummary) : [];
  540. const reviewSection = hasReviews ? `
  541. <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:16px;">6.5 · 真实评论抽样</h3>
  542. <p class="section-sub fade-up" style="margin-bottom:24px;">对 Top ${reviewSamples.length} 款抓取真实评论 · 按星级分布 + 正负面抽样</p>
  543. <div class="grid grid-1" style="margin-bottom:32px;">
  544. ${reviewSamples.map((r) => {
  545. const posSample = r.positiveReviews[0];
  546. const negSample = r.negativeReviews[0];
  547. const starStr = [5, 4, 3, 2, 1].map((s) => `${'★'.repeat(s)} ${r.starDist[s] || 0}`).join(' ');
  548. return `<div class="card fade-up" style="border-left:3px solid var(--amber);">
  549. <div style="display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:14px;">
  550. <div>
  551. <div class="text-mono text-xs" style="color:var(--amber);">${esc(r.asin)}</div>
  552. <h4 style="font-size:0.95rem;margin-top:4px;">${esc(r.brand || 'N/A')} · ${esc((r.title || '').slice(0, 50))}</h4>
  553. </div>
  554. <div style="text-align:right;flex-shrink:0;">
  555. <div style="color:var(--green);font-weight:700;font-size:1.1rem;">★ ${r.averageRating.toFixed(2)}</div>
  556. <div class="text-xs" style="color:var(--text-3);">${r.total} 条抽样</div>
  557. </div>
  558. </div>
  559. <div class="text-xs text-mono" style="color:var(--text-3);margin-bottom:12px;">${starStr}</div>
  560. <div class="grid grid-2" style="gap:12px;">
  561. ${posSample ? `<div style="padding:12px;background:rgba(0,220,130,.06);border-left:2px solid var(--green);border-radius:0 6px 6px 0;">
  562. <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>
  563. <div class="text-sm" style="color:var(--text-2);line-height:1.5;">${esc(cleanText(posSample.Content || posSample.content || '').slice(0, 240))}</div>
  564. </div>` : '<div></div>'}
  565. ${negSample ? `<div style="padding:12px;background:rgba(255,77,141,.06);border-left:2px solid var(--rose);border-radius:0 6px 6px 0;">
  566. <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>
  567. <div class="text-sm" style="color:var(--text-2);line-height:1.5;">${esc(cleanText(negSample.Content || negSample.content || '').slice(0, 240))}</div>
  568. </div>` : '<div></div>'}
  569. </div>
  570. </div>`;
  571. }).join('')}
  572. </div>
  573. <div class="card fade-up">
  574. <div class="text-mono text-xs text-amber">STAR DISTRIBUTION</div>
  575. <h3 class="text-h2" style="margin-top:10px;">各产品 5 星分布对比</h3>
  576. <div id="chartStarDist" class="chart-container" style="margin-top:20px;min-height:320px;"></div>
  577. </div>` : '';
  578. // ═════════════════════════════════════
  579. // 6.5+ · VOC 用户原声(分析层 · 紧跟 6.5 评论抽样)
  580. // ═════════════════════════════════════
  581. const vt = amz.amazonThemes;
  582. const renderBarRow = (items, color, totalRef) => items.map((t) => {
  583. const pct = totalRef ? Math.min(100, (t.count / totalRef * 100)) : 0;
  584. return `<div class="fade-up" style="margin-bottom:12px;">
  585. <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:5px;gap:8px;">
  586. <div style="display:flex;gap:8px;align-items:center;min-width:0;">
  587. <span style="font-size:1rem;flex-shrink:0;">${t.emoji || t.icon || '•'}</span>
  588. <span style="font-weight:600;font-size:0.82rem;overflow:hidden;text-overflow:ellipsis;">${esc(t.tag.split(' · ')[1] || t.tag)}</span>
  589. </div>
  590. <span class="text-mono" style="font-size:0.78rem;color:${color};flex-shrink:0;">${t.count} · ${t.pct.toFixed(1)}%</span>
  591. </div>
  592. <div style="height:6px;background:rgba(255,255,255,0.05);border-radius:3px;overflow:hidden;">
  593. <div style="height:100%;background:${color};width:${pct.toFixed(1)}%;border-radius:3px;transition:width 1.2s cubic-bezier(.16,.84,.33,1);"></div>
  594. </div>
  595. ${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>` : ''}
  596. </div>`;
  597. }).join('');
  598. const vocThemesSection = vt && (vt.pain.length || vt.bright.length || vt.scenarios.length) ? `
  599. <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:12px;">6.5+ · VOC 用户原声主题</h3>
  600. <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>
  601. <div class="grid grid-3" style="margin-bottom:40px;gap:20px;">
  602. <div class="card fade-up" style="border-left:3px solid var(--rose);">
  603. <div class="text-mono text-xs" style="color:var(--rose);">PAIN POINTS · 痛点</div>
  604. <h3 class="text-h3" style="margin-top:8px;margin-bottom:16px;">🔴 用户核心痛点 Top ${vt.pain.length}</h3>
  605. ${vt.pain.length ? renderBarRow(vt.pain, 'var(--rose)', vt.negReviews || 1) : '<div class="text-sm" style="color:var(--text-3);">差评样本不足以提炼共同痛点</div>'}
  606. </div>
  607. <div class="card fade-up" style="border-left:3px solid var(--green);">
  608. <div class="text-mono text-xs" style="color:var(--green);">BRIGHT SPOTS · 亮点</div>
  609. <h3 class="text-h3" style="margin-top:8px;margin-bottom:16px;">🟢 用户最爱亮点 Top ${vt.bright.length}</h3>
  610. ${vt.bright.length ? renderBarRow(vt.bright, 'var(--green)', vt.posReviews || 1) : '<div class="text-sm" style="color:var(--text-3);">好评样本不足</div>'}
  611. </div>
  612. <div class="card fade-up" style="border-left:3px solid var(--blue);">
  613. <div class="text-mono text-xs" style="color:var(--blue);">USAGE SCENARIOS · 场景</div>
  614. <h3 class="text-h3" style="margin-top:8px;margin-bottom:16px;">📍 使用场景分布</h3>
  615. ${vt.scenarios.length ? renderBarRow(vt.scenarios, 'var(--blue)', vt.totalReviews || 1) : '<div class="text-sm" style="color:var(--text-3);">评论中未明显捉到场景信号</div>'}
  616. </div>
  617. </div>` : '';
  618. // ═════════════════════════════════════
  619. // 6.6 · 战略建议(分析层 · 收尾)
  620. // ═════════════════════════════════════
  621. const st = amz.strategy;
  622. const renderTrack = (track, accent, eyebrow) => `<div class="card card-accent fade-up" style="border-left-color:${accent};">
  623. <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;">
  624. <div class="text-mono text-xs" style="color:${accent};">${eyebrow}</div>
  625. <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>
  626. </div>
  627. <h3 class="text-h2" style="margin-top:10px;margin-bottom:14px;">${esc(track.title)}</h3>
  628. <ul style="list-style:none;display:flex;flex-direction:column;gap:10px;color:var(--text-2);font-size:0.88rem;line-height:1.6;">
  629. ${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('')}
  630. </ul>
  631. </div>`;
  632. const strategySection = st ? `
  633. <h3 class="text-h2 fade-up" style="margin-top:8px;margin-bottom:12px;">6.6 · 从数据到行动 · 三马车战略建议</h3>
  634. <p class="section-sub fade-up" style="margin-bottom:22px;">基于以上所有分析维度(机会等级 <strong style="color:${toneColor(op?.gradeColor || 'amber')};">${esc(op?.grade || '—')}</strong>)导出的分阶段可执行动作</p>
  635. <div class="grid grid-3" style="margin-bottom:20px;gap:18px;">
  636. ${renderTrack(st.short, 'var(--amber)', 'TRACK 1 · 0-30 DAYS')}
  637. ${renderTrack(st.mid, 'var(--green)', 'TRACK 2 · 30-90 DAYS')}
  638. ${renderTrack(st.long, 'var(--purple)', 'TRACK 3 · 90+ DAYS')}
  639. </div>` : '';
  640. return `<section class="report-section" id="s6" data-tone="green" data-slide-num="${slideNum}/${slideTotal}">
  641. <div class="section-inner">
  642. <div class="section-head rise-in">
  643. <div class="section-eyebrow">06 · Amazon Benchmark</div>
  644. <h2 class="text-h1"><span class="headline-anim">Amazon 国际对标</span></h2>
  645. <p class="section-sub">${amz.keywords.length} 组英文关键词 × 3 页 · ${amz.total} 款产品 · Top ${amz.detailProducts.length} 深度详情 · Top ${Object.keys(amz.reviewsSummary).length} 真实评论</p>
  646. </div>
  647. ${executiveLayer}
  648. ${overview}
  649. ${brandStructure}
  650. ${dossiersSection}
  651. ${topMatrix}
  652. ${detailSection}
  653. ${reviewSection}
  654. ${vocThemesSection}
  655. ${strategySection}
  656. </div>
  657. </section>`;
  658. }
  659. function renderSDouyin({ A, slideNum, slideTotal }) {
  660. const dy = A.douyin;
  661. if (!dy || dy.empty) return '';
  662. const v = dy.video;
  663. const u = dy.userProfile;
  664. const topCmts = dy.topComments || [];
  665. const topPosts = dy.topPosts || [];
  666. const userPosts = dy.userPosts || [];
  667. const searches = dy.searches || {};
  668. const shareType = dy.shareType || 'video';
  669. const accountCards = dy.accountCards || [];
  670. const agg = dy.aggMetrics;
  671. // ---------- 账号矩阵(主号 + 竞品号 + 垂类号,多卡一排)----------
  672. // 只在有 2+ 账号时才展示矩阵,单账号走 heroUser 旧样式
  673. const roleToneMap = {
  674. '官方号': { bg: 'rgba(255,0,80,.14)', fg: '#FF3E75', border: 'rgba(255,0,80,.3)' },
  675. '品牌竞品': { bg: 'rgba(245,166,35,.14)', fg: '#F5A623', border: 'rgba(245,166,35,.3)' },
  676. '品牌竞品号': { bg: 'rgba(245,166,35,.14)', fg: '#F5A623', border: 'rgba(245,166,35,.3)' },
  677. '垂类博主': { bg: 'rgba(139,92,246,.14)', fg: '#A78BFA', border: 'rgba(139,92,246,.3)' },
  678. '垂类博主号': { bg: 'rgba(139,92,246,.14)', fg: '#A78BFA', border: 'rgba(139,92,246,.3)' },
  679. '补充号': { bg: 'rgba(0,220,130,.14)', fg: '#22C581', border: 'rgba(0,220,130,.3)' },
  680. };
  681. const accountMatrix = accountCards.length >= 2 ? `
  682. <div class="section-sub rise-in" style="margin-top:12px;margin-bottom:14px;color:var(--text-3);">
  683. <strong style="color:#FF3E75;">抖音账号矩阵 · ${accountCards.length} 个账号</strong>
  684. ${agg ? `· 累计粉丝 <strong style="color:var(--amber);">${fmtNum(agg.total_followers)}</strong>
  685. · 采样作品 <strong>${agg.total_posts_sampled}</strong>
  686. · 认证账号 <strong style="color:var(--green);">${agg.verified_count}/${agg.total_accounts}</strong>` : ''}
  687. </div>
  688. <div class="grid grid-${Math.min(accountCards.length, 3)}" style="margin-bottom:32px;gap:18px;">
  689. ${accountCards.map((c) => {
  690. const tone = roleToneMap[c.role] || roleToneMap['官方号'];
  691. const hasData = !c.error && c.nickname !== '—';
  692. if (!hasData) {
  693. return `<div class="card fade-up" style="border-left:3px solid ${tone.fg};opacity:.6;">
  694. <div class="text-mono text-xs" style="color:${tone.fg};letter-spacing:.08em;">${esc(c.role)}</div>
  695. <h4 class="text-h3" style="margin-top:8px;">${esc(c.label || c.role)}</h4>
  696. <div class="text-sm" style="color:var(--text-3);margin-top:10px;line-height:1.5;">
  697. ⚠ ${esc((c.error || '数据不可用').slice(0, 80))}
  698. </div>
  699. <div class="text-xs text-mono" style="margin-top:10px;color:var(--text-3);word-break:break-all;">${esc(c.share_url || '')}</div>
  700. </div>`;
  701. }
  702. return `<div class="card fade-up" style="border-left:3px solid ${tone.fg};">
  703. <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:12px;">
  704. <span class="tag" style="background:${tone.bg};color:${tone.fg};border-color:${tone.border};">${esc(c.role)}</span>
  705. ${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>` : ''}
  706. </div>
  707. <div style="display:flex;gap:12px;align-items:center;margin-bottom:12px;">
  708. ${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>`}
  709. <div style="flex:1;min-width:0;">
  710. <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>
  711. <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>
  712. </div>
  713. </div>
  714. <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:12px;">
  715. <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
  716. <div style="color:${tone.fg};font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(c.follower_count)}</div>
  717. <div style="color:var(--text-3);font-size:.62rem;letter-spacing:.06em;margin-top:2px;">粉丝</div>
  718. </div>
  719. <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
  720. <div style="font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(c.aweme_count)}</div>
  721. <div style="color:var(--text-3);font-size:.62rem;letter-spacing:.06em;margin-top:2px;">作品</div>
  722. </div>
  723. <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
  724. <div style="color:var(--green);font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(c.total_favorited)}</div>
  725. <div style="color:var(--text-3);font-size:.62rem;letter-spacing:.06em;margin-top:2px;">获赞</div>
  726. </div>
  727. </div>
  728. ${c.top_post ? `<div style="padding:10px;background:rgba(255,255,255,.02);border-radius:6px;border-left:2px solid ${tone.border};">
  729. <div class="text-xs" style="color:var(--text-3);letter-spacing:.08em;font-family:'JetBrains Mono';margin-bottom:6px;">TOP POST</div>
  730. <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>
  731. <div class="text-xs text-mono" style="margin-top:6px;color:var(--text-3);display:flex;gap:10px;">
  732. <span style="color:var(--amber);">♥ ${fmtNum(c.top_post.digg)}</span>
  733. <span style="color:var(--green);">💬 ${fmtNum(c.top_post.comment)}</span>
  734. ${c.top_post.play > 0 ? `<span>▶ ${fmtNum(c.top_post.play)}</span>` : ''}
  735. </div>
  736. </div>` : `<div class="text-xs" style="color:var(--text-3);padding:10px;background:rgba(255,255,255,.02);border-radius:6px;">暂无作品数据</div>`}
  737. </div>`;
  738. }).join('')}
  739. </div>` : '';
  740. // ---------- Hero:作者画像卡 + 作品节奏 ----------
  741. const heroUser = u ? `
  742. <div class="grid grid-2" style="margin-bottom:36px;gap:24px;">
  743. <div class="card card-glow fade-up" style="display:flex;gap:18px;align-items:flex-start;">
  744. ${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>'}
  745. <div style="flex:1;min-width:0;">
  746. <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;">
  747. <span class="tag" style="background:rgba(255,0,80,.15);color:#FF3E75;border-color:rgba(255,0,80,.3);">抖音 · ${shareType === 'user' ? '品牌旗舰店' : '视频'}</span>
  748. ${u.custom_verify ? `<span class="tag" style="background:rgba(245,166,35,.15);color:var(--amber);">${esc(u.custom_verify.slice(0, 20))}</span>` : ''}
  749. ${u.ip_location ? `<span class="tag gray">${esc(u.ip_location)}</span>` : ''}
  750. </div>
  751. <h3 class="text-h2" style="margin-bottom:8px;">${esc(u.nickname || '—')}</h3>
  752. <div class="text-sm" style="margin-bottom:14px;color:var(--text-2);line-height:1.6;">${esc((u.signature || '暂无简介').slice(0, 120))}</div>
  753. <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;font-size:0.78rem;">
  754. <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>
  755. <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>
  756. <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>
  757. <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>
  758. </div>
  759. </div>
  760. </div>
  761. <div class="card fade-up">
  762. <div class="text-mono text-xs" style="color:#FF3E75;">PRODUCTION RHYTHM</div>
  763. <h3 class="text-h2" style="margin-top:8px;">作品节奏均值</h3>
  764. ${dy.authorStats ? `<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:18px;">
  765. <div style="text-align:center;padding:14px;background:rgba(245,166,35,0.06);border-radius:10px;">
  766. <div style="color:var(--amber);font-weight:800;font-size:1.4rem;font-family:'JetBrains Mono';">${fmtNum(Math.round(dy.authorStats.avgDigg))}</div>
  767. <div style="color:var(--text-3);font-size:0.7rem;margin-top:4px;letter-spacing:0.08em;">AVG ♥ LIKES</div>
  768. </div>
  769. <div style="text-align:center;padding:14px;background:rgba(0,220,130,0.06);border-radius:10px;">
  770. <div style="color:var(--green);font-weight:800;font-size:1.4rem;font-family:'JetBrains Mono';">${fmtNum(Math.round(dy.authorStats.avgComment))}</div>
  771. <div style="color:var(--text-3);font-size:0.7rem;margin-top:4px;letter-spacing:0.08em;">AVG 💬 COMMENT</div>
  772. </div>
  773. <div style="text-align:center;padding:14px;background:rgba(139,92,246,0.06);border-radius:10px;">
  774. <div style="color:var(--purple);font-weight:800;font-size:1.4rem;font-family:'JetBrains Mono';">${fmtNum(Math.round(dy.authorStats.avgPlay))}</div>
  775. <div style="color:var(--text-3);font-size:0.7rem;margin-top:4px;letter-spacing:0.08em;">AVG ▶ PLAY</div>
  776. </div>
  777. </div>
  778. <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;">
  779. ${(u.follower_count || 0) > 500000
  780. ? `<strong style="color:#FF3E75;">头部品牌号</strong>:${fmtNum(u.follower_count)} 粉丝 + 高频发布 (${u.aweme_count} 作品) 已建立稳定内容资产,适合做 KOL 型种草。`
  781. : (u.follower_count || 0) > 10000
  782. ? `<strong style="color:var(--amber);">成长期品牌号</strong>:粉丝 ${fmtNum(u.follower_count)},内容储备 ${u.aweme_count} 支,应加大高转化爆款内容产出。`
  783. : `<strong style="color:var(--text-2);">起步品牌号</strong>:粉丝仅 ${fmtNum(u.follower_count)},首要任务是建立内容基线和选定垂类话题。`}
  784. </div>` : '<p class="text-sm" style="color:var(--text-3);margin-top:14px;">暂无作品均值数据。</p>'}
  785. </div>
  786. </div>` : '';
  787. // ---------- Hot video 卡片(仅 video-type 分享链)----------
  788. const videoCard = v ? `
  789. <div class="card fade-up" style="margin-bottom:32px;display:flex;gap:18px;align-items:flex-start;">
  790. ${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;">` : ''}
  791. <div style="flex:1;min-width:0;">
  792. <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;">
  793. <span class="tag" style="background:rgba(255,0,80,.12);color:#FF3E75;border-color:rgba(255,0,80,.25);">品牌视频</span>
  794. ${(v.text_extra || []).slice(0, 3).map((t) => `<span class="tag">#${esc(t)}</span>`).join('')}
  795. </div>
  796. <div style="font-weight:600;font-size:0.94rem;line-height:1.5;margin-bottom:12px;">${esc(cleanText(v.desc || '').slice(0, 160))}</div>
  797. <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;font-size:0.78rem;margin-bottom:10px;">
  798. <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>
  799. <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>
  800. <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>
  801. <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>
  802. </div>
  803. </div>
  804. </div>` : '';
  805. // ---------- Top Posts 散点 + 卡片网格 ----------
  806. const topPostsSection = topPosts.length ? `
  807. <h3 class="text-h2 rise-in" style="margin-top:8px;margin-bottom:18px;">品牌号作品热度图 · Top ${topPosts.length}</h3>
  808. <div class="grid grid-2" style="margin-bottom:32px;gap:24px;">
  809. <div class="card fade-up">
  810. <div class="text-mono text-xs" style="color:#FF3E75;">POST SCATTER</div>
  811. <h4 class="text-h3" style="margin-top:8px;margin-bottom:16px;">X=点赞 · Y=评论 · 气泡=转发</h4>
  812. <div id="chartDyPosts" class="chart-container" style="min-height:380px;"></div>
  813. </div>
  814. <div class="card fade-up">
  815. <div class="text-mono text-xs" style="color:var(--amber);">TOP ${Math.min(topPosts.length, 5)} POSTS</div>
  816. <h4 class="text-h3" style="margin-top:8px;margin-bottom:16px;">爆款作品清单</h4>
  817. <div style="display:flex;flex-direction:column;gap:12px;">
  818. ${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;">
  819. ${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>'}
  820. <div style="flex:1;min-width:0;">
  821. <div class="text-xs" style="color:var(--amber);font-family:'JetBrains Mono';margin-bottom:3px;">#${String(i + 1).padStart(2, '0')}</div>
  822. <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>
  823. <div class="text-xs text-mono" style="color:var(--text-3);display:flex;gap:10px;">
  824. <span style="color:var(--amber);">♥ ${fmtNum(p.digg)}</span>
  825. <span style="color:var(--green);">💬 ${fmtNum(p.comment)}</span>
  826. <span>▶ ${fmtNum(p.play)}</span>
  827. </div>
  828. </div>
  829. </div>`).join('')}
  830. </div>
  831. </div>
  832. </div>` : '';
  833. // ---------- 评论墙 ----------
  834. const commentWall = topCmts.length ? `
  835. <h3 class="text-h2 rise-in" style="margin-top:8px;margin-bottom:18px;">抖音评论墙 · Top ${Math.min(topCmts.length, 12)}</h3>
  836. <div class="voice-wall" style="margin-bottom:32px;">
  837. ${topCmts.slice(0, 12).map((c, i) => `<div class="voice-card ${i < 3 ? 'hot' : ''} fade-up">
  838. <p class="voice-content">${esc(cleanText(c.text || ''))}</p>
  839. <div class="voice-meta">
  840. <span>${esc((c.user?.nickname || '匿名').slice(0, 14))}${c.ip_label ? ' · ' + esc(c.ip_label) : ''}</span>
  841. <span class="voice-likes">♥ ${fmtNum(c.digg_count || 0)}</span>
  842. </div>
  843. </div>`).join('')}
  844. </div>` : '';
  845. // ---------- 完整作品表 ----------
  846. const userPostsTable = userPosts.length ? `
  847. <h3 class="text-h2 rise-in" style="margin-top:8px;margin-bottom:18px;">品牌号完整作品流 · ${userPosts.length} 条</h3>
  848. <div class="table-wrap fade-up" style="margin-bottom:32px;">
  849. <table class="data-tbl">
  850. <thead><tr><th>#</th><th>内容</th><th>点赞</th><th>评论</th><th>播放</th><th>分享</th><th>收藏</th></tr></thead>
  851. <tbody>
  852. ${userPosts.slice().sort((a, b) => (b.statistics?.digg_count || 0) - (a.statistics?.digg_count || 0)).slice(0, 12).map((p, i) => `<tr>
  853. <td class="tb-num">${String(i + 1).padStart(2, '0')}</td>
  854. <td style="font-size:0.86rem;max-width:360px;">${esc(cleanText(p.desc || '').slice(0, 72))}</td>
  855. <td class="tb-num" style="color:var(--amber);">${fmtNum(p.statistics?.digg_count || 0)}</td>
  856. <td class="tb-num" style="color:var(--green);">${fmtNum(p.statistics?.comment_count || 0)}</td>
  857. <td class="tb-num">${fmtNum(p.statistics?.play_count || 0)}</td>
  858. <td class="tb-num">${fmtNum(p.statistics?.share_count || 0)}</td>
  859. <td class="tb-num">${fmtNum(p.statistics?.collect_count || 0)}</td>
  860. </tr>`).join('')}
  861. </tbody>
  862. </table>
  863. </div>` : '';
  864. // ---------- 关键词搜索(若有 cookie)----------
  865. const searchSection = Object.keys(searches).length ? `
  866. <h3 class="text-h2 rise-in" style="margin-top:8px;margin-bottom:18px;">品类关键词热度</h3>
  867. <div class="grid grid-${Math.min(Object.keys(searches).length, 3)}" style="margin-bottom:32px;">
  868. ${Object.entries(searches).map(([kw, info]) => {
  869. 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>`;
  870. 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>`;
  871. return `<div class="card fade-up">
  872. <div class="text-mono text-xs text-amber">KEYWORD "${esc(kw)}"</div>
  873. <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px;font-size:0.8rem;">
  874. <div><div style="color:var(--text-3);">视频数</div><div style="font-weight:700;font-size:1.05rem;">${info.count}</div></div>
  875. <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>
  876. </div>
  877. <div style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
  878. ${(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;">
  879. <span class="text-mono" style="color:var(--amber);margin-right:6px;">${fmtNum(t.digg)}❤</span>${esc((t.desc || '').slice(0, 50))}
  880. </div>`).join('')}
  881. </div>
  882. </div>`;
  883. }).join('')}
  884. </div>` : '';
  885. return `<section class="report-section" id="sdy" data-tone="rose" data-slide-num="${slideNum}/${slideTotal}">
  886. <div class="section-inner">
  887. <div class="section-head rise-in">
  888. <div class="section-eyebrow" style="color:#FF3E75;">DY · Douyin Presence</div>
  889. <h2 class="text-h1"><span class="headline-anim">抖音阵地深度</span></h2>
  890. <p class="section-sub">${accountCards.length >= 2 ? `多账号矩阵采样:官方号 + 品牌竞品 + 垂类博主 · 主号深度画像如下` : (shareType === 'user' ? '通过品牌旗舰店分享链直抓 · 账号画像 / 作品流 / 评论 / 品类搜索' : '通过品牌视频分享链直抓 · 视频/评论/作者主页/品类搜索')}</p>
  891. </div>
  892. ${renderDouyinTrendsBlock({ A })}
  893. ${accountMatrix}
  894. ${heroUser}
  895. ${videoCard}
  896. ${topPostsSection}
  897. ${commentWall}
  898. ${userPostsTable}
  899. ${searchSection}
  900. </div>
  901. </section>`;
  902. }
  903. function renderS7Themes({ themes, slideNum, slideTotal }) {
  904. const renderGroup = (list, cls, title, eyebrow, color) => `
  905. <div>
  906. <div class="section-eyebrow fade-up" style="color:${color};">${eyebrow}</div>
  907. <h3 class="text-h2 fade-up" style="margin-bottom:18px;">${title}</h3>
  908. <div class="grid grid-3">
  909. ${list.slice(0, 6).map((t) => `<div class="theme-poster ${cls} fade-up">
  910. <h4>${esc(t.tag)}</h4>
  911. <div class="count"><span class="num">${t.count}</span>次提及</div>
  912. <ul class="examples">
  913. ${(t.examples || []).slice(0, 2).map((e) => `<li>"${esc(cleanText(e).slice(0, 80))}"</li>`).join('')}
  914. </ul>
  915. </div>`).join('\n ')}
  916. </div>
  917. </div>`;
  918. return `<section class="report-section" id="s7" data-tone="blue" data-slide-num="${slideNum}/${slideTotal}">
  919. <div class="section-inner">
  920. <div class="section-head rise-in">
  921. <div class="section-eyebrow">07 · Themes Cluster</div>
  922. <h2 class="text-h1"><span class="headline-anim">痛点 · 亮点 · 场景</span></h2>
  923. <p class="section-sub">从真实语料中提炼出的 VOC 主题 · 标注频次与典型原声</p>
  924. </div>
  925. <div style="display:flex;flex-direction:column;gap:56px;">
  926. ${renderGroup(themes.pain, 'pain', '痛点主题', '07.1 · PAIN POINTS', 'var(--rose)')}
  927. ${renderGroup(themes.bright, 'bright', '亮点主题', '07.2 · BRIGHT SPOTS', 'var(--green)')}
  928. ${renderGroup(themes.scenarios, 'scene', '场景主题', '07.3 · USAGE SCENARIOS', 'var(--blue)')}
  929. </div>
  930. </div>
  931. </section>`;
  932. }
  933. function renderS8Matrix({ themes, slideNum, slideTotal }) {
  934. const q1 = themes.pain.filter((t) => ['口感差', '效果不显'].includes(t.tag)).slice(0, 2);
  935. const q2 = themes.bright.slice(0, 3);
  936. const q3 = themes.pain.filter((t) => ['信任危机', '价格敏感', '副作用担忧'].includes(t.tag)).slice(0, 2);
  937. const q4 = themes.scenarios.slice(0, 2);
  938. return `<section class="report-section" id="s8" data-tone="purple" data-slide-num="${slideNum}/${slideTotal}">
  939. <div class="section-inner">
  940. <div class="section-head rise-in">
  941. <div class="section-eyebrow">08 · Opportunity Matrix</div>
  942. <h2 class="text-h1"><span class="headline-anim">品类机会矩阵</span></h2>
  943. <p class="section-sub">四象限 · 产品可解决性 × 用户痛点强度</p>
  944. </div>
  945. <div class="matrix-2x2 fade-up">
  946. <div class="axis-y">痛点强度 LOW → HIGH</div>
  947. <div class="matrix-cell q1">
  948. <div class="cell-label text-amber">QUADRANT I · 产品机会</div>
  949. <h5>高痛点 · 产品可优化解决</h5>
  950. <p>${q1.map((t) => `<strong class="text-amber">${esc(t.tag)}</strong>(${t.count})`).join(' / ') || '尚无强信号'}。这些痛点可通过产品配方、剂型、包装直接优化 — 直接迭代 ROI 最高。</p>
  951. </div>
  952. <div class="matrix-cell q2">
  953. <div class="cell-label text-green">QUADRANT II · 强化既有</div>
  954. <h5>低痛点 · 产品可强化放大</h5>
  955. <p>${q2.map((t) => `<strong class="text-green">${esc(t.tag)}</strong>(${t.count})`).join(' / ') || '尚无强信号'}。这些是消费者已认同的优势,内容营销直接放大。</p>
  956. </div>
  957. <div class="matrix-cell q3">
  958. <div class="cell-label text-rose">QUADRANT III · 教育必投</div>
  959. <h5>高痛点 · 非产品端</h5>
  960. <p>${q3.map((t) => `<strong class="text-rose">${esc(t.tag)}</strong>(${t.count})`).join(' / ') || '尚无强信号'}。这些是品类心智问题,需要 PR / 教育内容 / 品牌信任建设长期解决。</p>
  961. </div>
  962. <div class="matrix-cell q4">
  963. <div class="cell-label text-purple">QUADRANT IV · 场景机会</div>
  964. <h5>潜在使用场景 · 可扩展</h5>
  965. <p>${q4.map((t) => `<strong class="text-purple">${esc(t.tag)}</strong>(${t.count})`).join(' / ') || '尚无强信号'}。这些场景是新增长入口,可通过 campaign / 联名 / KOC 铺量打开。</p>
  966. </div>
  967. <div class="axis-x">产品可解决性 LOW → HIGH</div>
  968. </div>
  969. </div>
  970. </section>`;
  971. }
  972. function renderS9Action({ A, themes, slideNum, slideTotal }) {
  973. const topPain = themes.pain[0]?.tag || '核心痛点';
  974. const topScene = themes.scenarios[0]?.tag || '场景';
  975. const topKol = A.kols[0];
  976. return `<section class="report-section" id="s9" data-tone="amber" data-slide-num="${slideNum}/${slideTotal}">
  977. <div class="section-inner">
  978. <div class="section-head rise-in">
  979. <div class="section-eyebrow">09 · Action Plan</div>
  980. <h2 class="text-h1"><span class="headline-anim">30 天行动建议</span></h2>
  981. <p class="section-sub">产品 / 内容 / 渠道 三路并进,从 VOC 到落地。</p>
  982. </div>
  983. <div class="grid grid-3" style="margin-bottom:40px;">
  984. <div class="card card-accent fade-up">
  985. <div class="text-mono text-xs text-amber">TRACK A · PRODUCT</div>
  986. <h3 class="text-h2" style="margin-top:10px;">产品端</h3>
  987. <ul style="margin-top:16px;list-style:none;display:flex;flex-direction:column;gap:10px;color:var(--text-2);font-size:0.9rem;">
  988. <li>✓ 针对「${esc(topPain)}」推出改良版 SKU / 新剂型</li>
  989. <li>✓ 包装文案直接借用高赞评论原文</li>
  990. <li>✓ 上市前用同样 VOC 链路做 A/B 预测试</li>
  991. </ul>
  992. </div>
  993. <div class="card card-accent fade-up" style="border-left-color:var(--green);">
  994. <div class="text-mono text-xs text-green">TRACK B · CONTENT</div>
  995. <h3 class="text-h2" style="margin-top:10px;">内容端</h3>
  996. <ul style="margin-top:16px;list-style:none;display:flex;flex-direction:column;gap:10px;color:var(--text-2);font-size:0.9rem;">
  997. <li>✓ 切入「${esc(topScene)}」场景做系列种草</li>
  998. <li>✓ 复用 Top 爆款笔记标题公式批量产出</li>
  999. <li>✓ 用高赞评论作为视频字幕 / 金句 BGM</li>
  1000. </ul>
  1001. </div>
  1002. <div class="card card-accent fade-up" style="border-left-color:var(--purple);">
  1003. <div class="text-mono text-xs text-purple">TRACK C · CHANNEL</div>
  1004. <h3 class="text-h2" style="margin-top:10px;">渠道端</h3>
  1005. <ul style="margin-top:16px;list-style:none;display:flex;flex-direction:column;gap:10px;color:var(--text-2);font-size:0.9rem;">
  1006. <li>✓ 头部 ${A.kolTier.head.length} 位优先合作${topKol ? `(如 ${esc(topKol.nickname)})` : ''}</li>
  1007. <li>✓ 腰部 ${A.kolTier.mid.length} 位批量铺垫品类心智</li>
  1008. <li>✓ KOC ${A.kolTier.tail.length} 位素材铺量支撑投流</li>
  1009. </ul>
  1010. </div>
  1011. </div>
  1012. <h3 class="text-h2 fade-up" style="margin-bottom:18px;">30 天时间线</h3>
  1013. <div class="tl fade-up">
  1014. <div class="tl-item">
  1015. <div class="tl-time">DAY 01 — 07 · 对齐</div>
  1016. <h5>对齐核心 VOC 发现 + 快速小样测试</h5>
  1017. <p>把本报告的 Top 3 洞察 / Top 痛点 / Top 爆款笔记,在内部做 1 次品牌 / 产品 / 运营 / 投放四方对齐会。选 1-2 个高赞评论金句,做小样社交投流测试。</p>
  1018. </div>
  1019. <div class="tl-item">
  1020. <div class="tl-time">DAY 08 — 14 · 内容</div>
  1021. <h5>复用爆款结构,启动 KOC 铺量</h5>
  1022. <p>以「${esc(topScene)}」为切入点,量产 20-30 条 KOC 笔记;每条笔记用不同的高赞评论做卡点。配合 ${A.kolTier.mid.length} 位腰部达人同步发种草。</p>
  1023. </div>
  1024. <div class="tl-item">
  1025. <div class="tl-time">DAY 15 — 21 · 放大</div>
  1026. <h5>基于数据优选,头部合作上场</h5>
  1027. <p>根据 Day 08-14 的测试数据,锁定 2-3 个最强素材方向,启动头部 KOL 合作。同步通过 Amazon 国际对标数据测试是否跨境放量。</p>
  1028. </div>
  1029. <div class="tl-item">
  1030. <div class="tl-time">DAY 22 — 30 · 复盘</div>
  1031. <h5>收集新一轮 VOC,迭代下次投放</h5>
  1032. <p>重新跑 VOC 采集脚本,对比 30 天前后的痛点词频变化。把"新涌现的痛点"作为下一迭代的产品优化 input,形成闭环。</p>
  1033. </div>
  1034. </div>
  1035. </div>
  1036. </section>`;
  1037. }
  1038. function renderFooter() {
  1039. return `<footer class="report-footer">
  1040. <div>Generated by <strong class="text-amber">BRAIN·HACK / VOC</strong> · Powered by TikHub + Sorftime + OpenClaw Skills</div>
  1041. <div class="meta">Data sources: Xiaohongshu · Amazon (via Sorftime) · Douyin (via TikHub)</div>
  1042. </footer>`;
  1043. }
  1044. // ============================================================
  1045. // TikTok · Amazon 的内容端对标(独立 slide)
  1046. // ============================================================
  1047. function renderSTikTok({ A, slideNum, slideTotal }) {
  1048. const tt = A.tiktok;
  1049. if (!tt || tt.empty) return '';
  1050. const m = tt.metrics || {};
  1051. const topVids = tt.topVideos || [];
  1052. const topAuthors = tt.topAuthors || [];
  1053. const topCmts = tt.topComments || [];
  1054. const topTags = tt.topHashtags || [];
  1055. const topRegions = tt.topRegions || [];
  1056. const byKw = tt.byKeywordStats || {};
  1057. const keywords = tt.keywords_used || Object.keys(byKw);
  1058. const topCatUsers = tt.topCategoryUsers || [];
  1059. const TT_CYAN = '#25F4EE';
  1060. const TT_PINK = '#FE2C55';
  1061. // --- Hero metrics 四大数字 + 认证率 + 触达粉丝 ---
  1062. const heroMetrics = `
  1063. <div class="grid grid-4" style="margin-bottom:28px;gap:16px;">
  1064. <div class="card card-glow fade-up" style="border-left:3px solid ${TT_CYAN};">
  1065. <div class="text-mono text-xs" style="color:${TT_CYAN};letter-spacing:.12em;">VIDEOS</div>
  1066. <div style="font-family:'JetBrains Mono';font-size:2.4rem;font-weight:900;color:${TT_CYAN};margin-top:8px;">${fmtNum(m.totalVideos)}</div>
  1067. <div class="text-xs" style="color:var(--text-3);margin-top:4px;">跨 ${keywords.length} 个英文关键词</div>
  1068. </div>
  1069. <div class="card fade-up" style="border-left:3px solid var(--amber);">
  1070. <div class="text-mono text-xs text-amber" style="letter-spacing:.12em;">TOTAL PLAYS</div>
  1071. <div style="font-family:'JetBrains Mono';font-size:2.4rem;font-weight:900;color:var(--amber);margin-top:8px;">${fmtNum(m.totalPlay)}</div>
  1072. <div class="text-xs" style="color:var(--text-3);margin-top:4px;">平均 ER ${(m.avgEngagementRate * 100).toFixed(1)}%</div>
  1073. </div>
  1074. <div class="card fade-up" style="border-left:3px solid ${TT_PINK};">
  1075. <div class="text-mono text-xs" style="color:${TT_PINK};letter-spacing:.12em;">TOTAL LIKES</div>
  1076. <div style="font-family:'JetBrains Mono';font-size:2.4rem;font-weight:900;color:${TT_PINK};margin-top:8px;">${fmtNum(m.totalDigg)}</div>
  1077. <div class="text-xs" style="color:var(--text-3);margin-top:4px;">评论 ${fmtNum(m.totalComment)} · 分享 ${fmtNum(m.totalShare)}</div>
  1078. </div>
  1079. <div class="card fade-up" style="border-left:3px solid var(--green);">
  1080. <div class="text-mono text-xs text-green" style="letter-spacing:.12em;">CREATORS</div>
  1081. <div style="font-family:'JetBrains Mono';font-size:2.4rem;font-weight:900;color:var(--green);margin-top:8px;">${m.totalAuthors}</div>
  1082. <div class="text-xs" style="color:var(--text-3);margin-top:4px;">认证 ${m.verifiedAuthors} · 触达粉丝 ${fmtNum(m.totalFollowersReached)}</div>
  1083. </div>
  1084. </div>`;
  1085. // --- 关键词覆盖 bar row ---
  1086. const maxKwDigg = Math.max(1, ...Object.values(byKw).map((v) => v.totalDigg || 0));
  1087. const keywordCoverage = Object.keys(byKw).length ? `
  1088. <div class="card fade-up" style="margin-bottom:28px;border-left:3px solid ${TT_CYAN};">
  1089. <div class="text-mono text-xs" style="color:${TT_CYAN};letter-spacing:.12em;">KEYWORD COVERAGE</div>
  1090. <h3 class="text-h3" style="margin-top:8px;margin-bottom:14px;">英文关键词覆盖深度 · TikTok 搜索反馈</h3>
  1091. <div style="display:flex;flex-direction:column;gap:10px;">
  1092. ${Object.entries(byKw).map(([kw, s]) => {
  1093. const pct = Math.round((s.totalDigg / maxKwDigg) * 100);
  1094. return `<div style="display:grid;grid-template-columns:220px 1fr auto;gap:14px;align-items:center;">
  1095. <div class="text-sm" style="font-family:'JetBrains Mono';color:var(--text-2);">${esc(kw)}</div>
  1096. <div style="background:rgba(255,255,255,.06);border-radius:4px;height:14px;position:relative;overflow:hidden;">
  1097. <div style="width:${pct}%;height:100%;background:linear-gradient(90deg, ${TT_CYAN}, ${TT_PINK});border-radius:4px;transition:width .8s;"></div>
  1098. </div>
  1099. <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>
  1100. </div>`;
  1101. }).join('')}
  1102. </div>
  1103. </div>` : '';
  1104. // --- Top 作者卡 ---
  1105. const authorsBlock = topAuthors.length ? `
  1106. <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>
  1107. <div class="grid grid-${Math.min(topAuthors.length, 3)}" style="margin-bottom:32px;gap:18px;">
  1108. ${topAuthors.map((a) => `<div class="card fade-up" style="border-left:3px solid ${TT_CYAN};">
  1109. <div style="display:flex;gap:12px;align-items:center;margin-bottom:12px;">
  1110. ${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>`}
  1111. <div style="flex:1;min-width:0;">
  1112. <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>
  1113. <div class="text-xs text-mono" style="color:${TT_CYAN};margin-top:3px;">@${esc(a.unique_id || '')}${a.region ? ' · ' + esc(a.region) : ''}</div>
  1114. </div>
  1115. ${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>` : ''}
  1116. </div>
  1117. <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>
  1118. <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:10px;">
  1119. <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
  1120. <div style="color:${TT_CYAN};font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(a.follower_count)}</div>
  1121. <div style="color:var(--text-3);font-size:.62rem;margin-top:2px;letter-spacing:.06em;">FOLLOWERS</div>
  1122. </div>
  1123. <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
  1124. <div style="font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(a.aweme_count)}</div>
  1125. <div style="color:var(--text-3);font-size:.62rem;margin-top:2px;letter-spacing:.06em;">VIDEOS</div>
  1126. </div>
  1127. <div style="text-align:center;padding:8px 4px;background:rgba(255,255,255,.02);border-radius:6px;">
  1128. <div style="color:var(--amber);font-weight:800;font-family:'JetBrains Mono';font-size:.95rem;">${fmtNum(a.total_favorited)}</div>
  1129. <div style="color:var(--text-3);font-size:.62rem;margin-top:2px;letter-spacing:.06em;">LIKES</div>
  1130. </div>
  1131. </div>
  1132. ${a.best_post ? `<div style="padding:10px;background:rgba(255,255,255,.02);border-radius:6px;border-left:2px solid ${TT_CYAN};">
  1133. <div class="text-xs" style="color:var(--text-3);letter-spacing:.06em;font-family:'JetBrains Mono';margin-bottom:6px;">BEST POST</div>
  1134. <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>
  1135. <div class="text-xs text-mono" style="margin-top:6px;color:var(--text-3);display:flex;gap:10px;">
  1136. <span style="color:${TT_PINK};">♥ ${fmtNum(a.best_post.digg)}</span>
  1137. <span>▶ ${fmtNum(a.best_post.play)}</span>
  1138. </div>
  1139. </div>` : `<div class="text-xs" style="color:var(--text-3);padding:10px;background:rgba(255,255,255,.02);border-radius:6px;">暂无 Best Post 数据</div>`}
  1140. </div>`).join('')}
  1141. </div>` : '';
  1142. // --- Top Videos 网格 ---
  1143. const topVideosBlock = topVids.length ? `
  1144. <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>
  1145. <div class="grid grid-3" style="margin-bottom:32px;gap:16px;">
  1146. ${topVids.slice(0, 9).map((v, i) => `<div class="card fade-up" style="border-left:3px solid ${i < 3 ? TT_PINK : TT_CYAN};">
  1147. <div style="display:flex;gap:10px;margin-bottom:10px;">
  1148. ${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>`}
  1149. <div style="flex:1;min-width:0;">
  1150. <div style="display:flex;gap:4px;align-items:center;margin-bottom:4px;">
  1151. <span class="text-mono text-xs" style="color:${i < 3 ? TT_PINK : TT_CYAN};font-weight:800;">#${String(i + 1).padStart(2, '0')}</span>
  1152. ${v._from_keyword ? `<span class="text-xs" style="color:var(--text-3);opacity:.7;">· ${esc(v._from_keyword)}</span>` : ''}
  1153. </div>
  1154. <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>
  1155. </div>
  1156. </div>
  1157. <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;">
  1158. <span style="color:${TT_PINK};">♥ ${fmtNum(v.digg)}</span>
  1159. <span style="color:var(--green);">💬 ${fmtNum(v.comment)}</span>
  1160. <span>▶ ${fmtNum(v.play)}</span>
  1161. <span style="color:var(--text-3);">↗ ${fmtNum(v.share)}</span>
  1162. </div>
  1163. ${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;">
  1164. <span style="color:${TT_CYAN};">@${esc(v.author.unique_id || v.author.nickname || '')}</span>
  1165. <span>· ${fmtNum(v.author.follower_count || 0)}粉</span>
  1166. ${v.author.custom_verify ? `<span style="color:var(--amber);">✓</span>` : ''}
  1167. </div>` : ''}
  1168. ${(v.hashtags || []).length ? `<div style="margin-top:6px;display:flex;flex-wrap:wrap;gap:4px;">
  1169. ${(v.hashtags || []).slice(0, 4).map((h) => `<span class="text-xs" style="color:${TT_CYAN};opacity:.8;">#${esc(h)}</span>`).join('')}
  1170. </div>` : ''}
  1171. </div>`).join('')}
  1172. </div>` : '';
  1173. // --- 评论墙 ---
  1174. const commentBlock = topCmts.length ? `
  1175. <h3 class="text-h2 rise-in" style="margin-top:12px;margin-bottom:18px;">📣 海外用户原声 Top ${Math.min(topCmts.length, 12)}</h3>
  1176. <div class="voice-wall" style="margin-bottom:32px;">
  1177. ${topCmts.slice(0, 12).map((c, i) => `<div class="voice-card ${i < 3 ? 'hot' : ''} fade-up">
  1178. <p class="voice-content">${esc(cleanText(c.text || ''))}</p>
  1179. <div class="voice-meta">
  1180. <span>${esc((c.user?.nickname || c.user?.unique_id || 'user').slice(0, 20))}${c.user?.region ? ' · ' + esc(c.user.region) : ''}</span>
  1181. <span class="voice-likes">♥ ${fmtNum(c.digg_count || 0)}</span>
  1182. </div>
  1183. </div>`).join('')}
  1184. </div>` : '';
  1185. // --- Hashtag 词云(Top List) ---
  1186. const hashtagBlock = topTags.length ? `
  1187. <div class="grid grid-2" style="margin-bottom:32px;gap:24px;">
  1188. <div class="card fade-up" style="border-left:3px solid ${TT_CYAN};">
  1189. <div class="text-mono text-xs" style="color:${TT_CYAN};letter-spacing:.12em;">HASHTAG FREQUENCY</div>
  1190. <h3 class="text-h3" style="margin-top:8px;margin-bottom:14px;">Top ${Math.min(topTags.length, 16)} 热门标签</h3>
  1191. <div style="display:flex;flex-wrap:wrap;gap:8px;">
  1192. ${topTags.slice(0, 16).map((t, i) => {
  1193. const size = i < 3 ? '1.1rem' : i < 8 ? '.95rem' : '.85rem';
  1194. const opacity = i < 3 ? 1 : 0.85;
  1195. const color = i < 3 ? TT_PINK : TT_CYAN;
  1196. 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};">
  1197. #${esc(t.tag)} <span style="color:var(--text-3);font-size:.7rem;margin-left:4px;">${t.count}</span>
  1198. </span>`;
  1199. }).join('')}
  1200. </div>
  1201. </div>
  1202. <div class="card fade-up" style="border-left:3px solid var(--amber);">
  1203. <div class="text-mono text-xs text-amber" style="letter-spacing:.12em;">REGION MIX</div>
  1204. <h3 class="text-h3" style="margin-top:8px;margin-bottom:14px;">创作者地区分布</h3>
  1205. ${topRegions.length ? `<div style="display:flex;flex-direction:column;gap:8px;">
  1206. ${topRegions.map(([r, c]) => {
  1207. const pct = Math.round((c / (m.totalVideos || 1)) * 100);
  1208. return `<div style="display:grid;grid-template-columns:90px 1fr auto;gap:10px;align-items:center;">
  1209. <div class="text-xs text-mono" style="color:var(--text-2);">${esc(r)}</div>
  1210. <div style="background:rgba(255,255,255,.06);border-radius:4px;height:10px;overflow:hidden;">
  1211. <div style="width:${pct}%;height:100%;background:${TT_CYAN};border-radius:4px;"></div>
  1212. </div>
  1213. <div class="text-xs text-mono" style="color:var(--text-3);min-width:60px;text-align:right;">${c} · ${pct}%</div>
  1214. </div>`;
  1215. }).join('')}
  1216. </div>` : `<p class="text-sm" style="color:var(--text-3);">无地区数据</p>`}
  1217. </div>
  1218. </div>` : '';
  1219. // --- 品类头部账号(从 user search 获取) ---
  1220. const catUserBlock = topCatUsers.length ? `
  1221. <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>
  1222. <div class="grid grid-3" style="margin-bottom:32px;gap:12px;">
  1223. ${topCatUsers.map((u) => `<div class="card fade-up" style="padding:14px;">
  1224. <div style="display:flex;gap:10px;align-items:center;">
  1225. ${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>`}
  1226. <div style="flex:1;min-width:0;">
  1227. <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>
  1228. <div class="text-xs text-mono" style="color:${TT_CYAN};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">@${esc(u.unique_id || '')}</div>
  1229. </div>
  1230. <div style="text-align:right;flex-shrink:0;">
  1231. <div class="text-xs text-mono" style="color:${TT_CYAN};font-weight:800;">${fmtNum(u.follower_count || 0)}</div>
  1232. <div class="text-xs" style="color:var(--text-3);font-size:.65rem;">粉丝</div>
  1233. </div>
  1234. </div>
  1235. </div>`).join('')}
  1236. </div>` : '';
  1237. return `<section class="report-section" id="sttk" data-tone="cyan" data-slide-num="${slideNum}/${slideTotal}">
  1238. <div class="section-inner">
  1239. <div class="section-head rise-in">
  1240. <div class="section-eyebrow" style="color:${TT_CYAN};">TT · TikTok Presence</div>
  1241. <h2 class="text-h1"><span class="headline-anim">TikTok 海外阵地</span></h2>
  1242. <p class="section-sub">Amazon 的内容端对标 · <strong style="color:${TT_CYAN};">${keywords.map((k) => esc(k)).join(' / ')}</strong> · 不需 cookie 直通海外版</p>
  1243. </div>
  1244. ${heroMetrics}
  1245. ${keywordCoverage}
  1246. ${authorsBlock}
  1247. ${topVideosBlock}
  1248. ${commentBlock}
  1249. ${hashtagBlock}
  1250. ${catUserBlock}
  1251. ${renderTTAdsBlock({ A })}
  1252. </div>
  1253. </section>`;
  1254. }
  1255. // ============================================================
  1256. // Market Intel · TikTok Ads 官方情报 block(注入 TikTok slide 尾部)
  1257. // ============================================================
  1258. function renderTTAdsBlock({ A }) {
  1259. const mi = A.marketIntel;
  1260. if (!mi || mi.empty || !mi.ttAds?.hasData) return '';
  1261. const { insights, relatedKeywords, relatedFromKeyword } = mi.ttAds;
  1262. const TT_CYAN = '#25F4EE';
  1263. const TT_PINK = '#FE2C55';
  1264. // Insight 卡:每个 keyword 1 张
  1265. const insightsCards = insights.length ? `
  1266. <div class="grid grid-${Math.min(insights.length, 3)}" style="margin-bottom:24px;gap:18px;">
  1267. ${insights.map((ins, i) => {
  1268. const postChange = ins.post_change || 0;
  1269. const trendColor = postChange > 50 ? '#22c55e' : (postChange > 0 ? TT_CYAN : '#f97316');
  1270. const trendArrow = postChange > 0 ? '▲' : (postChange < 0 ? '▼' : '●');
  1271. return `<div class="card card-glow fade-up" style="border-left:3px solid ${TT_PINK};">
  1272. <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
  1273. <div class="text-mono text-xs" style="color:${TT_PINK};letter-spacing:.12em;">AD KEYWORD #${i + 1}</div>
  1274. <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>
  1275. </div>
  1276. <h3 class="text-h3" style="margin:0 0 14px;font-family:'JetBrains Mono';font-size:1.1rem;color:${TT_CYAN};">"${esc(ins.keyword)}"</h3>
  1277. <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:14px;">
  1278. <div style="padding:10px;background:rgba(255,255,255,.03);border-radius:6px;">
  1279. <div class="text-xs text-mono" style="color:var(--text-3);letter-spacing:.06em;">IMPRESSIONS</div>
  1280. <div style="font-family:'JetBrains Mono';font-size:1.4rem;font-weight:900;color:${TT_CYAN};margin-top:4px;">${fmtNum(ins.impression)}</div>
  1281. </div>
  1282. <div style="padding:10px;background:rgba(255,255,255,.03);border-radius:6px;">
  1283. <div class="text-xs text-mono" style="color:var(--text-3);letter-spacing:.06em;">POST CHANGE</div>
  1284. <div style="font-family:'JetBrains Mono';font-size:1.4rem;font-weight:900;color:${trendColor};margin-top:4px;">${trendArrow} ${(postChange || 0).toFixed(1)}%</div>
  1285. </div>
  1286. <div style="padding:10px;background:rgba(255,255,255,.03);border-radius:6px;">
  1287. <div class="text-xs text-mono" style="color:var(--text-3);letter-spacing:.06em;">CTR</div>
  1288. <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>
  1289. </div>
  1290. <div style="padding:10px;background:rgba(255,255,255,.03);border-radius:6px;">
  1291. <div class="text-xs text-mono" style="color:var(--text-3);letter-spacing:.06em;">CVR</div>
  1292. <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>
  1293. </div>
  1294. </div>
  1295. <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);">
  1296. <span>📝 ${fmtNum(ins.post)} posts</span>
  1297. <span style="color:${TT_PINK};">♥ ${fmtNum(ins.like)}</span>
  1298. <span>💬 ${fmtNum(ins.comment)}</span>
  1299. <span>↗ ${fmtNum(ins.share)}</span>
  1300. <span>💰 ${fmtNum(ins.cost)}</span>
  1301. </div>
  1302. </div>`;
  1303. }).join('')}
  1304. </div>` : '';
  1305. // Related keywords 词云:按 score 渐变字号
  1306. const relatedBlock = relatedKeywords.length ? `
  1307. <div class="card fade-up" style="margin-bottom:24px;border-left:3px solid ${TT_CYAN};">
  1308. <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
  1309. <div class="text-mono text-xs" style="color:${TT_CYAN};letter-spacing:.12em;">RELATED KEYWORD NETWORK</div>
  1310. <span class="text-xs" style="color:var(--text-3);">Top ${relatedKeywords.length} · 来源: "${esc(relatedFromKeyword)}"</span>
  1311. </div>
  1312. <h3 class="text-h3" style="margin:0 0 14px;">TikTok Ads 官方相关词图谱</h3>
  1313. <div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
  1314. ${relatedKeywords.map((r, i) => {
  1315. const pct = (r.score || 0) / 100;
  1316. const fontSize = (0.8 + pct * 0.6).toFixed(2); // 0.8~1.4rem
  1317. const color = i < 3 ? TT_PINK : (i < 10 ? TT_CYAN : 'var(--text-2)');
  1318. const opacity = (0.6 + pct * 0.4).toFixed(2);
  1319. 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};">
  1320. ${esc(r.name)} <span style="color:var(--text-3);font-size:.65rem;margin-left:4px;">${r.score}</span>
  1321. </span>`;
  1322. }).join('')}
  1323. </div>
  1324. </div>` : '';
  1325. return `
  1326. <div style="margin-top:32px;padding-top:24px;border-top:1px solid rgba(37,244,238,.15);">
  1327. <div class="section-eyebrow rise-in" style="color:${TT_PINK};">TT · OFFICIAL MARKET INTEL</div>
  1328. <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>
  1329. ${insightsCards}
  1330. ${relatedBlock}
  1331. </div>`;
  1332. }
  1333. // ============================================================
  1334. // Market Intel · Douyin 全站热榜基线 block(注入 Douyin slide 顶部)
  1335. // ============================================================
  1336. function renderDouyinTrendsBlock({ A }) {
  1337. const mi = A.marketIntel;
  1338. if (!mi || mi.empty || !mi.douyinGlobal?.hasGlobal) return '';
  1339. const { hotWords, webHotSearch, hotTopics, categoryHitTotal, filterKeywords } = mi.douyinGlobal;
  1340. const DY_PINK = '#FF3E75';
  1341. const hotWordsBlock = hotWords.length ? `
  1342. <div class="card fade-up" style="border-left:3px solid ${DY_PINK};">
  1343. <div class="text-mono text-xs" style="color:${DY_PINK};letter-spacing:.12em;">HOT WORD BILLBOARD</div>
  1344. <div class="text-xs" style="color:var(--text-3);margin-top:4px;">抖音星图 · 近 24h 热词 Top 6</div>
  1345. <div style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
  1346. ${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);">
  1347. <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>
  1348. <span class="text-sm" style="color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(w.title)}</span>
  1349. <span class="text-xs text-mono" style="color:${DY_PINK};font-weight:700;">${fmtNum(w.score)}</span>
  1350. </div>`).join('')}
  1351. </div>
  1352. </div>` : '';
  1353. const webHotBlock = webHotSearch.length ? `
  1354. <div class="card fade-up" style="border-left:3px solid var(--amber);">
  1355. <div class="text-mono text-xs text-amber" style="letter-spacing:.12em;">WEB HOT SEARCH</div>
  1356. <div class="text-xs" style="color:var(--text-3);margin-top:4px;">抖音 Web 首页热搜 Top 6</div>
  1357. <div style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
  1358. ${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);">
  1359. <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>
  1360. <span class="text-sm" style="color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(w.word)}</span>
  1361. <span class="text-xs text-mono" style="color:var(--amber);font-weight:700;">${fmtNum(w.view_count)}</span>
  1362. </div>`).join('')}
  1363. </div>
  1364. </div>` : '';
  1365. const hotTopicsBlock = hotTopics.length ? `
  1366. <div class="card fade-up" style="border-left:3px solid var(--green);">
  1367. <div class="text-mono text-xs text-green" style="letter-spacing:.12em;">HOT TOPIC CHALLENGE</div>
  1368. <div class="text-xs" style="color:var(--text-3);margin-top:4px;">抖音热挑战 Top 5</div>
  1369. <div style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
  1370. ${hotTopics.map((t, i) => `<div style="padding:8px;background:rgba(255,255,255,.02);border-radius:6px;">
  1371. <div style="display:flex;justify-content:space-between;gap:8px;align-items:center;">
  1372. <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>
  1373. <span class="text-mono" style="color:var(--green);font-weight:800;font-size:.75rem;white-space:nowrap;">▶ ${fmtNum(t.play_cnt)}</span>
  1374. </div>
  1375. <div class="text-xs text-mono" style="color:var(--text-3);margin-top:3px;">📝 ${fmtNum(t.publish_cnt)} 篇参与</div>
  1376. </div>`).join('')}
  1377. </div>
  1378. </div>` : '';
  1379. const categoryNote = filterKeywords.length ? `
  1380. <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;">
  1381. <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> 条相关内容 —
  1382. ${categoryHitTotal === 0 ? '说明本品类在抖音全站 Top 榜未形成内容沉淀,是<strong class="text-amber">品牌蓝海</strong>:现有头部创作者尚未进入健康/滋补赛道。' : '说明本品类已有热点渗透,可顺势挂载热话题做内容承接。'}
  1383. </div>` : '';
  1384. return `
  1385. <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);">
  1386. <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
  1387. <div>
  1388. <div class="section-eyebrow" style="color:${DY_PINK};">DOUYIN · CONTENT ECOSYSTEM BASELINE</div>
  1389. <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>
  1390. </div>
  1391. <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>
  1392. </div>
  1393. <div class="grid grid-3" style="gap:14px;">
  1394. ${hotWordsBlock}
  1395. ${webHotBlock}
  1396. ${hotTopicsBlock}
  1397. </div>
  1398. ${categoryNote}
  1399. </div>`;
  1400. }
  1401. function buildHTML({ product, category, collectedAt, A, themes }) {
  1402. const amz = A.amazon || {};
  1403. const hasDouyin = A.douyin && !A.douyin.empty;
  1404. const hasTikTok = A.tiktok && !A.tiktok.empty;
  1405. // 鲜活的星级分布:把每款产品的 5 星分布转成 ECharts 友好结构
  1406. const starDistProducts = Object.values(amz.reviewsSummary || {}).map((r) => ({
  1407. asin: r.asin,
  1408. brand: r.brand || 'N/A',
  1409. title: (r.title || '').slice(0, 28),
  1410. stars: [5, 4, 3, 2, 1].map((s) => r.starDist[s] || 0),
  1411. }));
  1412. // 抖音作品散点
  1413. const douyinPosts = hasDouyin ? (A.douyin.topPosts || []).concat(
  1414. (A.douyin.userPosts || []).slice(0, 20).map((v) => ({
  1415. aweme_id: v.aweme_id, desc: v.desc,
  1416. digg: v.statistics?.digg_count || 0,
  1417. comment: v.statistics?.comment_count || 0,
  1418. share: v.statistics?.share_count || 0,
  1419. play: v.statistics?.play_count || 0,
  1420. }))
  1421. ).filter((v, i, arr) => arr.findIndex((x) => x.aweme_id === v.aweme_id) === i).slice(0, 30) : [];
  1422. const chartData = {
  1423. priceBuckets: amz.priceBuckets || A.priceBuckets || {},
  1424. topBrands: (A.topBrands || []).slice(0, 8),
  1425. topBrandShares: (amz.topBrands || []).slice(0, 8).map((b) => ({
  1426. name: b.brand, value: +b.share.toFixed(1), sales: b.sales, count: b.count,
  1427. })),
  1428. ratingBuckets: amz.ratingBuckets || {},
  1429. reviewCountBuckets: amz.reviewCountBuckets || {},
  1430. starDistProducts,
  1431. topIPs: A.topIPs || [],
  1432. noteHeatmap: (A.topNotes || []).slice(0, 20).map((n) => ({
  1433. title: cleanText(n.title || '').slice(0, 22),
  1434. liked: n.liked_count || 0,
  1435. comments: n.comments_count || 0,
  1436. collected: n.collected_count || 0,
  1437. })),
  1438. douyinPosts,
  1439. };
  1440. // Slide 编号(基础 10 张 + TikTok/抖音 各占一张)
  1441. const slideTotal = 10 + (hasTikTok ? 1 : 0) + (hasDouyin ? 1 : 0);
  1442. const sn = (i) => String(i).padStart(2, '0');
  1443. let n = 1;
  1444. const s0 = sn(n++), s1 = sn(n++), s2 = sn(n++), s3 = sn(n++), s4 = sn(n++);
  1445. const s5 = sn(n++), s6 = sn(n++);
  1446. const sttk = hasTikTok ? sn(n++) : '';
  1447. const sdy = hasDouyin ? sn(n++) : '';
  1448. const s7 = sn(n++), s8 = sn(n++), s9 = sn(n++);
  1449. const ST = String(slideTotal).padStart(2, '0');
  1450. return `<!DOCTYPE html>
  1451. <html lang="zh-CN">
  1452. <head>
  1453. <meta charset="UTF-8">
  1454. <meta name="viewport" content="width=device-width,initial-scale=1.0">
  1455. <title>${esc(product)} · VOC 洞察报告</title>
  1456. <script>document.documentElement.classList.add('js');</script>
  1457. <!-- 本地离线字体(同目录 assets/fonts/) · 双击 HTML 即可用,无需网络 -->
  1458. <link href="./assets/fonts/fonts.css" rel="stylesheet">
  1459. <!-- 在线 fallback:仅当本地字体缺失时异步加载(不阻塞首屏) -->
  1460. <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'">
  1461. <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
  1462. ${STYLES}
  1463. </head>
  1464. <body>
  1465. ${renderNav(product, hasDouyin, hasTikTok)}
  1466. <div class="slide-counter" id="slideCounter">
  1467. <span class="now">${s0}</span>
  1468. <span>/</span>
  1469. <span class="total">${ST}</span>
  1470. <span class="bar"></span>
  1471. </div>
  1472. <div class="kbd-hint" id="kbdHint">
  1473. <kbd>↓</kbd> / <kbd>Space</kbd>
  1474. <span style="margin:0 4px;opacity:.5;">|</span>
  1475. <span style="opacity:.7;">滚动或按键切换</span>
  1476. </div>
  1477. <main class="deck">
  1478. ${renderS0Cover({ product, category, collectedAt, A, slideNum: s0, slideTotal: ST })}
  1479. ${renderS1Summary({ A, slideNum: s1, slideTotal: ST })}
  1480. ${renderS2Insights({ A, themes, slideNum: s2, slideTotal: ST })}
  1481. ${renderS3Notes({ A, slideNum: s3, slideTotal: ST })}
  1482. ${renderS4Voices({ A, slideNum: s4, slideTotal: ST })}
  1483. ${renderS5KOL({ A, slideNum: s5, slideTotal: ST })}
  1484. ${renderS6Amazon({ A, slideNum: s6, slideTotal: ST })}
  1485. ${hasTikTok ? renderSTikTok({ A, slideNum: sttk, slideTotal: ST }) : ''}
  1486. ${hasDouyin ? renderSDouyin({ A, slideNum: sdy, slideTotal: ST }) : ''}
  1487. ${renderS7Themes({ themes, slideNum: s7, slideTotal: ST })}
  1488. ${renderS8Matrix({ themes, slideNum: s8, slideTotal: ST })}
  1489. ${renderS9Action({ A, themes, slideNum: s9, slideTotal: ST })}
  1490. </main>
  1491. ${renderFooter()}
  1492. <script>
  1493. window.__CHART_DATA__ = ${JSON.stringify(chartData)};
  1494. ${SCRIPTS}
  1495. </script>
  1496. </body>
  1497. </html>`;
  1498. }
  1499. module.exports = { buildHTML };