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 { 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;

View File

@ -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<ConfigSchema>
private basePath: string
private store: Store<ConfigSchema>;
private basePath: string;
constructor() {
this.store = new Store<ConfigSchema>({
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<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 {
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<SiteConfig>): 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");
}
}

View File

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

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 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>
@ -168,14 +155,12 @@ interface ServiceAutoStart {
interface AppSettings {
autoLaunch: boolean
startMinimized: boolean
autoStartServices: boolean
}
const basePath = ref('')
const appSettings = reactive<AppSettings>({
autoLaunch: false,
startMinimized: false,
autoStartServices: false
startMinimized: false
})
const services = reactive<ServiceAutoStart[]>([
@ -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)