// ==============================================================================
// <品类名> · VOC 报告 UI 组件库(模板)
// ==============================================================================
// 职责:
// - BRAND 色板(品类专属)
// - 10 个核心组件(Cover / Agenda / Divider / SectionHead / VocCard
// StatStrip / CompareTable / Matrix2x2 / InsightHero / DecisionList)
// - esc/fmtLikes 等工具
// ==============================================================================
// ==========================================================
// 1. BRAND 色板(按品类定制)
// TODO: 修改为当前品类的主题色
// ==========================================================
const BRAND = {
// 品类主色
MAIN: '#4A90E2',
MAIN_GLOW: 'rgba(74,144,226,0.15)',
// 品牌背书色(江中国货等)
JIANG_GREEN: '#1A7C5F',
OTC_BLUE: '#0071CE',
// 情绪表达色
CORAL: '#FF6B6B', // 痛点 / 焦虑
LACTIC_MINT: '#8FD3B8', // 平和 / 解法
WINE_RED: '#722F37', // 反向 / 禁区
GOLD: '#D4AF37', // 尊贵 / 溢价
// 辅助色
TECH_BLUE: '#4A90E2',
PURPLE: '#8B5CF6',
SAGE: '#9CAF88',
BERRY_PINK: '#E63E7B',
ROSE: '#FF4D8D',
SKY_BLUE: '#56CCF2',
WARM_ORANGE: '#FF9F43',
SUNNY_YELLOW: '#FFCC33',
// 分龄色(儿童品类可选)
BABY_PEACH: '#FFB6A3',
PRESCHOOL_LIME: '#B5E48C',
SCHOOL_LAVENDER: '#B5A3E0',
};
// ==========================================================
// 2. 工具函数
// ==========================================================
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);
}
// ==========================================================
// 3. renderVocCard · VOC 证据卡(核心组件)
// ==========================================================
function renderVocCard(item, opts = {}) {
if (!item) return '';
const accent = opts.accent || BRAND.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}」
`;
}
// ==========================================================
// 4. renderSectionHead · 子章节 Hero
// ==========================================================
function renderSectionHead(opts = {}) {
const { eyebrow, title, subtitle, color = BRAND.MAIN } = opts;
return `
${eyebrow ? `
${eyebrow}
` : ''}
${title || ''}
${subtitle ? `
${subtitle}
` : ''}
`;
}
// ==========================================================
// 5. renderStatStrip · KPI 数字条
// ==========================================================
function renderStatStrip(stats, opts = {}) {
const accent = opts.accent || BRAND.MAIN;
return `
${stats.map((s) => `
${esc(s.num)}
${esc(s.label)}
${s.delta ? `
${esc(s.delta)}
` : ''}
`).join('')}
`;
}
// ==========================================================
// 6. renderDecisionList · 落地决策列表(每子章收尾)
// ==========================================================
function renderDecisionList(items, opts = {}) {
const accent = opts.accent || BRAND.MAIN;
const title = opts.title || '落地决策';
const columns = opts.columns || 2;
return `
🎯 ${esc(title)}
${items.map((x) => `
${x}
`).join('')}
`;
}
// ==========================================================
// 7. renderInsightHero · 金句观点卡
// ==========================================================
function renderInsightHero(opts = {}) {
const { eyebrow, title, subtitle, color = BRAND.MAIN } = opts;
return `
${eyebrow ? `
${eyebrow}
` : ''}
${title || ''}
${subtitle ? `
${subtitle}
` : ''}
`;
}
// ==========================================================
// 8. renderCompareTable · 对比表格
// ==========================================================
function renderCompareTable(opts = {}) {
const { title, headers = [], rows = [], accent = BRAND.MAIN } = opts;
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('')}
`;
}
// ==========================================================
// 9. renderMatrix2x2 · 无人地带四象限
// ==========================================================
function renderMatrix2x2(opts = {}) {
const { xLabel, yLabel, xLow, xHigh, yLow, yHigh, quadrants = {}, accent = BRAND.MAIN } = opts;
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)}
` : ''}
`;
}
// ==========================================================
// 10. renderTheoryBox · 理论盒子(mapping 类)
// ==========================================================
function renderTheoryBox(opts = {}) {
const { theory, accent = BRAND.MAIN, mapping } = opts;
return `
${theory ? `
${esc(theory)}
` : ''}
${mapping || ''}
`;
}
// ==========================================================
// 11. renderFindingCard · 发现洞察卡
// ==========================================================
function renderFindingCard(opts = {}) {
const { icon, title, body, accent = BRAND.MAIN } = opts;
return `
${icon ? `
${icon}
` : ''}
${title ? `
${title}
` : ''}
${body || ''}
`;
}
// ==========================================================
// 12. 导出
// ==========================================================
module.exports = {
BRAND,
esc,
fmtLikes,
renderVocCard,
renderSectionHead,
renderStatStrip,
renderDecisionList,
renderInsightHero,
renderCompareTable,
renderMatrix2x2,
renderTheoryBox,
renderFindingCard,
};