refactor: 添加进程管理功能,支持强制清理相关进程和获取进程信息
This commit is contained in:
@@ -11,8 +11,37 @@ import {
|
|||||||
} from 'electron'
|
} from 'electron'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { spawn } from 'child_process'
|
import { spawn, exec } from 'child_process'
|
||||||
import { getAppRoot, checkEnvironment } from './services/environmentService'
|
import { getAppRoot, checkEnvironment } from './services/environmentService'
|
||||||
|
|
||||||
|
// 强制清理相关进程的函数
|
||||||
|
async function forceKillRelatedProcesses(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { killAllRelatedProcesses } = await import('./utils/processManager')
|
||||||
|
await killAllRelatedProcesses()
|
||||||
|
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) => {
|
||||||
|
// 使用更简单的命令强制结束相关进程
|
||||||
|
exec(`taskkill /f /im python.exe`, (error) => {
|
||||||
|
if (error) {
|
||||||
|
log.warn('备用清理方法失败:', error.message)
|
||||||
|
} else {
|
||||||
|
log.info('备用清理方法执行成功')
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
|
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
|
||||||
import {
|
import {
|
||||||
setMainWindow as setPythonMainWindow,
|
setMainWindow as setPythonMainWindow,
|
||||||
@@ -385,7 +414,25 @@ function createWindow() {
|
|||||||
updateTrayVisibility(currentConfig)
|
updateTrayVisibility(currentConfig)
|
||||||
log.info('窗口已最小化到托盘,任务栏图标已隐藏')
|
log.info('窗口已最小化到托盘,任务栏图标已隐藏')
|
||||||
} else {
|
} else {
|
||||||
saveWindowState()
|
// 立即保存窗口状态,不使用防抖
|
||||||
|
if (!win.isDestroyed()) {
|
||||||
|
try {
|
||||||
|
const config = loadConfig()
|
||||||
|
const bounds = win.getBounds()
|
||||||
|
const isMaximized = win.isMaximized()
|
||||||
|
|
||||||
|
if (!isMaximized) {
|
||||||
|
config.UI.size = `${bounds.width},${bounds.height}`
|
||||||
|
config.UI.location = `${bounds.x},${bounds.y}`
|
||||||
|
}
|
||||||
|
config.UI.maximized = isMaximized
|
||||||
|
|
||||||
|
saveConfig(config)
|
||||||
|
log.info('窗口状态已保存')
|
||||||
|
} catch (error) {
|
||||||
|
log.error('保存窗口状态失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -395,6 +442,19 @@ function createWindow() {
|
|||||||
screen.removeListener('display-metrics-changed', recomputeMinSize)
|
screen.removeListener('display-metrics-changed', recomputeMinSize)
|
||||||
// 置空模块级引用
|
// 置空模块级引用
|
||||||
mainWindow = null
|
mainWindow = null
|
||||||
|
|
||||||
|
// 如果是正在退出,立即执行进程清理
|
||||||
|
if (isQuitting) {
|
||||||
|
log.info('窗口关闭,执行最终清理')
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await forceKillRelatedProcesses()
|
||||||
|
} catch (e) {
|
||||||
|
log.error('最终清理失败:', e)
|
||||||
|
}
|
||||||
|
process.exit(0)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
win.on('minimize', () => {
|
win.on('minimize', () => {
|
||||||
@@ -441,7 +501,7 @@ function createWindow() {
|
|||||||
|
|
||||||
// 保存窗口状态(带防抖)
|
// 保存窗口状态(带防抖)
|
||||||
function saveWindowState() {
|
function saveWindowState() {
|
||||||
if (!mainWindow) return
|
if (!mainWindow || mainWindow.isDestroyed()) return
|
||||||
|
|
||||||
// 清除之前的定时器
|
// 清除之前的定时器
|
||||||
if (saveWindowStateTimeout) {
|
if (saveWindowStateTimeout) {
|
||||||
@@ -451,9 +511,15 @@ function saveWindowState() {
|
|||||||
// 设置新的定时器,500ms后保存
|
// 设置新的定时器,500ms后保存
|
||||||
saveWindowStateTimeout = setTimeout(() => {
|
saveWindowStateTimeout = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
|
// 再次检查窗口是否存在且未销毁
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
|
log.warn('窗口已销毁,跳过保存状态')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const config = loadConfig()
|
const config = loadConfig()
|
||||||
const bounds = mainWindow!.getBounds()
|
const bounds = mainWindow.getBounds()
|
||||||
const isMaximized = mainWindow!.isMaximized()
|
const isMaximized = mainWindow.isMaximized()
|
||||||
|
|
||||||
// 只有在窗口不是最大化状态时才保存位置和大小
|
// 只有在窗口不是最大化状态时才保存位置和大小
|
||||||
if (!isMaximized) {
|
if (!isMaximized) {
|
||||||
@@ -496,10 +562,58 @@ ipcMain.handle('window-maximize', () => {
|
|||||||
|
|
||||||
ipcMain.handle('window-close', () => {
|
ipcMain.handle('window-close', () => {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
|
isQuitting = true
|
||||||
mainWindow.close()
|
mainWindow.close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加强制退出处理器
|
||||||
|
ipcMain.handle('app-quit', () => {
|
||||||
|
isQuitting = true
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加进程管理相关的 IPC 处理器
|
||||||
|
ipcMain.handle('get-related-processes', async () => {
|
||||||
|
try {
|
||||||
|
const { getRelatedProcesses } = await import('./utils/processManager')
|
||||||
|
return await getRelatedProcesses()
|
||||||
|
} catch (error) {
|
||||||
|
log.error('获取进程信息失败:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('kill-all-processes', async () => {
|
||||||
|
try {
|
||||||
|
await forceKillRelatedProcesses()
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
log.error('强制清理进程失败:', error)
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 添加一个测试用的强制退出命令
|
||||||
|
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 }
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('window-is-maximized', () => {
|
ipcMain.handle('window-is-maximized', () => {
|
||||||
return mainWindow ? mainWindow.isMaximized() : false
|
return mainWindow ? mainWindow.isMaximized() : false
|
||||||
})
|
})
|
||||||
@@ -627,9 +741,10 @@ ipcMain.handle('start-backend', async () => {
|
|||||||
return startBackend(appRoot)
|
return startBackend(appRoot)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ipcMain.handle('stop-backend', async () => {
|
ipcMain.handle('stop-backend', async () => {
|
||||||
// return stopBackend()
|
const { stopBackend } = await import('./services/pythonService')
|
||||||
// })
|
return stopBackend()
|
||||||
|
})
|
||||||
|
|
||||||
// Git相关
|
// Git相关
|
||||||
ipcMain.handle('download-git', async () => {
|
ipcMain.handle('download-git', async () => {
|
||||||
@@ -1011,19 +1126,67 @@ app.on('before-quit', async event => {
|
|||||||
|
|
||||||
log.info('应用准备退出')
|
log.info('应用准备退出')
|
||||||
|
|
||||||
|
// 清理定时器
|
||||||
|
if (saveWindowStateTimeout) {
|
||||||
|
clearTimeout(saveWindowStateTimeout)
|
||||||
|
saveWindowStateTimeout = null
|
||||||
|
}
|
||||||
|
|
||||||
// 清理托盘
|
// 清理托盘
|
||||||
destroyTray()
|
destroyTray()
|
||||||
|
|
||||||
// try {
|
// 立即开始强制清理,不等待优雅关闭
|
||||||
// await stopBackend()
|
log.info('开始强制清理所有相关进程')
|
||||||
// log.info('后端服务已停止')
|
|
||||||
// } catch (e) {
|
try {
|
||||||
// log.error('停止后端时出错:', e)
|
// 并行执行多种清理方法
|
||||||
// console.error('停止后端时出错:', e)
|
const cleanupPromises = [
|
||||||
// } finally {
|
// 方法1: 使用我们的进程管理器
|
||||||
// log.info('应用退出')
|
forceKillRelatedProcesses(),
|
||||||
// app.exit(0)
|
|
||||||
// }
|
// 方法2: 直接使用 taskkill 命令
|
||||||
|
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`
|
||||||
|
]
|
||||||
|
|
||||||
|
let completed = 0
|
||||||
|
commands.forEach(cmd => {
|
||||||
|
exec(cmd, () => {
|
||||||
|
completed++
|
||||||
|
if (completed === commands.length) {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}, 500)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1054,7 +1217,10 @@ app.whenReady().then(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') app.quit()
|
if (process.platform !== 'darwin') {
|
||||||
|
isQuitting = true
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
|
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
|
||||||
windowClose: () => ipcRenderer.invoke('window-close'),
|
windowClose: () => ipcRenderer.invoke('window-close'),
|
||||||
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
|
||||||
|
appQuit: () => ipcRenderer.invoke('app-quit'),
|
||||||
|
|
||||||
|
// 进程管理
|
||||||
|
getRelatedProcesses: () => ipcRenderer.invoke('get-related-processes'),
|
||||||
|
killAllProcesses: () => ipcRenderer.invoke('kill-all-processes'),
|
||||||
|
forceExit: () => ipcRenderer.invoke('force-exit'),
|
||||||
|
|
||||||
// 初始化相关API
|
// 初始化相关API
|
||||||
checkEnvironment: () => ipcRenderer.invoke('check-environment'),
|
checkEnvironment: () => ipcRenderer.invoke('check-environment'),
|
||||||
|
|||||||
@@ -574,38 +574,69 @@ export async function startBackend(appRoot: string, timeoutMs = 30_000) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 停止后端进程(如果没启动就直接返回成功) */
|
/** 停止后端进程(如果没启动就直接返回成功) */
|
||||||
// export async function stopBackend() {
|
export async function stopBackend() {
|
||||||
// if (!backendProc || backendProc.killed) {
|
if (!backendProc || backendProc.killed) {
|
||||||
// console.log('[Backend] 未运行,无需停止')
|
console.log('[Backend] 未运行,无需停止')
|
||||||
// return { success: true }
|
return { success: true }
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// const pid = backendProc.pid
|
const pid = backendProc.pid
|
||||||
// console.log('[Backend] 正在停止后端服务, PID =', pid)
|
console.log('[Backend] 正在停止后端服务, PID =', pid)
|
||||||
//
|
|
||||||
// return new Promise<{ success: boolean; error?: string }>(resolve => {
|
return new Promise<{ success: boolean; error?: string }>(resolve => {
|
||||||
// // 清监听,避免重复日志
|
// 设置超时,确保不会无限等待
|
||||||
// backendProc?.stdout?.removeAllListeners('data')
|
const timeout = setTimeout(() => {
|
||||||
// backendProc?.stderr?.removeAllListeners('data')
|
console.warn('[Backend] 停止超时,强制结束进程')
|
||||||
//
|
try {
|
||||||
// backendProc!.once('exit', (code, signal) => {
|
if (backendProc && !backendProc.killed) {
|
||||||
// console.log('[Backend] 已退出', { code, signal })
|
// 在 Windows 上使用 taskkill 强制结束进程树
|
||||||
// backendProc = null
|
if (process.platform === 'win32') {
|
||||||
// resolve({ success: true })
|
const { exec } = require('child_process')
|
||||||
// })
|
exec(`taskkill /f /t /pid ${pid}`, (error: any) => {
|
||||||
//
|
if (error) {
|
||||||
// backendProc!.once('error', err => {
|
console.error('[Backend] taskkill 失败:', error)
|
||||||
// console.error('[Backend] 停止时出错:', err)
|
} else {
|
||||||
// backendProc = null
|
console.log('[Backend] 进程树已强制结束')
|
||||||
// resolve({ success: false, error: err instanceof Error ? err.message : String(err) })
|
}
|
||||||
// })
|
})
|
||||||
//
|
} else {
|
||||||
// try {
|
backendProc.kill('SIGKILL')
|
||||||
// backendProc!.kill() // 默认 SIGTERM,Windows 下等价于结束进程
|
}
|
||||||
// } catch (e) {
|
}
|
||||||
// console.error('[Backend] kill 调用失败:', e)
|
} catch (e) {
|
||||||
// backendProc = null
|
console.error('[Backend] 强制结束失败:', e)
|
||||||
// resolve({ success: false, error: e instanceof Error ? e.message : String(e) })
|
}
|
||||||
// }
|
backendProc = null
|
||||||
// })
|
resolve({ success: true })
|
||||||
// }
|
}, 2000) // 2秒超时
|
||||||
|
|
||||||
|
// 清监听,避免重复日志
|
||||||
|
backendProc?.stdout?.removeAllListeners('data')
|
||||||
|
backendProc?.stderr?.removeAllListeners('data')
|
||||||
|
|
||||||
|
backendProc!.once('exit', (code, signal) => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
console.log('[Backend] 已退出', { code, signal })
|
||||||
|
backendProc = null
|
||||||
|
resolve({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
backendProc!.once('error', err => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
console.error('[Backend] 停止时出错:', err)
|
||||||
|
backendProc = null
|
||||||
|
resolve({ success: false, error: err instanceof Error ? err.message : String(err) })
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 首先尝试优雅关闭
|
||||||
|
backendProc!.kill('SIGTERM')
|
||||||
|
console.log('[Backend] 已发送 SIGTERM 信号')
|
||||||
|
} catch (e) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
console.error('[Backend] kill 调用失败:', e)
|
||||||
|
backendProc = null
|
||||||
|
resolve({ success: false, error: e instanceof Error ? e.message : String(e) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
125
frontend/electron/utils/processManager.ts
Normal file
125
frontend/electron/utils/processManager.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { exec } from 'child_process'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { getAppRoot } from '../services/environmentService'
|
||||||
|
|
||||||
|
export interface ProcessInfo {
|
||||||
|
pid: number
|
||||||
|
name: string
|
||||||
|
commandLine: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有相关的进程信息
|
||||||
|
*/
|
||||||
|
export async function getRelatedProcesses(): Promise<ProcessInfo[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
resolve([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const appRoot = getAppRoot()
|
||||||
|
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
||||||
|
|
||||||
|
// 使用 wmic 获取详细的进程信息
|
||||||
|
const cmd = `wmic process where "Name='python.exe' or Name='AUTO-MAS.exe' or CommandLine like '%main.py%'" get ProcessId,Name,CommandLine /format:csv`
|
||||||
|
|
||||||
|
exec(cmd, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('获取进程信息失败:', error)
|
||||||
|
resolve([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const processes: ProcessInfo[] = []
|
||||||
|
const lines = stdout.split('\n').filter(line => line.trim() && !line.startsWith('Node'))
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parts = line.split(',')
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
const commandLine = parts[1] || ''
|
||||||
|
const name = parts[2] || ''
|
||||||
|
const pid = parseInt(parts[3]) || 0
|
||||||
|
|
||||||
|
if (pid > 0 && (
|
||||||
|
commandLine.includes(pythonExePath) ||
|
||||||
|
commandLine.includes('main.py') ||
|
||||||
|
name === 'AUTO-MAS.exe'
|
||||||
|
)) {
|
||||||
|
processes.push({ pid, name, commandLine })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(processes)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制结束指定的进程
|
||||||
|
*/
|
||||||
|
export async function killProcess(pid: number): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
resolve(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(`taskkill /f /t /pid ${pid}`, (error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error(`结束进程 ${pid} 失败:`, error.message)
|
||||||
|
resolve(false)
|
||||||
|
} else {
|
||||||
|
console.log(`进程 ${pid} 已结束`)
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制结束所有相关进程
|
||||||
|
*/
|
||||||
|
export async function killAllRelatedProcesses(): Promise<void> {
|
||||||
|
console.log('开始清理所有相关进程...')
|
||||||
|
|
||||||
|
const processes = await getRelatedProcesses()
|
||||||
|
console.log(`找到 ${processes.length} 个相关进程:`)
|
||||||
|
|
||||||
|
for (const proc of processes) {
|
||||||
|
console.log(`- PID: ${proc.pid}, Name: ${proc.name}, CMD: ${proc.commandLine.substring(0, 100)}...`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行结束所有进程
|
||||||
|
const killPromises = processes.map(proc => killProcess(proc.pid))
|
||||||
|
await Promise.all(killPromises)
|
||||||
|
|
||||||
|
console.log('进程清理完成')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待进程结束
|
||||||
|
*/
|
||||||
|
export async function waitForProcessExit(pid: number, timeoutMs: number = 5000): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
const checkProcess = () => {
|
||||||
|
if (Date.now() - startTime > timeoutMs) {
|
||||||
|
resolve(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(`tasklist /fi "PID eq ${pid}"`, (error, stdout) => {
|
||||||
|
if (error || !stdout.includes(pid.toString())) {
|
||||||
|
resolve(true)
|
||||||
|
} else {
|
||||||
|
setTimeout(checkProcess, 100)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
checkProcess()
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.2",
|
"version": "5.0.0-alpha.1",
|
||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"",
|
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"",
|
||||||
|
|||||||
@@ -12,11 +12,7 @@
|
|||||||
<span v-if="updateInfo?.if_need_update" class="update-hint" :title="getUpdateTooltip()">
|
<span v-if="updateInfo?.if_need_update" class="update-hint" :title="getUpdateTooltip()">
|
||||||
检测到更新 {{ updateInfo.latest_version }} 请尽快更新
|
检测到更新 {{ updateInfo.latest_version }} 请尽快更新
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span v-if="backendUpdateInfo?.if_need_update" class="update-hint" :title="getUpdateTooltip()">
|
||||||
v-if="backendUpdateInfo?.if_need_update"
|
|
||||||
class="update-hint"
|
|
||||||
:title="getUpdateTooltip()"
|
|
||||||
>
|
|
||||||
检测到更新后端有更新。请重启软件即可自动完成更新
|
检测到更新后端有更新。请重启软件即可自动完成更新
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -32,11 +28,7 @@
|
|||||||
<button class="control-button minimize-button" @click="minimizeWindow" title="最小化">
|
<button class="control-button minimize-button" @click="minimizeWindow" title="最小化">
|
||||||
<MinusOutlined />
|
<MinusOutlined />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="control-button maximize-button" @click="toggleMaximize" :title="isMaximized ? '还原' : '最大化'">
|
||||||
class="control-button maximize-button"
|
|
||||||
@click="toggleMaximize"
|
|
||||||
:title="isMaximized ? '还原' : '最大化'"
|
|
||||||
>
|
|
||||||
<BorderOutlined />
|
<BorderOutlined />
|
||||||
</button>
|
</button>
|
||||||
<button class="control-button close-button" @click="closeWindow" title="关闭">
|
<button class="control-button close-button" @click="closeWindow" title="关闭">
|
||||||
@@ -125,20 +117,41 @@ const toggleMaximize = async () => {
|
|||||||
|
|
||||||
const closeWindow = async () => {
|
const closeWindow = async () => {
|
||||||
try {
|
try {
|
||||||
// 先调用后端关闭API
|
console.log('开始关闭应用...')
|
||||||
await Service.closeApiCoreClosePost()
|
|
||||||
console.log('Backend close API called successfully')
|
// 先检查当前进程状态
|
||||||
// 然后关闭窗口
|
try {
|
||||||
await window.electronAPI?.windowClose()
|
const processes = await window.electronAPI?.getRelatedProcesses()
|
||||||
|
console.log('关闭前的进程状态:', processes)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('无法获取进程状态:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步调用后端关闭API,不等待响应
|
||||||
|
Service.closeApiCoreClosePost().catch(error => {
|
||||||
|
console.warn('Backend close API failed (this is expected):', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用更激进的强制退出方法
|
||||||
|
try {
|
||||||
|
console.log('执行强制退出...')
|
||||||
|
await window.electronAPI?.forceExit()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to close window:', error)
|
console.error('强制退出失败,尝试备用方法:', error)
|
||||||
// 即使API调用失败,也尝试关闭窗口
|
|
||||||
|
// 备用方法:先尝试正常关闭
|
||||||
try {
|
try {
|
||||||
await window.electronAPI?.windowClose()
|
await window.electronAPI?.windowClose()
|
||||||
} catch (closeError) {
|
setTimeout(async () => {
|
||||||
console.error('Failed to close window after API error:', closeError)
|
await window.electronAPI?.appQuit()
|
||||||
|
}, 500)
|
||||||
|
} catch (backupError) {
|
||||||
|
console.error('备用方法也失败:', backupError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('关闭应用失败:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pollOnce = async () => {
|
const pollOnce = async () => {
|
||||||
@@ -189,7 +202,8 @@ onBeforeUnmount(() => {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
overflow: hidden; /* 新增:裁剪超出顶栏的发光 */
|
overflow: hidden;
|
||||||
|
/* 新增:裁剪超出顶栏的发光 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-bar-dark {
|
.title-bar-dark {
|
||||||
@@ -208,24 +222,29 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
position: relative; /* 使阴影绝对定位基准 */
|
position: relative;
|
||||||
|
/* 使阴影绝对定位基准 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 新增:主题色虚化圆形阴影 */
|
/* 新增:主题色虚化圆形阴影 */
|
||||||
.logo-glow {
|
.logo-glow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 55px; /* 调整:更贴近图标 */
|
left: 55px;
|
||||||
|
/* 调整:更贴近图标 */
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 200px; /* 缩小尺寸以适配 32px 高度 */
|
width: 200px;
|
||||||
|
/* 缩小尺寸以适配 32px 高度 */
|
||||||
height: 100px;
|
height: 100px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: radial-gradient(circle at 50% 50%, var(--ant-color-primary) 0%, rgba(0, 0, 0, 0) 70%);
|
background: radial-gradient(circle at 50% 50%, var(--ant-color-primary) 0%, rgba(0, 0, 0, 0) 70%);
|
||||||
filter: blur(24px); /* 降低模糊避免越界过多 */
|
filter: blur(24px);
|
||||||
|
/* 降低模糊避免越界过多 */
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-bar-dark .logo-glow {
|
.title-bar-dark .logo-glow {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
filter: blur(24px);
|
filter: blur(24px);
|
||||||
@@ -235,7 +254,8 @@ onBeforeUnmount(() => {
|
|||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1; /* 确保在阴影上方 */
|
z-index: 1;
|
||||||
|
/* 确保在阴影上方 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-text {
|
.title-text {
|
||||||
@@ -403,9 +423,11 @@ onBeforeUnmount(() => {
|
|||||||
0% {
|
0% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
background-position: 100% 50%;
|
background-position: 100% 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
background-position: 0% 50%;
|
background-position: 0% 50%;
|
||||||
}
|
}
|
||||||
@@ -416,14 +438,17 @@ onBeforeUnmount(() => {
|
|||||||
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
|
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
33% {
|
33% {
|
||||||
filter: drop-shadow(0 0 6px rgba(255, 152, 0, 0.5)) brightness(1.08);
|
filter: drop-shadow(0 0 6px rgba(255, 152, 0, 0.5)) brightness(1.08);
|
||||||
transform: scale(1.003);
|
transform: scale(1.003);
|
||||||
}
|
}
|
||||||
|
|
||||||
66% {
|
66% {
|
||||||
filter: drop-shadow(0 0 5px rgba(76, 175, 80, 0.45)) brightness(1.05);
|
filter: drop-shadow(0 0 5px rgba(76, 175, 80, 0.45)) brightness(1.05);
|
||||||
transform: scale(1.002);
|
transform: scale(1.002);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
|
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
@@ -435,10 +460,12 @@ onBeforeUnmount(() => {
|
|||||||
opacity: 0.08;
|
opacity: 0.08;
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.04;
|
opacity: 0.04;
|
||||||
transform: scale(1.02);
|
transform: scale(1.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 0.08;
|
opacity: 0.08;
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
|
|||||||
Reference in New Issue
Block a user