Enhance icon handling in Electron app by adding support for ICO format, update package.json for build resources, and refactor auto-launch settings to use Windows Task Scheduler for improved functionality.

This commit is contained in:
ethanfly 2025-12-26 05:26:43 +08:00
parent d5a845d354
commit f72e4b7a6f
8 changed files with 197 additions and 171 deletions

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -17,32 +17,42 @@ import { ServiceManager } from "./services/ServiceManager";
import { HostsManager } from "./services/HostsManager"; import { HostsManager } from "./services/HostsManager";
import { ConfigStore } from "./services/ConfigStore"; import { ConfigStore } from "./services/ConfigStore";
// 获取托盘图标路径 (使用 PNG) // 获取图标路径
function getTrayIconPath(): string { function getIconPath(filename: string): string {
const { existsSync } = require("fs"); 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; 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 { function getWindowIconPath(): string {
const { existsSync } = require("fs"); return getIconPath("icon.ico");
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");
} }
// 创建托盘图标 // 创建托盘图标
@ -229,12 +239,6 @@ app.whenReady().then(async () => {
createTray(); createTray();
createWindow(); createWindow();
// 检查是否启用开机自启且自动启动服务
const autoStartServices = configStore.get("autoStartServicesOnBoot");
if (autoStartServices) {
await serviceManager.startAll();
}
app.on("activate", () => { app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); createWindow();
@ -477,23 +481,79 @@ ipcMain.handle("config:setBasePath", (_, path: string) =>
); );
// ==================== 应用设置 ==================== // ==================== 应用设置 ====================
// 设置开机自启 // 设置开机自启(以管理员模式,使用任务计划程序)
ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => { ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
app.setLoginItemSettings({ const { execSync } = require("child_process");
openAtLogin: enabled, const exePath = app.getPath("exe");
openAsHidden: true, // 静默启动 const taskName = "PHPerDevManager";
args: ["--hidden"],
}); // 开发模式下不支持
configStore.set("autoLaunch", enabled); if (!app.isPackaged) {
return { return {
success: true, success: false,
message: enabled ? "已启用开机自启" : "已禁用开机自启", 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", () => { ipcMain.handle("app:getAutoLaunch", async () => {
return app.getLoginItemSettings().openAtLogin; 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; 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", () => { ipcMain.handle("app:quit", () => {
isQuitting = true; isQuitting = true;

View File

@ -1,221 +1,222 @@
import Store from 'electron-store' import Store from "electron-store";
import { join, dirname } from 'path' import { join, dirname } from "path";
import { existsSync, mkdirSync } from 'fs' import { existsSync, mkdirSync } from "fs";
import { app } from 'electron' import { app } from "electron";
interface ConfigSchema { interface ConfigSchema {
basePath: string basePath: string;
phpVersions: string[] phpVersions: string[];
mysqlVersions: string[] mysqlVersions: string[];
nginxVersions: string[] nginxVersions: string[];
redisVersions: string[] redisVersions: string[];
activePhpVersion: string activePhpVersion: string;
autoStart: { autoStart: {
nginx: boolean nginx: boolean;
mysql: boolean mysql: boolean;
redis: boolean redis: boolean;
} };
sites: SiteConfig[] sites: SiteConfig[];
} }
export interface SiteConfig { export interface SiteConfig {
name: string name: string;
domain: string domain: string;
rootPath: string rootPath: string;
phpVersion: string phpVersion: string;
isLaravel: boolean isLaravel: boolean;
ssl: boolean ssl: boolean;
enabled: boolean enabled: boolean;
} }
// 获取应用安装目录下的 data 路径 // 获取应用安装目录下的 data 路径
function getDefaultBasePath(): string { function getDefaultBasePath(): string {
if (app.isPackaged) { if (app.isPackaged) {
// 生产环境:使用可执行文件所在目录下的 data 文件夹 // 生产环境:使用可执行文件所在目录下的 data 文件夹
const exePath = app.getPath('exe') const exePath = app.getPath("exe");
const appDir = dirname(exePath) const appDir = dirname(exePath);
return join(appDir, 'data') return join(appDir, "data");
} else { } else {
// 开发环境:使用项目根目录下的 data 文件夹 // 开发环境:使用项目根目录下的 data 文件夹
return join(process.cwd(), 'data') return join(process.cwd(), "data");
} }
} }
export class ConfigStore { export class ConfigStore {
private store: Store<ConfigSchema> private store: Store<ConfigSchema>;
private basePath: string private basePath: string;
constructor() { constructor() {
this.store = new Store<ConfigSchema>({ this.store = new Store<ConfigSchema>({
defaults: { defaults: {
basePath: getDefaultBasePath(), basePath: "",
phpVersions: [], phpVersions: [],
mysqlVersions: [], mysqlVersions: [],
nginxVersions: [], nginxVersions: [],
redisVersions: [], redisVersions: [],
activePhpVersion: '', activePhpVersion: "",
autoStart: { autoStart: {
nginx: false, nginx: false,
mysql: false, mysql: false,
redis: false redis: false,
}, },
sites: [] sites: [],
} },
}) });
this.basePath = this.store.get('basePath') // 直接使用应用安装目录下的 data 路径
this.ensureDirectories() this.basePath = getDefaultBasePath();
this.store.set("basePath", this.basePath);
this.ensureDirectories();
} }
private ensureDirectories(): void { private ensureDirectories(): void {
const dirs = [ const dirs = [
this.basePath, this.basePath,
join(this.basePath, 'php'), join(this.basePath, "php"),
join(this.basePath, 'mysql'), join(this.basePath, "mysql"),
join(this.basePath, 'nginx'), join(this.basePath, "nginx"),
join(this.basePath, 'nginx', 'sites-available'), join(this.basePath, "nginx", "sites-available"),
join(this.basePath, 'nginx', 'sites-enabled'), join(this.basePath, "nginx", "sites-enabled"),
join(this.basePath, 'nginx', 'ssl'), join(this.basePath, "nginx", "ssl"),
join(this.basePath, 'redis'), join(this.basePath, "redis"),
join(this.basePath, 'logs'), join(this.basePath, "logs"),
join(this.basePath, 'temp'), join(this.basePath, "temp"),
join(this.basePath, 'www') join(this.basePath, "www"),
] ];
for (const dir of dirs) { for (const dir of dirs) {
if (!existsSync(dir)) { if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true }) mkdirSync(dir, { recursive: true });
} }
} }
} }
get<K extends keyof ConfigSchema>(key: K): ConfigSchema[K] { get<K extends keyof ConfigSchema>(key: K): ConfigSchema[K] {
return this.store.get(key) return this.store.get(key);
} }
set<K extends keyof ConfigSchema>(key: K, value: ConfigSchema[K]): void { set<K extends keyof ConfigSchema>(key: K, value: ConfigSchema[K]): void {
this.store.set(key, value) this.store.set(key, value);
} }
getBasePath(): string { getBasePath(): string {
return this.basePath return this.basePath;
} }
setBasePath(path: string): void { setBasePath(path: string): void {
this.basePath = path this.basePath = path;
this.store.set('basePath', path) this.store.set("basePath", path);
this.ensureDirectories() this.ensureDirectories();
} }
getPhpPath(version: string): string { getPhpPath(version: string): string {
return join(this.basePath, 'php', `php-${version}`) return join(this.basePath, "php", `php-${version}`);
} }
getMysqlPath(version: string): string { getMysqlPath(version: string): string {
return join(this.basePath, 'mysql', `mysql-${version}`) return join(this.basePath, "mysql", `mysql-${version}`);
} }
getNginxPath(): string { getNginxPath(): string {
return join(this.basePath, 'nginx') return join(this.basePath, "nginx");
} }
getRedisPath(): string { getRedisPath(): string {
return join(this.basePath, 'redis') return join(this.basePath, "redis");
} }
getNodePath(): string { getNodePath(): string {
return join(this.basePath, 'nodejs') return join(this.basePath, "nodejs");
} }
getLogsPath(): string { getLogsPath(): string {
return join(this.basePath, 'logs') return join(this.basePath, "logs");
} }
getTempPath(): string { getTempPath(): string {
return join(this.basePath, 'temp') return join(this.basePath, "temp");
} }
getWwwPath(): string { getWwwPath(): string {
return join(this.basePath, 'www') return join(this.basePath, "www");
} }
getSitesAvailablePath(): string { getSitesAvailablePath(): string {
return join(this.basePath, 'nginx', 'sites-available') return join(this.basePath, "nginx", "sites-available");
} }
getSitesEnabledPath(): string { getSitesEnabledPath(): string {
return join(this.basePath, 'nginx', 'sites-enabled') return join(this.basePath, "nginx", "sites-enabled");
} }
getSSLPath(): string { getSSLPath(): string {
return join(this.basePath, 'nginx', 'ssl') return join(this.basePath, "nginx", "ssl");
} }
addPhpVersion(version: string): void { addPhpVersion(version: string): void {
const versions = this.store.get('phpVersions') const versions = this.store.get("phpVersions");
if (!versions.includes(version)) { if (!versions.includes(version)) {
versions.push(version) versions.push(version);
this.store.set('phpVersions', versions) this.store.set("phpVersions", versions);
} }
} }
removePhpVersion(version: string): void { removePhpVersion(version: string): void {
const versions = this.store.get('phpVersions') const versions = this.store.get("phpVersions");
const index = versions.indexOf(version) const index = versions.indexOf(version);
if (index > -1) { if (index > -1) {
versions.splice(index, 1) versions.splice(index, 1);
this.store.set('phpVersions', versions) this.store.set("phpVersions", versions);
} }
} }
addMysqlVersion(version: string): void { addMysqlVersion(version: string): void {
const versions = this.store.get('mysqlVersions') const versions = this.store.get("mysqlVersions");
if (!versions.includes(version)) { if (!versions.includes(version)) {
versions.push(version) versions.push(version);
this.store.set('mysqlVersions', versions) this.store.set("mysqlVersions", versions);
} }
} }
removeMysqlVersion(version: string): void { removeMysqlVersion(version: string): void {
const versions = this.store.get('mysqlVersions') const versions = this.store.get("mysqlVersions");
const index = versions.indexOf(version) const index = versions.indexOf(version);
if (index > -1) { if (index > -1) {
versions.splice(index, 1) versions.splice(index, 1);
this.store.set('mysqlVersions', versions) this.store.set("mysqlVersions", versions);
} }
} }
addSite(site: SiteConfig): void { addSite(site: SiteConfig): void {
const sites = this.store.get('sites') const sites = this.store.get("sites");
sites.push(site) sites.push(site);
this.store.set('sites', sites) this.store.set("sites", sites);
} }
removeSite(name: string): void { removeSite(name: string): void {
const sites = this.store.get('sites') const sites = this.store.get("sites");
const index = sites.findIndex(s => s.name === name) const index = sites.findIndex((s) => s.name === name);
if (index > -1) { if (index > -1) {
sites.splice(index, 1) sites.splice(index, 1);
this.store.set('sites', sites) this.store.set("sites", sites);
} }
} }
updateSite(name: string, site: Partial<SiteConfig>): void { updateSite(name: string, site: Partial<SiteConfig>): void {
const sites = this.store.get('sites') const sites = this.store.get("sites");
const index = sites.findIndex(s => s.name === name) const index = sites.findIndex((s) => s.name === name);
if (index > -1) { if (index > -1) {
// 如果传入完整对象则替换,否则合并 // 如果传入完整对象则替换,否则合并
if (site.domain && site.rootPath) { if (site.domain && site.rootPath) {
sites[index] = site as SiteConfig sites[index] = site as SiteConfig;
} else { } else {
sites[index] = { ...sites[index], ...site } sites[index] = { ...sites[index], ...site };
} }
this.store.set('sites', sites) this.store.set("sites", sites);
} }
} }
getSites(): SiteConfig[] { getSites(): SiteConfig[] {
return this.store.get('sites') return this.store.get("sites");
} }
} }

View File

@ -42,8 +42,10 @@
"appId": "com.phper.devmanager", "appId": "com.phper.devmanager",
"productName": "PHPer开发环境管理器", "productName": "PHPer开发环境管理器",
"copyright": "Copyright © 2024 PHPer", "copyright": "Copyright © 2024 PHPer",
"icon": "build/icon.ico",
"directories": { "directories": {
"output": "release" "output": "release",
"buildResources": "build"
}, },
"files": [ "files": [
"dist/**/*", "dist/**/*",
@ -64,6 +66,7 @@
] ]
} }
], ],
"icon": "build/icon.ico",
"requestedExecutionLevel": "requireAdministrator", "requestedExecutionLevel": "requireAdministrator",
"signAndEditExecutable": false "signAndEditExecutable": false
}, },

BIN
public/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -42,19 +42,6 @@
/> />
</div> </div>
</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> </div>
@ -168,14 +155,12 @@ interface ServiceAutoStart {
interface AppSettings { interface AppSettings {
autoLaunch: boolean autoLaunch: boolean
startMinimized: boolean startMinimized: boolean
autoStartServices: boolean
} }
const basePath = ref('') const basePath = ref('')
const appSettings = reactive<AppSettings>({ const appSettings = reactive<AppSettings>({
autoLaunch: false, autoLaunch: false,
startMinimized: false, startMinimized: false
autoStartServices: false
}) })
const services = reactive<ServiceAutoStart[]>([ const services = reactive<ServiceAutoStart[]>([
@ -191,7 +176,6 @@ const loadSettings = async () => {
// //
appSettings.autoLaunch = await window.electronAPI?.app?.getAutoLaunch() || false appSettings.autoLaunch = await window.electronAPI?.app?.getAutoLaunch() || false
appSettings.startMinimized = await window.electronAPI?.app?.getStartMinimized() || false appSettings.startMinimized = await window.electronAPI?.app?.getStartMinimized() || false
appSettings.autoStartServices = await window.electronAPI?.app?.getAutoStartServices() || false
// //
for (const service of services) { for (const service of services) {
@ -210,7 +194,6 @@ const toggleAutoLaunch = async (enabled: boolean) => {
if (!enabled) { if (!enabled) {
// //
appSettings.startMinimized = false appSettings.startMinimized = false
appSettings.autoStartServices = false
} }
} else { } else {
ElMessage.error(result?.message || '设置失败') 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) => { const toggleAutoStart = async (name: string, enabled: boolean) => {
try { try {
const result = await window.electronAPI?.service.setAutoStart(name, enabled) const result = await window.electronAPI?.service.setAutoStart(name, enabled)