371 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ═══════════════════════════════════════════════════
* 小红书卡片模板 · 2026审美版 · 1080×1440 (3:4)
* ═══════════════════════════════════════════════════
*
* 风格:极简大标题 + 高对比度 + 大面积留白 + 渐变色块
* 支持 CSS 变量驱动配色(从 registry 预设传入)
*
* 布局:
* - default: 标题 + 正文列表(教程/干货)
* - quote: 金句居中大字
* - hero: 大标题 + 一句话副标题(封面)
*/
import { STYLES } from '../config.js'
const W = 1080
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
*/
export function xiaohongshuCard(data) {
const {
title = '',
body = '',
subtitle = '',
tag = '',
layout = 'default',
pageNum = 1,
totalPages = 1,
cssVars = {},
} = data
const bodyLines = body.split('\n').filter(Boolean)
// 渲染正文:支持列表、小标题、加粗
const bodyHtml = bodyLines.map(line => {
const trimmed = line.trimStart()
if (trimmed.startsWith('- ') || trimmed.startsWith('• ')) {
const text = trimmed.replace(/^[-•]\s*/, '')
return `<li>${text}</li>`
}
if (trimmed.startsWith('## ')) {
return `<h3 class="sub-heading">${trimmed.slice(3)}</h3>`
}
const formatted = trimmed.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
return `<p>${formatted}</p>`
}).join('\n')
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>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@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 {
width: ${W}px;
height: ${H}px;
overflow: hidden;
background: ${C.bg};
font-family: 'Noto Sans SC', 'PingFang SC', -apple-system, sans-serif;
color: ${C.text};
position: relative;
}
/* ── 装饰 · 几何光斑 ── */
.orb {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.15;
pointer-events: none;
}
.orb-1 {
width: 400px;
height: 400px;
background: ${C.accent};
top: -80px;
left: -120px;
}
.orb-2 {
width: 300px;
height: 300px;
background: ${C.highlight};
bottom: 100px;
right: -100px;
opacity: 0.1;
}
/* ── 主容器 ── */
.container {
position: relative;
z-index: 1;
height: 100%;
display: flex;
flex-direction: column;
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 {
display: inline-block;
padding: 10px 24px;
background: ${C.cardBg};
color: ${C.accent};
font-size: 18px;
font-weight: 700;
border-radius: 12px;
margin-bottom: 28px;
align-self: flex-start;
letter-spacing: 3px;
border: 2px solid ${C.accent}22;
}
/* ── 大标题 · 2026审美核心 ── */
.title {
font-family: 'Noto Serif SC', 'Source Han Serif SC', serif;
font-size: 68px;
font-weight: 900;
line-height: 1.25;
color: ${C.primary};
margin-bottom: 20px;
letter-spacing: 2px;
}
.subtitle {
font-size: 28px;
font-weight: 400;
color: ${C.textMuted};
margin-bottom: 40px;
letter-spacing: 1px;
line-height: 1.5;
opacity: 0.8;
}
/* ── 正文 ── */
.body {
flex: 1;
font-size: 26px;
line-height: 1.8;
color: ${C.text};
}
.body p {
margin-bottom: 14px;
}
.body ul, .body ol {
padding-left: 0;
margin-bottom: 14px;
list-style: none;
}
.body li {
font-size: 26px;
line-height: 1.8;
margin-bottom: 10px;
color: ${C.text};
position: relative;
padding-left: 36px;
}
.body li::before {
content: "▸";
position: absolute;
left: 0;
color: ${C.accent};
font-size: 18px;
top: 4px;
font-weight: 700;
}
.sub-heading {
font-family: 'Noto Serif SC', serif;
font-size: 32px;
font-weight: 700;
color: ${C.accent};
margin: 32px 0 16px 0;
letter-spacing: 1px;
}
.body strong {
color: ${C.highlight};
font-weight: 700;
}
/* ── 金句布局 ── */
.quote-wrapper {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding: 0 60px;
}
.quote-icon {
font-size: 100px;
line-height: 1;
opacity: 0.2;
margin-bottom: 16px;
}
.quote-text {
font-family: 'Noto Serif SC', serif;
font-size: 52px;
font-weight: 700;
line-height: 1.5;
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;
}
.hero-subtitle {
font-size: 30px;
color: ${C.textMuted};
margin-top: 24px;
letter-spacing: 2px;
opacity: 0.8;
}
.hero-decorator {
width: 120px;
height: 4px;
background: linear-gradient(90deg, ${C.accent}, ${C.highlight});
margin: 32px 0;
border-radius: 2px;
}
/* ── 底部 · 子域名水印 ── */
.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 24px;
font-size: 16px;
color: ${C.textMuted};
opacity: 0.5;
letter-spacing: 1px;
}
.footer-domain {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 16px;
color: ${C.accent};
opacity: 0.6;
letter-spacing: 1px;
}
.footer-dot {
display: inline-block;
width: 6px;
height: 6px;
background: ${C.accent};
border-radius: 50%;
margin: 0 8px;
opacity: 0.4;
}
</style>
</head>
<body>
<div class="orb orb-1"></div>
<div class="orb orb-2"></div>
<div class="container">
${tag ? `<div class="tag">${tag}</div>` : ''}
${layout === 'quote' ? `
<div class="quote-wrapper">
<div class="quote-icon">"</div>
<div class="quote-text">${title}</div>
${subtitle ? `<div class="quote-author">—— ${subtitle}</div>` : ''}
</div>
` : layout === 'hero' ? `
<div class="hero-wrapper">
<div class="hero-title">${title}</div>
<div class="hero-decorator"></div>
${subtitle ? `<div class="hero-subtitle">${subtitle}</div>` : ''}
</div>
` : `
<div class="accent-line"></div>
<div class="title">${title}</div>
${subtitle ? `<div class="subtitle">${subtitle}</div>` : ''}
<div class="body">
${bodyHtml}
</div>
`}
<div class="footer">
<span class="footer-domain">cover.guanghulab.com</span>
${totalPages > 1 ? `<span>${pageNum}<span class="footer-dot"></span>${totalPages}</span>` : ''}
</div>
</div>
</body>
</html>`
}
export function xiaohongshuCarousel(data, pages) {
return pages.map((body, i) => xiaohongshuCard({
...data,
body,
pageNum: i + 1,
totalPages: pages.length,
}))
}