| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- // ==============================================================================
- // <品类名> · 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, '>')
- .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 `
- <div style="padding:14px 16px; background:var(--bg-1); border:1px solid var(--border);
- border-left:3px solid ${accent}; border-radius:6px;">
- <div style="display:flex; justify-content:space-between; gap:10px; margin-bottom:8px; font-size:0.78rem;">
- <div style="flex:1; min-width:0;">
- <span class="text-mono" style="color:${accent}; font-weight:700; letter-spacing:0.1em;">
- ${(item.platform || 'XHS').toUpperCase()}
- </span>
- <span style="color:var(--text-3);"> · ${esc(item.nickname || '匿名')}</span>
- ${item.ip ? `<span style="color:var(--text-3);"> · ${esc(item.ip)}</span>` : ''}
- </div>
- <div style="color:${accent}; font-family:var(--font-mono); font-weight:700; white-space:nowrap;">
- ♥${fmtLikes(item.likes)}
- </div>
- </div>
- <div style="color:var(--text-1); font-size:0.88rem; line-height:1.7;">
- 「${esc(content)}${more}」
- </div>
- </div>`;
- }
- // ==========================================================
- // 4. renderSectionHead · 子章节 Hero
- // ==========================================================
- function renderSectionHead(opts = {}) {
- const { eyebrow, title, subtitle, color = BRAND.MAIN } = opts;
- return `
- <div style="margin-bottom:20px;">
- ${eyebrow ? `<div class="text-mono text-xs" style="color:${color}; letter-spacing:0.2em; margin-bottom:10px; font-weight:700;">${eyebrow}</div>` : ''}
- <h2 style="font-family:var(--font-head); font-weight:800; font-size:1.9rem; line-height:1.3; color:var(--text-1); margin:0 0 12px 0;">${title || ''}</h2>
- ${subtitle ? `<div style="color:var(--text-2); font-size:0.95rem; line-height:1.75; max-width:900px;">${subtitle}</div>` : ''}
- </div>`;
- }
- // ==========================================================
- // 5. renderStatStrip · KPI 数字条
- // ==========================================================
- function renderStatStrip(stats, opts = {}) {
- const accent = opts.accent || BRAND.MAIN;
- return `
- <div class="grid" style="grid-template-columns:repeat(${stats.length},1fr); gap:12px; margin:20px 0;">
- ${stats.map((s) => `
- <div style="padding:18px; background:${s.color || accent}0C; border:1px solid ${s.color || accent}33; border-radius:8px; text-align:center;">
- <div style="color:${s.color || accent}; font-family:var(--font-head); font-weight:900; font-size:2rem; line-height:1;">${esc(s.num)}</div>
- <div style="color:var(--text-2); font-size:0.82rem; margin-top:6px;">${esc(s.label)}</div>
- ${s.delta ? `<div style="color:${s.color || accent}; font-size:0.72rem; font-family:var(--font-mono); margin-top:4px;">${esc(s.delta)}</div>` : ''}
- </div>`).join('')}
- </div>`;
- }
- // ==========================================================
- // 6. renderDecisionList · 落地决策列表(每子章收尾)
- // ==========================================================
- function renderDecisionList(items, opts = {}) {
- const accent = opts.accent || BRAND.MAIN;
- const title = opts.title || '落地决策';
- const columns = opts.columns || 2;
- return `
- <div style="margin-top:24px; padding:20px 24px; background:${accent}08; border:1px solid ${accent}33; border-radius:var(--radius-md);">
- <div class="text-mono text-xs" style="color:${accent}; letter-spacing:0.18em; margin-bottom:14px; font-weight:700;">🎯 ${esc(title)}</div>
- <div class="grid" style="grid-template-columns:repeat(${columns},1fr); gap:10px;">
- ${items.map((x) => `
- <div style="padding:10px 14px; background:var(--bg-1); border-left:3px solid ${accent}; border-radius:4px; color:var(--text-1); font-size:0.86rem; line-height:1.65;">
- ${x}
- </div>`).join('')}
- </div>
- </div>`;
- }
- // ==========================================================
- // 7. renderInsightHero · 金句观点卡
- // ==========================================================
- function renderInsightHero(opts = {}) {
- const { eyebrow, title, subtitle, color = BRAND.MAIN } = opts;
- return `
- <div style="padding:24px 28px; background:linear-gradient(135deg, ${color}14, ${color}04); border:2px solid ${color}44; border-radius:var(--radius-lg); margin:20px 0;">
- ${eyebrow ? `<div class="text-mono text-xs" style="color:${color}; letter-spacing:0.2em; margin-bottom:12px; font-weight:700;">${eyebrow}</div>` : ''}
- <div style="color:var(--text-1); font-size:1.1rem; line-height:1.8;">${title || ''}</div>
- ${subtitle ? `<div style="color:var(--text-2); font-size:0.9rem; line-height:1.7; margin-top:10px;">${subtitle}</div>` : ''}
- </div>`;
- }
- // ==========================================================
- // 8. renderCompareTable · 对比表格
- // ==========================================================
- function renderCompareTable(opts = {}) {
- const { title, headers = [], rows = [], accent = BRAND.MAIN } = opts;
- return `
- <div style="margin:20px 0;">
- ${title ? `<div class="text-mono text-xs" style="color:${accent}; letter-spacing:0.18em; margin-bottom:12px; font-weight:700;">${esc(title)}</div>` : ''}
- <div style="overflow-x:auto;">
- <table style="width:100%; border-collapse:collapse; font-size:0.86rem;">
- <thead>
- <tr style="border-bottom:2px solid ${accent}44;">
- ${headers.map((h) => `<th style="text-align:left; padding:10px 12px; color:var(--text-3); font-weight:700; font-size:0.78rem; letter-spacing:0.05em;">${esc(h)}</th>`).join('')}
- </tr>
- </thead>
- <tbody>
- ${rows.map((row) => `
- <tr style="border-bottom:1px solid var(--border);">
- ${row.map((cell) => {
- const c = typeof cell === 'object' ? cell : { html: cell };
- return `<td style="padding:10px 12px; color:${c.color || 'var(--text-1)'}; ${c.bold ? 'font-weight:700;' : ''}">${c.html || c}</td>`;
- }).join('')}
- </tr>`).join('')}
- </tbody>
- </table>
- </div>
- </div>`;
- }
- // ==========================================================
- // 9. renderMatrix2x2 · 无人地带四象限
- // ==========================================================
- function renderMatrix2x2(opts = {}) {
- const { xLabel, yLabel, xLow, xHigh, yLow, yHigh, quadrants = {}, accent = BRAND.MAIN } = opts;
- const renderQ = (q) => {
- if (!q) return '<div></div>';
- return `
- <div style="padding:16px; background:${(q.color || accent)}0C; border:1px solid ${(q.color || accent)}33; border-radius:6px;">
- <div class="text-mono text-xs" style="color:${q.color || accent}; font-weight:700; margin-bottom:8px;">${q.title || ''}</div>
- <ul style="margin:0; padding-left:18px; color:var(--text-1); font-size:0.82rem; line-height:1.7;">
- ${(q.items || []).map((i) => `<li>${i}</li>`).join('')}
- </ul>
- ${q.note ? `<div style="color:var(--text-3); font-size:0.76rem; margin-top:8px; font-style:italic;">${esc(q.note)}</div>` : ''}
- </div>`;
- };
- return `
- <div style="margin:24px 0;">
- <div style="display:grid; grid-template-columns:auto 1fr 1fr; grid-template-rows:auto 1fr 1fr auto; gap:12px; align-items:stretch;">
- <div></div>
- <div style="text-align:center; color:var(--text-3); font-size:0.78rem; font-weight:700;">${esc(xLow || '')}</div>
- <div style="text-align:center; color:var(--text-3); font-size:0.78rem; font-weight:700;">${esc(xHigh || '')}</div>
- <div style="writing-mode:vertical-rl; text-align:center; color:var(--text-3); font-size:0.78rem; font-weight:700; padding:8px 0;">${esc(yHigh || '')}</div>
- ${renderQ(quadrants.q2)}
- ${renderQ(quadrants.q1)}
- <div style="writing-mode:vertical-rl; text-align:center; color:var(--text-3); font-size:0.78rem; font-weight:700; padding:8px 0;">${esc(yLow || '')}</div>
- ${renderQ(quadrants.q3)}
- ${renderQ(quadrants.q4)}
- <div></div>
- <div style="grid-column:2/4; text-align:center; color:${accent}; font-family:var(--font-mono); font-size:0.82rem; letter-spacing:0.12em; margin-top:4px;">← ${esc(xLabel || '')} →</div>
- </div>
- ${yLabel ? `<div style="text-align:center; color:${accent}; font-family:var(--font-mono); font-size:0.82rem; letter-spacing:0.12em; margin-top:8px;">Y: ${esc(yLabel)}</div>` : ''}
- </div>`;
- }
- // ==========================================================
- // 10. renderTheoryBox · 理论盒子(mapping 类)
- // ==========================================================
- function renderTheoryBox(opts = {}) {
- const { theory, accent = BRAND.MAIN, mapping } = opts;
- return `
- <div style="padding:16px 18px; background:${accent}08; border-left:3px solid ${accent}; border-radius:var(--radius-md);">
- ${theory ? `<div class="text-mono text-xs" style="color:${accent}; letter-spacing:0.15em; font-weight:700; margin-bottom:10px;">${esc(theory)}</div>` : ''}
- ${mapping || ''}
- </div>`;
- }
- // ==========================================================
- // 11. renderFindingCard · 发现洞察卡
- // ==========================================================
- function renderFindingCard(opts = {}) {
- const { icon, title, body, accent = BRAND.MAIN } = opts;
- return `
- <div style="padding:16px 18px; background:var(--bg-1); border:1px solid ${accent}33; border-left:4px solid ${accent}; border-radius:var(--radius-md);">
- <div style="display:flex; align-items:flex-start; gap:12px;">
- ${icon ? `<div style="font-size:1.6rem; line-height:1;">${icon}</div>` : ''}
- <div style="flex:1;">
- ${title ? `<div style="color:var(--text-1); font-weight:700; font-size:0.95rem; margin-bottom:6px;">${title}</div>` : ''}
- <div style="color:var(--text-2); font-size:0.84rem; line-height:1.7;">${body || ''}</div>
- </div>
- </div>
- </div>`;
- }
- // ==========================================================
- // 12. 导出
- // ==========================================================
- module.exports = {
- BRAND,
- esc,
- fmtLikes,
- renderVocCard,
- renderSectionHead,
- renderStatStrip,
- renderDecisionList,
- renderInsightHero,
- renderCompareTable,
- renderMatrix2x2,
- renderTheoryBox,
- renderFindingCard,
- };
|