Files
frontend-miniapp/scripts/generate-explain-unit-previews.cjs

174 lines
11 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.
const fs = require('fs')
const path = require('path')
const projectRoot = path.resolve(__dirname, '..')
const outputDir = path.join(projectRoot, 'static', 'explain', 'unit-previews')
// 业务单元来自当前 H5 讲解页实际渲染的一级 outline生成结果需与映射表同步。
const units = [
{ id: '7467940240901013508', hall: '地球厅', name: '第一单元:宜居地球', theme: 'earth' },
{ id: '7467940240901013509', hall: '地球厅', name: '第二单元:地史探源', theme: 'geology' },
{ id: '7467940240901013510', hall: '地球厅', name: '第三单元:华夏山水', theme: 'landscape' },
{ id: '7467940240901013511', hall: '地球厅', name: '第四单元:矿产资源——大地的馈赠', theme: 'mineral' },
{ id: '7467940240901013512', hall: '地球厅', name: '专题区:地心穿越+自然灾害', theme: 'disaster' },
{ id: '7467940240901013530', hall: '家园厅', name: '序厅', theme: 'home' },
{ id: '7467940240901013531', hall: '家园厅', name: '第一单元:天地共鉴', theme: 'home' },
{ id: '7467940240901013532', hall: '家园厅', name: '第二单元:万物共生', theme: 'ecology' },
{ id: '7467940240901013533', hall: '家园厅', name: '第三单元:家园共筑', theme: 'home' },
{ id: '7467940240901013534', hall: '家园厅', name: '尾厅', theme: 'home' },
{ id: '7467940240901013513', hall: '恐龙厅', name: '第一单元:初识恐龙', theme: 'dinosaur' },
{ id: '7467940240901013514', hall: '恐龙厅', name: '第二单元:恐龙解密', theme: 'dinosaur' },
{ id: '7467940240901013515', hall: '恐龙厅', name: '第三单元:恐龙时代', theme: 'dinosaur' },
{ id: '7467940240901013516', hall: '恐龙厅', name: '第四单元:粤赣育龙', theme: 'dinosaur' },
{ id: '7467940240901013517', hall: '恐龙厅', name: '第五单元:终结与新生', theme: 'dinosaur' },
{ id: '2060939176503275521', hall: '人类厅', name: '第一单元:什么是灵长类', theme: 'human' },
{ id: '2060939176637493249', hall: '人类厅', name: '第二单元:猿与猴', theme: 'human' },
{ id: '2060939176767516674', hall: '人类厅', name: '第三单元:丛林远足', theme: 'forest' },
{ id: '2060939176897540098', hall: '人类厅', name: '第四单元:与巨兽同行', theme: 'human' },
{ id: '2060939177027563521', hall: '人类厅', name: '第五单元:从燧石到硅基', theme: 'human' },
{ id: '7467940240901013523', hall: '生态厅', name: '前言', theme: 'ecology' },
{ id: '7467940240901013524', hall: '生态厅', name: '第二单元:各得其所——世界动物地理区系', theme: 'ecology' },
{ id: '7467940240901013525', hall: '生态厅', name: '第三单元:地球之窗——全球主要生态系统景观', theme: 'ecology' },
{ id: '7467940240901013526', hall: '生物厅', name: '序篇:生物的命名与分类', theme: 'biology' },
{ id: '7467940240901013527', hall: '生物厅', name: '第二单元:中国南方保护动物', theme: 'biology' },
{ id: '7467940240901013528', hall: '生物厅', name: '第三单元:生物基础知识', theme: 'biology' },
{ id: '7467940240901013529', hall: '生物厅', name: '第四单元:生物大观', theme: 'biology' },
{ id: '7467940240901013518', hall: '演化厅', name: '第零单元:基于博物学的进化思想', theme: 'evolution' },
{ id: '7467940240901013519', hall: '演化厅', name: '第一单元:经典进化学说——影响世界的理论', theme: 'evolution' },
{ id: '7467940240901013520', hall: '演化厅', name: '第二单元:遗传与变异——演化的内在驱动力', theme: 'evolution' },
{ id: '7467940240901013521', hall: '演化厅', name: '第三单元:演化方向的选择——适应', theme: 'evolution' },
{ id: '7467940240901013522', hall: '演化厅', name: '第四单元:三十八亿年的创造——从单细胞到智慧生命', theme: 'evolution' },
{ id: '7467940240901013505', hall: '宇宙厅', name: '第一单元:仰望苍穹——人类对宇宙认识的历程', theme: 'cosmos' },
{ id: '7467940240901013506', hall: '宇宙厅', name: '第二单元:拾级而观——等级宇宙结构', theme: 'cosmos' },
{ id: '7467940240901013507', hall: '宇宙厅', name: '第三单元:采石知天——行星科学与深空探测', theme: 'meteorite' }
]
const themes = {
earth: ['#d7e9d2', '#fbf6d8', '#2f6f5f', '#8eac50'],
geology: ['#ebe1d3', '#bfd5dc', '#674c3b', '#c08c45'],
landscape: ['#dcefd7', '#f4efd0', '#265d4e', '#92b65a'],
mineral: ['#e8e1d4', '#f1f4df', '#4b4f5a', '#c2a95b'],
disaster: ['#eee0d1', '#d7e3e0', '#5b3d35', '#d69d45'],
home: ['#edf2df', '#f5f2d7', '#334c45', '#b2bf47'],
ecology: ['#dbeedb', '#f4f2d1', '#2a5a49', '#87a946'],
dinosaur: ['#e7e0cd', '#dce8d5', '#4d4932', '#9a7844'],
human: ['#e8e0d7', '#eef0d6', '#4b3f36', '#b8884b'],
forest: ['#d9eddc', '#eff3d2', '#2f5f43', '#76a159'],
biology: ['#dff0df', '#eef4d4', '#2f604b', '#7bb36c'],
evolution: ['#e6e6d8', '#dbe9df', '#3f5147', '#9ea34a'],
cosmos: ['#dce5ef', '#f2f1d4', '#26354e', '#7b8bb7'],
meteorite: ['#dedfe5', '#f4efd5', '#343746', '#b19b56'],
fallback: ['#e8eadf', '#f7f5df', '#3f4d43', '#a8b64c']
}
const escapeXml = (value) => String(value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
const shortTitle = (name) => name
.replace(/^第[一二三四五六七八九十零]+单元[:]?/, '')
.replace(/^专题区[:]?/, '')
.replace(/^序篇[:]?/, '')
.replace(/[——+].*$/, '')
.trim()
const motif = (theme, accent, dark) => {
if (theme === 'cosmos') {
return `
<circle cx="222" cy="58" r="24" fill="${accent}" opacity=".55"/>
<circle cx="278" cy="92" r="8" fill="${dark}" opacity=".8"/>
<path d="M42 132 C100 86 172 82 322 42" fill="none" stroke="${dark}" stroke-width="7" stroke-linecap="round" opacity=".28"/>
<path d="M222 58 C242 77 265 88 294 91" fill="none" stroke="${dark}" stroke-width="3" opacity=".65"/>
`
}
if (theme === 'meteorite' || theme === 'mineral' || theme === 'geology') {
return `
<path d="M240 34 L330 78 L298 160 L208 150 L178 78 Z" fill="${accent}" opacity=".72"/>
<path d="M238 52 L292 82 L278 130 L220 126 L204 84 Z" fill="none" stroke="${dark}" stroke-width="5" opacity=".55"/>
<path d="M36 146 C86 122 132 120 190 146 S284 174 348 134" fill="none" stroke="${dark}" stroke-width="8" opacity=".18"/>
`
}
if (theme === 'dinosaur') {
return `
<path d="M206 119 C220 74 258 48 308 60 C327 65 340 80 344 98 C318 88 292 94 276 116 C256 143 228 151 196 143 Z" fill="${accent}" opacity=".78"/>
<circle cx="312" cy="84" r="5" fill="${dark}" opacity=".85"/>
<path d="M198 142 C160 142 128 132 94 106" fill="none" stroke="${dark}" stroke-width="12" stroke-linecap="round" opacity=".36"/>
<path d="M92 106 C70 96 56 76 52 56" fill="none" stroke="${dark}" stroke-width="7" stroke-linecap="round" opacity=".3"/>
`
}
if (theme === 'human') {
return `
<circle cx="270" cy="72" r="28" fill="${accent}" opacity=".78"/>
<path d="M228 150 C232 108 246 93 270 93 C294 93 309 110 314 150" fill="none" stroke="${dark}" stroke-width="12" stroke-linecap="round" opacity=".36"/>
<path d="M58 142 C92 98 132 78 180 80" fill="none" stroke="${dark}" stroke-width="8" stroke-linecap="round" opacity=".22"/>
`
}
if (theme === 'biology' || theme === 'ecology' || theme === 'forest') {
return `
<path d="M248 142 C306 124 336 80 328 36 C284 42 244 78 230 130 Z" fill="${accent}" opacity=".72"/>
<path d="M230 132 C270 108 294 78 326 38" fill="none" stroke="${dark}" stroke-width="5" opacity=".45"/>
<path d="M70 154 C110 104 154 82 210 76" fill="none" stroke="${dark}" stroke-width="8" stroke-linecap="round" opacity=".18"/>
<circle cx="90" cy="70" r="18" fill="${accent}" opacity=".35"/>
`
}
if (theme === 'evolution') {
return `
<path d="M64 142 C94 94 124 76 160 98 C190 116 198 70 230 58 C264 44 300 68 326 118" fill="none" stroke="${dark}" stroke-width="9" stroke-linecap="round" opacity=".28"/>
<circle cx="84" cy="128" r="13" fill="${accent}" opacity=".8"/>
<circle cx="160" cy="98" r="15" fill="${accent}" opacity=".68"/>
<circle cx="232" cy="58" r="17" fill="${accent}" opacity=".58"/>
<circle cx="322" cy="116" r="20" fill="${accent}" opacity=".76"/>
`
}
return `
<path d="M64 154 C108 114 156 96 208 98 C266 101 300 76 340 42" fill="none" stroke="${dark}" stroke-width="9" stroke-linecap="round" opacity=".22"/>
<circle cx="272" cy="74" r="34" fill="${accent}" opacity=".65"/>
<path d="M246 120 C286 109 316 84 334 48" fill="none" stroke="${dark}" stroke-width="5" opacity=".4"/>
`
}
const svgFor = ({ id, hall, name, theme }) => {
const [start, end, dark, accent] = themes[theme] || themes.fallback
const title = shortTitle(name) || name
return `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 640 360" role="img" aria-label="${escapeXml(hall)} ${escapeXml(title)}">
<defs>
<linearGradient id="bg-${id}" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="${start}"/>
<stop offset="1" stop-color="${end}"/>
</linearGradient>
<pattern id="dots-${id}" width="34" height="34" patternUnits="userSpaceOnUse">
<circle cx="4" cy="4" r="2.4" fill="${dark}" opacity=".13"/>
</pattern>
</defs>
<rect width="640" height="360" rx="28" fill="url(#bg-${id})"/>
<rect width="640" height="360" fill="url(#dots-${id})" opacity=".75"/>
<path d="M0 274 C108 226 190 248 288 220 C410 184 518 204 640 146 L640 360 L0 360 Z" fill="#fff" opacity=".36"/>
<g transform="translate(232 56)">
${motif(theme, accent, dark)}
</g>
<rect x="38" y="42" width="126" height="36" rx="18" fill="#ffffff" opacity=".72"/>
<text x="101" y="66" text-anchor="middle" font-size="20" font-weight="700" fill="${dark}" font-family="HarmonyOS Sans SC, Microsoft YaHei, sans-serif">${escapeXml(hall)}</text>
<text x="40" y="166" font-size="30" font-weight="800" fill="${dark}" font-family="HarmonyOS Sans SC, Microsoft YaHei, sans-serif">${escapeXml(title.slice(0, 11))}</text>
<text x="40" y="207" font-size="20" font-weight="600" fill="${dark}" opacity=".72" font-family="HarmonyOS Sans SC, Microsoft YaHei, sans-serif">自然导览 · 图文讲解</text>
<path d="M40 252 L156 252" stroke="${accent}" stroke-width="9" stroke-linecap="round"/>
<path d="M40 276 L108 276" stroke="${dark}" stroke-width="5" stroke-linecap="round" opacity=".32"/>
</svg>
`
}
fs.mkdirSync(outputDir, { recursive: true })
units.forEach((unit) => {
fs.writeFileSync(path.join(outputDir, `${unit.id}.svg`), svgFor(unit), 'utf8')
})
fs.writeFileSync(path.join(outputDir, 'default.svg'), svgFor({
id: 'default',
hall: '讲解',
name: '自然博物馆导览',
theme: 'fallback'
}), 'utf8')
console.log(`Generated ${units.length + 1} explain unit preview SVG files in ${path.relative(projectRoot, outputDir)}`)