gen-raw-data-pages.js 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. /**
  2. * 原始 Excel 宣发数据 → 漂亮 HTML 单页
  3. * 输入:
  4. * shuhua/舒华跑步机四月宣发报备KOC发布数据.xlsx
  5. * majiyong/【新鲜菜项目】马记永4月宣发数据汇总(1).xlsx
  6. * 输出:
  7. * shuhua/shuhua-raw-data.html
  8. * majiyong/majiyong-raw-data.html
  9. *
  10. * 视觉风格:与 *-closing-report.html 一致(暗色 + ECharts)
  11. */
  12. const fs = require('fs');
  13. const path = require('path');
  14. const XLSX = require('xlsx');
  15. const ROOT = path.resolve(__dirname, '..');
  16. const SHUHUA_XLSX = path.join(ROOT, 'shuhua', '舒华跑步机四月宣发报备KOC发布数据.xlsx');
  17. const MAJI_XLSX = path.join(ROOT, 'majiyong', '【新鲜菜项目】马记永4月宣发数据汇总(1).xlsx');
  18. const SHUHUA_OUT = path.join(ROOT, 'shuhua', 'shuhua-raw-data.html');
  19. const MAJI_OUT = path.join(ROOT, 'majiyong', 'majiyong-raw-data.html');
  20. // ============== utils ==============
  21. const num = (v) => {
  22. const n = Number(v);
  23. return Number.isFinite(n) ? n : 0;
  24. };
  25. const esc = (s) => String(s == null ? '' : s)
  26. .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
  27. .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
  28. const fmt = (n) => {
  29. n = Number(n) || 0;
  30. if (Math.abs(n) >= 1e8) return (n / 1e8).toFixed(2) + ' 亿';
  31. if (Math.abs(n) >= 1e4) return (n / 1e4).toFixed(2) + ' 万';
  32. return n.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
  33. };
  34. const fmtInt = (n) => (Number(n) || 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
  35. const fmtMoney = (n) => '¥' + (Number(n) || 0).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
  36. // excel serial → yyyy-MM-dd
  37. const excelDate = (v) => {
  38. if (!v) return '';
  39. if (typeof v === 'string' && /\d{4}-\d{1,2}-\d{1,2}/.test(v)) return v;
  40. const n = Number(v);
  41. if (!Number.isFinite(n) || n < 30000 || n > 80000) return String(v);
  42. const ms = (n - 25569) * 86400 * 1000;
  43. const d = new Date(ms);
  44. const Y = d.getUTCFullYear();
  45. const M = String(d.getUTCMonth() + 1).padStart(2, '0');
  46. const D = String(d.getUTCDate()).padStart(2, '0');
  47. return `${Y}-${M}-${D}`;
  48. };
  49. const safeHref = (u) => {
  50. const s = String(u || '').trim();
  51. if (!s || !/^https?:\/\//i.test(s)) return '';
  52. return s;
  53. };
  54. // ============== shared CSS / shell ==============
  55. const COMMON_HEAD = (title) => `<!DOCTYPE html>
  56. <html lang="zh-CN">
  57. <head>
  58. <meta charset="UTF-8">
  59. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  60. <title>${esc(title)}</title>
  61. <link rel="preconnect" href="https://fonts.googleapis.com">
  62. <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  63. <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@500;700;800&display=swap" rel="stylesheet">
  64. <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
  65. <style>
  66. :root {
  67. --bg-0:#0A0A0A; --bg-1:#111; --bg-2:#1A1A1A; --bg-3:#252525;
  68. --border:rgba(255,255,255,0.08); --border-strong:rgba(255,255,255,0.16);
  69. --text-1:#F5F5F5; --text-2:#A8A8A8; --text-3:#6B6B6B;
  70. --amber:#F5A623; --green:#00DC82; --purple:#8B5CF6; --rose:#FF4D8D; --blue:#3B82F6; --red:#EF4444;
  71. --font-head:'Space Grotesk','Inter',-apple-system,'PingFang SC',sans-serif;
  72. --font-body:'Inter',-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;
  73. --font-mono:'JetBrains Mono',ui-monospace,monospace;
  74. }
  75. *{margin:0;padding:0;box-sizing:border-box}
  76. html{scroll-behavior:smooth}
  77. 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}
  78. body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;
  79. background:radial-gradient(ellipse 900px 700px at 20% 25%,rgba(245,166,35,0.08),transparent 60%),
  80. radial-gradient(ellipse 800px 600px at 80% 75%,rgba(139,92,246,0.06),transparent 60%)}
  81. a{color:var(--amber);text-decoration:none}
  82. a:hover{text-decoration:underline}
  83. ::selection{background:var(--amber);color:#000}
  84. .wrap{position:relative;z-index:1;max-width:1320px;margin:0 auto;padding:clamp(40px,7vw,80px) clamp(20px,5vw,60px)}
  85. .hero{padding-bottom:48px;border-bottom:1px solid var(--border);margin-bottom:48px}
  86. .eyebrow{display:inline-flex;align-items:center;gap:10px;font-family:var(--font-mono);font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;color:var(--amber);font-weight:500;margin-bottom:18px}
  87. .eyebrow::before{content:'';display:inline-block;width:24px;height:1px;background:var(--amber)}
  88. .h0{font-family:var(--font-head);font-weight:900;letter-spacing:-.04em;line-height:.98;font-size:clamp(2.4rem,6vw,4.6rem)}
  89. .lead{color:var(--text-2);font-size:1.05rem;max-width:780px;margin-top:18px;line-height:1.8}
  90. .meta-row{display:flex;gap:32px;margin-top:28px;flex-wrap:wrap}
  91. .meta-item{font-family:var(--font-mono);font-size:.82rem;color:var(--text-2)}
  92. .meta-item strong{color:var(--text-1);font-weight:600;display:block;margin-top:4px;font-size:1.1rem}
  93. section.block{margin-bottom:64px}
  94. .section-head{display:flex;align-items:baseline;gap:16px;margin-bottom:24px;flex-wrap:wrap}
  95. .section-num{font-family:var(--font-mono);font-size:.78rem;letter-spacing:.25em;color:var(--text-3)}
  96. .h1{font-family:var(--font-head);font-weight:800;letter-spacing:-.02em;line-height:1.1;font-size:clamp(1.6rem,3vw,2.2rem)}
  97. .sub{color:var(--text-2);max-width:880px;margin-bottom:24px}
  98. .grid{display:grid;gap:18px}
  99. .grid-2{grid-template-columns:repeat(auto-fit,minmax(360px,1fr))}
  100. .grid-3{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}
  101. .grid-4{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
  102. .grid-5{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}
  103. .metric{background:var(--bg-1);border:1px solid var(--border);border-radius:12px;padding:22px;position:relative;overflow:hidden;min-width:0}
  104. .metric::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,var(--amber),transparent);opacity:.6}
  105. .metric.green::before{background:linear-gradient(90deg,var(--green),transparent)}
  106. .metric.blue::before{background:linear-gradient(90deg,var(--blue),transparent)}
  107. .metric.rose::before{background:linear-gradient(90deg,var(--rose),transparent)}
  108. .metric.purple::before{background:linear-gradient(90deg,var(--purple),transparent)}
  109. .metric-label{font-family:var(--font-mono);font-size:.7rem;letter-spacing:.15em;text-transform:uppercase;color:var(--text-3);margin-bottom:10px}
  110. .metric-value{font-family:var(--font-head);font-weight:800;font-size:clamp(1.5rem,2.4vw,2.1rem);letter-spacing:-.03em;line-height:1.05;word-break:break-all}
  111. .metric-unit{font-family:var(--font-mono);font-size:.78rem;color:var(--amber);font-weight:500;margin-left:4px}
  112. .metric-sub{margin-top:12px;font-size:.78rem;color:var(--text-2);line-height:1.5}
  113. .card{background:linear-gradient(180deg,rgba(255,255,255,.025) 0%,rgba(255,255,255,.005) 100%),var(--bg-1);border:1px solid var(--border);border-radius:12px;padding:24px}
  114. .chart{width:100%;height:340px}
  115. .chart-tall{width:100%;height:440px}
  116. .controls{display:flex;gap:12px;margin-bottom:14px;flex-wrap:wrap;align-items:center}
  117. .controls input,.controls select{background:var(--bg-2);color:var(--text-1);border:1px solid var(--border);border-radius:8px;padding:9px 12px;font-family:var(--font-body);font-size:.88rem;min-width:160px}
  118. .controls input:focus,.controls select:focus{outline:none;border-color:var(--amber)}
  119. .controls .hint{color:var(--text-3);font-size:.78rem;font-family:var(--font-mono)}
  120. .scroll-table{max-height:78vh;overflow:auto;border:1px solid var(--border);border-radius:12px;background:var(--bg-1)}
  121. .scroll-table::-webkit-scrollbar{width:8px;height:8px}
  122. .scroll-table::-webkit-scrollbar-thumb{background:var(--bg-3);border-radius:4px}
  123. table{width:100%;border-collapse:collapse;font-size:.84rem}
  124. th,td{padding:9px 11px;border-bottom:1px solid var(--border);text-align:left;white-space:nowrap}
  125. th{color:var(--text-2);font-weight:500;font-size:.72rem;letter-spacing:.06em;text-transform:uppercase;background:var(--bg-2);position:sticky;top:0;z-index:2;cursor:pointer;user-select:none}
  126. th.num,td.num{text-align:right;font-family:var(--font-mono)}
  127. th .arr{color:var(--text-3);margin-left:4px;font-size:.7rem}
  128. th.active{color:var(--amber)}
  129. tr:hover td{background:rgba(255,255,255,.025)}
  130. td.link a{color:var(--amber);font-family:var(--font-mono);font-size:.78rem}
  131. .tag{display:inline-block;padding:2px 8px;border-radius:4px;font-size:.7rem;font-family:var(--font-mono);background:rgba(245,166,35,.1);color:var(--amber);border:1px solid rgba(245,166,35,.22)}
  132. .tag.g{background:rgba(0,220,130,.08);color:var(--green);border-color:rgba(0,220,130,.22)}
  133. .tag.b{background:rgba(59,130,246,.08);color:var(--blue);border-color:rgba(59,130,246,.22)}
  134. .tag.r{background:rgba(255,77,141,.08);color:var(--rose);border-color:rgba(255,77,141,.22)}
  135. .tag.p{background:rgba(139,92,246,.08);color:var(--purple);border-color:rgba(139,92,246,.22)}
  136. footer{margin-top:80px;padding-top:32px;border-top:1px solid var(--border);color:var(--text-3);font-size:.8rem;font-family:var(--font-mono);text-align:center}
  137. </style>
  138. </head><body><div class="wrap">`;
  139. const COMMON_FOOT = (footText) => `
  140. <footer>${esc(footText)}</footer>
  141. </div>
  142. <script>
  143. // 通用表格排序 + 搜索
  144. function bindTable(tableId){
  145. const tbl=document.getElementById(tableId); if(!tbl) return;
  146. const ths=tbl.querySelectorAll('thead th'); const tbody=tbl.querySelector('tbody');
  147. const rows=Array.from(tbody.querySelectorAll('tr'));
  148. let sortKey=-1, sortDir=1;
  149. ths.forEach((th,idx)=>{
  150. th.addEventListener('click',()=>{
  151. const isNum=th.classList.contains('num');
  152. if(sortKey===idx) sortDir=-sortDir; else { sortKey=idx; sortDir=1; }
  153. ths.forEach(t=>{ t.classList.remove('active'); const a=t.querySelector('.arr'); if(a)a.textContent=''; });
  154. th.classList.add('active');
  155. const arr=th.querySelector('.arr'); if(arr) arr.textContent=sortDir>0?'▲':'▼';
  156. rows.sort((a,b)=>{
  157. const av=a.children[idx].dataset.v ?? a.children[idx].textContent.trim();
  158. const bv=b.children[idx].dataset.v ?? b.children[idx].textContent.trim();
  159. if(isNum){ return (Number(av)-Number(bv))*sortDir; }
  160. return String(av).localeCompare(String(bv),'zh')*sortDir;
  161. });
  162. rows.forEach(r=>tbody.appendChild(r));
  163. });
  164. });
  165. return rows;
  166. }
  167. function bindFilter(tableId,inputId,selectIds){
  168. const tbl=document.getElementById(tableId); if(!tbl) return;
  169. const tbody=tbl.querySelector('tbody');
  170. const rows=Array.from(tbody.querySelectorAll('tr'));
  171. const inp=document.getElementById(inputId);
  172. const sels=(selectIds||[]).map(id=>document.getElementById(id)).filter(Boolean);
  173. function apply(){
  174. const q=(inp&&inp.value||'').trim().toLowerCase();
  175. const filters=sels.map(s=>({col:Number(s.dataset.col),val:s.value}));
  176. let n=0;
  177. rows.forEach(r=>{
  178. let ok=true;
  179. if(q){ ok=r.textContent.toLowerCase().includes(q); }
  180. if(ok){
  181. for(const f of filters){
  182. if(f.val && r.children[f.col] && r.children[f.col].textContent.trim()!==f.val){ ok=false; break; }
  183. }
  184. }
  185. r.style.display=ok?'':'none';
  186. if(ok) n++;
  187. });
  188. const cnt=document.getElementById(tableId+'-cnt'); if(cnt) cnt.textContent=n+' / '+rows.length;
  189. }
  190. if(inp) inp.addEventListener('input',apply);
  191. sels.forEach(s=>s.addEventListener('change',apply));
  192. apply();
  193. }
  194. </script>
  195. </body></html>`;
  196. // ============== ECharts options ==============
  197. const echartsBaseTheme = `
  198. textStyle:{color:'#A8A8A8',fontFamily:'Inter,PingFang SC'},
  199. legend:{textStyle:{color:'#A8A8A8'}},
  200. tooltip:{backgroundColor:'#1A1A1A',borderColor:'rgba(255,255,255,0.1)',textStyle:{color:'#F5F5F5'}},
  201. grid:{left:60,right:30,top:50,bottom:50},
  202. xAxis:{axisLine:{lineStyle:{color:'rgba(255,255,255,0.1)'}},axisLabel:{color:'#A8A8A8',fontSize:11},splitLine:{lineStyle:{color:'rgba(255,255,255,0.04)'}}},
  203. yAxis:{axisLine:{lineStyle:{color:'rgba(255,255,255,0.1)'}},axisLabel:{color:'#A8A8A8',fontSize:11},splitLine:{lineStyle:{color:'rgba(255,255,255,0.04)'}}}
  204. `;
  205. // ============== SHUHUA builder ==============
  206. function buildShuhua() {
  207. const wb = XLSX.readFile(SHUHUA_XLSX);
  208. const sheet = wb.Sheets[wb.SheetNames[0]];
  209. const rows = XLSX.utils.sheet_to_json(sheet, { defval: '' });
  210. const items = rows.map((r) => ({
  211. idx: num(r['序号']),
  212. type: String(r['类型'] || '').trim(),
  213. name: String(r['名称'] || '').trim(),
  214. homepage: String(r['主页链接'] || '').trim(),
  215. accountId: String(r['账号ID'] || '').trim(),
  216. followers: num(r['粉丝']),
  217. liked: num(r['赞藏']),
  218. imgPrice: num(r['图文']),
  219. imgOrderPrice: num(r['图文下单价']),
  220. textPrice: num(r['写文']),
  221. priceTotal: num(r['合计含税直发价格']),
  222. publishDate: excelDate(r['发布时间']),
  223. contentType: String(r['内容类型'] || '').trim() || '未分类',
  224. publishLink: String(r['发布链接'] || '').trim(),
  225. impressions: num(r['曝光量']),
  226. reads: num(r['阅读量']),
  227. likes: num(r['点赞']),
  228. collects: num(r['收藏']),
  229. comments: num(r['评论']),
  230. engagement: num(r['互动总量']),
  231. }));
  232. const sum = items.reduce((a, r) => ({
  233. spend: a.spend + r.priceTotal,
  234. imp: a.imp + r.impressions,
  235. reads: a.reads + r.reads,
  236. likes: a.likes + r.likes,
  237. collects: a.collects + r.collects,
  238. comments: a.comments + r.comments,
  239. eng: a.eng + r.engagement,
  240. followers: a.followers + r.followers,
  241. }), { spend: 0, imp: 0, reads: 0, likes: 0, collects: 0, comments: 0, eng: 0, followers: 0 });
  242. const total = {
  243. kocs: items.length,
  244. ...sum,
  245. engRate: sum.reads ? (sum.eng / sum.reads * 100) : 0,
  246. cpe: sum.eng ? (sum.spend / sum.eng) : 0,
  247. cpm: sum.imp ? (sum.spend * 1000 / sum.imp) : 0,
  248. avgFans: items.length ? Math.round(sum.followers / items.length) : 0,
  249. };
  250. // 内容类型聚合
  251. const byCt = {};
  252. for (const r of items) {
  253. const k = r.contentType || '未分类';
  254. if (!byCt[k]) byCt[k] = { count: 0, eng: 0, reads: 0, spend: 0 };
  255. byCt[k].count++;
  256. byCt[k].eng += r.engagement;
  257. byCt[k].reads += r.reads;
  258. byCt[k].spend += r.priceTotal;
  259. }
  260. const ctStats = Object.entries(byCt).map(([k, v]) => ({ name: k, ...v })).sort((a, b) => b.eng - a.eng);
  261. // 类型聚合(达人级别)
  262. const byTp = {};
  263. for (const r of items) {
  264. const k = r.type || '未分类';
  265. if (!byTp[k]) byTp[k] = { count: 0, eng: 0, spend: 0 };
  266. byTp[k].count++;
  267. byTp[k].eng += r.engagement;
  268. byTp[k].spend += r.priceTotal;
  269. }
  270. const tpStats = Object.entries(byTp).map(([k, v]) => ({ name: k, ...v }));
  271. const ctOptions = [...new Set(items.map(r => r.contentType))].filter(Boolean).sort();
  272. const tpOptions = [...new Set(items.map(r => r.type))].filter(Boolean).sort();
  273. const tableRows = items.slice().sort((a, b) => b.engagement - a.engagement).map((r) => {
  274. const href = safeHref(r.publishLink);
  275. const homeHref = safeHref(r.homepage);
  276. return `<tr>
  277. <td data-v="${r.idx}">${r.idx}</td>
  278. <td><span class="tag b">${esc(r.type)}</span></td>
  279. <td>${homeHref ? `<a href="${esc(homeHref)}" target="_blank">${esc(r.name)}</a>` : esc(r.name)}</td>
  280. <td><span class="tag">${esc(r.contentType)}</span></td>
  281. <td>${esc(r.publishDate)}</td>
  282. <td class="num" data-v="${r.followers}">${fmtInt(r.followers)}</td>
  283. <td class="num" data-v="${r.liked}">${fmtInt(r.liked)}</td>
  284. <td class="num" data-v="${r.priceTotal}">${fmtMoney(r.priceTotal)}</td>
  285. <td class="num" data-v="${r.impressions}">${fmtInt(r.impressions)}</td>
  286. <td class="num" data-v="${r.reads}">${fmtInt(r.reads)}</td>
  287. <td class="num" data-v="${r.likes}">${fmtInt(r.likes)}</td>
  288. <td class="num" data-v="${r.collects}">${fmtInt(r.collects)}</td>
  289. <td class="num" data-v="${r.comments}">${fmtInt(r.comments)}</td>
  290. <td class="num" data-v="${r.engagement}"><strong style="color:var(--amber)">${fmtInt(r.engagement)}</strong></td>
  291. <td class="link">${href ? `<a href="${esc(href)}" target="_blank">打开</a>` : '<span style="color:var(--text-3)">—</span>'}</td>
  292. </tr>`;
  293. }).join('');
  294. const scatterData = JSON.stringify(items.map(r => [r.followers, r.engagement, r.name, r.contentType]));
  295. const ctNames = JSON.stringify(ctStats.map(x => x.name));
  296. const ctEng = JSON.stringify(ctStats.map(x => x.eng));
  297. const ctCnt = JSON.stringify(ctStats.map(x => x.count));
  298. const tpPie = JSON.stringify(tpStats.map(x => ({ name: x.name, value: x.count })));
  299. const html = COMMON_HEAD('舒华跑步机 · 四月 KOC 宣发原始数据') + `
  300. <section class="hero">
  301. <div class="eyebrow">SHUHUA · APRIL 2025 · RAW DATA</div>
  302. <h1 class="h0">舒华跑步机<br/>四月 KOC 宣发 · 原始数据</h1>
  303. <p class="lead">数据源:<code style="color:var(--text-1)">舒华跑步机四月宣发报备KOC发布数据.xlsx</code>,共 <strong style="color:var(--amber)">${total.kocs}</strong> 位 KOC 达人。本页对原始 Excel 字段进行结构化重构,所有指标可即时筛选、排序、跳转原帖。</p>
  304. <div class="meta-row">
  305. <div class="meta-item">达人数<strong>${total.kocs}</strong></div>
  306. <div class="meta-item">总曝光<strong>${fmt(total.imp)}</strong></div>
  307. <div class="meta-item">总互动<strong>${fmt(total.eng)}</strong></div>
  308. <div class="meta-item">总投放<strong>${fmtMoney(total.spend)}</strong></div>
  309. <div class="meta-item">互动率<strong>${total.engRate.toFixed(2)}%</strong></div>
  310. </div>
  311. </section>
  312. <section class="block">
  313. <div class="section-head"><span class="section-num">01 / OVERVIEW</span><h2 class="h1">核心指标总览</h2></div>
  314. <div class="grid grid-4">
  315. <div class="metric"><div class="metric-label">KOC 达人</div><div class="metric-value">${total.kocs}<span class="metric-unit">位</span></div><div class="metric-sub">平均粉丝 ${fmtInt(total.avgFans)}</div></div>
  316. <div class="metric green"><div class="metric-label">总曝光量</div><div class="metric-value">${fmt(total.imp)}</div><div class="metric-sub">CPM ${total.cpm.toFixed(2)} 元</div></div>
  317. <div class="metric blue"><div class="metric-label">总阅读量</div><div class="metric-value">${fmt(total.reads)}</div><div class="metric-sub">点赞 ${fmtInt(total.likes)} · 收藏 ${fmtInt(total.collects)} · 评论 ${fmtInt(total.comments)}</div></div>
  318. <div class="metric rose"><div class="metric-label">总互动量</div><div class="metric-value">${fmt(total.eng)}</div><div class="metric-sub">互动率 ${total.engRate.toFixed(2)}% · CPE ${total.cpe.toFixed(2)} 元</div></div>
  319. <div class="metric purple"><div class="metric-label">总投放金额</div><div class="metric-value">${fmtMoney(total.spend)}</div><div class="metric-sub">单 KOC 均价 ${fmtMoney(total.spend / total.kocs)}</div></div>
  320. </div>
  321. </section>
  322. <section class="block">
  323. <div class="section-head"><span class="section-num">02 / CHARTS</span><h2 class="h1">维度可视化</h2></div>
  324. <div class="grid grid-2">
  325. <div class="card"><div style="margin-bottom:10px;color:var(--text-2);font-size:.86rem">内容类型 · 互动量与篇数</div><div id="ct-bar" class="chart"></div></div>
  326. <div class="card"><div style="margin-bottom:10px;color:var(--text-2);font-size:.86rem">达人级别分布</div><div id="tp-pie" class="chart"></div></div>
  327. <div class="card" style="grid-column:1/-1"><div style="margin-bottom:10px;color:var(--text-2);font-size:.86rem">粉丝量 × 互动量 散点(每点 = 1 KOC)</div><div id="fans-eng" class="chart-tall"></div></div>
  328. </div>
  329. </section>
  330. <section class="block">
  331. <div class="section-head"><span class="section-num">03 / TABLE</span><h2 class="h1">明细数据表</h2></div>
  332. <p class="sub">点击表头即可排序,支持全字段关键词搜索 + 类型/内容类型筛选。默认按 <strong style="color:var(--amber)">互动总量</strong> 降序。</p>
  333. <div class="controls">
  334. <input id="sh-q" placeholder="🔍 搜索昵称 / 类型 / 内容类型…">
  335. <select id="sh-fc" data-col="3"><option value="">全部内容类型</option>${ctOptions.map(o => `<option>${esc(o)}</option>`).join('')}</select>
  336. <select id="sh-ft" data-col="1"><option value="">全部达人级别</option>${tpOptions.map(o => `<option>${esc(o)}</option>`).join('')}</select>
  337. <span class="hint" id="sh-tbl-cnt"></span>
  338. </div>
  339. <div class="scroll-table"><table id="sh-tbl"><thead><tr>
  340. <th class="num">#<span class="arr"></span></th>
  341. <th>级别<span class="arr"></span></th>
  342. <th>昵称<span class="arr"></span></th>
  343. <th>内容类型<span class="arr"></span></th>
  344. <th>发布日期<span class="arr"></span></th>
  345. <th class="num">粉丝<span class="arr"></span></th>
  346. <th class="num">赞藏<span class="arr"></span></th>
  347. <th class="num">投放价<span class="arr"></span></th>
  348. <th class="num">曝光<span class="arr"></span></th>
  349. <th class="num">阅读<span class="arr"></span></th>
  350. <th class="num">点赞<span class="arr"></span></th>
  351. <th class="num">收藏<span class="arr"></span></th>
  352. <th class="num">评论<span class="arr"></span></th>
  353. <th class="num">互动<span class="arr"></span></th>
  354. <th>原帖<span class="arr"></span></th>
  355. </tr></thead><tbody>${tableRows}</tbody></table></div>
  356. </section>
  357. <script>
  358. echarts.init(document.getElementById('ct-bar')).setOption({
  359. ${echartsBaseTheme},
  360. legend:{data:['互动量','篇数'],top:0},
  361. xAxis:{type:'category',data:${ctNames},axisLabel:{interval:0,rotate:30}},
  362. yAxis:[{type:'value',name:'互动量'},{type:'value',name:'篇数'}],
  363. series:[
  364. {type:'bar',name:'互动量',data:${ctEng},itemStyle:{color:'#F5A623',borderRadius:[4,4,0,0]}},
  365. {type:'line',name:'篇数',yAxisIndex:1,data:${ctCnt},itemStyle:{color:'#00DC82'},lineStyle:{width:2},symbol:'circle',symbolSize:8}
  366. ]
  367. });
  368. echarts.init(document.getElementById('tp-pie')).setOption({
  369. ${echartsBaseTheme},
  370. tooltip:{trigger:'item'},
  371. legend:{bottom:0,textStyle:{color:'#A8A8A8'}},
  372. series:[{type:'pie',radius:['45%','72%'],center:['50%','45%'],
  373. label:{color:'#A8A8A8',formatter:'{b}\\n{d}%'},
  374. itemStyle:{borderColor:'#0A0A0A',borderWidth:2},
  375. data:${tpPie}.map((d,i)=>({...d,itemStyle:{color:['#F5A623','#8B5CF6','#3B82F6','#00DC82','#FF4D8D','#EF4444'][i%6]}}))
  376. }]
  377. });
  378. echarts.init(document.getElementById('fans-eng')).setOption({
  379. ${echartsBaseTheme},
  380. tooltip:{trigger:'item',formatter:p=>p.data[2]+'<br/>粉丝: '+p.data[0].toLocaleString()+'<br/>互动: '+p.data[1].toLocaleString()+'<br/>类型: '+p.data[3]},
  381. xAxis:{type:'value',name:'粉丝量',axisLabel:{formatter:v=>v>=10000?(v/10000).toFixed(0)+'w':v}},
  382. yAxis:{type:'value',name:'互动量'},
  383. series:[{type:'scatter',data:${scatterData},symbolSize:d=>Math.max(6,Math.min(28,Math.sqrt(d[1])/2)),itemStyle:{color:'#F5A623',opacity:.75,borderColor:'#0A0A0A',borderWidth:1}}]
  384. });
  385. bindTable('sh-tbl');
  386. bindFilter('sh-tbl','sh-q',['sh-fc','sh-ft']);
  387. </script>` + COMMON_FOOT('舒华跑步机 · 四月 KOC 宣发原始数据 · Generated from 舒华跑步机四月宣发报备KOC发布数据.xlsx');
  388. fs.writeFileSync(SHUHUA_OUT, html, 'utf8');
  389. console.log('✅ 舒华原始数据页生成:', SHUHUA_OUT, `(${(fs.statSync(SHUHUA_OUT).size / 1024).toFixed(1)} KB)`);
  390. }
  391. // ============== MAJIYONG builder ==============
  392. function buildMajiyong() {
  393. const wb = XLSX.readFile(MAJI_XLSX);
  394. // 数据汇总
  395. const summaryRows = XLSX.utils.sheet_to_json(wb.Sheets['数据汇总'], { defval: '' });
  396. const summary = summaryRows.filter(r => r['合作量级']).map(r => ({
  397. tier: String(r['合作量级']).trim(),
  398. count: num(r['合作数量']),
  399. eng: num(r['互动量']),
  400. imp: num(r['曝光量']),
  401. spend: num(r['达人采购费用']),
  402. adSpend: num(r['投流费用']),
  403. cpe: num(r['单次互动成本']),
  404. cpm: num(r['千次曝光成本']),
  405. }));
  406. const grandTotal = summary.find(r => /合计|总计|总和/.test(r.tier)) || summary.reduce((a, r) => ({
  407. tier: '合计', count: a.count + r.count, eng: a.eng + r.eng, imp: a.imp + r.imp,
  408. spend: a.spend + r.spend, adSpend: a.adSpend + r.adSpend, cpe: 0, cpm: 0,
  409. }), { tier: '合计', count: 0, eng: 0, imp: 0, spend: 0, adSpend: 0, cpe: 0, cpm: 0 });
  410. const totalSpend = grandTotal.spend + grandTotal.adSpend;
  411. grandTotal.cpe = grandTotal.eng ? totalSpend / grandTotal.eng : 0;
  412. grandTotal.cpm = grandTotal.imp ? totalSpend * 1000 / grandTotal.imp : 0;
  413. const summaryList = summary.filter(r => !/合计|总计|总和/.test(r.tier));
  414. // KOL
  415. const kolRowsRaw = XLSX.utils.sheet_to_json(wb.Sheets['KOL合作表'], { defval: '' });
  416. const kol = kolRowsRaw.filter(r => r['序号'] && typeof r['序号'] === 'number').map(r => ({
  417. idx: num(r['序号']),
  418. tier: String(r['量级'] || '').trim(),
  419. ctype: String(r['类型'] || '').trim() || '未分类',
  420. name: String(r['小红书昵称'] || '').trim(),
  421. homepage: String(r['主页链接'] || '').trim(),
  422. followers: Math.round(num(r['粉丝数/w']) * 10000),
  423. region: String(r['地区'] || '').trim(),
  424. form: String(r['合作形式'] || '').trim(),
  425. price: num(r['下单价格']),
  426. sync: String(r['可同步平台'] || '').trim(),
  427. publishDate: excelDate(r['发布时间']),
  428. link: String(r['发布链接'] || '').trim(),
  429. likes: num(r['点赞']),
  430. collects: num(r['收藏']),
  431. comments: num(r['评论']),
  432. eng: num(r['总互动']),
  433. imp: num(r['曝光量']),
  434. syncEng: num(r['同步互动量']),
  435. syncImp: num(r['同步曝光量']),
  436. engTotal: num(r['互动量小计']),
  437. impTotal: num(r['曝光量小计']),
  438. }));
  439. // KOC
  440. const kocRowsRaw = XLSX.utils.sheet_to_json(wb.Sheets['KOC合作表'], { defval: '' });
  441. const koc = kocRowsRaw.filter(r => r['序号'] && typeof r['序号'] === 'number').map(r => ({
  442. idx: num(r['序号']),
  443. tier: String(r['量级'] || '').trim(),
  444. ctype: String(r['类型'] || '').trim() || '未分类',
  445. name: String(r['小红书昵称'] || '').trim(),
  446. homepage: String(r['主页链接'] || '').trim(),
  447. followers: Math.round(num(r['粉丝数/w']) * 10000),
  448. region: String(r['地区'] || '').trim(),
  449. publishDate: excelDate(r['发布时间']),
  450. link: String(r['发布链接'] || '').trim(),
  451. likes: num(r['点赞']),
  452. collects: num(r['收藏']),
  453. comments: num(r['评论']),
  454. eng: num(r['总互动']),
  455. imp: num(r['曝光量']),
  456. engTotal: num(r['互动量小计']),
  457. impTotal: num(r['曝光量小计']),
  458. }));
  459. // KOL/KOC 聚合
  460. const aggBy = (arr, key) => {
  461. const m = {};
  462. for (const r of arr) {
  463. const k = r[key] || '未分类';
  464. if (!m[k]) m[k] = { count: 0, eng: 0, imp: 0, spend: 0 };
  465. m[k].count++; m[k].eng += r.engTotal || r.eng; m[k].imp += r.impTotal || r.imp;
  466. m[k].spend += (r.price || 0);
  467. }
  468. return Object.entries(m).map(([k, v]) => ({ name: k, ...v })).sort((a, b) => b.eng - a.eng);
  469. };
  470. const kolByType = aggBy(kol, 'ctype');
  471. const kocByType = aggBy(koc, 'ctype');
  472. const kocByRegion = (() => {
  473. const m = {};
  474. for (const r of koc) {
  475. const k = (String(r.region).slice(0, 20)) || '未知';
  476. if (!m[k]) m[k] = 0; m[k] += r.engTotal || r.eng;
  477. }
  478. return Object.entries(m).map(([k, v]) => ({ name: k, value: v })).sort((a, b) => b.value - a.value).slice(0, 12);
  479. })();
  480. const buildKolRow = (r) => {
  481. const href = safeHref(r.link), homeHref = safeHref(r.homepage);
  482. return `<tr>
  483. <td data-v="${r.idx}">${r.idx}</td>
  484. <td><span class="tag p">${esc(r.tier)}</span></td>
  485. <td><span class="tag b">${esc(r.ctype)}</span></td>
  486. <td>${homeHref ? `<a href="${esc(homeHref)}" target="_blank">${esc(r.name)}</a>` : esc(r.name)}</td>
  487. <td class="num" data-v="${r.followers}">${fmtInt(r.followers)}</td>
  488. <td>${esc(r.region)}</td>
  489. <td>${esc(r.form)}</td>
  490. <td class="num" data-v="${r.price}">${fmtMoney(r.price)}</td>
  491. <td>${esc(r.publishDate)}</td>
  492. <td class="num" data-v="${r.likes}">${fmtInt(r.likes)}</td>
  493. <td class="num" data-v="${r.collects}">${fmtInt(r.collects)}</td>
  494. <td class="num" data-v="${r.comments}">${fmtInt(r.comments)}</td>
  495. <td class="num" data-v="${r.engTotal}"><strong style="color:var(--amber)">${fmtInt(r.engTotal)}</strong></td>
  496. <td class="num" data-v="${r.impTotal}">${fmtInt(r.impTotal)}</td>
  497. <td class="link">${href ? `<a href="${esc(href)}" target="_blank">打开</a>` : '<span style="color:var(--text-3)">—</span>'}</td>
  498. </tr>`;
  499. };
  500. const buildKocRow = (r) => {
  501. const href = safeHref(r.link), homeHref = safeHref(r.homepage);
  502. return `<tr>
  503. <td data-v="${r.idx}">${r.idx}</td>
  504. <td><span class="tag g">${esc(r.tier)}</span></td>
  505. <td><span class="tag">${esc(r.ctype)}</span></td>
  506. <td>${homeHref ? `<a href="${esc(homeHref)}" target="_blank">${esc(r.name)}</a>` : esc(r.name)}</td>
  507. <td class="num" data-v="${r.followers}">${fmtInt(r.followers)}</td>
  508. <td>${esc(r.region)}</td>
  509. <td>${esc(r.publishDate)}</td>
  510. <td class="num" data-v="${r.likes}">${fmtInt(r.likes)}</td>
  511. <td class="num" data-v="${r.collects}">${fmtInt(r.collects)}</td>
  512. <td class="num" data-v="${r.comments}">${fmtInt(r.comments)}</td>
  513. <td class="num" data-v="${r.engTotal}"><strong style="color:var(--amber)">${fmtInt(r.engTotal)}</strong></td>
  514. <td class="num" data-v="${r.impTotal}">${fmtInt(r.impTotal)}</td>
  515. <td class="link">${href ? `<a href="${esc(href)}" target="_blank">打开</a>` : '<span style="color:var(--text-3)">—</span>'}</td>
  516. </tr>`;
  517. };
  518. const kolRows = kol.slice().sort((a, b) => b.engTotal - a.engTotal).map(buildKolRow).join('');
  519. const kocRows = koc.slice().sort((a, b) => b.engTotal - a.engTotal).map(buildKocRow).join('');
  520. const kolCtOpts = [...new Set(kol.map(r => r.ctype))].filter(Boolean).sort();
  521. const kolTierOpts = [...new Set(kol.map(r => r.tier))].filter(Boolean).sort();
  522. const kocCtOpts = [...new Set(koc.map(r => r.ctype))].filter(Boolean).sort();
  523. const kocTierOpts = [...new Set(koc.map(r => r.tier))].filter(Boolean).sort();
  524. const kocRegionOpts = [...new Set(koc.map(r => r.region))].filter(Boolean).sort();
  525. const sumNames = JSON.stringify(summaryList.map(r => r.tier));
  526. const sumEng = JSON.stringify(summaryList.map(r => r.eng));
  527. const sumImp = JSON.stringify(summaryList.map(r => r.imp));
  528. const sumSpend = JSON.stringify(summaryList.map(r => r.spend + r.adSpend));
  529. const ctMix = JSON.stringify([...kolByType, ...kocByType].reduce((arr, x) => {
  530. const e = arr.find(y => y.name === x.name);
  531. if (e) e.value += x.eng; else arr.push({ name: x.name, value: x.eng });
  532. return arr;
  533. }, []).sort((a, b) => b.value - a.value));
  534. const regionData = JSON.stringify(kocByRegion);
  535. const html = COMMON_HEAD('马记永 · 四月新鲜菜宣发原始数据') + `
  536. <section class="hero">
  537. <div class="eyebrow">MAJIYONG · APRIL 2025 · RAW DATA</div>
  538. <h1 class="h0">马记永兰州牛肉面<br/>四月新鲜菜宣发 · 原始数据</h1>
  539. <p class="lead">数据源:<code style="color:var(--text-1)">【新鲜菜项目】马记永4月宣发数据汇总.xlsx</code>,包含数据汇总 / KOL 合作表 / KOC 合作表 三张工作表。本页对原始 Excel 字段进行结构化重构,所有指标可即时筛选、排序、跳转原帖。</p>
  540. <div class="meta-row">
  541. <div class="meta-item">达人总数<strong>${grandTotal.count}</strong></div>
  542. <div class="meta-item">总曝光<strong>${fmt(grandTotal.imp)}</strong></div>
  543. <div class="meta-item">总互动<strong>${fmt(grandTotal.eng)}</strong></div>
  544. <div class="meta-item">总投放<strong>${fmtMoney(totalSpend)}</strong></div>
  545. <div class="meta-item">CPE<strong>${grandTotal.cpe.toFixed(2)} 元</strong></div>
  546. <div class="meta-item">CPM<strong>${grandTotal.cpm.toFixed(2)} 元</strong></div>
  547. </div>
  548. </section>
  549. <section class="block">
  550. <div class="section-head"><span class="section-num">01 / OVERVIEW</span><h2 class="h1">核心指标总览</h2></div>
  551. <div class="grid grid-4">
  552. <div class="metric"><div class="metric-label">合作达人</div><div class="metric-value">${grandTotal.count}<span class="metric-unit">位</span></div><div class="metric-sub">KOL ${kol.length} · KOC ${koc.length}</div></div>
  553. <div class="metric green"><div class="metric-label">总曝光量</div><div class="metric-value">${fmt(grandTotal.imp)}</div><div class="metric-sub">CPM ${grandTotal.cpm.toFixed(2)} 元</div></div>
  554. <div class="metric rose"><div class="metric-label">总互动量</div><div class="metric-value">${fmt(grandTotal.eng)}</div><div class="metric-sub">CPE ${grandTotal.cpe.toFixed(2)} 元</div></div>
  555. <div class="metric purple"><div class="metric-label">总投放金额</div><div class="metric-value">${fmtMoney(totalSpend)}</div><div class="metric-sub">达人采购 ${fmtMoney(grandTotal.spend)} · 投流 ${fmtMoney(grandTotal.adSpend)}</div></div>
  556. </div>
  557. </section>
  558. <section class="block">
  559. <div class="section-head"><span class="section-num">02 / SUMMARY</span><h2 class="h1">数据汇总(按合作量级)</h2></div>
  560. <div class="card" style="padding:0">
  561. <table style="font-size:.9rem"><thead><tr>
  562. <th>合作量级</th><th class="num">数量</th><th class="num">互动量</th><th class="num">曝光量</th>
  563. <th class="num">采购费用</th><th class="num">投流费用</th><th class="num">单次互动</th><th class="num">千次曝光</th>
  564. </tr></thead><tbody>
  565. ${summary.map(r => `<tr>
  566. <td><strong>${esc(r.tier)}</strong></td>
  567. <td class="num">${fmtInt(r.count)}</td>
  568. <td class="num">${fmtInt(r.eng)}</td>
  569. <td class="num">${fmtInt(r.imp)}</td>
  570. <td class="num">${fmtMoney(r.spend)}</td>
  571. <td class="num">${fmtMoney(r.adSpend)}</td>
  572. <td class="num">${r.cpe ? r.cpe.toFixed(2) + ' 元' : '—'}</td>
  573. <td class="num">${r.cpm ? r.cpm.toFixed(2) + ' 元' : '—'}</td>
  574. </tr>`).join('')}
  575. </tbody></table>
  576. </div>
  577. </section>
  578. <section class="block">
  579. <div class="section-head"><span class="section-num">03 / CHARTS</span><h2 class="h1">维度可视化</h2></div>
  580. <div class="grid grid-2">
  581. <div class="card"><div style="margin-bottom:10px;color:var(--text-2);font-size:.86rem">合作量级 · 互动 vs 曝光 vs 花费</div><div id="sum-chart" class="chart"></div></div>
  582. <div class="card"><div style="margin-bottom:10px;color:var(--text-2);font-size:.86rem">内容类型 · 互动量分布</div><div id="ctmix-pie" class="chart"></div></div>
  583. <div class="card" style="grid-column:1/-1"><div style="margin-bottom:10px;color:var(--text-2);font-size:.86rem">KOC Top12 地区 · 互动量</div><div id="region-bar" class="chart"></div></div>
  584. </div>
  585. </section>
  586. <section class="block">
  587. <div class="section-head"><span class="section-num">04 / KOL</span><h2 class="h1">KOL 合作明细(${kol.length})</h2></div>
  588. <p class="sub">默认按互动量小计降序。可搜索 / 按级别 / 按内容类型筛选。</p>
  589. <div class="controls">
  590. <input id="kol-q" placeholder="🔍 搜索 KOL…">
  591. <select id="kol-ft" data-col="1"><option value="">全部级别</option>${kolTierOpts.map(o => `<option>${esc(o)}</option>`).join('')}</select>
  592. <select id="kol-fc" data-col="2"><option value="">全部内容类型</option>${kolCtOpts.map(o => `<option>${esc(o)}</option>`).join('')}</select>
  593. <span class="hint" id="kol-tbl-cnt"></span>
  594. </div>
  595. <div class="scroll-table"><table id="kol-tbl"><thead><tr>
  596. <th class="num">#<span class="arr"></span></th>
  597. <th>级别<span class="arr"></span></th>
  598. <th>类型<span class="arr"></span></th>
  599. <th>昵称<span class="arr"></span></th>
  600. <th class="num">粉丝<span class="arr"></span></th>
  601. <th>地区<span class="arr"></span></th>
  602. <th>合作形式<span class="arr"></span></th>
  603. <th class="num">下单价<span class="arr"></span></th>
  604. <th>发布<span class="arr"></span></th>
  605. <th class="num">点赞<span class="arr"></span></th>
  606. <th class="num">收藏<span class="arr"></span></th>
  607. <th class="num">评论<span class="arr"></span></th>
  608. <th class="num">互动小计<span class="arr"></span></th>
  609. <th class="num">曝光小计<span class="arr"></span></th>
  610. <th>原帖<span class="arr"></span></th>
  611. </tr></thead><tbody>${kolRows}</tbody></table></div>
  612. </section>
  613. <section class="block">
  614. <div class="section-head"><span class="section-num">05 / KOC</span><h2 class="h1">KOC 合作明细(${koc.length})</h2></div>
  615. <p class="sub">默认按互动量小计降序。可搜索 / 按级别 / 按内容类型 / 按地区筛选。</p>
  616. <div class="controls">
  617. <input id="koc-q" placeholder="🔍 搜索 KOC…">
  618. <select id="koc-ft" data-col="1"><option value="">全部级别</option>${kocTierOpts.map(o => `<option>${esc(o)}</option>`).join('')}</select>
  619. <select id="koc-fc" data-col="2"><option value="">全部内容类型</option>${kocCtOpts.map(o => `<option>${esc(o)}</option>`).join('')}</select>
  620. <select id="koc-fr" data-col="5"><option value="">全部地区</option>${kocRegionOpts.map(o => `<option>${esc(o)}</option>`).join('')}</select>
  621. <span class="hint" id="koc-tbl-cnt"></span>
  622. </div>
  623. <div class="scroll-table"><table id="koc-tbl"><thead><tr>
  624. <th class="num">#<span class="arr"></span></th>
  625. <th>级别<span class="arr"></span></th>
  626. <th>类型<span class="arr"></span></th>
  627. <th>昵称<span class="arr"></span></th>
  628. <th class="num">粉丝<span class="arr"></span></th>
  629. <th>地区<span class="arr"></span></th>
  630. <th>发布<span class="arr"></span></th>
  631. <th class="num">点赞<span class="arr"></span></th>
  632. <th class="num">收藏<span class="arr"></span></th>
  633. <th class="num">评论<span class="arr"></span></th>
  634. <th class="num">互动小计<span class="arr"></span></th>
  635. <th class="num">曝光小计<span class="arr"></span></th>
  636. <th>原帖<span class="arr"></span></th>
  637. </tr></thead><tbody>${kocRows}</tbody></table></div>
  638. </section>
  639. <script>
  640. echarts.init(document.getElementById('sum-chart')).setOption({
  641. ${echartsBaseTheme},
  642. legend:{data:['互动量','曝光量','总花费'],top:0,textStyle:{color:'#A8A8A8'}},
  643. xAxis:{type:'category',data:${sumNames},axisLabel:{interval:0,rotate:15}},
  644. yAxis:[{type:'value',name:'量'},{type:'value',name:'花费(元)'}],
  645. series:[
  646. {type:'bar',name:'互动量',data:${sumEng},itemStyle:{color:'#F5A623',borderRadius:[4,4,0,0]}},
  647. {type:'bar',name:'曝光量',data:${sumImp},itemStyle:{color:'#3B82F6',borderRadius:[4,4,0,0]}},
  648. {type:'line',name:'总花费',yAxisIndex:1,data:${sumSpend},itemStyle:{color:'#FF4D8D'},lineStyle:{width:2},symbol:'circle',symbolSize:8}
  649. ]
  650. });
  651. echarts.init(document.getElementById('ctmix-pie')).setOption({
  652. ${echartsBaseTheme},
  653. tooltip:{trigger:'item',formatter:'{b}<br/>{c} ({d}%)'},
  654. legend:{bottom:0,textStyle:{color:'#A8A8A8'},type:'scroll'},
  655. series:[{type:'pie',radius:['42%','70%'],center:['50%','45%'],
  656. label:{color:'#A8A8A8'},itemStyle:{borderColor:'#0A0A0A',borderWidth:2},
  657. data:${ctMix}.map((d,i)=>({...d,itemStyle:{color:['#F5A623','#8B5CF6','#3B82F6','#00DC82','#FF4D8D','#EF4444','#FCD34D','#A78BFA'][i%8]}}))
  658. }]
  659. });
  660. echarts.init(document.getElementById('region-bar')).setOption({
  661. ${echartsBaseTheme},
  662. grid:{left:90,right:30,top:20,bottom:30},
  663. tooltip:{trigger:'axis'},
  664. xAxis:{type:'value'},
  665. yAxis:{type:'category',data:${regionData}.map(d=>d.name).reverse(),axisLabel:{color:'#A8A8A8'}},
  666. series:[{type:'bar',data:${regionData}.map(d=>d.value).reverse(),itemStyle:{color:'#00DC82',borderRadius:[0,4,4,0]}}]
  667. });
  668. bindTable('kol-tbl'); bindFilter('kol-tbl','kol-q',['kol-ft','kol-fc']);
  669. bindTable('koc-tbl'); bindFilter('koc-tbl','koc-q',['koc-ft','koc-fc','koc-fr']);
  670. </script>` + COMMON_FOOT('马记永 · 四月新鲜菜宣发原始数据 · Generated from 【新鲜菜项目】马记永4月宣发数据汇总.xlsx');
  671. fs.writeFileSync(MAJI_OUT, html, 'utf8');
  672. console.log('✅ 马记永原始数据页生成:', MAJI_OUT, `(${(fs.statSync(MAJI_OUT).size / 1024).toFixed(1)} KB)`);
  673. }
  674. buildShuhua();
  675. buildMajiyong();