import { electronApp, optimizer } from '@electron-toolkit/utils' import { app, BrowserWindow, dialog, ipcMain, IpcMainInvokeEvent, Menu } from 'electron' import type { SessionState } from '../shared/types/session' import { getAppConfig, getConfigFilePath, initAppConfig, mergeAppConfig } from './app-config' import { fileLogger, initFileLogger, writeFileLog, type FileLogLevel } from './file-logger' import type { JsonValue } from '../shared/types/app-config' import { createLoginWindow, createMainWindow, createTicketWindow } from './window' type CallStatus = 'idle' | 'calling' | 'paused' | 'working' | 'evaluating' | 'transferring' let sessionState: SessionState = { empUid: null, winUid: null, queueToken: null, } let mainCallStatus: CallStatus = 'idle' let loginWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(() => { initAppConfig() initFileLogger() fileLogger.info(`Loaded app config from ${getConfigFilePath()}`) fileLogger.info('Application started') ipcMain.handle('appConfig:get', () => getAppConfig()) ipcMain.handle('appConfig:set', (_event: IpcMainInvokeEvent, partial: unknown) => { if (!partial || typeof partial !== 'object' || Array.isArray(partial)) { throw new Error('Invalid config: expected a plain object') } mergeAppConfig(partial as Record) return true }) const validLogLevels: FileLogLevel[] = ['debug', 'info', 'warn', 'error'] ipcMain.handle('app:log', (_event: IpcMainInvokeEvent, level: unknown, message: unknown) => { if (typeof level !== 'string' || !validLogLevels.includes(level as FileLogLevel)) { return } if (typeof message !== 'string' || message.length > 32_000) { return } writeFileLog(level as FileLogLevel, message) }) // Set app user model id for windows electronApp.setAppUserModelId('com.electron') loginWindow = createLoginWindow() // Default open or close DevTools by F12 in development // and ignore CommandOrControl + R in production. // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils app.on('browser-window-created', (_, window) => { optimizer.watchWindowShortcuts(window) }) // IPC test ipcMain.on('login-success', () => { // 创建主窗口 mainWindow = createMainWindow() // 关闭登录窗口 if (loginWindow) { loginWindow.close() loginWindow = null } }) // 窗口控制的IPC监听器 ipcMain.on('window-minimize', event => { const window = BrowserWindow.fromWebContents(event.sender) if (window) window.minimize() }) ipcMain.on('window-close', event => { const window = BrowserWindow.fromWebContents(event.sender) if (window) window.close() }) // TicketList -> Main.vue 触发:用于“在票号列表窗口点呼叫/评价后,把主界面对应按钮逻辑执行起来” ipcMain.on('ticket:main-action', (_event, action: unknown, payload: unknown) => { if (!mainWindow) return if (action !== 'call' && action !== 'evaluate') return try { mainWindow.show() mainWindow.focus() mainWindow.webContents.send('main:ticket-action', action, payload) } catch { // ignore } }) // 应用菜单IPC监听器 ipcMain.on('show-context-menu', event => { console.log('main中:show-context-menu') const template = [ { label: '办税员窗口', click: () => { event.sender.copy() // 执行复制 }, }, { label: '票号列表', click: () => { if (mainWindow) { createTicketWindow(mainWindow) } }, }, { type: 'separator' }, { label: '退出程序', click: () => { app.quit() }, }, ] const menu = Menu.buildFromTemplate(template as Electron.MenuItemConstructorOptions[]) menu.popup() }) // 暂停菜单IPC监听器 ipcMain.on('show-pause-menu', event => { console.log('main中:show-pause-menu') const template = [ { label: '午休', click: () => { event.sender.send('pause-reason-selected', '午休') }, }, { label: '休息一下', click: () => { event.sender.send('pause-reason-selected', '休息一下') }, }, { label: '整理资料', click: () => { event.sender.send('pause-reason-selected', '整理资料') }, }, { label: '其他', click: () => { event.sender.send('pause-reason-selected', '其他') }, }, ] const menu = Menu.buildFromTemplate(template as Electron.MenuItemConstructorOptions[]) menu.popup() }) app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) loginWindow = createLoginWindow() }) }) // ✅ 安全的 IPC 接口:获取整个会话状态 ipcMain.handle('session:get', () => { return sessionState }) ipcMain.handle('mainCallStatus:get', () => { return mainCallStatus }) ipcMain.handle('mainCallStatus:set', (_event: IpcMainInvokeEvent, status: unknown) => { if ( status !== 'idle' && status !== 'calling' && status !== 'paused' && status !== 'working' && status !== 'evaluating' && status !== 'transferring' ) { throw new Error('Invalid main call status') } mainCallStatus = status return true }) // ✅ 安全的 IPC 接口:设置 access token(专用,不暴露通用 setter) ipcMain.handle( 'session:setAccessToken', (_event: IpcMainInvokeEvent, newSessionState: SessionState) => { if ( typeof newSessionState.queueToken !== 'string' || newSessionState.queueToken.length > 2048 ) { throw new Error('Invalid token') } sessionState.queueToken = newSessionState.queueToken sessionState.empUid = newSessionState.empUid sessionState.winUid = newSessionState.winUid fileLogger.info('[Session] Token updated') return true }, ) // ✅ 清除会话(登出) ipcMain.handle('session:clear', () => { sessionState = { empUid: null, winUid: null, queueToken: null } fileLogger.info('[Session] Cleared') return true }) /** 主进程原生确认框(dialog.showMessageBox),返回是否点击第一按钮(如「是」) */ ipcMain.handle( 'dialog:confirm', async (event: IpcMainInvokeEvent, payload: unknown): Promise => { if (!payload || typeof payload !== 'object' || Array.isArray(payload)) { throw new Error('Invalid dialog payload') } const p = payload as Record const title = typeof p.title === 'string' ? p.title.slice(0, 256) : '提示' const message = typeof p.message === 'string' ? p.message.slice(0, 4096) : '' const okLabel = typeof p.okLabel === 'string' ? p.okLabel.slice(0, 64) : '是' const cancelLabel = typeof p.cancelLabel === 'string' ? p.cancelLabel.slice(0, 64) : '否' const parent = BrowserWindow.fromWebContents(event.sender) const options = { type: 'warning' as const, title, message, buttons: [okLabel, cancelLabel], defaultId: 1, cancelId: 1, noLink: true, } const { response } = parent ? await dialog.showMessageBox(parent, options) : await dialog.showMessageBox(options) return response === 0 }, ) // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. // 导出窗口实例和方法供其他模块使用 module.exports = { getMainWindow: () => mainWindow, getLoginWindow: () => loginWindow, createMainWindow: () => { mainWindow = createMainWindow() return mainWindow }, closeLoginWindow: () => { if (loginWindow) { loginWindow.close() loginWindow = null } }, }