feat: 小红书模板2026审美升级 · CSS变量驱动配色 · 几何光斑装饰 · 三种布局(default/quote/hero) · 支持预设风格切换

This commit is contained in:
bingshuo 2026-05-27 18:31:06 +08:00
parent 5785cd8895
commit 991452b544

View File

@ -1,31 +1,32 @@
/** /**
* *
* 小红书卡片模板 · 1080×1440 (3:4) * 小红书卡片模板 · 2026审美版 · 1080×1440 (3:4)
* *
* *
* 支持 * 风格极简大标题 + 高对比度 + 大面积留白 + 渐变色块
* - 单页卡片封面/金句/干货 * 支持 CSS 变量驱动配色 registry 预设传入
* - 多页轮播长内容自动分页 *
* 布局
* - default: 标题 + 正文列表教程/干货
* - quote: 金句居中大字
* - hero: 大标题 + 一句话副标题封面
*/ */
import { STYLES } from '../config.js' import { STYLES } from '../config.js'
const W = STYLES.sizes.xiaohongshu.width const W = 1080
const H = STYLES.sizes.xiaohongshu.height const H = 1440
/**
* CSS 变量对象转换为 :root 样式
*/
function cssVarBlock(vars = {}) {
const lines = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`)
return `:root {\n${lines.join('\n')}\n }`
}
/** /**
* 生成小红书卡片 HTML * 生成小红书卡片 HTML
* @param {object} data
* @param {string} data.title - 标题
* @param {string} data.body - 正文支持 \n 换行
* @param {string} data.subtitle - 副标题可选
* @param {string} data.tag - 标签可选干货·教程
* @param {string} data.quote - 金句/引用可选
* @param {string} data.footer - 底部文字可选如账号名
* @param {string} data.layout - 布局: 'default' | 'quote' | 'list' | 'minimal'
* @param {number} data.pageNum - 当前页码多页时
* @param {number} data.totalPages - 总页数多页时
*/ */
export function xiaohongshuCard(data) { export function xiaohongshuCard(data) {
const { const {
@ -33,33 +34,46 @@ export function xiaohongshuCard(data) {
body = '', body = '',
subtitle = '', subtitle = '',
tag = '', tag = '',
quote = '',
footer = '',
layout = 'default', layout = 'default',
pageNum = 1, pageNum = 1,
totalPages = 1, totalPages = 1,
cssVars = {},
} = data } = data
const C = STYLES.colors
const F = STYLES.fonts
const T = STYLES.typography
// 正文按换行分段
const bodyLines = body.split('\n').filter(Boolean) const bodyLines = body.split('\n').filter(Boolean)
// 渲染正文:支持列表、小标题、加粗
const bodyHtml = bodyLines.map(line => { const bodyHtml = bodyLines.map(line => {
// 检测是否以 - 开头(列表项) const trimmed = line.trimStart()
if (line.trimStart().startsWith('- ')) { if (trimmed.startsWith('- ') || trimmed.startsWith('• ')) {
return `<li>${line.trimStart().slice(2)}</li>` const text = trimmed.replace(/^[-•]\s*/, '')
return `<li>${text}</li>`
} }
// 检测是否以 # 开头(小标题) if (trimmed.startsWith('## ')) {
if (line.trimStart().startsWith('#')) { return `<h3 class="sub-heading">${trimmed.slice(3)}</h3>`
return `<h3 class="sub-heading">${line.trimStart().replace(/^#+\s*/, '')}</h3>`
} }
return `<p>${line}</p>` // 内联加粗 **text**
const formatted = trimmed.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
return `<p>${formatted}</p>`
}).join('\n') }).join('\n')
const hasList = bodyLines.some(l => l.trimStart().startsWith('- ')) const hasList = bodyLines.some(l => {
const t = l.trimStart()
return t.startsWith('- ') || t.startsWith('• ')
})
const C = {
bg: 'var(--bg, #faf8f5)',
primary: 'var(--primary, #1a1a2e)',
accent: 'var(--accent, #4f8cff)',
highlight: 'var(--highlight, #00d4ff)',
gold: 'var(--gold, #c9a96e)',
warm: 'var(--warm, #f5e6c8)',
text: 'var(--text, #1a1a2e)',
textMuted: 'var(--textMuted, #6b7280)',
cardBg: 'var(--cardBg, #ffffff)',
border: 'var(--border, #e5e7eb)',
}
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
@ -68,219 +82,263 @@ export function xiaohongshuCard(data) {
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@300;400;500;700&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
${cssVarBlock(cssVars)}
body { body {
width: ${W}px; width: ${W}px;
height: ${H}px; height: ${H}px;
overflow: hidden; overflow: hidden;
background: ${C.bg}; background: ${C.bg};
font-family: ${F.body}; font-family: 'Noto Sans SC', 'PingFang SC', -apple-system, sans-serif;
display: flex; color: ${C.text};
flex-direction: column;
position: relative; position: relative;
} }
/* ── 装饰角标 ── */ /* ── 装饰 · 几何光斑 ── */
.corner-tl, .corner-br { .orb {
position: absolute; position: absolute;
width: 60px; border-radius: 50%;
height: 60px; filter: blur(80px);
border-color: ${C.accent};
opacity: 0.15; opacity: 0.15;
pointer-events: none;
} }
.corner-tl { .orb-1 {
top: 24px; left: 24px; width: 400px;
border-top: 2px solid; height: 400px;
border-left: 2px solid; background: ${C.accent};
top: -80px;
left: -120px;
} }
.corner-br { .orb-2 {
bottom: 24px; right: 24px; width: 300px;
border-bottom: 2px solid; height: 300px;
border-right: 2px solid; background: ${C.highlight};
bottom: 100px;
right: -100px;
opacity: 0.1;
} }
/* ── 主容器 ── */
.container { .container {
flex: 1; position: relative;
padding: ${STYLES.spacing.paddingY}px ${STYLES.spacing.paddingX}px; z-index: 1;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; padding: 80px 72px 60px;
}
/* ── 顶部渐变装饰线 ── */
.accent-line {
width: 80px;
height: 5px;
background: linear-gradient(90deg, ${C.accent}, ${C.highlight});
border-radius: 3px;
margin-bottom: 40px;
} }
/* ── 标签 ── */ /* ── 标签 ── */
.tag { .tag {
display: inline-block; display: inline-block;
padding: 8px 20px; padding: 10px 24px;
background: ${C.accent}; background: ${C.cardBg};
color: ${C.textLight}; color: ${C.accent};
font-size: ${T.captionSize}px; font-size: 18px;
border-radius: 20px; font-weight: 700;
margin-bottom: 24px; border-radius: 12px;
margin-bottom: 28px;
align-self: flex-start; align-self: flex-start;
letter-spacing: 2px; letter-spacing: 3px;
border: 2px solid ${C.accent}22;
} }
/* ── 标题 ── */ /* ── 标题 · 2026审美核心 ── */
.title { .title {
font-family: ${F.title}; font-family: 'Noto Serif SC', 'Source Han Serif SC', serif;
font-size: ${T.titleSize}px; font-size: 68px;
font-weight: 700; font-weight: 900;
line-height: 1.35; line-height: 1.25;
color: ${C.primary}; color: ${C.primary};
margin-bottom: ${subtitle ? 16 : 32}px; margin-bottom: 20px;
letter-spacing: 2px; letter-spacing: 2px;
} }
.subtitle { .subtitle {
font-size: ${T.subtitleSize}px; font-size: 28px;
font-weight: 300; font-weight: 400;
color: ${C.textMuted}; color: ${C.textMuted};
margin-bottom: 32px; margin-bottom: 40px;
letter-spacing: 1px; letter-spacing: 1px;
line-height: 1.5;
opacity: 0.8;
} }
/* ── 正文 ── */ /* ── 正文 ── */
.body { .body {
flex: 1; flex: 1;
font-size: ${T.bodySize}px; font-size: 26px;
line-height: ${T.lineHeight}; line-height: 1.8;
color: ${C.text}; color: ${C.text};
} }
.body p { .body p {
margin-bottom: 12px; margin-bottom: 14px;
} }
.body ul, .body ol { .body ul, .body ol {
padding-left: 32px; padding-left: 0;
margin-bottom: 12px; margin-bottom: 14px;
list-style: none;
} }
.body li { .body li {
font-size: ${T.bodySize}px; font-size: 26px;
line-height: ${T.lineHeight}; line-height: 1.8;
margin-bottom: 8px; margin-bottom: 10px;
color: ${C.text}; color: ${C.text};
list-style: none;
position: relative; position: relative;
padding-left: 24px; padding-left: 36px;
} }
.body li::before { .body li::before {
content: ""; content: "";
position: absolute; position: absolute;
left: 0; left: 0;
color: ${C.highlight}; color: ${C.accent};
font-size: 14px; font-size: 18px;
top: 6px; top: 4px;
font-weight: 700;
} }
.sub-heading { .sub-heading {
font-family: ${F.title}; font-family: 'Noto Serif SC', serif;
font-size: ${T.subtitleSize}px; font-size: 32px;
font-weight: 600; font-weight: 700;
color: ${C.accent}; color: ${C.accent};
margin: 24px 0 12px 0; margin: 32px 0 16px 0;
letter-spacing: 1px; letter-spacing: 1px;
} }
.body strong {
color: ${C.highlight};
font-weight: 700;
}
/* ── 金句布局 ── */ /* ── 金句布局 ── */
.quote-section { .quote-wrapper {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
padding: 0 40px; padding: 0 60px;
} }
.quote-mark { .quote-icon {
font-size: 80px; font-size: 100px;
font-family: ${F.title};
color: ${C.highlight};
opacity: 0.3;
line-height: 1; line-height: 1;
opacity: 0.2;
margin-bottom: 16px; margin-bottom: 16px;
} }
.quote-text { .quote-text {
font-family: ${F.title}; font-family: 'Noto Serif SC', serif;
font-size: 40px; font-size: 52px;
font-weight: 600; font-weight: 700;
line-height: 1.5; line-height: 1.5;
color: ${C.primary}; color: ${C.primary};
letter-spacing: 4px;
}
.quote-author {
margin-top: 40px;
font-size: 24px;
color: ${C.textMuted};
letter-spacing: 2px;
}
/* ── Hero 封面布局 ── */
.hero-wrapper {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.hero-title {
font-family: 'Noto Serif SC', serif;
font-size: 80px;
font-weight: 900;
line-height: 1.2;
color: ${C.primary};
letter-spacing: 3px; letter-spacing: 3px;
} }
.quote-author { .hero-subtitle {
margin-top: 32px; font-size: 30px;
font-size: ${T.smallSize}px;
color: ${C.textMuted}; color: ${C.textMuted};
margin-top: 24px;
letter-spacing: 2px;
opacity: 0.8;
} }
/* ── 极简布局 ── */ .hero-decorator {
.minimal-title { width: 120px;
font-family: ${F.title}; height: 4px;
font-size: 48px; background: linear-gradient(90deg, ${C.accent}, ${C.highlight});
font-weight: 300; margin: 32px 0;
line-height: 1.4; border-radius: 2px;
color: ${C.primary};
letter-spacing: 4px;
text-align: center;
}
.minimal-divider {
width: 60px;
height: 1px;
background: ${C.gold};
margin: 32px auto;
} }
/* ── 底部 ── */ /* ── 底部 ── */
.footer { .footer {
padding: 24px ${STYLES.spacing.paddingX}px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-top: 1px solid ${C.divider}; padding-top: 24px;
font-size: ${T.captionSize}px; font-size: 16px;
color: ${C.textMuted}; color: ${C.textMuted};
opacity: 0.5;
letter-spacing: 1px;
} }
.brand { .footer-dot {
opacity: ${STYLES.brand.opacity}; display: inline-block;
} width: 6px;
height: 6px;
.pagination { background: ${C.accent};
font-size: ${T.captionSize}px; border-radius: 50%;
color: ${C.textMuted}; margin: 0 8px;
opacity: 0.4;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="corner-tl"></div> <div class="orb orb-1"></div>
<div class="corner-br"></div> <div class="orb orb-2"></div>
<div class="container"> <div class="container">
${tag ? `<div class="tag">${tag}</div>` : ''} ${tag ? `<div class="tag">${tag.toUpperCase()}</div>` : ''}
${layout === 'quote' ? ` ${layout === 'quote' ? `
<div class="quote-section"> <div class="quote-wrapper">
<div class="quote-mark">"</div> <div class="quote-icon">"</div>
<div class="quote-text">${quote || title}</div> <div class="quote-text">${title}</div>
${subtitle ? `<div class="quote-author">${subtitle}</div>` : ''} ${subtitle ? `<div class="quote-author">—— ${subtitle}</div>` : ''}
</div> </div>
` : layout === 'minimal' ? ` ` : layout === 'hero' ? `
<div style="flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;"> <div class="hero-wrapper">
<div class="minimal-title">${title}</div> <div class="hero-title">${title}</div>
<div class="minimal-divider"></div> <div class="hero-decorator"></div>
${body ? `<div style="font-size:${T.bodySize}px;color:${C.textMuted};text-align:center;max-width:80%;">${body}</div>` : ''} ${subtitle ? `<div class="hero-subtitle">${subtitle}</div>` : ''}
</div> </div>
` : ` ` : `
<div class="accent-line"></div>
<div class="title">${title}</div> <div class="title">${title}</div>
${subtitle ? `<div class="subtitle">${subtitle}</div>` : ''} ${subtitle ? `<div class="subtitle">${subtitle}</div>` : ''}
<div class="body"> <div class="body">
@ -288,11 +346,11 @@ export function xiaohongshuCard(data) {
</div> </div>
`} `}
</div> <div class="footer">
<span>光湖 · 封面工作室</span>
${totalPages > 1 ? `<span>${pageNum}<span class="footer-dot"></span>${totalPages}</span>` : ''}
</div>
<div class="footer">
<span class="brand">${STYLES.brand.text}</span>
${totalPages > 1 ? `<span class="pagination">${pageNum} / ${totalPages}</span>` : ''}
</div> </div>
</body> </body>
@ -302,9 +360,6 @@ export function xiaohongshuCard(data) {
/** /**
* 生成小红书轮播多页 * 生成小红书轮播多页
* @param {object} data - xiaohongshuCard
* @param {string[]} pages - 每页正文内容数组
* @returns {string[]} HTML 数组
*/ */
export function xiaohongshuCarousel(data, pages) { export function xiaohongshuCarousel(data, pages) {
return pages.map((body, i) => xiaohongshuCard({ return pages.map((body, i) => xiaohongshuCard({