mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 21:14:21 +08:00
feat(窗口管理): 添加系统托盘功能并优化窗口关闭行为
- 新增 TrayService 实现系统托盘功能 - 主窗口关闭时默认隐藏而非退出 - 添加 window_zoom 配置项支持窗口缩放 - 引入 TrayServiceToken 作为依赖注入标识 - 通过 isQuitting 状态控制应用退出行为
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export {
|
|||||||
EventRepositoryToken,
|
EventRepositoryToken,
|
||||||
SettlementRepositoryToken,
|
SettlementRepositoryToken,
|
||||||
ThemeServiceToken,
|
ThemeServiceToken,
|
||||||
WindowManagerToken
|
WindowManagerToken,
|
||||||
|
TrayServiceToken
|
||||||
} from './tokens'
|
} from './tokens'
|
||||||
export type {
|
export type {
|
||||||
appRuntimeContext,
|
appRuntimeContext,
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: '/' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user