| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775 |
- /**
- * 马记永结案报告 · HTML + MD 渲染层
- */
- const fs = require('fs');
- const path = require('path');
- const ROOT = path.resolve(__dirname, '..');
- const OUT_HTML = path.join(ROOT, 'majiyong-closing-report.html');
- const OUT_MD = path.join(ROOT, 'majiyong-closing-report.md');
- module.exports = function render(M) {
- const { items, kolItems, kocItems, summary, tierStats, typeStats, topRegions, byTier,
- allComments, noiseComments, noiseRate, topicHits, TOPICS, wordcloudComments, cloudBrand, cloudActivity, cloudCategory,
- PROS, CONS, RECS, OPPORTUNITIES, TOP5,
- fmt, pct, esc, num } = M;
- // ---------- 明细行 (注: 篇数大,做 KOL 全表 + KOC Top 50) ----------
- const detailKolRows = [...kolItems].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement)).map((r, i) => `
- <tr>
- <td class="num">${i + 1}</td>
- <td>${esc(r.name)}</td>
- <td><span class="tag">${esc(r.tier || '-')}</span></td>
- <td><span class="tag gray">${esc(r.contentType || '-')}</span></td>
- <td><span class="tag gray">${esc(String(r.region || '-').slice(0, 3))}</span></td>
- <td class="num mono">${(num(r.followersWan) * 10000).toLocaleString('zh-CN')}</td>
- <td class="num mono">¥${fmt(r.priceOrder)}</td>
- <td class="num mono">${fmt(r.impressionsSubtotal || r.impressions)}</td>
- <td class="num mono">${fmt(r.likes)}</td>
- <td class="num mono">${fmt(r.comments)}</td>
- <td class="num mono ${num(r.engagementSubtotal || r.engagement) === 0 ? 'dim' : ''}">${fmt(r.engagementSubtotal || r.engagement)}</td>
- <td>${r.noteId ? '<span class="tag green">已解析</span>' : '<span class="tag rose">短链失效</span>'}</td>
- </tr>`).join('');
- const detailKocTop = [...kocItems].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement)).slice(0, 50).map((r, i) => `
- <tr>
- <td class="num">${i + 1}</td>
- <td>${esc(r.name)}</td>
- <td><span class="tag gray">${esc(r.tier || 'KOC')}</span></td>
- <td><span class="tag gray">${esc(r.contentType || '-')}</span></td>
- <td><span class="tag gray">${esc(String(r.region || '-').slice(0, 3))}</span></td>
- <td class="num mono">${(num(r.followersWan) * 10000).toLocaleString('zh-CN')}</td>
- <td class="num mono">${fmt(r.likes)}</td>
- <td class="num mono">${fmt(r.comments)}</td>
- <td class="num mono ${num(r.engagementSubtotal || r.engagement) === 0 ? 'dim' : ''}">${fmt(r.engagementSubtotal || r.engagement)}</td>
- <td>${(r._comments && r._comments.length) || 0}</td>
- </tr>`).join('');
- const topicRows = TOPICS.map((t) => {
- const hits = topicHits[t.tag];
- const sample = hits.sort((a, b) => b.like - a.like)[0];
- return `<tr>
- <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>
- <td class="num mono">${hits.length}</td>
- <td class="num mono">${(hits.length / Math.max(allComments.length, 1) * 100).toFixed(1)}%</td>
- <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>
- </tr>`;
- }).join('');
- // 真实评论筛选 v4: 7 层过滤 + 产品强相关硬约束 + 主题多样性轮询
- const NOISE_RE = /互赞|互助|加油.*互|可以.*点赞|点赞.*返|蹲.*搭子|互粉/;
- const OFFTOPIC_RE = /眼影|口红|睫毛|化妆|包包|分装|借我|借用|手能借|护肤|穿搭|开箱|美术生|画的|画得|画风|笔法|插画|手工|盒子.*像/;
- const PROMO_RE = /推广|求关注|求互|私信|加微|商务合作|接广告|涨粉/;
- const SHILL_RE = /宝子|亲测|无套路|必.{0,2}冲|绝绝子|拉满|薅羊毛|薅得|放心冲|闭眼冲|爆棚|颜值直接|快冲|姐妹.{0,3}冲|实力宠粉|超(级|绝|香)|爆款|出圈|0元购|0元薅|真香|yyds|YYDS|惊艳到|跺脚/;
- const META_KOL_RE = /博主|up主|UP主|作者|接广|涨粉|掉粉|播放|粉丝.*多|9000.*掉.*\d|完啦R/;
- const EMOJI_HEAVY_RE = /(🥬|🥢|🌿|🤩|💯|🔥|💥|✅|💦|📸|🥳|🥞|🛒|🍣|🎉|✨|💚|❗|‼️)/g;
- const stripDeco = (s) => s.replace(/\[[^\]]+\]/g, '').replace(/[@#][^\s@#]+/g, '').replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '');
- // 产品强相关词典: 9 主题,评论必须命中至少 1 个才算 VOC 有效
- const PRODUCT_TOPICS = {
- '口味体验': /好吃|难吃|味道|香|辣|咸|淡|腻|清爽|清甜|甜杆|西兰|拌|脆|嫩|爽口|入味/,
- '产品创新': /新菜|新鲜|鲜菜|凉拌|凉菜|拌菜|不过夜|当天|现做|现拌|出.{0,3}菜|上.{0,3}菜/,
- '门店开店': /开店|分店|开到|什么时候.*开|建议.*开|我们这.*没|这边.*没|本地.*有|哪里有店|店在哪/,
- '健康营养': /健康|减脂|低脂|营养|蔬菜|清淡|0负担|低卡/,
- '价格性价比': /便宜|划算|实惠|贵|价格|性价比|套餐|¥/,
- '活动机制': /换菜|换券|换免费|带.*换|带.*能换|花.*换|报纸|活动.*介绍|活动.*在哪|怎么参与|怎么换|领券|领劵/,
- '场景外带': /外带|打包|下酒|带回家|带走|外卖|食堂|带娃|带朋友/,
- '复购推荐': /常去|经常.*吃|经常.*去|每次|又来|回头|推荐|尝尝|去试|去尝/,
- '品牌情感': /马记永.*好|马记永.*爱|爱.*马记永|喜欢.*马记永|马记永.*绝/,
- };
- const PRODUCT_RE = new RegExp(Object.values(PRODUCT_TOPICS).map((r) => r.source).join('|'));
- const realComments = allComments.filter((c) => {
- const txt = c.content || '';
- if (c.nick && c.koc && c.nick === c.koc) return false; // 博主自评
- // @ 别人开头的回复 = 缺乏上下文 (除非内容本身够长够独立)
- if (/^\s*@/.test(txt) && stripDeco(txt.replace(/^\s*@[^\s@#]+/, '')).length < 20) return false;
- if (NOISE_RE.test(txt) || OFFTOPIC_RE.test(txt) || PROMO_RE.test(txt) || SHILL_RE.test(txt) || META_KOL_RE.test(txt)) return false;
- const emojis = txt.match(EMOJI_HEAVY_RE) || [];
- if (emojis.length >= 3) return false;
- const core = stripDeco(txt);
- if (core.length < 6) return false;
- if (core.length > 80 && emojis.length >= 1) return false;
- if (core.length > 120) return false;
- if (!PRODUCT_RE.test(txt)) return false; // 硬约束: 必须产品相关
- return true;
- });
- // 按主题分桶 (只归首个匹配主题)
- const buckets = {};
- for (const tag of Object.keys(PRODUCT_TOPICS)) buckets[tag] = [];
- for (const c of realComments) {
- for (const [tag, re] of Object.entries(PRODUCT_TOPICS)) {
- if (re.test(c.content)) { buckets[tag].push(c); break; }
- }
- }
- // 排序: 点赞降序;同点赞按"信息量"降序 (产品关键词命中数 + 内容长度)
- const informationScore = (c) => {
- let hits = 0;
- for (const re of Object.values(PRODUCT_TOPICS)) if (re.test(c.content)) hits++;
- const core = stripDeco(c.content);
- return hits * 20 + core.length;
- };
- for (const tag of Object.keys(buckets)) {
- buckets[tag].sort((a, b) => (b.like - a.like) || (informationScore(b) - informationScore(a)));
- }
- // 主题轮询: 先每主题 Top 2,补到 16
- const topPicks = [];
- const queues = Object.entries(buckets).map(([tag, list]) => ({ tag, list, idx: 0 }));
- for (let round = 0; round < 4 && topPicks.length < 16; round++) {
- for (const q of queues) {
- if (topPicks.length >= 16) break;
- if (q.idx < q.list.length && q.idx < round + 1) {
- topPicks.push({ topic: q.tag, c: q.list[q.idx] });
- q.idx++;
- }
- }
- }
- realComments.length = 0;
- realComments.push(...topPicks.map((p) => p.c));
- const realCommentList = topPicks.map(({ topic, c }) => `
- <div class="quote">
- <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>
- <div class="quote-text">${esc(c.content.replace(/\n/g, ' '))}</div>
- </div>`).join('');
- const HTML = `<!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
- <title>马记永·四月「新鲜菜」KOL/KOC 宣发结案报告</title>
- <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/echarts-wordcloud@2.1.0/dist/echarts-wordcloud.min.js"></script>
- <style>
- :root {
- --bg-0: #0A0A0A; --bg-1: #111111; --bg-2: #1A1A1A; --bg-3: #252525;
- --border: rgba(255,255,255,0.08); --border-strong: rgba(255,255,255,0.16);
- --text-1: #F5F5F5; --text-2: #A8A8A8; --text-3: #6B6B6B;
- --amber: #F5A623; --green: #00DC82; --purple: #8B5CF6; --rose: #FF4D8D; --blue: #3B82F6; --red: #EF4444;
- --font-head: 'Inter','Noto Sans SC','PingFang SC','Microsoft YaHei',system-ui,sans-serif;
- --font-body: 'Noto Sans SC','Inter','PingFang SC','Microsoft YaHei',system-ui,sans-serif;
- --font-mono: 'JetBrains Mono','SF Mono',Consolas,monospace;
- }
- * { margin: 0; padding: 0; box-sizing: border-box; }
- html { scroll-behavior: smooth; }
- 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; }
- body::before { content: ''; position: fixed; inset: 0; pointer-events: none; z-index: 0;
- background: radial-gradient(ellipse 900px 700px at 20% 25%, rgba(245,166,35,0.08), transparent 60%),
- radial-gradient(ellipse 800px 600px at 80% 75%, rgba(0,220,130,0.06), transparent 60%); }
- main { position: relative; z-index: 1; }
- ::selection { background: var(--amber); color: #000; }
- 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; }
- section .inner { width: 100%; max-width: 1280px; margin: 0 auto; }
- 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); }
- .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; }
- .eyebrow::before { content: ''; display: inline-block; width: 24px; height: 1px; background: var(--amber); }
- .h0 { font-family: var(--font-head); font-weight: 900; letter-spacing: -0.04em; line-height: 0.95; font-size: clamp(3rem, 8vw, 6rem); }
- .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; }
- .h2 { font-family: var(--font-head); font-weight: 700; font-size: clamp(1.4rem, 2.4vw, 1.8rem); margin: 32px 0 16px; }
- .lead { color: var(--text-2); font-size: 1.05rem; max-width: 720px; margin-top: 18px; line-height: 1.8; }
- .sub { color: var(--text-2); margin-bottom: 32px; max-width: 800px; }
- .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); }
- .grid { display: grid; gap: 22px; }
- .grid-2 { grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); }
- .grid-3 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
- .grid-4 { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
- .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; }
- .card:hover { transform: translateY(-3px); border-color: var(--border-strong); }
- .card-accent { border-left: 3px solid var(--amber); }
- .card-green { border-left: 3px solid var(--green); }
- .card-rose { border-left: 3px solid var(--rose); }
- .card-blue { border-left: 3px solid var(--blue); }
- .metric { background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; padding: 24px; position: relative; overflow: hidden; min-width: 0; }
- .metric::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, var(--amber), transparent); opacity: 0.6; }
- .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; }
- .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; }
- .metric-unit { font-family: var(--font-mono); font-size: 0.85rem; color: var(--amber); font-weight: 500; margin-left: 4px; }
- .metric-sub { margin-top: 14px; font-size: 0.82rem; color: var(--text-2); line-height: 1.5; }
- .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); }
- .tag.green { background: rgba(0,220,130,0.08); color: var(--green); border-color: rgba(0,220,130,0.2); }
- .tag.purple { background: rgba(139,92,246,0.08); color: var(--purple); border-color: rgba(139,92,246,0.2); }
- .tag.rose { background: rgba(255,77,141,0.08); color: var(--rose); border-color: rgba(255,77,141,0.2); }
- .tag.blue { background: rgba(59,130,246,0.08); color: var(--blue); border-color: rgba(59,130,246,0.2); }
- .tag.gray { background: rgba(255,255,255,0.04); color: var(--text-2); border-color: var(--border); }
- table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
- th, td { padding: 10px 12px; border-bottom: 1px solid var(--border); text-align: left; }
- 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; }
- td.num, th.num { text-align: right; }
- tr:hover td { background: rgba(255,255,255,0.025); }
- td.sample { color: var(--text-2); font-size: 0.82rem; max-width: 380px; }
- .scroll-table { max-height: 70vh; overflow: auto; border: 1px solid var(--border); border-radius: 12px; background: var(--bg-1); }
- .scroll-table::-webkit-scrollbar { width: 8px; height: 8px; }
- .scroll-table::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: 4px; }
- .chart { width: 100%; height: 360px; }
- .chart-tall { width: 100%; height: 460px; }
- .quote { background: var(--bg-1); border-left: 3px solid var(--amber); border-radius: 0 8px 8px 0; padding: 14px 18px; margin-bottom: 12px; }
- .quote-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; flex-wrap: wrap; }
- .quote-text { color: var(--text-1); font-size: 0.94rem; line-height: 1.65; }
- .rec-row { display: flex; gap: 18px; padding: 16px 0; border-bottom: 1px solid var(--border); align-items: flex-start; }
- .rec-row:last-child { border-bottom: none; }
- .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; }
- .rec-p.P0 { background: rgba(255,77,141,0.1); color: var(--rose); border: 1px solid rgba(255,77,141,0.3); }
- .rec-p.P1 { background: rgba(245,166,35,0.1); color: var(--amber); border: 1px solid rgba(245,166,35,0.3); }
- .rec-p.P2 { background: rgba(139,92,246,0.1); color: var(--purple); border: 1px solid rgba(139,92,246,0.3); }
- .rec-content h4 { font-size: 1.05rem; margin-bottom: 6px; }
- .rec-content p { color: var(--text-2); font-size: 0.9rem; line-height: 1.65; }
- .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); }
- .cover-meta { display: flex; gap: 32px; margin-top: 32px; flex-wrap: wrap; }
- .cover-meta-item { font-family: var(--font-mono); font-size: 0.85rem; color: var(--text-2); }
- .cover-meta-item strong { color: var(--text-1); font-weight: 600; display: block; margin-top: 4px; font-size: 1.1rem; }
- .toc { display: flex; gap: 14px; margin-top: 48px; flex-wrap: wrap; }
- .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; }
- .toc a:hover { border-color: var(--amber); color: var(--amber); }
- @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; } }
- </style>
- </head>
- <body>
- <main>
- <!-- ============= 封面 ============= -->
- <section class="cover" data-num="00 / COVER">
- <div class="inner">
- <div class="eyebrow">MAJIYONG · 兰州牛肉面 · 新鲜菜项目 · 2026.04</div>
- <h1 class="h0">马记永「新鲜菜」<br/>KOL/KOC 宣发<br/><span style="color:var(--amber)">结案报告</span></h1>
- <p class="lead">基于 ${summary.total} 篇报备笔记 (KOL ${summary.kolCount} + KOC ${summary.kocCount}) + ${allComments.length} 条公开评论真实抓取,完整复盘投放规模、内容形态、互动效率、评论 VOC 与品牌词渗透,并给出下一轮可执行的优化建议。</p>
- <div class="cover-meta">
- <div class="cover-meta-item">投放期间<strong>2026-04</strong></div>
- <div class="cover-meta-item">总报备<strong>${summary.total} 篇</strong></div>
- <div class="cover-meta-item">KOL 投入<strong>¥${fmt(summary.kolSpend)}</strong></div>
- <div class="cover-meta-item">总曝光<strong>${fmt(summary.totalImpressions)}</strong></div>
- <div class="cover-meta-item">总互动<strong>${fmt(summary.totalEngagement)}</strong></div>
- <div class="cover-meta-item">真实评论<strong>${allComments.length} 条</strong></div>
- </div>
- <div class="toc">
- <a href="#sec-1">① 数据汇总</a>
- <a href="#sec-2">② 数据明细</a>
- <a href="#sec-3">③ 话题数据</a>
- <a href="#sec-4">④ 品牌相关词</a>
- <a href="#sec-5">⑤ 优点 / 不足 / 建议</a>
- <a href="#sec-6" style="border-color:var(--rose);color:var(--rose)">⑥ 新机会挖掘 ★</a>
- </div>
- </div>
- </section>
- <!-- ============= 1. 数据汇总 ============= -->
- <section id="sec-1" data-num="01 / SUMMARY">
- <div class="inner">
- <div class="eyebrow">Section 01</div>
- <h2 class="h1">数据汇总 — 投放规模与产出效率</h2>
- <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>
- <div class="grid grid-4">
- <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>
- <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>
- <div class="metric"><div class="metric-label">总曝光</div><div class="metric-value">${fmt(summary.totalImpressions)}</div><div class="metric-sub">大盘心智渗透</div></div>
- <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>
- <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>
- <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>
- <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>
- <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>
- </div>
- <h3 class="h2">按量级分层产出</h3>
- <div class="grid grid-2">
- <div class="card"><div id="chart-tier-engagement" class="chart"></div></div>
- <div class="card"><div id="chart-tier-rate" class="chart"></div></div>
- </div>
- <h3 class="h2">内容形态产出对比</h3>
- <div class="grid grid-2">
- <div class="card"><div id="chart-type-engagement" class="chart"></div></div>
- <div class="card"><div id="chart-region" class="chart"></div></div>
- </div>
- </div>
- </section>
- <!-- ============= 2. 数据明细 ============= -->
- <section id="sec-2" data-num="02 / DETAIL">
- <div class="inner">
- <div class="eyebrow">Section 02</div>
- <h2 class="h1">数据明细 — KOL 全量 + KOC Top 50</h2>
- <p class="sub">KOL 合作表 ${summary.kolCount} 篇全量展示,KOC 合作表 ${summary.kocCount} 篇按互动降序取 Top 50 (完整明细见 \`majiyong-closing-report.md\`)。</p>
- <h3 class="h2">KOL 全量 (${summary.kolCount} 篇)</h3>
- <div class="scroll-table">
- <table>
- <thead><tr>
- <th class="num">#</th><th>KOL</th><th>量级</th><th>内容形态</th><th>区域</th>
- <th class="num">粉丝</th><th class="num">单价</th>
- <th class="num">曝光</th><th class="num">点赞</th><th class="num">评论</th><th class="num">互动</th>
- <th>笔记</th>
- </tr></thead>
- <tbody>${detailKolRows}</tbody>
- </table>
- </div>
- <h3 class="h2">KOC Top 50 (按互动降序)</h3>
- <div class="scroll-table">
- <table>
- <thead><tr>
- <th class="num">#</th><th>KOC</th><th>分层</th><th>内容形态</th><th>区域</th>
- <th class="num">粉丝</th><th class="num">点赞</th><th class="num">评论</th><th class="num">互动</th><th class="num">真实评论</th>
- </tr></thead>
- <tbody>${detailKocTop}</tbody>
- </table>
- </div>
- </div>
- </section>
- <!-- ============= 3. 话题数据 ============= -->
- <section id="sec-3" data-num="03 / TOPICS">
- <div class="inner">
- <div class="eyebrow">Section 03</div>
- <h2 class="h1">话题数据 — 评论 VOC 主题分布</h2>
- <p class="sub">基于 ${allComments.length} 条真实评论 (含 ${noiseComments.length} 条互助互赞噪声 = ${noiseRate.toFixed(2)}%),按 ${TOPICS.length} 个主题进行规则化标注。**新鲜菜主题命中 ${topicHits['新菜/新鲜菜/产品创新'].length} 条,核心心智渗透成功**。</p>
- <div class="grid grid-2">
- <div class="card"><div id="chart-topics" class="chart-tall"></div></div>
- <div class="card">
- <h3 class="h2" style="margin-top:0">主题命中明细</h3>
- <table>
- <thead><tr><th>主题</th><th class="num">命中</th><th class="num">占比</th><th>典型样本</th></tr></thead>
- <tbody>${topicRows}</tbody>
- </table>
- </div>
- </div>
- <h3 class="h2">真实评论原声 (Top 16)</h3>
- <p class="sub">产品强相关 · 多主题轮询 · 已剔除噪声/托式营销/博主自评,展示具有产品对话价值的真实评论。</p>
- <div class="grid grid-2">${realCommentList}</div>
- </div>
- </section>
- <!-- ============= 4. 品牌相关词存量 ============= -->
- <section id="sec-4" data-num="04 / WORDS">
- <div class="inner">
- <div class="eyebrow">Section 04</div>
- <h2 class="h1">品牌相关词存量 — 心智渗透盘点</h2>
- <p class="sub">从 ${allComments.length} 条评论文本提取,显示品牌词、活动词、品类词在用户对话中的实际渗透情况。</p>
- <div class="grid grid-2">
- <div class="card card-accent">
- <h3 class="h2" style="margin-top:0">① 品牌词</h3>
- <p class="text-sm text-3">马记永 / 兰州牛肉面 / 马记永兰州 等核心品牌词</p>
- <div id="cloud-brand" class="chart"></div>
- </div>
- <div class="card card-green">
- <h3 class="h2" style="margin-top:0">② 活动相关词</h3>
- <p class="text-sm text-3">新鲜菜 / 春日新品 / 联名 / 试吃 等本期投放活动词</p>
- <div id="cloud-activity" class="chart"></div>
- </div>
- <div class="card card-blue">
- <h3 class="h2" style="margin-top:0">③ 品类词占位</h3>
- <p class="text-sm text-3">兰州拉面 / 西北菜 / 凉拌菜 等品类心智词</p>
- <div id="cloud-category" class="chart"></div>
- </div>
- <div class="card card-rose">
- <h3 class="h2" style="margin-top:0">④ 评论高频词云</h3>
- <p class="text-sm text-3">从全部评论文本提取的高频关键词</p>
- <div id="cloud-comments" class="chart"></div>
- </div>
- </div>
- <h3 class="h2">关键洞察</h3>
- <div class="grid grid-3">
- <div class="card card-green">
- <div class="tag green">✓ 品牌词渗透稳定</div>
- <p style="margin-top:14px;color:var(--text-2)">"马记永"作为品牌词在评论中高频自然出现,说明品牌已形成稳定心智占位,基础好。</p>
- </div>
- <div class="card card-accent">
- <div class="tag">✓ 活动词"新鲜菜"成功</div>
- <p style="margin-top:14px;color:var(--text-2)">"新鲜菜"在评论中超越价格主题,本轮"新鲜菜"心智宣发核心目标达成,继续放大可形成长期资产。</p>
- </div>
- <div class="card card-rose">
- <div class="tag rose">⚠ 竞品词出现</div>
- <p style="margin-top:14px;color:var(--text-2)">"陈香贵 / 张拉拉"等竞品在评论中被自然提及,需要建立心智份额对照基线持续监测。</p>
- </div>
- </div>
- </div>
- </section>
- <!-- ============= 5. 优点 / 不足 / 建议 ============= -->
- <section id="sec-5" data-num="05 / VERDICT">
- <div class="inner">
- <div class="eyebrow">Section 05</div>
- <h2 class="h1">本次宣发的优点、不足 & 后续投放建议</h2>
- <h3 class="h2"><span class="tag green">✓ 优点</span></h3>
- <div class="grid grid-2">
- ${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('')}
- </div>
- <h3 class="h2"><span class="tag rose">✗ 不足</span></h3>
- <div class="grid grid-2">
- ${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('')}
- </div>
- <h3 class="h2"><span class="tag">→ 后续投放建议</span></h3>
- <div class="card">
- ${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('')}
- </div>
- <h3 class="h2">下一轮预算建议分配</h3>
- <div class="grid grid-4">
- <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>
- <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>
- <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>
- <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>
- </div>
- </div>
- </section>
- <!-- ============= 6. 新机会挖掘 ============= -->
- <section id="sec-6" data-num="06 / OPPORTUNITIES">
- <div class="inner">
- <div class="eyebrow" style="color:var(--rose)">Section 06 · ★ 重中之重</div>
- <h2 class="h1">新机会挖掘 — 从 ${allComments.length} 条评论逆向反推 ${OPPORTUNITIES.length} 个新机会</h2>
- <p class="sub">**结案报告告诉老板"过去花得值不值",本节告诉团队"下一步该怎么花得更值"。** 把每一条评论当成一次市场对话逐字解读,从微观信号反推产品/内容/人群/投放/数据 5 大象限的机会缺口。完整逐条解读见配套文档 <a href="./majiyong-new-opportunities.md" style="color:var(--amber);text-decoration:underline">majiyong-new-opportunities.md</a>。</p>
- <h3 class="h2">⭐ Top 5 P0 优先级 (下周可启动)</h3>
- <div class="grid grid-3">
- ${TOP5.map((id, idx) => {
- const o = OPPORTUNITIES.find((x) => x.id === id);
- return `<div class="card card-rose" style="position:relative">
- <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>
- <div class="tag rose" style="margin-bottom:10px">${o.id} · ${o.p}</div>
- <h4 style="font-size:1.1rem;margin-bottom:10px">${esc(o.t)}</h4>
- <p style="color:var(--text-3);font-size:0.78rem;margin-bottom:8px"><strong style="color:var(--text-2)">来源信号:</strong> ${esc(o.signal)}</p>
- <p style="color:var(--text-2);font-size:0.88rem;line-height:1.6">${esc(o.action)}</p>
- </div>`;
- }).join('')}
- </div>
- <p class="sub" style="margin-top:32px">完整 ${OPPORTUNITIES.length} 个新机会按 5 大象限分独立章节深度展开,见下方 Section 06.A ~ 06.E。</p>
- <div class="toc" style="margin-top:24px">
- <a href="#sec-6a" style="border-color:var(--amber);color:var(--amber)">06.A 产品卖点 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('A')).length})</a>
- <a href="#sec-6b" style="border-color:var(--green);color:var(--green)">06.B 内容形态 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('B')).length})</a>
- <a href="#sec-6c" style="border-color:var(--purple);color:var(--purple)">06.C 人群渠道 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('C')).length})</a>
- <a href="#sec-6d" style="border-color:var(--blue);color:var(--blue)">06.D 投放机制 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('D')).length})</a>
- <a href="#sec-6e" style="border-color:var(--rose);color:var(--rose)">06.E 数据机制 (${OPPORTUNITIES.filter((o)=>o.cat.startsWith('E')).length})</a>
- </div>
- </div>
- </section>
- ${(() => {
- const QUADRANTS = [
- { letter: 'A', cat: 'A · 产品卖点', color: 'amber', tagClass: '', accent: 'card-accent', desc: '从用户评论里反推的产品/SKU 级机会 — 把"已经被用户说出口"的需求变成可上架的菜单/套餐/卖点。' },
- { letter: 'B', cat: 'B · 内容形态', color: 'green', tagClass: 'green', accent: 'card-green', desc: '内容打法升级 — 同样的预算,如何用更高 ROI 的内容形态,把好的东西讲得更让人记得住。' },
- { letter: 'C', cat: 'C · 人群渠道', color: 'purple', tagClass: 'purple', accent: '', desc: '人群和渠道的二八优化 — 让每一分预算都投在"会真去吃"的人身上,而不是"看了没法吃"的远方观众。' },
- { letter: 'D', cat: 'D · 投放机制', color: 'blue', tagClass: 'blue', accent: 'card-blue', desc: '投放过程的机制化 — 把"凭感觉发笔记"变成"算法驱动止损/加投/预警",每一轮都比上一轮更聪明。' },
- { letter: 'E', cat: 'E · 数据机制', color: 'rose', tagClass: 'rose', accent: 'card-rose', desc: '长效数据资产 — 主题词典、竞品对照,这些是可以持续复用 10 轮投放的底层基建。' },
- ];
- return QUADRANTS.map((q) => {
- const list = OPPORTUNITIES.filter((o) => o.cat === q.cat);
- return `
- <!-- ============= 6.${q.letter} ${q.cat} ============= -->
- <section id="sec-6${q.letter.toLowerCase()}" data-num="06.${q.letter} / ${q.cat.split('·')[1].trim()}" style="justify-content:flex-start">
- <div class="inner">
- <div class="eyebrow" style="color:var(--${q.color})">Section 06.${q.letter} · ${q.cat}</div>
- <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>
- <p class="sub">${esc(q.desc)}</p>
- <div class="grid" style="gap:28px;margin-top:32px">
- ${list.map((o, idx) => `
- <div class="card ${q.accent}" style="position:relative">
- <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>
- <div style="display:flex;gap:10px;align-items:center;margin-bottom:14px;flex-wrap:wrap">
- <span class="tag ${q.tagClass}" style="font-family:var(--font-mono);font-size:0.8rem">${o.id}</span>
- <span class="tag ${o.p === 'P0' ? 'rose' : o.p === 'P1' ? '' : 'purple'}">${o.p}</span>
- <h3 style="font-family:var(--font-head);font-size:1.4rem;font-weight:700;letter-spacing:-0.01em">${esc(o.t)}</h3>
- </div>
- <div class="grid grid-3" style="gap:18px;margin-top:18px">
- <div style="background:var(--bg-0);border-radius:8px;padding:18px;border-left:2px solid var(--${q.color})">
- <div class="metric-label" style="margin-bottom:8px">📍 信号 / Signal</div>
- <p style="color:var(--text-2);font-size:0.88rem;line-height:1.65">${esc(o.signal)}</p>
- </div>
- <div style="background:var(--bg-0);border-radius:8px;padding:18px;border-left:2px solid var(--${q.color})">
- <div class="metric-label" style="margin-bottom:8px">🎯 行动 / Action</div>
- <p style="color:var(--text-1);font-size:0.88rem;line-height:1.65;font-weight:500">${esc(o.action)}</p>
- </div>
- <div style="background:var(--bg-0);border-radius:8px;padding:18px;border-left:2px solid var(--${q.color})">
- <div class="metric-label" style="margin-bottom:8px">� 预期 / Outcome</div>
- <p style="color:var(--text-2);font-size:0.88rem;line-height:1.65">${esc(o.outcome || '-')}</p>
- </div>
- </div>
- <div style="margin-top:22px;padding:20px;background:var(--bg-0);border-radius:8px">
- <div class="metric-label" style="margin-bottom:10px">💡 为什么要做 / Why</div>
- <p style="color:var(--text-2);font-size:0.92rem;line-height:1.75">${esc(o.why || '-')}</p>
- </div>
- ${Array.isArray(o.how) && o.how.length ? `
- <div style="margin-top:18px;padding:20px;background:var(--bg-0);border-radius:8px">
- <div class="metric-label" style="margin-bottom:12px">🛠️ 如何执行 / How (${o.how.length} 步)</div>
- <ol style="margin:0;padding-left:24px;color:var(--text-2);font-size:0.9rem;line-height:1.85">
- ${o.how.map((step) => `<li style="margin-bottom:6px">${esc(step)}</li>`).join('')}
- </ol>
- </div>
- ` : ''}
- </div>
- `).join('')}
- </div>
- </div>
- </section>`;
- }).join('');
- })()}
- <!-- ============= 页脚 ============= -->
- <section style="min-height:auto;padding:60px 24px;background:var(--bg-1)" data-num="END">
- <div class="inner" style="text-align:center">
- <p class="text-sm text-3">马记永·四月「新鲜菜」KOL/KOC 宣发结案报告 · 基于 ${summary.total} 篇笔记 + ${allComments.length} 条评论真实数据生成</p>
- <p class="text-xs text-3 mono" style="margin-top:8px">Generated ${new Date().toISOString().slice(0,19).replace('T',' ')}</p>
- </div>
- </section>
- </main>
- <script>
- const TIER_DATA = ${JSON.stringify(tierStats)};
- const TYPE_DATA = ${JSON.stringify(typeStats.slice(0, 12))};
- const REGION_DATA = ${JSON.stringify(topRegions)};
- const TOPIC_DATA = ${JSON.stringify(TOPICS.map((t) => ({ tag: t.tag, color: t.color, count: topicHits[t.tag].length })))};
- const CLOUD_BRAND = ${JSON.stringify(cloudBrand)};
- const CLOUD_ACTIVITY = ${JSON.stringify(cloudActivity)};
- const CLOUD_CATEGORY = ${JSON.stringify(cloudCategory)};
- const CLOUD_COMMENTS = ${JSON.stringify(wordcloudComments.slice(0, 80).map((d) => ({ name: d.name, value: d.value })))};
- const COMMON = { backgroundColor: 'transparent', textStyle: { fontFamily: 'Inter, Noto Sans SC, system-ui', color: '#A8A8A8' } };
- function initWhenReady(){
- if (!window.echarts) { setTimeout(initWhenReady, 200); return; }
- echarts.init(document.getElementById('chart-tier-engagement')).setOption({ ...COMMON,
- title: { text: '量级 · 总互动 / 篇数', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
- tooltip: { trigger:'axis' }, legend: { textStyle:{color:'#A8A8A8'}, top:0, right:0 },
- grid: { left:60, right:30, bottom:50, top:60 },
- xAxis: { type:'category', data: TIER_DATA.map(d=>d.tier), axisLabel:{ color:'#A8A8A8', interval:0, rotate:15 } },
- yAxis: [{ type:'value', name:'总互动', axisLabel:{color:'#A8A8A8'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}}, nameTextStyle:{color:'#A8A8A8'} },
- { type:'value', name:'篇数', axisLabel:{color:'#A8A8A8'}, splitLine:{show:false}, nameTextStyle:{color:'#A8A8A8'} }],
- series: [{ name:'总互动', type:'bar', data: TIER_DATA.map(d=>d.eng), itemStyle:{color:'#F5A623', borderRadius:[4,4,0,0]} },
- { name:'篇数', type:'line', yAxisIndex:1, data: TIER_DATA.map(d=>d.count), itemStyle:{color:'#00DC82'}, smooth:true }]
- });
- echarts.init(document.getElementById('chart-tier-rate')).setOption({ ...COMMON,
- title: { text: '量级 · 互动率 (%)', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
- tooltip: { trigger:'axis', formatter: (p)=> p[0].name+'<br/>'+p[0].value.toFixed(2)+'%' },
- grid: { left:60, right:30, bottom:50, top:60 },
- xAxis: { type:'category', data: TIER_DATA.map(d=>d.tier), axisLabel:{color:'#A8A8A8', interval:0, rotate:15} },
- yAxis: { type:'value', axisLabel:{color:'#A8A8A8', formatter:'{value}%'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}} },
- series: [{ type:'bar', data: TIER_DATA.map(d=>+d.rate.toFixed(2)), itemStyle:{color:'#8B5CF6', borderRadius:[4,4,0,0]} }]
- });
- echarts.init(document.getElementById('chart-type-engagement')).setOption({ ...COMMON,
- title: { text: '内容形态 · 总互动 (Top 12)', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
- tooltip: { trigger:'axis' }, grid: { left:140, right:30, bottom:30, top:50 },
- xAxis: { type:'value', axisLabel:{color:'#A8A8A8'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}} },
- yAxis: { type:'category', data: TYPE_DATA.map(d=>d.type), axisLabel:{color:'#A8A8A8', fontSize:11} },
- 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} }]
- });
- echarts.init(document.getElementById('chart-region')).setOption({ ...COMMON,
- title: { text: '区域分布 · 篇数 Top 12', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
- tooltip: { trigger:'axis' }, grid: { left:60, right:30, bottom:50, top:50 },
- xAxis: { type:'category', data: REGION_DATA.map(d=>d.region), axisLabel:{color:'#A8A8A8'} },
- yAxis: { type:'value', axisLabel:{color:'#A8A8A8'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}} },
- series: [{ type:'bar', data: REGION_DATA.map(d=>d.count), itemStyle:{color:'#00DC82', borderRadius:[4,4,0,0]} }]
- });
- echarts.init(document.getElementById('chart-topics')).setOption({ ...COMMON,
- title: { text: '评论主题命中分布', left: 0, textStyle: { color:'#F5F5F5', fontSize:14, fontWeight:600 } },
- tooltip: { trigger:'axis' }, grid: { left:140, right:40, bottom:30, top:50 },
- xAxis: { type:'value', axisLabel:{color:'#A8A8A8'}, splitLine:{lineStyle:{color:'rgba(255,255,255,0.05)'}} },
- yAxis: { type:'category', data: TOPIC_DATA.map(d=>d.tag), axisLabel:{color:'#A8A8A8', fontSize:12} },
- series: [{ type:'bar', data: TOPIC_DATA.map(d=>({value:d.count, itemStyle:{color:d.color, borderRadius:[0,4,4,0]}})),
- label:{show:true, position:'right', color:'#F5F5F5', fontSize:12} }]
- });
- function wordcloud(id, data, colors){
- echarts.init(document.getElementById(id)).setOption({ ...COMMON,
- tooltip: { show: true, formatter: (p)=> p.name + ' · ' + p.value },
- series: [{ type:'wordCloud', shape:'circle', sizeRange:[12,56], rotationRange:[-30,30], rotationStep:30, gridSize:8, drawOutOfBound:false, layoutAnimation:true,
- textStyle:{ fontFamily:'Noto Sans SC, Inter, sans-serif', fontWeight:600, color: () => colors[Math.floor(Math.random()*colors.length)] },
- emphasis:{ textStyle:{ color:'#F5A623', shadowBlur:10, shadowColor:'rgba(245,166,35,0.5)' } },
- data: data }]
- });
- }
- wordcloud('cloud-brand', CLOUD_BRAND, ['#F5A623','#FFB849','#FFC97D']);
- wordcloud('cloud-activity', CLOUD_ACTIVITY, ['#00DC82','#34E899','#5EEAB1']);
- wordcloud('cloud-category', CLOUD_CATEGORY, ['#3B82F6','#60A5FA','#93C5FD']);
- wordcloud('cloud-comments', CLOUD_COMMENTS, ['#FF4D8D','#FF7AAB','#FFA3C2','#A8A8A8']);
- }
- initWhenReady();
- </script>
- </body>
- </html>`;
- fs.writeFileSync(OUT_HTML, HTML, 'utf8');
- console.log('✓ HTML:', OUT_HTML, Math.round(HTML.length / 1024) + 'KB');
- // ---------- MD ----------
- const md = [];
- const W = (s) => md.push(s);
- W('# 马记永 · 四月「新鲜菜」KOL/KOC 宣发结案报告');
- W('');
- W(`> **投放期间**: 2026-04 · **生成时间**: ${new Date().toISOString().slice(0, 19).replace('T', ' ')} `);
- W(`> **数据范围**: ${summary.total} 篇报备笔记 (KOL ${summary.kolCount} + KOC ${summary.kocCount}) + ${allComments.length} 条公开评论 (覆盖 ${items.filter((x) => (x._comments || []).length > 0).length}/${summary.total} 篇)`);
- W('');
- W('---');
- W('');
- W('## 一、数据汇总');
- W('');
- W('| 指标 | 数值 | 备注 |');
- W('|---|---:|---|');
- W(`| 总报备笔记 | ${summary.total} | KOL ${summary.kolCount} + KOC ${summary.kocCount} |`);
- W(`| KOL 总预算 | ¥${fmt(summary.kolSpend)} | CPM ¥${summary.cpm.toFixed(2)} |`);
- W(`| 总曝光 | ${fmt(summary.totalImpressions)} | 大盘心智渗透 |`);
- W(`| 总互动 | ${fmt(summary.totalEngagement)} | CPE ¥${summary.cpe.toFixed(2)} |`);
- W(`| 点赞 / 收藏 / 评论 (xlsx) | ${fmt(summary.totalLikes)} / ${fmt(summary.totalCollects)} / ${fmt(summary.totalXlsxComments)} | - |`);
- W(`| 整体互动率 | ${pct(summary.engagementRate)} | 行业基准 3-5% |`);
- W(`| 笔记解析成功 | ${summary.resolved}/${summary.total} | 短链失效 ${summary.failed} 篇 |`);
- W(`| 真实评论抓取 | ${allComments.length} 条 | 噪声率 ${noiseRate.toFixed(2)}% |`);
- W('');
- W('### 按量级分层产出');
- W('');
- W('| 量级 | 篇数 | 总曝光 | 总互动 | 平均互动 | 互动率 | 投入 | CPE |');
- W('|---|---:|---:|---:|---:|---:|---:|---:|');
- for (const t of tierStats) {
- W(`| ${t.tier} | ${t.count} | ${fmt(t.imp)} | ${fmt(t.eng)} | ${t.avgEng} | ${t.rate.toFixed(2)}% | ¥${fmt(t.spend)} | ${t.cpe === '-' ? '-' : '¥' + t.cpe} |`);
- }
- W('');
- W('### 按内容形态产出 (Top 12)');
- W('');
- W('| 内容形态 | 篇数 | 总曝光 | 总互动 | 平均互动 | 互动率 |');
- W('|---|---:|---:|---:|---:|---:|');
- for (const t of typeStats.slice(0, 12)) {
- W(`| ${t.type} | ${t.count} | ${fmt(t.imp)} | ${fmt(t.eng)} | ${t.avgEng} | ${t.rate.toFixed(2)}% |`);
- }
- W('');
- W('### 区域分布 Top 12');
- W('');
- W('| 区域 | 篇数 | 总互动 |');
- W('|---|---:|---:|');
- for (const r of topRegions) W(`| ${r.region} | ${r.count} | ${fmt(r.eng)} |`);
- W('');
- W('---');
- W('');
- W(`## 二、数据明细 (KOL ${summary.kolCount} 全量 + KOC Top 50)`);
- W('');
- W('### 2.1 KOL 全量');
- W('');
- W('| # | KOL | 量级 | 内容形态 | 区域 | 粉丝 | 单价 | 曝光 | 点赞 | 评论 | 互动 |');
- W('|---:|---|---|---|---|---:|---:|---:|---:|---:|---:|');
- const kolSorted = [...kolItems].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement));
- for (let i = 0; i < kolSorted.length; i++) {
- const r = kolSorted[i];
- 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)} |`);
- }
- W('');
- W('### 2.2 KOC Top 50');
- W('');
- W('| # | KOC | 分层 | 内容形态 | 区域 | 粉丝 | 点赞 | 评论 | 互动 | 真实评论 |');
- W('|---:|---|---|---|---|---:|---:|---:|---:|---:|');
- const kocTop = [...kocItems].sort((a, b) => num(b.engagementSubtotal || b.engagement) - num(a.engagementSubtotal || a.engagement)).slice(0, 50);
- for (let i = 0; i < kocTop.length; i++) {
- const r = kocTop[i];
- 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} |`);
- }
- W('');
- W('---');
- W('');
- W('## 三、话题数据 — 评论 VOC 主题分布');
- W('');
- W(`基于 **${allComments.length}** 条真实评论 (含 ${noiseComments.length} 条噪声 = ${noiseRate.toFixed(2)}%) 的规则化主题命中:`);
- W('');
- W('| 主题 | 命中 | 占比 | 典型样本 |');
- W('|---|---:|---:|---|');
- for (const t of TOPICS) {
- const hits = topicHits[t.tag];
- const sample = hits.sort((a, b) => b.like - a.like)[0];
- const sampleText = sample ? `[${sample.koc}@${sample.ip || '-'}] ${sample.content.replace(/\n/g, ' ').replace(/\|/g, '\\|').slice(0, 80)}` : '-';
- W(`| ${t.tag} | ${hits.length} | ${(hits.length / Math.max(allComments.length, 1) * 100).toFixed(1)}% | ${sampleText} |`);
- }
- W('');
- W('### 真实评论原声 Top 16 (产品强相关 · 多主题轮询)');
- W('');
- for (const { topic, c } of topPicks) {
- W(`- **[${topic}]** \`${c.koc}@${c.ip || '-'}\`: ${c.content.replace(/\n/g, ' ')}`);
- }
- W('');
- W('---');
- W('');
- W('## 四、品牌相关词存量');
- W('');
- W('### 4.1 品牌词');
- W('');
- W('| 词 | 评论命中 | 状态 |');
- W('|---|---:|---|');
- for (const w of cloudBrand) W(`| ${w.name} | ${w.value - 5 > 0 ? w.value - 5 : 0} | ${w.value > 5 ? '✓ 已渗透' : '○ 待沉淀'} |`);
- W('');
- W('### 4.2 活动相关词');
- W('');
- W('| 词 | 评论命中 | 状态 |');
- W('|---|---:|---|');
- for (const w of cloudActivity) W(`| ${w.name} | ${w.value - 5 > 0 ? w.value - 5 : 0} | ${w.value > 5 ? '✓ 已渗透' : '○ 待沉淀'} |`);
- W('');
- W('### 4.3 品类词占位');
- W('');
- W('| 词 | 评论命中 | 状态 |');
- W('|---|---:|---|');
- for (const w of cloudCategory) W(`| ${w.name} | ${w.value - 5 > 0 ? w.value - 5 : 0} | ${w.value > 5 ? '✓ 已渗透' : '○ 待沉淀'} |`);
- W('');
- W('### 4.4 评论高频词 Top 50');
- W('');
- W('| 词 | 频次 |');
- W('|---|---:|');
- for (const w of wordcloudComments.slice(0, 50)) W(`| ${w.name} | ${w.value} |`);
- W('');
- W('---');
- W('');
- W('## 五、本次宣发的优点 / 不足 / 后续建议');
- W('');
- W('### 5.1 ✓ 优点');
- W('');
- for (const p of PROS) {
- W(`**${p.title}**`);
- W('');
- W(p.text.replace(/<\/?b>/g, '**').replace(/<code>([^<]+)<\/code>/g, '`$1`'));
- W('');
- }
- W('### 5.2 ✗ 不足');
- W('');
- for (const c of CONS) {
- W(`**${c.title}**`);
- W('');
- W(c.text.replace(/<\/?b>/g, '**'));
- W('');
- }
- W('### 5.3 → 后续投放建议');
- W('');
- W('| 优先级 | 行动 | 说明 |');
- W('|---|---|---|');
- for (const r of RECS) W(`| **${r.p}** | ${r.t} | ${r.b} |`);
- W('');
- W('### 5.4 下一轮预算建议分配');
- W('');
- W('| 模块 | 占比 | 说明 |');
- W('|---|---:|---|');
- W('| 头/腰部 KOL | 35% | 2~3 位创意/跨界 KOL,延续高杠杆爆款方法论 |');
- W('| 差异化探店 KOC | 35% | 5 赛道分发 (减脂/早午餐/家庭/一人食/商务),各 15-20 篇 |');
- W('| 二创 IP 池 + 在地达人 | 15% | 沉淀长期 IP 资产 + 月度打卡官 |');
- W('| 薯条 + 评论运营 + 数据 | 15% | Top 5% 加投 + SOP + 风险词预警 + 求开店承接 |');
- W('');
- W('---');
- W('');
- W(`*本结案报告基于 ${summary.total} 篇笔记 (KOL ${summary.kolCount} + KOC ${summary.kocCount}) + ${allComments.length} 条公开评论真实数据,通过可复用 VOC 工作流自动生成。配套交互式 HTML 报告: \`majiyong-closing-report.html\` · 配套深度新机会文档: \`majiyong-new-opportunities.md\`*`);
- fs.writeFileSync(OUT_MD, md.join('\n'), 'utf8');
- console.log('✓ MD:', OUT_MD, md.length, 'lines');
- };
|