🎨 铸渊图片工作室 - 小红书/即刻/海报配图生成工具
为冰朔定制专属配图生成系统,支持三种卡片类型:
- 小红书竖版卡片 (1080×1440, 3:4)
- 即刻方卡 (1080×1080, 1:1)
- 海报 (1080×1920, 9:16)
特性:多套配色方案、多种布局风格、
Puppeteer + HTML/CSS 渲染引擎、
可部署 Web 服务 + 预览页面
使用方式:冰朔在对话里告诉铸渊内容和类型,
铸渊运行脚本生成,冰朔直接下载使用。
This commit is contained in:
parent
7d8537baef
commit
c1b3c02ef6
124
image-studio/config.js
Normal file
124
image-studio/config.js
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════
|
||||
* 铸渊图片工作室 · 风格配置系统
|
||||
* ═══════════════════════════════════════════════════
|
||||
*
|
||||
* 冰朔,这里所有的颜色、字体、间距你都可以改。
|
||||
* 告诉我想要的「感觉」,我来调。
|
||||
*/
|
||||
|
||||
export const STYLES = {
|
||||
|
||||
/* ── 当前风格:光湖极简 ── */
|
||||
name: '光湖极简',
|
||||
|
||||
colors: {
|
||||
primary: '#1a1a2e', // 主色 · 深蓝黑
|
||||
secondary: '#16213e', // 辅色 · 深蓝
|
||||
accent: '#0f3460', // 强调色 · 普鲁士蓝
|
||||
highlight: '#e94560', // 高亮 · 珊瑚红
|
||||
gold: '#c9a96e', // 金色 · 点缀
|
||||
warm: '#f5e6c8', // 暖白 · 背景
|
||||
|
||||
bg: '#faf8f5', // 页面背景
|
||||
bgCard: '#ffffff', // 卡片背景
|
||||
text: '#1a1a2e', // 正文
|
||||
textMuted: '#6b7280', // 辅助文字
|
||||
textLight: '#ffffff', // 浅色背景上的文字
|
||||
border: '#e5e7eb', // 边框
|
||||
divider: '#f0e6d3', // 分割线 · 暖色
|
||||
},
|
||||
|
||||
fonts: {
|
||||
title: '"Noto Serif SC", "Source Han Serif SC", serif',
|
||||
body: '"Noto Sans SC", "Source Han Sans SC", "PingFang SC", sans-serif',
|
||||
quote: '"ZCOOL QingKe HuangYou", cursive',
|
||||
mono: '"JetBrains Mono", "Fira Code", monospace',
|
||||
},
|
||||
|
||||
/* 卡片尺寸 */
|
||||
sizes: {
|
||||
xiaohongshu: { width: 1080, height: 1440 }, // 3:4
|
||||
jike: { width: 1080, height: 1080 }, // 1:1
|
||||
poster: { width: 1080, height: 1920 }, // 9:16 海报
|
||||
},
|
||||
|
||||
/* 排版 */
|
||||
typography: {
|
||||
titleSize: 56,
|
||||
subtitleSize: 32,
|
||||
bodySize: 28,
|
||||
smallSize: 22,
|
||||
captionSize: 18,
|
||||
lineHeight: 1.6,
|
||||
},
|
||||
|
||||
/* 间距 */
|
||||
spacing: {
|
||||
paddingX: 64,
|
||||
paddingY: 60,
|
||||
gap: 32,
|
||||
},
|
||||
|
||||
/* 水印/品牌标识 */
|
||||
brand: {
|
||||
show: true,
|
||||
text: '光湖 · 铸渊',
|
||||
size: 16,
|
||||
opacity: 0.4,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
/* ── 预先定义几套配色方案 ── */
|
||||
|
||||
export const COLOR_SCHEMES = {
|
||||
|
||||
/* 光湖极简(默认) */
|
||||
guanghu: {
|
||||
name: '光湖极简',
|
||||
colors: {
|
||||
primary: '#1a1a2e', bg: '#faf8f5', accent: '#0f3460',
|
||||
highlight: '#e94560', gold: '#c9a96e', warm: '#f5e6c8',
|
||||
}
|
||||
},
|
||||
|
||||
/* 奶油暖调 */
|
||||
cream: {
|
||||
name: '奶油暖调',
|
||||
colors: {
|
||||
primary: '#5d4037', bg: '#fef7f0', accent: '#8d6e63',
|
||||
highlight: '#e07a5f', gold: '#d4a373', warm: '#fae1dd',
|
||||
}
|
||||
},
|
||||
|
||||
/* 暗夜深蓝 */
|
||||
night: {
|
||||
name: '暗夜深蓝',
|
||||
colors: {
|
||||
primary: '#e0e0e0', bg: '#0d1117', accent: '#58a6ff',
|
||||
highlight: '#f78166', gold: '#d4a373', warm: '#21262d',
|
||||
}
|
||||
},
|
||||
|
||||
/* 文艺绿植 */
|
||||
green: {
|
||||
name: '文艺绿植',
|
||||
colors: {
|
||||
primary: '#2d3e2f', bg: '#f5f9f2', accent: '#5a8f5a',
|
||||
highlight: '#c78b5c', gold: '#b8a06e', warm: '#e8f0e0',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 切换到指定配色方案
|
||||
*/
|
||||
export function useScheme(name) {
|
||||
const scheme = COLOR_SCHEMES[name]
|
||||
if (!scheme) return false
|
||||
Object.assign(STYLES.colors, scheme.colors)
|
||||
STYLES.name = scheme.name
|
||||
return true
|
||||
}
|
||||
48
image-studio/deploy/setup.sh
Executable file
48
image-studio/deploy/setup.sh
Executable file
@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# ═══════════════════════════════════════════════════
|
||||
# 铸渊图片工作室 · 部署脚本
|
||||
# ═══════════════════════════════════════════════════
|
||||
# 用法: bash deploy/setup.sh
|
||||
# 在目标服务器上运行
|
||||
|
||||
set -e
|
||||
|
||||
echo "🎨 铸渊图片工作室 · 部署开始"
|
||||
|
||||
# 1. 安装系统依赖
|
||||
echo "📦 安装系统依赖..."
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq \
|
||||
ca-certificates fonts-liberation libappindicator3-1 \
|
||||
libasound2 libatk-bridge2.0-0 libatk1.0-0 libcups2 \
|
||||
libdbus-1-3 libgdk-pixbuf2.0-0 libnspr4 libnss3 \
|
||||
libx11-xcb1 libxcomposite1 libxdamage1 libxrandr2 \
|
||||
xdg-utils libgbm1 libxkbcommon0 > /dev/null 2>&1
|
||||
|
||||
echo " ✅ 系统依赖安装完成"
|
||||
|
||||
# 2. 安装 Node 依赖
|
||||
echo "📦 安装 Node 依赖..."
|
||||
cd "$(dirname "$0")/.."
|
||||
npm install --production 2>&1 | tail -1
|
||||
|
||||
# 3. 创建输出目录
|
||||
mkdir -p output
|
||||
|
||||
# 4. 启动服务
|
||||
echo "🚀 启动图片工作室服务..."
|
||||
pm2 delete zhuyuan-image-studio 2>/dev/null || true
|
||||
pm2 start server.js --name zhuyuan-image-studio -- --port 3912
|
||||
pm2 save
|
||||
|
||||
echo ""
|
||||
echo "✅ 部署完成!"
|
||||
echo " 服务端口: 3912"
|
||||
echo " 预览地址: http://<服务器IP>:3912"
|
||||
echo ""
|
||||
echo " Nginx 配置参考:"
|
||||
echo " location /images {"
|
||||
echo " proxy_pass http://127.0.0.1:3912;"
|
||||
echo " proxy_set_header Host \$host;"
|
||||
echo " proxy_set_header X-Real-IP \$remote_addr;"
|
||||
echo " }"
|
||||
198
image-studio/generate.js
Normal file
198
image-studio/generate.js
Normal file
@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════
|
||||
* 铸渊图片工作室 · 主入口
|
||||
* ═══════════════════════════════════════════════════
|
||||
*
|
||||
* 冰朔,你告诉我内容和想要的类型,我帮你生成。
|
||||
*
|
||||
* 用法:
|
||||
* node generate.js xiaohongshu --title "标题" --body "正文"
|
||||
* node generate.js jike --title "观点" --body "内容"
|
||||
* node generate.js poster --title "海报标题" --details "时间:5月1日" "地点:广州"
|
||||
*
|
||||
* 也可以直接在代码里调用 API:
|
||||
* import { generate } from './generate.js'
|
||||
* await generate({ type: 'xiaohongshu', title: '...', body: '...' })
|
||||
*/
|
||||
|
||||
import { renderToImage, renderCarousel } from './renderer.js'
|
||||
import { xiaohongshuCard, xiaohongshuCarousel } from './templates/xiaohongshu.js'
|
||||
import { jikeCard } from './templates/jike.js'
|
||||
import { posterCard } from './templates/poster.js'
|
||||
import { STYLES, useScheme } from './config.js'
|
||||
|
||||
|
||||
/**
|
||||
* 统一生成入口
|
||||
* @param {object} opts
|
||||
* @param {'xiaohongshu'|'jike'|'poster'} opts.type - 卡片类型
|
||||
* @param {string} opts.title - 标题
|
||||
* @param {string} opts.body - 正文
|
||||
* @param {string} opts.subtitle - 副标题
|
||||
* @param {string} opts.tag - 标签
|
||||
* @param {string} opts.quote - 金句(小红书 quote 布局用)
|
||||
* @param {string} opts.emoji - Emoji(即刻用)
|
||||
* @param {string} opts.footer - 底部文字
|
||||
* @param {string[]} opts.details - 详情列表(海报用)
|
||||
* @param {string} opts.cta - 行动号召(海报用)
|
||||
* @param {'default'|'quote'|'list'|'minimal'} opts.layout - 布局
|
||||
* @param {'default'|'notice'|'recruit'} opts.style - 海报风格
|
||||
* @param {string} opts.scheme - 配色方案名
|
||||
* @param {string} opts.output - 输出文件名(不含扩展名)
|
||||
* @param {boolean} opts.carousel - 是否拆为多页轮播
|
||||
* @param {string[]} opts.pages - 多页正文(轮播用)
|
||||
* @returns {Promise<{files: string[], type: string}>}
|
||||
*/
|
||||
export async function generate(opts = {}) {
|
||||
const {
|
||||
type = 'xiaohongshu',
|
||||
title = '',
|
||||
body = '',
|
||||
subtitle = '',
|
||||
tag = '',
|
||||
quote = '',
|
||||
emoji = '',
|
||||
footer = '',
|
||||
details = [],
|
||||
cta = '',
|
||||
layout = 'default',
|
||||
style = 'default',
|
||||
scheme = '',
|
||||
output = '',
|
||||
carousel = false,
|
||||
pages = [],
|
||||
} = opts
|
||||
|
||||
// 切换配色
|
||||
if (scheme) useScheme(scheme)
|
||||
|
||||
const baseName = output || `${type}_${Date.now()}`
|
||||
const size = STYLES.sizes[type] || STYLES.sizes.xiaohongshu
|
||||
|
||||
let files = []
|
||||
|
||||
switch (type) {
|
||||
case 'xiaohongshu': {
|
||||
if (carousel && pages.length > 0) {
|
||||
const htmls = xiaohongshuCarousel({ title, subtitle, tag, quote, layout }, pages)
|
||||
files = await renderCarousel(htmls, { ...size, baseName })
|
||||
} else {
|
||||
const html = xiaohongshuCard({ title, body, subtitle, tag, quote, footer, layout })
|
||||
files = [await renderToImage(html, { ...size, name: baseName })]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'jike': {
|
||||
const html = jikeCard({ title, body, meta: subtitle, emoji, layout })
|
||||
files = [await renderToImage(html, { ...size, name: baseName })]
|
||||
break
|
||||
}
|
||||
|
||||
case 'poster': {
|
||||
const html = posterCard({ title, subtitle, body, details, cta, footer, tag, style })
|
||||
files = [await renderToImage(html, { ...size, name: baseName })]
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`未知卡片类型: ${type}`)
|
||||
}
|
||||
|
||||
return { files, type }
|
||||
}
|
||||
|
||||
|
||||
/* ── CLI 入口 ── */
|
||||
async function cli() {
|
||||
const args = process.argv.slice(2)
|
||||
const type = args.find(a => !a.startsWith('--'))
|
||||
|
||||
if (!type || type === 'help') {
|
||||
console.log(`
|
||||
铸渊图片工作室 · 用法
|
||||
|
||||
node generate.js <类型> [选项]
|
||||
|
||||
类型:
|
||||
xiaohongshu 小红书竖版卡片 (1080×1440)
|
||||
jike 即刻方卡 (1080×1080)
|
||||
poster 海报 (1080×1920)
|
||||
|
||||
选项:
|
||||
--title 标题
|
||||
--body 正文
|
||||
--subtitle 副标题/元信息
|
||||
--tag 标签
|
||||
--quote 金句(小红书 quote 布局)
|
||||
--emoji Emoji(即刻)
|
||||
--details 详情(海报,可多个)
|
||||
--cta 行动号召(海报)
|
||||
--footer 底部文字
|
||||
--layout 布局: default|quote|list|minimal
|
||||
--style 海报风格: default|notice|recruit
|
||||
--scheme 配色: guanghu|cream|night|green
|
||||
--output 输出文件名
|
||||
|
||||
示例:
|
||||
node generate.js xiaohongshu --title "如何高效学习" --body "第一点\\n第二点\\n第三点"
|
||||
node generate.js jike --title "一个思考" --body "今天想到的..." --emoji "💡"
|
||||
node generate.js poster --title "放假通知" --style notice --details "时间:5月1日" "地点:广州"
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
function getArg(key) {
|
||||
const idx = args.indexOf(key)
|
||||
if (idx === -1) return undefined
|
||||
// 收集后续所有非 -- 开头的参数,直到遇到下一个 --
|
||||
const values = []
|
||||
for (let i = idx + 1; i < args.length; i++) {
|
||||
if (args[i].startsWith('--')) break
|
||||
values.push(args[i])
|
||||
}
|
||||
return values.length === 0 ? undefined
|
||||
: values.length === 1 ? values[0]
|
||||
: values
|
||||
}
|
||||
|
||||
const opts = {
|
||||
type,
|
||||
title: getArg('--title') || '',
|
||||
body: (() => {
|
||||
const b = getArg('--body')
|
||||
const text = Array.isArray(b) ? b.join('\n') : (b || '')
|
||||
// 将 \n 转义符转为真实换行
|
||||
return text.replace(/\\n/g, '\n')
|
||||
})(),
|
||||
subtitle: getArg('--subtitle') || '',
|
||||
tag: getArg('--tag') || '',
|
||||
quote: getArg('--quote') || '',
|
||||
emoji: getArg('--emoji') || '',
|
||||
footer: getArg('--footer') || '',
|
||||
details: (() => {
|
||||
const d = getArg('--details')
|
||||
return Array.isArray(d) ? d : (d ? [d] : [])
|
||||
})(),
|
||||
cta: getArg('--cta') || '',
|
||||
layout: getArg('--layout') || 'default',
|
||||
style: getArg('--style') || 'default',
|
||||
scheme: getArg('--scheme') || '',
|
||||
output: getArg('--output') || '',
|
||||
}
|
||||
|
||||
console.log(`🎨 生成${type}卡片...`)
|
||||
const result = await generate(opts)
|
||||
console.log(`✅ 生成完成!`)
|
||||
result.files.forEach(f => console.log(` 📄 ${f}`))
|
||||
}
|
||||
|
||||
// 如果直接运行
|
||||
if (process.argv[1] && (process.argv[1].includes('generate.js'))) {
|
||||
cli().catch(err => {
|
||||
console.error('❌ 生成失败:', err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
BIN
image-studio/output/test_jike.png
Normal file
BIN
image-studio/output/test_jike.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
BIN
image-studio/output/test_poster.png
Normal file
BIN
image-studio/output/test_poster.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
image-studio/output/test_recruit.png
Normal file
BIN
image-studio/output/test_recruit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
BIN
image-studio/output/test_xiaohongshu.png
Normal file
BIN
image-studio/output/test_xiaohongshu.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
1136
image-studio/package-lock.json
generated
Normal file
1136
image-studio/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
image-studio/package.json
Normal file
16
image-studio/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "zhuyuan-image-studio",
|
||||
"version": "1.0.0",
|
||||
"description": "铸渊图片工作室 — 小红书/即刻/海报配图生成工具",
|
||||
"type": "module",
|
||||
"main": "generate.js",
|
||||
"scripts": {
|
||||
"generate": "node generate.js",
|
||||
"server": "node server.js",
|
||||
"deploy": "pm2 start server.js --name zhuyuan-image-studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.0",
|
||||
"puppeteer": "^24.0.0"
|
||||
}
|
||||
}
|
||||
88
image-studio/renderer.js
Normal file
88
image-studio/renderer.js
Normal file
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════
|
||||
* 铸渊图片工作室 · Puppeteer 渲染引擎
|
||||
* ═══════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import puppeteer from 'puppeteer'
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const OUTPUT_DIR = join(__dirname, 'output')
|
||||
|
||||
/**
|
||||
* 渲染 HTML 为图片
|
||||
* @param {string} html - 完整 HTML 字符串
|
||||
* @param {object} opts
|
||||
* @param {number} opts.width - 图片宽度
|
||||
* @param {number} opts.height - 图片高度
|
||||
* @param {string} opts.name - 输出文件名(不含扩展名)
|
||||
* @param {string} opts.format - 输出格式 png/webp/jpeg
|
||||
* @param {number} opts.quality- 图片质量(仅 webp/jpeg)
|
||||
* @returns {Promise<string>} 输出文件路径
|
||||
*/
|
||||
export async function renderToImage(html, opts = {}) {
|
||||
const {
|
||||
width = 1080,
|
||||
height = 1440,
|
||||
name = `card_${Date.now()}`,
|
||||
format = 'png',
|
||||
quality = 90,
|
||||
} = opts
|
||||
|
||||
if (!existsSync(OUTPUT_DIR)) mkdirSync(OUTPUT_DIR, { recursive: true })
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: '/usr/bin/google-chrome',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
||||
})
|
||||
|
||||
try {
|
||||
const page = await browser.newPage()
|
||||
await page.setViewport({ width, height })
|
||||
|
||||
// 注入完整的 HTML
|
||||
await page.setContent(html, { waitUntil: 'networkidle0' })
|
||||
|
||||
// 等待字体加载
|
||||
await page.evaluate(() => document.fonts.ready)
|
||||
|
||||
// 额外等一会儿让渲染稳定
|
||||
await new Promise(r => setTimeout(r, 500))
|
||||
|
||||
const outputPath = join(OUTPUT_DIR, `${name}.${format}`)
|
||||
|
||||
await page.screenshot({
|
||||
path: outputPath,
|
||||
type: format,
|
||||
clip: { x: 0, y: 0, width, height },
|
||||
quality: format !== 'png' ? quality : undefined,
|
||||
})
|
||||
|
||||
return outputPath
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 批量生成多页(轮播图)
|
||||
* @param {string[]} pages - 每页的 HTML
|
||||
* @param {object} opts
|
||||
* @returns {Promise<string[]>} 输出文件路径数组
|
||||
*/
|
||||
export async function renderCarousel(pages, opts = {}) {
|
||||
const results = []
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const name = opts.baseName
|
||||
? `${opts.baseName}_p${i + 1}`
|
||||
: `carousel_${Date.now()}_p${i + 1}`
|
||||
const path = await renderToImage(pages[i], { ...opts, name })
|
||||
results.push(path)
|
||||
}
|
||||
return results
|
||||
}
|
||||
104
image-studio/server.js
Normal file
104
image-studio/server.js
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════
|
||||
* 铸渊图片工作室 · Web 服务
|
||||
* ═══════════════════════════════════════════════════
|
||||
*
|
||||
* 部署到服务器,冰朔可以在浏览器预览已生成的图片。
|
||||
* 我在对话里生成图片后,冰朔可以直接访问链接下载。
|
||||
*/
|
||||
|
||||
import express from 'express'
|
||||
import { generate } from './generate.js'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { readdirSync, existsSync } from 'fs'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const OUTPUT_DIR = join(__dirname, 'output')
|
||||
const PORT = process.env.PORT || 3912
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
|
||||
/* ── 静态文件服务 ── */
|
||||
app.use('/output', express.static(OUTPUT_DIR))
|
||||
|
||||
/* ── 首页 · 预览画廊 ── */
|
||||
app.get('/', (req, res) => {
|
||||
let files = []
|
||||
try {
|
||||
if (existsSync(OUTPUT_DIR)) {
|
||||
files = readdirSync(OUTPUT_DIR)
|
||||
.filter(f => /\.(png|webp|jpg)$/i.test(f))
|
||||
.sort()
|
||||
.reverse()
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const galleryHtml = files.length > 0
|
||||
? files.map(f =>
|
||||
`<a href="/output/${f}" target="_blank"><img src="/output/${f}" loading="lazy"></a>`
|
||||
).join('\n')
|
||||
: '<div class="empty">还没有生成的图片<br>在对话里告诉铸渊你想做什么图</div>'
|
||||
|
||||
res.send(`<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>铸渊图片工作室</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, 'PingFang SC', 'Noto Sans SC', sans-serif;
|
||||
background: #faf8f5; color: #1a1a2e; padding: 40px;
|
||||
max-width: 1000px; margin: 0 auto;
|
||||
}
|
||||
h1 { font-size: 28px; margin-bottom: 4px; }
|
||||
.sub { color: #6b7280; margin-bottom: 24px; font-size: 14px; }
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.gallery a {
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.gallery a:hover { transform: scale(1.02); }
|
||||
.gallery img { width: 100%; display: block; }
|
||||
.empty {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center; padding: 80px 20px;
|
||||
color: #9ca3af; font-size: 16px; line-height: 2;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🎨 铸渊图片工作室</h1>
|
||||
<p class="sub">冰朔 · 你的配图我包了 · 在对话里告诉我内容就行</p>
|
||||
<div class="gallery">${galleryHtml}</div>
|
||||
</body>
|
||||
</html>`)
|
||||
})
|
||||
|
||||
/* ── 生成 API ── */
|
||||
app.post('/api/generate', async (req, res) => {
|
||||
try {
|
||||
const result = await generate(req.body)
|
||||
res.json({
|
||||
success: true,
|
||||
files: result.files,
|
||||
urls: result.files.map(f => `/output/${f.split('/').pop()}`),
|
||||
})
|
||||
} catch (err) {
|
||||
res.status(400).json({ success: false, error: err.message })
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🎨 铸渊图片工作室运行在 http://localhost:${PORT}`)
|
||||
})
|
||||
190
image-studio/templates/jike.js
Normal file
190
image-studio/templates/jike.js
Normal file
@ -0,0 +1,190 @@
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════
|
||||
* 即刻卡片模板 · 1080×1080 (1:1)
|
||||
* ═══════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
import { STYLES } from '../config.js'
|
||||
|
||||
const W = STYLES.sizes.jike.width
|
||||
const H = STYLES.sizes.jike.height
|
||||
|
||||
|
||||
/**
|
||||
* 生成即刻卡片 HTML
|
||||
* @param {object} data
|
||||
* @param {string} data.title - 标题/核心观点
|
||||
* @param {string} data.body - 正文
|
||||
* @param {string} data.meta - 元信息(如「2026-05 · 思考碎片」)
|
||||
* @param {string} data.emoji - 封面 emoji(可选)
|
||||
* @param {'default'|'quote'|'minimal'} data.layout - 布局
|
||||
*/
|
||||
export function jikeCard(data) {
|
||||
const {
|
||||
title = '',
|
||||
body = '',
|
||||
meta = '',
|
||||
emoji = '',
|
||||
layout = 'default',
|
||||
} = data
|
||||
|
||||
const C = STYLES.colors
|
||||
const F = STYLES.fonts
|
||||
const T = STYLES.typography
|
||||
|
||||
const bodyLines = body.split('\n').filter(Boolean)
|
||||
const bodyHtml = bodyLines.map(line => `<p>${line}</p>`).join('\n')
|
||||
|
||||
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&family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
|
||||
|
||||
body {
|
||||
width: ${W}px;
|
||||
height: ${H}px;
|
||||
overflow: hidden;
|
||||
background: ${C.bg};
|
||||
font-family: ${F.body};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 顶部渐变装饰条 */
|
||||
.accent-bar {
|
||||
height: 6px;
|
||||
background: linear-gradient(90deg, ${C.accent}, ${C.highlight}, ${C.gold});
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
padding: 48px 56px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: ${T.captionSize}px;
|
||||
color: ${C.textMuted};
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.emoji-row {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: ${F.title};
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
color: ${C.primary};
|
||||
margin-bottom: 20px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
font-size: 26px;
|
||||
line-height: 1.7;
|
||||
color: ${C.text};
|
||||
}
|
||||
|
||||
.body p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 金句布局 */
|
||||
.quote-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.quote-emoji {
|
||||
font-size: 72px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.quote-line {
|
||||
width: 40px;
|
||||
height: 2px;
|
||||
background: ${C.gold};
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
|
||||
.quote-title {
|
||||
font-family: ${F.title};
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
color: ${C.primary};
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.quote-body {
|
||||
margin-top: 20px;
|
||||
font-size: 24px;
|
||||
color: ${C.textMuted};
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.footer {
|
||||
padding: 20px 56px 28px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${C.divider};
|
||||
font-size: ${T.captionSize}px;
|
||||
color: ${C.textMuted};
|
||||
}
|
||||
|
||||
.brand {
|
||||
opacity: ${STYLES.brand.opacity};
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="accent-bar"></div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
${meta ? `<div class="meta">${meta}</div>` : ''}
|
||||
|
||||
${layout === 'quote' ? `
|
||||
<div class="quote-section">
|
||||
${emoji ? `<div class="quote-emoji">${emoji}</div>` : ''}
|
||||
<div class="quote-line"></div>
|
||||
<div class="quote-title">${title}</div>
|
||||
${body ? `<div class="quote-body">${body}</div>` : ''}
|
||||
</div>
|
||||
` : `
|
||||
${emoji ? `<div class="emoji-row">${emoji}</div>` : ''}
|
||||
<div class="title">${title}</div>
|
||||
<div class="body">${bodyHtml}</div>
|
||||
`}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span class="brand">${STYLES.brand.text}</span>
|
||||
<span>即刻 · Jike</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
316
image-studio/templates/poster.js
Normal file
316
image-studio/templates/poster.js
Normal file
@ -0,0 +1,316 @@
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════
|
||||
* 海报模板 · 1080×1920 (9:16)
|
||||
* ═══════════════════════════════════════════════════
|
||||
*
|
||||
* 适用于:放假通知、收稿海报、活动公告等
|
||||
*/
|
||||
|
||||
import { STYLES } from '../config.js'
|
||||
|
||||
const W = STYLES.sizes.poster.width
|
||||
const H = STYLES.sizes.poster.height
|
||||
|
||||
|
||||
/**
|
||||
* 生成海报 HTML
|
||||
* @param {object} data
|
||||
* @param {string} data.title - 大标题
|
||||
* @param {string} data.subtitle - 副标题(可选)
|
||||
* @param {string} data.body - 正文内容
|
||||
* @param {string[]} data.details - 详情列表(如时间/地点/要求)
|
||||
* @param {string} data.cta - 行动号召(如「欢迎投稿」)
|
||||
* @param {string} data.footer - 底部信息(如主办方/联系方式)
|
||||
* @param {string} data.tag - 角标标签(如「通知」)
|
||||
* @param {'default'|'notice'|'recruit'} data.style - 海报风格
|
||||
*/
|
||||
export function posterCard(data) {
|
||||
const {
|
||||
title = '',
|
||||
subtitle = '',
|
||||
body = '',
|
||||
details = [],
|
||||
cta = '',
|
||||
footer = '',
|
||||
tag = '',
|
||||
style = 'default',
|
||||
} = data
|
||||
|
||||
const C = STYLES.colors
|
||||
const F = STYLES.fonts
|
||||
const T = STYLES.typography
|
||||
|
||||
// 详情列表渲染
|
||||
const detailsHtml = details.map((d, i) => {
|
||||
// 支持 key: value 格式
|
||||
const colonIdx = d.indexOf(':')
|
||||
if (colonIdx > 0 && colonIdx < 20) {
|
||||
const key = d.slice(0, colonIdx)
|
||||
const val = d.slice(colonIdx + 1)
|
||||
return `
|
||||
<div class="detail-item">
|
||||
<span class="detail-key">${key}</span>
|
||||
<span class="detail-colon">:</span>
|
||||
<span class="detail-val">${val}</span>
|
||||
</div>`
|
||||
}
|
||||
return `<div class="detail-item">${d}</div>`
|
||||
}).join('\n')
|
||||
|
||||
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&display=swap');
|
||||
|
||||
body {
|
||||
width: ${W}px;
|
||||
height: ${H}px;
|
||||
overflow: hidden;
|
||||
background: ${C.bg};
|
||||
font-family: ${F.body};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ── 背景装饰 ── */
|
||||
.bg-accent {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, ${C.accent}08 0%, transparent 70%);
|
||||
}
|
||||
.bg-accent-bl {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, ${C.highlight}06 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
padding: 72px 64px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* ── 标签 ── */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 6px 18px;
|
||||
border: 1px solid ${C.accent};
|
||||
color: ${C.accent};
|
||||
font-size: ${T.captionSize}px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 32px;
|
||||
align-self: flex-start;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
/* ── 标题区 ── */
|
||||
.title-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: ${F.title};
|
||||
font-size: 64px;
|
||||
font-weight: 900;
|
||||
line-height: 1.25;
|
||||
color: ${C.primary};
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.title-accent {
|
||||
color: ${C.highlight};
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-family: ${F.title};
|
||||
font-size: ${T.subtitleSize}px;
|
||||
font-weight: 300;
|
||||
color: ${C.textMuted};
|
||||
margin-top: 12px;
|
||||
letter-spacing: 6px;
|
||||
}
|
||||
|
||||
/* ── 分割线 ── */
|
||||
.divider {
|
||||
width: 80px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, ${C.gold}, ${C.highlight});
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
/* ── 正文 ── */
|
||||
.body {
|
||||
font-size: ${T.bodySize}px;
|
||||
line-height: ${T.lineHeight};
|
||||
color: ${C.text};
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.body p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* ── 详情列表 ── */
|
||||
.details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
padding: 32px;
|
||||
background: ${C.warm}30;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid ${C.accent};
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
font-size: ${T.bodySize}px;
|
||||
line-height: 1.5;
|
||||
color: ${C.text};
|
||||
}
|
||||
|
||||
.detail-key {
|
||||
font-weight: 600;
|
||||
color: ${C.accent};
|
||||
}
|
||||
.detail-colon { color: ${C.textMuted}; }
|
||||
.detail-val { color: ${C.text}; }
|
||||
|
||||
/* ── CTA ── */
|
||||
.cta-section {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cta {
|
||||
display: inline-block;
|
||||
padding: 16px 48px;
|
||||
background: linear-gradient(135deg, ${C.accent}, ${C.primary});
|
||||
color: ${C.textLight};
|
||||
font-size: ${T.subtitleSize}px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
/* ── 底部 ── */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid ${C.divider};
|
||||
font-size: ${T.captionSize}px;
|
||||
color: ${C.textMuted};
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.brand-line {
|
||||
opacity: ${STYLES.brand.opacity};
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ── 通知风格:更正式 ── */
|
||||
.notice-title {
|
||||
font-family: ${F.title};
|
||||
font-size: 56px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
letter-spacing: 8px;
|
||||
color: ${C.primary};
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.notice-sub {
|
||||
text-align: center;
|
||||
font-size: ${T.smallSize}px;
|
||||
color: ${C.textMuted};
|
||||
letter-spacing: 4px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
/* ── 收稿风格:更有活力 ── */
|
||||
.recruit-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 24px;
|
||||
background: ${C.highlight};
|
||||
color: white;
|
||||
font-size: ${T.smallSize}px;
|
||||
border-radius: 4px;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 20px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.recruit-title {
|
||||
font-family: ${F.title};
|
||||
font-size: 60px;
|
||||
font-weight: 900;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
color: ${C.primary};
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="bg-accent"></div>
|
||||
<div class="bg-accent-bl"></div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
${style === 'notice' ? `
|
||||
${tag ? `<div class="tag" style="align-self:center;">${tag}</div>` : ''}
|
||||
<div class="notice-title">${title}</div>
|
||||
${subtitle ? `<div class="notice-sub">${subtitle}</div>` : ''}
|
||||
<div class="divider" style="align-self:center;"></div>
|
||||
` : style === 'recruit' ? `
|
||||
<div class="recruit-badge">${tag || '征稿启事'}</div>
|
||||
<div class="recruit-title">${title}</div>
|
||||
${subtitle ? `<div style="text-align:center;font-size:${T.smallSize}px;color:${C.textMuted};letter-spacing:2px;margin-bottom:24px;">${subtitle}</div>` : ''}
|
||||
` : `
|
||||
${tag ? `<div class="tag">${tag}</div>` : ''}
|
||||
<div class="title-section">
|
||||
<div class="title">${title}</div>
|
||||
${subtitle ? `<div class="subtitle">${subtitle}</div>` : ''}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
`}
|
||||
|
||||
${body ? `<div class="body"><p>${body}</p></div>` : ''}
|
||||
|
||||
${details.length > 0 ? `<div class="details">${detailsHtml}</div>` : ''}
|
||||
|
||||
<div style="flex:1;"></div>
|
||||
|
||||
${cta ? `
|
||||
<div class="cta-section">
|
||||
<div class="cta">${cta}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="footer">
|
||||
${footer ? `<div>${footer}</div>` : ''}
|
||||
<div class="brand-line">${STYLES.brand.text}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
316
image-studio/templates/xiaohongshu.js
Normal file
316
image-studio/templates/xiaohongshu.js
Normal file
@ -0,0 +1,316 @@
|
||||
/**
|
||||
* ═══════════════════════════════════════════════════
|
||||
* 小红书卡片模板 · 1080×1440 (3:4)
|
||||
* ═══════════════════════════════════════════════════
|
||||
*
|
||||
* 支持:
|
||||
* - 单页卡片(封面/金句/干货)
|
||||
* - 多页轮播(长内容自动分页)
|
||||
*/
|
||||
|
||||
import { STYLES } from '../config.js'
|
||||
|
||||
const W = STYLES.sizes.xiaohongshu.width
|
||||
const H = STYLES.sizes.xiaohongshu.height
|
||||
|
||||
|
||||
/**
|
||||
* 生成小红书卡片 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) {
|
||||
const {
|
||||
title = '',
|
||||
body = '',
|
||||
subtitle = '',
|
||||
tag = '',
|
||||
quote = '',
|
||||
footer = '',
|
||||
layout = 'default',
|
||||
pageNum = 1,
|
||||
totalPages = 1,
|
||||
} = data
|
||||
|
||||
const C = STYLES.colors
|
||||
const F = STYLES.fonts
|
||||
const T = STYLES.typography
|
||||
|
||||
// 正文按换行分段
|
||||
const bodyLines = body.split('\n').filter(Boolean)
|
||||
|
||||
const bodyHtml = bodyLines.map(line => {
|
||||
// 检测是否以 - 开头(列表项)
|
||||
if (line.trimStart().startsWith('- ')) {
|
||||
return `<li>${line.trimStart().slice(2)}</li>`
|
||||
}
|
||||
// 检测是否以 # 开头(小标题)
|
||||
if (line.trimStart().startsWith('#')) {
|
||||
return `<h3 class="sub-heading">${line.trimStart().replace(/^#+\s*/, '')}</h3>`
|
||||
}
|
||||
return `<p>${line}</p>`
|
||||
}).join('\n')
|
||||
|
||||
const hasList = bodyLines.some(l => l.trimStart().startsWith('- '))
|
||||
|
||||
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&family=Noto+Sans+SC:wght@300;400;500;700&display=swap');
|
||||
|
||||
body {
|
||||
width: ${W}px;
|
||||
height: ${H}px;
|
||||
overflow: hidden;
|
||||
background: ${C.bg};
|
||||
font-family: ${F.body};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ── 装饰角标 ── */
|
||||
.corner-tl, .corner-br {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-color: ${C.accent};
|
||||
opacity: 0.15;
|
||||
}
|
||||
.corner-tl {
|
||||
top: 24px; left: 24px;
|
||||
border-top: 2px solid;
|
||||
border-left: 2px solid;
|
||||
}
|
||||
.corner-br {
|
||||
bottom: 24px; right: 24px;
|
||||
border-bottom: 2px solid;
|
||||
border-right: 2px solid;
|
||||
}
|
||||
|
||||
.container {
|
||||
flex: 1;
|
||||
padding: ${STYLES.spacing.paddingY}px ${STYLES.spacing.paddingX}px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ── 标签 ── */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 8px 20px;
|
||||
background: ${C.accent};
|
||||
color: ${C.textLight};
|
||||
font-size: ${T.captionSize}px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 24px;
|
||||
align-self: flex-start;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
/* ── 标题 ── */
|
||||
.title {
|
||||
font-family: ${F.title};
|
||||
font-size: ${T.titleSize}px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
color: ${C.primary};
|
||||
margin-bottom: ${subtitle ? 16 : 32}px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: ${T.subtitleSize}px;
|
||||
font-weight: 300;
|
||||
color: ${C.textMuted};
|
||||
margin-bottom: 32px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* ── 正文 ── */
|
||||
.body {
|
||||
flex: 1;
|
||||
font-size: ${T.bodySize}px;
|
||||
line-height: ${T.lineHeight};
|
||||
color: ${C.text};
|
||||
}
|
||||
|
||||
.body p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.body ul, .body ol {
|
||||
padding-left: 32px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.body li {
|
||||
font-size: ${T.bodySize}px;
|
||||
line-height: ${T.lineHeight};
|
||||
margin-bottom: 8px;
|
||||
color: ${C.text};
|
||||
list-style: none;
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.body li::before {
|
||||
content: "◆";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: ${C.highlight};
|
||||
font-size: 14px;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
.sub-heading {
|
||||
font-family: ${F.title};
|
||||
font-size: ${T.subtitleSize}px;
|
||||
font-weight: 600;
|
||||
color: ${C.accent};
|
||||
margin: 24px 0 12px 0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* ── 金句布局 ── */
|
||||
.quote-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0 40px;
|
||||
}
|
||||
|
||||
.quote-mark {
|
||||
font-size: 80px;
|
||||
font-family: ${F.title};
|
||||
color: ${C.highlight};
|
||||
opacity: 0.3;
|
||||
line-height: 1;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-family: ${F.title};
|
||||
font-size: 40px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
color: ${C.primary};
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.quote-author {
|
||||
margin-top: 32px;
|
||||
font-size: ${T.smallSize}px;
|
||||
color: ${C.textMuted};
|
||||
}
|
||||
|
||||
/* ── 极简布局 ── */
|
||||
.minimal-title {
|
||||
font-family: ${F.title};
|
||||
font-size: 48px;
|
||||
font-weight: 300;
|
||||
line-height: 1.4;
|
||||
color: ${C.primary};
|
||||
letter-spacing: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.minimal-divider {
|
||||
width: 60px;
|
||||
height: 1px;
|
||||
background: ${C.gold};
|
||||
margin: 32px auto;
|
||||
}
|
||||
|
||||
/* ── 底部 ── */
|
||||
.footer {
|
||||
padding: 24px ${STYLES.spacing.paddingX}px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${C.divider};
|
||||
font-size: ${T.captionSize}px;
|
||||
color: ${C.textMuted};
|
||||
}
|
||||
|
||||
.brand {
|
||||
opacity: ${STYLES.brand.opacity};
|
||||
}
|
||||
|
||||
.pagination {
|
||||
font-size: ${T.captionSize}px;
|
||||
color: ${C.textMuted};
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="corner-tl"></div>
|
||||
<div class="corner-br"></div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
${tag ? `<div class="tag">${tag}</div>` : ''}
|
||||
|
||||
${layout === 'quote' ? `
|
||||
<div class="quote-section">
|
||||
<div class="quote-mark">"</div>
|
||||
<div class="quote-text">${quote || title}</div>
|
||||
${subtitle ? `<div class="quote-author">${subtitle}</div>` : ''}
|
||||
</div>
|
||||
` : layout === 'minimal' ? `
|
||||
<div style="flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;">
|
||||
<div class="minimal-title">${title}</div>
|
||||
<div class="minimal-divider"></div>
|
||||
${body ? `<div style="font-size:${T.bodySize}px;color:${C.textMuted};text-align:center;max-width:80%;">${body}</div>` : ''}
|
||||
</div>
|
||||
` : `
|
||||
<div class="title">${title}</div>
|
||||
${subtitle ? `<div class="subtitle">${subtitle}</div>` : ''}
|
||||
<div class="body">
|
||||
${bodyHtml}
|
||||
</div>
|
||||
`}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span class="brand">${STYLES.brand.text}</span>
|
||||
${totalPages > 1 ? `<span class="pagination">${pageNum} / ${totalPages}</span>` : ''}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成小红书轮播(多页)
|
||||
* @param {object} data - 同 xiaohongshuCard
|
||||
* @param {string[]} pages - 每页正文内容数组
|
||||
* @returns {string[]} HTML 数组
|
||||
*/
|
||||
export function xiaohongshuCarousel(data, pages) {
|
||||
return pages.map((body, i) => xiaohongshuCard({
|
||||
...data,
|
||||
body,
|
||||
pageNum: i + 1,
|
||||
totalPages: pages.length,
|
||||
}))
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user