feat: 完成记忆窗口位置,最小化到托盘和托盘区图标

This commit is contained in:
2025-09-05 00:27:37 +08:00
parent 1ffd8c1a5a
commit b9a7b6a889
4 changed files with 394 additions and 32 deletions

View File

@@ -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)
}
}
})

View File

@@ -35,6 +35,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
saveConfig: (config: any) => ipcRenderer.invoke('save-config', config),
loadConfig: () => ipcRenderer.invoke('load-config'),
resetConfig: () => ipcRenderer.invoke('reset-config'),
// 托盘设置实时更新
updateTraySettings: (uiSettings: any) => ipcRenderer.invoke('update-tray-settings', uiSettings),
// 日志文件操作
getLogPath: () => ipcRenderer.invoke('get-log-path'),

View File

@@ -40,8 +40,8 @@ export interface SettingsData {
IfSelfStart: boolean
}
UI: {
IfShowTray: boolean
IfToTray: boolean
IfShowTray: boolean // 常驻显示托盘
IfToTray: boolean // 最小化到托盘
location: string
maximized: boolean
size: string

View File

@@ -4,13 +4,11 @@ import { useRouter } from 'vue-router'
import {
Button,
Card,
Divider,
Radio,
Select,
Space,
Switch,
Tabs,
InputNumber,
Input,
message,
} from 'ant-design-vue'
@@ -172,6 +170,17 @@ const saveSettings = async (category: keyof SettingsData, changes: any) => {
const handleSettingChange = async (category: keyof SettingsData, key: string, value: any) => {
const changes = { [key]: value }
await saveSettings(category, changes)
// 如果是UI设置的托盘相关配置立即更新托盘状态
if (category === 'UI' && (key === 'IfShowTray' || key === 'IfToTray')) {
try {
if ((window as any).electronAPI && (window as any).electronAPI.updateTraySettings) {
await (window as any).electronAPI.updateTraySettings({ [key]: value })
}
} catch (error) {
console.error('更新托盘设置失败:', error)
}
}
}
const handleThemeModeChange = (e: any) => {
@@ -570,20 +579,21 @@ onMounted(() => {
<Space direction="vertical" size="large" style="width: 100%">
<div class="setting-item">
<h3>系统托盘</h3>
<p class="setting-description">配置系统托盘的显示和行为</p>
<Space direction="vertical" size="middle">
<div class="switch-item">
<Switch
v-model:checked="settings.UI.IfShowTray"
@change="checked => handleSettingChange('UI', 'IfShowTray', checked)"
/>
<span class="switch-label">显示系统托盘图标</span>
<span class="switch-label">常驻显示托盘图标</span>
</div>
<div class="switch-item">
<Switch
v-model:checked="settings.UI.IfToTray"
@change="checked => handleSettingChange('UI', 'IfToTray', checked)"
/>
<span class="switch-label">关闭时最小化到托盘</span>
<span class="switch-label">最小化到托盘</span>
</div>
</Space>
</div>