Update application icon, enhance settings for auto-launch and service management, and improve tray functionality in the Electron app

This commit is contained in:
ethanfly 2025-12-26 04:00:31 +08:00
parent 9103faec32
commit 7737a06290
9 changed files with 324 additions and 19 deletions

View File

@ -1,7 +1,7 @@
# PHPer 开发环境管理器
<p align="center">
<img src="public/favicon.svg" alt="PHPer Logo" width="120" height="120">
<img src="public/icon.ico" alt="PHPer Logo" width="120" height="120">
</p>
<p align="center">
@ -168,7 +168,7 @@ phper/
│ └── Settings.vue # 设置
├── public/ # 静态资源
│ └── favicon.svg # 应用图标
│ └── icon.ico # 应用图标
├── index.html # HTML 模板
├── package.json # 项目配置

View File

@ -1,4 +1,4 @@
import { app, BrowserWindow, ipcMain, shell } from 'electron'
import { app, BrowserWindow, ipcMain, shell, Tray, Menu, nativeImage } from 'electron'
import { join } from 'path'
import { PhpManager } from './services/PhpManager'
import { MysqlManager } from './services/MysqlManager'
@ -10,6 +10,8 @@ import { HostsManager } from './services/HostsManager'
import { ConfigStore } from './services/ConfigStore'
let mainWindow: BrowserWindow | null = null
let tray: Tray | null = null
let isQuitting = false
// 发送下载进度到渲染进程
export function sendDownloadProgress(type: string, progress: number, downloaded: number, total: number) {
@ -46,7 +48,8 @@ function createWindow() {
height: 40
},
frame: false,
icon: join(__dirname, '../public/icon.ico')
icon: join(__dirname, '../public/icon.ico'),
show: false // 先不显示,等 ready-to-show
})
// 开发环境加载 Vite 开发服务器
@ -58,14 +61,112 @@ function createWindow() {
mainWindow.loadFile(join(__dirname, '../dist/index.html'))
}
// 窗口准备好后显示
mainWindow.once('ready-to-show', () => {
// 检查是否开机自启且静默启动
const startMinimized = configStore.get('startMinimized')
if (!startMinimized) {
mainWindow?.show()
}
})
// 关闭按钮改为最小化到托盘
mainWindow.on('close', (event) => {
if (!isQuitting) {
event.preventDefault()
mainWindow?.hide()
}
})
mainWindow.on('closed', () => {
mainWindow = null
})
}
app.whenReady().then(() => {
// 创建系统托盘
function createTray() {
// 创建托盘图标
const iconPath = join(__dirname, '../public/favicon.svg')
let trayIcon
try {
trayIcon = nativeImage.createFromPath(iconPath)
if (trayIcon.isEmpty()) {
// 如果 SVG 无法加载,创建一个简单的图标
trayIcon = nativeImage.createEmpty()
}
} catch (e) {
trayIcon = nativeImage.createEmpty()
}
tray = new Tray(trayIcon)
tray.setToolTip('PHPer 开发环境管理器')
// 创建托盘菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '显示主窗口',
click: () => {
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
} else {
createWindow()
}
}
},
{ type: 'separator' },
{
label: '启动全部服务',
click: async () => {
const result = await serviceManager.startAll()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('service-status-changed')
}
}
},
{
label: '停止全部服务',
click: async () => {
const result = await serviceManager.stopAll()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('service-status-changed')
}
}
},
{ type: 'separator' },
{
label: '退出',
click: () => {
isQuitting = true
app.quit()
}
}
])
tray.setContextMenu(contextMenu)
// 双击托盘图标显示窗口
tray.on('double-click', () => {
if (mainWindow) {
mainWindow.show()
mainWindow.focus()
} else {
createWindow()
}
})
}
app.whenReady().then(async () => {
createTray()
createWindow()
// 检查是否启用开机自启且自动启动服务
const autoStartServices = configStore.get('autoStartServicesOnBoot')
if (autoStartServices) {
await serviceManager.startAll()
}
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
@ -73,10 +174,14 @@ app.whenReady().then(() => {
})
})
// 不要在所有窗口关闭时退出,保持托盘运行
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
// 什么都不做,保持后台运行
})
// 真正退出前清理
app.on('before-quit', () => {
isQuitting = true
})
// ==================== IPC 处理程序 ====================
@ -194,3 +299,48 @@ ipcMain.handle('config:set', (_, key: string, value: any) => configStore.set(key
ipcMain.handle('config:getBasePath', () => configStore.getBasePath())
ipcMain.handle('config:setBasePath', (_, path: string) => configStore.setBasePath(path))
// ==================== 应用设置 ====================
// 设置开机自启
ipcMain.handle('app:setAutoLaunch', async (_, enabled: boolean) => {
app.setLoginItemSettings({
openAtLogin: enabled,
openAsHidden: true, // 静默启动
args: ['--hidden']
})
configStore.set('autoLaunch', enabled)
return { success: true, message: enabled ? '已启用开机自启' : '已禁用开机自启' }
})
// 获取开机自启状态
ipcMain.handle('app:getAutoLaunch', () => {
return app.getLoginItemSettings().openAtLogin
})
// 设置启动时最小化到托盘
ipcMain.handle('app:setStartMinimized', (_, enabled: boolean) => {
configStore.set('startMinimized', enabled)
return { success: true }
})
// 获取启动时最小化状态
ipcMain.handle('app:getStartMinimized', () => {
return configStore.get('startMinimized') || false
})
// 设置开机自启时自动启动服务
ipcMain.handle('app:setAutoStartServices', (_, enabled: boolean) => {
configStore.set('autoStartServicesOnBoot', enabled)
return { success: true }
})
// 获取自动启动服务状态
ipcMain.handle('app:getAutoStartServices', () => {
return configStore.get('autoStartServicesOnBoot') || false
})
// 真正退出应用
ipcMain.handle('app:quit', () => {
isQuitting = true
app.quit()
})

View File

@ -123,6 +123,25 @@ contextBridge.exposeInMainWorld('electronAPI', {
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value),
getBasePath: () => ipcRenderer.invoke('config:getBasePath'),
setBasePath: (path: string) => ipcRenderer.invoke('config:setBasePath', path)
},
// 应用设置
app: {
setAutoLaunch: (enabled: boolean) => ipcRenderer.invoke('app:setAutoLaunch', enabled),
getAutoLaunch: () => ipcRenderer.invoke('app:getAutoLaunch'),
setStartMinimized: (enabled: boolean) => ipcRenderer.invoke('app:setStartMinimized', enabled),
getStartMinimized: () => ipcRenderer.invoke('app:getStartMinimized'),
setAutoStartServices: (enabled: boolean) => ipcRenderer.invoke('app:setAutoStartServices', enabled),
getAutoStartServices: () => ipcRenderer.invoke('app:getAutoStartServices'),
quit: () => ipcRenderer.invoke('app:quit')
},
// 监听服务状态变化
onServiceStatusChanged: (callback: () => void) => {
ipcRenderer.on('service-status-changed', callback)
},
removeServiceStatusChangedListener: (callback: () => void) => {
ipcRenderer.removeListener('service-status-changed', callback)
}
})

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/x-icon" href="/icon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PHPer 开发环境管理器</title>
</head>
@ -11,4 +11,3 @@
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -56,9 +56,16 @@
"arch": [
"x64"
]
},
{
"target": "portable",
"arch": [
"x64"
]
}
],
"requestedExecutionLevel": "requireAdministrator"
"requestedExecutionLevel": "requireAdministrator",
"signAndEditExecutable": false
},
"nsis": {
"oneClick": false,

BIN
public/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -8,6 +8,56 @@
<p class="page-description">配置应用程序和服务设置</p>
</div>
<!-- 应用设置 -->
<div class="card">
<div class="card-header">
<span class="card-title">
<el-icon><Monitor /></el-icon>
应用设置
</span>
</div>
<div class="card-content">
<div class="setting-item">
<div class="setting-info">
<h4 class="setting-title">开机自动启动</h4>
<p class="setting-description">Windows 启动时自动运行 PHPer 开发环境管理器</p>
</div>
<div class="setting-action">
<el-switch
v-model="appSettings.autoLaunch"
@change="(val) => toggleAutoLaunch(val as boolean)"
/>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<h4 class="setting-title">启动时最小化到托盘</h4>
<p class="setting-description">开机自启时不弹出主窗口直接在系统托盘后台运行</p>
</div>
<div class="setting-action">
<el-switch
v-model="appSettings.startMinimized"
@change="(val) => toggleStartMinimized(val as boolean)"
:disabled="!appSettings.autoLaunch"
/>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<h4 class="setting-title">启动时自动运行所有服务</h4>
<p class="setting-description">开机自启时自动启动 NginxMySQLRedis 等服务</p>
</div>
<div class="setting-action">
<el-switch
v-model="appSettings.autoStartServices"
@change="(val) => toggleAutoStartServices(val as boolean)"
:disabled="!appSettings.autoLaunch"
/>
</div>
</div>
</div>
</div>
<!-- 基础设置 -->
<div class="card">
<div class="card-header">
@ -33,13 +83,14 @@
</div>
</div>
<!-- 开机自启动 -->
<!-- 服务开机自启动 -->
<div class="card">
<div class="card-header">
<span class="card-title">
<el-icon><Timer /></el-icon>
开机自启动
服务开机自启动
</span>
<span class="card-subtitle">独立于应用的 Windows 服务自启</span>
</div>
<div class="card-content">
<div class="setting-item" v-for="service in services" :key="service.name">
@ -75,7 +126,7 @@
<h2 class="app-title">PHPer 开发环境管理器</h2>
<p class="app-version">版本 1.0.0</p>
<p class="app-desc">
一站式 PHP 开发环境管理工具支持 PHPMySQLNginxRedis 的安装和管理
一站式 PHP 开发环境管理工具支持 PHPMySQLNginxRedisNode.js 的安装和管理
</p>
</div>
</div>
@ -92,6 +143,9 @@
<el-button @click="openLink('https://redis.io/')">
Redis 官网
</el-button>
<el-button @click="openLink('https://nodejs.org/')">
Node.js 官网
</el-button>
</div>
</div>
</div>
@ -102,6 +156,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Monitor } from '@element-plus/icons-vue'
interface ServiceAutoStart {
name: string
@ -110,19 +165,35 @@ interface ServiceAutoStart {
autoStart: boolean
}
interface AppSettings {
autoLaunch: boolean
startMinimized: boolean
autoStartServices: boolean
}
const basePath = ref('')
const appSettings = reactive<AppSettings>({
autoLaunch: false,
startMinimized: false,
autoStartServices: false
})
const services = reactive<ServiceAutoStart[]>([
{ name: 'nginx', displayName: 'Nginx', description: '开机时自动启动 Nginx 服务', autoStart: false },
{ name: 'mysql', displayName: 'MySQL', description: '开机时自动启动 MySQL 服务', autoStart: false },
{ name: 'redis', displayName: 'Redis', description: '开机时自动启动 Redis 服务', autoStart: false },
{ name: 'php-cgi', displayName: 'PHP-CGI', description: '开机时自动启动 PHP FastCGI 进程', autoStart: false }
{ name: 'nginx', displayName: 'Nginx', description: '开机时自动启动 Nginx 服务(独立于应用)', autoStart: false },
{ name: 'mysql', displayName: 'MySQL', description: '开机时自动启动 MySQL 服务(独立于应用)', autoStart: false },
{ name: 'redis', displayName: 'Redis', description: '开机时自动启动 Redis 服务(独立于应用)', autoStart: false }
])
const loadSettings = async () => {
try {
basePath.value = await window.electronAPI?.config.getBasePath() || ''
//
//
appSettings.autoLaunch = await window.electronAPI?.app?.getAutoLaunch() || false
appSettings.startMinimized = await window.electronAPI?.app?.getStartMinimized() || false
appSettings.autoStartServices = await window.electronAPI?.app?.getAutoStartServices() || false
//
for (const service of services) {
service.autoStart = await window.electronAPI?.service.getAutoStart(service.name) || false
}
@ -131,6 +202,46 @@ const loadSettings = async () => {
}
}
const toggleAutoLaunch = async (enabled: boolean) => {
try {
const result = await window.electronAPI?.app?.setAutoLaunch(enabled)
if (result?.success) {
ElMessage.success(result.message)
if (!enabled) {
//
appSettings.startMinimized = false
appSettings.autoStartServices = false
}
} else {
ElMessage.error(result?.message || '设置失败')
appSettings.autoLaunch = !enabled
}
} catch (error: any) {
ElMessage.error(error.message)
appSettings.autoLaunch = !enabled
}
}
const toggleStartMinimized = async (enabled: boolean) => {
try {
await window.electronAPI?.app?.setStartMinimized(enabled)
ElMessage.success(enabled ? '已启用启动时最小化' : '已禁用启动时最小化')
} catch (error: any) {
ElMessage.error(error.message)
appSettings.startMinimized = !enabled
}
}
const toggleAutoStartServices = async (enabled: boolean) => {
try {
await window.electronAPI?.app?.setAutoStartServices(enabled)
ElMessage.success(enabled ? '已启用自动启动服务' : '已禁用自动启动服务')
} catch (error: any) {
ElMessage.error(error.message)
appSettings.autoStartServices = !enabled
}
}
const toggleAutoStart = async (name: string, enabled: boolean) => {
try {
const result = await window.electronAPI?.service.setAutoStart(name, enabled)
@ -163,6 +274,12 @@ onMounted(() => {
</script>
<style lang="scss" scoped>
.card-subtitle {
font-size: 12px;
color: var(--text-muted);
margin-left: 12px;
}
.setting-item {
display: flex;
align-items: center;

13
src/vite-env.d.ts vendored
View File

@ -105,6 +105,19 @@ interface Window {
getBasePath: () => Promise<string>
setBasePath: (path: string) => Promise<void>
}
app: {
setAutoLaunch: (enabled: boolean) => Promise<{ success: boolean; message: string }>
getAutoLaunch: () => Promise<boolean>
setStartMinimized: (enabled: boolean) => Promise<{ success: boolean }>
getStartMinimized: () => Promise<boolean>
setAutoStartServices: (enabled: boolean) => Promise<{ success: boolean }>
getAutoStartServices: () => Promise<boolean>
quit: () => Promise<void>
}
onServiceStatusChanged: (callback: () => void) => void
removeServiceStatusChangedListener: (callback: () => void) => void
}
}