gen-majiyong-closing-render.js 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  1. /**
  2. * 马记永结案报告 · HTML + MD 渲染层
  3. */
  4. const fs = require('fs');
  5. const path = require('path');
  6. const ROOT = path.resolve(__dirname, '..');
  7. const OUT_HTML = path.join(ROOT, 'majiyong-closing-report.html');
  8. const OUT_MD = path.join(ROOT, 'majiyong-closing-report.md');
  9. module.exports = function render(M) {
  10. const { items, kolItems, kocItems, summary, tierStats, typeStats, topRegions, byTier,
  11. allComments, noiseComments, noiseRate, topicHits, TOPICS, wordcloudComments, cloudBrand, cloudActivity, cloudCategory,
  12. PROS, CONS, RECS, OPPORTUNITIES, TOP5,
  13. fmt, pct, esc, num } = M;
  14. // ---------- 明细行 (注: 篇数大,做 KOL 全表 + KOC Top 50) ----------
  15. const detailKolRows = [...kolItems].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement)).map((r, i) => `
  16. <tr>
  17. <td class="num">${i + 1}</td>
  18. <td>${esc(r.name)}</td>
  19. <td><span class="tag">${esc(r.tier || '-')}</span></td>
  20. <td><span class="tag gray">${esc(r.contentType || '-')}</span></td>
  21. <td><span class="tag gray">${esc(String(r.region || '-').slice(0, 3))}</span></td>
  22. <td class="num mono">${(num(r.followersWan) * 10000).toLocaleString('zh-CN')}</td>
  23. <td class="num mono">¥${fmt(r.priceOrder)}</td>
  24. <td class="num mono">${fmt(r.impressionsSubtotal || r.impressions)}</td>
  25. <td class="num mono">${fmt(r.likes)}</td>
  26. <td class="num mono">${fmt(r.comments)}</td>
  27. <td class="num mono ${num(r.engagementSubtotal || r.engagement) === 0 ? 'dim' : ''}">${fmt(r.engagementSubtotal || r.engagement)}</td>
  28. <td>${r.noteId ? '<span class="tag green">已解析</span>' : '<span class="tag rose">短链失效</span>'}</td>
  29. </tr>`).join('');
  30. const detailKocTop = [...kocItems].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement)).slice(0, 50).map((r, i) => `
  31. <tr>
  32. <td class="num">${i + 1}</td>
  33. <td>${esc(r.name)}</td>
  34. <td><span class="tag gray">${esc(r.tier || 'KOC')}</span></td>
  35. <td><span class="tag gray">${esc(r.contentType || '-')}</span></td>
  36. <td><span class="tag gray">${esc(String(r.region || '-').slice(0, 3))}</span></td>
  37. <td class="num mono">${(num(r.followersWan) * 10000).toLocaleString('zh-CN')}</td>
  38. <td class="num mono">${fmt(r.likes)}</td>
  39. <td class="num mono">${fmt(r.comments)}</td>
  40. <td class="num mono ${num(r.engagementSubtotal || r.engagement) === 0 ? 'dim' : ''}">${fmt(r.engagementSubtotal || r.engagement)}</td>
  41. <td>${(r._comments && r._comments.length) || 0}</td>
  42. </tr>`).join('');
  43. const topicRows = TOPICS.map((t) => {
  44. const hits = topicHits[t.tag];
  45. const sample = hits.sort((a, b) => b.like - a.like)[0];
  46. return `<tr>
  47. <td><span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:${t.color}; margin-right:8px; vertical-align:middle"></span>${esc(t.tag)}</td>
  48. <td class="num mono">${hits.length}</td>
  49. <td class="num mono">${(hits.length / Math.max(allComments.length, 1) * 100).toFixed(1)}%</td>
  50. <td class="sample">${sample ? `<span class="text-xs text-3">[${esc(sample.koc)}@${esc(sample.ip || '-')}]</span> ${esc(sample.content.replace(/\n/g, ' ').slice(0, 80))}${sample.content.length > 80 ? '…' : ''}` : '<span class="text-3">-</span>'}</td>
  51. </tr>`;
  52. }).join('');
  53. // 真实评论筛选 v4: 7 层过滤 + 产品强相关硬约束 + 主题多样性轮询
  54. const NOISE_RE = /互赞|互助|加油.*互|可以.*点赞|点赞.*返|蹲.*搭子|互粉/;
  55. const OFFTOPIC_RE = /眼影|口红|睫毛|化妆|包包|分装|借我|借用|手能借|护肤|穿搭|开箱|美术生|画的|画得|画风|笔法|插画|手工|盒子.*像/;
  56. const PROMO_RE = /推广|求关注|求互|私信|加微|商务合作|接广告|涨粉/;
  57. const SHILL_RE = /宝子|亲测|无套路|必.{0,2}冲|绝绝子|拉满|薅羊毛|薅得|放心冲|闭眼冲|爆棚|颜值直接|快冲|姐妹.{0,3}冲|实力宠粉|超(级|绝|香)|爆款|出圈|0元购|0元薅|真香|yyds|YYDS|惊艳到|跺脚/;
  58. const META_KOL_RE = /博主|up主|UP主|作者|接广|涨粉|掉粉|播放|粉丝.*多|9000.*掉.*\d|完啦R/;
  59. const EMOJI_HEAVY_RE = /(🥬|🥢|🌿|🤩|💯|🔥|💥|✅|💦|📸|🥳|🥞|🛒|🍣|🎉|✨|💚|❗|‼️)/g;
  60. const stripDeco = (s) => s.replace(/\[[^\]]+\]/g, '').replace(/[@#][^\s@#]+/g, '').replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '');
  61. // 产品强相关词典: 9 主题,评论必须命中至少 1 个才算 VOC 有效
  62. const PRODUCT_TOPICS = {
  63. '口味体验': /好吃|难吃|味道|香|辣|咸|淡|腻|清爽|清甜|甜杆|西兰|拌|脆|嫩|爽口|入味/,
  64. '产品创新': /新菜|新鲜|鲜菜|凉拌|凉菜|拌菜|不过夜|当天|现做|现拌|出.{0,3}菜|上.{0,3}菜/,
  65. '门店开店': /开店|分店|开到|什么时候.*开|建议.*开|我们这.*没|这边.*没|本地.*有|哪里有店|店在哪/,
  66. '健康营养': /健康|减脂|低脂|营养|蔬菜|清淡|0负担|低卡/,
  67. '价格性价比': /便宜|划算|实惠|贵|价格|性价比|套餐|¥/,
  68. '活动机制': /换菜|换券|换免费|带.*换|带.*能换|花.*换|报纸|活动.*介绍|活动.*在哪|怎么参与|怎么换|领券|领劵/,
  69. '场景外带': /外带|打包|下酒|带回家|带走|外卖|食堂|带娃|带朋友/,
  70. '复购推荐': /常去|经常.*吃|经常.*去|每次|又来|回头|推荐|尝尝|去试|去尝/,
  71. '品牌情感': /马记永.*好|马记永.*爱|爱.*马记永|喜欢.*马记永|马记永.*绝/,
  72. };
  73. const PRODUCT_RE = new RegExp(Object.values(PRODUCT_TOPICS).map((r) => r.source).join('|'));
  74. const realComments = allComments.filter((c) => {
  75. const txt = c.content || '';
  76. if (c.nick && c.koc && c.nick === c.koc) return false; // 博主自评
  77. // @ 别人开头的回复 = 缺乏上下文 (除非内容本身够长够独立)
  78. if (/^\s*@/.test(txt) && stripDeco(txt.replace(/^\s*@[^\s@#]+/, '')).length < 20) return false;
  79. if (NOISE_RE.test(txt) || OFFTOPIC_RE.test(txt) || PROMO_RE.test(txt) || SHILL_RE.test(txt) || META_KOL_RE.test(txt)) return false;
  80. const emojis = txt.match(EMOJI_HEAVY_RE) || [];
  81. if (emojis.length >= 3) return false;
  82. const core = stripDeco(txt);
  83. if (core.length < 6) return false;
  84. if (core.length > 80 && emojis.length >= 1) return false;
  85. if (core.length > 120) return false;
  86. if (!PRODUCT_RE.test(txt)) return false; // 硬约束: 必须产品相关
  87. return true;
  88. });
  89. // 按主题分桶 (只归首个匹配主题)
  90. const buckets = {};
  91. for (const tag of Object.keys(PRODUCT_TOPICS)) buckets[tag] = [];
  92. for (const c of realComments) {
  93. for (const [tag, re] of Object.entries(PRODUCT_TOPICS)) {
  94. if (re.test(c.content)) { buckets[tag].push(c); break; }
  95. }
  96. }
  97. // 排序: 点赞降序;同点赞按"信息量"降序 (产品关键词命中数 + 内容长度)
  98. const informationScore = (c) => {
  99. let hits = 0;
  100. for (const re of Object.values(PRODUCT_TOPICS)) if (re.test(c.content)) hits++;
  101. const core = stripDeco(c.content);
  102. return hits * 20 + core.length;
  103. };
  104. for (const tag of Object.keys(buckets)) {
  105. buckets[tag].sort((a, b) => (b.like - a.like) || (informationScore(b) - informationScore(a)));
  106. }
  107. // 主题轮询: 先每主题 Top 2,补到 16
  108. const topPicks = [];
  109. const queues = Object.entries(buckets).map(([tag, list]) => ({ tag, list, idx: 0 }));
  110. for (let round = 0; round < 4 && topPicks.length < 16; round++) {
  111. for (const q of queues) {
  112. if (topPicks.length >= 16) break;
  113. if (q.idx < q.list.length && q.idx < round + 1) {
  114. topPicks.push({ topic: q.tag, c: q.list[q.idx] });
  115. q.idx++;
  116. }
  117. }
  118. }
  119. realComments.length = 0;
  120. realComments.push(...topPicks.map((p) => p.c));
  121. const realCommentList = topPicks.map(({ topic, c }) => `
  122. <div class="quote">
  123. <div class="quote-meta"><span class="tag green">${esc(topic)}</span> <span class="tag gray">${esc(c.koc)}</span> <span class="text-xs text-3">${esc(c.ip || '')}</span></div>
  124. <div class="quote-text">${esc(c.content.replace(/\n/g, ' '))}</div>
  125. </div>`).join('');
  126. const HTML = `<!DOCTYPE html>
  127. <html lang="zh-CN">
  128. <head>
  129. <meta charset="UTF-8"/>
  130. <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  131. <title>马记永·四月「新鲜菜」KOL/KOC 宣发结案报告</title>
  132. <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  133. <script src="https://cdn.jsdelivr.net/npm/echarts-wordcloud@2.1.0/dist/echarts-wordcloud.min.js"></script>
  134. <style>
  135. :root {
  136. --bg-0: #0A0A0A; --bg-1: #111111; --bg-2: #1A1A1A; --bg-3: #252525;
  137. --border: rgba(255,255,255,0.08); --border-strong: rgba(255,255,255,0.16);
  138. --text-1: #F5F5F5; --text-2: #A8A8A8; --text-3: #6B6B6B;
  139. --amber: #F5A623; --green: #00DC82; --purple: #8B5CF6; --rose: #FF4D8D; --blue: #3B82F6; --red: #EF4444;
  140. --font-head: 'Inter','Noto Sans SC','PingFang SC','Microsoft YaHei',system-ui,sans-serif;
  141. --font-body: 'Noto Sans SC','Inter','PingFang SC','Microsoft YaHei',system-ui,sans-serif;
  142. --font-mono: 'JetBrains Mono','SF Mono',Consolas,monospace;
  143. }
  144. * { margin: 0; padding: 0; box-sizing: border-box; }
  145. html { scroll-behavior: smooth; }
  146. body { font-family: var(--font-body); background: var(--bg-0); color: var(--text-1); line-height: 1.65; -webkit-font-smoothing: antialiased; overflow-x: hidden; }
  147. body::before { content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 0;
  148. background: radial-gradient(ellipse 900px 700px at 20% 25%, rgba(245,166,35,0.08), transparent 60%),
  149. radial-gradient(ellipse 800px 600px at 80% 75%, rgba(0,220,130,0.06), transparent 60%); }
  150. main { position: relative; z-index: 1; }
  151. ::selection { background: var(--amber); color: #000; }
  152. section { min-height: 100vh; padding: clamp(60px, 10vh, 120px) clamp(24px, 6vw, 80px); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; justify-content: center; position: relative; }
  153. section .inner { width: 100%; max-width: 1280px; margin: 0 auto; }
  154. section::before { content: attr(data-num); position: absolute; top: 32px; right: 60px; font-family: var(--font-mono); font-weight: 600; font-size: 0.78rem; letter-spacing: 0.25em; color: var(--text-3); }
  155. .eyebrow { display: inline-flex; align-items: center; gap: 10px; font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--amber); font-weight: 500; margin-bottom: 18px; }
  156. .eyebrow::before { content: ''; display: inline-block; width: 24px; height: 1px; background: var(--amber); }
  157. .h0 { font-family: var(--font-head); font-weight: 900; letter-spacing: -0.04em; line-height: 0.95; font-size: clamp(3rem, 8vw, 6rem); }
  158. .h1 { font-family: var(--font-head); font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; font-size: clamp(2rem, 4vw, 3rem); margin-bottom: 16px; }
  159. .h2 { font-family: var(--font-head); font-weight: 700; font-size: clamp(1.4rem, 2.4vw, 1.8rem); margin: 32px 0 16px; }
  160. .lead { color: var(--text-2); font-size: 1.05rem; max-width: 720px; margin-top: 18px; line-height: 1.8; }
  161. .sub { color: var(--text-2); margin-bottom: 32px; max-width: 800px; }
  162. .text-3 { color: var(--text-3); } .text-xs { font-size: 0.74rem; } .text-sm { font-size: 0.85rem; } .mono { font-family: var(--font-mono); } .dim { color: var(--text-3); }
  163. .grid { display: grid; gap: 22px; }
  164. .grid-2 { grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); }
  165. .grid-3 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
  166. .grid-4 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
  167. .card { background: linear-gradient(180deg, rgba(255,255,255,0.025) 0%, rgba(255,255,255,0.005) 100%), var(--bg-1); border: 1px solid var(--border); border-radius: 12px; padding: 28px; transition: transform 0.3s, border-color 0.3s; }
  168. .card:hover { transform: translateY(-3px); border-color: var(--border-strong); }
  169. .card-accent { border-left: 3px solid var(--amber); }
  170. .card-green { border-left: 3px solid var(--green); }
  171. .card-rose { border-left: 3px solid var(--rose); }
  172. .card-blue { border-left: 3px solid var(--blue); }
  173. .metric { background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; padding: 24px; position: relative; overflow: hidden; min-width: 0; }
  174. .metric::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, var(--amber), transparent); opacity: 0.6; }
  175. .metric-label { font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--text-3); margin-bottom: 10px; }
  176. .metric-value { font-family: var(--font-head); font-weight: 800; font-size: clamp(1.6rem, 2.6vw, 2.3rem); letter-spacing: -0.03em; line-height: 1.05; word-break: break-all; }
  177. .metric-unit { font-family: var(--font-mono); font-size: 0.85rem; color: var(--amber); font-weight: 500; margin-left: 4px; }
  178. .metric-sub { margin-top: 14px; font-size: 0.82rem; color: var(--text-2); line-height: 1.5; }
  179. .tag { display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 4px; font-size: 0.72rem; font-weight: 500; background: rgba(245,166,35,0.1); color: var(--amber); border: 1px solid rgba(245,166,35,0.22); }
  180. .tag.green { background: rgba(0,220,130,0.08); color: var(--green); border-color: rgba(0,220,130,0.2); }
  181. .tag.purple { background: rgba(139,92,246,0.08); color: var(--purple); border-color: rgba(139,92,246,0.2); }
  182. .tag.rose { background: rgba(255,77,141,0.08); color: var(--rose); border-color: rgba(255,77,141,0.2); }
  183. .tag.blue { background: rgba(59,130,246,0.08); color: var(--blue); border-color: rgba(59,130,246,0.2); }
  184. .tag.gray { background: rgba(255,255,255,0.04); color: var(--text-2); border-color: var(--border); }
  185. table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
  186. th, td { padding: 10px 12px; border-bottom: 1px solid var(--border); text-align: left; }
  187. th { color: var(--text-3); font-weight: 500; font-size: 0.74rem; letter-spacing: 0.06em; text-transform: uppercase; background: var(--bg-2); position: sticky; top: 0; z-index: 1; }
  188. td.num, th.num { text-align: right; }
  189. tr:hover td { background: rgba(255,255,255,0.025); }
  190. td.sample { color: var(--text-2); font-size: 0.82rem; max-width: 380px; }
  191. .scroll-table { max-height: 70vh; overflow: auto; border: 1px solid var(--border); border-radius: 12px; background: var(--bg-1); }
  192. .scroll-table::-webkit-scrollbar { width: 8px; height: 8px; }
  193. .scroll-table::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: 4px; }
  194. .chart { width: 100%; height: 360px; }
  195. .chart-tall { width: 100%; height: 460px; }
  196. .quote { background: var(--bg-1); border-left: 3px solid var(--amber); border-radius: 0 8px 8px 0; padding: 14px 18px; margin-bottom: 12px; }
  197. .quote-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; flex-wrap: wrap; }
  198. .quote-text { color: var(--text-1); font-size: 0.94rem; line-height: 1.65; }
  199. .rec-row { display: flex; gap: 18px; padding: 16px 0; border-bottom: 1px solid var(--border); align-items: flex-start; }
  200. .rec-row:last-child { border-bottom: none; }
  201. .rec-p { flex-shrink: 0; width: 48px; font-family: var(--font-mono); font-weight: 700; font-size: 0.9rem; padding: 6px 10px; border-radius: 4px; text-align: center; }
  202. .rec-p.P0 { background: rgba(255,77,141,0.1); color: var(--rose); border: 1px solid rgba(255,77,141,0.3); }
  203. .rec-p.P1 { background: rgba(245,166,35,0.1); color: var(--amber); border: 1px solid rgba(245,166,35,0.3); }
  204. .rec-p.P2 { background: rgba(139,92,246,0.1); color: var(--purple); border: 1px solid rgba(139,92,246,0.3); }
  205. .rec-content h4 { font-size: 1.05rem; margin-bottom: 6px; }
  206. .rec-content p { color: var(--text-2); font-size: 0.9rem; line-height: 1.65; }
  207. .cover { background: radial-gradient(ellipse 60% 80% at 30% 30%, rgba(245,166,35,0.12) 0%, transparent 70%), radial-gradient(ellipse 40% 60% at 80% 70%, rgba(0,220,130,0.06) 0%, transparent 70%), var(--bg-0); }
  208. .cover-meta { display: flex; gap: 32px; margin-top: 32px; flex-wrap: wrap; }
  209. .cover-meta-item { font-family: var(--font-mono); font-size: 0.85rem; color: var(--text-2); }
  210. .cover-meta-item strong { color: var(--text-1); font-weight: 600; display: block; margin-top: 4px; font-size: 1.1rem; }
  211. .toc { display: flex; gap: 14px; margin-top: 48px; flex-wrap: wrap; }
  212. .toc a { padding: 8px 16px; border: 1px solid var(--border); border-radius: 100px; color: var(--text-2); font-size: 0.85rem; transition: all 0.2s; cursor: pointer; text-decoration: none; }
  213. .toc a:hover { border-color: var(--amber); color: var(--amber); }
  214. @media (max-width: 768px) { section { padding: 80px 20px; min-height: auto; } .h0 { font-size: 2.5rem; } table { font-size: 0.74rem; } td, th { padding: 6px 8px; } }
  215. </style>
  216. </head>
  217. <body>
  218. <main>
  219. <!-- ============= 封面 ============= -->
  220. <section class="cover" data-num="00 / COVER">
  221. <div class="inner">
  222. <div class="eyebrow">MAJIYONG · 兰州牛肉面 · 新鲜菜项目 · 2026.04</div>
  223. <h1 class="h0">马记永「新鲜菜」<br/>KOL/KOC 宣发<br/><span style="color:var(--amber)">结案报告</span></h1>
  224. <p class="lead">基于 ${summary.total} 篇报备笔记 (KOL ${summary.kolCount} + KOC ${summary.kocCount}) + ${allComments.length} 条公开评论真实抓取,完整复盘投放规模、内容形态、互动效率、评论 VOC 与品牌词渗透,并给出下一轮可执行的优化建议。</p>
  225. <div class="cover-meta">
  226. <div class="cover-meta-item">投放期间<strong>2026-04</strong></div>
  227. <div class="cover-meta-item">总报备<strong>${summary.total} 篇</strong></div>
  228. <div class="cover-meta-item">KOL 投入<strong>¥${fmt(summary.kolSpend)}</strong></div>
  229. <div class="cover-meta-item">总曝光<strong>${fmt(summary.totalImpressions)}</strong></div>
  230. <div class="cover-meta-item">总互动<strong>${fmt(summary.totalEngagement)}</strong></div>
  231. <div class="cover-meta-item">真实评论<strong>${allComments.length} 条</strong></div>
  232. </div>
  233. <div class="toc">
  234. <a href="#sec-1">① 数据汇总</a>
  235. <a href="#sec-2">② 数据明细</a>
  236. <a href="#sec-3">③ 话题数据</a>
  237. <a href="#sec-4">④ 品牌相关词</a>
  238. <a href="#sec-5">⑤ 优点 / 不足 / 建议</a>
  239. <a href="#sec-6" style="border-color:var(--rose);color:var(--rose)">⑥ 新机会挖掘 ★</a>
  240. </div>
  241. </div>
  242. </section>
  243. <!-- ============= 1. 数据汇总 ============= -->
  244. <section id="sec-1" data-num="01 / SUMMARY">
  245. <div class="inner">
  246. <div class="eyebrow">Section 01</div>
  247. <h2 class="h1">数据汇总 — 投放规模与产出效率</h2>
  248. <p class="sub">本期 ${summary.total} 篇笔记 (KOL ${summary.kolCount} + KOC ${summary.kocCount}),KOL 投入预算 ¥${fmt(summary.kolSpend)},总曝光 ${fmt(summary.totalImpressions)} / 总互动 ${fmt(summary.totalEngagement)},整体互动率 ${pct(summary.engagementRate)},综合 CPE ¥${summary.cpe.toFixed(2)}。</p>
  249. <div class="grid grid-4">
  250. <div class="metric"><div class="metric-label">总报备笔记</div><div class="metric-value">${summary.total}<span class="metric-unit">篇</span></div><div class="metric-sub">KOL ${summary.kolCount} · KOC ${summary.kocCount}</div></div>
  251. <div class="metric"><div class="metric-label">KOL 总预算</div><div class="metric-value">¥${fmt(summary.kolSpend)}</div><div class="metric-sub">CPM ¥${summary.cpm.toFixed(2)}</div></div>
  252. <div class="metric"><div class="metric-label">总曝光</div><div class="metric-value">${fmt(summary.totalImpressions)}</div><div class="metric-sub">大盘心智渗透</div></div>
  253. <div class="metric"><div class="metric-label">总互动</div><div class="metric-value">${fmt(summary.totalEngagement)}</div><div class="metric-sub">CPE ¥${summary.cpe.toFixed(2)}</div></div>
  254. <div class="metric"><div class="metric-label">点赞 / 收藏</div><div class="metric-value">${fmt(summary.totalLikes)}<span class="metric-unit"> / ${fmt(summary.totalCollects)}</span></div><div class="metric-sub">收藏率 ${(summary.totalCollects/Math.max(summary.totalLikes,1)*100).toFixed(1)}%</div></div>
  255. <div class="metric"><div class="metric-label">互动率</div><div class="metric-value">${summary.engagementRate.toFixed(2)}<span class="metric-unit">%</span></div><div class="metric-sub">行业基准 3-5%</div></div>
  256. <div class="metric"><div class="metric-label">真实评论抓取</div><div class="metric-value">${fmt(allComments.length)}<span class="metric-unit">/ ${summary.totalXlsxComments} xlsx</span></div><div class="metric-sub">含子评论,已去重</div></div>
  257. <div class="metric"><div class="metric-label">笔记解析</div><div class="metric-value">${summary.resolved}<span class="metric-unit">/${summary.total}</span></div><div class="metric-sub">短链失效 ${summary.failed} 篇</div></div>
  258. </div>
  259. <h3 class="h2">按量级分层产出</h3>
  260. <div class="grid grid-2">
  261. <div class="card"><div id="chart-tier-engagement" class="chart"></div></div>
  262. <div class="card"><div id="chart-tier-rate" class="chart"></div></div>
  263. </div>
  264. <h3 class="h2">内容形态产出对比</h3>
  265. <div class="grid grid-2">
  266. <div class="card"><div id="chart-type-engagement" class="chart"></div></div>
  267. <div class="card"><div id="chart-region" class="chart"></div></div>
  268. </div>
  269. </div>
  270. </section>
  271. <!-- ============= 2. 数据明细 ============= -->
  272. <section id="sec-2" data-num="02 / DETAIL">
  273. <div class="inner">
  274. <div class="eyebrow">Section 02</div>
  275. <h2 class="h1">数据明细 — KOL 全量 + KOC Top 50</h2>
  276. <p class="sub">KOL 合作表 ${summary.kolCount} 篇全量展示,KOC 合作表 ${summary.kocCount} 篇按互动降序取 Top 50 (完整明细见 \`majiyong-closing-report.md\`)。</p>
  277. <h3 class="h2">KOL 全量 (${summary.kolCount} 篇)</h3>
  278. <div class="scroll-table">
  279. <table>
  280. <thead><tr>
  281. <th class="num">#</th><th>KOL</th><th>量级</th><th>内容形态</th><th>区域</th>
  282. <th class="num">粉丝</th><th class="num">单价</th>
  283. <th class="num">曝光</th><th class="num">点赞</th><th class="num">评论</th><th class="num">互动</th>
  284. <th>笔记</th>
  285. </tr></thead>
  286. <tbody>${detailKolRows}</tbody>
  287. </table>
  288. </div>
  289. <h3 class="h2">KOC Top 50 (按互动降序)</h3>
  290. <div class="scroll-table">
  291. <table>
  292. <thead><tr>
  293. <th class="num">#</th><th>KOC</th><th>分层</th><th>内容形态</th><th>区域</th>
  294. <th class="num">粉丝</th><th class="num">点赞</th><th class="num">评论</th><th class="num">互动</th><th class="num">真实评论</th>
  295. </tr></thead>
  296. <tbody>${detailKocTop}</tbody>
  297. </table>
  298. </div>
  299. </div>
  300. </section>
  301. <!-- ============= 3. 话题数据 ============= -->
  302. <section id="sec-3" data-num="03 / TOPICS">
  303. <div class="inner">
  304. <div class="eyebrow">Section 03</div>
  305. <h2 class="h1">话题数据 — 评论 VOC 主题分布</h2>
  306. <p class="sub">基于 ${allComments.length} 条真实评论 (含 ${noiseComments.length} 条互助互赞噪声 = ${noiseRate.toFixed(2)}%),按 ${TOPICS.length} 个主题进行规则化标注。**新鲜菜主题命中 ${topicHits['新菜/新鲜菜/产品创新'].length} 条,核心心智渗透成功**。</p>
  307. <div class="grid grid-2">
  308. <div class="card"><div id="chart-topics" class="chart-tall"></div></div>
  309. <div class="card">
  310. <h3 class="h2" style="margin-top:0">主题命中明细</h3>
  311. <table>
  312. <thead><tr><th>主题</th><th class="num">命中</th><th class="num">占比</th><th>典型样本</th></tr></thead>
  313. <tbody>${topicRows}</tbody>
  314. </table>
  315. </div>
  316. </div>
  317. <h3 class="h2">真实评论原声 (Top 16)</h3>
  318. <p class="sub">产品强相关 · 多主题轮询 · 已剔除噪声/托式营销/博主自评,展示具有产品对话价值的真实评论。</p>
  319. <div class="grid grid-2">${realCommentList}</div>
  320. </div>
  321. </section>
  322. <!-- ============= 4. 品牌相关词存量 ============= -->
  323. <section id="sec-4" data-num="04 / WORDS">
  324. <div class="inner">
  325. <div class="eyebrow">Section 04</div>
  326. <h2 class="h1">品牌相关词存量 — 心智渗透盘点</h2>
  327. <p class="sub">从 ${allComments.length} 条评论文本提取,显示品牌词、活动词、品类词在用户对话中的实际渗透情况。</p>
  328. <div class="grid grid-2">
  329. <div class="card card-accent">
  330. <h3 class="h2" style="margin-top:0">① 品牌词</h3>
  331. <p class="text-sm text-3">马记永 / 兰州牛肉面 / 马记永兰州 等核心品牌词</p>
  332. <div id="cloud-brand" class="chart"></div>
  333. </div>
  334. <div class="card card-green">
  335. <h3 class="h2" style="margin-top:0">② 活动相关词</h3>
  336. <p class="text-sm text-3">新鲜菜 / 春日新品 / 联名 / 试吃 等本期投放活动词</p>
  337. <div id="cloud-activity" class="chart"></div>
  338. </div>
  339. <div class="card card-blue">
  340. <h3 class="h2" style="margin-top:0">③ 品类词占位</h3>
  341. <p class="text-sm text-3">兰州拉面 / 西北菜 / 凉拌菜 等品类心智词</p>
  342. <div id="cloud-category" class="chart"></div>
  343. </div>
  344. <div class="card card-rose">
  345. <h3 class="h2" style="margin-top:0">④ 评论高频词云</h3>
  346. <p class="text-sm text-3">从全部评论文本提取的高频关键词</p>
  347. <div id="cloud-comments" class="chart"></div>
  348. </div>
  349. </div>
  350. <h3 class="h2">关键洞察</h3>
  351. <div class="grid grid-3">
  352. <div class="card card-green">
  353. <div class="tag green">✓ 品牌词渗透稳定</div>
  354. <p style="margin-top:14px;color:var(--text-2)">"马记永"作为品牌词在评论中高频自然出现,说明品牌已形成稳定心智占位,基础好。</p>
  355. </div>
  356. <div class="card card-accent">
  357. <div class="tag">✓ 活动词"新鲜菜"成功</div>
  358. <p style="margin-top:14px;color:var(--text-2)">"新鲜菜"在评论中超越价格主题,本轮"新鲜菜"心智宣发核心目标达成,继续放大可形成长期资产。</p>
  359. </div>
  360. <div class="card card-rose">
  361. <div class="tag rose">⚠ 竞品词出现</div>
  362. <p style="margin-top:14px;color:var(--text-2)">"陈香贵 / 张拉拉"等竞品在评论中被自然提及,需要建立心智份额对照基线持续监测。</p>
  363. </div>
  364. </div>
  365. </div>
  366. </section>
  367. <!-- ============= 5. 优点 / 不足 / 建议 ============= -->
  368. <section id="sec-5" data-num="05 / VERDICT">
  369. <div class="inner">
  370. <div class="eyebrow">Section 05</div>
  371. <h2 class="h1">本次宣发的优点、不足 & 后续投放建议</h2>
  372. <h3 class="h2"><span class="tag green">✓ 优点</span></h3>
  373. <div class="grid grid-2">
  374. ${PROS.map((p) => `<div class="card card-green"><h4 style="margin-bottom:10px;color:var(--green)">${esc(p.title)}</h4><p style="color:var(--text-2);font-size:0.9rem">${p.text}</p></div>`).join('')}
  375. </div>
  376. <h3 class="h2"><span class="tag rose">✗ 不足</span></h3>
  377. <div class="grid grid-2">
  378. ${CONS.map((c) => `<div class="card card-rose"><h4 style="margin-bottom:10px;color:var(--rose)">${esc(c.title)}</h4><p style="color:var(--text-2);font-size:0.9rem">${c.text}</p></div>`).join('')}
  379. </div>
  380. <h3 class="h2"><span class="tag">→ 后续投放建议</span></h3>
  381. <div class="card">
  382. ${RECS.map((r) => `<div class="rec-row"><div class="rec-p ${r.p}">${r.p}</div><div class="rec-content"><h4>${esc(r.t)}</h4><p>${r.b}</p></div></div>`).join('')}
  383. </div>
  384. <h3 class="h2">下一轮预算建议分配</h3>
  385. <div class="grid grid-4">
  386. <div class="metric"><div class="metric-label">头/腰部 KOL</div><div class="metric-value">35<span class="metric-unit">%</span></div><div class="metric-sub">2~3 位创意/跨界 KOL,延续高杠杆爆款方法论</div></div>
  387. <div class="metric"><div class="metric-label">差异化探店 KOC</div><div class="metric-value">35<span class="metric-unit">%</span></div><div class="metric-sub">5 赛道分发 (减脂/早午餐/家庭/一人食/商务),各 15-20 篇</div></div>
  388. <div class="metric"><div class="metric-label">二创 IP 池 + 在地达人</div><div class="metric-value">15<span class="metric-unit">%</span></div><div class="metric-sub">沉淀长期 IP 资产 + 月度打卡官</div></div>
  389. <div class="metric"><div class="metric-label">薯条 + 评论运营 + 数据</div><div class="metric-value">15<span class="metric-unit">%</span></div><div class="metric-sub">Top 5% 加投 + SOP + 风险词预警 + 求开店承接</div></div>
  390. </div>
  391. </div>
  392. </section>
  393. <!-- ============= 6. 新机会挖掘 ============= -->
  394. <section id="sec-6" data-num="06 / OPPORTUNITIES">
  395. <div class="inner">
  396. <div class="eyebrow" style="color:var(--rose)">Section 06 · ★ 重中之重</div>
  397. <h2 class="h1">新机会挖掘 — 从 ${allComments.length} 条评论逆向反推 ${OPPORTUNITIES.length} 个新机会</h2>
  398. <p class="sub">**结案报告告诉老板"过去花得值不值",本节告诉团队"下一步该怎么花得更值"。** 把每一条评论当成一次市场对话逐字解读,从微观信号反推产品/内容/人群/投放/数据 5 大象限的机会缺口。完整逐条解读见配套文档 <a href="./majiyong-new-opportunities.md" style="color:var(--amber);text-decoration:underline">majiyong-new-opportunities.md</a>。</p>
  399. <h3 class="h2">⭐ Top 5 P0 优先级 (下周可启动)</h3>
  400. <div class="grid grid-3">
  401. ${TOP5.map((id, idx) => {
  402. const o = OPPORTUNITIES.find((x) => x.id === id);
  403. return `<div class="card card-rose" style="position:relative">
  404. <div style="position:absolute;top:14px;right:18px;font-family:var(--font-mono);font-size:1.6rem;font-weight:800;color:var(--rose);opacity:0.6">#${idx + 1}</div>
  405. <div class="tag rose" style="margin-bottom:10px">${o.id} · ${o.p}</div>
  406. <h4 style="font-size:1.1rem;margin-bottom:10px">${esc(o.t)}</h4>
  407. <p style="color:var(--text-3);font-size:0.78rem;margin-bottom:8px"><strong style="color:var(--text-2)">来源信号:</strong> ${esc(o.signal)}</p>
  408. <p style="color:var(--text-2);font-size:0.88rem;line-height:1.6">${esc(o.action)}</p>
  409. </div>`;
  410. }).join('')}
  411. </div>
  412. <p class="sub" style="margin-top:32px">完整 ${OPPORTUNITIES.length} 个新机会按 5 大象限分独立章节深度展开,见下方 Section 06.A ~ 06.E。</p>
  413. <div class="toc" style="margin-top:24px">
  414. <a href="#sec-6a" style="border-color:var(--amber);color:var(--amber)">06.A 产品卖点 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('A')).length})</a>
  415. <a href="#sec-6b" style="border-color:var(--green);color:var(--green)">06.B 内容形态 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('B')).length})</a>
  416. <a href="#sec-6c" style="border-color:var(--purple);color:var(--purple)">06.C 人群渠道 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('C')).length})</a>
  417. <a href="#sec-6d" style="border-color:var(--blue);color:var(--blue)">06.D 投放机制 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('D')).length})</a>
  418. <a href="#sec-6e" style="border-color:var(--rose);color:var(--rose)">06.E 数据机制 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('E')).length})</a>
  419. </div>
  420. </div>
  421. </section>
  422. ${(() => {
  423. const QUADRANTS = [
  424. { letter: 'A', cat: 'A · 产品卖点', color: 'amber', tagClass: '', accent: 'card-accent', desc: '从用户评论里反推的产品/SKU 级机会 — 把"已经被用户说出口"的需求变成可上架的菜单/套餐/卖点。' },
  425. { letter: 'B', cat: 'B · 内容形态', color: 'green', tagClass: 'green', accent: 'card-green', desc: '内容打法升级 — 同样的预算,如何用更高 ROI 的内容形态,把好的东西讲得更让人记得住。' },
  426. { letter: 'C', cat: 'C · 人群渠道', color: 'purple', tagClass: 'purple', accent: '', desc: '人群和渠道的二八优化 — 让每一分预算都投在"会真去吃"的人身上,而不是"看了没法吃"的远方观众。' },
  427. { letter: 'D', cat: 'D · 投放机制', color: 'blue', tagClass: 'blue', accent: 'card-blue', desc: '投放过程的机制化 — 把"凭感觉发笔记"变成"算法驱动止损/加投/预警",每一轮都比上一轮更聪明。' },
  428. { letter: 'E', cat: 'E · 数据机制', color: 'rose', tagClass: 'rose', accent: 'card-rose', desc: '长效数据资产 — 主题词典、竞品对照,这些是可以持续复用 10 轮投放的底层基建。' },
  429. ];
  430. return QUADRANTS.map((q) => {
  431. const list = OPPORTUNITIES.filter((o) => o.cat === q.cat);
  432. return `
  433. <!-- ============= 6.${q.letter} ${q.cat} ============= -->
  434. <section id="sec-6${q.letter.toLowerCase()}" data-num="06.${q.letter} / ${q.cat.split('·')[1].trim()}" style="justify-content:flex-start">
  435. <div class="inner">
  436. <div class="eyebrow" style="color:var(--${q.color})">Section 06.${q.letter} · ${q.cat}</div>
  437. <h2 class="h1" style="color:var(--${q.color})">${esc(q.cat)} <span style="color:var(--text-3);font-size:0.5em;font-weight:500">${list.length} 个新机会</span></h2>
  438. <p class="sub">${esc(q.desc)}</p>
  439. <div class="grid" style="gap:28px;margin-top:32px">
  440. ${list.map((o, idx) => `
  441. <div class="card ${q.accent}" style="position:relative">
  442. <div style="position:absolute;top:18px;right:24px;font-family:var(--font-mono);font-size:2.4rem;font-weight:800;color:var(--${q.color});opacity:0.15;line-height:1">${o.id}</div>
  443. <div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;flex-wrap:wrap">
  444. <span class="tag ${q.tagClass}" style="font-family:var(--font-mono);font-size:0.8rem">${o.id}</span>
  445. <span class="tag ${o.p === 'P0' ? 'rose' : o.p === 'P1' ? '' : 'purple'}">${o.p}</span>
  446. <h3 style="font-family:var(--font-head);font-size:1.4rem;font-weight:700;letter-spacing:-0.01em">${esc(o.t)}</h3>
  447. </div>
  448. <div class="grid grid-3" style="gap:18px;margin-top:18px">
  449. <div style="background:var(--bg-0);border-radius:8px;padding:18px;border-left:2px solid var(--${q.color})">
  450. <div class="metric-label" style="margin-bottom:8px">📍 信号 / Signal</div>
  451. <p style="color:var(--text-2);font-size:0.88rem;line-height:1.65">${esc(o.signal)}</p>
  452. </div>
  453. <div style="background:var(--bg-0);border-radius:8px;padding:18px;border-left:2px solid var(--${q.color})">
  454. <div class="metric-label" style="margin-bottom:8px">🎯 行动 / Action</div>
  455. <p style="color:var(--text-1);font-size:0.88rem;line-height:1.65;font-weight:500">${esc(o.action)}</p>
  456. </div>
  457. <div style="background:var(--bg-0);border-radius:8px;padding:18px;border-left:2px solid var(--${q.color})">
  458. <div class="metric-label" style="margin-bottom:8px">� 预期 / Outcome</div>
  459. <p style="color:var(--text-2);font-size:0.88rem;line-height:1.65">${esc(o.outcome || '-')}</p>
  460. </div>
  461. </div>
  462. <div style="margin-top:22px;padding:20px;background:var(--bg-0);border-radius:8px">
  463. <div class="metric-label" style="margin-bottom:10px">💡 为什么要做 / Why</div>
  464. <p style="color:var(--text-2);font-size:0.92rem;line-height:1.75">${esc(o.why || '-')}</p>
  465. </div>
  466. ${Array.isArray(o.how) && o.how.length ? `
  467. <div style="margin-top:18px;padding:20px;background:var(--bg-0);border-radius:8px">
  468. <div class="metric-label" style="margin-bottom:12px">🛠️ 如何执行 / How (${o.how.length} 步)</div>
  469. <ol style="margin:0;padding-left:24px;color:var(--text-2);font-size:0.9rem;line-height:1.85">
  470. ${o.how.map((step) => `<li style="margin-bottom:6px">${esc(step)}</li>`).join('')}
  471. </ol>
  472. </div>
  473. ` : ''}
  474. </div>
  475. `).join('')}
  476. </div>
  477. </div>
  478. </section>`;
  479. }).join('');
  480. })()}
  481. <!-- ============= 页脚 ============= -->
  482. <section style="min-height:auto;padding:60px 24px;background:var(--bg-1)" data-num="END">
  483. <div class="inner" style="text-align:center">
  484. <p class="text-sm text-3">马记永·四月「新鲜菜」KOL/KOC 宣发结案报告 · 基于 ${summary.total} 篇笔记 + ${allComments.length} 条评论真实数据生成</p>
  485. <p class="text-xs text-3 mono" style="margin-top:8px">Generated ${new Date().toISOString().slice(0,19).replace('T',' ')}</p>
  486. </div>
  487. </section>
  488. </main>
  489. <script>
  490. const TIER_DATA = ${JSON.stringify(tierStats)};
  491. const TYPE_DATA = ${JSON.stringify(typeStats.slice(0, 12))};
  492. const REGION_DATA = ${JSON.stringify(topRegions)};
  493. const TOPIC_DATA = ${JSON.stringify(TOPICS.map((t) => ({ tag: t.tag, color: t.color, count: topicHits[t.tag].length })))};
  494. const CLOUD_BRAND = ${JSON.stringify(cloudBrand)};
  495. const CLOUD_ACTIVITY = ${JSON.stringify(cloudActivity)};
  496. const CLOUD_CATEGORY = ${JSON.stringify(cloudCategory)};
  497. const CLOUD_COMMENTS = ${JSON.stringify(wordcloudComments.slice(0, 80).map((d) => ({ name: d.name, value: d.value })))};
  498. const COMMON = { backgroundColor: 'transparent', textStyle: { fontFamily: 'Inter, Noto Sans SC, system-ui', color: '#A8A8A8' } };
  499. function initWhenReady(){
  500. if (!window.echarts) { setTimeout(initWhenReady, 200); return; }
  501. echarts.init(document.getElementById('chart-tier-engagement')).setOption({ ...COMMON,
  502. title: { text: '量级 · 总互动 / 篇数', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
  503. tooltip: { trigger:'axis' }, legend: { textStyle:{color:'#A8A8A8'}, top:0, right:0 },
  504. grid: { left:60, right:30, bottom:50, top:60 },
  505. xAxis: { type:'category', data: TIER_DATA.map(d=>d.tier), axisLabel:{ color:'#A8A8A8', interval:0, rotate:15 } },
  506. yAxis: [{ type:'value', name:'总互动', axisLabel:{color:'#A8A8A8'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}}, nameTextStyle:{color:'#A8A8A8'} },
  507. { type:'value', name:'篇数', axisLabel:{color:'#A8A8A8'}, splitLine:{show:false}, nameTextStyle:{color:'#A8A8A8'} }],
  508. series: [{ name:'总互动', type:'bar', data: TIER_DATA.map(d=>d.eng), itemStyle:{color:'#F5A623', borderRadius:[4,4,0,0]} },
  509. { name:'篇数', type:'line', yAxisIndex:1, data: TIER_DATA.map(d=>d.count), itemStyle:{color:'#00DC82'}, smooth:true }]
  510. });
  511. echarts.init(document.getElementById('chart-tier-rate')).setOption({ ...COMMON,
  512. title: { text: '量级 · 互动率 (%)', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
  513. tooltip: { trigger:'axis', formatter: (p)=> p[0].name+'<br/>'+p[0].value.toFixed(2)+'%' },
  514. grid: { left:60, right:30, bottom:50, top:60 },
  515. xAxis: { type:'category', data: TIER_DATA.map(d=>d.tier), axisLabel:{color:'#A8A8A8', interval:0, rotate:15} },
  516. yAxis: { type:'value', axisLabel:{color:'#A8A8A8', formatter:'{value}%'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}} },
  517. series: [{ type:'bar', data: TIER_DATA.map(d=>+d.rate.toFixed(2)), itemStyle:{color:'#8B5CF6', borderRadius:[4,4,0,0]} }]
  518. });
  519. echarts.init(document.getElementById('chart-type-engagement')).setOption({ ...COMMON,
  520. title: { text: '内容形态 · 总互动 (Top 12)', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
  521. tooltip: { trigger:'axis' }, grid: { left:140, right:30, bottom:30, top:50 },
  522. xAxis: { type:'value', axisLabel:{color:'#A8A8A8'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}} },
  523. yAxis: { type:'category', data: TYPE_DATA.map(d=>d.type), axisLabel:{color:'#A8A8A8', fontSize:11} },
  524. series: [{ type:'bar', data: TYPE_DATA.map(d=>d.eng), itemStyle:{color:'#3B82F6', borderRadius:[0,4,4,0]}, label:{show:true, position:'right', color:'#F5F5F5', fontSize:11} }]
  525. });
  526. echarts.init(document.getElementById('chart-region')).setOption({ ...COMMON,
  527. title: { text: '区域分布 · 篇数 Top 12', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
  528. tooltip: { trigger:'axis' }, grid: { left:60, right:30, bottom:50, top:50 },
  529. xAxis: { type:'category', data: REGION_DATA.map(d=>d.region), axisLabel:{color:'#A8A8A8'} },
  530. yAxis: { type:'value', axisLabel:{color:'#A8A8A8'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}} },
  531. series: [{ type:'bar', data: REGION_DATA.map(d=>d.count), itemStyle:{color:'#00DC82', borderRadius:[4,4,0,0]} }]
  532. });
  533. echarts.init(document.getElementById('chart-topics')).setOption({ ...COMMON,
  534. title: { text: '评论主题命中分布', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
  535. tooltip: { trigger:'axis' }, grid: { left:140, right:40, bottom:30, top:50 },
  536. xAxis: { type:'value', axisLabel:{color:'#A8A8A8'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}} },
  537. yAxis: { type:'category', data: TOPIC_DATA.map(d=>d.tag), axisLabel:{color:'#A8A8A8', fontSize:12} },
  538. series: [{ type:'bar', data: TOPIC_DATA.map(d=>({value:d.count, itemStyle:{color:d.color, borderRadius:[0,4,4,0]}})),
  539. label:{show:true, position:'right', color:'#F5F5F5', fontSize:12} }]
  540. });
  541. function wordcloud(id, data, colors){
  542. echarts.init(document.getElementById(id)).setOption({ ...COMMON,
  543. tooltip: { show: true, formatter: (p)=> p.name + ' · ' + p.value },
  544. series: [{ type:'wordCloud', shape:'circle', sizeRange:[12,56], rotationRange:[-30,30], rotationStep:30, gridSize:8, drawOutOfBound:false, layoutAnimation:true,
  545. textStyle:{ fontFamily:'Noto Sans SC, Inter, sans-serif', fontWeight:600, color: () => colors[Math.floor(Math.random()*colors.length)] },
  546. emphasis:{ textStyle:{ color:'#F5A623', shadowBlur:10, shadowColor:'rgba(245,166,35,0.5)' } },
  547. data: data }]
  548. });
  549. }
  550. wordcloud('cloud-brand', CLOUD_BRAND, ['#F5A623','#FFB849','#FFC97D']);
  551. wordcloud('cloud-activity', CLOUD_ACTIVITY, ['#00DC82','#34E899','#5EEAB1']);
  552. wordcloud('cloud-category', CLOUD_CATEGORY, ['#3B82F6','#60A5FA','#93C5FD']);
  553. wordcloud('cloud-comments', CLOUD_COMMENTS, ['#FF4D8D','#FF7AAB','#FFA3C2','#A8A8A8']);
  554. }
  555. initWhenReady();
  556. </script>
  557. </body>
  558. </html>`;
  559. fs.writeFileSync(OUT_HTML, HTML, 'utf8');
  560. console.log('✓ HTML:', OUT_HTML, Math.round(HTML.length / 1024) + 'KB');
  561. // ---------- MD ----------
  562. const md = [];
  563. const W = (s) => md.push(s);
  564. W('# 马记永 · 四月「新鲜菜」KOL/KOC 宣发结案报告');
  565. W('');
  566. W(`> **投放期间**: 2026-04 · **生成时间**: ${new Date().toISOString().slice(0, 19).replace('T', ' ')} `);
  567. W(`> **数据范围**: ${summary.total} 篇报备笔记 (KOL ${summary.kolCount} + KOC ${summary.kocCount}) + ${allComments.length} 条公开评论 (覆盖 ${items.filter((x) => (x._comments || []).length > 0).length}/${summary.total} 篇)`);
  568. W('');
  569. W('---');
  570. W('');
  571. W('## 一、数据汇总');
  572. W('');
  573. W('| 指标 | 数值 | 备注 |');
  574. W('|---|---:|---|');
  575. W(`| 总报备笔记 | ${summary.total} | KOL ${summary.kolCount} + KOC ${summary.kocCount} |`);
  576. W(`| KOL 总预算 | ¥${fmt(summary.kolSpend)} | CPM ¥${summary.cpm.toFixed(2)} |`);
  577. W(`| 总曝光 | ${fmt(summary.totalImpressions)} | 大盘心智渗透 |`);
  578. W(`| 总互动 | ${fmt(summary.totalEngagement)} | CPE ¥${summary.cpe.toFixed(2)} |`);
  579. W(`| 点赞 / 收藏 / 评论 (xlsx) | ${fmt(summary.totalLikes)} / ${fmt(summary.totalCollects)} / ${fmt(summary.totalXlsxComments)} | - |`);
  580. W(`| 整体互动率 | ${pct(summary.engagementRate)} | 行业基准 3-5% |`);
  581. W(`| 笔记解析成功 | ${summary.resolved}/${summary.total} | 短链失效 ${summary.failed} 篇 |`);
  582. W(`| 真实评论抓取 | ${allComments.length} 条 | 噪声率 ${noiseRate.toFixed(2)}% |`);
  583. W('');
  584. W('### 按量级分层产出');
  585. W('');
  586. W('| 量级 | 篇数 | 总曝光 | 总互动 | 平均互动 | 互动率 | 投入 | CPE |');
  587. W('|---|---:|---:|---:|---:|---:|---:|---:|');
  588. for (const t of tierStats) {
  589. W(`| ${t.tier} | ${t.count} | ${fmt(t.imp)} | ${fmt(t.eng)} | ${t.avgEng} | ${t.rate.toFixed(2)}% | ¥${fmt(t.spend)} | ${t.cpe === '-' ? '-' : '¥' + t.cpe} |`);
  590. }
  591. W('');
  592. W('### 按内容形态产出 (Top 12)');
  593. W('');
  594. W('| 内容形态 | 篇数 | 总曝光 | 总互动 | 平均互动 | 互动率 |');
  595. W('|---|---:|---:|---:|---:|---:|');
  596. for (const t of typeStats.slice(0, 12)) {
  597. W(`| ${t.type} | ${t.count} | ${fmt(t.imp)} | ${fmt(t.eng)} | ${t.avgEng} | ${t.rate.toFixed(2)}% |`);
  598. }
  599. W('');
  600. W('### 区域分布 Top 12');
  601. W('');
  602. W('| 区域 | 篇数 | 总互动 |');
  603. W('|---|---:|---:|');
  604. for (const r of topRegions) W(`| ${r.region} | ${r.count} | ${fmt(r.eng)} |`);
  605. W('');
  606. W('---');
  607. W('');
  608. W(`## 二、数据明细 (KOL ${summary.kolCount} 全量 + KOC Top 50)`);
  609. W('');
  610. W('### 2.1 KOL 全量');
  611. W('');
  612. W('| # | KOL | 量级 | 内容形态 | 区域 | 粉丝 | 单价 | 曝光 | 点赞 | 评论 | 互动 |');
  613. W('|---:|---|---|---|---|---:|---:|---:|---:|---:|---:|');
  614. const kolSorted = [...kolItems].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement));
  615. for (let i = 0; i < kolSorted.length; i++) {
  616. const r = kolSorted[i];
  617. W(`| ${i + 1} | ${r.name} | ${r.tier || '-'} | ${r.contentType || '-'} | ${String(r.region || '-').slice(0, 3)} | ${(num(r.followersWan) * 10000).toLocaleString('zh-CN')} | ¥${fmt(r.priceOrder)} | ${fmt(r.impressionsSubtotal || r.impressions)} | ${fmt(r.likes)} | ${fmt(r.comments)} | ${fmt(r.engagementSubtotal || r.engagement)} |`);
  618. }
  619. W('');
  620. W('### 2.2 KOC Top 50');
  621. W('');
  622. W('| # | KOC | 分层 | 内容形态 | 区域 | 粉丝 | 点赞 | 评论 | 互动 | 真实评论 |');
  623. W('|---:|---|---|---|---|---:|---:|---:|---:|---:|');
  624. const kocTop = [...kocItems].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement)).slice(0, 50);
  625. for (let i = 0; i < kocTop.length; i++) {
  626. const r = kocTop[i];
  627. W(`| ${i + 1} | ${r.name} | ${r.tier || 'KOC'} | ${r.contentType || '-'} | ${String(r.region || '-').slice(0, 3)} | ${(num(r.followersWan) * 10000).toLocaleString('zh-CN')} | ${fmt(r.likes)} | ${fmt(r.comments)} | ${fmt(r.engagementSubtotal || r.engagement)} | ${(r._comments && r._comments.length) || 0} |`);
  628. }
  629. W('');
  630. W('---');
  631. W('');
  632. W('## 三、话题数据 — 评论 VOC 主题分布');
  633. W('');
  634. W(`基于 **${allComments.length}** 条真实评论 (含 ${noiseComments.length} 条噪声 = ${noiseRate.toFixed(2)}%) 的规则化主题命中:`);
  635. W('');
  636. W('| 主题 | 命中 | 占比 | 典型样本 |');
  637. W('|---|---:|---:|---|');
  638. for (const t of TOPICS) {
  639. const hits = topicHits[t.tag];
  640. const sample = hits.sort((a, b) => b.like - a.like)[0];
  641. const sampleText = sample ? `[${sample.koc}@${sample.ip || '-'}] ${sample.content.replace(/\n/g, ' ').replace(/\|/g, '\\|').slice(0, 80)}` : '-';
  642. W(`| ${t.tag} | ${hits.length} | ${(hits.length / Math.max(allComments.length, 1) * 100).toFixed(1)}% | ${sampleText} |`);
  643. }
  644. W('');
  645. W('### 真实评论原声 Top 16 (产品强相关 · 多主题轮询)');
  646. W('');
  647. for (const { topic, c } of topPicks) {
  648. W(`- **[${topic}]** \`${c.koc}@${c.ip || '-'}\`: ${c.content.replace(/\n/g, ' ')}`);
  649. }
  650. W('');
  651. W('---');
  652. W('');
  653. W('## 四、品牌相关词存量');
  654. W('');
  655. W('### 4.1 品牌词');
  656. W('');
  657. W('| 词 | 评论命中 | 状态 |');
  658. W('|---|---:|---|');
  659. for (const w of cloudBrand) W(`| ${w.name} | ${w.value - 5 > 0 ? w.value - 5 : 0} | ${w.value > 5 ? '✓ 已渗透' : '○ 待沉淀'} |`);
  660. W('');
  661. W('### 4.2 活动相关词');
  662. W('');
  663. W('| 词 | 评论命中 | 状态 |');
  664. W('|---|---:|---|');
  665. for (const w of cloudActivity) W(`| ${w.name} | ${w.value - 5 > 0 ? w.value - 5 : 0} | ${w.value > 5 ? '✓ 已渗透' : '○ 待沉淀'} |`);
  666. W('');
  667. W('### 4.3 品类词占位');
  668. W('');
  669. W('| 词 | 评论命中 | 状态 |');
  670. W('|---|---:|---|');
  671. for (const w of cloudCategory) W(`| ${w.name} | ${w.value - 5 > 0 ? w.value - 5 : 0} | ${w.value > 5 ? '✓ 已渗透' : '○ 待沉淀'} |`);
  672. W('');
  673. W('### 4.4 评论高频词 Top 50');
  674. W('');
  675. W('| 词 | 频次 |');
  676. W('|---|---:|');
  677. for (const w of wordcloudComments.slice(0, 50)) W(`| ${w.name} | ${w.value} |`);
  678. W('');
  679. W('---');
  680. W('');
  681. W('## 五、本次宣发的优点 / 不足 / 后续建议');
  682. W('');
  683. W('### 5.1 ✓ 优点');
  684. W('');
  685. for (const p of PROS) {
  686. W(`**${p.title}**`);
  687. W('');
  688. W(p.text.replace(/<\/?b>/g, '**').replace(/<code>([^<]+)<\/code>/g, '`$1`'));
  689. W('');
  690. }
  691. W('### 5.2 ✗ 不足');
  692. W('');
  693. for (const c of CONS) {
  694. W(`**${c.title}**`);
  695. W('');
  696. W(c.text.replace(/<\/?b>/g, '**'));
  697. W('');
  698. }
  699. W('### 5.3 → 后续投放建议');
  700. W('');
  701. W('| 优先级 | 行动 | 说明 |');
  702. W('|---|---|---|');
  703. for (const r of RECS) W(`| **${r.p}** | ${r.t} | ${r.b} |`);
  704. W('');
  705. W('### 5.4 下一轮预算建议分配');
  706. W('');
  707. W('| 模块 | 占比 | 说明 |');
  708. W('|---|---:|---|');
  709. W('| 头/腰部 KOL | 35% | 2~3 位创意/跨界 KOL,延续高杠杆爆款方法论 |');
  710. W('| 差异化探店 KOC | 35% | 5 赛道分发 (减脂/早午餐/家庭/一人食/商务),各 15-20 篇 |');
  711. W('| 二创 IP 池 + 在地达人 | 15% | 沉淀长期 IP 资产 + 月度打卡官 |');
  712. W('| 薯条 + 评论运营 + 数据 | 15% | Top 5% 加投 + SOP + 风险词预警 + 求开店承接 |');
  713. W('');
  714. W('---');
  715. W('');
  716. W(`*本结案报告基于 ${summary.total} 篇笔记 (KOL ${summary.kolCount} + KOC ${summary.kocCount}) + ${allComments.length} 条公开评论真实数据,通过可复用 VOC 工作流自动生成。配套交互式 HTML 报告: \`majiyong-closing-report.html\` · 配套深度新机会文档: \`majiyong-new-opportunities.md\`*`);
  717. fs.writeFileSync(OUT_MD, md.join('\n'), 'utf8');
  718. console.log('✓ MD:', OUT_MD, md.length, 'lines');
  719. };