// 小红书小组件渲染引擎 v1.0 // 将intent-mapper产出的widget栈渲染为完整HTML // 国作登字-2026-A-00037559 import { WIDGETS, randomVariant } from './widgets.js' const FONT = "'Noto Sans CJK SC','WenQuanYi Micro Hei','PingFang SC',sans-serif" const TITLE_FONT = "'Noto Serif CJK SC','Source Han Serif SC',serif" const PRESET_SCHEMES = { warm: { bg: '#FEF7F0', cardBg: '#FFFFFF', primary: '#5D4037', accent: '#E07A5F', muted: '#8D6E63', warm: '#FAE1DD', border: '#F0E6D3' }, tech: { bg: '#0A0E27', cardBg: '#121838', primary: '#E0E6FF', accent: '#4F8CFF', muted: '#6B7D99', warm: '#1A1F3A', border: '#1E2A4A' }, green: { bg: '#F5F9F2', cardBg: '#FFFFFF', primary: '#1B3A1B', accent: '#5A8F5A', muted: '#6B8B6B', warm: '#E8F0E0', border: '#D4E4CC' }, minimal: { bg: '#FFFFFF', cardBg: '#FAFAFA', primary: '#111111', accent: '#333333', muted: '#888888', warm: '#F5F5F5', border: '#E0E0E0' }, rose: { bg: '#FDF2F8', cardBg: '#FFFFFF', primary: '#4A1942', accent: '#9D4E8D', muted: '#8E6290', warm: '#FCE4EC', border: '#F0D4E0' }, } const SCHEME_NAMES = { warm: '奶油暖调', tech: '科技蓝', green: '文艺绿植', minimal: '极简黑白', rose: '玫瑰粉调', } export function buildCSS(scheme, customVars = {}) { const s = PRESET_SCHEMES[scheme] || PRESET_SCHEMES.warm return ` :root { --bg: ${customVars.bg || s.bg}; --cardBg: ${customVars.cardBg || s.cardBg}; --primary: ${customVars.primary || s.primary}; --accent: ${customVars.accent || s.accent}; --muted: ${customVars.muted || s.muted}; --warm: ${customVars.warm || s.warm}; --border: ${customVars.border || s.border}; } *{margin:0;padding:0;box-sizing:border-box} body{ width:1080px;height:1440px;overflow:hidden; font-family:${FONT}; background:var(--bg); display:flex;justify-content:center;align-items:center; } .card{ width:960px;min-height:1320px;background:var(--cardBg); border-radius:60px;box-shadow:0 20px 80px rgba(0,0,0,.06); display:flex;flex-direction:column; padding:50px 60px 40px; position:relative;overflow:hidden; gap:24px; } .widget-title{ text-align:center;padding-top:10px; } .widget-title h1{ font-family:${TITLE_FONT}; font-size:62px;font-weight:800;color:var(--primary); line-height:1.2;letter-spacing:1px; } .widget-title .hl{ color:var(--accent);position:relative;display:inline-block; } .widget-row{ display:flex;align-items:center;justify-content:center; } .widget-row.start{justify-content:flex-start} .widget-row.between{justify-content:space-between} .flex-1{flex:1} .decor-section{text-align:center;padding:8px 0} ` } export function renderWidget(widgetId, variantId, params = {}) { const w = WIDGETS[widgetId] if (!w) return '' const variant = variantId ? w.variants.find(v => v.id === variantId) : randomVariant(widgetId) if (!variant) return '' try { return w.render({ ...variant, ...params }) } catch (e) { return `` } } export function renderStack(stack, options = {}) { const scheme = options.scheme || 'warm' const title = options.title || '' const css = buildCSS(scheme, options.customVars || {}) let body = '
' // 装饰区域(顶部) const decorWidgets = stack.filter(s => { const w = WIDGETS[s.component] return w && w.category === 'decor' }) if (decorWidgets.length > 0) { body += '
' body += decorWidgets.map(s => renderWidget(s.component, s.variant, s.params)).join('') body += '
' } // 标题 if (title) { body += `

${title}

` } // 主要内容widget const mainWidgets = stack.filter(s => { const w = WIDGETS[s.component] return w && w.category !== 'decor' }) for (const s of mainWidgets) { body += `
${renderWidget(s.component, s.variant, s.params)}
` } body += '
' return `${body}` } // 完全随机风格生成 export function randomizeComposition(baseComposition) { const { contentType, mood, stack } = baseComposition // 随机丢弃20%的widget const filtered = stack.filter(() => Math.random() > 0.2) if (filtered.length === 0) filtered.push(stack[0]) // 随机替换变体 const randomized = filtered.map(s => ({ ...s, variant: WIDGETS[s.component]?.variants ? WIDGETS[s.component].variants[Math.floor(Math.random() * WIDGETS[s.component].variants.length)].id : s.variant, })) // 随机加一个装饰 const allDecor = ['dot-grid','gradient-stripe','sticker-badge','hand-drawn-circle','corner-fold'] if (Math.random() > 0.4) { const d = allDecor[Math.floor(Math.random() * allDecor.length)] if (!randomized.find(s => s.component === d)) { randomized.unshift({ component: d, variant: randomVariant(d)?.id, params: {} }) } } return { ...baseComposition, stack: randomized } }