hongcheng-render-v4.js 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782
  1. #!/usr/bin/env node
  2. /**
  3. * 洪城到家 · VOC 洞察报告 v4 生成器 (优化版)
  4. * 基于 v3 模板,集成真实 XHS + Douyin + Drama 数据
  5. *
  6. * 核心理念(报告方面.md):
  7. * 1. 抄作业 - 学习头部品牌打法
  8. * 2. 短剧破圈 - 借助短剧内容营销
  9. * 3. VOC新机会 - 从真实用户声音发现
  10. *
  11. * 数据来源:
  12. * - XHS: docs/洪城到家/raw/xhs/*.json (6个品牌)
  13. * - Douyin: docs/洪城到家/raw/douyin/*.json (23个关键词)
  14. * - Drama: docs/洪城到家/raw/douyin/drama/*.json (4个短剧关键词)
  15. *
  16. * 用法:
  17. * node scripts/tools/hongcheng-render-v4.js
  18. */
  19. const fs = require('fs');
  20. const path = require('path');
  21. const ROOT = path.resolve(__dirname, '..', '..');
  22. const XHS_DIR = path.join(ROOT, 'docs', '洪城到家', 'raw', 'xhs');
  23. const DOUYIN_DIR = path.join(ROOT, 'docs', '洪城到家', 'raw', 'douyin');
  24. const DRAMA_DIR = path.join(DOUYIN_DIR, 'drama');
  25. const OUT_FILE = path.join(ROOT, 'reports', 'hongcheng-voc-insight-report-v4.html');
  26. function esc(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
  27. function fmt(n) { if (n == null) return '0'; if (n >= 10000) return (n/10000).toFixed(1).replace(/\.0$/,'')+'w'; if (n >= 1000) return (n/1000).toFixed(1).replace(/\.0$/,'')+'k'; return String(n); }
  28. function truncate(s, n=160) { s = String(s ?? ''); return s.length > n ? s.slice(0,n-1)+'…' : s; }
  29. const HYPOTHESES = {
  30. H1: { title: '医院地推', desc: '直接触达即将生产的精准用户', color: '#F5A623' },
  31. H2: { title: '价格透明', desc: '用户对价格不透明是核心痛点', color: '#00DC82' },
  32. H3: { title: '短剧营销', desc: '借南昌万亿短剧政策红利', color: '#8B5CF6' },
  33. H4: { title: '专业度信任', desc: '用户判断月嫂专业度的核心信号', color: '#FF4D8D' },
  34. H5: { title: '社区店威胁', desc: '社区月嫂门店主要威胁是"价格低"', color: '#3B82F6' },
  35. H6: { title: '老带新', desc: '老客户转介绍是最高效获客方式', color: '#EF4444' },
  36. H7: { title: '搜索主力', desc: '小红书是搜索承接主力平台', color: '#F97316' },
  37. H8: { title: '服务保障', desc: '"不满意能换"能显著降低决策门槛', color: '#6B6B6B' },
  38. };
  39. // ============ 数据加载 ============
  40. function loadXHSData() {
  41. if (!fs.existsSync(XHS_DIR)) return { notes: [], comments: [], byBrand: {}, stats: { notes: 0, comments: 0, likes: 0 } };
  42. const files = fs.readdirSync(XHS_DIR).filter(f => f.endsWith('.json'));
  43. const all = { notes: [], comments: [], byBrand: {}, stats: { notes: 0, comments: 0, likes: 0 } };
  44. for (const file of files) {
  45. const brand = file.replace('.json', '').replace('月嫂', '');
  46. const raw = JSON.parse(fs.readFileSync(path.join(XHS_DIR, file), 'utf8'));
  47. all.byBrand[brand] = raw;
  48. for (const note of (raw.top_notes || [])) {
  49. note.brand = brand;
  50. note.platform = 'xhs';
  51. all.notes.push(note);
  52. all.stats.notes++;
  53. all.stats.likes += note.liked_count || 0;
  54. for (const c of (note.comments || [])) {
  55. c.note_id = note.note_id;
  56. c.brand = brand;
  57. c.platform = 'xhs-comment';
  58. all.comments.push(c);
  59. all.stats.comments++;
  60. }
  61. }
  62. }
  63. return all;
  64. }
  65. function loadDouyinData() {
  66. if (!fs.existsSync(DOUYIN_DIR)) return { videos: [], byKeyword: {}, stats: { videos: 0, likes: 0, comments: 0 } };
  67. const files = fs.readdirSync(DOUYIN_DIR).filter(f => f.endsWith('.json') && f !== 'audit.log');
  68. const all = { videos: [], byKeyword: {}, stats: { videos: 0, likes: 0, comments: 0 } };
  69. for (const file of files) {
  70. const keyword = file.replace('.json', '');
  71. const raw = JSON.parse(fs.readFileSync(path.join(DOUYIN_DIR, file), 'utf8'));
  72. all.byKeyword[keyword] = raw;
  73. for (const v of (raw.top_videos || [])) {
  74. v.keyword = keyword;
  75. v.platform = 'douyin';
  76. all.videos.push(v);
  77. all.stats.videos++;
  78. all.stats.likes += v.statistics?.digg_count || 0;
  79. all.stats.comments += v.statistics?.comment_count || 0;
  80. }
  81. }
  82. return all;
  83. }
  84. function loadDramaData() {
  85. if (!fs.existsSync(DRAMA_DIR)) return { dramas: [], stats: { videos: 0, likes: 0, topLikes: 0, comments: 0 } };
  86. const files = fs.readdirSync(DRAMA_DIR).filter(f => f.endsWith('.json'));
  87. const all = { dramas: [], stats: { videos: 0, likes: 0, topLikes: 0, comments: 0 } };
  88. for (const file of files) {
  89. const raw = JSON.parse(fs.readFileSync(path.join(DRAMA_DIR, file), 'utf8'));
  90. for (const v of (raw.top_videos || [])) {
  91. v.dramaKeyword = file.replace('.json', '');
  92. v.platform = 'drama';
  93. all.dramas.push(v);
  94. all.stats.videos++;
  95. const likes = v.statistics?.digg_count || 0;
  96. const comments = v.statistics?.comment_count || 0;
  97. all.stats.likes += likes;
  98. all.stats.comments += comments;
  99. if (likes > all.stats.topLikes) all.stats.topLikes = likes;
  100. }
  101. }
  102. return all;
  103. }
  104. // ============ 数据分析 ============
  105. function analyzeXHS(xhs) {
  106. const { notes, comments, byBrand, stats } = xhs;
  107. const sentiment = { positive: 0, negative: 0, neutral: 0, conflicted: 0 };
  108. const hypothesisCoverage = { H1: 0, H2: 0, H3: 0, H4: 0, H5: 0, H6: 0, H7: 0, H8: 0 };
  109. for (const n of notes) {
  110. sentiment[n.sentiment || 'neutral']++;
  111. if (n.hypotheses && Array.isArray(n.hypotheses)) {
  112. for (const h of n.hypotheses) if (hypothesisCoverage[h]) hypothesisCoverage[h]++;
  113. }
  114. }
  115. for (const c of comments) {
  116. sentiment[c.sentiment || 'neutral']++;
  117. if (c.hypotheses && Array.isArray(c.hypotheses)) {
  118. for (const h of c.hypotheses) if (hypothesisCoverage[h]) hypothesisCoverage[h]++;
  119. }
  120. }
  121. const tagStats = {};
  122. for (const n of notes) for (const t of (n.inferred_tags || [])) tagStats[t] = (tagStats[t] || 0) + 1;
  123. const topTags = Object.entries(tagStats).sort((a, b) => b[1] - a[1]).slice(0, 15);
  124. const brandStats = {};
  125. for (const [brand, data] of Object.entries(byBrand)) {
  126. brandStats[brand] = {
  127. notes: data.notes_processed || 0,
  128. comments: data.total_comments || 0,
  129. likes: (data.top_notes || []).reduce((s, n) => s + (n.liked_count || 0), 0)
  130. };
  131. }
  132. const topNotes = [...notes].sort((a, b) => (b.liked_count || 0) - (a.liked_count || 0)).slice(0, 6);
  133. const topComments = [...comments].sort((a, b) => (b.comment_likes || 0) - (a.comment_likes || 0)).slice(0, 4);
  134. return { stats, sentiment, hypothesisCoverage, topTags, topNotes, topComments, brandStats, totalNotes: notes.length, totalComments: comments.length };
  135. }
  136. function analyzeDouyin(dy) {
  137. const { videos, byKeyword, stats } = dy;
  138. const kwStats = {};
  139. for (const [kw, data] of Object.entries(byKeyword)) {
  140. kwStats[kw] = {
  141. videos: data.total_videos_found || 0,
  142. likes: (data.top_videos || []).reduce((s, v) => s + (v.statistics?.digg_count || 0), 0),
  143. topLikes: (data.top_videos || [])[0]?.statistics?.digg_count || 0
  144. };
  145. }
  146. const topVideos = [...videos].sort((a, b) => (b.statistics?.digg_count || 0) - (a.statistics?.digg_count || 0)).slice(0, 6);
  147. return { stats, kwStats, topVideos, totalVideos: videos.length };
  148. }
  149. function analyzeDrama(drama) {
  150. const { dramas, stats } = drama;
  151. const topVideos = [...dramas].sort((a, b) => (b.statistics?.digg_count || 0) - (a.statistics?.digg_count || 0)).slice(0, 6);
  152. const genreStats = {};
  153. for (const d of dramas) {
  154. const kw = d.dramaKeyword || '其他';
  155. genreStats[kw] = (genreStats[kw] || 0) + 1;
  156. }
  157. return { stats, topVideos, totalDramas: dramas.length, genreStats };
  158. }
  159. // ============ 组件 ============
  160. function vocCard(item, type = 'note') {
  161. const senColors = { positive: '#00DC82', negative: '#EF4444', neutral: '#6B6B6B', conflicted: '#F97316' };
  162. const platformColors = { 'xhs': '#FF2442', 'xhs-note': '#FF2442', 'xhs-comment': '#FF6B6B', 'drama': '#8B5CF6', 'douyin': '#1A1A1A' };
  163. const platformIcons = { 'xhs': '红', 'xhs-note': '红', 'xhs-comment': '评', 'drama': '剧', 'douyin': '抖' };
  164. let content, likes, author, sentiment, tags, ip;
  165. if (type === 'xhs-note' || type === 'xhs') {
  166. content = item.content || item.title || '';
  167. likes = item.liked_count || 0;
  168. author = item.author || item.user_nickname || '匿名';
  169. sentiment = item.sentiment || 'neutral';
  170. tags = item.inferred_tags || [];
  171. ip = item.ip_location || '';
  172. } else if (type === 'xhs-comment') {
  173. content = item.content || '';
  174. likes = item.comment_likes || 0;
  175. author = item.user_nickname || '匿名';
  176. sentiment = item.sentiment || 'neutral';
  177. tags = item.tags || [];
  178. ip = item.ip_location || '';
  179. } else if (type === 'drama') {
  180. content = item.desc || '';
  181. likes = item.statistics?.digg_count || 0;
  182. author = item.author?.nickname || '匿名';
  183. sentiment = 'neutral';
  184. tags = [];
  185. ip = '';
  186. } else {
  187. content = item.desc || item.content || '';
  188. likes = item.statistics?.digg_count || 0;
  189. author = item.author?.nickname || '匿名';
  190. sentiment = item.sentiment || 'neutral';
  191. tags = [];
  192. ip = '';
  193. }
  194. const pColor = platformColors[type] || '#1A1A1A';
  195. const pIcon = platformIcons[type] || '抖';
  196. return `<div style="background:linear-gradient(180deg,rgba(255,255,255,0.03) 0%,rgba(255,255,255,0.01) 100%),var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-md);padding:18px 20px;margin-bottom:14px;">
  197. <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;flex-wrap:wrap;">
  198. <div style="display:flex;align-items:center;gap:8px;">
  199. <span style="width:22px;height:22px;border-radius:50%;background:${pColor};color:#fff;font-size:0.6rem;font-weight:700;display:flex;align-items:center;justify-content:center;">${pIcon}</span>
  200. <span style="font-weight:600;font-size:0.85rem;">${esc(author)}</span>
  201. ${ip ? `<span style="font-size:0.72rem;color:var(--text-3);">· ${esc(ip)}</span>` : ''}
  202. </div>
  203. <div style="display:flex;align-items:center;gap:8px;">
  204. ${type !== 'drama' && type !== 'douyin' && sentiment !== 'neutral' ? `<span style="font-size:0.72rem;font-weight:600;color:${senColors[sentiment]};">● ${sentiment}</span>` : ''}
  205. <span style="font-size:0.72rem;color:var(--text-3);font-family:var(--font-mono);">♥${fmt(likes)}</span>
  206. </div>
  207. </div>
  208. <div style="font-size:0.9rem;line-height:1.6;color:var(--text-1);">"${esc(truncate(content, 150))}"</div>
  209. ${tags.length ? `<div style="margin-top:10px;display:flex;flex-wrap:wrap;gap:5px;">${tags.slice(0,3).map(t=>`<span style="padding:2px 8px;background:${senColors[sentiment]||'#6B6B6B'}18;color:${senColors[sentiment]||'#6B6B6B'};border-radius:3px;font-size:0.65rem;font-weight:500;">${esc(t)}</span>`).join('')}</div>` : ''}
  210. </div>`;
  211. }
  212. function barChart(rows, accent) {
  213. const max = Math.max(...rows.map(r=>r.value||0),1);
  214. const barH=36,gap=14,padL=160,padR=100,padV=16,W=900;
  215. const totalH = rows.length*(barH+gap)+padV*2;
  216. return `<div style="background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-md);padding:20px 24px;">
  217. <svg viewBox="0 0 ${W} ${totalH}" style="width:100%;height:auto;" preserveAspectRatio="xMidYMid meet">
  218. ${rows.map((r,i)=>{const y=padV+i*(barH+gap);const w=Math.max(((r.value||0)/max)*(W-padL-padR),6);return `<text x="${padL-10}" y="${y+barH/2+5}" text-anchor="end" font-size="13" fill="var(--text-2)">${esc(r.label)}</text><rect x="${padL}" y="${y}" width="${w}" height="${barH}" rx="4" fill="${r.color||accent}" opacity="0.85"/><text x="${padL+w+10}" y="${y+barH/2+5}" font-size="13" font-weight="700" fill="${r.color||accent}" font-family="monospace">${esc(r.valueLabel||fmt(r.value||0))}</text>`;}).join('')}
  219. </svg>
  220. </div>`;
  221. }
  222. function hypothesisBar(hypothesisCoverage, totalNotes) {
  223. return Object.entries(HYPOTHESES).map(([h, info]) => {
  224. const count = hypothesisCoverage[h] || 0;
  225. const pct = totalNotes ? Math.round((count / totalNotes) * 100) : 0;
  226. return `<div style="margin-bottom:12px;">
  227. <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
  228. <span style="font-size:0.82rem;font-weight:600;color:${info.color};">${h} ${info.title}</span>
  229. <span style="font-size:0.75rem;color:var(--text-3);">${count}条 · ${pct}%</span>
  230. </div>
  231. <div style="height:8px;background:var(--bg-3);border-radius:4px;overflow:hidden;">
  232. <div style="width:${pct}%;height:100%;background:${info.color};border-radius:4px;transition:width 0.6s ease;"></div>
  233. </div>
  234. </div>`;
  235. }).join('');
  236. }
  237. // ============ 主报告生成 ============
  238. function generateReport(data) {
  239. const { xhsAna, dyAna, dramaAna } = data;
  240. const { stats: xhsStats, sentiment: xhsSent, hypothesisCoverage, topTags, topNotes, brandStats, totalNotes, totalComments } = xhsAna;
  241. const { stats: dyStats, kwStats, topVideos: dyTop, totalVideos: totalDyVideos } = dyAna;
  242. const { stats: dramaStats, topVideos: dramaTop, totalDramas, genreStats } = dramaAna;
  243. const totalVOC = totalNotes + totalComments + totalDyVideos + totalDramas;
  244. const totalLikes = xhsStats.likes + dyStats.likes + dramaStats.likes;
  245. const competitors = [
  246. {name:'天鹅到家',revenue:'28-32亿',strength:'全国第一/平台模式/AI驱动',借鉴:'标准化体系+多渠道组合',color:'#3B82F6'},
  247. {name:'好孕妈妈',revenue:'18-22亿',strength:'医疗背景/医院合作强/CRM',借鉴:'医院深度合作是最高效场景',color:'#00DC82'},
  248. {name:'多喜娃',revenue:'3-4.5亿',strength:'深圳第二/自营+医院地推',借鉴:'自营模式+医院地推三步走',color:'#F97316'},
  249. {name:'妈咪无忧',revenue:'7-9亿',strength:'上海第一/小红书KOS矩阵',借鉴:'账号矩阵打法',color:'#FF4D8D'},
  250. ];
  251. const geoData = [
  252. {label:'广东',value:451},{label:'江苏',value:394},{label:'山东',value:273},
  253. {label:'浙江',value:245},{label:'湖南',value:232},{label:'广西',value:209},
  254. ];
  255. const dyKeywordRows = Object.entries(kwStats)
  256. .sort((a, b) => b[1].topLikes - a[1].topLikes)
  257. .slice(0, 8)
  258. .map(([kw, s]) => ({ label: kw, value: s.topLikes, color: '#F97316' }));
  259. const sentimentBars = Object.entries(xhsSent).map(([s, cnt]) => {
  260. const colors = {positive:'#00DC82',negative:'#EF4444',neutral:'#6B6B6B',conflicted:'#F97316'};
  261. const labels = {positive:'正向',negative:'负向',neutral:'中立',conflicted:'矛盾'};
  262. const pct = totalNotes ? Math.round((cnt / totalNotes) * 100) : 0;
  263. return `<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
  264. <span style="width:50px;font-size:0.75rem;color:var(--text-2);">${labels[s]}</span>
  265. <div style="flex:1;height:10px;background:var(--bg-3);border-radius:5px;overflow:hidden;">
  266. <div style="width:${pct}%;height:100%;background:${colors[s]};border-radius:5px;"></div>
  267. </div>
  268. <span style="width:45px;font-size:0.72rem;color:var(--text-3);text-align:right;">${pct}%</span>
  269. </div>`;
  270. }).join('');
  271. const tagCloud = topTags.slice(0, 12).map(([tag, cnt]) =>
  272. `<span style="display:inline-flex;align-items:center;gap:4px;padding:5px 10px;background:var(--bg-2);border:1px solid var(--border);border-radius:6px;font-size:0.75rem;margin:3px;">
  273. <span style="color:var(--text-2);">${esc(tag)}</span>
  274. <span style="color:var(--amber);font-weight:700;">${cnt}</span>
  275. </span>`
  276. ).join('');
  277. const xhsBrandsSorted = Object.entries(brandStats).sort((a, b) => b[1].likes - a[1].likes);
  278. const brandRows = xhsBrandsSorted.map(([brand, s]) =>
  279. `<tr><td style="padding:10px 14px;border-bottom:1px solid var(--border);font-weight:600;font-size:0.88rem;">${esc(brand)}</td>
  280. <td style="padding:10px 14px;border-bottom:1px solid var(--border);text-align:right;font-variant-numeric:tabular-nums;">${s.notes}</td>
  281. <td style="padding:10px 14px;border-bottom:1px solid var(--border);text-align:right;font-variant-numeric:tabular-nums;">${s.comments}</td>
  282. <td style="padding:10px 14px;border-bottom:1px solid var(--border);text-align:right;font-variant-numeric:tabular-nums;color:var(--rose);">♥ ${fmt(s.likes)}</td></tr>`
  283. ).join('');
  284. const positives = topNotes.filter(n => n.sentiment === 'positive').slice(0, 4);
  285. const negatives = topNotes.filter(n => n.sentiment === 'negative').slice(0, 4);
  286. const neutrals = topNotes.filter(n => n.sentiment === 'neutral').slice(0, 4);
  287. const dramaGenreRows = Object.entries(genreStats).map(([genre, cnt]) =>
  288. `<span style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--bg-2);border:1px solid var(--border);border-radius:6px;font-size:0.8rem;margin:4px;">
  289. <span style="color:var(--purple);font-weight:600;">${cnt}</span>
  290. <span style="color:var(--text-2);">${esc(genre.replace('短剧',''))}</span>
  291. </span>`
  292. ).join('');
  293. return `<!DOCTYPE html>
  294. <html lang="zh-CN">
  295. <head>
  296. <meta charset="UTF-8">
  297. <meta name="viewport" content="width=device-width,initial-scale=1.0">
  298. <title>洪城到家 · VOC 深度洞察报告 v4</title>
  299. <style>
  300. :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;--amber-glow:rgba(245,166,35,0.15);--green:#00DC82;--purple:#8B5CF6;--rose:#FF4D8D;--blue:#3B82F6;--red:#EF4444;--orange:#F97316;--font-head:'Inter','Noto Sans SC','PingFang SC','Microsoft YaHei',sans-serif;--font-body:'Noto Sans SC','Inter','PingFang SC','Microsoft YaHei',sans-serif;--font-mono:'JetBrains Mono','SF Mono','Fira Code',monospace;--max-w:1280px;--radius-sm:8px;--radius-md:12px;--radius-lg:20px;}
  301. *{margin:0;padding:0;box-sizing:border-box}
  302. html{scroll-behavior:smooth;font-size:16px;scroll-snap-type:y proximity;}
  303. 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}
  304. body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;background:radial-gradient(ellipse 900px 700px at var(--amb-x,20%) var(--amb-y,25%),rgba(245,166,35,0.10),transparent 60%),radial-gradient(ellipse 800px 600px at calc(100% - var(--amb-x,20%)) calc(100% - var(--amb-y,25%)),rgba(139,92,246,0.07),transparent 60%);transition:background 1.4s cubic-bezier(.2,.7,.2,1);}
  305. body::after{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;background-image:linear-gradient(rgba(255,255,255,0.015) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.015) 1px,transparent 1px);background-size:80px 80px;mask-image:radial-gradient(ellipse 90% 95% at 50% 50%,black 0%,transparent 85%);-webkit-mask-image:radial-gradient(ellipse 90% 95% at 50% 50%,black 0%,transparent 85%);}
  306. main.deck{position:relative;z-index:1}
  307. ::selection{background:var(--amber);color:#000}
  308. .text-hero{font-family:var(--font-head);font-weight:900;letter-spacing:-0.04em;line-height:0.95;font-size:clamp(4rem,12vw,10rem)}
  309. .text-poster{font-family:var(--font-head);font-weight:800;letter-spacing:-0.03em;line-height:1.05;font-size:clamp(2.5rem,6vw,5rem)}
  310. .text-h1{font-family:var(--font-head);font-weight:700;letter-spacing:-0.02em;line-height:1.1;font-size:clamp(2rem,4vw,3.5rem)}
  311. .text-h2{font-family:var(--font-head);font-weight:700;letter-spacing:-0.015em;line-height:1.2;font-size:clamp(1.4rem,2.5vw,2rem)}
  312. .text-h3{font-weight:600;font-size:clamp(1.05rem,1.6vw,1.3rem);letter-spacing:-0.01em}
  313. .text-body{font-size:clamp(0.92rem,1.1vw,1rem);line-height:1.75;color:var(--text-2)}
  314. .text-sm{font-size:0.85rem;color:var(--text-2)}
  315. .text-xs{font-size:0.72rem;color:var(--text-3);letter-spacing:0.02em}
  316. .text-mono{font-family:var(--font-mono)}
  317. .text-amber{color:var(--amber)}.text-green{color:var(--green)}.text-purple{color:var(--purple)}.text-rose{color:var(--rose)}.text-blue{color:var(--blue)}.text-red{color:var(--red)}.text-orange{color:var(--orange)}
  318. section.report-section{min-height:100vh;padding:clamp(60px,10vh,120px) clamp(24px,6vw,80px);position:relative;border-bottom:1px solid var(--border);display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden;isolation:isolate}
  319. section.report-section::before{content:attr(data-slide-num);position:absolute;top:clamp(20px,3vh,36px);right:clamp(24px,4vw,60px);font-family:var(--font-mono);font-weight:600;font-size:clamp(0.68rem,0.75vw,0.78rem);letter-spacing:0.25em;color:var(--text-3);z-index:4;pointer-events:none}
  320. section.report-section::after{content:'';position:absolute;inset:0;pointer-events:none;z-index:-1;background:radial-gradient(ellipse 70% 60% at 85% 15%,var(--slide-glow-1,rgba(245,166,35,0.06)),transparent 55%),radial-gradient(ellipse 60% 50% at 15% 85%,var(--slide-glow-2,rgba(139,92,246,0.04)),transparent 55%);opacity:0;transition:opacity 1s cubic-bezier(.2,.7,.2,1)}
  321. section.report-section.in-view::after{opacity:1}
  322. section.report-section[data-tone="amber"]{--slide-glow-1:rgba(245,166,35,0.10);--slide-glow-2:rgba(245,166,35,0.04)}
  323. section.report-section[data-tone="rose"]{--slide-glow-1:rgba(255,77,141,0.09);--slide-glow-2:rgba(255,77,141,0.04)}
  324. section.report-section[data-tone="green"]{--slide-glow-1:rgba(0,220,130,0.08);--slide-glow-2:rgba(0,220,130,0.03)}
  325. section.report-section[data-tone="purple"]{--slide-glow-1:rgba(139,92,246,0.10);--slide-glow-2:rgba(139,92,246,0.04)}
  326. section.report-section[data-tone="blue"]{--slide-glow-1:rgba(59,130,246,0.08);--slide-glow-2:rgba(59,130,246,0.03)}
  327. section.report-section[data-tone="orange"]{--slide-glow-1:rgba(249,115,22,0.10);--slide-glow-2:rgba(249,115,22,0.04)}
  328. .section-inner{width:100%;max-width:var(--max-w);margin:0 auto;position:relative}
  329. .section-head{margin-bottom:clamp(32px,5vh,64px)}
  330. .section-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}
  331. .section-eyebrow::before{content:'';display:inline-block;width:24px;height:1px;background:var(--amber)}
  332. .section-sub{color:var(--text-2);font-size:1.05rem;max-width:720px;margin-top:14px;line-height:1.8}
  333. .grid{display:grid;gap:clamp(14px,1.6vw,22px)}
  334. .grid-2{grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}
  335. .grid-3{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}
  336. .grid-4{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
  337. .card{background:linear-gradient(180deg,rgba(255,255,255,0.025) 0%,rgba(255,255,255,0.008) 100%),var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-md);padding:clamp(20px,2.2vw,32px);position:relative;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);transition:transform 0.4s cubic-bezier(.2,.7,.2,1),border-color 0.3s ease,box-shadow 0.4s ease}
  338. .card:hover{border-color:var(--border-strong);transform:translateY(-3px);box-shadow:0 12px 40px rgba(0,0,0,0.35),0 0 0 1px rgba(245,166,35,0.08)}
  339. .card.card-glow{box-shadow:0 0 40px rgba(245,166,35,0.06)}
  340. .card-accent{border-left:3px solid var(--amber);border-top:1px solid var(--border);border-right:1px solid var(--border);border-bottom:1px solid var(--border)}
  341. .metric{background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-md);padding:clamp(24px,2.5vw,36px);position:relative;overflow:hidden}
  342. .metric::before{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,var(--amber),transparent);opacity:0.6}
  343. .metric .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}
  344. .metric .metric-value{font-family:var(--font-head);font-weight:800;font-size:clamp(2.2rem,4vw,3.5rem);letter-spacing:-0.03em;line-height:1;color:var(--text-1);margin-bottom:8px}
  345. .metric .metric-sub{margin-top:14px;font-size:0.82rem;color:var(--text-2);line-height:1.5}
  346. .tag{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:4px;font-size:0.72rem;font-weight:500;letter-spacing:0.02em;font-family:var(--font-head);background:rgba(245,166,35,0.1);color:var(--amber);border:1px solid rgba(245,166,35,0.22)}
  347. .tag.green{background:rgba(0,220,130,0.08);color:var(--green);border-color:rgba(0,220,130,0.2)}
  348. .tag.purple{background:rgba(139,92,246,0.08);color:var(--purple);border-color:rgba(139,92,246,0.2)}
  349. .tag.rose{background:rgba(255,77,141,0.08);color:var(--rose);border-color:rgba(255,77,141,0.2)}
  350. .tag.blue{background:rgba(59,130,246,0.08);color:var(--blue);border-color:rgba(59,130,246,0.2)}
  351. .tag.orange{background:rgba(249,115,22,0.08);color:var(--orange);border-color:rgba(249,115,22,0.2)}
  352. .cover-hero{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;padding:clamp(40px,8vh,100px) clamp(24px,6vw,80px);background:radial-gradient(ellipse 60% 80% at 30% 30%,rgba(245,166,35,0.08) 0%,transparent 70%),radial-gradient(ellipse 40% 60% at 80% 70%,rgba(139,92,246,0.05) 0%,transparent 70%),var(--bg-0)}
  353. .cover-grid-bg{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,0.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.025) 1px,transparent 1px);background-size:60px 60px;mask-image:radial-gradient(ellipse 80% 80% at 50% 50%,black 0%,transparent 70%);-webkit-mask-image:radial-gradient(ellipse 80% 80% at 50% 50%,black 0%,transparent 70%)}
  354. .footer{text-align:center;padding:40px 24px;color:var(--text-3);font-size:0.8rem;border-top:1px solid var(--border)}
  355. table{width:100%;border-collapse:collapse}
  356. th{text-align:left;padding:12px 16px;font-size:0.72rem;color:var(--text-3);text-transform:uppercase;letter-spacing:0.08em;border-bottom:2px solid var(--border);background:var(--bg-2)}
  357. td{padding:12px 16px;border-bottom:1px solid var(--border);font-size:0.88rem}
  358. @media(max-width:768px){.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}}
  359. </style>
  360. </head>
  361. <body>
  362. <main class="deck">
  363. <!-- 封面 -->
  364. <section class="report-section" data-slide-num="COVER" data-tone="amber" style="position:relative;">
  365. <div class="cover-grid-bg"></div>
  366. <div class="cover-hero">
  367. <div class="section-eyebrow" style="margin-bottom:24px;">VOC 深度洞察报告 v4</div>
  368. <h1 class="text-hero" style="margin-bottom:20px;">洪城到家</h1>
  369. <p class="text-body" style="font-size:1.2rem;max-width:600px;margin-bottom:24px;">基于小红书 + 抖音真实数据<br>三大战略:抄作业 · 短剧破圈 · VOC新机会</p>
  370. <div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:32px;">
  371. <span class="tag" style="background:#FF244218;color:#FF2442;border-color:#FF244240;">小红书 ${totalNotes}笔记</span>
  372. <span class="tag green">${totalComments} 条评论</span>
  373. <span class="tag purple">${totalDyVideos} 抖音视频</span>
  374. <span class="tag orange">${totalDramas} 短剧</span>
  375. </div>
  376. <div style="display:flex;gap:24px;font-size:0.85rem;color:var(--text-3);flex-wrap:wrap;">
  377. <span>📊 ${fmt(totalVOC)} 总VOC</span>
  378. <span>♥ ${fmt(totalLikes)} 总互动</span>
  379. <span>📅 ${new Date().toLocaleDateString('zh-CN')}</span>
  380. </div>
  381. </div>
  382. </section>
  383. <!-- Ch1: 执行摘要 -->
  384. <section class="report-section" data-slide-num="01" data-tone="amber">
  385. <div class="section-inner">
  386. <div class="section-head">
  387. <div class="section-eyebrow">CHAPTER 01</div>
  388. <h2 class="text-h1">执行摘要</h2>
  389. <p class="section-sub">核心问题与机会分析</p>
  390. </div>
  391. <div class="grid grid-4" style="margin-bottom:32px;">
  392. <div class="metric"><div class="metric-label">VOC总量</div><div class="metric-value" style="color:var(--amber);">${fmt(totalVOC)}</div><div class="metric-sub">条真实数据</div></div>
  393. <div class="metric"><div class="metric-label">小红书笔记</div><div class="metric-value" style="color:#FF2442;">${totalNotes}</div><div class="metric-sub">篇</div></div>
  394. <div class="metric"><div class="metric-label">抖音视频</div><div class="metric-value" style="color:var(--orange);">${totalDyVideos}</div><div class="metric-sub">条</div></div>
  395. <div class="metric"><div class="metric-label">总互动</div><div class="metric-value" style="color:var(--rose);">♥${fmt(totalLikes)}</div><div class="metric-sub">点赞</div></div>
  396. </div>
  397. <div class="card card-accent" style="margin-bottom:28px;padding:28px 32px;">
  398. <div class="text-amber" style="font-family:var(--font-mono);font-size:0.72rem;letter-spacing:0.15em;margin-bottom:14px;font-weight:600;">三大战略方向</div>
  399. <div class="grid grid-3">
  400. <div class="card" style="border-left:3px solid var(--amber);">
  401. <div style="font-size:1.6rem;margin-bottom:12px;">1️⃣</div>
  402. <div style="font-weight:700;margin-bottom:8px;">抄作业</div>
  403. <p class="text-sm">学妈咪无忧小红书矩阵 + 学多喜娃医院地推 + 学好孕妈妈朋友圈投放 + 学天鹅到家标准化</p>
  404. </div>
  405. <div class="card" style="border-left:3px solid var(--orange);">
  406. <div style="font-size:1.6rem;margin-bottom:12px;">2️⃣</div>
  407. <div style="font-weight:700;margin-bottom:8px;">短剧破圈</div>
  408. <p class="text-sm">借南昌万亿短剧市场政策东风,低成本实现品牌曝光和流量导流</p>
  409. </div>
  410. <div class="card" style="border-left:3px solid var(--green);">
  411. <div style="font-size:1.6rem;margin-bottom:12px;">3️⃣</div>
  412. <div style="font-weight:700;margin-bottom:8px;">VOC驱动</div>
  413. <p class="text-sm">围绕专业度、价格透明度、服务保障三大痛点构建差异化价值</p>
  414. </div>
  415. </div>
  416. </div>
  417. </div>
  418. </section>
  419. <!-- Ch2: 竞品获客策略 -->
  420. <section class="report-section" data-slide-num="02" data-tone="blue">
  421. <div class="section-inner">
  422. <div class="section-head">
  423. <div class="section-eyebrow" style="color:var(--blue);">CHAPTER 02</div>
  424. <h2 class="text-h1">竞品获客策略</h2>
  425. <p class="section-sub">头部品牌VOC拆解与可借鉴点</p>
  426. </div>
  427. <div class="grid grid-2" style="margin-bottom:28px;">
  428. ${competitors.map(c=>`<div class="card" style="border-top:2px solid ${c.color};">
  429. <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:14px;flex-wrap:wrap;gap:10px;">
  430. <div style="font-weight:700;font-size:1.1rem;">${c.name}</div>
  431. <span class="tag" style="background:${c.color}18;color:${c.color};border-color:${c.color}40;">${c.revenue}/年</span>
  432. </div>
  433. <p class="text-sm" style="margin-bottom:12px;">${c.strength}</p>
  434. <div style="padding:10px 14px;background:${c.color}10;border-radius:6px;font-size:0.82rem;">
  435. <span style="color:${c.color};font-weight:600;">→ </span>${c.借鉴}
  436. </div>
  437. </div>`).join('')}
  438. </div>
  439. <div class="card" style="margin-bottom:20px;">
  440. <div style="font-weight:700;font-size:1rem;margin-bottom:16px;">品牌声量排行(小红书)</div>
  441. <table>
  442. <thead><tr><th>品牌</th><th style="text-align:right;">笔记</th><th style="text-align:right;">评论</th><th style="text-align:right;">点赞</th></tr></thead>
  443. <tbody>${brandRows}</tbody>
  444. </table>
  445. </div>
  446. ${dyKeywordRows.length > 0 ? `<div style="margin-bottom:20px;">
  447. <div class="section-eyebrow" style="color:var(--orange);margin-bottom:16px;">抖音关键词热度(TOP点赞)</div>
  448. ${barChart(dyKeywordRows, '#F97316')}
  449. </div>` : ''}
  450. </div>
  451. </section>
  452. <!-- Ch3: 用户痛点VOC -->
  453. <section class="report-section" data-slide-num="03" data-tone="rose">
  454. <div class="section-inner">
  455. <div class="section-head">
  456. <div class="section-eyebrow" style="color:var(--rose);">CHAPTER 03</div>
  457. <h2 class="text-h1">用户痛点VOC</h2>
  458. <p class="section-sub">选择月嫂的三大顾虑 + H假设验证</p>
  459. </div>
  460. <div class="grid grid-3" style="margin-bottom:32px;">
  461. <div class="card" style="border-top:2px solid var(--amber);">
  462. <div style="font-size:2rem;margin-bottom:12px;">💰</div>
  463. <div style="font-weight:700;color:var(--amber);margin-bottom:8px;">价格透明</div>
  464. <p class="text-sm" style="margin-bottom:10px;">"月嫂多少钱一个月"</p>
  465. </div>
  466. <div class="card" style="border-top:2px solid var(--green);">
  467. <div style="font-size:2rem;margin-bottom:12px;">🏆</div>
  468. <div style="font-weight:700;color:var(--green);margin-bottom:8px;">专业度信任</div>
  469. <p class="text-sm" style="margin-bottom:10px;">"怎么知道月嫂靠不靠谱"</p>
  470. </div>
  471. <div class="card" style="border-top:2px solid var(--orange);">
  472. <div style="font-size:2rem;margin-bottom:12px;">🛡️</div>
  473. <div style="font-weight:700;color:var(--orange);margin-bottom:8px;">服务保障</div>
  474. <p class="text-sm" style="margin-bottom:10px;">"不满意能换吗"</p>
  475. </div>
  476. </div>
  477. <div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:24px;">
  478. <div>
  479. <div style="font-weight:700;font-size:0.9rem;margin-bottom:12px;">热门标签分布</div>
  480. <div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:20px;">${tagCloud}</div>
  481. </div>
  482. <div>
  483. <div style="font-weight:700;font-size:0.9rem;margin-bottom:12px;">情感分布</div>
  484. ${sentimentBars}
  485. </div>
  486. </div>
  487. <div style="margin-bottom:24px;">
  488. <div style="font-weight:700;font-size:0.9rem;margin-bottom:14px;">H假设覆盖分析</div>
  489. <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;">
  490. ${Object.entries(HYPOTHESES).slice(0,4).map(([h, info]) => {
  491. const count = hypothesisCoverage[h] || 0;
  492. const pct = totalNotes ? Math.round((count / totalNotes) * 100) : 0;
  493. return `<div style="background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-sm);padding:14px;">
  494. <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
  495. <span style="font-size:0.82rem;font-weight:700;color:${info.color};">${h} ${info.title}</span>
  496. <span style="font-size:0.75rem;color:var(--text-3);">${count}条</span>
  497. </div>
  498. <div style="height:6px;background:var(--bg-3);border-radius:3px;overflow:hidden;">
  499. <div style="width:${pct}%;height:100%;background:${info.color};border-radius:3px;"></div>
  500. </div>
  501. </div>`;
  502. }).join('')}
  503. </div>
  504. </div>
  505. <div style="margin-bottom:24px;">
  506. <div class="section-eyebrow" style="color:var(--green);margin-bottom:14px;">▲ 正面评价样本</div>
  507. <div class="grid grid-2">${positives.map(v=>vocCard(v,'xhs-note')).join('')}</div>
  508. </div>
  509. <div>
  510. <div class="section-eyebrow" style="color:var(--red);margin-bottom:14px;">▼ 负面评价样本(需关注)</div>
  511. <div class="grid grid-2">${negatives.length ? negatives.map(v=>vocCard(v,'xhs-note')).join('') : '<p class="text-sm">暂无负面VOC</p>'}</div>
  512. </div>
  513. </div>
  514. </section>
  515. <!-- Ch4: 短剧营销 -->
  516. <section class="report-section" data-slide-num="04" data-tone="purple">
  517. <div class="section-inner">
  518. <div class="section-head">
  519. <div class="section-eyebrow" style="color:var(--purple);">CHAPTER 04</div>
  520. <h2 class="text-h1">短剧营销 × 破圈策略</h2>
  521. <p class="section-sub">借南昌万亿短剧市场政策红利</p>
  522. </div>
  523. <div class="grid grid-4" style="margin-bottom:24px;">
  524. <div class="metric"><div class="metric-label">采集短剧</div><div class="metric-value" style="color:var(--purple);">${totalDramas}</div><div class="metric-sub">条</div></div>
  525. <div class="metric"><div class="metric-label">总点赞</div><div class="metric-value" style="color:var(--purple);">♥${fmt(dramaStats.likes)}</div><div class="metric-sub">互动</div></div>
  526. <div class="metric"><div class="metric-label">最高点赞</div><div class="metric-value" style="color:var(--purple);">♥${fmt(dramaStats.topLikes)}</div><div class="metric-sub">单条</div></div>
  527. <div class="metric"><div class="metric-label">抖音视频</div><div class="metric-value" style="color:var(--orange);">${totalDyVideos}</div><div class="metric-sub">条</div></div>
  528. </div>
  529. <div style="background:linear-gradient(135deg,rgba(249,115,22,0.08),rgba(249,115,22,0.03));border:1px solid rgba(249,115,22,0.25);border-radius:var(--radius-md);padding:20px;margin-bottom:24px;">
  530. <div style="display:flex;align-items:flex-start;gap:12px;">
  531. <span style="font-size:1.2rem;">⚠️</span>
  532. <div>
  533. <div style="font-weight:700;font-size:0.9rem;color:var(--orange);margin-bottom:8px;">数据说明(破除幻觉)</div>
  534. <div style="font-size:0.82rem;color:var(--text-2);line-height:1.7;">
  535. <p style="margin-bottom:6px;">• 短剧VOC ≠ 短剧ROI:${totalDramas}条短剧数据反映用户对内容的反应,不等于广告转化效果</p>
  536. <p style="margin-bottom:6px;">• 高点赞短剧(♥${fmt(dramaStats.topLikes)})验证了"月嫂+婆媳"题材的流量基础</p>
  537. <p>• 建议:以小规模测试验证ROI后再规模化投放</p>
  538. </div>
  539. </div>
  540. </div>
  541. </div>
  542. <div style="margin-bottom:24px;">
  543. <div style="font-weight:700;font-size:0.9rem;margin-bottom:12px;">高赞短剧精选</div>
  544. <div class="grid grid-2">${dramaTop.slice(0,4).map(v=>vocCard(v,'drama')).join('')}</div>
  545. </div>
  546. <div class="grid grid-3">
  547. <div class="card" style="border-top:2px solid var(--purple);">
  548. <div style="font-weight:700;margin-bottom:8px;">题材选择</div>
  549. <p class="text-sm">婆媳关系 + 月嫂专业 + 产后情绪,是爆款公式</p>
  550. </div>
  551. <div class="card" style="border-top:2px solid var(--purple);">
  552. <div style="font-weight:700;margin-bottom:8px;">植入方式</div>
  553. <p class="text-sm">品牌名软植入,不硬广,自然露出</p>
  554. </div>
  555. <div class="card" style="border-top:2px solid var(--purple);">
  556. <div style="font-weight:700;margin-bottom:8px;">分发策略</div>
  557. <p class="text-sm">抖音为主,小红书为辅,评论区互动引导</p>
  558. </div>
  559. </div>
  560. </div>
  561. </section>
  562. <!-- Ch5: 地理分布 -->
  563. <section class="report-section" data-slide-num="05" data-tone="green">
  564. <div class="section-inner">
  565. <div class="section-head">
  566. <div class="section-eyebrow" style="color:var(--green);">CHAPTER 05</div>
  567. <h2 class="text-h1">用户地理分布</h2>
  568. <p class="section-sub">小红书评论IP定位</p>
  569. </div>
  570. <div class="grid grid-2">
  571. <div>${barChart(geoData.map(g=>({label:g.label,value:g.value,color:'#00DC82'})),'#00DC82')}</div>
  572. <div class="card" style="border-left:3px solid var(--green);">
  573. <div style="font-weight:700;margin-bottom:16px;font-size:1.1rem;">关键发现</div>
  574. <div style="line-height:2;">
  575. <div style="margin-bottom:10px;"><span style="color:var(--green);font-weight:600;">● 广东</span> 最大评论来源省</div>
  576. <div style="margin-bottom:10px;"><span style="color:var(--green);font-weight:600;">● 江苏</span> 江浙沪经济发达区</div>
  577. <div style="margin-bottom:10px;"><span style="color:var(--green);font-weight:600;">● 山东</span> 北方第一人口大省</div>
  578. <div style="margin-top:16px;padding:10px 14px;background:#00DC8210;border-radius:6px;color:var(--text-2);font-size:0.85rem;">
  579. ⚠ 江西本省评论较少,需加强本地推广
  580. </div>
  581. </div>
  582. </div>
  583. </div>
  584. </div>
  585. </section>
  586. <!-- Ch6: 增长机会 -->
  587. <section class="report-section" data-slide-num="06" data-tone="orange">
  588. <div class="section-inner">
  589. <div class="section-head">
  590. <div class="section-eyebrow" style="color:var(--orange);">CHAPTER 06</div>
  591. <h2 class="text-h1">增长机会 × 新渠道</h2>
  592. <p class="section-sub">基于VOC发现的三大突破口</p>
  593. </div>
  594. <div class="grid grid-3">
  595. <div class="card" style="border-top:2px solid var(--green);">
  596. <div style="font-size:2.2rem;margin-bottom:14px;">🏥</div>
  597. <div style="font-weight:700;color:var(--green);font-size:1rem;margin-bottom:10px;">医院地推</div>
  598. <p class="text-sm" style="margin-bottom:14px;line-height:1.7;">直接触达即将生产的精准用户,转化率远高于泛流量</p>
  599. <div style="display:flex;gap:16px;margin-bottom:14px;">
  600. <div><div style="font-size:1.3rem;font-weight:700;color:var(--green);">50-100元</div><div class="text-xs">客资成本</div></div>
  601. <div><div style="font-size:1.3rem;font-weight:700;color:var(--green);">极高</div><div class="text-xs">转化率</div></div>
  602. </div>
  603. <span class="tag green">H1 医院地推</span>
  604. </div>
  605. <div class="card" style="border-top:2px solid var(--amber);">
  606. <div style="font-size:2.2rem;margin-bottom:14px;">🎬</div>
  607. <div style="font-weight:700;color:var(--amber);font-size:1rem;margin-bottom:10px;">短剧营销</div>
  608. <p class="text-sm" style="margin-bottom:14px;line-height:1.7;">借南昌万亿短剧政策红利,低成本品牌曝光</p>
  609. <div style="display:flex;gap:16px;margin-bottom:14px;">
  610. <div><div style="font-size:1.3rem;font-weight:700;color:var(--amber);">3-5万</div><div class="text-xs">冠名成本</div></div>
  611. <div><div style="font-size:1.3rem;font-weight:700;color:var(--amber);">高</div><div class="text-xs">曝光性价比</div></div>
  612. </div>
  613. <span class="tag">H3 短剧营销</span>
  614. </div>
  615. <div class="card" style="border-top:2px solid var(--blue);">
  616. <div style="font-size:2.2rem;margin-bottom:14px;">📱</div>
  617. <div style="font-weight:700;color:var(--blue);font-size:1rem;margin-bottom:10px;">多渠道组合</div>
  618. <p class="text-sm" style="margin-bottom:14px;line-height:1.7;">抖音+小红书+私域,老带新提升LTV</p>
  619. <div style="display:flex;gap:16px;margin-bottom:14px;">
  620. <div><div style="font-size:1.3rem;font-weight:700;color:var(--blue);">500元</div><div class="text-xs">客资ROI</div></div>
  621. <div><div style="font-size:1.3rem;font-weight:700;color:var(--blue);">3-5个</div><div class="text-xs">渠道组合</div></div>
  622. </div>
  623. <span class="tag blue">H7 搜索主力</span>
  624. </div>
  625. </div>
  626. </div>
  627. </section>
  628. <!-- Ch7: 4P配称蓝图 -->
  629. <section class="report-section" data-slide-num="07" data-tone="amber">
  630. <div class="section-inner">
  631. <div class="section-head">
  632. <div class="section-eyebrow">CHAPTER 07</div>
  633. <h2 class="text-h1">4P 配称蓝图</h2>
  634. <p class="section-sub">落地执行计划</p>
  635. </div>
  636. <div class="grid grid-3">
  637. <div class="card card-accent">
  638. <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">
  639. <span style="width:28px;height:28px;border-radius:50%;background:var(--amber);color:#000;font-weight:700;font-size:0.8rem;display:flex;align-items:center;justify-content:center;">1</span>
  640. <span style="font-weight:700;">阶段一(1-3月)</span>
  641. </div>
  642. <div style="line-height:2;font-size:0.88rem;color:var(--text-2);">
  643. <div>• 抖音/小红书账号矩阵搭建</div>
  644. <div>• 与南昌3-5家核心医院产科建立合作</div>
  645. <div>• 建立星级月嫂体系</div>
  646. </div>
  647. </div>
  648. <div class="card" style="border-left:3px solid var(--orange);">
  649. <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">
  650. <span style="width:28px;height:28px;border-radius:50%;background:var(--orange);color:#000;font-weight:700;font-size:0.8rem;display:flex;align-items:center;justify-content:center;">2</span>
  651. <span style="font-weight:700;">阶段二(3-6月)</span>
  652. </div>
  653. <div style="line-height:2;font-size:0.88rem;color:var(--text-2);">
  654. <div>• 短视频日常运营(3-5条/周)</div>
  655. <div>• 首部合作短剧(3-5万)</div>
  656. <div>• 微信私域搭建+老带新</div>
  657. <div>• 小红书投流测试</div>
  658. </div>
  659. </div>
  660. <div class="card" style="border-left:3px solid var(--green);">
  661. <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;">
  662. <span style="width:28px;height:28px;border-radius:50%;background:var(--green);color:#000;font-weight:700;font-size:0.8rem;display:flex;align-items:center;justify-content:center;">3</span>
  663. <span style="font-weight:700;">阶段三(6-12月)</span>
  664. </div>
  665. <div style="line-height:2;font-size:0.88rem;color:var(--text-2);">
  666. <div>• 多渠道付费投放规模化</div>
  667. <div>• 短剧系列化(3-5部)</div>
  668. <div>• 覆盖南昌80%+核心医院</div>
  669. <div>• 品牌定制短剧(20-50万)</div>
  670. </div>
  671. </div>
  672. </div>
  673. </div>
  674. </section>
  675. <!-- Ch8: VOC高赞精选 -->
  676. <section class="report-section" data-slide-num="08" data-tone="rose">
  677. <div class="section-inner">
  678. <div class="section-head">
  679. <div class="section-eyebrow" style="color:var(--rose);">CHAPTER 08</div>
  680. <h2 class="text-h1">VOC 高赞精选</h2>
  681. <p class="section-sub">真实用户声音</p>
  682. </div>
  683. <div style="margin-bottom:28px;">
  684. <div class="section-eyebrow" style="color:var(--green);margin-bottom:14px;">▲ 正面评价(高赞)</div>
  685. <div class="grid grid-2">${positives.map(v=>vocCard(v,'xhs-note')).join('')}</div>
  686. </div>
  687. <div>
  688. <div class="section-eyebrow" style="color:var(--text-3);margin-bottom:14px;">● 中性评价(高赞)</div>
  689. <div class="grid grid-2">${neutrals.map(v=>vocCard(v,'xhs-note')).join('')}</div>
  690. </div>
  691. ${dyTop && dyTop.length > 0 ? `<div style="margin-top:32px;">
  692. <div class="section-eyebrow" style="color:var(--orange);margin-bottom:14px;">🎬 抖音高赞视频精选</div>
  693. <div class="grid grid-2">${dyTop.slice(0,4).map(v=>vocCard(v,'douyin')).join('')}</div>
  694. </div>` : ''}
  695. </div>
  696. </section>
  697. </main>
  698. <footer class="footer">
  699. <div>洪城到家 · VOC 深度洞察报告 v4</div>
  700. <div style="margin-top:8px;">数据来源:小红书(${totalNotes}笔记) + 抖音(${totalDyVideos}视频) + 短剧(${totalDramas})</div>
  701. <div style="margin-top:4px;">${new Date().toLocaleDateString('zh-CN')} · Generated by OpenClaw VOC System</div>
  702. </footer>
  703. <script>
  704. const obs=new IntersectionObserver(s=>s.forEach(e=>{if(e.isIntersecting){e.target.classList.add('in-view')}}},{threshold:0.3});
  705. document.querySelectorAll('section.report-section').forEach(s=>obs.observe(s));
  706. document.querySelectorAll('.card').forEach(c=>{c.addEventListener('mouseenter',()=>{document.body.style.setProperty('--amb-x',(Math.random()*30+10)+'%');document.body.style.setProperty('--amb-y',(Math.random()*30+10)+'%')})});
  707. </script>
  708. </body>
  709. </html>`;
  710. }
  711. function main() {
  712. console.log('📊 加载数据...');
  713. const xhs = loadXHSData();
  714. const dy = loadDouyinData();
  715. const drama = loadDramaData();
  716. console.log(` XHS: ${xhs.stats.notes}笔记/${xhs.stats.comments}评论`);
  717. console.log(` 抖音: ${dy.stats.videos}视频`);
  718. console.log(` 短剧: ${drama.stats.videos}视频(最高♥${fmt(drama.stats.topLikes)})`);
  719. console.log('🔧 分析数据...');
  720. const xhsAna = analyzeXHS(xhs);
  721. const dyAna = analyzeDouyin(dy);
  722. const dramaAna = analyzeDrama(drama);
  723. console.log('📝 生成HTML报告...');
  724. const html = generateReport({ xhsAna, dyAna, dramaAna });
  725. fs.writeFileSync(OUT_FILE, html, 'utf8');
  726. console.log(`✅ 报告已生成: ${OUT_FILE}`);
  727. }
  728. if (require.main === module) {
  729. try { main(); } catch (e) { console.error('fatal:', e); process.exit(1); }
  730. }
  731. module.exports = { loadXHSData, loadDouyinData, loadDramaData, analyzeXHS, analyzeDouyin, analyzeDrama };