refactor: 添加进程管理功能,支持强制清理相关进程和获取进程信息
This commit is contained in:
@@ -11,8 +11,37 @@ import {
|
||||
} from 'electron'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { spawn } from 'child_process'
|
||||
import { spawn, exec } from 'child_process'
|
||||
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 setPythonMainWindow,
|
||||
@@ -385,7 +414,25 @@ function createWindow() {
|
||||
updateTrayVisibility(currentConfig)
|
||||
log.info('窗口已最小化到托盘,任务栏图标已隐藏')
|
||||
} 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)
|
||||
// 置空模块级引用
|
||||
mainWindow = null
|
||||
|
||||
// 如果是正在退出,立即执行进程清理
|
||||
if (isQuitting) {
|
||||
log.info('窗口关闭,执行最终清理')
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await forceKillRelatedProcesses()
|
||||
} catch (e) {
|
||||
log.error('最终清理失败:', e)
|
||||
}
|
||||
process.exit(0)
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
|
||||
win.on('minimize', () => {
|
||||
@@ -441,7 +501,7 @@ function createWindow() {
|
||||
|
||||
// 保存窗口状态(带防抖)
|
||||
function saveWindowState() {
|
||||
if (!mainWindow) return
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return
|
||||
|
||||
// 清除之前的定时器
|
||||
if (saveWindowStateTimeout) {
|
||||
@@ -451,9 +511,15 @@ function saveWindowState() {
|
||||
// 设置新的定时器,500ms后保存
|
||||
saveWindowStateTimeout = setTimeout(() => {
|
||||
try {
|
||||
// 再次检查窗口是否存在且未销毁
|
||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||
log.warn('窗口已销毁,跳过保存状态')
|
||||
return
|
||||
}
|
||||
|
||||
const config = loadConfig()
|
||||
const bounds = mainWindow!.getBounds()
|
||||
const isMaximized = mainWindow!.isMaximized()
|
||||
const bounds = mainWindow.getBounds()
|
||||
const isMaximized = mainWindow.isMaximized()
|
||||
|
||||
// 只有在窗口不是最大化状态时才保存位置和大小
|
||||
if (!isMaximized) {
|
||||
@@ -496,10 +562,58 @@ ipcMain.handle('window-maximize', () => {
|
||||
|
||||
ipcMain.handle('window-close', () => {
|
||||
if (mainWindow) {
|
||||
isQuitting = true
|
||||
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', () => {
|
||||
return mainWindow ? mainWindow.isMaximized() : false
|
||||
})
|
||||
@@ -627,9 +741,10 @@ ipcMain.handle('start-backend', async () => {
|
||||
return startBackend(appRoot)
|
||||
})
|
||||
|
||||
// ipcMain.handle('stop-backend', async () => {
|
||||
// return stopBackend()
|
||||
// })
|
||||
ipcMain.handle('stop-backend', async () => {
|
||||
const { stopBackend } = await import('./services/pythonService')
|
||||
return stopBackend()
|
||||
})
|
||||
|
||||
// Git相关
|
||||
ipcMain.handle('download-git', async () => {
|
||||
@@ -1011,19 +1126,67 @@ app.on('before-quit', async event => {
|
||||
|
||||
log.info('应用准备退出')
|
||||
|
||||
// 清理定时器
|
||||
if (saveWindowStateTimeout) {
|
||||
clearTimeout(saveWindowStateTimeout)
|
||||
saveWindowStateTimeout = null
|
||||
}
|
||||
|
||||
// 清理托盘
|
||||
destroyTray()
|
||||
|
||||
// try {
|
||||
// await stopBackend()
|
||||
// log.info('后端服务已停止')
|
||||
// } catch (e) {
|
||||
// log.error('停止后端时出错:', e)
|
||||
// console.error('停止后端时出错:', e)
|
||||
// } finally {
|
||||
// log.info('应用退出')
|
||||
// app.exit(0)
|
||||
// }
|
||||
// 立即开始强制清理,不等待优雅关闭
|
||||
log.info('开始强制清理所有相关进程')
|
||||
|
||||
try {
|
||||
// 并行执行多种清理方法
|
||||
const cleanupPromises = [
|
||||
// 方法1: 使用我们的进程管理器
|
||||
forceKillRelatedProcesses(),
|
||||
|
||||
// 方法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', () => {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
if (process.platform !== 'darwin') {
|
||||
isQuitting = true
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
|
||||
@@ -16,6 +16,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
|
||||
windowClose: () => ipcRenderer.invoke('window-close'),
|
||||
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
|
||||
checkEnvironment: () => ipcRenderer.invoke('check-environment'),
|
||||
|
||||
@@ -574,38 +574,69 @@ export async function startBackend(appRoot: string, timeoutMs = 30_000) {
|
||||
}
|
||||
|
||||
/** 停止后端进程(如果没启动就直接返回成功) */
|
||||
// export async function stopBackend() {
|
||||
// if (!backendProc || backendProc.killed) {
|
||||
// console.log('[Backend] 未运行,无需停止')
|
||||
// return { success: true }
|
||||
// }
|
||||
//
|
||||
// const pid = backendProc.pid
|
||||
// console.log('[Backend] 正在停止后端服务, PID =', pid)
|
||||
//
|
||||
// return new Promise<{ success: boolean; error?: string }>(resolve => {
|
||||
// // 清监听,避免重复日志
|
||||
// backendProc?.stdout?.removeAllListeners('data')
|
||||
// backendProc?.stderr?.removeAllListeners('data')
|
||||
//
|
||||
// backendProc!.once('exit', (code, signal) => {
|
||||
// console.log('[Backend] 已退出', { code, signal })
|
||||
// backendProc = null
|
||||
// resolve({ success: true })
|
||||
// })
|
||||
//
|
||||
// backendProc!.once('error', err => {
|
||||
// console.error('[Backend] 停止时出错:', err)
|
||||
// backendProc = null
|
||||
// resolve({ success: false, error: err instanceof Error ? err.message : String(err) })
|
||||
// })
|
||||
//
|
||||
// try {
|
||||
// backendProc!.kill() // 默认 SIGTERM,Windows 下等价于结束进程
|
||||
// } catch (e) {
|
||||
// console.error('[Backend] kill 调用失败:', e)
|
||||
// backendProc = null
|
||||
// resolve({ success: false, error: e instanceof Error ? e.message : String(e) })
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
export async function stopBackend() {
|
||||
if (!backendProc || backendProc.killed) {
|
||||
console.log('[Backend] 未运行,无需停止')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
const pid = backendProc.pid
|
||||
console.log('[Backend] 正在停止后端服务, PID =', pid)
|
||||
|
||||
return new Promise<{ success: boolean; error?: string }>(resolve => {
|
||||
// 设置超时,确保不会无限等待
|
||||
const timeout = setTimeout(() => {
|
||||
console.warn('[Backend] 停止超时,强制结束进程')
|
||||
try {
|
||||
if (backendProc && !backendProc.killed) {
|
||||
// 在 Windows 上使用 taskkill 强制结束进程树
|
||||
if (process.platform === 'win32') {
|
||||
const { exec } = require('child_process')
|
||||
exec(`taskkill /f /t /pid ${pid}`, (error: any) => {
|
||||
if (error) {
|
||||
console.error('[Backend] taskkill 失败:', error)
|
||||
} else {
|
||||
console.log('[Backend] 进程树已强制结束')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
backendProc.kill('SIGKILL')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Backend] 强制结束失败:', 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",
|
||||
"private": true,
|
||||
"version": "1.0.2",
|
||||
"version": "5.0.0-alpha.1",
|
||||
"main": "dist-electron/main.js",
|
||||
"scripts": {
|
||||
"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()">
|
||||
检测到更新 {{ updateInfo.latest_version }} 请尽快更新
|
||||
</span>
|
||||
<span
|
||||
v-if="backendUpdateInfo?.if_need_update"
|
||||
class="update-hint"
|
||||
:title="getUpdateTooltip()"
|
||||
>
|
||||
<span v-if="backendUpdateInfo?.if_need_update" class="update-hint" :title="getUpdateTooltip()">
|
||||
检测到更新后端有更新。请重启软件即可自动完成更新
|
||||
</span>
|
||||
</span>
|
||||
@@ -32,11 +28,7 @@
|
||||
<button class="control-button minimize-button" @click="minimizeWindow" title="最小化">
|
||||
<MinusOutlined />
|
||||
</button>
|
||||
<button
|
||||
class="control-button maximize-button"
|
||||
@click="toggleMaximize"
|
||||
:title="isMaximized ? '还原' : '最大化'"
|
||||
>
|
||||
<button class="control-button maximize-button" @click="toggleMaximize" :title="isMaximized ? '还原' : '最大化'">
|
||||
<BorderOutlined />
|
||||
</button>
|
||||
<button class="control-button close-button" @click="closeWindow" title="关闭">
|
||||
@@ -125,19 +117,40 @@ const toggleMaximize = async () => {
|
||||
|
||||
const closeWindow = async () => {
|
||||
try {
|
||||
// 先调用后端关闭API
|
||||
await Service.closeApiCoreClosePost()
|
||||
console.log('Backend close API called successfully')
|
||||
// 然后关闭窗口
|
||||
await window.electronAPI?.windowClose()
|
||||
} catch (error) {
|
||||
console.error('Failed to close window:', error)
|
||||
// 即使API调用失败,也尝试关闭窗口
|
||||
console.log('开始关闭应用...')
|
||||
|
||||
// 先检查当前进程状态
|
||||
try {
|
||||
await window.electronAPI?.windowClose()
|
||||
} catch (closeError) {
|
||||
console.error('Failed to close window after API error:', closeError)
|
||||
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) {
|
||||
console.error('强制退出失败,尝试备用方法:', error)
|
||||
|
||||
// 备用方法:先尝试正常关闭
|
||||
try {
|
||||
await window.electronAPI?.windowClose()
|
||||
setTimeout(async () => {
|
||||
await window.electronAPI?.appQuit()
|
||||
}, 500)
|
||||
} catch (backupError) {
|
||||
console.error('备用方法也失败:', backupError)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('关闭应用失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +202,8 @@ onBeforeUnmount(() => {
|
||||
user-select: none;
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
overflow: hidden; /* 新增:裁剪超出顶栏的发光 */
|
||||
overflow: hidden;
|
||||
/* 新增:裁剪超出顶栏的发光 */
|
||||
}
|
||||
|
||||
.title-bar-dark {
|
||||
@@ -208,24 +222,29 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
position: relative; /* 使阴影绝对定位基准 */
|
||||
position: relative;
|
||||
/* 使阴影绝对定位基准 */
|
||||
}
|
||||
|
||||
/* 新增:主题色虚化圆形阴影 */
|
||||
.logo-glow {
|
||||
position: absolute;
|
||||
left: 55px; /* 调整:更贴近图标 */
|
||||
left: 55px;
|
||||
/* 调整:更贴近图标 */
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 200px; /* 缩小尺寸以适配 32px 高度 */
|
||||
width: 200px;
|
||||
/* 缩小尺寸以适配 32px 高度 */
|
||||
height: 100px;
|
||||
pointer-events: none;
|
||||
border-radius: 50%;
|
||||
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;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.title-bar-dark .logo-glow {
|
||||
opacity: 0.7;
|
||||
filter: blur(24px);
|
||||
@@ -235,7 +254,8 @@ onBeforeUnmount(() => {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
z-index: 1; /* 确保在阴影上方 */
|
||||
z-index: 1;
|
||||
/* 确保在阴影上方 */
|
||||
}
|
||||
|
||||
.title-text {
|
||||
@@ -382,7 +402,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
/* 为相邻的更新提示添加间距 */
|
||||
.update-hint + .update-hint {
|
||||
.update-hint+.update-hint {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
@@ -403,9 +423,11 @@ onBeforeUnmount(() => {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
@@ -416,14 +438,17 @@ onBeforeUnmount(() => {
|
||||
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
33% {
|
||||
filter: drop-shadow(0 0 6px rgba(255, 152, 0, 0.5)) brightness(1.08);
|
||||
transform: scale(1.003);
|
||||
}
|
||||
|
||||
66% {
|
||||
filter: drop-shadow(0 0 5px rgba(76, 175, 80, 0.45)) brightness(1.05);
|
||||
transform: scale(1.002);
|
||||
}
|
||||
|
||||
100% {
|
||||
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
|
||||
transform: scale(1);
|
||||
@@ -435,10 +460,12 @@ onBeforeUnmount(() => {
|
||||
opacity: 0.08;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.04;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.08;
|
||||
transform: scale(0.98);
|
||||
|
||||
Reference in New Issue
Block a user