Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4abce26cf2 | |||
| 982a51ef52 | |||
| 11ad2cf722 | |||
| 1189177568 | |||
| 965bc618ee | |||
| 2e06bc5277 | |||
| d8faf27108 | |||
| dac4cc805e |
52
README.md
52
README.md
@ -63,13 +63,16 @@
|
||||
### 🐘 PHP 版本管理
|
||||
|
||||
| 功能 | 说明 |
|
||||
| ---------- | ---------------------------------------------------------- |
|
||||
| ------------ | ---------------------------------------------------------- |
|
||||
| 多版本管理 | 支持同时安装 PHP 8.1、8.2、8.3、8.4、8.5 等多个版本 |
|
||||
| CGI 独立控制 | 每个 PHP 版本可独立启动/停止 CGI 进程,支持多版本并行运行 |
|
||||
| 端口自动分配 | 各版本自动分配端口(如 8.4→9084, 8.3→9083) |
|
||||
| 一键切换 | 点击即可切换 PHP 版本,自动配置系统环境变量 |
|
||||
| 扩展管理 | 可视化管理 PHP 扩展,支持在线安装(从 PECL) |
|
||||
| 配置编辑 | 在线编辑 php.ini,无需手动查找配置文件 |
|
||||
| 自动配置 | 安装时自动启用常用扩展(curl、gd、mbstring、pdo_mysql 等) |
|
||||
| Composer | 集成 Composer 管理,支持镜像源切换(阿里云、腾讯云等) |
|
||||
| 日志查看 | 直接查看 PHP 错误日志 |
|
||||
| 下载源 | 从 [windows.php.net](https://windows.php.net) 官方下载 |
|
||||
|
||||
### 🐬 MySQL 管理
|
||||
@ -139,15 +142,31 @@
|
||||
- 🎯 **Laravel 一键配置** - 自动配置 public 目录和伪静态规则
|
||||
- 🔒 **SSL 证书申请** - 集成 Let's Encrypt 自动申请
|
||||
- 📝 **Hosts 自动配置** - 自动添加域名到系统 hosts 文件
|
||||
- 📋 **站点日志查看** - 查看每个站点的访问日志和错误日志
|
||||
- 🌐 **一键打开站点** - 点击域名在默认浏览器打开
|
||||
|
||||
### 📋 日志查看
|
||||
|
||||
| 功能 | 说明 |
|
||||
| ------------ | ---------------------------------------------- |
|
||||
| 多服务日志 | 支持查看 Nginx、PHP、MySQL、Redis 日志 |
|
||||
| 站点日志 | 查看各站点的访问日志和错误日志 |
|
||||
| 实时刷新 | 支持刷新日志内容,查看最新记录 |
|
||||
| 行数控制 | 可配置显示的日志行数(100-5000 行) |
|
||||
| 快速清空 | 一键清空指定日志文件 |
|
||||
| 打开目录 | 快速在文件管理器中打开日志目录 |
|
||||
|
||||
### ⚙️ 其他功能
|
||||
|
||||
- 🚀 **开机自启动** - 可配置各服务开机自动启动
|
||||
- 🚀 **开机自启动** - 可配置各服务开机自动启动(静默模式,无弹窗)
|
||||
- 🔇 **静默启动** - 所有服务启动无黑色窗口闪烁
|
||||
- 📋 **Hosts 管理** - 可视化管理系统 hosts 文件
|
||||
- 🌙 **深色/浅色主题** - 支持主题切换
|
||||
- 📊 **服务状态监控** - 实时显示各服务运行状态
|
||||
- ⏳ **加载状态提示** - 版本列表加载时显示 Loading 状态
|
||||
- ⚡ **页面切换优化** - 使用 KeepAlive 缓存页面,切换无闪烁
|
||||
- 🔢 **自动版本号** - 打包时自动更新版本号
|
||||
- 📥 **下载源说明** - 清晰显示各软件的下载来源
|
||||
- 🌐 **默认浏览器打开** - 站点链接自动在默认浏览器打开
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
@ -186,8 +205,16 @@ npm run electron:dev
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
# 构建 Windows 安装包
|
||||
npm run electron:build
|
||||
# 构建 Windows 安装包(自动更新 patch 版本号 +0.0.1)
|
||||
npm run build
|
||||
|
||||
# 指定版本号更新类型
|
||||
npm run build:patch # 1.0.0 -> 1.0.1
|
||||
npm run build:minor # 1.0.0 -> 1.1.0
|
||||
npm run build:major # 1.0.0 -> 2.0.0
|
||||
|
||||
# 不更新版本号直接打包
|
||||
npm run build:nobump
|
||||
```
|
||||
|
||||
构建完成后,安装包将生成在 `release` 目录中。
|
||||
@ -209,14 +236,19 @@ phper/
|
||||
│ ├── PythonManager.ts # Python 版本管理器
|
||||
│ ├── GitManager.ts # Git 管理器
|
||||
│ ├── ServiceManager.ts # 开机自启服务管理器
|
||||
│ └── HostsManager.ts # Hosts 文件管理器
|
||||
│ ├── HostsManager.ts # Hosts 文件管理器
|
||||
│ └── LogManager.ts # 日志管理器
|
||||
│
|
||||
├── src/ # Vue 前端源码
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── App.vue # 根组件(含 KeepAlive 缓存)
|
||||
│ ├── main.ts # 入口文件
|
||||
│ ├── vite-env.d.ts # 类型声明
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ └── index.ts
|
||||
│ ├── stores/ # Pinia 状态管理
|
||||
│ │ └── serviceStore.ts # 服务状态存储
|
||||
│ ├── components/ # 公共组件
|
||||
│ │ └── LogViewer.vue # 日志查看器组件
|
||||
│ ├── styles/ # 样式文件
|
||||
│ │ └── main.scss # 全局样式(含主题变量)
|
||||
│ └── views/ # 页面视图
|
||||
@ -232,8 +264,12 @@ phper/
|
||||
│ ├── HostsManager.vue # Hosts 管理
|
||||
│ └── Settings.vue # 设置
|
||||
│
|
||||
├── scripts/ # 构建脚本
|
||||
│ └── bump-version.js # 版本号自动更新脚本
|
||||
│
|
||||
├── public/ # 静态资源
|
||||
│ └── icon.svg # 应用图标
|
||||
│ ├── icon.svg # 应用图标
|
||||
│ └── version.json # 版本信息(构建时生成)
|
||||
│
|
||||
├── index.html # HTML 模板
|
||||
├── package.json # 项目配置
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
|
||||
exports.default = async function(context) {
|
||||
// 只在 Windows 上执行
|
||||
@ -9,35 +7,14 @@ exports.default = async function(context) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Running afterPack hook to set icon...');
|
||||
console.log('Running afterPack hook to set icon and version info...');
|
||||
|
||||
const appOutDir = context.appOutDir;
|
||||
const productName = context.packager.appInfo.productName;
|
||||
const version = context.packager.appInfo.version;
|
||||
const exePath = path.join(appOutDir, `${productName}.exe`);
|
||||
const iconPath = path.join(__dirname, 'icon.ico');
|
||||
|
||||
// rcedit 路径
|
||||
const userHome = os.homedir();
|
||||
const cacheDir = path.join(userHome, 'AppData', 'Local', 'electron-builder', 'Cache', 'winCodeSign');
|
||||
|
||||
// 查找 rcedit
|
||||
let rceditPath = null;
|
||||
if (fs.existsSync(cacheDir)) {
|
||||
const dirs = fs.readdirSync(cacheDir);
|
||||
for (const dir of dirs) {
|
||||
const possiblePath = path.join(cacheDir, dir, 'rcedit-x64.exe');
|
||||
if (fs.existsSync(possiblePath)) {
|
||||
rceditPath = possiblePath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!rceditPath) {
|
||||
console.warn('rcedit not found, skipping icon modification');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(exePath)) {
|
||||
console.warn(`Exe not found: ${exePath}`);
|
||||
return;
|
||||
@ -49,17 +26,29 @@ exports.default = async function(context) {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Setting icon for: ${exePath}`);
|
||||
console.log(`Using icon: ${iconPath}`);
|
||||
console.log(`Using rcedit: ${rceditPath}`);
|
||||
// rcedit 是默认导出
|
||||
const rcedit = require('rcedit');
|
||||
|
||||
execSync(`"${rceditPath}" "${exePath}" --set-icon "${iconPath}"`, {
|
||||
stdio: 'inherit'
|
||||
console.log(`Setting icon and version info for: ${exePath}`);
|
||||
console.log(`Using icon: ${iconPath}`);
|
||||
|
||||
await rcedit(exePath, {
|
||||
icon: iconPath,
|
||||
'version-string': {
|
||||
'ProductName': productName,
|
||||
'FileDescription': productName,
|
||||
'CompanyName': 'PHPer',
|
||||
'LegalCopyright': 'Copyright © 2024 PHPer',
|
||||
'OriginalFilename': `${productName}.exe`,
|
||||
'InternalName': productName
|
||||
},
|
||||
'file-version': version,
|
||||
'product-version': version
|
||||
});
|
||||
|
||||
console.log('Icon set successfully!');
|
||||
console.log('Icon and version info set successfully!');
|
||||
} catch (error) {
|
||||
console.error('Failed to set icon:', error.message);
|
||||
// 不阻止打包继续
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
251
electron/main.ts
251
electron/main.ts
@ -13,10 +13,12 @@ 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";
|
||||
import { PythonManager } from "./services/PythonManager";
|
||||
import { LogManager } from "./services/LogManager";
|
||||
import { ConfigStore } from "./services/ConfigStore";
|
||||
|
||||
// 获取图标路径
|
||||
@ -97,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", {
|
||||
@ -116,10 +118,12 @@ 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);
|
||||
const pythonManager = new PythonManager(configStore);
|
||||
const logManager = new LogManager(configStore);
|
||||
|
||||
function createWindow() {
|
||||
const appIcon = createWindowIcon();
|
||||
@ -239,6 +243,15 @@ function createTray() {
|
||||
});
|
||||
}
|
||||
|
||||
// 设置应用名称和 Windows AppUserModelId(用于任务栏图标分组和进程名称显示)
|
||||
const APP_NAME = "PHPer开发环境管理器";
|
||||
const APP_ID = "com.phper.devmanager";
|
||||
|
||||
app.setName(APP_NAME);
|
||||
if (process.platform === "win32") {
|
||||
app.setAppUserModelId(APP_ID);
|
||||
}
|
||||
|
||||
// 单实例锁定
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
@ -304,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));
|
||||
|
||||
@ -321,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",
|
||||
@ -356,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 管理 ====================
|
||||
@ -371,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());
|
||||
@ -435,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());
|
||||
@ -475,109 +488,139 @@ 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),
|
||||
);
|
||||
|
||||
// ==================== 应用设置 ====================
|
||||
// 设置开机自启(以管理员模式,使用任务计划程序)
|
||||
// 设置开机自启(以管理员模式,使用任务计划程序,静默启动)
|
||||
ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
|
||||
const { execSync } = require("child_process");
|
||||
const { execSync, exec } = require("child_process");
|
||||
const { writeFileSync, unlinkSync, existsSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
const exePath = app.getPath("exe");
|
||||
const taskName = "PHPerDevManager";
|
||||
|
||||
@ -601,12 +644,18 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
|
||||
// 忽略删除失败(可能任务不存在)
|
||||
}
|
||||
|
||||
// 创建任务计划程序任务,以最高权限运行
|
||||
const command = `schtasks /create /tn "${taskName}" /tr "\\"${exePath}\\"" /sc onlogon /rl highest /f`;
|
||||
// 创建 VBS 启动脚本(确保静默启动)
|
||||
const appDir = require("path").dirname(exePath);
|
||||
const vbsPath = join(appDir, "silent_start.vbs");
|
||||
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run """${exePath.replace(/\\/g, "\\\\")}""", 0, False`;
|
||||
writeFileSync(vbsPath, vbsContent);
|
||||
|
||||
// 创建任务计划程序任务,运行 VBS 脚本实现静默启动
|
||||
const command = `schtasks /create /tn "${taskName}" /tr "wscript.exe \\"${vbsPath}\\"" /sc onlogon /rl highest /f`;
|
||||
execSync(command, { encoding: "buffer", windowsHide: true });
|
||||
|
||||
configStore.set("autoLaunch", true);
|
||||
return { success: true, message: "已启用开机自启(管理员模式)" };
|
||||
return { success: true, message: "已启用开机自启(静默模式)" };
|
||||
} else {
|
||||
// 删除任务计划程序任务
|
||||
try {
|
||||
@ -617,6 +666,18 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
|
||||
} catch (e) {
|
||||
// 忽略删除失败
|
||||
}
|
||||
|
||||
// 删除 VBS 脚本
|
||||
const appDir = require("path").dirname(exePath);
|
||||
const vbsPath = join(appDir, "silent_start.vbs");
|
||||
if (existsSync(vbsPath)) {
|
||||
try {
|
||||
unlinkSync(vbsPath);
|
||||
} catch (e) {
|
||||
// 忽略删除失败
|
||||
}
|
||||
}
|
||||
|
||||
configStore.set("autoLaunch", false);
|
||||
return { success: true, message: "已禁用开机自启" };
|
||||
}
|
||||
@ -650,6 +711,38 @@ 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 || "";
|
||||
buildDate = versionInfo.buildDate || "";
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略错误
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
buildTime,
|
||||
buildDate,
|
||||
isPackaged: app.isPackaged,
|
||||
};
|
||||
});
|
||||
|
||||
// 设置启动时最小化到托盘
|
||||
ipcMain.handle("app:setStartMinimized", (_, enabled: boolean) => {
|
||||
configStore.set("startMinimized", enabled);
|
||||
@ -666,3 +759,17 @@ ipcMain.handle("app:quit", () => {
|
||||
isQuitting = true;
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// ==================== 日志管理 ====================
|
||||
ipcMain.handle("log:getFiles", () => logManager.getLogFiles());
|
||||
ipcMain.handle("log:read", (_, logPath: string, lines?: number) =>
|
||||
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),
|
||||
);
|
||||
|
||||
@ -1,210 +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),
|
||||
},
|
||||
|
||||
// 应用设置
|
||||
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'),
|
||||
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,
|
||||
};
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
457
electron/services/GoManager.ts
Normal file
457
electron/services/GoManager.ts
Normal 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",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
277
electron/services/LogManager.ts
Normal file
277
electron/services/LogManager.ts
Normal file
@ -0,0 +1,277 @@
|
||||
import { ConfigStore } from './ConfigStore'
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
||||
import { join, basename } from 'path'
|
||||
|
||||
export interface LogFile {
|
||||
name: string
|
||||
path: string
|
||||
size: number
|
||||
modifiedTime: Date
|
||||
type: 'nginx' | 'nginx-error' | 'nginx-access' | 'php' | 'mysql' | 'mysql-error' | 'site-access' | 'site-error'
|
||||
}
|
||||
|
||||
export interface LogContent {
|
||||
content: string
|
||||
totalLines: number
|
||||
fileSize: number
|
||||
}
|
||||
|
||||
export class LogManager {
|
||||
private configStore: ConfigStore
|
||||
|
||||
constructor(configStore: ConfigStore) {
|
||||
this.configStore = configStore
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的日志文件列表
|
||||
*/
|
||||
async getLogFiles(): Promise<{ nginx: LogFile[], php: LogFile[], mysql: LogFile[], sites: LogFile[] }> {
|
||||
const result = {
|
||||
nginx: [] as LogFile[],
|
||||
php: [] as LogFile[],
|
||||
mysql: [] as LogFile[],
|
||||
sites: [] as LogFile[]
|
||||
}
|
||||
|
||||
// Nginx 日志
|
||||
const nginxPath = this.configStore.getNginxPath()
|
||||
const nginxLogsDir = join(nginxPath, 'logs')
|
||||
if (existsSync(nginxLogsDir)) {
|
||||
const files = this.scanLogDir(nginxLogsDir)
|
||||
for (const file of files) {
|
||||
if (file.name.includes('error')) {
|
||||
result.nginx.push({ ...file, type: 'nginx-error' })
|
||||
} else if (file.name.includes('access')) {
|
||||
result.nginx.push({ ...file, type: 'nginx-access' })
|
||||
} else {
|
||||
result.nginx.push({ ...file, type: 'nginx' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PHP 日志 - 检查每个 PHP 版本的日志
|
||||
const phpVersions = this.configStore.get('phpVersions') || []
|
||||
for (const version of phpVersions) {
|
||||
const phpPath = this.configStore.getPhpPath(version)
|
||||
const phpLogsDir = join(phpPath, 'logs')
|
||||
if (existsSync(phpLogsDir)) {
|
||||
const files = this.scanLogDir(phpLogsDir)
|
||||
for (const file of files) {
|
||||
result.php.push({ ...file, type: 'php', name: `[${version}] ${file.name}` })
|
||||
}
|
||||
}
|
||||
// 也检查 php.ini 中配置的 error_log
|
||||
const phpErrorLog = join(phpPath, 'php_errors.log')
|
||||
if (existsSync(phpErrorLog)) {
|
||||
const stat = statSync(phpErrorLog)
|
||||
result.php.push({
|
||||
name: `[${version}] php_errors.log`,
|
||||
path: phpErrorLog,
|
||||
size: stat.size,
|
||||
modifiedTime: stat.mtime,
|
||||
type: 'php'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MySQL 日志
|
||||
const mysqlVersions = this.configStore.get('mysqlVersions') || []
|
||||
for (const version of mysqlVersions) {
|
||||
const mysqlPath = this.configStore.getMysqlPath(version)
|
||||
// MySQL 日志通常在 data 目录下
|
||||
const mysqlDataDir = join(mysqlPath, 'data')
|
||||
if (existsSync(mysqlDataDir)) {
|
||||
const files = readdirSync(mysqlDataDir)
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.err') || file.endsWith('.log')) {
|
||||
const filePath = join(mysqlDataDir, file)
|
||||
const stat = statSync(filePath)
|
||||
const logType = file.includes('error') || file.endsWith('.err') ? 'mysql-error' : 'mysql'
|
||||
result.mysql.push({
|
||||
name: `[${version}] ${file}`,
|
||||
path: filePath,
|
||||
size: stat.size,
|
||||
modifiedTime: stat.mtime,
|
||||
type: logType
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// 也检查 logs 目录
|
||||
const mysqlLogsDir = join(mysqlPath, 'logs')
|
||||
if (existsSync(mysqlLogsDir)) {
|
||||
const files = this.scanLogDir(mysqlLogsDir)
|
||||
for (const file of files) {
|
||||
result.mysql.push({ ...file, type: 'mysql', name: `[${version}] ${file.name}` })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 站点日志 - Nginx sites logs
|
||||
const sites = this.configStore.get('sites') || []
|
||||
for (const site of sites) {
|
||||
// 站点日志通常在 nginx/logs 目录下,以域名命名
|
||||
const siteAccessLog = join(nginxLogsDir, `${site.domain}.access.log`)
|
||||
const siteErrorLog = join(nginxLogsDir, `${site.domain}.error.log`)
|
||||
|
||||
if (existsSync(siteAccessLog)) {
|
||||
const stat = statSync(siteAccessLog)
|
||||
result.sites.push({
|
||||
name: `${site.domain} - 访问日志`,
|
||||
path: siteAccessLog,
|
||||
size: stat.size,
|
||||
modifiedTime: stat.mtime,
|
||||
type: 'site-access'
|
||||
})
|
||||
}
|
||||
|
||||
if (existsSync(siteErrorLog)) {
|
||||
const stat = statSync(siteErrorLog)
|
||||
result.sites.push({
|
||||
name: `${site.domain} - 错误日志`,
|
||||
path: siteErrorLog,
|
||||
size: stat.size,
|
||||
modifiedTime: stat.mtime,
|
||||
type: 'site-error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取日志文件内容
|
||||
* @param logPath 日志文件路径
|
||||
* @param lines 读取的行数(从末尾开始),默认 500 行
|
||||
*/
|
||||
async readLog(logPath: string, lines: number = 500): Promise<LogContent> {
|
||||
if (!existsSync(logPath)) {
|
||||
return { content: '日志文件不存在', totalLines: 0, fileSize: 0 }
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = statSync(logPath)
|
||||
const fileSize = stat.size
|
||||
|
||||
// 如果文件小于 1MB,直接读取全部内容
|
||||
if (fileSize < 1024 * 1024) {
|
||||
const content = readFileSync(logPath, 'utf-8')
|
||||
const allLines = content.split('\n')
|
||||
const totalLines = allLines.length
|
||||
|
||||
// 取最后 N 行
|
||||
const lastLines = allLines.slice(-lines).join('\n')
|
||||
return { content: lastLines, totalLines, fileSize }
|
||||
}
|
||||
|
||||
// 大文件:从末尾读取
|
||||
const content = await this.readLastLines(logPath, lines)
|
||||
return { content, totalLines: lines, fileSize }
|
||||
} catch (error: any) {
|
||||
return { content: `读取日志失败: ${error.message}`, totalLines: 0, fileSize: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件末尾读取指定行数
|
||||
*/
|
||||
private async readLastLines(filePath: string, lines: number): Promise<string> {
|
||||
const fs = await import('fs/promises')
|
||||
const stat = await fs.stat(filePath)
|
||||
const fileSize = stat.size
|
||||
|
||||
// 估算需要读取的字节数(假设每行约 200 字节)
|
||||
const bytesToRead = Math.min(fileSize, lines * 200)
|
||||
const startPosition = Math.max(0, fileSize - bytesToRead)
|
||||
|
||||
const buffer = Buffer.alloc(bytesToRead)
|
||||
const fd = await fs.open(filePath, 'r')
|
||||
await fd.read(buffer, 0, bytesToRead, startPosition)
|
||||
await fd.close()
|
||||
|
||||
const content = buffer.toString('utf-8')
|
||||
const allLines = content.split('\n')
|
||||
|
||||
// 第一行可能不完整,跳过
|
||||
const completeLines = startPosition === 0 ? allLines : allLines.slice(1)
|
||||
|
||||
return completeLines.slice(-lines).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空日志文件
|
||||
*/
|
||||
async clearLog(logPath: string): Promise<{ success: boolean, message: string }> {
|
||||
if (!existsSync(logPath)) {
|
||||
return { success: false, message: '日志文件不存在' }
|
||||
}
|
||||
|
||||
try {
|
||||
const fs = await import('fs/promises')
|
||||
await fs.writeFile(logPath, '')
|
||||
return { success: true, message: '日志已清空' }
|
||||
} catch (error: any) {
|
||||
return { success: false, message: `清空日志失败: ${error.message}` }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描目录中的日志文件
|
||||
*/
|
||||
private scanLogDir(dir: string): LogFile[] {
|
||||
const files: LogFile[] = []
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
return files
|
||||
}
|
||||
|
||||
try {
|
||||
const items = readdirSync(dir)
|
||||
for (const item of items) {
|
||||
const filePath = join(dir, item)
|
||||
const stat = statSync(filePath)
|
||||
|
||||
if (stat.isFile() && (item.endsWith('.log') || item.endsWith('.err'))) {
|
||||
files.push({
|
||||
name: item,
|
||||
path: filePath,
|
||||
size: stat.size,
|
||||
modifiedTime: stat.mtime,
|
||||
type: 'nginx' // 默认类型,调用方会覆盖
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('扫描日志目录失败:', error)
|
||||
}
|
||||
|
||||
return files.sort((a, b) => b.modifiedTime.getTime() - a.modifiedTime.getTime())
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志文件路径(用于在文件管理器中打开)
|
||||
*/
|
||||
getLogDirectory(type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string): string {
|
||||
switch (type) {
|
||||
case 'nginx':
|
||||
return join(this.configStore.getNginxPath(), 'logs')
|
||||
case 'php':
|
||||
if (version) {
|
||||
return join(this.configStore.getPhpPath(version), 'logs')
|
||||
}
|
||||
return join(this.configStore.getBasePath(), 'php')
|
||||
case 'mysql':
|
||||
if (version) {
|
||||
return join(this.configStore.getMysqlPath(version), 'data')
|
||||
}
|
||||
return join(this.configStore.getBasePath(), 'mysql')
|
||||
case 'sites':
|
||||
return join(this.configStore.getNginxPath(), 'logs')
|
||||
default:
|
||||
return this.configStore.getBasePath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -331,9 +331,12 @@ export class NginxManager {
|
||||
return { success: true, message: 'Nginx 已经在运行' }
|
||||
}
|
||||
|
||||
// 启动 Nginx
|
||||
const child = spawn(nginxExe, [], {
|
||||
cwd: nginxPath,
|
||||
// 使用 VBScript 静默启动 Nginx
|
||||
const vbsPath = join(nginxPath, 'start_nginx.vbs')
|
||||
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.CurrentDirectory = "${nginxPath.replace(/\\/g, '\\\\')}"\nWshShell.Run """${nginxExe.replace(/\\/g, '\\\\')}""", 0, False`
|
||||
writeFileSync(vbsPath, vbsContent)
|
||||
|
||||
const child = spawn('wscript.exe', [vbsPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -307,15 +307,16 @@ export class RedisManager {
|
||||
await this.createDefaultConfig()
|
||||
}
|
||||
|
||||
// 使用相对路径启动(避免 Cygwin 路径问题)
|
||||
// Redis Windows 版本使用 Cygwin,需要在正确的工作目录下用相对路径
|
||||
// 使用 VBScript 静默启动 Redis(避免黑窗口闪烁)
|
||||
const configFileName = 'redis.windows.conf'
|
||||
const child = spawn(redisServer, [configFileName], {
|
||||
cwd: redisPath,
|
||||
const vbsPath = join(redisPath, 'start_redis.vbs')
|
||||
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.CurrentDirectory = "${redisPath.replace(/\\/g, '\\\\')}"\nWshShell.Run """${redisServer.replace(/\\/g, '\\\\')}""" & " " & "${configFileName}", 0, False`
|
||||
writeFileSync(vbsPath, vbsContent)
|
||||
|
||||
const child = spawn('wscript.exe', [vbsPath], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
shell: false
|
||||
windowsHide: true
|
||||
})
|
||||
child.unref()
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ConfigStore } from './ConfigStore'
|
||||
import { exec, spawn } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync } from 'fs'
|
||||
import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
@ -562,6 +562,32 @@ export class ServiceManager {
|
||||
}
|
||||
|
||||
private async startProcess(exe: string, args: string[], cwd: string): Promise<void> {
|
||||
// 使用 VBScript 来完全隐藏窗口启动进程
|
||||
const argsStr = args.map(a => `"${a}"`).join(' ')
|
||||
const command = args.length > 0 ? `"${exe}" ${argsStr}` : `"${exe}"`
|
||||
|
||||
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run ${JSON.stringify(command)}, 0, False`
|
||||
const vbsPath = join(cwd, `start_${Date.now()}.vbs`)
|
||||
|
||||
try {
|
||||
writeFileSync(vbsPath, vbsContent)
|
||||
await execAsync(`cscript //nologo "${vbsPath}"`, {
|
||||
cwd,
|
||||
windowsHide: true,
|
||||
timeout: 10000
|
||||
})
|
||||
// 延迟删除 VBS 文件
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (existsSync(vbsPath)) {
|
||||
unlinkSync(vbsPath)
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略删除失败
|
||||
}
|
||||
}, 2000)
|
||||
} catch (error) {
|
||||
// 如果 VBS 方式失败,回退到 spawn
|
||||
const child = spawn(exe, args, {
|
||||
cwd,
|
||||
detached: true,
|
||||
@ -570,5 +596,6 @@ export class ServiceManager {
|
||||
})
|
||||
child.unref()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
232
package-lock.json
generated
232
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "phper-dev-manager",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "phper-dev-manager",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
@ -26,12 +26,13 @@
|
||||
"concurrently": "^8.2.2",
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"rcedit": "^5.0.2",
|
||||
"sass": "^1.69.5",
|
||||
"typescript": "^5.3.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-electron": "^0.15.5",
|
||||
"vite-plugin-electron-renderer": "^0.14.5",
|
||||
"vue-tsc": "^1.8.25",
|
||||
"vue-tsc": "^3.2.1",
|
||||
"wait-on": "^7.2.0"
|
||||
}
|
||||
},
|
||||
@ -1880,34 +1881,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz",
|
||||
"integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
|
||||
"version": "2.4.27",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz",
|
||||
"integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/source-map": "1.11.1"
|
||||
"@volar/source-map": "2.4.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/source-map": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz",
|
||||
"integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
|
||||
"version": "2.4.27",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.27.tgz",
|
||||
"integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"muggle-string": "^0.3.1"
|
||||
}
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz",
|
||||
"integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
|
||||
"version": "2.4.27",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.27.tgz",
|
||||
"integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "1.11.1",
|
||||
"path-browserify": "^1.0.1"
|
||||
"@volar/language-core": "2.4.27",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vscode-uri": "^3.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
@ -1967,45 +1966,32 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "1.8.27",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz",
|
||||
"integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.1.tgz",
|
||||
"integrity": "sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "~1.11.1",
|
||||
"@volar/source-map": "~1.11.1",
|
||||
"@vue/compiler-dom": "^3.3.0",
|
||||
"@vue/shared": "^3.3.0",
|
||||
"computeds": "^0.0.1",
|
||||
"minimatch": "^9.0.3",
|
||||
"muggle-string": "^0.3.1",
|
||||
"@volar/language-core": "2.4.27",
|
||||
"@vue/compiler-dom": "^3.5.0",
|
||||
"@vue/shared": "^3.5.0",
|
||||
"alien-signals": "^3.0.0",
|
||||
"muggle-string": "^0.4.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
"picomatch": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"node_modules/@vue/language-core/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
@ -2191,6 +2177,13 @@
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
|
||||
"integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
@ -2931,13 +2924,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/computeds": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
|
||||
"integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -3148,6 +3134,31 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn-windows-exe": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz",
|
||||
"integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/malept"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund"
|
||||
}
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@malept/cross-spawn-promise": "^1.1.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@ -3177,13 +3188,6 @@
|
||||
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debounce-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
|
||||
@ -4466,16 +4470,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
|
||||
@ -4629,6 +4623,22 @@
|
||||
"is-ci": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@ -4683,6 +4693,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@ -5167,9 +5190,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz",
|
||||
"integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -5576,6 +5599,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/rcedit": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-5.0.2.tgz",
|
||||
"integrity": "sha512-dgysxaeXZ4snLpPjn8aVtHvZDCx+aRcvZbaWBgl1poU6OPustMvOkj9a9ZqASQ6i5Y5szJ13LSvglEOwrmgUxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn-windows-exe": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-config-file": {
|
||||
"version": "6.3.2",
|
||||
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz",
|
||||
@ -6491,6 +6527,13 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.26",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||
@ -6554,46 +6597,21 @@
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-template-compiler": {
|
||||
"version": "2.7.16",
|
||||
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
|
||||
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "1.8.27",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz",
|
||||
"integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.1.tgz",
|
||||
"integrity": "sha512-I23Rk8dkQfmcSbxDO0dmg9ioMLjKA1pjlU3Lz6Jfk2pMGu3Uryu9810XkcZH24IzPbhzPCnkKo2rEMRX0skSrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/typescript": "~1.11.1",
|
||||
"@vue/language-core": "1.8.27",
|
||||
"semver": "^7.5.4"
|
||||
"@volar/typescript": "2.4.27",
|
||||
"@vue/language-core": "3.2.1"
|
||||
},
|
||||
"bin": {
|
||||
"vue-tsc": "bin/vue-tsc.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-tsc/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wait-on": {
|
||||
|
||||
23
package.json
23
package.json
@ -1,14 +1,20 @@
|
||||
{
|
||||
"name": "phper-dev-manager",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.10",
|
||||
"description": "PHP开发环境管理器 - 管理PHP、MySQL、Nginx、Redis服务",
|
||||
"main": "dist-electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build && electron-builder",
|
||||
"build": "node scripts/bump-version.js && vite build && electron-builder",
|
||||
"build:patch": "node scripts/bump-version.js patch && vite build && electron-builder",
|
||||
"build:minor": "node scripts/bump-version.js minor && vite build && electron-builder",
|
||||
"build:major": "node scripts/bump-version.js major && vite build && electron-builder",
|
||||
"build:nobump": "vite build && electron-builder",
|
||||
"build:check": "vue-tsc --noEmit && vite build && electron-builder",
|
||||
"preview": "vite preview",
|
||||
"electron:dev": "vite",
|
||||
"electron:build": "vite build && electron-builder"
|
||||
"electron:build": "node scripts/bump-version.js && vite build && electron-builder",
|
||||
"typecheck": "vue-tsc --noEmit"
|
||||
},
|
||||
"author": "PHPer",
|
||||
"license": "MIT",
|
||||
@ -18,12 +24,13 @@
|
||||
"concurrently": "^8.2.2",
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"rcedit": "^5.0.2",
|
||||
"sass": "^1.69.5",
|
||||
"typescript": "^5.3.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-electron": "^0.15.5",
|
||||
"vite-plugin-electron-renderer": "^0.14.5",
|
||||
"vue-tsc": "^1.8.25",
|
||||
"vue-tsc": "^3.2.1",
|
||||
"wait-on": "^7.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -68,7 +75,9 @@
|
||||
}
|
||||
],
|
||||
"icon": "build/icon.ico",
|
||||
"requestedExecutionLevel": "requireAdministrator"
|
||||
"executableName": "PHPer开发环境管理器",
|
||||
"requestedExecutionLevel": "requireAdministrator",
|
||||
"signAndEditExecutable": true
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
@ -78,6 +87,8 @@
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "PHPer开发环境管理器",
|
||||
"installerIcon": "build/icon.ico",
|
||||
"uninstallerIcon": "build/icon.ico",
|
||||
"installerLanguages": [
|
||||
"zh_CN",
|
||||
"en_US"
|
||||
|
||||
5
public/version.json
Normal file
5
public/version.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1.0.10",
|
||||
"buildTime": "2026-02-05T01:05:37.725Z",
|
||||
"buildDate": "2026/2/5"
|
||||
}
|
||||
58
scripts/bump-version.js
Normal file
58
scripts/bump-version.js
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 自动更新版本号脚本
|
||||
* 每次打包时自动增加 patch 版本号
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/bump-version.js # patch: 1.0.0 -> 1.0.1
|
||||
* node scripts/bump-version.js minor # minor: 1.0.0 -> 1.1.0
|
||||
* node scripts/bump-version.js major # major: 1.0.0 -> 2.0.0
|
||||
*/
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const packagePath = path.join(__dirname, '..', 'package.json')
|
||||
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'))
|
||||
|
||||
const currentVersion = pkg.version
|
||||
const [major, minor, patch] = currentVersion.split('.').map(Number)
|
||||
|
||||
// 获取命令行参数
|
||||
const bumpType = process.argv[2] || 'patch'
|
||||
|
||||
let newVersion
|
||||
switch (bumpType) {
|
||||
case 'major':
|
||||
newVersion = `${major + 1}.0.0`
|
||||
break
|
||||
case 'minor':
|
||||
newVersion = `${major}.${minor + 1}.0`
|
||||
break
|
||||
case 'patch':
|
||||
default:
|
||||
newVersion = `${major}.${minor}.${patch + 1}`
|
||||
break
|
||||
}
|
||||
|
||||
// 更新 package.json
|
||||
pkg.version = newVersion
|
||||
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
||||
|
||||
// 生成构建时间戳
|
||||
const buildTime = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
||||
|
||||
console.log(`✅ 版本号已更新: ${currentVersion} -> ${newVersion}`)
|
||||
console.log(`📦 构建时间: ${buildTime}`)
|
||||
|
||||
// 将版本信息写入一个文件,供应用读取
|
||||
const versionInfo = {
|
||||
version: newVersion,
|
||||
buildTime: new Date().toISOString(),
|
||||
buildDate: new Date().toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
const versionFilePath = path.join(__dirname, '..', 'public', 'version.json')
|
||||
fs.writeFileSync(versionFilePath, JSON.stringify(versionInfo, null, 2))
|
||||
|
||||
console.log(`📄 版本信息已写入: public/version.json`)
|
||||
|
||||
233
src/App.vue
233
src/App.vue
@ -34,15 +34,16 @@
|
||||
: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"
|
||||
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>
|
||||
|
||||
@ -62,10 +63,10 @@
|
||||
|
||||
<!-- 内容区 -->
|
||||
<main class="content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<keep-alive :include="cachedViews">
|
||||
<component :is="Component" :key="route.path" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
@ -73,104 +74,131 @@
|
||||
</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);
|
||||
|
||||
// 从 store 获取服务状态
|
||||
const serviceStatus = computed(() => ({
|
||||
// 缓存的视图列表 - 避免页面切换闪烁
|
||||
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
|
||||
}))
|
||||
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
|
||||
// 启动所有服务
|
||||
const startAll = async () => {
|
||||
startingAll.value = true;
|
||||
try {
|
||||
const result = await window.electronAPI?.service.startAll()
|
||||
const result = await window.electronAPI?.service.startAll();
|
||||
if (result?.success) {
|
||||
ElMessage.success(result.message)
|
||||
ElMessage.success(result.message);
|
||||
// 延迟刷新状态,等待服务启动
|
||||
setTimeout(() => store.refreshServiceStatus(), 2000)
|
||||
setTimeout(() => store.refreshServiceStatus(), 2000);
|
||||
} else {
|
||||
ElMessage.error(result?.message || '启动失败')
|
||||
ElMessage.error(result?.message || "启动失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message)
|
||||
ElMessage.error(error.message);
|
||||
} finally {
|
||||
startingAll.value = false
|
||||
startingAll.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 停止所有服务
|
||||
const stopAll = async () => {
|
||||
stoppingAll.value = true
|
||||
// 停止所有服务
|
||||
const stopAll = async () => {
|
||||
stoppingAll.value = true;
|
||||
try {
|
||||
const result = await window.electronAPI?.service.stopAll()
|
||||
const result = await window.electronAPI?.service.stopAll();
|
||||
if (result?.success) {
|
||||
ElMessage.success(result.message)
|
||||
await store.refreshServiceStatus()
|
||||
ElMessage.success(result.message);
|
||||
await store.refreshServiceStatus();
|
||||
} else {
|
||||
ElMessage.error(result?.message || '停止失败')
|
||||
ElMessage.error(result?.message || "停止失败");
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message)
|
||||
ElMessage.error(error.message);
|
||||
} finally {
|
||||
stoppingAll.value = false
|
||||
stoppingAll.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.documentElement.classList.add('dark')
|
||||
onMounted(() => {
|
||||
document.documentElement.classList.add("dark");
|
||||
// 初始化加载所有状态
|
||||
store.refreshAll()
|
||||
store.refreshAll();
|
||||
// 每 5 秒刷新一次状态
|
||||
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000)
|
||||
})
|
||||
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
onUnmounted(() => {
|
||||
if (statusInterval) {
|
||||
clearInterval(statusInterval)
|
||||
clearInterval(statusInterval);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-container {
|
||||
.app-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@ -178,9 +206,9 @@ onUnmounted(() => {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
.title-bar {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -189,15 +217,15 @@ onUnmounted(() => {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
-webkit-app-region: drag;
|
||||
padding: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-bar-left {
|
||||
.title-bar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.app-logo {
|
||||
.app-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@ -211,17 +239,17 @@ onUnmounted(() => {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
|
||||
font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title-bar-right {
|
||||
.title-bar-right {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
.title-btn {
|
||||
.title-btn {
|
||||
width: 36px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
@ -243,31 +271,31 @@ onUnmounted(() => {
|
||||
background: #e81123;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-container {
|
||||
.main-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
.sidebar {
|
||||
width: 220px;
|
||||
background: var(--bg-sidebar);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
@ -315,14 +343,14 @@ onUnmounted(() => {
|
||||
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
.sidebar-footer {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
@ -343,23 +371,12 @@ onUnmounted(() => {
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-content);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
512
src/components/LogViewer.vue
Normal file
512
src/components/LogViewer.vue
Normal file
@ -0,0 +1,512 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogTitle"
|
||||
width="900px"
|
||||
class="log-viewer-dialog"
|
||||
:close-on-click-modal="false"
|
||||
@closed="onClosed"
|
||||
>
|
||||
<!-- 日志文件选择器 -->
|
||||
<div class="log-selector">
|
||||
<el-tabs v-model="activeTab" @tab-change="onTabChange">
|
||||
<el-tab-pane label="Nginx" name="nginx">
|
||||
<div class="log-list" v-if="logFiles.nginx.length > 0">
|
||||
<div
|
||||
v-for="file in logFiles.nginx"
|
||||
:key="file.path"
|
||||
class="log-item"
|
||||
:class="{ active: selectedLog?.path === file.path }"
|
||||
@click="selectLog(file)"
|
||||
>
|
||||
<div class="log-item-info">
|
||||
<el-icon :class="getLogIcon(file.type)"><Document /></el-icon>
|
||||
<span class="log-name">{{ file.name }}</span>
|
||||
</div>
|
||||
<div class="log-item-meta">
|
||||
<span class="log-size">{{ formatSize(file.size) }}</span>
|
||||
<span class="log-time">{{ formatTime(file.modifiedTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-log">暂无 Nginx 日志文件</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="PHP" name="php">
|
||||
<div class="log-list" v-if="logFiles.php.length > 0">
|
||||
<div
|
||||
v-for="file in logFiles.php"
|
||||
:key="file.path"
|
||||
class="log-item"
|
||||
:class="{ active: selectedLog?.path === file.path }"
|
||||
@click="selectLog(file)"
|
||||
>
|
||||
<div class="log-item-info">
|
||||
<el-icon class="php-icon"><Document /></el-icon>
|
||||
<span class="log-name">{{ file.name }}</span>
|
||||
</div>
|
||||
<div class="log-item-meta">
|
||||
<span class="log-size">{{ formatSize(file.size) }}</span>
|
||||
<span class="log-time">{{ formatTime(file.modifiedTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-log">暂无 PHP 日志文件</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="MySQL" name="mysql">
|
||||
<div class="log-list" v-if="logFiles.mysql.length > 0">
|
||||
<div
|
||||
v-for="file in logFiles.mysql"
|
||||
:key="file.path"
|
||||
class="log-item"
|
||||
:class="{ active: selectedLog?.path === file.path }"
|
||||
@click="selectLog(file)"
|
||||
>
|
||||
<div class="log-item-info">
|
||||
<el-icon class="mysql-icon"><Document /></el-icon>
|
||||
<span class="log-name">{{ file.name }}</span>
|
||||
</div>
|
||||
<div class="log-item-meta">
|
||||
<span class="log-size">{{ formatSize(file.size) }}</span>
|
||||
<span class="log-time">{{ formatTime(file.modifiedTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-log">暂无 MySQL 日志文件</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="站点" name="sites">
|
||||
<div class="log-list" v-if="logFiles.sites.length > 0">
|
||||
<div
|
||||
v-for="file in logFiles.sites"
|
||||
:key="file.path"
|
||||
class="log-item"
|
||||
:class="{ active: selectedLog?.path === file.path }"
|
||||
@click="selectLog(file)"
|
||||
>
|
||||
<div class="log-item-info">
|
||||
<el-icon :class="file.type === 'site-error' ? 'error-icon' : 'access-icon'">
|
||||
<Document />
|
||||
</el-icon>
|
||||
<span class="log-name">{{ file.name }}</span>
|
||||
</div>
|
||||
<div class="log-item-meta">
|
||||
<span class="log-size">{{ formatSize(file.size) }}</span>
|
||||
<span class="log-time">{{ formatTime(file.modifiedTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-log">暂无站点日志文件</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 日志内容区域 -->
|
||||
<div class="log-content-area" v-if="selectedLog">
|
||||
<div class="log-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="selected-log-name">{{ selectedLog.name }}</span>
|
||||
<el-tag size="small" type="info">{{ formatSize(logContent.fileSize) }}</el-tag>
|
||||
<el-tag size="small" type="info">{{ logContent.totalLines }} 行</el-tag>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button size="small" @click="refreshLog" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button size="small" @click="openInExplorer">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
打开目录
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="clearLog">
|
||||
<el-icon><Delete /></el-icon>
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-content-wrapper">
|
||||
<pre class="log-content" ref="logContentRef">{{ logContent.content || '(日志为空)' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-content-area empty" v-else>
|
||||
<el-icon class="empty-icon"><Document /></el-icon>
|
||||
<p>请选择左侧的日志文件查看</p>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Document, Refresh, FolderOpened, Delete } from '@element-plus/icons-vue'
|
||||
|
||||
interface LogFile {
|
||||
name: string
|
||||
path: string
|
||||
size: number
|
||||
modifiedTime: string | Date
|
||||
type: string
|
||||
}
|
||||
|
||||
interface LogContent {
|
||||
content: string
|
||||
totalLines: number
|
||||
fileSize: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
initialTab?: 'nginx' | 'php' | 'mysql' | 'sites'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const activeTab = ref<'nginx' | 'php' | 'mysql' | 'sites'>('nginx')
|
||||
const loading = ref(false)
|
||||
const logContentRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const logFiles = reactive<{
|
||||
nginx: LogFile[]
|
||||
php: LogFile[]
|
||||
mysql: LogFile[]
|
||||
sites: LogFile[]
|
||||
}>({
|
||||
nginx: [],
|
||||
php: [],
|
||||
mysql: [],
|
||||
sites: []
|
||||
})
|
||||
|
||||
const selectedLog = ref<LogFile | null>(null)
|
||||
const logContent = reactive<LogContent>({
|
||||
content: '',
|
||||
totalLines: 0,
|
||||
fileSize: 0
|
||||
})
|
||||
|
||||
const dialogTitle = ref('日志查看器')
|
||||
|
||||
// 监听外部 modelValue 变化
|
||||
watch(() => props.modelValue, async (newVal) => {
|
||||
visible.value = newVal
|
||||
if (newVal) {
|
||||
if (props.initialTab) {
|
||||
activeTab.value = props.initialTab
|
||||
}
|
||||
await loadLogFiles()
|
||||
}
|
||||
})
|
||||
|
||||
// 同步内部状态到外部
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
})
|
||||
|
||||
// 加载日志文件列表
|
||||
const loadLogFiles = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const files = await window.electronAPI?.log.getFiles()
|
||||
if (files) {
|
||||
logFiles.nginx = files.nginx || []
|
||||
logFiles.php = files.php || []
|
||||
logFiles.mysql = files.mysql || []
|
||||
logFiles.sites = files.sites || []
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error('加载日志列表失败: ' + error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 选择日志文件
|
||||
const selectLog = async (file: LogFile) => {
|
||||
selectedLog.value = file
|
||||
await loadLogContent(file.path)
|
||||
}
|
||||
|
||||
// 加载日志内容
|
||||
const loadLogContent = async (logPath: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await window.electronAPI?.log.read(logPath, 1000)
|
||||
if (result) {
|
||||
logContent.content = result.content
|
||||
logContent.totalLines = result.totalLines
|
||||
logContent.fileSize = result.fileSize
|
||||
|
||||
// 滚动到底部
|
||||
await nextTick()
|
||||
if (logContentRef.value) {
|
||||
logContentRef.value.scrollTop = logContentRef.value.scrollHeight
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error('读取日志失败: ' + error.message)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新日志
|
||||
const refreshLog = async () => {
|
||||
if (selectedLog.value) {
|
||||
await loadLogContent(selectedLog.value.path)
|
||||
}
|
||||
await loadLogFiles()
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
const clearLog = async () => {
|
||||
if (!selectedLog.value) return
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要清空日志 "${selectedLog.value.name}" 吗?此操作不可恢复。`,
|
||||
'确认清空',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
const result = await window.electronAPI?.log.clear(selectedLog.value.path)
|
||||
if (result?.success) {
|
||||
ElMessage.success(result.message)
|
||||
await refreshLog()
|
||||
} else {
|
||||
ElMessage.error(result?.message || '清空失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('操作失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在文件管理器中打开
|
||||
const openInExplorer = async () => {
|
||||
if (!selectedLog.value) return
|
||||
|
||||
// 获取文件所在目录
|
||||
const path = selectedLog.value.path
|
||||
const dir = path.substring(0, path.lastIndexOf('\\'))
|
||||
await window.electronAPI?.openPath(dir)
|
||||
}
|
||||
|
||||
// Tab 切换
|
||||
const onTabChange = () => {
|
||||
selectedLog.value = null
|
||||
logContent.content = ''
|
||||
logContent.totalLines = 0
|
||||
logContent.fileSize = 0
|
||||
}
|
||||
|
||||
// 对话框关闭
|
||||
const onClosed = () => {
|
||||
selectedLog.value = null
|
||||
logContent.content = ''
|
||||
}
|
||||
|
||||
// 获取日志图标样式
|
||||
const getLogIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'nginx-error':
|
||||
case 'mysql-error':
|
||||
case 'site-error':
|
||||
return 'error-icon'
|
||||
case 'nginx-access':
|
||||
case 'site-access':
|
||||
return 'access-icon'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
const formatSize = (bytes: number): string => {
|
||||
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(1)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string | Date): string => {
|
||||
const date = typeof time === 'string' ? new Date(time) : time
|
||||
const now = new Date()
|
||||
const diff = now.getTime() - date.getTime()
|
||||
|
||||
if (diff < 60000) return '刚刚'
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前'
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前'
|
||||
if (diff < 604800000) return Math.floor(diff / 86400000) + ' 天前'
|
||||
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.log-viewer-dialog {
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
.log-selector {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 16px 20px 0;
|
||||
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.log-list {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(124, 58, 237, 0.1);
|
||||
border-left: 3px solid var(--accent-color);
|
||||
padding-left: 13px;
|
||||
}
|
||||
|
||||
.log-item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 18px;
|
||||
color: var(--text-muted);
|
||||
|
||||
&.error-icon {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
&.access-icon {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
&.php-icon {
|
||||
color: #777BB4;
|
||||
}
|
||||
|
||||
&.mysql-icon {
|
||||
color: #00758F;
|
||||
}
|
||||
}
|
||||
|
||||
.log-name {
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.log-item-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-log {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.log-content-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
&.empty {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--text-muted);
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.log-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
background: var(--bg-input);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.selected-log-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.log-content-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
background: #0d1117;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
font-family: 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #c9d1d9;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
min-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -248,10 +248,9 @@
|
||||
class="mini-site-card"
|
||||
>
|
||||
<a
|
||||
:href="(site.ssl ? 'https://' : 'http://') + site.domain"
|
||||
target="_blank"
|
||||
href="#"
|
||||
class="site-domain-link"
|
||||
@click.stop
|
||||
@click.prevent="openSite(site)"
|
||||
>
|
||||
{{ site.domain }}
|
||||
<el-icon class="link-icon"><Link /></el-icon>
|
||||
@ -277,11 +276,17 @@
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
安装路径
|
||||
</span>
|
||||
<div class="card-actions">
|
||||
<el-button size="small" @click="showLogViewer = true">
|
||||
<el-icon><Document /></el-icon>
|
||||
查看日志
|
||||
</el-button>
|
||||
<el-button size="small" @click="openBasePath">
|
||||
<el-icon><FolderOpened /></el-icon>
|
||||
打开目录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="path-display">
|
||||
<span class="path-label">基础路径:</span>
|
||||
@ -289,14 +294,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志查看器 -->
|
||||
<LogViewer v-model="showLogViewer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { ref, computed, onMounted, onActivated } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Link, Promotion } from '@element-plus/icons-vue'
|
||||
import { Link, Promotion, Document } from '@element-plus/icons-vue'
|
||||
import { useServiceStore } from '@/stores/serviceStore'
|
||||
import LogViewer from '@/components/LogViewer.vue'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'Dashboard'
|
||||
})
|
||||
|
||||
const store = useServiceStore()
|
||||
|
||||
@ -352,6 +366,7 @@ const basePath = computed(() => store.basePath)
|
||||
|
||||
const settingPhp = ref('')
|
||||
const settingNode = ref('')
|
||||
const showLogViewer = ref(false)
|
||||
|
||||
const startService = async (service: Service) => {
|
||||
serviceLoadingState.value[service.name] = true
|
||||
@ -533,6 +548,12 @@ const openBasePath = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 在默认浏览器中打开站点
|
||||
const openSite = (site: { domain: string, ssl: boolean }) => {
|
||||
const protocol = site.ssl ? 'https' : 'http'
|
||||
window.electronAPI?.openExternal(`${protocol}://${site.domain}`)
|
||||
}
|
||||
|
||||
const setActivePhp = async (version: string) => {
|
||||
settingPhp.value = version
|
||||
try {
|
||||
@ -570,8 +591,15 @@ const setActiveNode = async (version: string) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 总是刷新数据以确保数据最新
|
||||
// 首次加载:如果没有数据则全量刷新
|
||||
if (store.phpVersions.length === 0 && store.nodeVersions.length === 0) {
|
||||
await store.refreshAll()
|
||||
}
|
||||
})
|
||||
|
||||
// 从缓存激活时静默刷新状态(不会闪烁因为已有数据)
|
||||
onActivated(async () => {
|
||||
await store.refreshServiceStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -177,6 +177,11 @@
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'GitManager'
|
||||
})
|
||||
|
||||
interface GitVersion {
|
||||
version: string
|
||||
path: string
|
||||
|
||||
439
src/views/GoManager.vue
Normal file
439
src/views/GoManager.vue
Normal 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>
|
||||
@ -16,6 +16,10 @@
|
||||
Hosts 条目
|
||||
</span>
|
||||
<div class="card-actions">
|
||||
<el-button @click="refreshHosts" :loading="refreshing">
|
||||
<el-icon v-if="!refreshing"><RefreshRight /></el-icon>
|
||||
刷新 Hosts
|
||||
</el-button>
|
||||
<el-button @click="flushDns">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新 DNS
|
||||
@ -89,6 +93,11 @@
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'HostsManager'
|
||||
})
|
||||
|
||||
interface HostEntry {
|
||||
ip: string
|
||||
domain: string
|
||||
@ -96,6 +105,7 @@ interface HostEntry {
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const hosts = ref<HostEntry[]>([])
|
||||
const showAddDialog = ref(false)
|
||||
const adding = ref(false)
|
||||
@ -113,6 +123,19 @@ const loadHosts = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const refreshHosts = async () => {
|
||||
refreshing.value = true
|
||||
try {
|
||||
hosts.value = await window.electronAPI?.hosts.get() || []
|
||||
ElMessage.success('Hosts 列表已刷新')
|
||||
} catch (error: any) {
|
||||
console.error('刷新 hosts 失败:', error)
|
||||
ElMessage.error('刷新失败: ' + error.message)
|
||||
} finally {
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const addHost = async () => {
|
||||
if (!hostForm.ip || !hostForm.domain) {
|
||||
ElMessage.warning('请填写 IP 地址和域名')
|
||||
|
||||
@ -87,9 +87,13 @@
|
||||
密码
|
||||
</el-button>
|
||||
<el-button size="small" @click="showConfig(version)">
|
||||
<el-icon><Document /></el-icon>
|
||||
<el-icon><Setting /></el-icon>
|
||||
配置
|
||||
</el-button>
|
||||
<el-button size="small" @click="showLogViewerDialog">
|
||||
<el-icon><Document /></el-icon>
|
||||
日志
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@ -222,14 +226,23 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 日志查看器 -->
|
||||
<LogViewer v-model="showLogViewer" initial-tab="mysql" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, reactive, onMounted, onUnmounted, onActivated } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
import { InfoFilled, Setting } from '@element-plus/icons-vue'
|
||||
import { useServiceStore } from '@/stores/serviceStore'
|
||||
import LogViewer from '@/components/LogViewer.vue'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'MysqlManager'
|
||||
})
|
||||
|
||||
const store = useServiceStore()
|
||||
|
||||
@ -271,6 +284,13 @@ const configContent = ref('')
|
||||
const savingConfig = ref(false)
|
||||
const currentVersion = ref('')
|
||||
|
||||
// 日志查看器
|
||||
const showLogViewer = ref(false)
|
||||
|
||||
const showLogViewerDialog = () => {
|
||||
showLogViewer.value = true
|
||||
}
|
||||
|
||||
const loadVersions = async () => {
|
||||
try {
|
||||
installedVersions.value = await window.electronAPI?.mysql.getVersions() || []
|
||||
|
||||
@ -74,8 +74,12 @@
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重载配置
|
||||
</el-button>
|
||||
<el-button @click="showConfig">
|
||||
<el-button @click="showLogViewerDialog('nginx')">
|
||||
<el-icon><Document /></el-icon>
|
||||
查看日志
|
||||
</el-button>
|
||||
<el-button @click="showConfig">
|
||||
<el-icon><Setting /></el-icon>
|
||||
编辑配置
|
||||
</el-button>
|
||||
</div>
|
||||
@ -180,14 +184,23 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 日志查看器 -->
|
||||
<LogViewer v-model="showLogViewer" :initial-tab="logViewerTab" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, reactive, onMounted, onUnmounted, onActivated } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
import { InfoFilled, Setting } from '@element-plus/icons-vue'
|
||||
import { useServiceStore } from '@/stores/serviceStore'
|
||||
import LogViewer from '@/components/LogViewer.vue'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'NginxManager'
|
||||
})
|
||||
|
||||
const store = useServiceStore()
|
||||
|
||||
@ -219,6 +232,15 @@ const showConfigDialog = ref(false)
|
||||
const configContent = ref('')
|
||||
const savingConfig = ref(false)
|
||||
|
||||
// 日志查看器
|
||||
const showLogViewer = ref(false)
|
||||
const logViewerTab = ref<'nginx' | 'php' | 'mysql' | 'sites'>('nginx')
|
||||
|
||||
const showLogViewerDialog = (tab: 'nginx' | 'sites') => {
|
||||
logViewerTab.value = tab
|
||||
showLogViewer.value = true
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const versions = await window.electronAPI?.nginx.getVersions() || []
|
||||
|
||||
@ -140,6 +140,11 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Promotion, Box, InfoFilled, Loading } from '@element-plus/icons-vue'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'NodeManager'
|
||||
})
|
||||
|
||||
interface NodeVersion {
|
||||
version: string
|
||||
path: string
|
||||
|
||||
@ -45,11 +45,47 @@
|
||||
<div class="version-name">
|
||||
PHP {{ version.version }}
|
||||
<el-tag v-if="version.isActive" type="success" size="small" class="ml-2">当前使用</el-tag>
|
||||
<el-tag
|
||||
v-if="isCgiRunning(version.version)"
|
||||
type="success"
|
||||
size="small"
|
||||
class="ml-2"
|
||||
>
|
||||
CGI:{{ getCgiPort(version.version) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="version-path">{{ version.path }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-actions">
|
||||
<!-- CGI 控制按钮 -->
|
||||
<el-tooltip
|
||||
:content="isCgiRunning(version.version)
|
||||
? `停止 CGI (端口 ${getCgiPort(version.version)})`
|
||||
: `启动 CGI (端口 ${getCgiPort(version.version)})`"
|
||||
placement="top"
|
||||
>
|
||||
<el-button
|
||||
v-if="isCgiRunning(version.version)"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="stopCgi(version.version)"
|
||||
:loading="cgiLoading[version.version]"
|
||||
>
|
||||
<el-icon><VideoPause /></el-icon>
|
||||
CGI
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
size="small"
|
||||
@click="startCgi(version.version)"
|
||||
:loading="cgiLoading[version.version]"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
CGI
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-button
|
||||
v-if="!version.isActive"
|
||||
type="primary"
|
||||
@ -63,9 +99,13 @@
|
||||
扩展
|
||||
</el-button>
|
||||
<el-button size="small" @click="showConfig(version)">
|
||||
<el-icon><Document /></el-icon>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
配置
|
||||
</el-button>
|
||||
<el-button size="small" @click="showLogViewerDialog">
|
||||
<el-icon><Document /></el-icon>
|
||||
日志
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@ -312,21 +352,34 @@
|
||||
</div>
|
||||
<div v-else class="extensions-list">
|
||||
<div class="extensions-count">
|
||||
找到 {{ availableExtensions.length }} 个适用于 PHP {{ currentVersion }} 的扩展
|
||||
找到 {{ availableExtensions.length }} 个扩展
|
||||
</div>
|
||||
<div
|
||||
v-for="ext in availableExtensions"
|
||||
:key="ext.name"
|
||||
class="extension-item"
|
||||
:class="{ 'not-available': ext.notAvailableReason }"
|
||||
>
|
||||
<div class="ext-info">
|
||||
<div class="ext-main">
|
||||
<span class="ext-name" v-html="highlightKeyword(ext.name)"></span>
|
||||
<el-tag type="warning" size="small">v{{ ext.version }}</el-tag>
|
||||
<el-tag type="info" size="small">{{ ext.version === 'latest' ? '最新版' : 'v' + ext.version }}</el-tag>
|
||||
</div>
|
||||
<span class="ext-desc" v-if="ext.description">{{ ext.description }}</span>
|
||||
<span class="ext-not-available" v-if="ext.notAvailableReason">
|
||||
<el-icon><Warning /></el-icon>
|
||||
{{ ext.notAvailableReason }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 有明确不可用原因时显示不支持 -->
|
||||
<el-tooltip v-if="ext.notAvailableReason" :content="ext.notAvailableReason" placement="top">
|
||||
<el-button type="info" size="small" disabled>
|
||||
不支持
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<!-- 否则显示安装按钮 -->
|
||||
<el-button
|
||||
v-else
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="installExtension(ext)"
|
||||
@ -366,14 +419,23 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 日志查看器 -->
|
||||
<LogViewer v-model="showLogViewer" initial-tab="php" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, onUnmounted, onActivated } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { FolderOpened, InfoFilled } from '@element-plus/icons-vue'
|
||||
import { FolderOpened, InfoFilled, VideoPlay, VideoPause, EditPen, Warning } from '@element-plus/icons-vue'
|
||||
import { useServiceStore } from '@/stores/serviceStore'
|
||||
import LogViewer from '@/components/LogViewer.vue'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'PhpManager'
|
||||
})
|
||||
|
||||
const store = useServiceStore()
|
||||
|
||||
@ -402,9 +464,12 @@ interface AvailableExtension {
|
||||
downloadUrl: string
|
||||
description?: string
|
||||
packageName?: string // Packagist 包名,用于 PIE 安装
|
||||
supportedPhpVersions?: string[] // 支持的 PHP 版本
|
||||
notAvailableReason?: string // 不可用原因
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const initialLoaded = ref(false) // 标记是否已完成首次加载
|
||||
const installedVersions = ref<PhpVersion[]>([])
|
||||
const availableVersions = ref<AvailableVersion[]>([])
|
||||
const showInstallDialog = ref(false)
|
||||
@ -416,6 +481,30 @@ const downloadProgress = reactive({
|
||||
total: 0
|
||||
})
|
||||
|
||||
// CGI 状态管理
|
||||
interface CgiStatus {
|
||||
version: string
|
||||
port: number
|
||||
running: boolean
|
||||
}
|
||||
const cgiStatus = ref<CgiStatus[]>([])
|
||||
const cgiLoading = ref<Record<string, boolean>>({})
|
||||
|
||||
// 获取指定版本的 CGI 状态
|
||||
const getCgiStatus = (version: string): CgiStatus | undefined => {
|
||||
return cgiStatus.value.find(s => s.version === version)
|
||||
}
|
||||
|
||||
// 判断 CGI 是否运行中
|
||||
const isCgiRunning = (version: string): boolean => {
|
||||
return getCgiStatus(version)?.running ?? false
|
||||
}
|
||||
|
||||
// 获取 CGI 端口
|
||||
const getCgiPort = (version: string): number => {
|
||||
return getCgiStatus(version)?.port ?? 9000
|
||||
}
|
||||
|
||||
const showExtensionsDialog = ref(false)
|
||||
const loadingExtensions = ref(false)
|
||||
const extensions = ref<Extension[]>([])
|
||||
@ -465,6 +554,13 @@ const showConfigDialog = ref(false)
|
||||
const configContent = ref('')
|
||||
const savingConfig = ref(false)
|
||||
|
||||
// 日志查看器
|
||||
const showLogViewer = ref(false)
|
||||
|
||||
const showLogViewerDialog = () => {
|
||||
showLogViewer.value = true
|
||||
}
|
||||
|
||||
// Composer 相关
|
||||
const composerStatus = ref<{
|
||||
installed: boolean
|
||||
@ -489,6 +585,51 @@ const loadVersions = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载 CGI 状态
|
||||
const loadCgiStatus = async () => {
|
||||
try {
|
||||
cgiStatus.value = await window.electronAPI?.service.getPhpCgiStatus() || []
|
||||
} catch (error: any) {
|
||||
console.error('加载 CGI 状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动 CGI
|
||||
const startCgi = async (version: string) => {
|
||||
cgiLoading.value[version] = true
|
||||
try {
|
||||
const result = await window.electronAPI?.service.startPhpCgiVersion(version)
|
||||
if (result?.success) {
|
||||
ElMessage.success(result.message)
|
||||
await loadCgiStatus()
|
||||
} else {
|
||||
ElMessage.error(result?.message || '启动失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error('启动失败: ' + error.message)
|
||||
} finally {
|
||||
cgiLoading.value[version] = false
|
||||
}
|
||||
}
|
||||
|
||||
// 停止 CGI
|
||||
const stopCgi = async (version: string) => {
|
||||
cgiLoading.value[version] = true
|
||||
try {
|
||||
const result = await window.electronAPI?.service.stopPhpCgiVersion(version)
|
||||
if (result?.success) {
|
||||
ElMessage.success(result.message)
|
||||
await loadCgiStatus()
|
||||
} else {
|
||||
ElMessage.error(result?.message || '停止失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error('停止失败: ' + error.message)
|
||||
} finally {
|
||||
cgiLoading.value[version] = false
|
||||
}
|
||||
}
|
||||
|
||||
// Composer 相关方法
|
||||
const loadComposerStatus = async () => {
|
||||
try {
|
||||
@ -812,10 +953,30 @@ const formatSize = (bytes: number) => {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadVersions()
|
||||
loadAvailableVersions()
|
||||
onMounted(async () => {
|
||||
// 使用 store 中的缓存数据进行首次渲染(避免闪烁)
|
||||
if (store.phpVersions.length > 0) {
|
||||
installedVersions.value = store.phpVersions.map(v => ({
|
||||
version: v.version,
|
||||
path: v.path,
|
||||
isActive: v.isActive
|
||||
}))
|
||||
}
|
||||
|
||||
// 首次加载数据
|
||||
loading.value = installedVersions.value.length === 0
|
||||
|
||||
await Promise.all([
|
||||
loadVersions(),
|
||||
loadCgiStatus(),
|
||||
loadComposerStatus()
|
||||
])
|
||||
|
||||
loading.value = false
|
||||
initialLoaded.value = true
|
||||
|
||||
// 异步加载可用版本列表(不阻塞首屏)
|
||||
loadAvailableVersions()
|
||||
|
||||
// 监听下载进度
|
||||
window.electronAPI?.onDownloadProgress((data: any) => {
|
||||
@ -831,6 +992,13 @@ onMounted(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// 从缓存激活时静默刷新 CGI 状态
|
||||
onActivated(async () => {
|
||||
if (initialLoaded.value) {
|
||||
await loadCgiStatus()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.electronAPI?.removeDownloadProgressListener()
|
||||
})
|
||||
@ -928,6 +1096,11 @@ onUnmounted(() => {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&.not-available {
|
||||
opacity: 0.7;
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.ext-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -948,6 +1121,14 @@ onUnmounted(() => {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ext-not-available {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--warning-color, #e6a23c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -182,6 +182,11 @@ import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'PythonManager'
|
||||
})
|
||||
|
||||
interface PythonVersion {
|
||||
version: string
|
||||
path: string
|
||||
@ -528,6 +533,10 @@ onUnmounted(() => {
|
||||
.el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
.pip-hint {
|
||||
|
||||
@ -75,9 +75,13 @@
|
||||
重启
|
||||
</el-button>
|
||||
<el-button @click="showConfig">
|
||||
<el-icon><Document /></el-icon>
|
||||
<el-icon><Setting /></el-icon>
|
||||
编辑配置
|
||||
</el-button>
|
||||
<el-button @click="openLogDirectory">
|
||||
<el-icon><Document /></el-icon>
|
||||
日志
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -184,11 +188,16 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, reactive, onMounted, onUnmounted, onActivated } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
import { InfoFilled, Setting } from '@element-plus/icons-vue'
|
||||
import { useServiceStore } from '@/stores/serviceStore'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'RedisManager'
|
||||
})
|
||||
|
||||
const store = useServiceStore()
|
||||
|
||||
interface RedisStatus {
|
||||
@ -383,6 +392,19 @@ const saveConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 打开 Redis 日志目录
|
||||
const openLogDirectory = async () => {
|
||||
try {
|
||||
const basePath = await window.electronAPI?.config.getBasePath()
|
||||
if (basePath) {
|
||||
// Redis 日志通常与配置文件在同一目录
|
||||
await window.electronAPI?.openPath(basePath + '\\redis')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error('打开目录失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
|
||||
@ -111,7 +111,13 @@
|
||||
</div>
|
||||
<div class="app-details">
|
||||
<h2 class="app-title">PHPer 开发环境管理器</h2>
|
||||
<p class="app-version">版本 1.0.0</p>
|
||||
<p class="app-version">
|
||||
版本 {{ appVersion.version }}
|
||||
<span v-if="appVersion.buildDate" class="build-date">
|
||||
({{ appVersion.buildDate }})
|
||||
</span>
|
||||
<el-tag v-if="!appVersion.isPackaged" type="warning" size="small" style="margin-left: 8px">开发版</el-tag>
|
||||
</p>
|
||||
<p class="app-desc">
|
||||
一站式 PHP 开发环境管理工具,支持 PHP、MySQL、Nginx、Redis、Node.js 的安装和管理。
|
||||
</p>
|
||||
@ -145,6 +151,11 @@ import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Monitor } from '@element-plus/icons-vue'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'Settings'
|
||||
})
|
||||
|
||||
interface ServiceAutoStart {
|
||||
name: string
|
||||
displayName: string
|
||||
@ -169,6 +180,13 @@ const services = reactive<ServiceAutoStart[]>([
|
||||
{ name: 'redis', displayName: 'Redis', description: '开机时自动启动 Redis 服务(独立于应用)', autoStart: false }
|
||||
])
|
||||
|
||||
const appVersion = reactive({
|
||||
version: '1.0.0',
|
||||
buildTime: '',
|
||||
buildDate: '',
|
||||
isPackaged: false
|
||||
})
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
basePath.value = await window.electronAPI?.config.getBasePath() || ''
|
||||
@ -177,6 +195,15 @@ const loadSettings = async () => {
|
||||
appSettings.autoLaunch = await window.electronAPI?.app?.getAutoLaunch() || false
|
||||
appSettings.startMinimized = await window.electronAPI?.app?.getStartMinimized() || false
|
||||
|
||||
// 加载版本信息
|
||||
const versionInfo = await window.electronAPI?.app?.getVersion()
|
||||
if (versionInfo) {
|
||||
appVersion.version = versionInfo.version
|
||||
appVersion.buildTime = versionInfo.buildTime
|
||||
appVersion.buildDate = versionInfo.buildDate
|
||||
appVersion.isPackaged = versionInfo.isPackaged
|
||||
}
|
||||
|
||||
// 加载服务自启动设置
|
||||
for (const service of services) {
|
||||
service.autoStart = await window.electronAPI?.service.getAutoStart(service.name) || false
|
||||
@ -315,6 +342,15 @@ onMounted(() => {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.build-date {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
站点列表
|
||||
</span>
|
||||
<div class="header-actions">
|
||||
<el-button @click="showLogViewer = true">
|
||||
<el-icon><Document /></el-icon>
|
||||
站点日志
|
||||
</el-button>
|
||||
<el-button type="success" @click="showCreateLaravelDialog = true">
|
||||
<el-icon><Promotion /></el-icon>
|
||||
创建 Laravel 项目
|
||||
@ -328,14 +332,23 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 日志查看器 -->
|
||||
<LogViewer v-model="showLogViewer" initial-tab="sites" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { FolderOpened } from '@element-plus/icons-vue'
|
||||
import { FolderOpened, Document } from '@element-plus/icons-vue'
|
||||
import { useServiceStore } from '@/stores/serviceStore'
|
||||
import LogViewer from '@/components/LogViewer.vue'
|
||||
|
||||
// 定义组件名称以便 KeepAlive 正确缓存
|
||||
defineOptions({
|
||||
name: 'SitesManager'
|
||||
})
|
||||
|
||||
const store = useServiceStore()
|
||||
|
||||
@ -407,6 +420,8 @@ const editForm = reactive<SiteConfig>({
|
||||
// 创建 Laravel 项目
|
||||
const showCreateLaravelDialog = ref(false)
|
||||
const creatingLaravel = ref(false)
|
||||
const showLogViewer = ref(false)
|
||||
|
||||
const laravelForm = reactive({
|
||||
projectName: '',
|
||||
targetDir: '',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user