diff --git a/image-generator/xiaohongshu-widgets/widgets.js b/image-generator/xiaohongshu-widgets/widgets.js new file mode 100644 index 0000000..a714b67 --- /dev/null +++ b/image-generator/xiaohongshu-widgets/widgets.js @@ -0,0 +1,311 @@ +// 小红书风格小组件库 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 += `` + } + return `${dots}` + } + }, + + '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 `
` + } + }, + + '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 `
${v.text}
` + } + }, + + '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 `` + } + }, + + // ═══════════════════════════════════════ + // 信息类 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 `
${v.emoji ? `${v.emoji}` : ''}
${v.caption}
` + } + return `
${v.emoji ? `${v.emoji}` : ''}
` + } + }, + + '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 => + `
+
${i.icon} ${i.text}
+
${i.value}
+
` + ).join('') + return `
${items}
` + } + }, + + '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 `
+ ${prefix} + ${item} +
` + }).join('') + return `
${items}
` + } + }, + + // ═══════════════════════════════════════ + // 排版类 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 `
+
+
+ ${v.text || '便签内容'} +
+
` + } + }, + + '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 `
+ ${v.icon ? v.icon + ' ' : ''}${v.text || '金句内容'} +
` + } + }, + + '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 `
+ ${v.mood} +
` + } + }, + + // ═══════════════════════════════════════ + // 特殊效果 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 `
` + } + }, +} + +// 随机选择一个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) +}