init local code

This commit is contained in:
chenxing
2026-09-07 09:11:37 +08:00
commit a3a75c3680
112 changed files with 24837 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
/**
* 门户主题 favicon 兜底资产生成器(4 套皮肤各一份)
* - 输出 templates/themes/<theme>/assets/favicon.svg + favicon.png(128×128)
* - head.html 在 site.config['favicon'] 未配置时输出 url.asset('favicon.png')(见 fragments/head.html)
* 图形语言对齐种子 logo-8 系(圆点+书脊),主色取各主题 theme.json 的 primary 参数默认值;
* 小尺寸下高对比扁平化:无边框、无环形留白。
* 用法:node theme-favicons.cjs (在 front/ 下执行,依赖 ./node_modules/ws 与系统 Edge)
*/
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
const http = require("http");
const WebSocket = require("ws");
const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe";
const ROOT = path.join(__dirname, "..", "..", "service", "src", "main", "resources", "templates", "themes");
const THEMES = [
{ dir: "default", primary: "#1a5cb0" },
{ dir: "classic-red", primary: "#b01a2e" },
{ dir: "blue-white", primary: "#2f6bd8" },
{ dir: "warm-kinder", primary: "#e8722a" },
];
/** #rrggbb -> [r,g,b] */
function rgb(hex) {
return [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));
}
function mix(hex, target, t) {
const c = rgb(hex).map((v, i) => Math.round(v + (target[i] - v) * t));
return "#" + c.map(v => v.toString(16).padStart(2, "0")).join("");
}
const light = hex => mix(hex, [255, 255, 255], 0.18);
const dark = hex => mix(hex, [0, 0, 0], 0.30);
function faviconSvg(primary) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs><linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="${light(primary)}"/><stop offset="1" stop-color="${dark(primary)}"/>
</linearGradient></defs>
<rect width="64" height="64" fill="url(#bg)"/>
<circle cx="32" cy="25" r="10" fill="#ffffff"/>
<path d="M 16 49.3 Q 32 30.7 48 49.3 Z" fill="#ffffff" opacity="0.92"/>
</svg>`;
}
/* ---- CDP 栅格化(Edge headless,128×128 视图内 img 渲染 svg 矢量,天然高清) ---- */
const sleep = ms => new Promise(r => setTimeout(r, ms));
function getJson(port, p) {
return new Promise((resolve, reject) => {
http.get({ host: "127.0.0.1", port, path: p }, res => {
let d = "";
res.on("data", c => d += c);
res.on("end", () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } });
}).on("error", reject);
});
}
async function waitFor(fn, timeout, label) {
const t0 = Date.now();
for (;;) {
try { if (await fn()) return; } catch (e) { /* retry */ }
if (Date.now() - t0 > timeout) throw new Error("timeout: " + label);
await sleep(200);
}
}
async function rasterizeSvg(svgPath, pngPath) {
const PORT = 9400 + Math.floor(Math.random() * 300);
const profile = path.join(require("os").tmpdir(), "fav-" + process.pid + "-" + Math.floor(Math.random() * 1e6));
const htmlPath = path.join(profile, "icon.html");
fs.mkdirSync(profile, { recursive: true });
fs.writeFileSync(htmlPath,
`<html><head><style>html,body{margin:0;padding:0;background:#fff}</style></head>
<body><img src="file:///${svgPath.replace(/\\/g, "/")}" width="128" height="128"></body></html>`);
const proc = spawn(EDGE, [
"--headless=new", "--disable-gpu", "--no-sandbox", "--hide-scrollbars",
`--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`,
"--window-size=128,128", "--no-first-run", "--no-default-browser-check", "about:blank",
], { stdio: "ignore" });
try {
await waitFor(async () => { try { await getJson(PORT, "/json/version"); return true; } catch (e) { return false; } }, 20000, "devtools");
const pages = await getJson(PORT, "/json");
const page = pages.find(p => p.type === "page");
const ws = new WebSocket(page.webSocketDebuggerUrl, { maxPayload: 64 * 1024 * 1024 });
await new Promise(r => ws.on("open", r));
const cdp = {
id: 0,
pending: new Map(),
send(method, params = {}) {
return new Promise((resolve, reject) => {
const id = ++this.id;
this.pending.set(id, { resolve, reject });
ws.send(JSON.stringify({ id, method, params }));
});
},
};
ws.on("message", m => {
const msg = JSON.parse(m);
if (msg.id && cdp.pending.has(msg.id)) {
const { resolve, reject } = cdp.pending.get(msg.id);
cdp.pending.delete(msg.id);
msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result);
}
});
await cdp.send("Page.enable");
await cdp.send("Emulation.setDeviceMetricsOverride", { width: 128, height: 128, deviceScaleFactor: 1, mobile: false });
const loaded = new Promise(r => {
const on = m => { const d = JSON.parse(m); if (d.method === "Page.loadEventFired") { ws.off("message", on); r(); } };
ws.on("message", on);
});
await cdp.send("Page.navigate", { url: "file:///" + htmlPath.replace(/\\/g, "/") });
await loaded;
await sleep(500);
const shot = await cdp.send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
fs.writeFileSync(pngPath, Buffer.from(shot.data, "base64"));
ws.close();
} finally {
proc.kill();
}
}
(async () => {
for (const t of THEMES) {
const dir = path.join(ROOT, t.dir, "assets");
fs.writeFileSync(path.join(dir, "favicon.svg"), faviconSvg(t.primary));
await rasterizeSvg(path.join(dir, "favicon.svg"), path.join(dir, "favicon.png"));
console.log("wrote", t.dir, "favicon.svg + favicon.png");
}
console.log("done");
})().catch(e => { console.error(e); process.exit(1); });