feat(shared,web-admin,desktop-client,build): beUI 动效层与两端深色主题,全项目图标改用设计导出的新图标
动效层:把 beUI 的动效语言移植到两端,令牌放在 packages/shared/src/motion.ts(纯常量,后端也引用得到)。弹簧用数值积分生成 CSS linear(),纯 CSS 即可驱动,不必每组件一个 rAF 循环;两端各一份 composables/motion.ts 负责 DOM 部分,在 app.mount() 之前注入 :root。含 reduced-motion 全局降级与 hover 能力探测;灵动岛只移植单调曲线。
深色主题:Web 管理端补齐,机制与桌面客户端一致;语义色令牌只覆盖颜色,不动圆角与动效令牌。默认跟随系统,用户手动点过之后以用户为准。
图标:build/classhelper.{png,ico} 成为全项目唯一图形源,pnpm icons 从它派生各端图标与界面内品牌标。版本号统一到 1.1.2。
This commit is contained in:
1 parent
82c85b59c1
commit
ce9bed73f4
59 files changed
+2974
-336
No files matched your search
+14
-13
@@ -1,6 +1,11 @@
|
||||
/**
|
||||
* 图标生成启动器:pnpm icons
|
||||
* 复用 desktop-client 的 Electron 运行时渲染 SVG 并导出 PNG/ICO。
|
||||
*
|
||||
* 真正的活儿在 `scripts/icons/render.cjs`(跑在 Electron 里,用 Chromium 的 canvas
|
||||
* 把 `build/classhelper.png` 重采样成各端要的尺寸)。这里只负责把 Electron 拉起来。
|
||||
*
|
||||
* 图形源是**手工放入的设计导出**,不从任何 SVG 渲染 —— 改图标请换
|
||||
* `build/classhelper.{png,ico}` 这两个文件,别改这个脚本。
|
||||
*/
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
@@ -16,6 +21,14 @@ if (!fs.existsSync(electronDir)) {
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
for (const source of ['classhelper.png', 'classhelper.ico']) {
|
||||
const file = path.join(root, 'build', source);
|
||||
if (!fs.existsSync(file)) {
|
||||
console.error(`[icons] 缺少图形源 build/${source}(设计导出,需手工放入)`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
const executable = resolveElectronExecutable(electronDir);
|
||||
|
||||
const child = spawn(executable, [path.join(root, 'scripts', 'icons', 'render.cjs')], {
|
||||
@@ -25,17 +38,5 @@ const child = spawn(executable, [path.join(root, 'scripts', 'icons', 'render.cjs
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (code === 0) {
|
||||
// 托盘图标需要一张 PNG 随 renderer 一起打包(Vite 会把 public/ 复制到 dist/renderer)
|
||||
try {
|
||||
const source = path.join(root, 'packages', 'desktop-client', 'build', 'icon.png');
|
||||
const targetDir = path.join(root, 'packages', 'desktop-client', 'public');
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
fs.copyFileSync(source, path.join(targetDir, 'tray.png'));
|
||||
console.log('[icons] 托盘图标已同步:packages/desktop-client/public/tray.png');
|
||||
} catch (error) {
|
||||
console.warn('[icons] 同步托盘图标失败(不影响构建)', error);
|
||||
}
|
||||
}
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
+193
-114
@@ -1,157 +1,236 @@
|
||||
/**
|
||||
* 用 Electron 渲染 SVG 并导出应用图标(PNG + ICO)。
|
||||
* 应用图标导出器(`pnpm icons`)—— **全项目所有图标都从这里出**。
|
||||
*
|
||||
* 为什么用 Electron:可以复用系统字体渲染中文标识,且无需任何图形库/ImageMagick。
|
||||
* ## 图形源(唯一)
|
||||
*
|
||||
* 产物:
|
||||
* packages/web-admin/public/icon-{192,512}.png PWA 图标
|
||||
* packages/web-admin/public/apple-touch-icon.png iOS 图标
|
||||
* packages/web-admin/public/favicon.svg 浏览器标签页图标
|
||||
* packages/desktop-client/build/icon.png 512×512(electron-builder 用)
|
||||
* packages/desktop-client/build/icon.ico 多尺寸 ICO(安装包/EXE 图标)
|
||||
* build/icon.ico 服务端安装程序图标
|
||||
* 仓库根 `build/` 下那两份是**手工放入的设计导出**,不要用 `pnpm icons` 去覆盖它们:
|
||||
*
|
||||
* build/classhelper.png 345×339 PNG(调色板 + tRNS,带透明)—— 所有 PNG 尺寸由它重采样;
|
||||
* build/classhelper.ico 9 个尺寸(16/24/32/48/64/72/96/128/256,BMP 载荷)—— 由设计工具
|
||||
* **逐尺寸栅格化**,比把位图缩下去干净,因此**原样透传**,不重新生成。
|
||||
*
|
||||
* 历史上这里是用 Electron 渲染一段内联 SVG("蓝色圆角块 + 白色『班』字")生成全套图标;
|
||||
* 换成设计导出的位图之后,那套 SVG 已删除,本脚本只做重采样与分发。
|
||||
*
|
||||
* ## 产物
|
||||
*
|
||||
* packages/web-admin/public/icon-192.png PWA 图标
|
||||
* packages/web-admin/public/icon-512.png PWA 图标(manifest 里也当 maskable 用)
|
||||
* packages/web-admin/public/apple-touch-icon.png iOS 主屏图标(180)
|
||||
* packages/web-admin/public/favicon.png 标签页图标(64)
|
||||
* packages/web-admin/public/favicon.svg 标签页图标(内嵌 128 位图的 SVG 壳)
|
||||
* packages/web-admin/public/logo.png **界面内**的品牌标(侧栏 / 登录页,128)
|
||||
* packages/classisland-plugin/icon.png ClassIsland 插件图标(256)
|
||||
* packages/desktop-client/build/classhelper.{png,ico} 图标源在客户端的镜像
|
||||
* packages/desktop-client/build/icon.{png,ico} electron-builder 的**兜底约定名**
|
||||
* (`win.icon` 显式指向 classhelper.ico,
|
||||
* 这两个是与它同步的副本,别再删)
|
||||
* packages/desktop-client/public/tray.png 系统托盘图标(32)
|
||||
* packages/desktop-client/public/logo.png **界面内**的品牌标(侧栏 / 登录页,128)
|
||||
* website/assets/icon.png 官网(favicon / apple-touch / 页面内 brand)
|
||||
*
|
||||
* ## 为什么还是 Electron
|
||||
*
|
||||
* 要把一张位图重采样到 8 个尺寸(其中 512 那档是**放大**),仓库里没有任何图形库;
|
||||
* 而 Electron 自带 Chromium 的 canvas,重采样质量比 `nativeImage.resize()` 好得多
|
||||
* (后者一步到位、没有逐级折半,345→16 会走样)。为这一个脚本引 sharp/jimp 进
|
||||
* devDependencies 更不划算 —— 这条流水线从有图标那天起就是跑在 Electron 上的。
|
||||
*
|
||||
* ## 三条实现要点
|
||||
*
|
||||
* ① **非正方形源**:345×339 直接 `drawImage` 到正方形画布会拉变形,必须等比缩放后居中、
|
||||
* 四周补透明(补边只有 1.7%,肉眼不可见);
|
||||
* ② **逐级折半**:canvas 的降采样是一次双线性,345 → 16 一步到位会糊成一团,
|
||||
* 先折到目标的两倍以内再出最后一步;
|
||||
* ③ 图片以 `data:` URL 进页面:`data:` 图**不会污染 canvas**,`toDataURL()` 拿得到数据;
|
||||
* 换成 `file://` 就会因跨源被拒(Chromium 里 file:// 页面是 opaque origin)。
|
||||
*/
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..', '..');
|
||||
const SIZES = [16, 24, 32, 48, 64, 128, 180, 192, 256, 512];
|
||||
const BUILD_DIR = path.join(ROOT, 'build');
|
||||
const SOURCE_PNG = path.join(BUILD_DIR, 'classhelper.png');
|
||||
const SOURCE_ICO = path.join(BUILD_DIR, 'classhelper.ico');
|
||||
|
||||
/** 品牌图形:蓝色渐变圆角方块 + 白色"班"字 + 书本底纹 */
|
||||
function logoSvg(size) {
|
||||
const radius = Math.round(size * 0.22);
|
||||
const fontSize = Math.round(size * 0.52);
|
||||
const bookY = Math.round(size * 0.74);
|
||||
const bookWidth = Math.round(size * 0.5);
|
||||
const bookX = Math.round((size - bookWidth) / 2);
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#4facfe"/>
|
||||
<stop offset="55%" stop-color="#409eff"/>
|
||||
<stop offset="100%" stop-color="#1f6feb"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="${size}" height="${size}" rx="${radius}" ry="${radius}" fill="url(#g)"/>
|
||||
<text x="50%" y="52%" text-anchor="middle" dominant-baseline="central"
|
||||
font-family="'Microsoft YaHei','PingFang SC',sans-serif" font-weight="700"
|
||||
font-size="${fontSize}" fill="#ffffff">班</text>
|
||||
<rect x="${bookX}" y="${bookY}" width="${bookWidth}" height="${Math.max(2, Math.round(size * 0.045))}"
|
||||
rx="${Math.round(size * 0.02)}" fill="#ffffff" opacity="0.9"/>
|
||||
</svg>`;
|
||||
/** 需要从源 PNG 重采样出来的全部尺寸(去重后每个尺寸只渲染一次) */
|
||||
const PNG_SIZES = [32, 64, 128, 180, 192, 256, 512];
|
||||
|
||||
/** 客户端产物目录(electron-builder 的 buildResources) */
|
||||
const DESKTOP_BUILD = path.join(ROOT, 'packages', 'desktop-client', 'build');
|
||||
|
||||
function readSourceDataUrl() {
|
||||
if (!fs.existsSync(SOURCE_PNG)) {
|
||||
throw new Error(`缺少图形源 ${path.relative(ROOT, SOURCE_PNG)}(设计导出,需手工放入)`);
|
||||
}
|
||||
return `data:image/png;base64,${fs.readFileSync(SOURCE_PNG).toString('base64')}`;
|
||||
}
|
||||
|
||||
function htmlFor(size) {
|
||||
return `<!doctype html><html><head><meta charset="utf-8" /><style>
|
||||
html,body{margin:0;padding:0;background:transparent;width:${size}px;height:${size}px;overflow:hidden}
|
||||
svg{display:block}
|
||||
</style></head><body>${logoSvg(size)}</body></html>`;
|
||||
/**
|
||||
* 页面侧的重采样脚本。返回一张 `size×size`、内容等比居中、四周透明的 PNG(data URL)。
|
||||
*
|
||||
* 以字符串形式注入页面主世界执行(`executeJavaScript` 默认就在主世界,
|
||||
* 不受 `contextIsolation` 影响),因此这里不能用反引号。
|
||||
*/
|
||||
function resampleScript(size, sourceUrl) {
|
||||
return `(async () => {
|
||||
const SIZE = ${size};
|
||||
const img = new Image();
|
||||
img.src = ${JSON.stringify(sourceUrl)};
|
||||
await img.decode();
|
||||
|
||||
// 等比放进 SIZE×SIZE 的目标框
|
||||
const fit = Math.min(SIZE / img.naturalWidth, SIZE / img.naturalHeight);
|
||||
const boxW = Math.max(1, Math.round(img.naturalWidth * fit));
|
||||
const boxH = Math.max(1, Math.round(img.naturalHeight * fit));
|
||||
|
||||
// 逐级折半:一步到位降采样会走样,先折到目标的两倍以内
|
||||
let current = img;
|
||||
while (Math.round(current.width / 2) >= boxW && Math.round(current.height / 2) >= boxH) {
|
||||
const half = document.createElement('canvas');
|
||||
half.width = Math.max(1, Math.round(current.width / 2));
|
||||
half.height = Math.max(1, Math.round(current.height / 2));
|
||||
const halfCtx = half.getContext('2d');
|
||||
halfCtx.imageSmoothingEnabled = true;
|
||||
halfCtx.imageSmoothingQuality = 'high';
|
||||
halfCtx.drawImage(current, 0, 0, half.width, half.height);
|
||||
current = half;
|
||||
}
|
||||
|
||||
// 最后一次:按 current 的实际比例重算一次落位尺寸,避免折半取整带来的亚像素拉伸
|
||||
const finalFit = Math.min(SIZE / current.width, SIZE / current.height);
|
||||
const drawW = Math.max(1, Math.round(current.width * finalFit));
|
||||
const drawH = Math.max(1, Math.round(current.height * finalFit));
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = SIZE;
|
||||
canvas.height = SIZE;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(current, Math.floor((SIZE - drawW) / 2), Math.floor((SIZE - drawH) / 2), drawW, drawH);
|
||||
return canvas.toDataURL('image/png');
|
||||
})()`;
|
||||
}
|
||||
|
||||
/** 把若干 PNG 打包成 ICO 容器(ICO 允许直接内嵌 PNG 数据) */
|
||||
function buildIco(entries) {
|
||||
const count = entries.length;
|
||||
const header = Buffer.alloc(6);
|
||||
header.writeUInt16LE(0, 0); // reserved
|
||||
header.writeUInt16LE(1, 2); // type: icon
|
||||
header.writeUInt16LE(count, 4);
|
||||
|
||||
const directory = Buffer.alloc(16 * count);
|
||||
let offset = 6 + 16 * count;
|
||||
const payloads = [];
|
||||
|
||||
entries.forEach((entry, index) => {
|
||||
const base = index * 16;
|
||||
directory.writeUInt8(entry.size >= 256 ? 0 : entry.size, base + 0); // width
|
||||
directory.writeUInt8(entry.size >= 256 ? 0 : entry.size, base + 1); // height
|
||||
directory.writeUInt8(0, base + 2); // palette
|
||||
directory.writeUInt8(0, base + 3); // reserved
|
||||
directory.writeUInt16LE(1, base + 4); // color planes
|
||||
directory.writeUInt16LE(32, base + 6); // bits per pixel
|
||||
directory.writeUInt32LE(entry.data.length, base + 8);
|
||||
directory.writeUInt32LE(offset, base + 12);
|
||||
offset += entry.data.length;
|
||||
payloads.push(entry.data);
|
||||
});
|
||||
|
||||
return Buffer.concat([header, directory, ...payloads]);
|
||||
}
|
||||
|
||||
async function renderSizes() {
|
||||
const window = new BrowserWindow({
|
||||
width: 512,
|
||||
height: 512,
|
||||
show: false,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
webPreferences: { offscreen: false, backgroundThrottling: false },
|
||||
});
|
||||
|
||||
const tempDir = path.join(ROOT, '.cache', 'icons');
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
async function renderSizes(window, sourceUrl) {
|
||||
const pngBySize = new Map();
|
||||
|
||||
for (const size of SIZES) {
|
||||
const file = path.join(tempDir, `logo-${size}.html`);
|
||||
fs.writeFileSync(file, htmlFor(size), 'utf8');
|
||||
window.setContentSize(size, size);
|
||||
await window.loadFile(file);
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
|
||||
let buffer = (await window.webContents.capturePage({ x: 0, y: 0, width: size, height: size })).toPNG();
|
||||
|
||||
// 隐藏窗口在部分环境下会截到空图,短暂显示后重试
|
||||
if (!buffer || buffer.length === 0) {
|
||||
window.showInactive();
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
buffer = (await window.webContents.capturePage({ x: 0, y: 0, width: size, height: size })).toPNG();
|
||||
window.hide();
|
||||
}
|
||||
|
||||
if (!buffer || buffer.length === 0) {
|
||||
throw new Error(`渲染 ${size}×${size} 图标失败:截图为空`);
|
||||
for (const size of PNG_SIZES) {
|
||||
const dataUrl = await window.webContents.executeJavaScript(resampleScript(size, sourceUrl));
|
||||
if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image/png;base64,')) {
|
||||
throw new Error(`渲染 ${size}×${size} 失败:页面没有返回 PNG data URL`);
|
||||
}
|
||||
const buffer = Buffer.from(dataUrl.slice('data:image/png;base64,'.length), 'base64');
|
||||
if (buffer.length === 0) throw new Error(`渲染 ${size}×${size} 失败:数据为空`);
|
||||
console.log(`[icons] ${size}×${size} -> ${buffer.length} 字节`);
|
||||
pngBySize.set(size, buffer);
|
||||
}
|
||||
|
||||
window.destroy();
|
||||
return pngBySize;
|
||||
}
|
||||
|
||||
/** ICO 容器头解析,只用来核对设计导出里到底带了哪几个尺寸 */
|
||||
function readIcoSizes(buffer) {
|
||||
if (buffer.length < 6 || buffer.readUInt16LE(0) !== 0 || buffer.readUInt16LE(2) !== 1) {
|
||||
throw new Error(`${path.relative(ROOT, SOURCE_ICO)} 不是合法的 ICO 容器`);
|
||||
}
|
||||
const count = buffer.readUInt16LE(4);
|
||||
const sizes = [];
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const base = 6 + index * 16;
|
||||
sizes.push(`${buffer[base] || 256}×${buffer[base + 1] || 256}`);
|
||||
}
|
||||
return sizes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签页图标的 SVG 壳。
|
||||
*
|
||||
* 设计只给了位图,没有矢量源 —— 所以这里不做"假矢量",而是把一张 128 的 PNG 原样嵌进去。
|
||||
* 保留 `favicon.svg` 这个文件名是为了不动 `index.html` 里那条 `<link rel="icon" type="image/svg+xml">`:
|
||||
* 现代浏览器优先取 SVG 那条,删掉它反而要改 HTML、改文档。
|
||||
* 128 足够覆盖任何真实标签页尺寸(32px @4x = 128 物理像素)。
|
||||
*/
|
||||
function buildFaviconSvg(png128) {
|
||||
const base64 = png128.toString('base64');
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<image href="data:image/png;base64,${base64}" width="128" height="128" />
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
/** 内容一致就不写盘:反复跑 `pnpm icons` 不该在 git 里刷出无意义的改动 */
|
||||
function writeIfChanged(file, data) {
|
||||
if (!data || (Buffer.isBuffer(data) && data.length === 0)) {
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error(`拒绝写入空内容:${path.relative(ROOT, file)}`);
|
||||
}
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const existing = fs.existsSync(file) ? fs.readFileSync(file) : null;
|
||||
if (existing && existing.equals(data)) {
|
||||
console.log(`[icons] 未变化 ${path.relative(ROOT, file)}`);
|
||||
return;
|
||||
}
|
||||
fs.writeFileSync(file, data);
|
||||
console.log(`[icons] 写入 ${path.relative(ROOT, file)} (${data.length} 字节)`);
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
try {
|
||||
const pngBySize = await renderSizes();
|
||||
const sourceUrl = readSourceDataUrl();
|
||||
const icoBytes = fs.readFileSync(SOURCE_ICO);
|
||||
const icoSizes = readIcoSizes(icoBytes);
|
||||
console.log(`[icons] 图形源 ${path.relative(ROOT, SOURCE_PNG)}`);
|
||||
console.log(`[icons] ICO 尺寸 ${icoSizes.join(' / ')}`);
|
||||
|
||||
// Web 管理端(PWA)
|
||||
// 尺寸由页面里的 canvas 决定,窗口本身多大都行 —— 用 64×64 就够,且必须隐藏
|
||||
const window = new BrowserWindow({
|
||||
width: 64,
|
||||
height: 64,
|
||||
show: false,
|
||||
webPreferences: { offscreen: false, backgroundThrottling: false },
|
||||
});
|
||||
await window.loadURL(
|
||||
`data:text/html;charset=utf-8,${encodeURIComponent('<!doctype html><meta charset="utf-8"><title>icons</title>')}`,
|
||||
);
|
||||
|
||||
const pngBySize = await renderSizes(window, sourceUrl);
|
||||
window.destroy();
|
||||
|
||||
// ── Web 管理端(PWA + 标签页) ───────────────────────────────────────────
|
||||
const webPublic = path.join(ROOT, 'packages', 'web-admin', 'public');
|
||||
writeIfChanged(path.join(webPublic, 'icon-192.png'), pngBySize.get(192));
|
||||
writeIfChanged(path.join(webPublic, 'icon-512.png'), pngBySize.get(512));
|
||||
writeIfChanged(path.join(webPublic, 'apple-touch-icon.png'), pngBySize.get(180));
|
||||
writeIfChanged(path.join(webPublic, 'favicon.svg'), Buffer.from(logoSvg(64), 'utf8'));
|
||||
writeIfChanged(path.join(webPublic, 'favicon.png'), pngBySize.get(64));
|
||||
writeIfChanged(path.join(webPublic, 'favicon.svg'), Buffer.from(buildFaviconSvg(pngBySize.get(128)), 'utf8'));
|
||||
// 界面内的品牌标(侧栏 / 登录页)。与 favicon 分开一份的理由:favicon 是给浏览器标签页用的,
|
||||
// 而这张是**渲染进页面**的(`<img src="/logo.png">`),尺寸与缓存策略都不该混在一起。
|
||||
writeIfChanged(path.join(webPublic, 'logo.png'), pngBySize.get(128));
|
||||
|
||||
// 桌面客户端(electron-builder)
|
||||
const desktopBuild = path.join(ROOT, 'packages', 'desktop-client', 'build');
|
||||
writeIfChanged(path.join(desktopBuild, 'icon.png'), pngBySize.get(512));
|
||||
// ── ClassIsland 插件(清单同目录的 icon.png,ClassIsland 按约定读取) ──────
|
||||
writeIfChanged(path.join(ROOT, 'packages', 'classisland-plugin', 'icon.png'), pngBySize.get(256));
|
||||
|
||||
// ICO(安装包 / EXE 图标):包含常用尺寸
|
||||
const icoSizes = [16, 24, 32, 48, 64, 128, 256];
|
||||
const ico = buildIco(icoSizes.map((size) => ({ size, data: pngBySize.get(size) })));
|
||||
writeIfChanged(path.join(desktopBuild, 'icon.ico'), ico);
|
||||
writeIfChanged(path.join(ROOT, 'build', 'icon.ico'), ico);
|
||||
writeIfChanged(path.join(ROOT, 'build', 'icon.png'), pngBySize.get(512));
|
||||
// ── 桌面客户端 ──────────────────────────────────────────────────────────
|
||||
// classhelper.* 是源的镜像;icon.* 是 electron-builder 的兜底约定名(默认找 build/icon.ico),
|
||||
// 两套都留在客户端产物目录里,免得哪天显式配置被拿掉就悄悄退回旧图标。
|
||||
writeIfChanged(path.join(DESKTOP_BUILD, 'classhelper.png'), fs.readFileSync(SOURCE_PNG));
|
||||
writeIfChanged(path.join(DESKTOP_BUILD, 'classhelper.ico'), icoBytes);
|
||||
writeIfChanged(path.join(DESKTOP_BUILD, 'icon.png'), pngBySize.get(512));
|
||||
writeIfChanged(path.join(DESKTOP_BUILD, 'icon.ico'), icoBytes);
|
||||
|
||||
// 托盘:Windows 通知区域在 100% DPI 下就是 16×16,给 32×32 让它在
|
||||
// 150% / 200% DPI 上也有原生像素可用(原先直接丢了一张 345×339 的应用图标进去,
|
||||
// 非正方形会被系统拉伸)。
|
||||
writeIfChanged(path.join(ROOT, 'packages', 'desktop-client', 'public', 'tray.png'), pngBySize.get(32));
|
||||
|
||||
// 界面内的品牌标(侧栏 / 登录页)。
|
||||
// **客户端必须经 `BRAND_LOGO_URL`(src/renderer/config.ts)绑定给 `:src`**:
|
||||
// 生产环境是 file:// 加载 dist/renderer/index.html,根绝对路径会解析到盘符根目录;
|
||||
// 而字面量的相对 `src` 又会被 Vite 当成模块导入去解析、直接构建失败。
|
||||
writeIfChanged(path.join(ROOT, 'packages', 'desktop-client', 'public', 'logo.png'), pngBySize.get(128));
|
||||
|
||||
// ── 官网(纯静态站,图标同样是页面里的 brand 标记) ──────────────────────
|
||||
writeIfChanged(path.join(ROOT, 'website', 'assets', 'icon.png'), pngBySize.get(512));
|
||||
|
||||
console.log('[icons] 全部图标生成完成');
|
||||
app.exit(0);
|
||||
|
||||
Reference in new issue
Block a user