init local code
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* 生成后端种子静态资源: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");
|
||||
Reference in New Issue
Block a user