Update version to 1.0.10 in package.json and version.json, add GoManager support in main.ts, and enhance UI components to include Go management features. Refactor preload.ts and App.vue for improved service management and user experience.

This commit is contained in:
ethanfly 2026-02-05 09:08:23 +08:00
parent 982a51ef52
commit 4abce26cf2
9 changed files with 1591 additions and 538 deletions

View File

@ -13,6 +13,7 @@ import { MysqlManager } from "./services/MysqlManager";
import { NginxManager } from "./services/NginxManager";
import { RedisManager } from "./services/RedisManager";
import { NodeManager } from "./services/NodeManager";
import { GoManager } from "./services/GoManager";
import { ServiceManager } from "./services/ServiceManager";
import { HostsManager } from "./services/HostsManager";
import { GitManager } from "./services/GitManager";
@ -98,7 +99,7 @@ export function sendDownloadProgress(
type: string,
progress: number,
downloaded: number,
total: number
total: number,
) {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("download-progress", {
@ -117,6 +118,7 @@ const mysqlManager = new MysqlManager(configStore);
const nginxManager = new NginxManager(configStore);
const redisManager = new RedisManager(configStore);
const nodeManager = new NodeManager(configStore);
const goManager = new GoManager(configStore);
const serviceManager = new ServiceManager(configStore);
const hostsManager = new HostsManager();
const gitManager = new GitManager(configStore);
@ -315,7 +317,7 @@ ipcMain.handle("window:close", () => mainWindow?.close());
// 打开外部链接
ipcMain.handle("shell:openExternal", (_, url: string) =>
shell.openExternal(url)
shell.openExternal(url),
);
ipcMain.handle("shell:openPath", (_, path: string) => shell.openPath(path));
@ -332,33 +334,33 @@ ipcMain.handle("dialog:selectDirectory", async () => {
// ==================== PHP 管理 ====================
ipcMain.handle("php:getVersions", () => phpManager.getInstalledVersions());
ipcMain.handle("php:getAvailableVersions", () =>
phpManager.getAvailableVersions()
phpManager.getAvailableVersions(),
);
ipcMain.handle("php:install", (_, version: string) =>
phpManager.install(version)
phpManager.install(version),
);
ipcMain.handle("php:uninstall", (_, version: string) =>
phpManager.uninstall(version)
phpManager.uninstall(version),
);
ipcMain.handle("php:setActive", (_, version: string) =>
phpManager.setActive(version)
phpManager.setActive(version),
);
ipcMain.handle("php:getExtensions", (_, version: string) =>
phpManager.getExtensions(version)
phpManager.getExtensions(version),
);
ipcMain.handle("php:openExtensionDir", (_, version: string) =>
phpManager.openExtensionDir(version)
phpManager.openExtensionDir(version),
);
ipcMain.handle(
"php:getAvailableExtensions",
(_, version: string, searchKeyword?: string) =>
phpManager.getAvailableExtensions(version, searchKeyword)
phpManager.getAvailableExtensions(version, searchKeyword),
);
ipcMain.handle("php:enableExtension", (_, version: string, ext: string) =>
phpManager.enableExtension(version, ext)
phpManager.enableExtension(version, ext),
);
ipcMain.handle("php:disableExtension", (_, version: string, ext: string) =>
phpManager.disableExtension(version, ext)
phpManager.disableExtension(version, ext),
);
ipcMain.handle(
"php:installExtension",
@ -367,14 +369,14 @@ ipcMain.handle(
version: string,
ext: string,
downloadUrl?: string,
packageName?: string
) => phpManager.installExtension(version, ext, downloadUrl, packageName)
packageName?: string,
) => phpManager.installExtension(version, ext, downloadUrl, packageName),
);
ipcMain.handle("php:getConfig", (_, version: string) =>
phpManager.getConfig(version)
phpManager.getConfig(version),
);
ipcMain.handle("php:saveConfig", (_, version: string, config: string) =>
phpManager.saveConfig(version, config)
phpManager.saveConfig(version, config),
);
// ==================== Composer 管理 ====================
@ -382,62 +384,62 @@ ipcMain.handle("composer:getStatus", () => phpManager.getComposerStatus());
ipcMain.handle("composer:install", () => phpManager.installComposer());
ipcMain.handle("composer:uninstall", () => phpManager.uninstallComposer());
ipcMain.handle("composer:setMirror", (_, mirror: string) =>
phpManager.setComposerMirror(mirror)
phpManager.setComposerMirror(mirror),
);
ipcMain.handle(
"composer:createLaravelProject",
(_, projectName: string, targetDir: string) =>
phpManager.createLaravelProject(projectName, targetDir)
phpManager.createLaravelProject(projectName, targetDir),
);
// ==================== MySQL 管理 ====================
ipcMain.handle("mysql:getVersions", () => mysqlManager.getInstalledVersions());
ipcMain.handle("mysql:getAvailableVersions", () =>
mysqlManager.getAvailableVersions()
mysqlManager.getAvailableVersions(),
);
ipcMain.handle("mysql:install", (_, version: string) =>
mysqlManager.install(version)
mysqlManager.install(version),
);
ipcMain.handle("mysql:uninstall", (_, version: string) =>
mysqlManager.uninstall(version)
mysqlManager.uninstall(version),
);
ipcMain.handle("mysql:start", (_, version: string) =>
mysqlManager.start(version)
mysqlManager.start(version),
);
ipcMain.handle("mysql:stop", (_, version: string) =>
mysqlManager.stop(version)
mysqlManager.stop(version),
);
ipcMain.handle("mysql:restart", (_, version: string) =>
mysqlManager.restart(version)
mysqlManager.restart(version),
);
ipcMain.handle("mysql:getStatus", (_, version: string) =>
mysqlManager.getStatus(version)
mysqlManager.getStatus(version),
);
ipcMain.handle(
"mysql:changePassword",
(_, version: string, newPassword: string, currentPassword?: string) =>
mysqlManager.changeRootPassword(version, newPassword, currentPassword)
mysqlManager.changeRootPassword(version, newPassword, currentPassword),
);
ipcMain.handle("mysql:getConfig", (_, version: string) =>
mysqlManager.getConfig(version)
mysqlManager.getConfig(version),
);
ipcMain.handle("mysql:saveConfig", (_, version: string, config: string) =>
mysqlManager.saveConfig(version, config)
mysqlManager.saveConfig(version, config),
);
ipcMain.handle("mysql:reinitialize", (_, version: string) =>
mysqlManager.reinitialize(version)
mysqlManager.reinitialize(version),
);
// ==================== Nginx 管理 ====================
ipcMain.handle("nginx:getVersions", () => nginxManager.getInstalledVersions());
ipcMain.handle("nginx:getAvailableVersions", () =>
nginxManager.getAvailableVersions()
nginxManager.getAvailableVersions(),
);
ipcMain.handle("nginx:install", (_, version: string) =>
nginxManager.install(version)
nginxManager.install(version),
);
ipcMain.handle("nginx:uninstall", (_, version: string) =>
nginxManager.uninstall(version)
nginxManager.uninstall(version),
);
ipcMain.handle("nginx:start", () => nginxManager.start());
ipcMain.handle("nginx:stop", () => nginxManager.stop());
@ -446,39 +448,39 @@ ipcMain.handle("nginx:reload", () => nginxManager.reload());
ipcMain.handle("nginx:getStatus", () => nginxManager.getStatus());
ipcMain.handle("nginx:getConfig", () => nginxManager.getConfig());
ipcMain.handle("nginx:saveConfig", (_, config: string) =>
nginxManager.saveConfig(config)
nginxManager.saveConfig(config),
);
ipcMain.handle("nginx:getSites", () => nginxManager.getSites());
ipcMain.handle("nginx:addSite", (_, site: any) => nginxManager.addSite(site));
ipcMain.handle("nginx:removeSite", (_, name: string) =>
nginxManager.removeSite(name)
nginxManager.removeSite(name),
);
ipcMain.handle("nginx:updateSite", (_, originalName: string, site: any) =>
nginxManager.updateSite(originalName, site)
nginxManager.updateSite(originalName, site),
);
ipcMain.handle("nginx:enableSite", (_, name: string) =>
nginxManager.enableSite(name)
nginxManager.enableSite(name),
);
ipcMain.handle("nginx:disableSite", (_, name: string) =>
nginxManager.disableSite(name)
nginxManager.disableSite(name),
);
ipcMain.handle("nginx:generateLaravelConfig", (_, site: any) =>
nginxManager.generateLaravelConfig(site)
nginxManager.generateLaravelConfig(site),
);
ipcMain.handle("nginx:requestSSL", (_, domain: string, email: string) =>
nginxManager.requestSSLCertificate(domain, email)
nginxManager.requestSSLCertificate(domain, email),
);
// ==================== Redis 管理 ====================
ipcMain.handle("redis:getVersions", () => redisManager.getInstalledVersions());
ipcMain.handle("redis:getAvailableVersions", () =>
redisManager.getAvailableVersions()
redisManager.getAvailableVersions(),
);
ipcMain.handle("redis:install", (_, version: string) =>
redisManager.install(version)
redisManager.install(version),
);
ipcMain.handle("redis:uninstall", (_, version: string) =>
redisManager.uninstall(version)
redisManager.uninstall(version),
);
ipcMain.handle("redis:start", () => redisManager.start());
ipcMain.handle("redis:stop", () => redisManager.stop());
@ -486,103 +488,131 @@ ipcMain.handle("redis:restart", () => redisManager.restart());
ipcMain.handle("redis:getStatus", () => redisManager.getStatus());
ipcMain.handle("redis:getConfig", () => redisManager.getConfig());
ipcMain.handle("redis:saveConfig", (_, config: string) =>
redisManager.saveConfig(config)
redisManager.saveConfig(config),
);
// ==================== Node.js 管理 ====================
ipcMain.handle("node:getVersions", () => nodeManager.getInstalledVersions());
ipcMain.handle("node:getAvailableVersions", () =>
nodeManager.getAvailableVersions()
nodeManager.getAvailableVersions(),
);
ipcMain.handle("node:install", (_, version: string, downloadUrl: string) =>
nodeManager.install(version, downloadUrl)
nodeManager.install(version, downloadUrl),
);
ipcMain.handle("node:uninstall", (_, version: string) =>
nodeManager.uninstall(version)
nodeManager.uninstall(version),
);
ipcMain.handle("node:setActive", (_, version: string) =>
nodeManager.setActive(version)
nodeManager.setActive(version),
);
ipcMain.handle("node:getInfo", (_, version: string) =>
nodeManager.getNodeInfo(version)
nodeManager.getNodeInfo(version),
);
// ==================== Go 管理 ====================
ipcMain.handle("go:getVersions", () => goManager.getInstalledVersions());
ipcMain.handle("go:getAvailableVersions", () =>
goManager.getAvailableVersions(),
);
ipcMain.handle("go:install", (_, version: string, downloadUrl: string) =>
goManager.install(version, downloadUrl),
);
ipcMain.handle("go:uninstall", (_, version: string) =>
goManager.uninstall(version),
);
ipcMain.handle("go:setActive", (_, version: string) =>
goManager.setActive(version),
);
ipcMain.handle("go:getInfo", (_, version: string) =>
goManager.getGoInfo(version),
);
// ==================== 服务管理 ====================
ipcMain.handle("service:getAll", () => serviceManager.getAllServices());
ipcMain.handle("service:setAutoStart", (_, service: string, enabled: boolean) =>
serviceManager.setAutoStart(service, enabled)
serviceManager.setAutoStart(service, enabled),
);
ipcMain.handle("service:getAutoStart", (_, service: string) =>
serviceManager.getAutoStart(service)
serviceManager.getAutoStart(service),
);
ipcMain.handle("service:startAll", () => serviceManager.startAll());
ipcMain.handle("service:stopAll", () => serviceManager.stopAll());
// PHP-CGI 管理 - 支持多版本
ipcMain.handle("service:getPhpCgiStatus", () => serviceManager.getPhpCgiStatus());
ipcMain.handle("service:getPhpCgiStatus", () =>
serviceManager.getPhpCgiStatus(),
);
ipcMain.handle("service:startPhpCgi", () => serviceManager.startPhpCgi());
ipcMain.handle("service:stopPhpCgi", () => serviceManager.stopPhpCgi());
ipcMain.handle("service:startAllPhpCgi", () => serviceManager.startAllPhpCgi());
ipcMain.handle("service:stopAllPhpCgi", () => serviceManager.stopAllPhpCgi());
ipcMain.handle("service:startPhpCgiVersion", (_, version: string) => serviceManager.startPhpCgiVersion(version));
ipcMain.handle("service:stopPhpCgiVersion", (_, version: string) => serviceManager.stopPhpCgiVersion(version));
ipcMain.handle("service:getPhpCgiPort", (_, version: string) => serviceManager.getPhpCgiPort(version));
ipcMain.handle("service:startPhpCgiVersion", (_, version: string) =>
serviceManager.startPhpCgiVersion(version),
);
ipcMain.handle("service:stopPhpCgiVersion", (_, version: string) =>
serviceManager.stopPhpCgiVersion(version),
);
ipcMain.handle("service:getPhpCgiPort", (_, version: string) =>
serviceManager.getPhpCgiPort(version),
);
// ==================== Hosts 管理 ====================
ipcMain.handle("hosts:get", () => hostsManager.getHosts());
ipcMain.handle("hosts:add", (_, domain: string, ip: string) =>
hostsManager.addHost(domain, ip)
hostsManager.addHost(domain, ip),
);
ipcMain.handle("hosts:remove", (_, domain: string) =>
hostsManager.removeHost(domain)
hostsManager.removeHost(domain),
);
// ==================== Git 管理 ====================
ipcMain.handle("git:getVersions", () => gitManager.getInstalledVersions());
ipcMain.handle("git:getAvailableVersions", () =>
gitManager.getAvailableVersions()
gitManager.getAvailableVersions(),
);
ipcMain.handle("git:install", (_, version: string) =>
gitManager.install(version)
gitManager.install(version),
);
ipcMain.handle("git:uninstall", () => gitManager.uninstall());
ipcMain.handle("git:checkSystem", () => gitManager.checkSystemGit());
ipcMain.handle("git:getConfig", () => gitManager.getGitConfig());
ipcMain.handle("git:setConfig", (_, name: string, email: string) =>
gitManager.setGitConfig(name, email)
gitManager.setGitConfig(name, email),
);
// ==================== Python 管理 ====================
ipcMain.handle("python:getVersions", () => pythonManager.getInstalledVersions());
ipcMain.handle("python:getVersions", () =>
pythonManager.getInstalledVersions(),
);
ipcMain.handle("python:getAvailableVersions", () =>
pythonManager.getAvailableVersions()
pythonManager.getAvailableVersions(),
);
ipcMain.handle("python:install", (_, version: string) =>
pythonManager.install(version)
pythonManager.install(version),
);
ipcMain.handle("python:uninstall", (_, version: string) =>
pythonManager.uninstall(version)
pythonManager.uninstall(version),
);
ipcMain.handle("python:setActive", (_, version: string) =>
pythonManager.setActive(version)
pythonManager.setActive(version),
);
ipcMain.handle("python:checkSystem", () => pythonManager.checkSystemPython());
ipcMain.handle("python:getPipInfo", (_, version: string) =>
pythonManager.getPipInfo(version)
pythonManager.getPipInfo(version),
);
ipcMain.handle(
"python:installPackage",
(_, version: string, packageName: string) =>
pythonManager.installPackage(version, packageName)
pythonManager.installPackage(version, packageName),
);
// ==================== 配置管理 ====================
ipcMain.handle("config:get", (_, key: string) => configStore.get(key));
ipcMain.handle("config:set", (_, key: string, value: any) =>
configStore.set(key, value)
configStore.set(key, value),
);
ipcMain.handle("config:getBasePath", () => configStore.getBasePath());
ipcMain.handle("config:setBasePath", (_, path: string) =>
configStore.setBasePath(path)
configStore.setBasePath(path),
);
// ==================== 应用设置 ====================
@ -636,7 +666,7 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
} catch (e) {
// 忽略删除失败
}
// 删除 VBS 脚本
const appDir = require("path").dirname(exePath);
const vbsPath = join(appDir, "silent_start.vbs");
@ -647,7 +677,7 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
// 忽略删除失败
}
}
configStore.set("autoLaunch", false);
return { success: true, message: "已禁用开机自启" };
}
@ -685,17 +715,17 @@ ipcMain.handle("app:getAutoLaunch", async () => {
ipcMain.handle("app:getVersion", async () => {
const { existsSync, readFileSync } = require("fs");
const { join } = require("path");
const version = app.getVersion();
let buildTime = "";
let buildDate = "";
// 尝试读取版本信息文件
try {
const versionFilePath = app.isPackaged
? join(process.resourcesPath, "public", "version.json")
: join(__dirname, "..", "public", "version.json");
if (existsSync(versionFilePath)) {
const versionInfo = JSON.parse(readFileSync(versionFilePath, "utf-8"));
buildTime = versionInfo.buildTime || "";
@ -704,12 +734,12 @@ ipcMain.handle("app:getVersion", async () => {
} catch (e) {
// 忽略错误
}
return {
version,
buildTime,
buildDate,
isPackaged: app.isPackaged
isPackaged: app.isPackaged,
};
});
@ -733,9 +763,13 @@ ipcMain.handle("app:quit", () => {
// ==================== 日志管理 ====================
ipcMain.handle("log:getFiles", () => logManager.getLogFiles());
ipcMain.handle("log:read", (_, logPath: string, lines?: number) =>
logManager.readLog(logPath, lines)
logManager.readLog(logPath, lines),
);
ipcMain.handle("log:clear", (_, logPath: string) => logManager.clearLog(logPath));
ipcMain.handle("log:getDirectory", (_, type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string) =>
logManager.getLogDirectory(type, version)
ipcMain.handle("log:clear", (_, logPath: string) =>
logManager.clearLog(logPath),
);
ipcMain.handle(
"log:getDirectory",
(_, type: "nginx" | "php" | "mysql" | "sites", version?: string) =>
logManager.getLogDirectory(type, version),
);

View File

@ -1,220 +1,318 @@
import { contextBridge, ipcRenderer } from 'electron'
import { contextBridge, ipcRenderer } from "electron";
// 暴露安全的 API 到渲染进程
contextBridge.exposeInMainWorld('electronAPI', {
contextBridge.exposeInMainWorld("electronAPI", {
// 窗口控制
minimize: () => ipcRenderer.invoke('window:minimize'),
maximize: () => ipcRenderer.invoke('window:maximize'),
close: () => ipcRenderer.invoke('window:close'),
minimize: () => ipcRenderer.invoke("window:minimize"),
maximize: () => ipcRenderer.invoke("window:maximize"),
close: () => ipcRenderer.invoke("window:close"),
// Shell
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
openPath: (path: string) => ipcRenderer.invoke('shell:openPath', path),
openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
openPath: (path: string) => ipcRenderer.invoke("shell:openPath", path),
// Dialog
selectDirectory: () => ipcRenderer.invoke('dialog:selectDirectory'),
selectDirectory: () => ipcRenderer.invoke("dialog:selectDirectory"),
// 下载进度监听
onDownloadProgress: (callback: (data: { type: string; progress: number; downloaded: number; total: number }) => void) => {
ipcRenderer.on('download-progress', (_, data) => callback(data))
onDownloadProgress: (
callback: (data: {
type: string;
progress: number;
downloaded: number;
total: number;
}) => void,
) => {
ipcRenderer.on("download-progress", (_, data) => callback(data));
},
removeDownloadProgressListener: () => {
ipcRenderer.removeAllListeners('download-progress')
ipcRenderer.removeAllListeners("download-progress");
},
// PHP 管理
php: {
getVersions: () => ipcRenderer.invoke('php:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('php:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('php:install', version),
uninstall: (version: string) => ipcRenderer.invoke('php:uninstall', version),
setActive: (version: string) => ipcRenderer.invoke('php:setActive', version),
getExtensions: (version: string) => ipcRenderer.invoke('php:getExtensions', version),
openExtensionDir: (version: string) => ipcRenderer.invoke('php:openExtensionDir', version),
getAvailableExtensions: (version: string, searchKeyword?: string) => ipcRenderer.invoke('php:getAvailableExtensions', version, searchKeyword),
enableExtension: (version: string, ext: string) => ipcRenderer.invoke('php:enableExtension', version, ext),
disableExtension: (version: string, ext: string) => ipcRenderer.invoke('php:disableExtension', version, ext),
installExtension: (version: string, ext: string, downloadUrl?: string, packageName?: string) => ipcRenderer.invoke('php:installExtension', version, ext, downloadUrl, packageName),
getConfig: (version: string) => ipcRenderer.invoke('php:getConfig', version),
saveConfig: (version: string, config: string) => ipcRenderer.invoke('php:saveConfig', version, config)
getVersions: () => ipcRenderer.invoke("php:getVersions"),
getAvailableVersions: () => ipcRenderer.invoke("php:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("php:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("php:uninstall", version),
setActive: (version: string) =>
ipcRenderer.invoke("php:setActive", version),
getExtensions: (version: string) =>
ipcRenderer.invoke("php:getExtensions", version),
openExtensionDir: (version: string) =>
ipcRenderer.invoke("php:openExtensionDir", version),
getAvailableExtensions: (version: string, searchKeyword?: string) =>
ipcRenderer.invoke("php:getAvailableExtensions", version, searchKeyword),
enableExtension: (version: string, ext: string) =>
ipcRenderer.invoke("php:enableExtension", version, ext),
disableExtension: (version: string, ext: string) =>
ipcRenderer.invoke("php:disableExtension", version, ext),
installExtension: (
version: string,
ext: string,
downloadUrl?: string,
packageName?: string,
) =>
ipcRenderer.invoke(
"php:installExtension",
version,
ext,
downloadUrl,
packageName,
),
getConfig: (version: string) =>
ipcRenderer.invoke("php:getConfig", version),
saveConfig: (version: string, config: string) =>
ipcRenderer.invoke("php:saveConfig", version, config),
},
// Composer 管理
composer: {
getStatus: () => ipcRenderer.invoke('composer:getStatus'),
install: () => ipcRenderer.invoke('composer:install'),
uninstall: () => ipcRenderer.invoke('composer:uninstall'),
setMirror: (mirror: string) => ipcRenderer.invoke('composer:setMirror', mirror),
createLaravelProject: (projectName: string, targetDir: string) => ipcRenderer.invoke('composer:createLaravelProject', projectName, targetDir)
getStatus: () => ipcRenderer.invoke("composer:getStatus"),
install: () => ipcRenderer.invoke("composer:install"),
uninstall: () => ipcRenderer.invoke("composer:uninstall"),
setMirror: (mirror: string) =>
ipcRenderer.invoke("composer:setMirror", mirror),
createLaravelProject: (projectName: string, targetDir: string) =>
ipcRenderer.invoke(
"composer:createLaravelProject",
projectName,
targetDir,
),
},
// MySQL 管理
mysql: {
getVersions: () => ipcRenderer.invoke('mysql:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('mysql:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('mysql:install', version),
uninstall: (version: string) => ipcRenderer.invoke('mysql:uninstall', version),
start: (version: string) => ipcRenderer.invoke('mysql:start', version),
stop: (version: string) => ipcRenderer.invoke('mysql:stop', version),
restart: (version: string) => ipcRenderer.invoke('mysql:restart', version),
getStatus: (version: string) => ipcRenderer.invoke('mysql:getStatus', version),
changePassword: (version: string, newPassword: string, currentPassword?: string) => ipcRenderer.invoke('mysql:changePassword', version, newPassword, currentPassword),
getConfig: (version: string) => ipcRenderer.invoke('mysql:getConfig', version),
saveConfig: (version: string, config: string) => ipcRenderer.invoke('mysql:saveConfig', version, config)
getVersions: () => ipcRenderer.invoke("mysql:getVersions"),
getAvailableVersions: () =>
ipcRenderer.invoke("mysql:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("mysql:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("mysql:uninstall", version),
start: (version: string) => ipcRenderer.invoke("mysql:start", version),
stop: (version: string) => ipcRenderer.invoke("mysql:stop", version),
restart: (version: string) => ipcRenderer.invoke("mysql:restart", version),
getStatus: (version: string) =>
ipcRenderer.invoke("mysql:getStatus", version),
changePassword: (
version: string,
newPassword: string,
currentPassword?: string,
) =>
ipcRenderer.invoke(
"mysql:changePassword",
version,
newPassword,
currentPassword,
),
getConfig: (version: string) =>
ipcRenderer.invoke("mysql:getConfig", version),
saveConfig: (version: string, config: string) =>
ipcRenderer.invoke("mysql:saveConfig", version, config),
},
// Nginx 管理
nginx: {
getVersions: () => ipcRenderer.invoke('nginx:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('nginx:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('nginx:install', version),
uninstall: (version: string) => ipcRenderer.invoke('nginx:uninstall', version),
start: () => ipcRenderer.invoke('nginx:start'),
stop: () => ipcRenderer.invoke('nginx:stop'),
restart: () => ipcRenderer.invoke('nginx:restart'),
reload: () => ipcRenderer.invoke('nginx:reload'),
getStatus: () => ipcRenderer.invoke('nginx:getStatus'),
getConfig: () => ipcRenderer.invoke('nginx:getConfig'),
saveConfig: (config: string) => ipcRenderer.invoke('nginx:saveConfig', config),
getSites: () => ipcRenderer.invoke('nginx:getSites'),
addSite: (site: any) => ipcRenderer.invoke('nginx:addSite', site),
removeSite: (name: string) => ipcRenderer.invoke('nginx:removeSite', name),
updateSite: (originalName: string, site: any) => ipcRenderer.invoke('nginx:updateSite', originalName, site),
enableSite: (name: string) => ipcRenderer.invoke('nginx:enableSite', name),
disableSite: (name: string) => ipcRenderer.invoke('nginx:disableSite', name),
generateLaravelConfig: (site: any) => ipcRenderer.invoke('nginx:generateLaravelConfig', site),
requestSSL: (domain: string, email: string) => ipcRenderer.invoke('nginx:requestSSL', domain, email)
getVersions: () => ipcRenderer.invoke("nginx:getVersions"),
getAvailableVersions: () =>
ipcRenderer.invoke("nginx:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("nginx:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("nginx:uninstall", version),
start: () => ipcRenderer.invoke("nginx:start"),
stop: () => ipcRenderer.invoke("nginx:stop"),
restart: () => ipcRenderer.invoke("nginx:restart"),
reload: () => ipcRenderer.invoke("nginx:reload"),
getStatus: () => ipcRenderer.invoke("nginx:getStatus"),
getConfig: () => ipcRenderer.invoke("nginx:getConfig"),
saveConfig: (config: string) =>
ipcRenderer.invoke("nginx:saveConfig", config),
getSites: () => ipcRenderer.invoke("nginx:getSites"),
addSite: (site: any) => ipcRenderer.invoke("nginx:addSite", site),
removeSite: (name: string) => ipcRenderer.invoke("nginx:removeSite", name),
updateSite: (originalName: string, site: any) =>
ipcRenderer.invoke("nginx:updateSite", originalName, site),
enableSite: (name: string) => ipcRenderer.invoke("nginx:enableSite", name),
disableSite: (name: string) =>
ipcRenderer.invoke("nginx:disableSite", name),
generateLaravelConfig: (site: any) =>
ipcRenderer.invoke("nginx:generateLaravelConfig", site),
requestSSL: (domain: string, email: string) =>
ipcRenderer.invoke("nginx:requestSSL", domain, email),
},
// Redis 管理
redis: {
getVersions: () => ipcRenderer.invoke('redis:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('redis:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('redis:install', version),
uninstall: (version: string) => ipcRenderer.invoke('redis:uninstall', version),
start: () => ipcRenderer.invoke('redis:start'),
stop: () => ipcRenderer.invoke('redis:stop'),
restart: () => ipcRenderer.invoke('redis:restart'),
getStatus: () => ipcRenderer.invoke('redis:getStatus'),
getConfig: () => ipcRenderer.invoke('redis:getConfig'),
saveConfig: (config: string) => ipcRenderer.invoke('redis:saveConfig', config)
getVersions: () => ipcRenderer.invoke("redis:getVersions"),
getAvailableVersions: () =>
ipcRenderer.invoke("redis:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("redis:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("redis:uninstall", version),
start: () => ipcRenderer.invoke("redis:start"),
stop: () => ipcRenderer.invoke("redis:stop"),
restart: () => ipcRenderer.invoke("redis:restart"),
getStatus: () => ipcRenderer.invoke("redis:getStatus"),
getConfig: () => ipcRenderer.invoke("redis:getConfig"),
saveConfig: (config: string) =>
ipcRenderer.invoke("redis:saveConfig", config),
},
// Go 管理
go: {
getVersions: () => ipcRenderer.invoke("go:getVersions"),
getAvailableVersions: () => ipcRenderer.invoke("go:getAvailableVersions"),
install: (version: string, downloadUrl: string) =>
ipcRenderer.invoke("go:install", version, downloadUrl),
uninstall: (version: string) => ipcRenderer.invoke("go:uninstall", version),
setActive: (version: string) => ipcRenderer.invoke("go:setActive", version),
getInfo: (version: string) => ipcRenderer.invoke("go:getInfo", version),
},
// Node.js 管理
node: {
getVersions: () => ipcRenderer.invoke('node:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('node:getAvailableVersions'),
install: (version: string, downloadUrl: string) => ipcRenderer.invoke('node:install', version, downloadUrl),
uninstall: (version: string) => ipcRenderer.invoke('node:uninstall', version),
setActive: (version: string) => ipcRenderer.invoke('node:setActive', version),
getInfo: (version: string) => ipcRenderer.invoke('node:getInfo', version)
getVersions: () => ipcRenderer.invoke("node:getVersions"),
getAvailableVersions: () => ipcRenderer.invoke("node:getAvailableVersions"),
install: (version: string, downloadUrl: string) =>
ipcRenderer.invoke("node:install", version, downloadUrl),
uninstall: (version: string) =>
ipcRenderer.invoke("node:uninstall", version),
setActive: (version: string) =>
ipcRenderer.invoke("node:setActive", version),
getInfo: (version: string) => ipcRenderer.invoke("node:getInfo", version),
},
// Git 管理
git: {
getVersions: () => ipcRenderer.invoke('git:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('git:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('git:install', version),
uninstall: () => ipcRenderer.invoke('git:uninstall'),
checkSystem: () => ipcRenderer.invoke('git:checkSystem'),
getConfig: () => ipcRenderer.invoke('git:getConfig'),
setConfig: (name: string, email: string) => ipcRenderer.invoke('git:setConfig', name, email)
getVersions: () => ipcRenderer.invoke("git:getVersions"),
getAvailableVersions: () => ipcRenderer.invoke("git:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("git:install", version),
uninstall: () => ipcRenderer.invoke("git:uninstall"),
checkSystem: () => ipcRenderer.invoke("git:checkSystem"),
getConfig: () => ipcRenderer.invoke("git:getConfig"),
setConfig: (name: string, email: string) =>
ipcRenderer.invoke("git:setConfig", name, email),
},
// Python 管理
python: {
getVersions: () => ipcRenderer.invoke('python:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('python:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('python:install', version),
uninstall: (version: string) => ipcRenderer.invoke('python:uninstall', version),
setActive: (version: string) => ipcRenderer.invoke('python:setActive', version),
checkSystem: () => ipcRenderer.invoke('python:checkSystem'),
getPipInfo: (version: string) => ipcRenderer.invoke('python:getPipInfo', version),
installPackage: (version: string, packageName: string) => ipcRenderer.invoke('python:installPackage', version, packageName)
getVersions: () => ipcRenderer.invoke("python:getVersions"),
getAvailableVersions: () =>
ipcRenderer.invoke("python:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("python:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("python:uninstall", version),
setActive: (version: string) =>
ipcRenderer.invoke("python:setActive", version),
checkSystem: () => ipcRenderer.invoke("python:checkSystem"),
getPipInfo: (version: string) =>
ipcRenderer.invoke("python:getPipInfo", version),
installPackage: (version: string, packageName: string) =>
ipcRenderer.invoke("python:installPackage", version, packageName),
},
// 服务管理
service: {
getAll: () => ipcRenderer.invoke('service:getAll'),
setAutoStart: (service: string, enabled: boolean) => ipcRenderer.invoke('service:setAutoStart', service, enabled),
getAutoStart: (service: string) => ipcRenderer.invoke('service:getAutoStart', service),
startAll: () => ipcRenderer.invoke('service:startAll'),
stopAll: () => ipcRenderer.invoke('service:stopAll'),
getAll: () => ipcRenderer.invoke("service:getAll"),
setAutoStart: (service: string, enabled: boolean) =>
ipcRenderer.invoke("service:setAutoStart", service, enabled),
getAutoStart: (service: string) =>
ipcRenderer.invoke("service:getAutoStart", service),
startAll: () => ipcRenderer.invoke("service:startAll"),
stopAll: () => ipcRenderer.invoke("service:stopAll"),
// PHP-CGI 多版本管理
getPhpCgiStatus: () => ipcRenderer.invoke('service:getPhpCgiStatus'),
startPhpCgi: () => ipcRenderer.invoke('service:startPhpCgi'),
stopPhpCgi: () => ipcRenderer.invoke('service:stopPhpCgi'),
startAllPhpCgi: () => ipcRenderer.invoke('service:startAllPhpCgi'),
stopAllPhpCgi: () => ipcRenderer.invoke('service:stopAllPhpCgi'),
startPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:startPhpCgiVersion', version),
stopPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:stopPhpCgiVersion', version),
getPhpCgiPort: (version: string) => ipcRenderer.invoke('service:getPhpCgiPort', version)
getPhpCgiStatus: () => ipcRenderer.invoke("service:getPhpCgiStatus"),
startPhpCgi: () => ipcRenderer.invoke("service:startPhpCgi"),
stopPhpCgi: () => ipcRenderer.invoke("service:stopPhpCgi"),
startAllPhpCgi: () => ipcRenderer.invoke("service:startAllPhpCgi"),
stopAllPhpCgi: () => ipcRenderer.invoke("service:stopAllPhpCgi"),
startPhpCgiVersion: (version: string) =>
ipcRenderer.invoke("service:startPhpCgiVersion", version),
stopPhpCgiVersion: (version: string) =>
ipcRenderer.invoke("service:stopPhpCgiVersion", version),
getPhpCgiPort: (version: string) =>
ipcRenderer.invoke("service:getPhpCgiPort", version),
},
// Hosts 管理
hosts: {
get: () => ipcRenderer.invoke('hosts:get'),
add: (domain: string, ip: string) => ipcRenderer.invoke('hosts:add', domain, ip),
remove: (domain: string) => ipcRenderer.invoke('hosts:remove', domain)
get: () => ipcRenderer.invoke("hosts:get"),
add: (domain: string, ip: string) =>
ipcRenderer.invoke("hosts:add", domain, ip),
remove: (domain: string) => ipcRenderer.invoke("hosts:remove", domain),
},
// 配置管理
config: {
get: (key: string) => ipcRenderer.invoke('config:get', key),
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value),
getBasePath: () => ipcRenderer.invoke('config:getBasePath'),
setBasePath: (path: string) => ipcRenderer.invoke('config:setBasePath', path)
get: (key: string) => ipcRenderer.invoke("config:get", key),
set: (key: string, value: any) =>
ipcRenderer.invoke("config:set", key, value),
getBasePath: () => ipcRenderer.invoke("config:getBasePath"),
setBasePath: (path: string) =>
ipcRenderer.invoke("config:setBasePath", path),
},
// 日志管理
log: {
getFiles: () => ipcRenderer.invoke('log:getFiles'),
read: (logPath: string, lines?: number) => ipcRenderer.invoke('log:read', logPath, lines),
clear: (logPath: string) => ipcRenderer.invoke('log:clear', logPath),
getDirectory: (type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string) =>
ipcRenderer.invoke('log:getDirectory', type, version)
getFiles: () => ipcRenderer.invoke("log:getFiles"),
read: (logPath: string, lines?: number) =>
ipcRenderer.invoke("log:read", logPath, lines),
clear: (logPath: string) => ipcRenderer.invoke("log:clear", logPath),
getDirectory: (
type: "nginx" | "php" | "mysql" | "sites",
version?: string,
) => ipcRenderer.invoke("log:getDirectory", type, version),
},
// 应用设置
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'),
getVersion: () => ipcRenderer.invoke('app:getVersion') as Promise<{ version: string; buildTime: string; buildDate: string; isPackaged: boolean }>,
setAutoStartServices: (enabled: boolean) => ipcRenderer.invoke('app:setAutoStartServices', enabled),
getAutoStartServices: () => ipcRenderer.invoke('app:getAutoStartServices'),
quit: () => ipcRenderer.invoke('app:quit')
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"),
getVersion: () =>
ipcRenderer.invoke("app:getVersion") as Promise<{
version: string;
buildTime: string;
buildDate: string;
isPackaged: boolean;
}>,
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)
ipcRenderer.on("service-status-changed", callback);
},
removeServiceStatusChangedListener: (callback: () => void) => {
ipcRenderer.removeListener('service-status-changed', callback)
}
})
ipcRenderer.removeListener("service-status-changed", callback);
},
});
// 声明 Window 接口扩展
declare global {
interface Window {
electronAPI: typeof api
electronAPI: typeof api;
}
}
const api = {
minimize: () => ipcRenderer.invoke('window:minimize'),
maximize: () => ipcRenderer.invoke('window:maximize'),
close: () => ipcRenderer.invoke('window:close'),
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
openPath: (path: string) => ipcRenderer.invoke('shell:openPath', path),
minimize: () => ipcRenderer.invoke("window:minimize"),
maximize: () => ipcRenderer.invoke("window:maximize"),
close: () => ipcRenderer.invoke("window:close"),
openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
openPath: (path: string) => ipcRenderer.invoke("shell:openPath", path),
php: {} as any,
mysql: {} as any,
nginx: {} as any,
redis: {} as any,
service: {} as any,
hosts: {} as any,
config: {} as any
}
config: {} as any,
};

View File

@ -10,8 +10,10 @@ interface ConfigSchema {
nginxVersions: string[];
redisVersions: string[];
nodeVersions: string[];
goVersions: string[];
activePhpVersion: string;
activeNodeVersion: string;
activeGoVersion: string;
autoStart: {
nginx: boolean;
mysql: boolean;
@ -66,8 +68,10 @@ export class ConfigStore {
nginxVersions: [],
redisVersions: [],
nodeVersions: [],
goVersions: [],
activePhpVersion: "",
activeNodeVersion: "",
activeGoVersion: "",
autoStart: {
nginx: false,
mysql: false,
@ -99,6 +103,7 @@ export class ConfigStore {
join(this.basePath, "nginx", "ssl"),
join(this.basePath, "redis"),
join(this.basePath, "nodejs"),
join(this.basePath, "go"),
join(this.basePath, "logs"),
join(this.basePath, "temp"),
join(this.basePath, "www"),
@ -149,6 +154,10 @@ export class ConfigStore {
return join(this.basePath, "nodejs");
}
getGoPath(): string {
return join(this.basePath, "go");
}
getLogsPath(): string {
return join(this.basePath, "logs");
}

View File

@ -0,0 +1,457 @@
import { ConfigStore } from "./ConfigStore";
import { exec } from "child_process";
import { promisify } from "util";
import {
existsSync,
mkdirSync,
readdirSync,
rmSync,
unlinkSync,
renameSync,
} from "fs";
import { join } from "path";
import https from "https";
import http from "http";
import { createWriteStream } from "fs";
import unzipper from "unzipper";
import { sendDownloadProgress } from "../main";
const execAsync = promisify(exec);
interface GoVersion {
version: string;
path: string;
isActive: boolean;
}
interface AvailableGoVersion {
version: string;
stable: boolean;
downloadUrl: string;
filename: string;
}
export class GoManager {
private configStore: ConfigStore;
private versionsCache: AvailableGoVersion[] = [];
private cacheTime: number = 0;
private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 分钟缓存
constructor(configStore: ConfigStore) {
this.configStore = configStore;
}
/**
* Go
*/
async getInstalledVersions(): Promise<GoVersion[]> {
const versions: GoVersion[] = [];
const goPath = this.configStore.getGoPath();
const activeVersion = this.configStore.get("activeGoVersion") || "";
if (!existsSync(goPath)) {
return versions;
}
const dirs = readdirSync(goPath, { withFileTypes: true });
for (const dir of dirs) {
if (dir.isDirectory() && dir.name.startsWith("go-")) {
const versionDir = join(goPath, dir.name);
const goExe = join(versionDir, "bin", "go.exe");
if (existsSync(goExe)) {
const version = dir.name.replace("go-", "");
versions.push({
version,
path: versionDir,
isActive: version === activeVersion,
});
}
}
}
versions.sort((a, b) => {
const aParts = a.version.replace("go", "").split(".").map(Number);
const bParts = b.version.replace("go", "").split(".").map(Number);
for (let i = 0; i < 3; i++) {
if (aParts[i] !== bParts[i]) return bParts[i] - aParts[i];
}
return 0;
});
return versions;
}
/**
* go.dev
*/
async getAvailableVersions(): Promise<AvailableGoVersion[]> {
if (
this.versionsCache.length > 0 &&
Date.now() - this.cacheTime < this.CACHE_DURATION
) {
return this.versionsCache;
}
try {
const versions = await this.fetchVersionsFromGoDev();
if (versions.length > 0) {
this.versionsCache = versions;
this.cacheTime = Date.now();
return versions;
}
} catch (error) {
console.error("[Go] Failed to fetch versions:", error);
}
return this.getFallbackVersions();
}
/**
* go.dev/dl API
*/
private async fetchVersionsFromGoDev(): Promise<AvailableGoVersion[]> {
return new Promise((resolve, reject) => {
const url = "https://go.dev/dl/?mode=json";
https
.get(
url,
{
headers: { "User-Agent": "PHPer-Dev-Manager/1.0" },
timeout: 30000,
},
(res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
const releases = JSON.parse(data);
const versions: AvailableGoVersion[] = [];
for (const rel of releases) {
const winZip = rel.files?.find(
(f: any) =>
f.os === "windows" &&
f.arch === "amd64" &&
f.filename?.endsWith(".zip"),
);
if (winZip) {
versions.push({
version: rel.version,
stable: rel.stable ?? true,
downloadUrl: `https://go.dev/dl/${winZip.filename}`,
filename: winZip.filename,
});
}
}
resolve(versions.slice(0, 30));
} catch (e) {
reject(e);
}
});
},
)
.on("error", reject)
.on("timeout", () => reject(new Error("Request timeout")));
});
}
/**
* Go
*/
async install(
version: string,
downloadUrl: string,
): Promise<{ success: boolean; message: string }> {
try {
const goPath = this.configStore.getGoPath();
const tempPath = this.configStore.getTempPath();
const zipPath = join(tempPath, `go-${version}.zip`);
const extractDir = join(tempPath, `go-extract-${version}`);
const versionDir = join(goPath, `go-${version}`);
if (!existsSync(goPath)) mkdirSync(goPath, { recursive: true });
if (!existsSync(tempPath)) mkdirSync(tempPath, { recursive: true });
if (
existsSync(versionDir) &&
existsSync(join(versionDir, "bin", "go.exe"))
) {
return { success: false, message: `Go ${version} 已安装` };
}
console.log(`[Go] Downloading ${version}...`);
await this.downloadFile(downloadUrl, zipPath, `go-${version}`);
console.log(`[Go] Extracting ${version}...`);
await this.extractZip(zipPath, extractDir);
// Go zip 解压后根目录是 "go" 文件夹,需要重命名为 go-version
const innerGoDir = join(extractDir, "go");
if (existsSync(innerGoDir)) {
renameSync(innerGoDir, versionDir);
} else {
return { success: false, message: "解压失败:未找到 go 目录" };
}
try {
unlinkSync(zipPath);
rmSync(extractDir, { recursive: true, force: true });
} catch (e) {
// 忽略清理错误
}
if (!existsSync(join(versionDir, "bin", "go.exe"))) {
return { success: false, message: "安装失败go.exe 不存在" };
}
const goVersions = this.configStore.get("goVersions") || [];
if (!goVersions.includes(version)) {
goVersions.push(version);
this.configStore.set("goVersions", goVersions);
}
if (goVersions.length === 1) {
await this.setActive(version);
}
return { success: true, message: `Go ${version} 安装成功` };
} catch (error: any) {
return { success: false, message: `安装失败: ${error.message}` };
}
}
/**
* Go
*/
async uninstall(
version: string,
): Promise<{ success: boolean; message: string }> {
try {
const goPath = this.configStore.getGoPath();
const versionDir = join(goPath, `go-${version}`);
if (!existsSync(versionDir)) {
return { success: false, message: `Go ${version} 未安装` };
}
const activeVersion = this.configStore.get("activeGoVersion");
if (activeVersion === version) {
this.configStore.set("activeGoVersion", "");
}
rmSync(versionDir, { recursive: true, force: true });
const goVersions = this.configStore.get("goVersions") || [];
const index = goVersions.indexOf(version);
if (index > -1) {
goVersions.splice(index, 1);
this.configStore.set("goVersions", goVersions);
}
return { success: true, message: `Go ${version} 已卸载` };
} catch (error: any) {
return { success: false, message: `卸载失败: ${error.message}` };
}
}
/**
* Go
*/
async setActive(
version: string,
): Promise<{ success: boolean; message: string }> {
try {
const goPath = this.configStore.getGoPath();
const versionDir = join(goPath, `go-${version}`);
if (!existsSync(join(versionDir, "bin", "go.exe"))) {
return { success: false, message: `Go ${version} 未安装` };
}
await this.addToPath(versionDir);
this.configStore.set("activeGoVersion", version);
return { success: true, message: `已将 Go ${version} 设为默认版本` };
} catch (error: any) {
return { success: false, message: `设置失败: ${error.message}` };
}
}
/**
* Go
*/
async getGoInfo(
version: string,
): Promise<{ goVersion: string; path: string } | null> {
const goPath = this.configStore.getGoPath();
const versionDir = join(goPath, `go-${version}`);
const goExe = join(versionDir, "bin", "go.exe");
if (!existsSync(goExe)) return null;
try {
const { stdout } = await execAsync(`"${goExe}" version`, {
timeout: 5000,
});
return {
goVersion: stdout.trim(),
path: versionDir,
};
} catch {
return null;
}
}
private async downloadFile(
url: string,
dest: string,
name: string,
): Promise<void> {
return new Promise((resolve, reject) => {
const protocol = url.startsWith("https") ? https : http;
const request = protocol.get(
url,
{
headers: { "User-Agent": "PHPer-Dev-Manager/1.0" },
timeout: 600000,
},
(response) => {
if (response.statusCode === 301 || response.statusCode === 302) {
const redirectUrl = response.headers.location;
if (redirectUrl) {
this.downloadFile(redirectUrl, dest, name)
.then(resolve)
.catch(reject);
return;
}
}
if (response.statusCode !== 200) {
reject(new Error(`Download failed: HTTP ${response.statusCode}`));
return;
}
const totalSize = parseInt(
response.headers["content-length"] || "0",
10,
);
let downloadedSize = 0;
let lastProgressTime = 0;
const file = createWriteStream(dest);
response.on("data", (chunk) => {
downloadedSize += chunk.length;
const now = Date.now();
if (now - lastProgressTime > 500) {
const progress =
totalSize > 0
? Math.round((downloadedSize / totalSize) * 100)
: 0;
sendDownloadProgress("go", progress, downloadedSize, totalSize);
lastProgressTime = now;
}
});
response.pipe(file);
file.on("finish", () => {
file.close();
sendDownloadProgress("go", 100, totalSize, totalSize);
resolve();
});
file.on("error", (err) => {
unlinkSync(dest);
reject(err);
});
},
);
request.on("error", reject);
request.on("timeout", () => {
request.destroy();
reject(new Error("Download timeout"));
});
});
}
private async extractZip(zipPath: string, destDir: string): Promise<void> {
return new Promise((resolve, reject) => {
const { createReadStream } = require("fs");
createReadStream(zipPath)
.pipe(unzipper.Extract({ path: destDir }))
.on("close", resolve)
.on("error", reject);
});
}
private async addToPath(goPath: string): Promise<void> {
const binPath = join(goPath, "bin");
const psScript = `
$ErrorActionPreference = 'Stop'
$newPath = '${binPath.replace(/\\/g, "\\\\")}'
$currentPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$pathArray = $currentPath -split ';' | Where-Object { $_ -ne '' }
$filteredPaths = $pathArray | Where-Object {
$p = $_.ToLower()
-not ($p -like '*\\\\go-*\\\\bin*' -or $p -like '*\\\\go\\\\bin*' -or $p -like '*phper*go*')
}
$newPathArray = @($newPath) + $filteredPaths
$finalPath = ($newPathArray | Select-Object -Unique) -join ';'
[Environment]::SetEnvironmentVariable('Path', $finalPath, 'User')
Write-Output "PATH updated"
`;
const tempPs1 = join(this.configStore.getTempPath(), "update_go_path.ps1");
const { writeFileSync } = require("fs");
writeFileSync(tempPs1, psScript, "utf-8");
try {
await execAsync(`powershell -ExecutionPolicy Bypass -File "${tempPs1}"`, {
timeout: 30000,
});
} finally {
try {
unlinkSync(tempPs1);
} catch (e) {}
}
}
private getFallbackVersions(): AvailableGoVersion[] {
return [
{
version: "go1.25.7",
stable: true,
downloadUrl: "https://go.dev/dl/go1.25.7.windows-amd64.zip",
filename: "go1.25.7.windows-amd64.zip",
},
{
version: "go1.24.13",
stable: true,
downloadUrl: "https://go.dev/dl/go1.24.13.windows-amd64.zip",
filename: "go1.24.13.windows-amd64.zip",
},
{
version: "go1.23.5",
stable: true,
downloadUrl: "https://go.dev/dl/go1.23.5.windows-amd64.zip",
filename: "go1.23.5.windows-amd64.zip",
},
{
version: "go1.22.14",
stable: true,
downloadUrl: "https://go.dev/dl/go1.22.14.windows-amd64.zip",
filename: "go1.22.14.windows-amd64.zip",
},
];
}
}

View File

@ -1,6 +1,6 @@
{
"name": "phper-dev-manager",
"version": "1.0.8",
"version": "1.0.10",
"description": "PHP开发环境管理器 - 管理PHP、MySQL、Nginx、Redis服务",
"main": "dist-electron/main.js",
"scripts": {

View File

@ -1,5 +1,5 @@
{
"version": "1.0.8",
"buildTime": "2026-01-03T18:53:37.506Z",
"buildDate": "2026/1/4"
"version": "1.0.10",
"buildTime": "2026-02-05T01:05:37.725Z",
"buildDate": "2026/2/5"
}

View File

@ -29,20 +29,21 @@
<!-- 侧边栏 -->
<aside class="sidebar">
<nav class="nav-menu">
<router-link
v-for="item in menuItems"
:key="item.path"
<router-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
class="nav-item"
:class="{ active: $route.path === item.path }"
>
:class="{ active: $route.path === item.path }">
<el-icon class="nav-icon"><component :is="item.icon" /></el-icon>
<span class="nav-label">{{ item.label }}</span>
<span
v-if="item.service"
<span
v-if="item.service"
class="status-dot"
:class="{ running: serviceStatus[item.service as keyof typeof serviceStatus] }"
></span>
:class="{
running:
serviceStatus[item.service as keyof typeof serviceStatus],
}"></span>
</router-link>
</nav>
@ -73,299 +74,309 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useServiceStore } from './stores/serviceStore'
import { ref, computed, onMounted, onUnmounted } from "vue";
import { ElMessage } from "element-plus";
import { useServiceStore } from "./stores/serviceStore";
const store = useServiceStore()
const store = useServiceStore();
const isDark = ref(true)
const startingAll = ref(false)
const stoppingAll = ref(false)
const isDark = ref(true);
const startingAll = ref(false);
const stoppingAll = ref(false);
// -
const cachedViews = [
'Dashboard',
'PhpManager',
'MysqlManager',
'NginxManager',
'RedisManager',
'NodeManager',
'PythonManager',
'GitManager',
'SitesManager',
'HostsManager',
'Settings'
]
// -
const cachedViews = [
"Dashboard",
"PhpManager",
"MysqlManager",
"NginxManager",
"RedisManager",
"NodeManager",
"GoManager",
"PythonManager",
"GitManager",
"SitesManager",
"HostsManager",
"Settings",
];
// store
const serviceStatus = computed(() => ({
nginx: store.serviceStatus.nginx,
mysql: store.serviceStatus.mysql,
redis: store.serviceStatus.redis
}))
// store
const serviceStatus = computed(() => ({
nginx: store.serviceStatus.nginx,
mysql: store.serviceStatus.mysql,
redis: store.serviceStatus.redis,
}));
const menuItems = [
{ path: '/', label: '仪表盘', icon: 'Odometer', service: null },
{ path: '/php', label: 'PHP 管理', icon: 'Files', service: null },
{ path: '/mysql', label: 'MySQL 管理', icon: 'Coin', service: 'mysql' },
{ path: '/nginx', label: 'Nginx 管理', icon: 'Connection', service: 'nginx' },
{ path: '/redis', label: 'Redis 管理', icon: 'Grid', service: 'redis' },
{ path: '/nodejs', label: 'Node.js 管理', icon: 'Promotion', service: null },
{ path: '/python', label: 'Python 管理', icon: 'Platform', service: null },
{ path: '/git', label: 'Git 管理', icon: 'Share', service: null },
{ path: '/sites', label: '站点管理', icon: 'Monitor', service: null },
{ path: '/hosts', label: 'Hosts 管理', icon: 'Document', service: null },
{ path: '/settings', label: '设置', icon: 'Setting', service: null }
]
const menuItems = [
{ path: "/", label: "仪表盘", icon: "Odometer", service: null },
{ path: "/php", label: "PHP 管理", icon: "Files", service: null },
{ path: "/mysql", label: "MySQL 管理", icon: "Coin", service: "mysql" },
{
path: "/nginx",
label: "Nginx 管理",
icon: "Connection",
service: "nginx",
},
{ path: "/redis", label: "Redis 管理", icon: "Grid", service: "redis" },
{
path: "/nodejs",
label: "Node.js 管理",
icon: "Promotion",
service: null,
},
{ path: "/go", label: "Go 管理", icon: "Aim", service: null },
{ path: "/python", label: "Python 管理", icon: "Platform", service: null },
{ path: "/git", label: "Git 管理", icon: "Share", service: null },
{ path: "/sites", label: "站点管理", icon: "Monitor", service: null },
{ path: "/hosts", label: "Hosts 管理", icon: "Document", service: null },
{ path: "/settings", label: "设置", icon: "Setting", service: null },
];
let statusInterval: ReturnType<typeof setInterval> | null = null
let statusInterval: ReturnType<typeof setInterval> | null = null;
//
const minimize = () => window.electronAPI?.minimize()
const maximize = () => window.electronAPI?.maximize()
const close = () => window.electronAPI?.close()
//
const minimize = () => window.electronAPI?.minimize();
const maximize = () => window.electronAPI?.maximize();
const close = () => window.electronAPI?.close();
//
const toggleDark = () => {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
}
//
const toggleDark = () => {
isDark.value = !isDark.value;
document.documentElement.classList.toggle("dark", isDark.value);
};
//
const startAll = async () => {
startingAll.value = true
try {
const result = await window.electronAPI?.service.startAll()
if (result?.success) {
ElMessage.success(result.message)
//
setTimeout(() => store.refreshServiceStatus(), 2000)
} else {
ElMessage.error(result?.message || '启动失败')
//
const startAll = async () => {
startingAll.value = true;
try {
const result = await window.electronAPI?.service.startAll();
if (result?.success) {
ElMessage.success(result.message);
//
setTimeout(() => store.refreshServiceStatus(), 2000);
} else {
ElMessage.error(result?.message || "启动失败");
}
} catch (error: any) {
ElMessage.error(error.message);
} finally {
startingAll.value = false;
}
} catch (error: any) {
ElMessage.error(error.message)
} finally {
startingAll.value = false
}
}
};
//
const stopAll = async () => {
stoppingAll.value = true
try {
const result = await window.electronAPI?.service.stopAll()
if (result?.success) {
ElMessage.success(result.message)
await store.refreshServiceStatus()
} else {
ElMessage.error(result?.message || '停止失败')
//
const stopAll = async () => {
stoppingAll.value = true;
try {
const result = await window.electronAPI?.service.stopAll();
if (result?.success) {
ElMessage.success(result.message);
await store.refreshServiceStatus();
} else {
ElMessage.error(result?.message || "停止失败");
}
} catch (error: any) {
ElMessage.error(error.message);
} finally {
stoppingAll.value = false;
}
} catch (error: any) {
ElMessage.error(error.message)
} finally {
stoppingAll.value = false
}
}
};
onMounted(() => {
document.documentElement.classList.add('dark')
//
store.refreshAll()
// 5
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000)
})
onMounted(() => {
document.documentElement.classList.add("dark");
//
store.refreshAll();
// 5
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000);
});
onUnmounted(() => {
if (statusInterval) {
clearInterval(statusInterval)
}
})
onUnmounted(() => {
if (statusInterval) {
clearInterval(statusInterval);
}
});
</script>
<style lang="scss" scoped>
.app-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
}
.title-bar {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-titlebar);
border-bottom: 1px solid var(--border-color);
-webkit-app-region: drag;
padding: 0 12px;
}
.title-bar-left {
display: flex;
align-items: center;
gap: 12px;
}
.app-logo {
display: flex;
align-items: center;
gap: 8px;
.logo-icon {
width: 24px;
height: 24px;
}
.app-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
}
}
.title-bar-right {
display: flex;
gap: 4px;
-webkit-app-region: no-drag;
}
.title-btn {
width: 36px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
&:hover {
background: var(--bg-hover);
.app-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
}
&.close-btn:hover {
background: #e81123;
color: white;
}
}
.main-container {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 220px;
background: var(--bg-sidebar);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 16px 12px;
}
.nav-menu {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 10px;
text-decoration: none;
color: var(--text-secondary);
transition: all 0.2s;
&:hover {
background: var(--bg-hover);
color: var(--text-primary);
.title-bar {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-titlebar);
border-bottom: 1px solid var(--border-color);
-webkit-app-region: drag;
padding: 0 12px;
}
&.active {
background: var(--accent-gradient);
color: white;
box-shadow: 0 4px 12px rgba(123, 97, 255, 0.3);
.status-dot {
border-color: rgba(255, 255, 255, 0.3);
.title-bar-left {
display: flex;
align-items: center;
gap: 12px;
}
.app-logo {
display: flex;
align-items: center;
gap: 8px;
.logo-icon {
width: 24px;
height: 24px;
}
.app-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
}
}
.nav-icon {
font-size: 20px;
.title-bar-right {
display: flex;
gap: 4px;
-webkit-app-region: no-drag;
}
.nav-label {
font-size: 14px;
font-weight: 500;
.title-btn {
width: 36px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
&:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
&.close-btn:hover {
background: #e81123;
color: white;
}
}
.main-container {
flex: 1;
display: flex;
overflow: hidden;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6b7280;
border: 2px solid var(--bg-sidebar);
transition: all 0.3s;
&.running {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
.sidebar {
width: 220px;
background: var(--bg-sidebar);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 16px 12px;
}
.nav-menu {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 10px;
text-decoration: none;
color: var(--text-secondary);
transition: all 0.2s;
&:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
&.active {
background: var(--accent-gradient);
color: white;
box-shadow: 0 4px 12px rgba(123, 97, 255, 0.3);
.status-dot {
border-color: rgba(255, 255, 255, 0.3);
}
}
.nav-icon {
font-size: 20px;
}
.nav-label {
font-size: 14px;
font-weight: 500;
flex: 1;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6b7280;
border: 2px solid var(--bg-sidebar);
transition: all 0.3s;
&.running {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
}
}
}
}
.sidebar-footer {
padding-top: 16px;
border-top: 1px solid var(--border-color);
}
.quick-actions {
display: flex;
flex-direction: column;
gap: 10px;
padding: 0 12px;
:deep(.el-button) {
width: 100% !important;
height: 40px !important;
min-width: 100% !important;
max-width: 100% !important;
font-size: 14px !important;
justify-content: center !important;
border-radius: 8px !important;
padding: 0 16px !important;
margin-left: 0 !important;
.sidebar-footer {
padding-top: 16px;
border-top: 1px solid var(--border-color);
}
:deep(.el-button + .el-button) {
margin-left: 0 !important;
.quick-actions {
display: flex;
flex-direction: column;
gap: 10px;
padding: 0 12px;
:deep(.el-button) {
width: 100% !important;
height: 40px !important;
min-width: 100% !important;
max-width: 100% !important;
font-size: 14px !important;
justify-content: center !important;
border-radius: 8px !important;
padding: 0 16px !important;
margin-left: 0 !important;
}
:deep(.el-button + .el-button) {
margin-left: 0 !important;
}
}
}
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
background: var(--bg-content);
}
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
background: var(--bg-content);
}
</style>

View File

@ -1,76 +1,81 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { createRouter, createWebHashHistory } from "vue-router";
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
name: 'dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '仪表盘' }
path: "/",
name: "dashboard",
component: () => import("@/views/Dashboard.vue"),
meta: { title: "仪表盘" },
},
{
path: '/php',
name: 'php',
component: () => import('@/views/PhpManager.vue'),
meta: { title: 'PHP 管理' }
path: "/php",
name: "php",
component: () => import("@/views/PhpManager.vue"),
meta: { title: "PHP 管理" },
},
{
path: '/mysql',
name: 'mysql',
component: () => import('@/views/MysqlManager.vue'),
meta: { title: 'MySQL 管理' }
path: "/mysql",
name: "mysql",
component: () => import("@/views/MysqlManager.vue"),
meta: { title: "MySQL 管理" },
},
{
path: '/nginx',
name: 'nginx',
component: () => import('@/views/NginxManager.vue'),
meta: { title: 'Nginx 管理' }
path: "/nginx",
name: "nginx",
component: () => import("@/views/NginxManager.vue"),
meta: { title: "Nginx 管理" },
},
{
path: '/redis',
name: 'redis',
component: () => import('@/views/RedisManager.vue'),
meta: { title: 'Redis 管理' }
path: "/redis",
name: "redis",
component: () => import("@/views/RedisManager.vue"),
meta: { title: "Redis 管理" },
},
{
path: '/nodejs',
name: 'nodejs',
component: () => import('@/views/NodeManager.vue'),
meta: { title: 'Node.js 管理' }
path: "/nodejs",
name: "nodejs",
component: () => import("@/views/NodeManager.vue"),
meta: { title: "Node.js 管理" },
},
{
path: '/sites',
name: 'sites',
component: () => import('@/views/SitesManager.vue'),
meta: { title: '站点管理' }
path: "/go",
name: "go",
component: () => import("@/views/GoManager.vue"),
meta: { title: "Go 管理" },
},
{
path: '/hosts',
name: 'hosts',
component: () => import('@/views/HostsManager.vue'),
meta: { title: 'Hosts 管理' }
path: "/sites",
name: "sites",
component: () => import("@/views/SitesManager.vue"),
meta: { title: "站点管理" },
},
{
path: '/git',
name: 'git',
component: () => import('@/views/GitManager.vue'),
meta: { title: 'Git 管理' }
path: "/hosts",
name: "hosts",
component: () => import("@/views/HostsManager.vue"),
meta: { title: "Hosts 管理" },
},
{
path: '/python',
name: 'python',
component: () => import('@/views/PythonManager.vue'),
meta: { title: 'Python 管理' }
path: "/git",
name: "git",
component: () => import("@/views/GitManager.vue"),
meta: { title: "Git 管理" },
},
{
path: '/settings',
name: 'settings',
component: () => import('@/views/Settings.vue'),
meta: { title: '设置' }
}
]
})
export default router
path: "/python",
name: "python",
component: () => import("@/views/PythonManager.vue"),
meta: { title: "Python 管理" },
},
{
path: "/settings",
name: "settings",
component: () => import("@/views/Settings.vue"),
meta: { title: "设置" },
},
],
});
export default router;

439
src/views/GoManager.vue Normal file
View File

@ -0,0 +1,439 @@
<template>
<div class="page-container">
<div class="page-header">
<h1 class="page-title">
<span class="title-icon"
><el-icon><Aim /></el-icon
></span>
Go 管理
</h1>
<p class="page-description">管理本地 Go 版本支持多版本切换</p>
</div>
<!-- 下载进度 -->
<div
v-if="downloadProgress.percent > 0 && downloadProgress.percent < 100"
class="download-progress">
<div class="progress-info">
<span>正在下载 Go...</span>
<span
>{{ formatSize(downloadProgress.downloaded) }} /
{{ formatSize(downloadProgress.total) }}</span
>
</div>
<el-progress :percentage="downloadProgress.percent" :stroke-width="10" />
</div>
<!-- 已安装版本 -->
<div class="card">
<div class="card-header">
<span class="card-title">已安装版本</span>
<el-button type="primary" @click="showInstallDialog = true">
<el-icon><Plus /></el-icon>
安装新版本
</el-button>
</div>
<div class="card-content">
<div v-if="versions.length > 0" class="version-grid">
<div
v-for="version in versions"
:key="version.version"
class="version-card"
:class="{ active: version.isActive }">
<div class="version-main">
<div class="version-icon">
<el-icon :size="32"><Aim /></el-icon>
</div>
<div class="version-content">
<div class="version-title">
<span class="version-number">Go {{ version.version }}</span>
<el-tag
v-if="version.isActive"
type="success"
size="small"
effect="dark"
>当前版本</el-tag
>
</div>
</div>
</div>
<div class="version-actions">
<el-button
v-if="!version.isActive"
type="primary"
size="small"
@click="setActiveVersion(version.version)"
:loading="settingActive === version.version">
设为默认
</el-button>
<el-button
type="danger"
size="small"
plain
@click="uninstallVersion(version.version)"
:loading="uninstalling === version.version">
卸载
</el-button>
</div>
</div>
</div>
<el-empty v-else description="暂未安装 Go" />
</div>
</div>
<!-- 安装新版本对话框 -->
<el-dialog v-model="showInstallDialog" title="安装 Go" width="700px">
<el-alert type="info" :closable="false" class="mb-4">
<template #title>
<el-icon><InfoFilled /></el-icon>
下载源说明
</template>
Go 将从官方网站
<a href="https://go.dev/dl/" target="_blank">go.dev/dl</a> 下载 Windows
amd64 版本
</el-alert>
<div v-if="loadingAvailableVersions" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在获取可用版本列表...</span>
</div>
<div v-else-if="availableVersions.length === 0" class="empty-hint">
<span>暂无可用版本</span>
</div>
<div v-else class="available-versions">
<el-table
:data="availableVersions"
style="width: 100%"
max-height="400">
<el-table-column prop="version" label="版本" width="140" />
<el-table-column label="类型" width="100">
<template #default="{ row }">
<el-tag v-if="row.stable" type="success" size="small"
>Stable</el-tag
>
<el-tag v-else type="warning" size="small">Unstable</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button
v-if="!isInstalled(row.version)"
type="primary"
size="small"
@click="installVersion(row)"
:loading="installing === row.version">
安装
</el-button>
<el-tag v-else type="info" size="small">已安装</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="showInstallDialog = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Aim, InfoFilled, Loading } from "@element-plus/icons-vue";
defineOptions({
name: "GoManager",
});
interface GoVersion {
version: string;
path: string;
isActive: boolean;
}
interface AvailableGoVersion {
version: string;
stable: boolean;
downloadUrl: string;
filename: string;
}
const versions = ref<GoVersion[]>([]);
const availableVersions = ref<AvailableGoVersion[]>([]);
const showInstallDialog = ref(false);
const installing = ref("");
const uninstalling = ref("");
const settingActive = ref("");
const downloadProgress = reactive({
percent: 0,
downloaded: 0,
total: 0,
});
const loadVersions = async () => {
try {
versions.value = (await window.electronAPI?.go.getVersions()) || [];
} catch (error: any) {
console.error("加载版本失败:", error);
}
};
const loadingAvailableVersions = ref(false);
const loadAvailableVersions = async () => {
loadingAvailableVersions.value = true;
try {
availableVersions.value =
(await window.electronAPI?.go.getAvailableVersions()) || [];
} catch (error: any) {
console.error("加载可用版本失败:", error);
} finally {
loadingAvailableVersions.value = false;
}
};
const isInstalled = (version: string) => {
return versions.value.some((v) => v.version === version);
};
const installVersion = async (row: AvailableGoVersion) => {
installing.value = row.version;
downloadProgress.percent = 0;
downloadProgress.downloaded = 0;
downloadProgress.total = 0;
try {
const result = await window.electronAPI?.go.install(
row.version,
row.downloadUrl,
);
if (result?.success) {
ElMessage.success(result.message);
await loadVersions();
} else {
ElMessage.error(result?.message || "安装失败");
}
} catch (error: any) {
ElMessage.error(error.message);
} finally {
installing.value = "";
downloadProgress.percent = 0;
}
};
const uninstallVersion = async (version: string) => {
try {
await ElMessageBox.confirm(`确定要卸载 Go ${version} 吗?`, "确认卸载", {
type: "warning",
});
uninstalling.value = version;
const result = await window.electronAPI?.go.uninstall(version);
if (result?.success) {
ElMessage.success(result.message);
await loadVersions();
} else {
ElMessage.error(result?.message || "卸载失败");
}
} catch (error: any) {
if (error !== "cancel") {
ElMessage.error(error.message);
}
} finally {
uninstalling.value = "";
}
};
const setActiveVersion = async (version: string) => {
settingActive.value = version;
try {
const result = await window.electronAPI?.go.setActive(version);
if (result?.success) {
ElMessage.success(result.message);
await loadVersions();
} else {
ElMessage.error(result?.message || "设置失败");
}
} catch (error: any) {
ElMessage.error(error.message);
} finally {
settingActive.value = "";
}
};
const formatSize = (bytes: number) => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
const onDownloadProgress = (data: any) => {
if (data.type === "go") {
downloadProgress.percent = data.progress;
downloadProgress.downloaded = data.downloaded;
downloadProgress.total = data.total;
}
};
onMounted(() => {
loadVersions();
loadAvailableVersions();
window.electronAPI?.onDownloadProgress(onDownloadProgress);
});
onUnmounted(() => {
window.electronAPI?.removeDownloadProgressListener(onDownloadProgress);
});
</script>
<style lang="scss" scoped>
.version-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
.version-card {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
transition: all 0.3s;
display: flex;
flex-direction: column;
gap: 20px;
&:hover {
border-color: var(--accent-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
&.active {
border-color: var(--success-color);
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.08) 0%,
rgba(16, 185, 129, 0.02) 100%
);
.version-icon {
background: linear-gradient(135deg, #00add8 0%, #00add8 100%);
}
}
.version-main {
display: flex;
align-items: flex-start;
gap: 16px;
}
.version-icon {
width: 56px;
height: 56px;
border-radius: 14px;
background: linear-gradient(135deg, #00add8 0%, #00add8 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
.version-content {
flex: 1;
min-width: 0;
}
.version-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.version-number {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.5px;
}
.version-actions {
display: flex;
gap: 10px;
padding-top: 4px;
border-top: 1px solid var(--border-color);
.el-button {
flex: 1;
}
}
}
.available-versions {
.el-table {
--el-table-bg-color: transparent;
--el-table-tr-bg-color: transparent;
--el-table-header-bg-color: var(--bg-input);
}
}
.download-progress {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
color: var(--text-secondary);
}
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px;
color: var(--text-secondary);
.is-loading {
font-size: 24px;
animation: spin 1s linear infinite;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.mb-4 {
margin-bottom: 16px;
a {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.empty-hint {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
</style>