76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
import fs from 'node:fs';
|
|||
|
|
import path from 'node:path';
|
||
|
|
import { Menu, Tray, app, nativeImage } from 'electron';
|
||
|
|
import { logger } from './logger.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 系统托盘(需求 5):关闭主窗口不退出程序,而是隐藏到托盘后台运行;
|
||
|
|
* 托盘图标可恢复窗口,并提供"退出"入口(退出前统一清理资源)。
|
||
|
|
*/
|
||
|
|
let tray: Tray | null = null;
|
||
|
|
|
||
|
|
export interface TrayHandlers {
|
||
|
|
onShow(): void;
|
||
|
|
onHide(): void;
|
||
|
|
onQuit(): void;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 托盘图标:优先用打包进 dist/renderer 的 tray.png,其次资源目录 */
|
||
|
|
function resolveTrayIcon(): string | null {
|
||
|
|
const candidates = [
|
||
|
|
path.join(__dirname, '../renderer/tray.png'),
|
||
|
|
path.join(process.resourcesPath ?? '', 'tray.png'),
|
||
|
|
];
|
||
|
|
for (const candidate of candidates) {
|
||
|
|
if (candidate && fs.existsSync(candidate)) return candidate;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createTray(handlers: TrayHandlers): Tray | null {
|
||
|
|
if (tray && !tray.isDestroyed()) return tray;
|
||
|
|
|
||
|
|
const iconPath = resolveTrayIcon();
|
||
|
|
const image = iconPath ? nativeImage.createFromPath(iconPath) : nativeImage.createEmpty();
|
||
|
|
tray = new Tray(image);
|
||
|
|
|
||
|
|
tray.setToolTip('班级小助手(后台运行中)');
|
||
|
|
tray.setContextMenu(
|
||
|
|
Menu.buildFromTemplate([
|
||
|
|
{ label: '显示主窗口', click: () => handlers.onShow() },
|
||
|
|
{ label: '隐藏到托盘', click: () => handlers.onHide() },
|
||
|
|
{ type: 'separator' },
|
||
|
|
{ label: '退出班级小助手', click: () => handlers.onQuit() },
|
||
|
|
]),
|
||
|
|
);
|
||
|
|
// 左键单击/双击托盘图标:恢复窗口(用户习惯)
|
||
|
|
tray.on('click', () => handlers.onShow());
|
||
|
|
tray.on('double-click', () => handlers.onShow());
|
||
|
|
|
||
|
|
logger.info(iconPath ? `系统托盘已就绪:${iconPath}` : '系统托盘已就绪(未找到图标文件,使用空图标)');
|
||
|
|
return tray;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function destroyTray(): void {
|
||
|
|
if (!tray) return;
|
||
|
|
try {
|
||
|
|
tray.destroy();
|
||
|
|
} catch {
|
||
|
|
/* 忽略销毁异常 */
|
||
|
|
}
|
||
|
|
tray = null;
|
||
|
|
logger.info('系统托盘已销毁');
|
||
|
|
}
|
||
|
|
|
||
|
|
export function isTrayReady(): boolean {
|
||
|
|
return Boolean(tray && !tray.isDestroyed());
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 供"关于/设置"展示(也可用于冒烟断言) */
|
||
|
|
export function getTrayState(): { ready: boolean; hasIcon: boolean; appPath: string } {
|
||
|
|
return {
|
||
|
|
ready: isTrayReady(),
|
||
|
|
hasIcon: Boolean(resolveTrayIcon()),
|
||
|
|
appPath: app.getPath('exe'),
|
||
|
|
};
|
||
|
|
}
|