// 江中猴菇饮 · VOC 报告共享 UI 组件 // 所有子章渲染器通用:VOC 证据卡 / 理论盒 / 决策项 / Tag 云 / 统计条 // // 设计:不依赖外部图表库,纯 CSS + SVG 实现轻量可视化; // 所有 pattern-curated 样本自动加「◈ 模式归纳」徽章以保证透明度。 const BRAND = { JIANG_GREEN: '#2D9C5D', HOUGU_TAN: '#E8A87C', DEEP_AMBER: '#C96E3F', CREAM: '#F5E6D3', TECH_BLUE: '#0085CA', WARM_ORANGE: '#FF8C42', CORAL: '#FF6B6B', SAGE: '#87A96B', PURPLE: '#8B5CF6', ROSE: '#FF4D8D', }; const PLATFORM_LABELS = { xhs: { name: '小红书', color: '#FF2442', short: '红' }, douyin: { name: '抖音', color: '#1A1A1A', short: '抖' }, taobao: { name: '淘宝', color: '#FF5000', short: '淘' }, jd: { name: '京东', color: '#E1251B', short: '京' }, zhihu: { name: '知乎', color: '#0084FF', short: '知' }, dingxiang: { name: '丁香医生', color: '#00B368', short: '丁' }, tmall: { name: '天猫', color: '#FF0036', 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); } // ================================================================ // VOC 证据卡(单条)· 核心组件 // ================================================================ function renderVocCard(item, opts = {}) { const { accent = BRAND.HOUGU_TAN, showBasis = false, compact = false } = 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 || '匿名')} · ${esc(item.ip || '—')}
${sen.icon} ${sen.label} ♥${fmtLikes(item.likes)} ${isSeed ? `◈模式` : `✓真实`}
"${esc(item.content || '')}"
${tags.length ? `
${tags.map((t) => `${esc(t)}`).join('')}
` : ''} ${showBasis && item.basis ? `
依据:${esc(item.basis)}
` : ''}
`; } // ================================================================ // VOC 网格(多条) // ================================================================ function renderVocGrid(items, opts = {}) { const { columns = 2, accent = BRAND.HOUGU_TAN, compact = false } = opts; return `
${items.map((it) => renderVocCard(it, { accent, compact })).join('')}
`; } // ================================================================ // Insight Hero · 大洞察摘要条 // ================================================================ function renderInsightHero({ eyebrow, title, subtitle, color = BRAND.HOUGU_TAN }) { return `
${eyebrow ? `
${esc(eyebrow)}
` : ''}
${title}
${subtitle ? `
${subtitle}
` : ''}
`; } // ================================================================ // 决策项列表(Takeaways) // ================================================================ 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('')}
`; } // ================================================================ // 理论映射盒(KANO/JTBD/场景三元素) // ================================================================ function renderTheoryBox({ theory, mapping, accent = BRAND.PURPLE }) { return `
THEORY · ${esc(theory)}
${mapping}
`; } // ================================================================ // Tag 频次云(用于某假设的主题分布) // ================================================================ function renderTagCloud(tagGroups, opts = {}) { const { accent = BRAND.HOUGU_TAN, 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.HOUGU_TAN } = opts; return `
${stats.map((s) => `
${s.num}
${esc(s.label)}
${s.delta ? `
${esc(s.delta)}
` : ''}
`).join('')}
`; } // ================================================================ // 水平条形对比图(SVG,适用于多类别对比) // ================================================================ function renderBarChart(rows, opts = {}) { const { accent = BRAND.HOUGU_TAN, 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 = 60; 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('')}
`; } // ================================================================ // 2×2 矩阵图(四象限,用于未满足需求地图等) // ================================================================ function renderMatrix2x2({ xLabel, yLabel, xLow, xHigh, yLow, yHigh, quadrants, accent = BRAND.HOUGU_TAN }) { // quadrants: { q1: {title, color, items, note}, q2: ..., q3: ..., q4: ... } // q1=右上, q2=左上, q3=左下, q4=右下 const W = 760, H = 520, pad = 60; const cx = W / 2, cy = H / 2; 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, x, y) => `
${esc(q.title)}
${q.note ? `
${q.note}
` : ''}
`; return `
${esc(yLabel)}
${esc(yHigh)}
${esc(yHigh)}
${renderQuad(q2, 0, 0)}
${renderQuad(q1, 1, 0)}
${renderQuad(q3, 0, 1)}
${renderQuad(q4, 1, 1)}
${esc(xLow)} ← ${esc(xLabel)} → ${esc(xHigh)}
`; } // ================================================================ // 章节 section head(标题区通用) // ================================================================ function renderSectionHead({ eyebrow, title, subtitle, color = BRAND.HOUGU_TAN }) { return `
${esc(eyebrow)}

${title}

${subtitle ? `

${subtitle}

` : ''}
`; } module.exports = { BRAND, PLATFORM_LABELS, SENTIMENT_LABELS, esc, fmtLikes, renderVocCard, renderVocGrid, renderInsightHero, renderDecisionList, renderTheoryBox, renderTagCloud, renderStatStrip, renderBarChart, renderMatrix2x2, renderSectionHead, };