init local code
This commit is contained in:
@@ -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); });
|
||||
Reference in New Issue
Block a user