@@ -1,4 +1,4 @@
import { app , BrowserWindow , ipcMain , dialog , shell } from 'electron'
import { app , BrowserWindow , ipcMain , dialog , shell , Tray , Menu , nativeImage } from 'electron'
import * as path from 'path'
import * as fs from 'fs'
import { spawn } from 'child_process'
@@ -59,16 +59,232 @@ function restartAsAdmin(): void {
}
let mainWindow : BrowserWindow | null = null
let tray : Tray | null = null
let isQuitting = false
let saveWindowStateTimeout : NodeJS.Timeout | null = null
// 配置接口
interface AppConfig {
UI : {
IfShowTray : boolean
IfToTray : boolean
location : string
maximized : boolean
size : string
}
Start : {
IfMinimizeDirectly : boolean
IfSelfStart : boolean
}
[ key : string ] : any
}
// 默认配置
const defaultConfig : AppConfig = {
UI : {
IfShowTray : false ,
IfToTray : false ,
location : '100,100' ,
maximized : false ,
size : '1600,1000'
} ,
Start : {
IfMinimizeDirectly : false ,
IfSelfStart : false
}
}
// 加载配置
function loadConfig ( ) : AppConfig {
try {
const appRoot = getAppRoot ( )
const configPath = path . join ( appRoot , 'config' , 'frontend_config.json' )
if ( fs . existsSync ( configPath ) ) {
const configData = fs . readFileSync ( configPath , 'utf8' )
const config = JSON . parse ( configData )
return { . . . defaultConfig , . . . config }
}
} catch ( error ) {
log . error ( '加载配置失败:' , error )
}
return defaultConfig
}
// 保存配置
function saveConfig ( config : AppConfig ) {
try {
const appRoot = getAppRoot ( )
const configDir = path . join ( appRoot , 'config' )
const configPath = path . join ( configDir , 'frontend_config.json' )
if ( ! fs . existsSync ( configDir ) ) {
fs . mkdirSync ( configDir , { recursive : true } )
}
fs . writeFileSync ( configPath , JSON . stringify ( config , null , 2 ) , 'utf8' )
} catch ( error ) {
log . error ( '保存配置失败:' , error )
}
}
// 创建托盘
function createTray() {
if ( tray ) return
// 尝试多个可能的图标路径
const iconPaths = [
path . join ( __dirname , '../public/AUTO-MAS.ico' ) ,
path . join ( process . resourcesPath , 'assets/AUTO-MAS.ico' ) ,
path . join ( app . getAppPath ( ) , 'public/AUTO-MAS.ico' ) ,
path . join ( app . getAppPath ( ) , 'dist/AUTO-MAS.ico' )
]
let trayIcon
try {
// 尝试加载图标
for ( const iconPath of iconPaths ) {
if ( fs . existsSync ( iconPath ) ) {
trayIcon = nativeImage . createFromPath ( iconPath )
if ( ! trayIcon . isEmpty ( ) ) {
log . info ( ` 成功加载托盘图标: ${ iconPath } ` )
break
}
}
}
// 如果所有路径都失败,创建一个默认图标
if ( ! trayIcon || trayIcon . isEmpty ( ) ) {
log . warn ( '无法加载托盘图标,使用默认图标' )
trayIcon = nativeImage . createEmpty ( )
}
} catch ( error ) {
log . error ( '加载托盘图标失败:' , error )
trayIcon = nativeImage . createEmpty ( )
}
tray = new Tray ( trayIcon )
const contextMenu = Menu . buildFromTemplate ( [
{
label : '显示窗口' ,
click : ( ) = > {
if ( mainWindow ) {
if ( mainWindow . isMinimized ( ) ) {
mainWindow . restore ( )
}
mainWindow . setSkipTaskbar ( false ) // 恢复任务栏图标
mainWindow . show ( )
mainWindow . focus ( )
}
}
} ,
{
label : '隐藏窗口' ,
click : ( ) = > {
if ( mainWindow ) {
const currentConfig = loadConfig ( )
if ( currentConfig . UI . IfToTray ) {
mainWindow . setSkipTaskbar ( true ) // 隐藏任务栏图标
}
mainWindow . hide ( )
}
}
} ,
{ type : 'separator' } ,
{
label : '退出' ,
click : ( ) = > {
isQuitting = true
app . quit ( )
}
}
] )
tray . setContextMenu ( contextMenu )
tray . setToolTip ( 'AUTO_MAA' )
// 双击托盘图标显示/隐藏窗口
tray . on ( 'double-click' , ( ) = > {
if ( mainWindow ) {
const currentConfig = loadConfig ( )
if ( mainWindow . isVisible ( ) ) {
if ( currentConfig . UI . IfToTray ) {
mainWindow . setSkipTaskbar ( true ) // 隐藏任务栏图标
}
mainWindow . hide ( )
} else {
if ( mainWindow . isMinimized ( ) ) {
mainWindow . restore ( )
}
mainWindow . setSkipTaskbar ( false ) // 恢复任务栏图标
mainWindow . show ( )
mainWindow . focus ( )
}
}
} )
}
// 销毁托盘
function destroyTray() {
if ( tray ) {
tray . destroy ( )
tray = null
}
}
// 更新托盘状态
function updateTrayVisibility ( config : AppConfig ) {
// 根据需求逻辑判断是否应该显示托盘
let shouldShowTray = false
if ( config . UI . IfShowTray && config . UI . IfToTray ) {
// 勾选常驻显示托盘和最小化到托盘,就一直展示托盘
shouldShowTray = true
} else if ( config . UI . IfShowTray && ! config . UI . IfToTray ) {
// 勾选常驻显示托盘但没有最小化到托盘,就一直展示托盘
shouldShowTray = true
} else if ( ! config . UI . IfShowTray && config . UI . IfToTray ) {
// 没有常驻显示托盘但勾选最小化到托盘,有窗口时就只有窗口,最小化后任务栏消失,只有托盘
shouldShowTray = ! mainWindow || ! mainWindow . isVisible ( )
} else {
// 没有常驻显示托盘也没有最小化到托盘,托盘一直不展示
shouldShowTray = false
}
// 特殊情况:如果没有窗口显示且没有托盘,强制显示托盘避免程序成为幽灵
if ( ! shouldShowTray && ( ! mainWindow || ! mainWindow . isVisible ( ) ) && ! tray ) {
shouldShowTray = true
log . warn ( '防幽灵机制:强制显示托盘图标' )
}
if ( shouldShowTray && ! tray ) {
createTray ( )
log . info ( '托盘图标已创建' )
} else if ( ! shouldShowTray && tray ) {
destroyTray ( )
log . info ( '托盘图标已销毁' )
}
}
function createWindow() {
log . info ( '开始创建主窗口' )
const config = loadConfig ( )
// 解析窗口大小
const [ width , height ] = config . UI . size . split ( ',' ) . map ( s = > parseInt ( s . trim ( ) ) || 1600 )
const [ x , y ] = config . UI . location . split ( ',' ) . map ( s = > parseInt ( s . trim ( ) ) || 100 )
mainWindow = new BrowserWindow ( {
width : 1600 ,
height : 1000 ,
width : Math.max ( width , 800 ) ,
height : Math.max ( height , 600 ) ,
x ,
y ,
minWidth : 800 ,
minHeight : 600 ,
icon : path.join ( __dirname , '../src/assets /AUTO-MAS.ico' ) ,
icon : path.join ( __dirname , '../public /AUTO-MAS.ico' ) ,
frame : false , // 去掉系统标题栏
titleBarStyle : 'hidden' , // 隐藏标题栏
webPreferences : {
@@ -77,8 +293,14 @@ function createWindow() {
contextIsolation : true ,
} ,
autoHideMenuBar : true ,
show : ! config . Start . IfMinimizeDirectly , // 根据配置决定是否直接显示
} )
// 如果配置为最大化,则最大化窗口
if ( config . UI . maximized ) {
mainWindow . maximize ( )
}
mainWindow . setMenuBarVisibility ( false )
const devServer = process . env . VITE_DEV_SERVER_URL
if ( devServer ) {
@@ -90,11 +312,72 @@ function createWindow() {
mainWindow . loadFile ( indexHtmlPath )
}
// 窗口事件处理
mainWindow . on ( 'close' , ( event ) = > {
const currentConfig = loadConfig ( )
if ( ! isQuitting && currentConfig . UI . IfToTray ) {
// 如果启用了最小化到托盘,阻止关闭并隐藏窗口
event . preventDefault ( )
mainWindow ? . hide ( )
mainWindow ? . setSkipTaskbar ( true )
// 更新托盘状态
updateTrayVisibility ( currentConfig )
log . info ( '窗口已最小化到托盘,任务栏图标已隐藏' )
} else {
// 保存窗口状态
saveWindowState ( )
}
} )
mainWindow . on ( 'closed' , ( ) = > {
log . info ( '主窗口已关闭' )
mainWindow = null
} )
// 窗口最小化事件
mainWindow . on ( 'minimize' , ( ) = > {
const currentConfig = loadConfig ( )
if ( currentConfig . UI . IfToTray ) {
// 如果启用了最小化到托盘,隐藏窗口并从任务栏移除
mainWindow ? . hide ( )
mainWindow ? . setSkipTaskbar ( true )
// 更新托盘状态
updateTrayVisibility ( currentConfig )
log . info ( '窗口已最小化到托盘,任务栏图标已隐藏' )
}
} )
// 窗口显示/隐藏事件,用于更新托盘状态
mainWindow . on ( 'show' , ( ) = > {
const currentConfig = loadConfig ( )
// 窗口显示时,恢复任务栏图标
mainWindow ? . setSkipTaskbar ( false )
updateTrayVisibility ( currentConfig )
log . info ( '窗口已显示,任务栏图标已恢复' )
} )
mainWindow . on ( 'hide' , ( ) = > {
const currentConfig = loadConfig ( )
// 窗口隐藏时,根据配置决定是否隐藏任务栏图标
if ( currentConfig . UI . IfToTray ) {
mainWindow ? . setSkipTaskbar ( true )
log . info ( '窗口已隐藏,任务栏图标已隐藏' )
}
updateTrayVisibility ( currentConfig )
} )
// 窗口移动和调整大小时保存状态
mainWindow . on ( 'moved' , saveWindowState )
mainWindow . on ( 'resized' , saveWindowState )
mainWindow . on ( 'maximize' , saveWindowState )
mainWindow . on ( 'unmaximize' , saveWindowState )
// 设置各个服务的主窗口引用
if ( mainWindow ) {
setDownloadMainWindow ( mainWindow )
@@ -102,6 +385,40 @@ function createWindow() {
setGitMainWindow ( mainWindow )
log . info ( '主窗口创建完成,服务引用已设置' )
}
// 根据配置初始化托盘
updateTrayVisibility ( config )
}
// 保存窗口状态(带防抖)
function saveWindowState() {
if ( ! mainWindow ) return
// 清除之前的定时器
if ( saveWindowStateTimeout ) {
clearTimeout ( saveWindowStateTimeout )
}
// 设置新的定时器, 500ms后保存
saveWindowStateTimeout = setTimeout ( ( ) = > {
try {
const config = loadConfig ( )
const bounds = mainWindow ! . getBounds ( )
const isMaximized = mainWindow ! . 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 )
}
} , 500 )
}
// IPC处理函数
@@ -158,7 +475,7 @@ ipcMain.handle('select-file', async (event, filters = []) => {
} )
// 在系统默认浏览器中打开URL
ipcMain . handle ( 'open-url' , async ( event , url : string ) = > {
ipcMain . handle ( 'open-url' , async ( _ event, url : string ) = > {
try {
await shell . openExternal ( url )
return { success : true }
@@ -180,7 +497,7 @@ ipcMain.handle('check-environment', async () => {
} )
// Python相关
ipcMain . handle ( 'download-python' , async ( event , mirror = 'tsinghua' ) = > {
ipcMain . handle ( 'download-python' , async ( _ event, mirror = 'tsinghua' ) = > {
const appRoot = getAppRoot ( )
return downloadPython ( appRoot , mirror )
} )
@@ -190,7 +507,7 @@ ipcMain.handle('install-pip', async () => {
return installPipPackage ( appRoot )
} )
ipcMain . handle ( 'install-dependencies' , async ( event , mirror = 'tsinghua' ) = > {
ipcMain . handle ( 'install-dependencies' , async ( _ event, mirror = 'tsinghua' ) = > {
const appRoot = getAppRoot ( )
return installDependencies ( appRoot , mirror )
} )
@@ -208,7 +525,7 @@ ipcMain.handle('download-git', async () => {
ipcMain . handle (
'clone-backend' ,
async ( event , repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git' ) = > {
async ( _ event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git' ) = > {
const appRoot = getAppRoot ( )
return cloneBackend ( appRoot , repoUrl )
}
@@ -216,14 +533,14 @@ ipcMain.handle(
ipcMain . handle (
'update-backend' ,
async ( event , repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git' ) = > {
async ( _ event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git' ) = > {
const appRoot = getAppRoot ( )
return cloneBackend ( appRoot , repoUrl ) // 使用相同的逻辑, 会自动判断是pull还是clone
}
)
// 配置文件操作
ipcMain . handle ( 'save-config' , async ( event , config ) = > {
ipcMain . handle ( 'save-config' , async ( _ event, config ) = > {
try {
const appRoot = getAppRoot ( )
const configDir = path . join ( appRoot , 'config' )
@@ -236,12 +553,36 @@ ipcMain.handle('save-config', async (event, config) => {
fs . writeFileSync ( configPath , JSON . stringify ( config , null , 2 ) , 'utf8' )
console . log ( ` 配置已保存到: ${ configPath } ` )
// 如果是UI配置更新, 需要更新托盘状态
if ( config . UI ) {
updateTrayVisibility ( config )
}
} catch ( error ) {
console . error ( '保存配置文件失败:' , error )
throw error
}
} )
// 新增: 实时更新托盘状态的IPC处理器
ipcMain . handle ( 'update-tray-settings' , async ( _event , uiSettings ) = > {
try {
// 先更新配置文件
const currentConfig = loadConfig ( )
currentConfig . UI = { . . . currentConfig . UI , . . . uiSettings }
saveConfig ( currentConfig )
// 立即更新托盘状态
updateTrayVisibility ( currentConfig )
log . info ( '托盘设置已更新:' , uiSettings )
return true
} catch ( error ) {
log . error ( '更新托盘设置失败:' , error )
throw error
}
} )
ipcMain . handle ( 'load-config' , async ( ) = > {
try {
const appRoot = getAppRoot ( )
@@ -285,7 +626,7 @@ ipcMain.handle('get-log-path', async () => {
}
} )
ipcMain . handle ( 'get-log-files' , async ( ) = > {
ipcMain . handle ( 'get-log-files' , async ( _event ) = > {
try {
return getLogFiles ( )
} catch ( error ) {
@@ -294,7 +635,7 @@ ipcMain.handle('get-log-files', async () => {
}
} )
ipcMain . handle ( 'get-logs' , async ( event , lines? : number , fileName? : string ) = > {
ipcMain . handle ( 'get-logs' , async ( _ event, lines? : number , fileName? : string ) = > {
try {
let logFilePath : string
@@ -325,7 +666,7 @@ ipcMain.handle('get-logs', async (event, lines?: number, fileName?: string) => {
}
} )
ipcMain . handle ( 'clear-logs' , async ( event , fileName? : string ) = > {
ipcMain . handle ( 'clear-logs' , async ( _ event, fileName? : string ) = > {
try {
let logFilePath : string
@@ -348,7 +689,7 @@ ipcMain.handle('clear-logs', async (event, fileName?: string) => {
}
} )
ipcMain . handle ( 'clean-old-logs' , async ( event , daysToKeep = 7 ) = > {
ipcMain . handle ( 'clean-old-logs' , async ( _ event, daysToKeep = 7 ) = > {
try {
cleanOldLogs ( daysToKeep )
log . info ( ` 已清理 ${ daysToKeep } 天前的旧日志文件 ` )
@@ -359,7 +700,7 @@ ipcMain.handle('clean-old-logs', async (event, daysToKeep = 7) => {
} )
// 保留原有的日志操作方法以兼容现有代码
ipcMain . handle ( 'save-logs-to-file' , async ( event , logs : string ) = > {
ipcMain . handle ( 'save-logs-to-file' , async ( _ event, logs : string ) = > {
try {
const appRoot = getAppRoot ( )
const logsDir = path . join ( appRoot , 'logs' )
@@ -423,17 +764,25 @@ app.on('second-instance', () => {
app . on ( 'before-quit' , async event = > {
// 只处理一次,避免多重触发
event . preventDefault ( )
log . info ( '应用准备退出' )
try {
await stopBackend ( )
log . info ( '后端服务已停止 ' )
} catch ( e ) {
log . error ( '停止后端时出错:' , e )
console . error ( '停止后端时出错:' , e )
} finally {
log . info ( '应用退出' )
app . exit ( 0 )
if ( ! isQuitting ) {
event . preventDefault ( )
isQuitting = true
log . info ( '应用准备退出 ' )
// 清理托盘
destroyTray ( )
try {
await stopBackend ( )
log . info ( '后端服务已停止' )
} catch ( e ) {
log . error ( '停止后端时出错:' , e )
console . error ( '停止后端时出错:' , e )
} finally {
log . info ( '应用退出' )
app . exit ( 0 )
}
}
} )