diff --git a/build/afterPack.js b/build/afterPack.js new file mode 100644 index 0000000..eca6ad0 --- /dev/null +++ b/build/afterPack.js @@ -0,0 +1,65 @@ +const path = require('path'); +const { execSync } = require('child_process'); +const fs = require('fs'); +const os = require('os'); + +exports.default = async function(context) { + // 只在 Windows 上执行 + if (process.platform !== 'win32') { + return; + } + + console.log('Running afterPack hook to set icon...'); + + const appOutDir = context.appOutDir; + const productName = context.packager.appInfo.productName; + const exePath = path.join(appOutDir, `${productName}.exe`); + const iconPath = path.join(__dirname, 'icon.ico'); + + // rcedit 路径 + const userHome = os.homedir(); + const cacheDir = path.join(userHome, 'AppData', 'Local', 'electron-builder', 'Cache', 'winCodeSign'); + + // 查找 rcedit + let rceditPath = null; + if (fs.existsSync(cacheDir)) { + const dirs = fs.readdirSync(cacheDir); + for (const dir of dirs) { + const possiblePath = path.join(cacheDir, dir, 'rcedit-x64.exe'); + if (fs.existsSync(possiblePath)) { + rceditPath = possiblePath; + break; + } + } + } + + if (!rceditPath) { + console.warn('rcedit not found, skipping icon modification'); + return; + } + + if (!fs.existsSync(exePath)) { + console.warn(`Exe not found: ${exePath}`); + return; + } + + if (!fs.existsSync(iconPath)) { + console.warn(`Icon not found: ${iconPath}`); + return; + } + + try { + console.log(`Setting icon for: ${exePath}`); + console.log(`Using icon: ${iconPath}`); + console.log(`Using rcedit: ${rceditPath}`); + + execSync(`"${rceditPath}" "${exePath}" --set-icon "${iconPath}"`, { + stdio: 'inherit' + }); + + console.log('Icon set successfully!'); + } catch (error) { + console.error('Failed to set icon:', error.message); + } +}; + diff --git a/build/icon.ico b/build/icon.ico index 0a1b73f..628edfe 100644 Binary files a/build/icon.ico and b/build/icon.ico differ diff --git a/build/installer.nsh b/build/installer.nsh index 8aa8c7d..45f91ba 100644 --- a/build/installer.nsh +++ b/build/installer.nsh @@ -5,9 +5,13 @@ !macro customInstall ; 安装完成后的自定义操作 + ; 删除默认创建的桌面快捷方式,然后重新创建带正确图标的快捷方式 + Delete "$DESKTOP\PHPer开发环境管理器.lnk" + CreateShortCut "$DESKTOP\PHPer开发环境管理器.lnk" "$INSTDIR\PHPer开发环境管理器.exe" "" "$INSTDIR\PHPer开发环境管理器.exe" 0 !macroend !macro customUnInstall ; 卸载时的自定义操作 + Delete "$DESKTOP\PHPer开发环境管理器.lnk" !macroend diff --git a/electron/main.ts b/electron/main.ts index 8259465..76ad0fe 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -235,26 +235,55 @@ function createTray() { }); } -app.whenReady().then(async () => { - createTray(); - createWindow(); +// 单实例锁定 +const gotTheLock = app.requestSingleInstanceLock(); - app.on("activate", () => { - if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); +if (!gotTheLock) { + // 如果获取不到锁,说明已有实例在运行,退出当前实例 + app.quit(); +} else { + // 当第二个实例启动时,聚焦到第一个实例的窗口 + app.on("second-instance", () => { + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } + mainWindow.show(); + mainWindow.focus(); } }); -}); -// 不要在所有窗口关闭时退出,保持托盘运行 -app.on("window-all-closed", () => { - // 什么都不做,保持后台运行 -}); + app.whenReady().then(async () => { + createTray(); + createWindow(); -// 真正退出前清理 -app.on("before-quit", () => { - isQuitting = true; -}); + // 根据配置自动启动服务 + try { + const result = await serviceManager.startAutoStartServices(); + if (result.details.length > 0) { + console.log("自动启动服务:", result.details.join(", ")); + } + } catch (error) { + console.error("自动启动服务失败:", error); + } + + app.on("activate", () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); + }); + + // 不要在所有窗口关闭时退出,保持托盘运行 + app.on("window-all-closed", () => { + // 什么都不做,保持后台运行 + }); + + // 真正退出前清理 + app.on("before-quit", () => { + isQuitting = true; + }); +} // ==================== IPC 处理程序 ==================== diff --git a/electron/services/ConfigStore.ts b/electron/services/ConfigStore.ts index f134579..e5d9366 100644 --- a/electron/services/ConfigStore.ts +++ b/electron/services/ConfigStore.ts @@ -16,6 +16,9 @@ interface ConfigSchema { redis: boolean; }; sites: SiteConfig[]; + // 应用设置 + autoLaunch: boolean; + startMinimized: boolean; } export interface SiteConfig { @@ -60,6 +63,9 @@ export class ConfigStore { redis: false, }, sites: [], + // 应用设置默认值 + autoLaunch: false, + startMinimized: false, }, }); diff --git a/electron/services/ServiceManager.ts b/electron/services/ServiceManager.ts index 8222554..b83d344 100644 --- a/electron/services/ServiceManager.ts +++ b/electron/services/ServiceManager.ts @@ -158,6 +158,71 @@ export class ServiceManager { return this.checkAutoStart(service) } + /** + * 根据配置启动设置为自启动的服务 + */ + async startAutoStartServices(): Promise<{ success: boolean; message: string; details: string[] }> { + const details: string[] = [] + const autoStart = this.configStore.get('autoStart') + + try { + // 检查 Nginx 自启动 + if (autoStart.nginx) { + const nginxPath = this.configStore.getNginxPath() + if (existsSync(join(nginxPath, 'nginx.exe'))) { + if (!(await this.checkProcess('nginx.exe'))) { + await this.startProcess(join(nginxPath, 'nginx.exe'), [], nginxPath) + details.push('Nginx 已自动启动') + } + } + } + + // 检查 MySQL 自启动 + if (autoStart.mysql) { + const mysqlVersions = this.configStore.get('mysqlVersions') + if (mysqlVersions.length > 0) { + if (!(await this.checkProcess('mysqld.exe'))) { + for (const version of mysqlVersions) { + const mysqlPath = this.configStore.getMysqlPath(version) + const mysqld = join(mysqlPath, 'bin', 'mysqld.exe') + if (existsSync(mysqld)) { + // 使用 VBScript 隐藏窗口启动 + const vbsPath = join(mysqlPath, 'start_mysql.vbs') + const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run """${mysqld}"" --defaults-file=""${join(mysqlPath, 'my.ini')}""", 0, False` + writeFileSync(vbsPath, vbsContent) + await execAsync(`cscript //nologo "${vbsPath}"`, { cwd: mysqlPath }) + details.push(`MySQL ${version} 已自动启动`) + break // 只启动第一个版本 + } + } + } + } + } + + // 检查 Redis 自启动 + if (autoStart.redis) { + const redisPath = this.configStore.getRedisPath() + const redisServer = join(redisPath, 'redis-server.exe') + if (existsSync(redisServer)) { + if (!(await this.checkProcess('redis-server.exe'))) { + const configFile = join(redisPath, 'redis.windows.conf') + const args = existsSync(configFile) ? ['redis.windows.conf'] : [] + await this.startProcess(redisServer, args, redisPath) + details.push('Redis 已自动启动') + } + } + } + + if (details.length === 0) { + return { success: true, message: '没有需要自动启动的服务', details } + } + + return { success: true, message: '自动启动服务完成', details } + } catch (error: any) { + return { success: false, message: `自动启动失败: ${error.message}`, details } + } + } + /** * 启动所有已安装的服务 */ diff --git a/package.json b/package.json index 41412aa..917b016 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "productName": "PHPer开发环境管理器", "copyright": "Copyright © 2024 PHPer", "icon": "build/icon.ico", + "afterPack": "./build/afterPack.js", "directories": { "output": "release", "buildResources": "build" @@ -67,8 +68,7 @@ } ], "icon": "build/icon.ico", - "requestedExecutionLevel": "requireAdministrator", - "signAndEditExecutable": false + "requestedExecutionLevel": "requireAdministrator" }, "nsis": { "oneClick": false, diff --git a/public/icon.ico b/public/icon.ico index 0a1b73f..628edfe 100644 Binary files a/public/icon.ico and b/public/icon.ico differ