feat(窗口管理): 添加系统托盘功能并优化窗口关闭行为

- 新增 TrayService 实现系统托盘功能
- 主窗口关闭时默认隐藏而非退出
- 添加 window_zoom 配置项支持窗口缩放
- 引入 TrayServiceToken 作为依赖注入标识
- 通过 isQuitting 状态控制应用退出行为
This commit is contained in:
Linkon
2026-01-18 22:06:28 +08:00
parent e9f994b511
commit f57597ca57
7 changed files with 83 additions and 2 deletions
+2
View File
@@ -2,6 +2,8 @@ import { Context as BaseContext } from '../shared/kernel'
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
export class MainContext extends BaseContext { export class MainContext extends BaseContext {
public isQuitting = false
constructor() { constructor() {
super() super()
} }
+2 -1
View File
@@ -13,7 +13,8 @@ export {
EventRepositoryToken, EventRepositoryToken,
SettlementRepositoryToken, SettlementRepositoryToken,
ThemeServiceToken, ThemeServiceToken,
WindowManagerToken WindowManagerToken,
TrayServiceToken
} from './tokens' } from './tokens'
export type { export type {
appRuntimeContext, appRuntimeContext,
+1
View File
@@ -12,3 +12,4 @@ export const EventRepositoryToken = Symbol.for('secscore.eventRepository')
export const SettlementRepositoryToken = Symbol.for('secscore.settlementRepository') export const SettlementRepositoryToken = Symbol.for('secscore.settlementRepository')
export const ThemeServiceToken = Symbol.for('secscore.themeService') export const ThemeServiceToken = Symbol.for('secscore.themeService')
export const WindowManagerToken = Symbol.for('secscore.windowManager') export const WindowManagerToken = Symbol.for('secscore.windowManager')
export const TrayServiceToken = Symbol.for('secscore.trayService')
+11 -1
View File
@@ -14,6 +14,7 @@ import { AuthService } from './services/AuthService'
import { DataService } from './services/DataService' import { DataService } from './services/DataService'
import { ThemeService } from './services/ThemeService' import { ThemeService } from './services/ThemeService'
import { WindowManager, type windowManagerOptions } from './services/WindowManager' import { WindowManager, type windowManagerOptions } from './services/WindowManager'
import { TrayService } from './services/TrayService'
import { StudentRepository } from './repos/StudentRepository' import { StudentRepository } from './repos/StudentRepository'
import { ReasonRepository } from './repos/ReasonRepository' import { ReasonRepository } from './repos/ReasonRepository'
import { EventRepository } from './repos/EventRepository' import { EventRepository } from './repos/EventRepository'
@@ -31,7 +32,8 @@ import {
SettingsStoreToken, SettingsStoreToken,
StudentRepositoryToken, StudentRepositoryToken,
ThemeServiceToken, ThemeServiceToken,
WindowManagerToken WindowManagerToken,
TrayServiceToken
} from './hosting' } from './hosting'
type mainAppConfig = { type mainAppConfig = {
@@ -137,6 +139,10 @@ app.whenReady().then(async () => {
WindowManagerToken, WindowManagerToken,
(p) => new WindowManager(p.get(MainContext), config.window) (p) => new WindowManager(p.get(MainContext), config.window)
) )
services.addSingleton(
TrayServiceToken,
(p) => new TrayService(p.get(MainContext), config.window)
)
}) })
.configure(async (_builderContext, appCtx) => { .configure(async (_builderContext, appCtx) => {
const services = appCtx.services const services = appCtx.services
@@ -156,6 +162,8 @@ app.whenReady().then(async () => {
services.get(SettlementRepositoryToken) services.get(SettlementRepositoryToken)
services.get(ThemeServiceToken) services.get(ThemeServiceToken)
services.get(WindowManagerToken) services.get(WindowManagerToken)
const tray = services.get(TrayServiceToken) as TrayService
tray.initialize()
const logLevelSetting = ctx.settings.getValue('log_level') as logLevel const logLevelSetting = ctx.settings.getValue('log_level') as logLevel
if (logLevelSetting) { if (logLevelSetting) {
@@ -254,12 +262,14 @@ app.whenReady().then(async () => {
}) })
const host = await builder.build() const host = await builder.build()
const ctx = host.services.get(MainContext) as MainContext
await host.start() await host.start()
let disposing = false let disposing = false
const beforeQuitHandler = () => { const beforeQuitHandler = () => {
if (disposing) return if (disposing) return
disposing = true disposing = true
ctx.isQuitting = true
app.removeListener('before-quit', beforeQuitHandler) app.removeListener('before-quit', beforeQuitHandler)
void host.dispose() void host.dispose()
} }
+58
View File
@@ -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: '/' })
}
}
}
+8
View File
@@ -76,6 +76,14 @@ export class WindowManager extends Service {
win.webContents.setZoomFactor(zoom) win.webContents.setZoomFactor(zoom)
this.windows.set(input.key, win) this.windows.set(input.key, win)
win.on('close', (event) => {
if (!this.mainCtx.isQuitting && input.key === 'main') {
event.preventDefault()
win.hide()
}
})
win.on('closed', () => { win.on('closed', () => {
this.windows.delete(input.key) this.windows.delete(input.key)
}) })
+1
View File
@@ -20,6 +20,7 @@ export interface themeConfig {
export type settingsSpec = { export type settingsSpec = {
is_wizard_completed: boolean is_wizard_completed: boolean
log_level: logLevel log_level: logLevel
window_zoom: number
} }
export type settingsKey = keyof settingsSpec export type settingsKey = keyof settingsSpec