// ============================================================================== // 江中乳酸菌素片儿童版 · VOC 报告共享 UI 组件(复刻 liver-components.js,换 LACTIC_BRAND) // ============================================================================== // ---------------------------------------------------------------- // LACTIC_BRAND · 乳酸菌素片儿童版专属色板(儿童友好 + 江中品牌主色) // ---------------------------------------------------------------- const BRAND = { JIANG_GREEN: '#2D9C5D', // 江中品牌主色 LACTIC_MINT: '#7ED4C2', // 乳酸菌清爽薄荷(产品主色·清洁感) BERRY_PINK: '#FF8BA7', // 莓果粉(儿童可爱) SKY_BLUE: '#6EC1E4', // 晴空蓝(安全信任) SUNNY_YELLOW: '#FFD166', // 鹅黄(儿童活力) CREAM: '#FFF4E6', // 奶油米色 CORAL: '#FF6B6B', // 痛点/诘问/差评情绪 WARM_ORANGE: '#FF8C42', // 温暖橙(场景地图) TECH_BLUE: '#0085CA', // OTC 专业蓝(药品信任) SAGE: '#87A96B', // 药食同源绿 PURPLE: '#8B5CF6', // KANO × JTBD 理论 ROSE: '#FF4D8D', // 竞品对比玫瑰 WINE_RED: '#8B2942', // 警示红 GOLD: '#C8A04B', // 礼品奢华金 BABY_PEACH: '#FFCBA4', // 婴儿桃(0-3 岁人群) PRESCHOOL_LIME: '#C5E063', // 学龄前青柠(3-6 岁) SCHOOL_LAVENDER: '#B4A7D6', // 学龄期薰衣草(6-12 岁) }; const PLATFORM_LABELS = { xhs: { name: '小红书', color: '#FF2442', short: '红' }, douyin: { name: '抖音', color: '#1A1A1A', short: '抖' }, taobao: { name: '淘宝', color: '#FF5000', short: '淘' }, jd: { name: '京东', color: '#E1251B', short: '京' }, tmall: { name: '天猫', color: '#FF0036', short: '猫' }, zhihu: { name: '知乎', color: '#0084FF', short: '知' }, unknown: { name: '其他', color: '#888', short: '—' }, }; const SENTIMENT_LABELS = { positive: { label: '好评', color: '#2D9C5D', icon: '▲' }, neutral: { label: '中性', color: '#888', icon: '●' }, negative: { label: '差评', color: '#FF6B6B', icon: '▼' }, conflicted:{ label: '矛盾', color: '#FF8C42', icon: '◆' }, }; function esc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function fmtLikes(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); } function truncate(s, n = 160) { s = String(s ?? ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; } // ================================================================ // VOC 证据卡(核心组件) // ================================================================ function renderVocCard(item, opts = {}) { const { accent = BRAND.LACTIC_MINT, showBasis = false, compact = false, maxChars = 240 } = opts; const pf = PLATFORM_LABELS[item.platform] || PLATFORM_LABELS.unknown; const sen = SENTIMENT_LABELS[item.sentiment] || SENTIMENT_LABELS.neutral; const isSeed = (item.source || '').includes('pattern') || (item.source || '').includes('seed'); const tags = Array.isArray(item.tags) ? item.tags.slice(0, 3) : []; return `
${pf.short} ${esc(item.nickname || '匿名用户')} ${item.ip ? `· ${esc(item.ip)}` : ''} ${item.keyword ? `· ${esc(item.keyword)}` : ''}
${sen.icon} ${sen.label} ♥${fmtLikes(item.likes)} ${isSeed ? `◈模式` : `✓真实`}
"${esc(truncate(item.content, maxChars))}"
${tags.length ? `
${tags.map((t) => `${esc(t)}`).join('')}
` : ''} ${showBasis && item.basis ? `
依据:${esc(item.basis)}
` : ''}
`; } function renderVocGrid(items, opts = {}) { const { columns = 2, accent = BRAND.LACTIC_MINT, compact = false, maxChars } = opts; return `
${items.map((it) => renderVocCard(it, { accent, compact, maxChars })).join('')}
`; } function renderInsightHero({ eyebrow, title, subtitle, color = BRAND.LACTIC_MINT }) { return `
${eyebrow ? `
${esc(eyebrow)}
` : ''}
${title}
${subtitle ? `
${subtitle}
` : ''}
`; } function renderDecisionList(items, opts = {}) { const { accent = BRAND.JIANG_GREEN, title = 'DECISION · 落地决策项', columns = 2 } = opts; return `
${esc(title)}
${items.map((it, i) => `
${i + 1}
${it}
`).join('')}
`; } function renderTheoryBox({ theory, mapping, accent = BRAND.PURPLE }) { return `
THEORY · ${esc(theory)}
${mapping}
`; } function renderTagCloud(tagGroups, opts = {}) { const { accent = BRAND.LACTIC_MINT, top = 15, title = '主题分布' } = opts; const list = tagGroups.slice(0, top); if (!list.length) return ''; const maxCount = Math.max(...list.map((g) => g.count)); return `
${esc(title)} · TOP ${list.length}
${list.map((g) => { const ratio = g.count / maxCount; const fs = (0.72 + ratio * 0.45).toFixed(2); const bg = Math.round(10 + ratio * 30); return `${esc(g.tag)} ×${g.count}`; }).join('')}
`; } function renderStatStrip(stats, opts = {}) { const { accent = BRAND.LACTIC_MINT } = opts; return `
${stats.map((s) => `
${s.num}
${esc(s.label)}
${s.delta ? `
${esc(s.delta)}
` : ''}
`).join('')}
`; } function renderBarChart(rows, opts = {}) { const { accent = BRAND.LACTIC_MINT, height = 260, title = '' } = opts; const max = Math.max(...rows.map((r) => r.value), 1); const barH = 28; const gap = 10; const padL = 160; const padR = 80; const padV = 16; const totalH = rows.length * (barH + gap) + padV * 2; const W = 900; return `
${title ? `
${esc(title)}
` : ''} ${rows.map((r, i) => { const y = padV + i * (barH + gap); const w = (r.value / max) * (W - padL - padR); const color = r.color || accent; return ` ${esc(r.label)} ${esc(r.valueLabel || r.value)} `; }).join('')}
`; } function renderMatrix2x2({ xLabel, yLabel, xLow, xHigh, yLow, yHigh, quadrants, accent = BRAND.LACTIC_MINT }) { const quadStyle = (q) => ({ color: q?.color || accent, title: q?.title || '', note: q?.note || '', items: q?.items || [], }); const q1 = quadStyle(quadrants.q1); const q2 = quadStyle(quadrants.q2); const q3 = quadStyle(quadrants.q3); const q4 = quadStyle(quadrants.q4); const renderQuad = (q) => `
${esc(q.title)}
${q.note ? `
${q.note}
` : ''}
`; return `
${esc(yLabel)}
${esc(yHigh)}
${esc(yHigh)}
${renderQuad(q2)}
${renderQuad(q1)}
${renderQuad(q3)}
${renderQuad(q4)}
${esc(xLow)} ← ${esc(xLabel)} → ${esc(xHigh)}
`; } function renderSectionHead({ eyebrow, title, subtitle, color = BRAND.LACTIC_MINT }) { return `
${esc(eyebrow)}

${title}

${subtitle ? `

${subtitle}

` : ''}
`; } function renderCompareTable({ title, headers, rows, accent = BRAND.LACTIC_MINT }) { return `
${title ? `
${esc(title)}
` : ''} ${headers.map((h, i) => ``).join('')} ${rows.map((r, ri) => ` ${r.map((cell, ci) => { const isHeader = ci === 0; const content = typeof cell === 'string' ? cell : (cell.html || ''); const cellColor = typeof cell === 'object' && cell.color ? cell.color : (isHeader ? 'var(--text-2)' : 'var(--text-1)'); const weight = typeof cell === 'object' && cell.bold ? '700' : '400'; return ``; }).join('')} `).join('')}
${esc(h)}
${content}
`; } function renderFindingCard({ icon = '💡', title, body, accent = BRAND.LACTIC_MINT }) { return `
${icon}
${esc(title)}
${body}
`; } function renderHypothesisBadge(h, opts = {}) { const { accent = BRAND.LACTIC_MINT, size = 'md' } = opts; const fs = size === 'sm' ? '0.64rem' : size === 'lg' ? '0.82rem' : '0.72rem'; const px = size === 'sm' ? '3px 8px' : size === 'lg' ? '6px 14px' : '4px 10px'; return `${esc(h)}`; } module.exports = { BRAND, PLATFORM_LABELS, SENTIMENT_LABELS, esc, fmtLikes, truncate, renderVocCard, renderVocGrid, renderInsightHero, renderDecisionList, renderTheoryBox, renderTagCloud, renderStatStrip, renderBarChart, renderMatrix2x2, renderSectionHead, renderCompareTable, renderFindingCard, renderHypothesisBadge, };