|
|
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
|
|
|
}
|
|
|
},
|
|
|
}
|