Files
YschcmsFront/mock/seed-assets.cjs
T
2026-09-07 09:11:37 +08:00

231 lines
14 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.
/**
* 生成后端种子静态资源:D:/bsch/cms/upload/seed/
* - SVG 场景插画(校园主题分层构图:天空渐变/山峦/建筑剪影/树木/水面;无占位文字)
* - 最小合法 PDF(附件演示可真实打开)
* 轮播 1920×560 对齐主题包设计尺寸约定(themes 下的 theme.json bannerSize)。
* 用法:node seed-assets.cjs
*/
const fs = require("fs");
const path = require("path");
const OUT = "D:/bsch/cms/upload/seed";
fs.mkdirSync(OUT, { recursive: true });
function hashCode(s) {
let h = 0;
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
return Math.abs(h);
}
/** 确定性伪随机(同名资源每次生成完全一致,保证截图可复现) */
function rng(seed) {
let s = seed || 1;
return () => {
s = (s * 1664525 + 1013904223) | 0;
return ((s >>> 8) & 0xffff) / 0xffff;
};
}
/** 一栋建筑剪影(含窗户点阵) */
function building(x, baseY, w, h, fill, winFill, r) {
const cols = Math.max(2, Math.floor(w / 26));
const rows = Math.max(2, Math.floor(h / 30));
let wins = "";
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
if (r() < 0.25) continue; // 部分窗户熄灭,更像真实夜景/黄昏
wins += `<rect x="${(x + 8 + i * ((w - 16) / cols)).toFixed(1)}" y="${(baseY - h + 10 + j * ((h - 18) / rows)).toFixed(1)}" width="${(((w - 16) / cols) - 6).toFixed(1)}" height="${(((h - 18) / rows) - 8).toFixed(1)}" fill="${winFill}"/>`;
}
}
return `<rect x="${x}" y="${baseY - h}" width="${w}" height="${h}" rx="2" fill="${fill}"/>${wins}`;
}
/** 一棵树(圆形树冠 + 矩形树干) */
function tree(x, baseY, scale, leaf, trunk) {
const r = 16 * scale;
return `<rect x="${x - 2.5 * scale}" y="${baseY - 22 * scale}" width="${5 * scale}" height="${22 * scale}" fill="${trunk}"/>
<circle cx="${x}" cy="${baseY - 30 * scale}" r="${r}" fill="${leaf}"/>
<circle cx="${x - 11 * scale}" cy="${baseY - 24 * scale}" r="${r * 0.72}" fill="${leaf}"/>
<circle cx="${x + 11 * scale}" cy="${baseY - 25 * scale}" r="${r * 0.78}" fill="${leaf}"/>`;
}
/** 山峦剪影(两座) */
function mountains(w, baseY, c1, c2, r) {
const x1 = w * (0.05 + r() * 0.2);
const x2 = w * (0.55 + r() * 0.25);
const m1 = `M ${x1} ${baseY} L ${x1 + w * 0.22} ${baseY - 90 - r() * 70} L ${x1 + w * 0.46} ${baseY} Z`;
const m2 = `M ${x2} ${baseY} L ${x2 + w * 0.2} ${baseY - 70 - r() * 60} L ${x2 + w * 0.42} ${baseY} Z`;
return `<path d="${m1}" fill="${c1}"/><path d="${m2}" fill="${c2}"/>`;
}
/** 云(椭圆组) */
function cloud(cx, cy, scale, fill) {
return `<g fill="${fill}">
<ellipse cx="${cx}" cy="${cy}" rx="${38 * scale}" ry="${14 * scale}"/>
<ellipse cx="${cx - 24 * scale}" cy="${cy + 4 * scale}" rx="${26 * scale}" ry="${10 * scale}"/>
<ellipse cx="${cx + 26 * scale}" cy="${cy + 3 * scale}" rx="${28 * scale}" ry="${11 * scale}"/>
</g>`;
}
/** 完整校园场景(黄昏/清晨/白天三种氛围可选) */
function campusScene(name, w, h, palette, opts = {}) {
const seed = hashCode(name);
const r = rng(seed);
const p = {
sky1: palette[0], sky2: palette[1],
sun: palette[2] || "#fff",
far: palette[3] || "rgba(255,255,255,0.28)",
near: palette[4] || "rgba(20,40,70,0.35)",
ground: palette[5] || "#2c4a6e",
water: palette[6] || null,
building: palette[7] || "rgba(16,34,60,0.85)",
win: palette[8] || "rgba(255,224,150,0.9)",
leaf: palette[9] || "rgba(46,94,60,0.9)",
trunk: palette[10] || "rgba(66,48,30,0.9)",
cloud: palette[11] || "rgba(255,255,255,0.5)",
};
const horizon = h * (opts.horizon || 0.72);
let out = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
<defs>
<linearGradient id="sky-${seed}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="${p.sky1}"/><stop offset="1" stop-color="${p.sky2}"/>
</linearGradient>
<linearGradient id="water-${seed}" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="${p.sky2}" stop-opacity="0.9"/><stop offset="1" stop-color="${p.ground}" stop-opacity="0.95"/>
</linearGradient>
</defs>
<rect width="${w}" height="${h}" fill="url(#sky-${seed})"/>`;
// 太阳/月亮
out += `<circle cx="${(w * (0.15 + r() * 0.7)).toFixed(0)}" cy="${(horizon * (0.35 + r() * 0.3)).toFixed(0)}" r="${(h * 0.07).toFixed(0)}" fill="${p.sun}" opacity="0.9"/>`;
// 云
out += cloud(w * (0.12 + r() * 0.25), h * (0.12 + r() * 0.15), 0.9 + r() * 0.5, p.cloud);
out += cloud(w * (0.6 + r() * 0.3), h * (0.08 + r() * 0.12), 0.7 + r() * 0.4, p.cloud);
// 远山
out += mountains(w, horizon, p.far, p.far, r);
// 教学楼主群(画面中偏右)
const groundY = opts.water ? horizon + (h - horizon) * 0.42 : horizon;
const bx = w * (opts.buildingX || 0.42);
out += building(bx, groundY, w * 0.11, (h - groundY) * (1.15 + r() * 0.5), p.building, p.win, r);
out += building(bx + w * 0.115, groundY, w * 0.075, (h - groundY) * (0.85 + r() * 0.3), p.building, p.win, r);
out += building(bx - w * 0.062, groundY, w * 0.058, (h - groundY) * (0.7 + r() * 0.25), p.building, p.win, r);
// 钟楼(细高塔 + 尖顶)
const tx = bx + w * 0.2;
const th = (h - groundY) * (1.5 + r() * 0.3);
out += `<rect x="${tx}" y="${groundY - th}" width="${w * 0.028}" height="${th}" fill="${p.building}"/>
<rect x="${tx + w * 0.004}" y="${groundY - th + 10}" width="${w * 0.02}" height="${w * 0.02}" fill="${p.win}"/>
<path d="M ${tx - w * 0.004} ${groundY - th} L ${tx + w * 0.014} ${groundY - th - h * 0.05} L ${tx + w * 0.032} ${groundY - th} Z" fill="${p.building}"/>`;
// 地面
if (opts.water) {
out += `<rect x="0" y="${horizon}" width="${w}" height="${groundY - horizon}" fill="url(#water-${seed})"/>`;
out += `<rect x="0" y="${groundY}" width="${w}" height="${h - groundY}" fill="${p.ground}"/>`;
// 水面倒影横线
for (let i = 0; i < 5; i++) {
out += `<rect x="${(w * (0.1 + r() * 0.7)).toFixed(0)}" y="${(horizon + (groundY - horizon) * (0.2 + i * 0.16)).toFixed(0)}" width="${(w * (0.06 + r() * 0.2)).toFixed(0)}" height="2" fill="rgba(255,255,255,0.25)"/>`;
}
} else {
out += `<rect x="0" y="${horizon}" width="${w}" height="${h - horizon}" fill="${p.ground}"/>`;
}
// 行道树
const treeY = opts.water ? groundY + (h - groundY) * 0.22 : horizon + (h - horizon) * 0.18;
for (let i = 0; i < 6; i++) {
const tx2 = w * (0.03 + i * 0.165 + r() * 0.04);
if (tx2 > bx - w * 0.08 && tx2 < tx + w * 0.05 && !opts.water) continue; // 避开建筑
out += tree(tx2, treeY, 1 + r() * 0.5, p.leaf, p.trunk);
}
// 前景草坪弧线
out += `<path d="M 0 ${h} Q ${w / 2} ${h - h * 0.08} ${w} ${h} Z" fill="rgba(0,0,0,0.12)"/>`;
out += `</svg>`;
return out;
}
/** 校徽(圆形徽章:书本 + 星 + 环形留白,透明背景适配页头) */
function emblem() {
return `<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<defs><linearGradient id="lg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#2a6fc9"/><stop offset="1" stop-color="#12447f"/>
</linearGradient></defs>
<circle cx="48" cy="48" r="46" fill="url(#lg)"/>
<circle cx="48" cy="48" r="38.5" fill="none" stroke="#f5c542" stroke-width="1.6"/>
<circle cx="48" cy="48" r="41.5" fill="none" stroke="#ffffff" stroke-width="0.8" opacity="0.6"/>
<path d="M 26 58 Q 48 50 70 58 L 70 36 Q 48 28 26 36 Z" fill="#ffffff"/>
<path d="M 48 50 L 48 28" stroke="#2a6fc9" stroke-width="1.6"/>
<path d="M 48 14 L 50.6 21 L 58 21.4 L 52.2 26 L 54.2 33 L 48 29 L 41.8 33 L 43.8 26 L 38 21.4 L 45.4 21 Z" fill="#f5c542"/>
<rect x="24" y="62" width="48" height="3.4" rx="1.7" fill="#f5c542"/>
</svg>`;
}
/* ---- 轮播(1920×560,对齐 theme.json bannerSize 约定)---- */
fs.writeFileSync(path.join(OUT, "banner-1.svg"),
campusScene("banner-1", 1920, 560, ["#1c3f68", "#7fb2e5", "#fff2c9", "rgba(255,255,255,0.22)", "rgba(20,45,80,0.30)", "#27517f"], { water: true, buildingX: 0.40 }));
fs.writeFileSync(path.join(OUT, "banner-2.svg"),
campusScene("banner-2", 1920, 560, ["#0e4f4a", "#8fd0b0", "#fdeec1", "rgba(255,255,255,0.20)", "rgba(10,60,50,0.32)", "#1f6b52", "#2f8a68", "rgba(8,52,44,0.88)", "rgba(255,240,190,0.92)", "rgba(24,92,64,0.95)", "rgba(52,40,24,0.95)", "rgba(255,255,255,0.42)"], { water: true, buildingX: 0.46 }));
fs.writeFileSync(path.join(OUT, "banner-3.svg"),
campusScene("banner-3", 1920, 560, ["#3a1f5d", "#e08a6d", "#ffd9a0", "rgba(255,214,170,0.30)", "rgba(52,24,70,0.38)", "#4a2a63", "#6b3d84", "rgba(30,14,48,0.9)", "rgba(255,226,170,0.95)", "rgba(96,60,110,0.9)", "rgba(50,32,24,0.9)", "rgba(255,235,210,0.4)"], { horizon: 0.78, buildingX: 0.36 }));
fs.writeFileSync(path.join(OUT, "banner-4.svg"),
campusScene("banner-4", 1920, 560, ["#141c3a", "#4f7fd9", "#cfe0ff", "rgba(190,210,255,0.20)", "rgba(12,20,48,0.42)", "#1b2a52"], { horizon: 0.75, buildingX: 0.44 }));
/* ---- 文章封面(640×360)---- */
fs.writeFileSync(path.join(OUT, "cover-运动会.svg"),
campusScene("cover-sports", 640, 360, ["#0d3b66", "#69a8d8", "#fff3c4", "rgba(255,255,255,0.24)", "rgba(10,40,70,0.34)", "#1c5080", "#2e6da4", "rgba(10,34,62,0.9)", "rgba(255,230,160,0.92)", "rgba(30,90,70,0.95)", "rgba(50,38,22,0.95)", "rgba(255,255,255,0.5)"], { water: true }));
fs.writeFileSync(path.join(OUT, "cover-迎新晚会.svg"),
campusScene("cover-gala", 640, 360, ["#2a0e4e", "#c25b8a", "#ffe3a0", "rgba(255,200,220,0.26)", "rgba(40,12,64,0.4)", "#3c1a5c", "#57297a", "rgba(26,8,44,0.92)", "rgba(255,224,170,0.95)", "rgba(120,60,130,0.9)", "rgba(48,30,20,0.9)", "rgba(255,230,240,0.4)"], { horizon: 0.8, buildingX: 0.42 }));
fs.writeFileSync(path.join(OUT, "cover-创新创业.svg"),
campusScene("cover-inno", 640, 360, ["#03363d", "#4fd1c5", "#eafff9", "rgba(220,255,250,0.22)", "rgba(4,50,56,0.36)", "#065a63", "#0b7d85", "rgba(4,42,48,0.9)", "rgba(210,255,245,0.92)", "rgba(10,92,80,0.9)", "rgba(40,40,22,0.9)", "rgba(235,255,252,0.4)"], { water: true }));
/* ---- 正文配图(640×360)---- */
["art-1", "art-2", "art-3", "art-4", "art-5", "art-6"].forEach((n, i) => {
const palettes = [
["#1c3f68", "#7fb2e5", "#fff2c9", "rgba(255,255,255,0.22)", "rgba(20,45,80,0.30)", "#27517f"],
["#5d2019", "#e8a06a", "#fff0d0", "rgba(255,220,190,0.26)", "rgba(80,28,20,0.34)", "#7a3020", null, "rgba(70,24,16,0.9)", "rgba(255,235,200,0.92)", "rgba(160,70,40,0.9)", "rgba(56,36,20,0.9)", "rgba(255,240,220,0.42)"],
["#23306b", "#9fb4f0", "#f4f6ff", "rgba(230,235,255,0.24)", "rgba(24,32,86,0.32)", "#303f8c", "#4653a3", "rgba(18,24,66,0.9)", "rgba(235,240,255,0.92)", "rgba(50,70,140,0.9)", "rgba(44,36,26,0.9)", "rgba(245,248,255,0.45)"],
["#0e4f4a", "#8fd0b0", "#fdeec1", "rgba(255,255,255,0.20)", "rgba(10,60,50,0.32)", "#1f6b52", "#2f8a68", "rgba(8,52,44,0.88)", "rgba(255,240,190,0.92)", "rgba(24,92,64,0.95)", "rgba(52,40,24,0.95)", "rgba(255,255,255,0.42)"],
["#4a2a63", "#d9a0c8", "#ffe9f4", "rgba(255,230,245,0.24)", "rgba(60,32,82,0.34)", "#5c3675", "#754a8e", "rgba(44,22,60,0.9)", "rgba(255,235,245,0.92)", "rgba(110,70,120,0.9)", "rgba(50,32,22,0.9)", "rgba(255,245,250,0.42)"],
["#3d2b1f", "#d9b38c", "#fff6e3", "rgba(255,240,215,0.24)", "rgba(60,42,28,0.36)", "#5a4030", "#71523c", "rgba(52,36,24,0.9)", "rgba(255,240,210,0.92)", "rgba(120,90,50,0.9)", "rgba(46,32,18,0.9)", "rgba(255,250,235,0.42)"],
];
fs.writeFileSync(path.join(OUT, n + ".svg"), campusScene(n, 640, 360, palettes[i], { water: i % 2 === 0 }));
});
/* ---- 校徽 + 友链 logo ---- */
fs.writeFileSync(path.join(OUT, "logo-univ.svg"), emblem());
for (let i = 1; i <= 8; i++) {
const hues = [210, 160, 20, 270, 120, 340, 40, 190];
const hue = hues[i - 1];
fs.writeFileSync(path.join(OUT, `logo-${i}.svg`), `<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<rect x="4" y="4" width="88" height="88" rx="18" fill="hsl(${hue},52%,46%)"/>
<rect x="4" y="4" width="88" height="88" rx="18" fill="none" stroke="rgba(255,255,255,0.35)" stroke-width="2"/>
<circle cx="48" cy="38" r="15" fill="rgba(255,255,255,0.9)"/>
<path d="M 24 74 Q 48 46 72 74 Z" fill="rgba(255,255,255,0.75)"/>
</svg>`);
}
/* ---- 最小 PDF(附件) ---- */
function miniPdf(title) {
const text = `BT /F1 18 Tf 60 700 Td (${title}) Tj ET`;
const content = `1 0 0 1 0 0 cm\n${text}`;
const objs = [
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>",
`<< /Length ${content.length} >>\nstream\n${content}\nendstream`,
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
];
let pdf = "%PDF-1.4\n";
const offsets = [];
objs.forEach((o, i) => {
offsets.push(pdf.length);
pdf += `${i + 1} 0 obj\n${o}\nendobj\n`;
});
const xref = pdf.length;
pdf += `xref\n0 ${objs.length + 1}\n0000000000 65535 f \n`;
offsets.forEach(off => {
pdf += String(off).padStart(10, "0") + " 00000 n \n";
});
pdf += `trailer\n<< /Size ${objs.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF`;
return Buffer.from(pdf, "latin1");
}
fs.writeFileSync(path.join(OUT, "2026校历.pdf"), miniPdf("2026 QiulixiaoLi (seed)"));
fs.writeFileSync(path.join(OUT, "运动会秩序册.pdf"), miniPdf("YunDongHui ZhiXuCe (seed)"));
fs.writeFileSync(path.join(OUT, "迎新晚会节目单.pdf"), miniPdf("YingXin WanHui (seed)"));
console.log("seed assets written to", OUT, ":", fs.readdirSync(OUT).length, "files");