152 lines
4.9 KiB
JavaScript
Raw Permalink Normal View History

// 小红书小组件渲染引擎 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 `<!-- widget render error: ${widgetId} -->`
}
}
export function renderStack(stack, options = {}) {
const scheme = options.scheme || 'warm'
const title = options.title || ''
const css = buildCSS(scheme, options.customVars || {})
let body = '<div class="card">'
// 装饰区域(顶部)
const decorWidgets = stack.filter(s => {
const w = WIDGETS[s.component]
return w && w.category === 'decor'
})
if (decorWidgets.length > 0) {
body += '<div class="decor-section">'
body += decorWidgets.map(s => renderWidget(s.component, s.variant, s.params)).join('')
body += '</div>'
}
// 标题
if (title) {
body += `<div class="widget-title"><h1>${title}</h1></div>`
}
// 主要内容widget
const mainWidgets = stack.filter(s => {
const w = WIDGETS[s.component]
return w && w.category !== 'decor'
})
for (const s of mainWidgets) {
body += `<div class="widget-row start">${renderWidget(s.component, s.variant, s.params)}</div>`
}
body += '</div>'
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8"><style>${css}</style></head><body>${body}</body></html>`
}
// 完全随机风格生成
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 }
}