diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000..0a1b73f Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..002e72b Binary files /dev/null and b/build/icon.png differ diff --git a/electron/main.ts b/electron/main.ts index 5e4da29..8259465 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -17,32 +17,42 @@ import { ServiceManager } from "./services/ServiceManager"; import { HostsManager } from "./services/HostsManager"; import { ConfigStore } from "./services/ConfigStore"; -// 获取托盘图标路径 (使用 PNG) -function getTrayIconPath(): string { +// 获取图标路径 +function getIconPath(filename: string): string { const { existsSync } = require("fs"); - const paths = [ - join(__dirname, "../public/icon.png"), - join(__dirname, "../dist/icon.png"), + + // 打包后的路径 + if (app.isPackaged) { + const paths = [ + join(process.resourcesPath, "public", filename), + join(process.resourcesPath, filename), + join(__dirname, "../public", filename), + ]; + for (const p of paths) { + if (existsSync(p)) return p; + } + } + + // 开发环境路径 + const devPaths = [ + join(__dirname, "../public", filename), + join(__dirname, "../dist", filename), ]; - for (const p of paths) { + for (const p of devPaths) { if (existsSync(p)) return p; } - return join(__dirname, "../public/icon.svg"); + + return join(__dirname, "../public/icon.ico"); } -// 获取窗口图标路径 (Windows 需要 PNG/ICO) +// 获取托盘图标路径 +function getTrayIconPath(): string { + return getIconPath("icon.ico"); +} + +// 获取窗口图标路径 function getWindowIconPath(): string { - const { existsSync } = require("fs"); - const paths = [ - join(__dirname, "../public/icon.png"), - join(__dirname, "../dist/icon.png"), - join(__dirname, "../public/icon.ico"), - join(__dirname, "../dist/icon.ico"), - ]; - for (const p of paths) { - if (existsSync(p)) return p; - } - return join(__dirname, "../public/icon.svg"); + return getIconPath("icon.ico"); } // 创建托盘图标 @@ -229,12 +239,6 @@ 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(); @@ -477,23 +481,79 @@ ipcMain.handle("config:setBasePath", (_, path: string) => ); // ==================== 应用设置 ==================== -// 设置开机自启 +// 设置开机自启(以管理员模式,使用任务计划程序) ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => { - app.setLoginItemSettings({ - openAtLogin: enabled, - openAsHidden: true, // 静默启动 - args: ["--hidden"], - }); - configStore.set("autoLaunch", enabled); - return { - success: true, - message: enabled ? "已启用开机自启" : "已禁用开机自启", - }; + const { execSync } = require("child_process"); + const exePath = app.getPath("exe"); + const taskName = "PHPerDevManager"; + + // 开发模式下不支持 + if (!app.isPackaged) { + return { + success: false, + message: "开发模式下不支持开机自启,请打包后使用", + }; + } + + try { + if (enabled) { + // 先删除可能存在的旧任务 + try { + execSync(`schtasks /delete /tn "${taskName}" /f`, { + encoding: "buffer", + windowsHide: true, + }); + } catch (e) { + // 忽略删除失败(可能任务不存在) + } + + // 创建任务计划程序任务,以最高权限运行 + const command = `schtasks /create /tn "${taskName}" /tr "\\"${exePath}\\"" /sc onlogon /rl highest /f`; + execSync(command, { encoding: "buffer", windowsHide: true }); + + configStore.set("autoLaunch", true); + return { success: true, message: "已启用开机自启(管理员模式)" }; + } else { + // 删除任务计划程序任务 + try { + execSync(`schtasks /delete /tn "${taskName}" /f`, { + encoding: "buffer", + windowsHide: true, + }); + } catch (e) { + // 忽略删除失败 + } + configStore.set("autoLaunch", false); + return { success: true, message: "已禁用开机自启" }; + } + } catch (error: any) { + console.error("任务计划操作失败:", error); + return { + success: false, + message: "操作失败,请确保应用以管理员身份运行", + }; + } }); // 获取开机自启状态 -ipcMain.handle("app:getAutoLaunch", () => { - return app.getLoginItemSettings().openAtLogin; +ipcMain.handle("app:getAutoLaunch", async () => { + const { execSync } = require("child_process"); + const taskName = "PHPerDevManager"; + + // 开发模式下返回 false + if (!app.isPackaged) { + return false; + } + + try { + execSync(`schtasks /query /tn "${taskName}"`, { + encoding: "buffer", + windowsHide: true, + }); + return true; + } catch (e) { + return false; + } }); // 设置启动时最小化到托盘 @@ -507,17 +567,6 @@ 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; diff --git a/electron/services/ConfigStore.ts b/electron/services/ConfigStore.ts index a83fe90..f134579 100644 --- a/electron/services/ConfigStore.ts +++ b/electron/services/ConfigStore.ts @@ -1,221 +1,222 @@ -import Store from 'electron-store' -import { join, dirname } from 'path' -import { existsSync, mkdirSync } from 'fs' -import { app } from 'electron' +import Store from "electron-store"; +import { join, dirname } from "path"; +import { existsSync, mkdirSync } from "fs"; +import { app } from "electron"; interface ConfigSchema { - basePath: string - phpVersions: string[] - mysqlVersions: string[] - nginxVersions: string[] - redisVersions: string[] - activePhpVersion: string + basePath: string; + phpVersions: string[]; + mysqlVersions: string[]; + nginxVersions: string[]; + redisVersions: string[]; + activePhpVersion: string; autoStart: { - nginx: boolean - mysql: boolean - redis: boolean - } - sites: SiteConfig[] + nginx: boolean; + mysql: boolean; + redis: boolean; + }; + sites: SiteConfig[]; } export interface SiteConfig { - name: string - domain: string - rootPath: string - phpVersion: string - isLaravel: boolean - ssl: boolean - enabled: boolean + name: string; + domain: string; + rootPath: string; + phpVersion: string; + isLaravel: boolean; + ssl: boolean; + enabled: boolean; } // 获取应用安装目录下的 data 路径 function getDefaultBasePath(): string { if (app.isPackaged) { // 生产环境:使用可执行文件所在目录下的 data 文件夹 - const exePath = app.getPath('exe') - const appDir = dirname(exePath) - return join(appDir, 'data') + const exePath = app.getPath("exe"); + const appDir = dirname(exePath); + return join(appDir, "data"); } else { // 开发环境:使用项目根目录下的 data 文件夹 - return join(process.cwd(), 'data') + return join(process.cwd(), "data"); } } export class ConfigStore { - private store: Store - private basePath: string + private store: Store; + private basePath: string; constructor() { this.store = new Store({ defaults: { - basePath: getDefaultBasePath(), + basePath: "", phpVersions: [], mysqlVersions: [], nginxVersions: [], redisVersions: [], - activePhpVersion: '', + activePhpVersion: "", autoStart: { nginx: false, mysql: false, - redis: false + redis: false, }, - sites: [] - } - }) + sites: [], + }, + }); - this.basePath = this.store.get('basePath') - this.ensureDirectories() + // 直接使用应用安装目录下的 data 路径 + this.basePath = getDefaultBasePath(); + this.store.set("basePath", this.basePath); + this.ensureDirectories(); } private ensureDirectories(): void { const dirs = [ this.basePath, - join(this.basePath, 'php'), - join(this.basePath, 'mysql'), - join(this.basePath, 'nginx'), - join(this.basePath, 'nginx', 'sites-available'), - join(this.basePath, 'nginx', 'sites-enabled'), - join(this.basePath, 'nginx', 'ssl'), - join(this.basePath, 'redis'), - join(this.basePath, 'logs'), - join(this.basePath, 'temp'), - join(this.basePath, 'www') - ] + join(this.basePath, "php"), + join(this.basePath, "mysql"), + join(this.basePath, "nginx"), + join(this.basePath, "nginx", "sites-available"), + join(this.basePath, "nginx", "sites-enabled"), + join(this.basePath, "nginx", "ssl"), + join(this.basePath, "redis"), + join(this.basePath, "logs"), + join(this.basePath, "temp"), + join(this.basePath, "www"), + ]; for (const dir of dirs) { if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }) + mkdirSync(dir, { recursive: true }); } } } get(key: K): ConfigSchema[K] { - return this.store.get(key) + return this.store.get(key); } set(key: K, value: ConfigSchema[K]): void { - this.store.set(key, value) + this.store.set(key, value); } getBasePath(): string { - return this.basePath + return this.basePath; } setBasePath(path: string): void { - this.basePath = path - this.store.set('basePath', path) - this.ensureDirectories() + this.basePath = path; + this.store.set("basePath", path); + this.ensureDirectories(); } getPhpPath(version: string): string { - return join(this.basePath, 'php', `php-${version}`) + return join(this.basePath, "php", `php-${version}`); } getMysqlPath(version: string): string { - return join(this.basePath, 'mysql', `mysql-${version}`) + return join(this.basePath, "mysql", `mysql-${version}`); } getNginxPath(): string { - return join(this.basePath, 'nginx') + return join(this.basePath, "nginx"); } getRedisPath(): string { - return join(this.basePath, 'redis') + return join(this.basePath, "redis"); } getNodePath(): string { - return join(this.basePath, 'nodejs') + return join(this.basePath, "nodejs"); } getLogsPath(): string { - return join(this.basePath, 'logs') + return join(this.basePath, "logs"); } getTempPath(): string { - return join(this.basePath, 'temp') + return join(this.basePath, "temp"); } getWwwPath(): string { - return join(this.basePath, 'www') + return join(this.basePath, "www"); } getSitesAvailablePath(): string { - return join(this.basePath, 'nginx', 'sites-available') + return join(this.basePath, "nginx", "sites-available"); } getSitesEnabledPath(): string { - return join(this.basePath, 'nginx', 'sites-enabled') + return join(this.basePath, "nginx", "sites-enabled"); } getSSLPath(): string { - return join(this.basePath, 'nginx', 'ssl') + return join(this.basePath, "nginx", "ssl"); } addPhpVersion(version: string): void { - const versions = this.store.get('phpVersions') + const versions = this.store.get("phpVersions"); if (!versions.includes(version)) { - versions.push(version) - this.store.set('phpVersions', versions) + versions.push(version); + this.store.set("phpVersions", versions); } } removePhpVersion(version: string): void { - const versions = this.store.get('phpVersions') - const index = versions.indexOf(version) + const versions = this.store.get("phpVersions"); + const index = versions.indexOf(version); if (index > -1) { - versions.splice(index, 1) - this.store.set('phpVersions', versions) + versions.splice(index, 1); + this.store.set("phpVersions", versions); } } addMysqlVersion(version: string): void { - const versions = this.store.get('mysqlVersions') + const versions = this.store.get("mysqlVersions"); if (!versions.includes(version)) { - versions.push(version) - this.store.set('mysqlVersions', versions) + versions.push(version); + this.store.set("mysqlVersions", versions); } } removeMysqlVersion(version: string): void { - const versions = this.store.get('mysqlVersions') - const index = versions.indexOf(version) + const versions = this.store.get("mysqlVersions"); + const index = versions.indexOf(version); if (index > -1) { - versions.splice(index, 1) - this.store.set('mysqlVersions', versions) + versions.splice(index, 1); + this.store.set("mysqlVersions", versions); } } addSite(site: SiteConfig): void { - const sites = this.store.get('sites') - sites.push(site) - this.store.set('sites', sites) + const sites = this.store.get("sites"); + sites.push(site); + this.store.set("sites", sites); } removeSite(name: string): void { - const sites = this.store.get('sites') - const index = sites.findIndex(s => s.name === name) + const sites = this.store.get("sites"); + const index = sites.findIndex((s) => s.name === name); if (index > -1) { - sites.splice(index, 1) - this.store.set('sites', sites) + sites.splice(index, 1); + this.store.set("sites", sites); } } updateSite(name: string, site: Partial): void { - const sites = this.store.get('sites') - const index = sites.findIndex(s => s.name === name) + const sites = this.store.get("sites"); + const index = sites.findIndex((s) => s.name === name); if (index > -1) { // 如果传入完整对象则替换,否则合并 if (site.domain && site.rootPath) { - sites[index] = site as SiteConfig + sites[index] = site as SiteConfig; } else { - sites[index] = { ...sites[index], ...site } + sites[index] = { ...sites[index], ...site }; } - this.store.set('sites', sites) + this.store.set("sites", sites); } } getSites(): SiteConfig[] { - return this.store.get('sites') + return this.store.get("sites"); } } - diff --git a/package.json b/package.json index 94ce2ae..41412aa 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,10 @@ "appId": "com.phper.devmanager", "productName": "PHPer开发环境管理器", "copyright": "Copyright © 2024 PHPer", + "icon": "build/icon.ico", "directories": { - "output": "release" + "output": "release", + "buildResources": "build" }, "files": [ "dist/**/*", @@ -64,6 +66,7 @@ ] } ], + "icon": "build/icon.ico", "requestedExecutionLevel": "requireAdministrator", "signAndEditExecutable": false }, diff --git a/public/icon.ico b/public/icon.ico new file mode 100644 index 0000000..0a1b73f Binary files /dev/null and b/public/icon.ico differ diff --git a/public/icon.png b/public/icon.png index b7e0e88..002e72b 100644 Binary files a/public/icon.png and b/public/icon.png differ diff --git a/src/views/Settings.vue b/src/views/Settings.vue index 1c5bae0..2ba050f 100644 --- a/src/views/Settings.vue +++ b/src/views/Settings.vue @@ -42,19 +42,6 @@ /> -
-
-

启动时自动运行所有服务

-

开机自启时自动启动 Nginx、MySQL、Redis 等服务

-
-
- -
-
@@ -168,14 +155,12 @@ interface ServiceAutoStart { interface AppSettings { autoLaunch: boolean startMinimized: boolean - autoStartServices: boolean } const basePath = ref('') const appSettings = reactive({ autoLaunch: false, - startMinimized: false, - autoStartServices: false + startMinimized: false }) const services = reactive([ @@ -191,7 +176,6 @@ const loadSettings = async () => { // 加载应用设置 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) { @@ -210,7 +194,6 @@ const toggleAutoLaunch = async (enabled: boolean) => { if (!enabled) { // 禁用开机自启时,同时禁用相关选项 appSettings.startMinimized = false - appSettings.autoStartServices = false } } else { ElMessage.error(result?.message || '设置失败') @@ -232,16 +215,6 @@ const toggleStartMinimized = async (enabled: boolean) => { } } -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)