feat(electron): 添加系统级对话框功能

- 在 ElectronAPI 中新增对话框相关方法:showQuestionDialog、dialogResponse、resizeDialogWindow
- 实现独立的对话框窗口 dialog.html,支持自定义标题、消息和选项- 添加对话框窗口的键盘导航和焦点管理功能
- 在主进程中实现对话框窗口的创建、显示和响应处理
- 更新 WebSocket 消息监听器组件,使用系统级对话框替代应用内弹窗- 优化进程清理逻辑,增加多种清理方法并行执行
- 重构日志工具类,改进 Electron API 调用方式
- 调整 Git 更新检查逻辑,避免直接访问 GitHub
- 移除冗余的类型定义文件,统一 Electron API 接口定义
This commit is contained in:
MoeSnowyFox
2025-10-01 23:17:23 +08:00
parent 3d204980a2
commit 1d91204842
8 changed files with 799 additions and 409 deletions

View File

@@ -1,18 +1,28 @@
import {
app,
BrowserWindow,
ipcMain,
dialog,
shell,
Tray,
ipcMain,
Menu,
nativeImage,
screen,
shell,
Tray,
} from 'electron'
import * as path from 'path'
import * as fs from 'fs'
import { spawn, exec } from 'child_process'
import { getAppRoot, checkEnvironment } from './services/environmentService'
import { exec, spawn } from 'child_process'
import { checkEnvironment, getAppRoot } from './services/environmentService'
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
import {
downloadPython,
installDependencies,
installPipPackage,
setMainWindow as setPythonMainWindow,
startBackend,
} from './services/pythonService'
import { cloneBackend, downloadGit, setMainWindow as setGitMainWindow } from './services/gitService'
import { cleanOldLogs, getLogFiles, getLogPath, log, setupLogger } from './services/logService'
// 强制清理相关进程的函数
async function forceKillRelatedProcesses(): Promise<void> {
@@ -22,15 +32,15 @@ async function forceKillRelatedProcesses(): Promise<void> {
log.info('所有相关进程已清理')
} catch (error) {
log.error('清理进程时出错:', error)
// 备用清理方法
if (process.platform === 'win32') {
const appRoot = getAppRoot()
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
return new Promise((resolve) => {
return new Promise(resolve => {
// 使用更简单的命令强制结束相关进程
exec(`taskkill /f /im python.exe`, (error) => {
exec(`taskkill /f /im python.exe`, error => {
if (error) {
log.warn('备用清理方法失败:', error.message)
} else {
@@ -42,16 +52,6 @@ async function forceKillRelatedProcesses(): Promise<void> {
}
}
}
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
import {
setMainWindow as setPythonMainWindow,
downloadPython,
installPipPackage,
installDependencies,
startBackend,
} from './services/pythonService'
import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService'
import { setupLogger, log, getLogPath, getLogFiles, cleanOldLogs } from './services/logService'
// 检查是否以管理员权限运行
function isRunningAsAdmin(): boolean {
@@ -113,6 +113,7 @@ interface AppConfig {
IfMinimizeDirectly: boolean
IfSelfStart: boolean
}
[key: string]: any
}
@@ -304,7 +305,9 @@ function updateTrayVisibility(config: AppConfig) {
log.info('托盘图标已销毁')
}
}
let mainWindow: Electron.BrowserWindow | null = null
function createWindow() {
log.info('开始创建主窗口')
@@ -442,7 +445,7 @@ function createWindow() {
screen.removeListener('display-metrics-changed', recomputeMinSize)
// 置空模块级引用
mainWindow = null
// 如果是正在退出,立即执行进程清理
if (isQuitting) {
log.info('窗口关闭,执行最终清理')
@@ -598,19 +601,19 @@ ipcMain.handle('kill-all-processes', async () => {
ipcMain.handle('force-exit', async () => {
log.info('收到强制退出命令')
isQuitting = true
// 立即清理进程
try {
await forceKillRelatedProcesses()
} catch (e) {
log.error('强制清理失败:', e)
}
// 强制退出
setTimeout(() => {
process.exit(0)
}, 500)
return { success: true }
})
@@ -746,6 +749,137 @@ ipcMain.handle('stop-backend', async () => {
return stopBackend()
})
// 全局存储对话框窗口引用和回调
let dialogWindows = new Map<string, BrowserWindow>()
let dialogCallbacks = new Map<string, (result: boolean) => void>()
// 创建对话框窗口
function createQuestionDialog(questionData: any): Promise<boolean> {
return new Promise((resolve) => {
const messageId = questionData.messageId || 'dialog_' + Date.now()
// 存储回调函数
dialogCallbacks.set(messageId, resolve)
// 准备对话框数据
const dialogData = {
title: questionData.title || '操作确认',
message: questionData.message || '是否要执行此操作?',
options: questionData.options || ['确定', '取消'],
messageId: messageId
}
// 创建对话框窗口
const dialogWindow = new BrowserWindow({
width: 450,
height: 200,
minWidth: 350,
minHeight: 150,
maxWidth: 600,
maxHeight: 400,
resizable: true,
minimizable: false,
maximizable: false,
alwaysOnTop: true,
show: false,
frame: false,
modal: mainWindow ? true : false,
parent: mainWindow || undefined,
icon: path.join(__dirname, '../public/AUTO-MAS.ico'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
})
// 存储窗口引用
dialogWindows.set(messageId, dialogWindow)
// 编码对话框数据
const encodedData = encodeURIComponent(JSON.stringify(dialogData))
// 加载对话框页面
const dialogUrl = `file://${path.join(__dirname, '../public/dialog.html')}?data=${encodedData}`
dialogWindow.loadURL(dialogUrl)
// 窗口准备好后显示并居中
dialogWindow.once('ready-to-show', () => {
// 计算居中位置
if (mainWindow && !mainWindow.isDestroyed()) {
const mainBounds = mainWindow.getBounds()
const dialogBounds = dialogWindow.getBounds()
const x = Math.round(mainBounds.x + (mainBounds.width - dialogBounds.width) / 2)
const y = Math.round(mainBounds.y + (mainBounds.height - dialogBounds.height) / 2)
dialogWindow.setPosition(x, y)
} else {
dialogWindow.center()
}
dialogWindow.show()
dialogWindow.focus()
})
// 窗口关闭时清理
dialogWindow.on('closed', () => {
dialogWindows.delete(messageId)
const callback = dialogCallbacks.get(messageId)
if (callback) {
dialogCallbacks.delete(messageId)
callback(false) // 默认返回 false (取消)
}
})
log.info(`对话框窗口已创建: ${messageId}`)
})
}
// 显示问题对话框
ipcMain.handle('show-question-dialog', async (_event, questionData) => {
log.info('收到显示对话框请求:', questionData)
try {
const result = await createQuestionDialog(questionData)
log.info(`对话框结果: ${result}`)
return result
} catch (error) {
log.error('创建对话框失败:', error)
return false
}
})
// 处理对话框响应
ipcMain.handle('dialog-response', async (_event, messageId: string, choice: boolean) => {
log.info(`收到对话框响应: ${messageId} = ${choice}`)
const callback = dialogCallbacks.get(messageId)
if (callback) {
dialogCallbacks.delete(messageId)
callback(choice)
}
// 关闭对话框窗口
const dialogWindow = dialogWindows.get(messageId)
if (dialogWindow && !dialogWindow.isDestroyed()) {
dialogWindow.close()
}
dialogWindows.delete(messageId)
return true
})
// 调整对话框窗口大小
ipcMain.handle('resize-dialog-window', async (_event, height: number) => {
// 获取当前活动的对话框窗口(最后创建的)
const dialogWindow = Array.from(dialogWindows.values()).pop()
if (dialogWindow && !dialogWindow.isDestroyed()) {
const bounds = dialogWindow.getBounds()
dialogWindow.setBounds({
...bounds,
height: Math.max(150, Math.min(400, height))
})
}
})
// Git相关
ipcMain.handle('download-git', async () => {
const appRoot = getAppRoot()
@@ -785,7 +919,7 @@ ipcMain.handle('check-git-update', async () => {
// 不执行fetch直接检查本地状态
// 这样避免了直接访问GitHub而是在后续的pull操作中使用镜像站
// 获取当前HEAD的commit hash
const currentCommit = await new Promise<string>((resolve, reject) => {
const revParseProc = spawn(gitPath, ['rev-parse', 'HEAD'], {
@@ -811,14 +945,13 @@ ipcMain.handle('check-git-update', async () => {
})
log.info(`当前本地commit: ${currentCommit}`)
// 由于我们跳过了fetch步骤避免直接访问GitHub
// 我们无法准确知道远程是否有更新
// 因此返回true让后续的pull操作通过镜像站来检查和获取更新
// 如果没有更新pull操作会很快完成且不会有实际变化
log.info('跳过远程检查返回hasUpdate=true以触发镜像站更新流程')
return { hasUpdate: true, skipReason: 'avoided_github_access' }
} catch (error) {
log.error('检查Git更新失败:', error)
// 如果检查失败返回true以触发更新流程确保代码是最新的
@@ -1084,23 +1217,23 @@ app.on('before-quit', async event => {
// 立即开始强制清理,不等待优雅关闭
log.info('开始强制清理所有相关进程')
try {
// 并行执行多种清理方法
const cleanupPromises = [
// 方法1: 使用我们的进程管理器
forceKillRelatedProcesses(),
// 方法2: 直接使用 taskkill 命令
new Promise<void>((resolve) => {
new Promise<void>(resolve => {
if (process.platform === 'win32') {
const appRoot = getAppRoot()
const commands = [
`taskkill /f /im python.exe`,
`wmic process where "CommandLine like '%main.py%'" delete`,
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`,
]
let completed = 0
commands.forEach(cmd => {
exec(cmd, () => {
@@ -1110,26 +1243,26 @@ app.on('before-quit', async event => {
}
})
})
// 2秒超时
setTimeout(resolve, 2000)
} else {
resolve()
}
})
}),
]
// 最多等待3秒
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 3000))
await Promise.race([Promise.all(cleanupPromises), timeoutPromise])
log.info('进程清理完成')
} catch (e) {
log.error('进程清理时出错:', e)
}
log.info('应用强制退出')
// 使用 process.exit 而不是 app.exit更加强制
setTimeout(() => {
process.exit(0)

View File

@@ -63,6 +63,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', filePath),
// 对话框相关
showQuestionDialog: (questionData: any) => ipcRenderer.invoke('show-question-dialog', questionData),
dialogResponse: (messageId: string, choice: boolean) => ipcRenderer.invoke('dialog-response', messageId, choice),
resizeDialogWindow: (height: number) => ipcRenderer.invoke('resize-dialog-window', height),
// 监听下载进度
onDownloadProgress: (callback: (progress: any) => void) => {
ipcRenderer.on('download-progress', (_, progress) => callback(progress))