You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

275 lines
8.3 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<string, JsonValue>)
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<boolean> => {
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
throw new Error('Invalid dialog payload')
}
const p = payload as Record<string, unknown>
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
}
},
}