MODULE-COVER-001: full package for external AI consumption - server.js + renderer.js + generate.js + config.js - templates/ (5 templates: xiaohongshu + dynamic + jike + poster + registry) - package.json + README.md + MODULE.hdlp - INDEX.hdlp + SYSTEM.hdlp (HLDP declarations) - Fonts: Noto Sans CJK SC priority (no tofu blocks) - No domain watermark - Updated module-registry.json - Copyright 2026-A-00037559
193 lines
4.6 KiB
JavaScript
193 lines
4.6 KiB
JavaScript
/**
|
||
* ═══════════════════════════════════════════════════
|
||
* 万能动态模板 · 自适应任何内容
|
||
* ═══════════════════════════════════════════════════
|
||
*
|
||
* 不是固定类型。是根据冰朔的文字内容,现场设计。
|
||
*/
|
||
|
||
import { STYLES } from '../config.js'
|
||
|
||
|
||
/**
|
||
* 万能动态卡片
|
||
* @param {object} data
|
||
* @param {string} data.title - 提炼的标题
|
||
* @param {string} data.body - 正文
|
||
* @param {string[]} data.lines - 原文行数组
|
||
* @param {string} data.type - 内容类型(analyzeText 的结果)
|
||
*/
|
||
export function dynamicCard(data) {
|
||
const { title, body, lines = [], type = 'general' } = data
|
||
const C = STYLES.colors
|
||
const F = STYLES.fonts
|
||
const T = STYLES.typography
|
||
const W = 1080
|
||
const H = 1080
|
||
|
||
// 动态计算字号:内容越多字越小
|
||
const totalChars = (title + body).length
|
||
const titleSize = totalChars < 20 ? 52 : totalChars < 50 ? 44 : 36
|
||
const bodySize = totalChars < 50 ? 32 : totalChars < 150 ? 26 : 22
|
||
|
||
// 检测有没有列表
|
||
const hasList = lines.some(l => /^[-•·*]\s/.test(l.trim()))
|
||
// 检测是不是一句话
|
||
const isSingleLine = lines.length <= 2 && totalChars < 40
|
||
|
||
/* ── 生成内容 HTML ── */
|
||
let contentHtml = ''
|
||
|
||
if (isSingleLine) {
|
||
/* 一句话 · 大字居中 */
|
||
contentHtml = `
|
||
<div class="single-line">${escapeHtml(title)}</div>
|
||
`
|
||
} else if (hasList) {
|
||
/* 列表内容 */
|
||
const items = lines
|
||
.filter(l => /^[-•·*]\s/.test(l.trim()))
|
||
.map(l => `<li>${escapeHtml(l.replace(/^[-•·*]\s*/, ''))}</li>`)
|
||
.join('\n')
|
||
contentHtml = `
|
||
<div class="title" style="font-size:${titleSize}px">${escapeHtml(title)}</div>
|
||
<ul class="dynamic-list">${items}</ul>
|
||
`
|
||
} else {
|
||
/* 一般段落 */
|
||
const paragraphs = lines
|
||
.filter(Boolean)
|
||
.map(l => `<p>${escapeHtml(l)}</p>`)
|
||
.join('\n')
|
||
contentHtml = `
|
||
<div class="title" style="font-size:${titleSize}px">${escapeHtml(title)}</div>
|
||
<div class="body-text" style="font-size:${bodySize}px">${paragraphs}</div>
|
||
`
|
||
}
|
||
|
||
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;
|
||
position: relative;
|
||
}
|
||
|
||
/* 动态背景装饰 */
|
||
.bg-circle {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
opacity: 0.04;
|
||
}
|
||
.bg-c1 {
|
||
width: 400px; height: 400px;
|
||
background: ${C.accent};
|
||
top: -100px; right: -100px;
|
||
}
|
||
.bg-c2 {
|
||
width: 250px; height: 250px;
|
||
background: ${C.highlight};
|
||
bottom: -60px; left: -60px;
|
||
}
|
||
|
||
.container {
|
||
flex: 1;
|
||
padding: 56px 60px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.single-line {
|
||
font-family: ${F.title};
|
||
font-size: ${titleSize}px;
|
||
font-weight: 600;
|
||
line-height: 1.5;
|
||
color: ${C.primary};
|
||
text-align: center;
|
||
letter-spacing: 3px;
|
||
}
|
||
|
||
.title {
|
||
font-family: ${F.title};
|
||
font-weight: 700;
|
||
line-height: 1.35;
|
||
color: ${C.primary};
|
||
margin-bottom: 24px;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.body-text {
|
||
line-height: 1.7;
|
||
color: ${C.text};
|
||
}
|
||
.body-text p {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.dynamic-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
}
|
||
.dynamic-list li {
|
||
font-size: ${bodySize}px;
|
||
line-height: 1.5;
|
||
color: ${C.text};
|
||
padding: 12px 20px;
|
||
margin-bottom: 8px;
|
||
background: ${C.warm}40;
|
||
border-radius: 8px;
|
||
border-left: 3px solid ${C.accent};
|
||
}
|
||
|
||
/* 底部品牌 */
|
||
.brand-bar {
|
||
position: absolute;
|
||
bottom: 24px;
|
||
left: 0;
|
||
right: 0;
|
||
text-align: center;
|
||
font-size: 14px;
|
||
color: ${C.textMuted};
|
||
opacity: ${STYLES.brand.opacity};
|
||
z-index: 1;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="bg-circle bg-c1"></div>
|
||
<div class="bg-circle bg-c2"></div>
|
||
|
||
<div class="container">
|
||
${contentHtml}
|
||
</div>
|
||
|
||
<div class="brand-bar">${STYLES.brand.text}</div>
|
||
|
||
</body>
|
||
</html>`
|
||
}
|
||
|
||
|
||
function escapeHtml(str) {
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
}
|