From f57597ca57f200252dcdaf5a28387867e0c474a0 Mon Sep 17 00:00:00 2001 From: Linkon <116425752+Linkon-lcw@users.noreply.github.com> Date: Sun, 18 Jan 2026 22:06:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=AA=97=E5=8F=A3=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=B3=BB=E7=BB=9F=E6=89=98=E7=9B=98=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E7=AA=97=E5=8F=A3=E5=85=B3?= =?UTF-8?q?=E9=97=AD=E8=A1=8C=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 TrayService 实现系统托盘功能 - 主窗口关闭时默认隐藏而非退出 - 添加 window_zoom 配置项支持窗口缩放 - 引入 TrayServiceToken 作为依赖注入标识 - 通过 isQuitting 状态控制应用退出行为 --- src/main/context.ts | 2 ++ src/main/hosting/index.ts | 3 +- src/main/hosting/tokens.ts | 1 + src/main/index.ts | 12 ++++++- src/main/services/TrayService.ts | 58 ++++++++++++++++++++++++++++++ src/main/services/WindowManager.ts | 8 +++++ src/preload/types.ts | 1 + 7 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/main/services/TrayService.ts diff --git a/src/main/context.ts b/src/main/context.ts index da3b5dc..05ff78f 100644 --- a/src/main/context.ts +++ b/src/main/context.ts @@ -2,6 +2,8 @@ import { Context as BaseContext } from '../shared/kernel' import { ipcMain } from 'electron' export class MainContext extends BaseContext { + public isQuitting = false + constructor() { super() } diff --git a/src/main/hosting/index.ts b/src/main/hosting/index.ts index 3236dc5..cab2910 100644 --- a/src/main/hosting/index.ts +++ b/src/main/hosting/index.ts @@ -13,7 +13,8 @@ export { EventRepositoryToken, SettlementRepositoryToken, ThemeServiceToken, - WindowManagerToken + WindowManagerToken, + TrayServiceToken } from './tokens' export type { appRuntimeContext, diff --git a/src/main/hosting/tokens.ts b/src/main/hosting/tokens.ts index 6321d8b..a28ad91 100644 --- a/src/main/hosting/tokens.ts +++ b/src/main/hosting/tokens.ts @@ -12,3 +12,4 @@ export const EventRepositoryToken = Symbol.for('secscore.eventRepository') export const SettlementRepositoryToken = Symbol.for('secscore.settlementRepository') export const ThemeServiceToken = Symbol.for('secscore.themeService') export const WindowManagerToken = Symbol.for('secscore.windowManager') +export const TrayServiceToken = Symbol.for('secscore.trayService') diff --git a/src/main/index.ts b/src/main/index.ts index e647fdd..f05cb9e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -14,6 +14,7 @@ import { AuthService } from './services/AuthService' import { DataService } from './services/DataService' import { ThemeService } from './services/ThemeService' import { WindowManager, type windowManagerOptions } from './services/WindowManager' +import { TrayService } from './services/TrayService' import { StudentRepository } from './repos/StudentRepository' import { ReasonRepository } from './repos/ReasonRepository' import { EventRepository } from './repos/EventRepository' @@ -31,7 +32,8 @@ import { SettingsStoreToken, StudentRepositoryToken, ThemeServiceToken, - WindowManagerToken + WindowManagerToken, + TrayServiceToken } from './hosting' type mainAppConfig = { @@ -137,6 +139,10 @@ app.whenReady().then(async () => { WindowManagerToken, (p) => new WindowManager(p.get(MainContext), config.window) ) + services.addSingleton( + TrayServiceToken, + (p) => new TrayService(p.get(MainContext), config.window) + ) }) .configure(async (_builderContext, appCtx) => { const services = appCtx.services @@ -156,6 +162,8 @@ app.whenReady().then(async () => { services.get(SettlementRepositoryToken) services.get(ThemeServiceToken) services.get(WindowManagerToken) + const tray = services.get(TrayServiceToken) as TrayService + tray.initialize() const logLevelSetting = ctx.settings.getValue('log_level') as logLevel if (logLevelSetting) { @@ -254,12 +262,14 @@ app.whenReady().then(async () => { }) const host = await builder.build() + const ctx = host.services.get(MainContext) as MainContext await host.start() let disposing = false const beforeQuitHandler = () => { if (disposing) return disposing = true + ctx.isQuitting = true app.removeListener('before-quit', beforeQuitHandler) void host.dispose() } diff --git a/src/main/services/TrayService.ts b/src/main/services/TrayService.ts new file mode 100644 index 0000000..c732695 --- /dev/null +++ b/src/main/services/TrayService.ts @@ -0,0 +1,58 @@ +import { Service } from '../../shared/kernel' +import { MainContext } from '../context' +import { Tray, Menu, app, nativeImage } from 'electron' +import type { windowManagerOptions } from './WindowManager' + +export class TrayService extends Service { + private tray: Tray | null = null + + constructor( + ctx: MainContext, + private readonly opts: windowManagerOptions + ) { + super(ctx, 'tray') + } + + private get mainCtx() { + return this.ctx as MainContext + } + + public initialize() { + const icon = nativeImage.createFromPath(this.opts.icon) + this.tray = new Tray(icon) + + const contextMenu = Menu.buildFromTemplate([ + { + label: '显示主窗口', + click: () => { + this.showMainWindow() + } + }, + { type: 'separator' }, + { + label: '退出 SecScore', + click: () => { + app.quit() + } + } + ]) + + this.tray.setToolTip('SecScore 积分管理') + this.tray.setContextMenu(contextMenu) + + this.tray.on('double-click', () => { + this.showMainWindow() + }) + } + + private showMainWindow() { + const mainWin = this.mainCtx.windows.get('main') + if (mainWin) { + if (mainWin.isMinimized()) mainWin.restore() + mainWin.show() + mainWin.focus() + } else { + this.mainCtx.windows.open({ key: 'main', title: 'SecScore', route: '/' }) + } + } +} diff --git a/src/main/services/WindowManager.ts b/src/main/services/WindowManager.ts index 4e869d7..32ca28c 100644 --- a/src/main/services/WindowManager.ts +++ b/src/main/services/WindowManager.ts @@ -76,6 +76,14 @@ export class WindowManager extends Service { win.webContents.setZoomFactor(zoom) this.windows.set(input.key, win) + + win.on('close', (event) => { + if (!this.mainCtx.isQuitting && input.key === 'main') { + event.preventDefault() + win.hide() + } + }) + win.on('closed', () => { this.windows.delete(input.key) }) diff --git a/src/preload/types.ts b/src/preload/types.ts index a6f7e69..a92958f 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -20,6 +20,7 @@ export interface themeConfig { export type settingsSpec = { is_wizard_completed: boolean log_level: logLevel + window_zoom: number } export type settingsKey = keyof settingsSpec