312 lines
15 KiB
JavaScript
Raw Normal View History

// 小红书风格小组件库 v1.0
// 每个widget = 核心HTML模板 + 3~5个随机变体(variant) + CSS变量可覆写
// 国作登字-2026-A-00037559
// 设计原则: 每个widget是独立片段的HTML可随机组合不打架
export const WIDGETS = {
// ═══════════════════════════════════════
// 装饰类 Widgets
// ═══════════════════════════════════════
'dot-grid': {
name: '圆点矩阵',
category: 'decor',
variants: [
{ id: 'warm', css: { size: '6px', color: '#E07B39', opacity: '0.25', gap: '18px', cols: 8, rows: 3 } },
{ id: 'cool', css: { size: '4px', color: '#4A90A4', opacity: '0.2', gap: '22px', cols: 10, rows: 2 } },
{ id: 'mixed', css: { size: '5px', colors: ['#E07B39','#4A90A4','#5B8C5A'], opacity: '0.3', gap: '16px', cols: 6, rows: 4 } },
{ id: 'large', css: { size: '8px', color: '#2D5016', opacity: '0.12', gap: '24px', cols: 5, rows: 2 } },
],
render(v) {
const c = v.css, colors = c.colors || [c.color]
let dots = ''
const total = (c.cols || 6) * (c.rows || 3)
for (let i = 0; i < total; i++) {
const clr = colors[i % colors.length]
const r = Math.floor(i / c.cols), col = i % c.cols
const ox = (Math.random() - 0.5) * 4, oy = (Math.random() - 0.5) * 4
dots += `<circle cx="${col * parseInt(c.gap) + 20 + ox}" cy="${r * parseInt(c.gap) + 14 + oy}" r="${c.size}" fill="${clr}" opacity="${c.opacity}"/>`
}
return `<svg width="${c.cols * parseInt(c.gap) + 16}" height="${c.rows * parseInt(c.gap) + 16}" style="display:block">${dots}</svg>`
}
},
'gradient-stripe': {
name: '渐变装饰条',
category: 'decor',
variants: [
{ id: 'warm-sunset', css: { colors: ['#E07B39','#F5A623','#FFD700'], height: '6px', width: '80%', radius: '3px' } },
{ id: 'cool-ocean', css: { colors: ['#4A90A4','#5B8C5A','#7BC47B'], height: '4px', width: '60%', radius: '2px' } },
{ id: 'rose-gold', css: { colors: ['#E8435E','#C9A96E','#F5D0C5'], height: '5px', width: '70%', radius: '3px' } },
{ id: 'forest', css: { colors: ['#2D5016','#5B8C5A','#A8D5A2'], height: '8px', width: '50%', radius: '4px' } },
],
render(v) {
const c = v.css
const grad = c.colors.map((clr, i) => `${clr} ${(i / (c.colors.length - 1)) * 100}%`).join(', ')
return `<div style="width:${c.width};height:${c.height};border-radius:${c.radius};background:linear-gradient(90deg,${grad});margin:12px auto"></div>`
}
},
'sticker-badge': {
name: '贴纸角标',
category: 'decor',
variants: [
{ id: 'hot', text: 'HOT', bg: '#E8435E', color: '#FFF', rotate: '-8deg' },
{ id: 'new', text: 'NEW', bg: '#E07B39', color: '#FFF', rotate: '5deg' },
{ id: 'free', text: 'FREE', bg: '#5B8C5A', color: '#FFF', rotate: '-3deg' },
{ id: 'top', text: 'TOP', bg: '#4A90A4', color: '#FFF', rotate: '10deg' },
{ id: 'hand', text: '手写', bg: '#F5E6C8', color: '#8B4513', rotate: '-12deg' },
],
render(v) {
return `<div style="display:inline-block;background:${v.bg};color:${v.color};padding:6px 16px;border-radius:6px;font-size:18px;font-weight:800;transform:rotate(${v.rotate});box-shadow:2px 2px 0 rgba(0,0,0,.1);letter-spacing:2px">${v.text}</div>`
}
},
'hand-drawn-circle': {
name: '手绘圈线',
category: 'decor',
variants: [
{ id: 'orange', stroke: '#E07B39', width: '3px', dash: 'none', radius: '60px' },
{ id: 'green', stroke: '#5B8C5A', width: '2px', dash: '8,4', radius: '50px' },
{ id: 'blue', stroke: '#4A90A4', width: '2.5px', dash: '12,6', radius: '45px' },
{ id: 'pink', stroke: '#E8435E', width: '3px', dash: '6,3', radius: '55px' },
],
render(v) {
const d = parseInt(v.radius) * 2
return `<svg width="${d + 10}" height="${d + 10}" style="display:block"><circle cx="${d/2 + 5}" cy="${d/2 + 5}" r="${v.radius}" fill="none" stroke="${v.stroke}" stroke-width="${v.width}" ${v.dash !== 'none' ? `stroke-dasharray="${v.dash}"` : ''} opacity="0.6"/></svg>`
}
},
// ═══════════════════════════════════════
// 信息类 Widgets
// ═══════════════════════════════════════
'photo-frame': {
name: '照片框',
category: 'info',
description: '带白边/阴影的照片展示框,支持随机占位色块',
variants: [
{ id: 'polaroid', style: 'polaroid', borderColor: '#FFF', borderWidth: '12px 12px 40px 12px', rotate: '-2deg' },
{ id: 'rounded', style: 'rounded', borderColor: '#FFF', borderRadius: '16px', shadow: true },
{ id: 'film', style: 'film', borderColor: '#1A1A1A', borderWidth: '6px 6px 24px 6px', rotate: '1deg' },
{ id: 'clean', style: 'clean', borderColor: 'transparent', borderRadius: '20px' },
],
render(v) {
const w = v.width || 280, h = v.height || 210
const placeholderColors = ['#F5D0C5','#A8D5A2','#B5D4F4','#F5E6C8','#F4C0D1']
const bg = placeholderColors[Math.floor(Math.random() * placeholderColors.length)]
let style = `width:${w}px;height:${h}px;background:${bg}`
if (v.style === 'polaroid') style += `;border:${v.borderWidth} solid ${v.borderColor};transform:rotate(${v.rotate})`
else if (v.style === 'film') style += `;border:${v.borderWidth} solid ${v.borderColor};transform:rotate(${v.rotate})`
else if (v.style === 'rounded') style += `;border:4px solid ${v.borderColor};border-radius:${v.borderRadius}`
else style += `;border-radius:${v.borderRadius};overflow:hidden`
if (v.shadow) style += ';box-shadow:0 8px 30px rgba(0,0,0,.1)'
if (v.caption) {
return `<div style="display:inline-block"><div style="${style};position:relative">${v.emoji ? `<span style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:48px">${v.emoji}</span>` : ''}</div><div style="text-align:center;font-size:14px;color:#8B8680;margin-top:8px">${v.caption}</div></div>`
}
return `<div style="${style};position:relative">${v.emoji ? `<span style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:48px">${v.emoji}</span>` : ''}</div>`
}
},
'info-tag-row': {
name: '信息标签行',
category: 'info',
description: '一排小红书风格的信息标签:价格/难度/时长/评分等',
variants: [
{ id: 'price', items: [{ icon: '💰', text: '均价', value: '¥28' }, { icon: '⭐', text: '评分', value: '4.8' }, { icon: '⏱', text: '耗时', value: '30min' }] },
{ id: 'recipe', items: [{ icon: '👨‍🍳', text: '难度', value: '简单' }, { icon: '⏱', text: '时间', value: '20min' }, { icon: '🍽', text: '份量', value: '2人' }] },
{ id: 'travel', items: [{ icon: '📍', text: '目的地', value: '杭州' }, { icon: '💰', text: '预算', value: '¥500' }, { icon: '📅', text: '天数', value: '3天' }] },
{ id: 'study', items: [{ icon: '📚', text: '方法', value: '番茄钟' }, { icon: '⏱', text: '时长', value: '25min' }, { icon: '📈', text: '效果', value: '显著' }] },
],
render(v) {
const items = (v.items || v.variant?.items || []).map(i =>
`<div style="text-align:center;padding:10px 18px;background:#FDF8F3;border-radius:16px">
<div style="font-size:14px;color:#8B8680;margin-bottom:2px">${i.icon} ${i.text}</div>
<div style="font-size:22px;font-weight:800;color:#2D5016">${i.value}</div>
</div>`
).join('')
return `<div style="display:flex;gap:12px;justify-content:center">${items}</div>`
}
},
'bullet-list': {
name: '要点清单',
category: 'info',
description: '小红书风格的bullet point列表支持emoji前缀',
variants: [
{ id: 'check', bullet: '✅', color: '#5B8C5A', items: ['要点一','要点二','要点三'] },
{ id: 'star', bullet: '✦', color: '#E07B39', items: ['关键点A','关键点B','关键点C'] },
{ id: 'number', bullet: 'N', color: '#4A90A4', numbered: true, items: ['第一步','第二步','第三步'] },
{ id: 'heart', bullet: '♡', color: '#E8435E', items: ['喜欢的原因','喜欢的原因','喜欢的原因'] },
],
render(v) {
const items = (v.items || v.variant?.items || []).map((item, i) => {
const prefix = v.numbered ? `${String(i + 1).padStart(2, '0')}.` : v.bullet
return `<div style="display:flex;align-items:flex-start;gap:10px;margin-bottom:10px">
<span style="color:${v.color};font-weight:700;font-size:18px;flex-shrink:0;min-width:28px">${prefix}</span>
<span style="font-size:18px;color:#1A1A1A;line-height:1.6">${item}</span>
</div>`
}).join('')
return `<div style="padding:8px 0">${items}</div>`
}
},
// ═══════════════════════════════════════
// 排版类 Widgets
// ═══════════════════════════════════════
'split-layout': {
name: '左右分栏',
category: 'layout',
description: '左图右文或左文右图分栏,小红书经典排版',
variants: [
{ id: 'image-left', direction: 'row', ratio: '45:55', gap: '16px' },
{ id: 'image-right', direction: 'row-reverse', ratio: '45:55', gap: '16px' },
{ id: 'equal', direction: 'row', ratio: '50:50', gap: '20px' },
{ id: 'stack-top', direction: 'column', ratio: 'auto' },
],
render(v) {
const [l, r] = (v.ratio || '50:50').split(':').map(Number)
const dir = v.direction === 'column' ? 'column' : 'row'
return {
wrapper: true,
style: `display:flex;flex-direction:${dir};gap:${v.gap || '16px'};align-items:center`,
leftStyle: dir === 'column' ? 'width:100%' : `flex:0 0 ${l}%`,
rightStyle: dir === 'column' ? 'width:100%' : `flex:1`,
}
}
},
'card-grid': {
name: '卡片网格',
category: 'layout',
description: '小红书同款2x2或3列卡片网格',
variants: [
{ id: '2x2', cols: 2, gap: '12px', cardHeight: '100px' },
{ id: '1x3', cols: 3, gap: '10px', cardHeight: '140px' },
{ id: '1x2-wide', cols: 2, gap: '16px', cardHeight: '180px' },
],
render(v) {
return {
wrapper: true,
style: `display:grid;grid-template-columns:repeat(${v.cols},1fr);gap:${v.gap}`,
cardStyle: `height:${v.cardHeight};background:#FDF8F3;border-radius:16px;display:flex;align-items:center;justify-content:center`,
}
}
},
// ═══════════════════════════════════════
// 情感类 Widgets
// ═══════════════════════════════════════
'sticky-note': {
name: '便签纸',
category: 'emotion',
variants: [
{ id: 'yellow', bg: '#FFF9C4', shadow: '#E6D88A', rotate: '-3deg', pin: '#F5A623' },
{ id: 'pink', bg: '#FCE4EC', shadow: '#F0D0D8', rotate: '2deg', pin: '#E8435E' },
{ id: 'green', bg: '#E8F5E9', shadow: '#C8E6C9', rotate: '-1deg', pin: '#5B8C5A' },
{ id: 'blue', bg: '#E3F2FD', shadow: '#BBDEFB', rotate: '4deg', pin: '#4A90A4' },
],
render(v) {
return `<div style="position:relative;display:inline-block">
<div style="position:absolute;top:-8px;left:50%;transform:translateX(-50%);width:12px;height:12px;border-radius:50%;background:${v.pin};box-shadow:0 2px 4px rgba(0,0,0,.2);z-index:1"></div>
<div style="background:${v.bg};padding:20px 24px;border-radius:4px;transform:rotate(${v.rotate});box-shadow:2px 3px 6px ${v.shadow};max-width:200px;line-height:1.7">
<span style="font-size:16px;color:#4A3728;line-height:1.8">${v.text || '便签内容'}</span>
</div>
</div>`
}
},
'highlight-box': {
name: '高亮强调框',
category: 'emotion',
description: '小红书金句高亮框,荧光笔标注风格',
variants: [
{ id: 'yellow-marker', bg: '#FFF3CD', border: '#FFE69C', text: '#856404', icon: '💡' },
{ id: 'pink-marker', bg: '#FCE4EC', border: '#F48FB1', text: '#880E4F', icon: '❤️' },
{ id: 'green-marker', bg: '#E8F5E9', border: '#A5D6A7', text: '#1B5E20', icon: '🌟' },
{ id: 'blue-marker', bg: '#E3F2FD', border: '#90CAF9', text: '#0D47A1', icon: '📌' },
],
render(v) {
return `<div style="background:${v.bg};border-left:4px solid ${v.border};border-radius:4px 12px 12px 4px;padding:14px 20px;margin:8px 0">
<span style="font-size:15px;color:${v.text};line-height:1.7">${v.icon ? v.icon + ' ' : ''}${v.text || '金句内容'}</span>
</div>`
}
},
'mood-indicator': {
name: '心情标识',
category: 'emotion',
variants: [
{ id: 'happy', mood: '😊 开心', bg: '#FFF9C4', color: '#F5A623' },
{ id: 'calm', mood: '😌 平静', bg: '#E8F5E9', color: '#5B8C5A' },
{ id: 'excited', mood: '🤩 激动', bg: '#FCE4EC', color: '#E8435E' },
{ id: 'focused', mood: '🧘 专注', bg: '#E3F2FD', color: '#4A90A4' },
],
render(v) {
return `<div style="display:inline-flex;align-items:center;gap:8px;padding:8px 16px;background:${v.bg};border-radius:20px">
<span style="font-size:16px;color:${v.color};font-weight:700">${v.mood}</span>
</div>`
}
},
// ═══════════════════════════════════════
// 特殊效果 Widgets
// ═══════════════════════════════════════
'scribble-underline': {
name: '手写划线下划线',
category: 'decor',
variants: [
{ id: 'orange-wavy', color: '#E07B39', style: 'wavy', offset: '4px', width: '3px' },
{ id: 'green-straight', color: '#5B8C5A', style: 'solid', offset: '2px', width: '4px' },
{ id: 'pink-dashed', color: '#E8435E', style: 'dashed', offset: '6px', width: '2px' },
],
render(v) {
return {
cssClass: `text-decoration:underline;text-decoration-color:${v.color};text-decoration-thickness:${v.width};text-underline-offset:${v.offset};${v.style !== 'solid' ? `text-decoration-style:${v.style}` : ''}`,
inline: true,
}
}
},
'corner-fold': {
name: '折角效果',
category: 'decor',
variants: [
{ id: 'top-right', corner: 'top-right', color: '#FDF8F3', size: '30px' },
{ id: 'top-left', corner: 'top-left', color: '#E8F0E0', size: '24px' },
],
render(v) {
return `<div style="position:absolute;${v.corner === 'top-right' ? 'top:0;right:0' : 'top:0;left:0'};width:0;height:0;border-style:solid;border-width:${v.size};border-color:${v.color} ${v.color} transparent transparent;z-index:2"></div>`
}
},
}
// 随机选择一个widget的变体
export function randomVariant(widgetId) {
const w = WIDGETS[widgetId]
if (!w || !w.variants) return null
const idx = Math.floor(Math.random() * w.variants.length)
return w.variants[idx]
}
// 列举所有widget
export function listWidgets() {
return Object.entries(WIDGETS).map(([id, w]) => ({
id,
name: w.name,
category: w.category,
description: w.description || '',
variantCount: (w.variants || []).length,
}))
}
// 按类别筛选
export function widgetsByCategory(category) {
return listWidgets().filter(w => w.category === category)
}