// ============================================================================== // 精美 HTML 报告 · 视觉模式库(20+ copy-paste 小组件) // ============================================================================== // 使用方法: // 1. 不要整文件 require,而是查阅 → 复制对应 pattern 到你的 <品类>-components.js // 2. 每个 pattern 都有:① 适用场景 ② 输入参数 ③ HTML 范式 ④ 变体 // 3. 所有 pattern 都假设 BRAND.MAIN 是品类主色 // ============================================================================== // 公共依赖(你需要在实际 components.js 里定义) const BRAND_STUB = { MAIN: '#4A90E2', CORAL: '#FF6B6B', GOLD: '#D4AF37', JIANG_GREEN: '#1A7C5F', }; function esc(s) { return String(s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function fmtLikes(n) { if (!n) return '0'; if (n >= 10000) return (n / 10000).toFixed(1) + 'w'; if (n >= 1000) return (n / 1000).toFixed(1) + 'k'; return String(n); } // ============================================================ // P01 · Eyebrow · 小标签(所有 section head 开头必有) // ============================================================ // 适用:section head / insight / decision list 前的小标签 // 输入:text, color, letterSpacing function renderEyebrow({ text, color = BRAND_STUB.MAIN, letterSpacing = '0.2em' }) { return `
${esc(text)}
`; } // ============================================================ // P02 · Section Head · 三段式标题(eyebrow + title + subtitle) // ============================================================ // 适用:每子章节开头 // 输入:eyebrow, title(带 HTML, 允许 ), subtitle, color function renderSectionHead({ eyebrow, title, subtitle, color = BRAND_STUB.MAIN }) { return `
${eyebrow ? renderEyebrow({ text: eyebrow, color }) : ''}

${title || ''}

${subtitle ? `
${subtitle}
` : ''}
`; } // ============================================================ // P03 · VOC Card · 证据卡(核心组件 · 全报告贯穿) // ============================================================ // 适用:所有子章节 ≥ 3 张 // 输入:item { platform, nickname, ip, content, likes }, accent, maxChars function renderVocCard(item, opts = {}) { if (!item) return ''; const accent = opts.accent || BRAND_STUB.MAIN; const maxChars = opts.maxChars || 200; const content = String(item.content || '').slice(0, maxChars); const more = (item.content || '').length > maxChars ? '…' : ''; return `
${(item.platform || 'XHS').toUpperCase()} · ${esc(item.nickname || '匿名')} ${item.ip ? ` · ${esc(item.ip)}` : ''}
♥${fmtLikes(item.likes)}
「${esc(content)}${more}」
`; } // ============================================================ // P04 · VOC Card Compact · 紧凑证据卡(3 列布局用) // ============================================================ function renderVocCardCompact(item, opts = {}) { if (!item) return ''; const accent = opts.accent || BRAND_STUB.MAIN; const maxChars = opts.maxChars || 120; const content = String(item.content || '').slice(0, maxChars); const more = (item.content || '').length > maxChars ? '…' : ''; return `
${(item.platform || 'XHS').toUpperCase()} · ${esc(item.nickname || '匿名')} · ♥${fmtLikes(item.likes)}
「${esc(content)}${more}」
`; } // ============================================================ // P05 · StatStrip · KPI 数字条(4-6 个并列数字) // ============================================================ // 适用:Cover / 章节 hero / 章末总结 // 输入:stats = [{ num, label, delta, color }] function renderStatStrip(stats, opts = {}) { const accent = opts.accent || BRAND_STUB.MAIN; const cols = stats.length; return `
${stats.map((s) => `
${esc(s.num)}
${esc(s.label)}
${s.delta ? `
${esc(s.delta)}
` : ''}
`).join('')}
`; } // ============================================================ // P06 · Single Big Stat · 巨型单数字(镇场) // ============================================================ // 适用:Cover 核心数字 / 章节 hero 唯一金句数字 function renderBigStat({ num, label, delta, color = BRAND_STUB.MAIN }) { return `
${esc(num)}
${esc(label)}
${delta ? `
${esc(delta)}
` : ''}
`; } // ============================================================ // P07 · Insight Hero · 金句观点卡(渐变背景) // ============================================================ // 适用:章节核心洞察 · 每章 1-2 处 function renderInsightHero({ eyebrow, title, subtitle, color = BRAND_STUB.MAIN }) { return `
${eyebrow ? `
${esc(eyebrow)}
` : ''}
${title || ''}
${subtitle ? `
${subtitle}
` : ''}
`; } // ============================================================ // P08 · Decision List · 落地决策列表(每子章收尾) // ============================================================ // 适用:每子章节最后 · 2 列 grid // 输入:items = ['P0 · 动词:具体动作', ...] function renderDecisionList(items, opts = {}) { const accent = opts.accent || BRAND_STUB.MAIN; const title = opts.title || '落地决策'; const columns = opts.columns || 2; return `
🎯 ${esc(title)}
${items.map((x) => `
${x}
`).join('')}
`; } // ============================================================ // P09 · Compare Table · 对比表格 // ============================================================ // 适用:竞品 / 本品 vs 竞品矩阵 // 输入:headers = [], rows = [[cell, cell, ...], ...] // cell = string | { html, color, bold } function renderCompareTable({ title, headers = [], rows = [], accent = BRAND_STUB.MAIN }) { return `
${title ? `
${esc(title)}
` : ''}
${headers.map((h) => ``).join('')} ${rows.map((row) => ` ${row.map((cell) => { const c = typeof cell === 'object' ? cell : { html: cell }; return ``; }).join('')} `).join('')}
${esc(h)}
${c.html || c}
`; } // ============================================================ // P10 · Matrix 2x2 · 无人地带四象限 // ============================================================ // 适用:Ch6 竞品 × 无人地带定位 // 关键纪律:第 4 象限(本品独占)必须品类色高亮 function renderMatrix2x2({ xLabel, yLabel, xLow, xHigh, yLow, yHigh, quadrants = {}, accent = BRAND_STUB.MAIN }) { const renderQ = (q) => { if (!q) return '
'; return `
${q.title || ''}
${q.note ? `
${esc(q.note)}
` : ''}
`; }; return `
${esc(xLow || '')}
${esc(xHigh || '')}
${esc(yHigh || '')}
${renderQ(quadrants.q2)} ${renderQ(quadrants.q1)}
${esc(yLow || '')}
${renderQ(quadrants.q3)} ${renderQ(quadrants.q4)}
← ${esc(xLabel || '')} → ${yLabel ? '· Y: ' + esc(yLabel) : ''}
`; } // ============================================================ // P11 · Theory Box · 理论映射盒(KANO / JTBD / 3C) // ============================================================ function renderTheoryBox({ theory, mapping, accent = BRAND_STUB.MAIN }) { return `
${theory ? `
${esc(theory)}
` : ''} ${mapping || ''}
`; } // ============================================================ // P12 · Finding Card · 发现洞察卡(icon + 标题 + 正文) // ============================================================ // 适用:3-4 列并排的小发现 function renderFindingCard({ icon, title, body, accent = BRAND_STUB.MAIN }) { return `
${icon ? `
${icon}
` : ''}
${title ? `
${title}
` : ''}
${body || ''}
`; } // ============================================================ // P13 · Progress Bar · 横向比例条(单维度对比) // ============================================================ // 适用:竞品市占 / 假设覆盖率 / 渠道占比 function renderProgressBar({ label, value, max = 100, accent = BRAND_STUB.MAIN, suffix = '%' }) { const pct = Math.min(100, (value / max) * 100); return `
${esc(label)} ${value}${suffix}
`; } // ============================================================ // P14 · Inline Bar Chart · 小柱图(纯 div · 无依赖) // ============================================================ // 适用:多项对比 5-10 个 function renderInlineBars({ items, max, accent = BRAND_STUB.MAIN, height = 80 }) { const maxVal = max || Math.max(...items.map((i) => i.value)); return `
${items.map((i) => { const pct = Math.max(4, (i.value / maxVal) * 100); return `
${i.value}
${esc(i.label)}
`; }).join('')}
`; } // ============================================================ // P15 · Tag Cloud · 标签云(高频词) // ============================================================ function renderTagCloud({ tags, accent = BRAND_STUB.MAIN }) { return `
${tags.map((t) => { const size = t.count ? Math.min(1.1, 0.78 + Math.log10(t.count) * 0.1) : 0.82; const alpha = t.count ? Math.min(44, 14 + t.count) : 28; return `#${esc(t.label)}${t.count ? ` · ${t.count}` : ''}`; }).join('')}
`; } // ============================================================ // P16 · Flow Diagram · 决策链流程图(纯 div + Unicode 箭头) // ============================================================ function renderFlow({ steps, accent = BRAND_STUB.MAIN }) { return `
${steps.map((s, i) => { const box = `
${esc(s)}
`; const arrow = i < steps.length - 1 ? `
` : ''; return box + arrow; }).join('')}
`; } // ============================================================ // P17 · Platform Badge · 平台徽章(带图标) // ============================================================ function renderPlatformBadge(platform, count, color) { const icons = { xhs: '📕', douyin: '🎵', weibo: '📱', jd: '🛒', tmall: '🛍', amazon: '📦' }; const labels = { xhs: 'XHS', douyin: 'DOUYIN', weibo: 'WEIBO', jd: 'JD', tmall: 'TMALL', amazon: 'AMZN' }; return ` ${icons[platform] || '📊'} ${labels[platform] || platform.toUpperCase()}${count !== undefined ? ` · ${count}` : ''} `; } // ============================================================ // P18 · Hypothesis Tag · 假设标签(H1-H8) // ============================================================ function renderHypothesisTag(h, desc, color) { return `
${h} ${esc(desc || '')}
`; } // ============================================================ // P19 · Page Indicator · 右下角页码(固定定位) // ============================================================ // 使用:作为独立 HTML 节点放在 body 末尾,配合 JS 更新 function renderPageIndicator() { return `
1 / 1
`; } // 配套 JS · 嵌到 NAV_JS 末尾 const PAGE_INDICATOR_JS = ` (function(){ const indicator = document.getElementById('page-indicator'); const sections = document.querySelectorAll('.report-section'); if (!indicator || !sections.length) return; indicator.textContent = '1 / ' + sections.length; const observer = new IntersectionObserver((entries) => { for (const e of entries) { if (e.isIntersecting && e.intersectionRatio > 0.5) { const idx = [...sections].indexOf(e.target) + 1; indicator.textContent = idx + ' / ' + sections.length; break; } } }, { threshold: [0.5] }); sections.forEach((s) => observer.observe(s)); })(); `; // ============================================================ // P20 · Nav JS · 键盘导航(标配) // ============================================================ // 嵌到 render.js 的 NAV_JS 常量里 const NAV_JS = ` (function() { const sections = document.querySelectorAll('.report-section'); let idx = 0; function go(i) { idx = Math.max(0, Math.min(sections.length - 1, i)); sections[idx].scrollIntoView({ behavior: 'smooth' }); if (sections[idx].id) history.replaceState(null, '', '#' + sections[idx].id); } document.addEventListener('keydown', (e) => { if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') { e.preventDefault(); go(idx + 1); } if (e.key === 'ArrowLeft' || e.key === 'PageUp') { e.preventDefault(); go(idx - 1); } if (e.key === 'Home') go(0); if (e.key === 'End') go(sections.length - 1); }); if (location.hash) { const i = [...sections].findIndex((s) => '#' + s.id === location.hash); if (i >= 0) idx = i; } })(); `; // ============================================================ // P21 · Gradient Cover Background(Cover 专用) // ============================================================ const COVER_STYLE = ` .cover { background: linear-gradient(180deg, #0A0A0A 0%, #111 100%); position: relative; overflow: hidden; } .cover::before { content: ''; position: absolute; top: -50%; right: -10%; width: 60%; height: 200%; background: radial-gradient(circle, BRAND_MAIN_ALPHA 0%, transparent 70%); opacity: 0.3; pointer-events: none; } `; // ============================================================ // P22 · Print Friendly · 打印友好 // ============================================================ const PRINT_CSS = ` @media print { .report-section { min-height: auto; page-break-after: always; padding: 20px; border: none; } body { background: white; color: black; } .report-section, .section-inner { background: white !important; } [style*="color:var(--text-1)"] { color: black !important; } [style*="color:var(--text-2)"] { color: #333 !important; } [style*="color:var(--text-3)"] { color: #666 !important; } #page-indicator { display: none; } } `; // ============================================================ // P23 · Mobile Degradation · 移动端降级 // ============================================================ const MOBILE_CSS = ` @media (max-width: 768px) { .report-section { padding: 24px 16px; } .section-inner { max-width: 100%; } .grid { grid-template-columns: 1fr !important; gap: 10px !important; } .cover h1 { font-size: 2rem !important; } .divider h1 { font-size: 2.8rem !important; } h2 { font-size: 1.4rem !important; } [style*="font-size:4rem"] { font-size: 2.4rem !important; } [style*="font-size:5rem"] { font-size: 2.8rem !important; } [style*="font-size:3.6rem"] { font-size: 2rem !important; } } `; // ============================================================ // 导出(只是作为参考,实际使用时 copy-paste 到你的 components.js) // ============================================================ module.exports = { // 基础工具 esc, fmtLikes, // P01-P23 视觉模式 renderEyebrow, // P01 renderSectionHead, // P02 renderVocCard, // P03 renderVocCardCompact, // P04 renderStatStrip, // P05 renderBigStat, // P06 renderInsightHero, // P07 renderDecisionList, // P08 renderCompareTable, // P09 renderMatrix2x2, // P10 renderTheoryBox, // P11 renderFindingCard, // P12 renderProgressBar, // P13 renderInlineBars, // P14 renderTagCloud, // P15 renderFlow, // P16 renderPlatformBadge, // P17 renderHypothesisTag, // P18 renderPageIndicator, // P19 NAV_JS, // P20 PAGE_INDICATOR_JS, COVER_STYLE, // P21 PRINT_CSS, // P22 MOBILE_CSS, // P23 };