🎨 铸渊图片工作室 - 小红书/即刻/海报配图生成工具
为冰朔定制专属配图生成系统,支持三种卡片类型:
- 小红书竖版卡片 (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