// ==============================================================================
// 精美 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) => `| ${esc(h)} | `).join('')}
${rows.map((row) => `
${row.map((cell) => {
const c = typeof cell === 'object' ? cell : { html: cell };
return `| ${c.html || c} | `;
}).join('')}
`).join('')}
`;
}
// ============================================================
// 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.items || []).map((i) => `- ${i}
`).join('')}
${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 `
`;
}).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
};