Compare commits

...

8 Commits
1.0.0 ... main

Author SHA1 Message Date
4abce26cf2 Update version to 1.0.10 in package.json and version.json, add GoManager support in main.ts, and enhance UI components to include Go management features. Refactor preload.ts and App.vue for improved service management and user experience. 2026-02-05 09:08:23 +08:00
982a51ef52 Update package.json and afterPack.js to set application name and version info, enhancing build process and user experience. Enable executable signing in package.json and improve logging in afterPack.js for better clarity during icon and version setting. 2026-01-04 03:02:48 +08:00
11ad2cf722 Update version to 1.0.8 in package.json and version.json, reflecting recent changes and build updates. 2026-01-04 02:54:05 +08:00
1189177568 Enhance PhpManager to support fetching and displaying PECL extension availability, including reasons for unavailability. Update UI in PhpManager.vue to show extension version and availability status, improving user experience with clearer installation options and error handling. 2026-01-04 02:52:26 +08:00
965bc618ee Update version to 1.0.7 in package.json and version.json, and add refresh functionality in HostsManager.vue for improved host management experience. 2025-12-31 15:09:11 +08:00
2e06bc5277 Update package and build configurations to version 1.0.6, enhancing dependency management with updated versions of TypeScript, Vue, and other packages. Refactor afterPack script to utilize the rcedit module for icon setting, improving build process efficiency. Adjust PythonManager.vue styles for better UI consistency. 2025-12-26 16:09:17 +08:00
d8faf27108 Update README to enhance PHP and log management sections, adding new features such as independent CGI control, automatic port allocation, and site log viewing capabilities. Improve build instructions with version bumping commands and clarify silent startup options for services. 2025-12-26 15:49:28 +08:00
dac4cc805e Enhance build scripts and add log management features in Electron app. Update package.json to include version bumping commands and improve auto-launch functionality with silent startup options. Implement log management services for various components, ensuring better user experience and streamlined service operations. 2025-12-26 15:48:00 +08:00
30 changed files with 4082 additions and 1246 deletions

View File

@ -63,13 +63,16 @@
### 🐘 PHP 版本管理 ### 🐘 PHP 版本管理
| 功能 | 说明 | | 功能 | 说明 |
| ---------- | ---------------------------------------------------------- | | ------------ | ---------------------------------------------------------- |
| 多版本管理 | 支持同时安装 PHP 8.1、8.2、8.3、8.4、8.5 等多个版本 | | 多版本管理 | 支持同时安装 PHP 8.1、8.2、8.3、8.4、8.5 等多个版本 |
| CGI 独立控制 | 每个 PHP 版本可独立启动/停止 CGI 进程,支持多版本并行运行 |
| 端口自动分配 | 各版本自动分配端口(如 8.4→9084, 8.3→9083 |
| 一键切换 | 点击即可切换 PHP 版本,自动配置系统环境变量 | | 一键切换 | 点击即可切换 PHP 版本,自动配置系统环境变量 |
| 扩展管理 | 可视化管理 PHP 扩展,支持在线安装(从 PECL | | 扩展管理 | 可视化管理 PHP 扩展,支持在线安装(从 PECL |
| 配置编辑 | 在线编辑 php.ini无需手动查找配置文件 | | 配置编辑 | 在线编辑 php.ini无需手动查找配置文件 |
| 自动配置 | 安装时自动启用常用扩展curl、gd、mbstring、pdo_mysql 等) | | 自动配置 | 安装时自动启用常用扩展curl、gd、mbstring、pdo_mysql 等) |
| Composer | 集成 Composer 管理,支持镜像源切换(阿里云、腾讯云等) | | Composer | 集成 Composer 管理,支持镜像源切换(阿里云、腾讯云等) |
| 日志查看 | 直接查看 PHP 错误日志 |
| 下载源 | 从 [windows.php.net](https://windows.php.net) 官方下载 | | 下载源 | 从 [windows.php.net](https://windows.php.net) 官方下载 |
### 🐬 MySQL 管理 ### 🐬 MySQL 管理
@ -139,15 +142,31 @@
- 🎯 **Laravel 一键配置** - 自动配置 public 目录和伪静态规则 - 🎯 **Laravel 一键配置** - 自动配置 public 目录和伪静态规则
- 🔒 **SSL 证书申请** - 集成 Let's Encrypt 自动申请 - 🔒 **SSL 证书申请** - 集成 Let's Encrypt 自动申请
- 📝 **Hosts 自动配置** - 自动添加域名到系统 hosts 文件 - 📝 **Hosts 自动配置** - 自动添加域名到系统 hosts 文件
- 📋 **站点日志查看** - 查看每个站点的访问日志和错误日志
- 🌐 **一键打开站点** - 点击域名在默认浏览器打开
### 📋 日志查看
| 功能 | 说明 |
| ------------ | ---------------------------------------------- |
| 多服务日志 | 支持查看 Nginx、PHP、MySQL、Redis 日志 |
| 站点日志 | 查看各站点的访问日志和错误日志 |
| 实时刷新 | 支持刷新日志内容,查看最新记录 |
| 行数控制 | 可配置显示的日志行数100-5000 行) |
| 快速清空 | 一键清空指定日志文件 |
| 打开目录 | 快速在文件管理器中打开日志目录 |
### ⚙️ 其他功能 ### ⚙️ 其他功能
- 🚀 **开机自启动** - 可配置各服务开机自动启动 - 🚀 **开机自启动** - 可配置各服务开机自动启动(静默模式,无弹窗)
- 🔇 **静默启动** - 所有服务启动无黑色窗口闪烁
- 📋 **Hosts 管理** - 可视化管理系统 hosts 文件 - 📋 **Hosts 管理** - 可视化管理系统 hosts 文件
- 🌙 **深色/浅色主题** - 支持主题切换 - 🌙 **深色/浅色主题** - 支持主题切换
- 📊 **服务状态监控** - 实时显示各服务运行状态 - 📊 **服务状态监控** - 实时显示各服务运行状态
- ⏳ **加载状态提示** - 版本列表加载时显示 Loading 状态 - ⚡ **页面切换优化** - 使用 KeepAlive 缓存页面,切换无闪烁
- 🔢 **自动版本号** - 打包时自动更新版本号
- 📥 **下载源说明** - 清晰显示各软件的下载来源 - 📥 **下载源说明** - 清晰显示各软件的下载来源
- 🌐 **默认浏览器打开** - 站点链接自动在默认浏览器打开
## 🛠️ 技术栈 ## 🛠️ 技术栈
@ -186,8 +205,16 @@ npm run electron:dev
### 构建生产版本 ### 构建生产版本
```bash ```bash
# 构建 Windows 安装包 # 构建 Windows 安装包(自动更新 patch 版本号 +0.0.1
npm run electron:build 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` 目录中。 构建完成后,安装包将生成在 `release` 目录中。
@ -209,14 +236,19 @@ phper/
│ ├── PythonManager.ts # Python 版本管理器 │ ├── PythonManager.ts # Python 版本管理器
│ ├── GitManager.ts # Git 管理器 │ ├── GitManager.ts # Git 管理器
│ ├── ServiceManager.ts # 开机自启服务管理器 │ ├── ServiceManager.ts # 开机自启服务管理器
│ └── HostsManager.ts # Hosts 文件管理器 │ ├── HostsManager.ts # Hosts 文件管理器
│ └── LogManager.ts # 日志管理器
├── src/ # Vue 前端源码 ├── src/ # Vue 前端源码
│ ├── App.vue # 根组件 │ ├── App.vue # 根组件(含 KeepAlive 缓存)
│ ├── main.ts # 入口文件 │ ├── main.ts # 入口文件
│ ├── vite-env.d.ts # 类型声明 │ ├── vite-env.d.ts # 类型声明
│ ├── router/ # 路由配置 │ ├── router/ # 路由配置
│ │ └── index.ts │ │ └── index.ts
│ ├── stores/ # Pinia 状态管理
│ │ └── serviceStore.ts # 服务状态存储
│ ├── components/ # 公共组件
│ │ └── LogViewer.vue # 日志查看器组件
│ ├── styles/ # 样式文件 │ ├── styles/ # 样式文件
│ │ └── main.scss # 全局样式(含主题变量) │ │ └── main.scss # 全局样式(含主题变量)
│ └── views/ # 页面视图 │ └── views/ # 页面视图
@ -232,8 +264,12 @@ phper/
│ ├── HostsManager.vue # Hosts 管理 │ ├── HostsManager.vue # Hosts 管理
│ └── Settings.vue # 设置 │ └── Settings.vue # 设置
├── scripts/ # 构建脚本
│ └── bump-version.js # 版本号自动更新脚本
├── public/ # 静态资源 ├── public/ # 静态资源
│ └── icon.svg # 应用图标 │ ├── icon.svg # 应用图标
│ └── version.json # 版本信息(构建时生成)
├── index.html # HTML 模板 ├── index.html # HTML 模板
├── package.json # 项目配置 ├── package.json # 项目配置

View File

@ -1,7 +1,5 @@
const path = require('path'); const path = require('path');
const { execSync } = require('child_process');
const fs = require('fs'); const fs = require('fs');
const os = require('os');
exports.default = async function(context) { exports.default = async function(context) {
// 只在 Windows 上执行 // 只在 Windows 上执行
@ -9,35 +7,14 @@ exports.default = async function(context) {
return; 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 appOutDir = context.appOutDir;
const productName = context.packager.appInfo.productName; const productName = context.packager.appInfo.productName;
const version = context.packager.appInfo.version;
const exePath = path.join(appOutDir, `${productName}.exe`); const exePath = path.join(appOutDir, `${productName}.exe`);
const iconPath = path.join(__dirname, 'icon.ico'); 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)) { if (!fs.existsSync(exePath)) {
console.warn(`Exe not found: ${exePath}`); console.warn(`Exe not found: ${exePath}`);
return; return;
@ -49,17 +26,29 @@ exports.default = async function(context) {
} }
try { try {
console.log(`Setting icon for: ${exePath}`); // rcedit 是默认导出
console.log(`Using icon: ${iconPath}`); const rcedit = require('rcedit');
console.log(`Using rcedit: ${rceditPath}`);
execSync(`"${rceditPath}" "${exePath}" --set-icon "${iconPath}"`, { console.log(`Setting icon and version info for: ${exePath}`);
stdio: 'inherit' 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) { } catch (error) {
console.error('Failed to set icon:', error.message); console.error('Failed to set icon:', error.message);
// 不阻止打包继续
} }
}; };

View File

@ -13,10 +13,12 @@ import { MysqlManager } from "./services/MysqlManager";
import { NginxManager } from "./services/NginxManager"; import { NginxManager } from "./services/NginxManager";
import { RedisManager } from "./services/RedisManager"; import { RedisManager } from "./services/RedisManager";
import { NodeManager } from "./services/NodeManager"; import { NodeManager } from "./services/NodeManager";
import { GoManager } from "./services/GoManager";
import { ServiceManager } from "./services/ServiceManager"; import { ServiceManager } from "./services/ServiceManager";
import { HostsManager } from "./services/HostsManager"; import { HostsManager } from "./services/HostsManager";
import { GitManager } from "./services/GitManager"; import { GitManager } from "./services/GitManager";
import { PythonManager } from "./services/PythonManager"; import { PythonManager } from "./services/PythonManager";
import { LogManager } from "./services/LogManager";
import { ConfigStore } from "./services/ConfigStore"; import { ConfigStore } from "./services/ConfigStore";
// 获取图标路径 // 获取图标路径
@ -97,7 +99,7 @@ export function sendDownloadProgress(
type: string, type: string,
progress: number, progress: number,
downloaded: number, downloaded: number,
total: number total: number,
) { ) {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("download-progress", { mainWindow.webContents.send("download-progress", {
@ -116,10 +118,12 @@ const mysqlManager = new MysqlManager(configStore);
const nginxManager = new NginxManager(configStore); const nginxManager = new NginxManager(configStore);
const redisManager = new RedisManager(configStore); const redisManager = new RedisManager(configStore);
const nodeManager = new NodeManager(configStore); const nodeManager = new NodeManager(configStore);
const goManager = new GoManager(configStore);
const serviceManager = new ServiceManager(configStore); const serviceManager = new ServiceManager(configStore);
const hostsManager = new HostsManager(); const hostsManager = new HostsManager();
const gitManager = new GitManager(configStore); const gitManager = new GitManager(configStore);
const pythonManager = new PythonManager(configStore); const pythonManager = new PythonManager(configStore);
const logManager = new LogManager(configStore);
function createWindow() { function createWindow() {
const appIcon = createWindowIcon(); 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(); const gotTheLock = app.requestSingleInstanceLock();
@ -304,7 +317,7 @@ ipcMain.handle("window:close", () => mainWindow?.close());
// 打开外部链接 // 打开外部链接
ipcMain.handle("shell:openExternal", (_, url: string) => ipcMain.handle("shell:openExternal", (_, url: string) =>
shell.openExternal(url) shell.openExternal(url),
); );
ipcMain.handle("shell:openPath", (_, path: string) => shell.openPath(path)); ipcMain.handle("shell:openPath", (_, path: string) => shell.openPath(path));
@ -321,33 +334,33 @@ ipcMain.handle("dialog:selectDirectory", async () => {
// ==================== PHP 管理 ==================== // ==================== PHP 管理 ====================
ipcMain.handle("php:getVersions", () => phpManager.getInstalledVersions()); ipcMain.handle("php:getVersions", () => phpManager.getInstalledVersions());
ipcMain.handle("php:getAvailableVersions", () => ipcMain.handle("php:getAvailableVersions", () =>
phpManager.getAvailableVersions() phpManager.getAvailableVersions(),
); );
ipcMain.handle("php:install", (_, version: string) => ipcMain.handle("php:install", (_, version: string) =>
phpManager.install(version) phpManager.install(version),
); );
ipcMain.handle("php:uninstall", (_, version: string) => ipcMain.handle("php:uninstall", (_, version: string) =>
phpManager.uninstall(version) phpManager.uninstall(version),
); );
ipcMain.handle("php:setActive", (_, version: string) => ipcMain.handle("php:setActive", (_, version: string) =>
phpManager.setActive(version) phpManager.setActive(version),
); );
ipcMain.handle("php:getExtensions", (_, version: string) => ipcMain.handle("php:getExtensions", (_, version: string) =>
phpManager.getExtensions(version) phpManager.getExtensions(version),
); );
ipcMain.handle("php:openExtensionDir", (_, version: string) => ipcMain.handle("php:openExtensionDir", (_, version: string) =>
phpManager.openExtensionDir(version) phpManager.openExtensionDir(version),
); );
ipcMain.handle( ipcMain.handle(
"php:getAvailableExtensions", "php:getAvailableExtensions",
(_, version: string, searchKeyword?: string) => (_, version: string, searchKeyword?: string) =>
phpManager.getAvailableExtensions(version, searchKeyword) phpManager.getAvailableExtensions(version, searchKeyword),
); );
ipcMain.handle("php:enableExtension", (_, version: string, ext: string) => ipcMain.handle("php:enableExtension", (_, version: string, ext: string) =>
phpManager.enableExtension(version, ext) phpManager.enableExtension(version, ext),
); );
ipcMain.handle("php:disableExtension", (_, version: string, ext: string) => ipcMain.handle("php:disableExtension", (_, version: string, ext: string) =>
phpManager.disableExtension(version, ext) phpManager.disableExtension(version, ext),
); );
ipcMain.handle( ipcMain.handle(
"php:installExtension", "php:installExtension",
@ -356,14 +369,14 @@ ipcMain.handle(
version: string, version: string,
ext: string, ext: string,
downloadUrl?: string, downloadUrl?: string,
packageName?: string packageName?: string,
) => phpManager.installExtension(version, ext, downloadUrl, packageName) ) => phpManager.installExtension(version, ext, downloadUrl, packageName),
); );
ipcMain.handle("php:getConfig", (_, version: string) => ipcMain.handle("php:getConfig", (_, version: string) =>
phpManager.getConfig(version) phpManager.getConfig(version),
); );
ipcMain.handle("php:saveConfig", (_, version: string, config: string) => ipcMain.handle("php:saveConfig", (_, version: string, config: string) =>
phpManager.saveConfig(version, config) phpManager.saveConfig(version, config),
); );
// ==================== Composer 管理 ==================== // ==================== Composer 管理 ====================
@ -371,62 +384,62 @@ ipcMain.handle("composer:getStatus", () => phpManager.getComposerStatus());
ipcMain.handle("composer:install", () => phpManager.installComposer()); ipcMain.handle("composer:install", () => phpManager.installComposer());
ipcMain.handle("composer:uninstall", () => phpManager.uninstallComposer()); ipcMain.handle("composer:uninstall", () => phpManager.uninstallComposer());
ipcMain.handle("composer:setMirror", (_, mirror: string) => ipcMain.handle("composer:setMirror", (_, mirror: string) =>
phpManager.setComposerMirror(mirror) phpManager.setComposerMirror(mirror),
); );
ipcMain.handle( ipcMain.handle(
"composer:createLaravelProject", "composer:createLaravelProject",
(_, projectName: string, targetDir: string) => (_, projectName: string, targetDir: string) =>
phpManager.createLaravelProject(projectName, targetDir) phpManager.createLaravelProject(projectName, targetDir),
); );
// ==================== MySQL 管理 ==================== // ==================== MySQL 管理 ====================
ipcMain.handle("mysql:getVersions", () => mysqlManager.getInstalledVersions()); ipcMain.handle("mysql:getVersions", () => mysqlManager.getInstalledVersions());
ipcMain.handle("mysql:getAvailableVersions", () => ipcMain.handle("mysql:getAvailableVersions", () =>
mysqlManager.getAvailableVersions() mysqlManager.getAvailableVersions(),
); );
ipcMain.handle("mysql:install", (_, version: string) => ipcMain.handle("mysql:install", (_, version: string) =>
mysqlManager.install(version) mysqlManager.install(version),
); );
ipcMain.handle("mysql:uninstall", (_, version: string) => ipcMain.handle("mysql:uninstall", (_, version: string) =>
mysqlManager.uninstall(version) mysqlManager.uninstall(version),
); );
ipcMain.handle("mysql:start", (_, version: string) => ipcMain.handle("mysql:start", (_, version: string) =>
mysqlManager.start(version) mysqlManager.start(version),
); );
ipcMain.handle("mysql:stop", (_, version: string) => ipcMain.handle("mysql:stop", (_, version: string) =>
mysqlManager.stop(version) mysqlManager.stop(version),
); );
ipcMain.handle("mysql:restart", (_, version: string) => ipcMain.handle("mysql:restart", (_, version: string) =>
mysqlManager.restart(version) mysqlManager.restart(version),
); );
ipcMain.handle("mysql:getStatus", (_, version: string) => ipcMain.handle("mysql:getStatus", (_, version: string) =>
mysqlManager.getStatus(version) mysqlManager.getStatus(version),
); );
ipcMain.handle( ipcMain.handle(
"mysql:changePassword", "mysql:changePassword",
(_, version: string, newPassword: string, currentPassword?: string) => (_, version: string, newPassword: string, currentPassword?: string) =>
mysqlManager.changeRootPassword(version, newPassword, currentPassword) mysqlManager.changeRootPassword(version, newPassword, currentPassword),
); );
ipcMain.handle("mysql:getConfig", (_, version: string) => ipcMain.handle("mysql:getConfig", (_, version: string) =>
mysqlManager.getConfig(version) mysqlManager.getConfig(version),
); );
ipcMain.handle("mysql:saveConfig", (_, version: string, config: string) => ipcMain.handle("mysql:saveConfig", (_, version: string, config: string) =>
mysqlManager.saveConfig(version, config) mysqlManager.saveConfig(version, config),
); );
ipcMain.handle("mysql:reinitialize", (_, version: string) => ipcMain.handle("mysql:reinitialize", (_, version: string) =>
mysqlManager.reinitialize(version) mysqlManager.reinitialize(version),
); );
// ==================== Nginx 管理 ==================== // ==================== Nginx 管理 ====================
ipcMain.handle("nginx:getVersions", () => nginxManager.getInstalledVersions()); ipcMain.handle("nginx:getVersions", () => nginxManager.getInstalledVersions());
ipcMain.handle("nginx:getAvailableVersions", () => ipcMain.handle("nginx:getAvailableVersions", () =>
nginxManager.getAvailableVersions() nginxManager.getAvailableVersions(),
); );
ipcMain.handle("nginx:install", (_, version: string) => ipcMain.handle("nginx:install", (_, version: string) =>
nginxManager.install(version) nginxManager.install(version),
); );
ipcMain.handle("nginx:uninstall", (_, version: string) => ipcMain.handle("nginx:uninstall", (_, version: string) =>
nginxManager.uninstall(version) nginxManager.uninstall(version),
); );
ipcMain.handle("nginx:start", () => nginxManager.start()); ipcMain.handle("nginx:start", () => nginxManager.start());
ipcMain.handle("nginx:stop", () => nginxManager.stop()); 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:getStatus", () => nginxManager.getStatus());
ipcMain.handle("nginx:getConfig", () => nginxManager.getConfig()); ipcMain.handle("nginx:getConfig", () => nginxManager.getConfig());
ipcMain.handle("nginx:saveConfig", (_, config: string) => ipcMain.handle("nginx:saveConfig", (_, config: string) =>
nginxManager.saveConfig(config) nginxManager.saveConfig(config),
); );
ipcMain.handle("nginx:getSites", () => nginxManager.getSites()); ipcMain.handle("nginx:getSites", () => nginxManager.getSites());
ipcMain.handle("nginx:addSite", (_, site: any) => nginxManager.addSite(site)); ipcMain.handle("nginx:addSite", (_, site: any) => nginxManager.addSite(site));
ipcMain.handle("nginx:removeSite", (_, name: string) => ipcMain.handle("nginx:removeSite", (_, name: string) =>
nginxManager.removeSite(name) nginxManager.removeSite(name),
); );
ipcMain.handle("nginx:updateSite", (_, originalName: string, site: any) => ipcMain.handle("nginx:updateSite", (_, originalName: string, site: any) =>
nginxManager.updateSite(originalName, site) nginxManager.updateSite(originalName, site),
); );
ipcMain.handle("nginx:enableSite", (_, name: string) => ipcMain.handle("nginx:enableSite", (_, name: string) =>
nginxManager.enableSite(name) nginxManager.enableSite(name),
); );
ipcMain.handle("nginx:disableSite", (_, name: string) => ipcMain.handle("nginx:disableSite", (_, name: string) =>
nginxManager.disableSite(name) nginxManager.disableSite(name),
); );
ipcMain.handle("nginx:generateLaravelConfig", (_, site: any) => ipcMain.handle("nginx:generateLaravelConfig", (_, site: any) =>
nginxManager.generateLaravelConfig(site) nginxManager.generateLaravelConfig(site),
); );
ipcMain.handle("nginx:requestSSL", (_, domain: string, email: string) => ipcMain.handle("nginx:requestSSL", (_, domain: string, email: string) =>
nginxManager.requestSSLCertificate(domain, email) nginxManager.requestSSLCertificate(domain, email),
); );
// ==================== Redis 管理 ==================== // ==================== Redis 管理 ====================
ipcMain.handle("redis:getVersions", () => redisManager.getInstalledVersions()); ipcMain.handle("redis:getVersions", () => redisManager.getInstalledVersions());
ipcMain.handle("redis:getAvailableVersions", () => ipcMain.handle("redis:getAvailableVersions", () =>
redisManager.getAvailableVersions() redisManager.getAvailableVersions(),
); );
ipcMain.handle("redis:install", (_, version: string) => ipcMain.handle("redis:install", (_, version: string) =>
redisManager.install(version) redisManager.install(version),
); );
ipcMain.handle("redis:uninstall", (_, version: string) => ipcMain.handle("redis:uninstall", (_, version: string) =>
redisManager.uninstall(version) redisManager.uninstall(version),
); );
ipcMain.handle("redis:start", () => redisManager.start()); ipcMain.handle("redis:start", () => redisManager.start());
ipcMain.handle("redis:stop", () => redisManager.stop()); 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:getStatus", () => redisManager.getStatus());
ipcMain.handle("redis:getConfig", () => redisManager.getConfig()); ipcMain.handle("redis:getConfig", () => redisManager.getConfig());
ipcMain.handle("redis:saveConfig", (_, config: string) => ipcMain.handle("redis:saveConfig", (_, config: string) =>
redisManager.saveConfig(config) redisManager.saveConfig(config),
); );
// ==================== Node.js 管理 ==================== // ==================== Node.js 管理 ====================
ipcMain.handle("node:getVersions", () => nodeManager.getInstalledVersions()); ipcMain.handle("node:getVersions", () => nodeManager.getInstalledVersions());
ipcMain.handle("node:getAvailableVersions", () => ipcMain.handle("node:getAvailableVersions", () =>
nodeManager.getAvailableVersions() nodeManager.getAvailableVersions(),
); );
ipcMain.handle("node:install", (_, version: string, downloadUrl: string) => ipcMain.handle("node:install", (_, version: string, downloadUrl: string) =>
nodeManager.install(version, downloadUrl) nodeManager.install(version, downloadUrl),
); );
ipcMain.handle("node:uninstall", (_, version: string) => ipcMain.handle("node:uninstall", (_, version: string) =>
nodeManager.uninstall(version) nodeManager.uninstall(version),
); );
ipcMain.handle("node:setActive", (_, version: string) => ipcMain.handle("node:setActive", (_, version: string) =>
nodeManager.setActive(version) nodeManager.setActive(version),
); );
ipcMain.handle("node:getInfo", (_, version: string) => 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:getAll", () => serviceManager.getAllServices());
ipcMain.handle("service:setAutoStart", (_, service: string, enabled: boolean) => ipcMain.handle("service:setAutoStart", (_, service: string, enabled: boolean) =>
serviceManager.setAutoStart(service, enabled) serviceManager.setAutoStart(service, enabled),
); );
ipcMain.handle("service:getAutoStart", (_, service: string) => ipcMain.handle("service:getAutoStart", (_, service: string) =>
serviceManager.getAutoStart(service) serviceManager.getAutoStart(service),
); );
ipcMain.handle("service:startAll", () => serviceManager.startAll()); ipcMain.handle("service:startAll", () => serviceManager.startAll());
ipcMain.handle("service:stopAll", () => serviceManager.stopAll()); ipcMain.handle("service:stopAll", () => serviceManager.stopAll());
// PHP-CGI 管理 - 支持多版本 // PHP-CGI 管理 - 支持多版本
ipcMain.handle("service:getPhpCgiStatus", () => serviceManager.getPhpCgiStatus()); ipcMain.handle("service:getPhpCgiStatus", () =>
serviceManager.getPhpCgiStatus(),
);
ipcMain.handle("service:startPhpCgi", () => serviceManager.startPhpCgi()); ipcMain.handle("service:startPhpCgi", () => serviceManager.startPhpCgi());
ipcMain.handle("service:stopPhpCgi", () => serviceManager.stopPhpCgi()); ipcMain.handle("service:stopPhpCgi", () => serviceManager.stopPhpCgi());
ipcMain.handle("service:startAllPhpCgi", () => serviceManager.startAllPhpCgi()); ipcMain.handle("service:startAllPhpCgi", () => serviceManager.startAllPhpCgi());
ipcMain.handle("service:stopAllPhpCgi", () => serviceManager.stopAllPhpCgi()); ipcMain.handle("service:stopAllPhpCgi", () => serviceManager.stopAllPhpCgi());
ipcMain.handle("service:startPhpCgiVersion", (_, version: string) => serviceManager.startPhpCgiVersion(version)); ipcMain.handle("service:startPhpCgiVersion", (_, version: string) =>
ipcMain.handle("service:stopPhpCgiVersion", (_, version: string) => serviceManager.stopPhpCgiVersion(version)); serviceManager.startPhpCgiVersion(version),
ipcMain.handle("service:getPhpCgiPort", (_, version: string) => serviceManager.getPhpCgiPort(version)); );
ipcMain.handle("service:stopPhpCgiVersion", (_, version: string) =>
serviceManager.stopPhpCgiVersion(version),
);
ipcMain.handle("service:getPhpCgiPort", (_, version: string) =>
serviceManager.getPhpCgiPort(version),
);
// ==================== Hosts 管理 ==================== // ==================== Hosts 管理 ====================
ipcMain.handle("hosts:get", () => hostsManager.getHosts()); ipcMain.handle("hosts:get", () => hostsManager.getHosts());
ipcMain.handle("hosts:add", (_, domain: string, ip: string) => ipcMain.handle("hosts:add", (_, domain: string, ip: string) =>
hostsManager.addHost(domain, ip) hostsManager.addHost(domain, ip),
); );
ipcMain.handle("hosts:remove", (_, domain: string) => ipcMain.handle("hosts:remove", (_, domain: string) =>
hostsManager.removeHost(domain) hostsManager.removeHost(domain),
); );
// ==================== Git 管理 ==================== // ==================== Git 管理 ====================
ipcMain.handle("git:getVersions", () => gitManager.getInstalledVersions()); ipcMain.handle("git:getVersions", () => gitManager.getInstalledVersions());
ipcMain.handle("git:getAvailableVersions", () => ipcMain.handle("git:getAvailableVersions", () =>
gitManager.getAvailableVersions() gitManager.getAvailableVersions(),
); );
ipcMain.handle("git:install", (_, version: string) => ipcMain.handle("git:install", (_, version: string) =>
gitManager.install(version) gitManager.install(version),
); );
ipcMain.handle("git:uninstall", () => gitManager.uninstall()); ipcMain.handle("git:uninstall", () => gitManager.uninstall());
ipcMain.handle("git:checkSystem", () => gitManager.checkSystemGit()); ipcMain.handle("git:checkSystem", () => gitManager.checkSystemGit());
ipcMain.handle("git:getConfig", () => gitManager.getGitConfig()); ipcMain.handle("git:getConfig", () => gitManager.getGitConfig());
ipcMain.handle("git:setConfig", (_, name: string, email: string) => ipcMain.handle("git:setConfig", (_, name: string, email: string) =>
gitManager.setGitConfig(name, email) gitManager.setGitConfig(name, email),
); );
// ==================== Python 管理 ==================== // ==================== Python 管理 ====================
ipcMain.handle("python:getVersions", () => pythonManager.getInstalledVersions()); ipcMain.handle("python:getVersions", () =>
pythonManager.getInstalledVersions(),
);
ipcMain.handle("python:getAvailableVersions", () => ipcMain.handle("python:getAvailableVersions", () =>
pythonManager.getAvailableVersions() pythonManager.getAvailableVersions(),
); );
ipcMain.handle("python:install", (_, version: string) => ipcMain.handle("python:install", (_, version: string) =>
pythonManager.install(version) pythonManager.install(version),
); );
ipcMain.handle("python:uninstall", (_, version: string) => ipcMain.handle("python:uninstall", (_, version: string) =>
pythonManager.uninstall(version) pythonManager.uninstall(version),
); );
ipcMain.handle("python:setActive", (_, version: string) => ipcMain.handle("python:setActive", (_, version: string) =>
pythonManager.setActive(version) pythonManager.setActive(version),
); );
ipcMain.handle("python:checkSystem", () => pythonManager.checkSystemPython()); ipcMain.handle("python:checkSystem", () => pythonManager.checkSystemPython());
ipcMain.handle("python:getPipInfo", (_, version: string) => ipcMain.handle("python:getPipInfo", (_, version: string) =>
pythonManager.getPipInfo(version) pythonManager.getPipInfo(version),
); );
ipcMain.handle( ipcMain.handle(
"python:installPackage", "python:installPackage",
(_, version: string, packageName: string) => (_, version: string, packageName: string) =>
pythonManager.installPackage(version, packageName) pythonManager.installPackage(version, packageName),
); );
// ==================== 配置管理 ==================== // ==================== 配置管理 ====================
ipcMain.handle("config:get", (_, key: string) => configStore.get(key)); ipcMain.handle("config:get", (_, key: string) => configStore.get(key));
ipcMain.handle("config:set", (_, key: string, value: any) => 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:getBasePath", () => configStore.getBasePath());
ipcMain.handle("config:setBasePath", (_, path: string) => ipcMain.handle("config:setBasePath", (_, path: string) =>
configStore.setBasePath(path) configStore.setBasePath(path),
); );
// ==================== 应用设置 ==================== // ==================== 应用设置 ====================
// 设置开机自启(以管理员模式,使用任务计划程序 // 设置开机自启(以管理员模式,使用任务计划程序,静默启动
ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => { 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 exePath = app.getPath("exe");
const taskName = "PHPerDevManager"; const taskName = "PHPerDevManager";
@ -601,12 +644,18 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
// 忽略删除失败(可能任务不存在) // 忽略删除失败(可能任务不存在)
} }
// 创建任务计划程序任务,以最高权限运行 // 创建 VBS 启动脚本(确保静默启动)
const command = `schtasks /create /tn "${taskName}" /tr "\\"${exePath}\\"" /sc onlogon /rl highest /f`; 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 }); execSync(command, { encoding: "buffer", windowsHide: true });
configStore.set("autoLaunch", true); configStore.set("autoLaunch", true);
return { success: true, message: "已启用开机自启(管理员模式)" }; return { success: true, message: "已启用开机自启(静默模式)" };
} else { } else {
// 删除任务计划程序任务 // 删除任务计划程序任务
try { try {
@ -617,6 +666,18 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
} catch (e) { } 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); configStore.set("autoLaunch", false);
return { success: true, message: "已禁用开机自启" }; 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) => { ipcMain.handle("app:setStartMinimized", (_, enabled: boolean) => {
configStore.set("startMinimized", enabled); configStore.set("startMinimized", enabled);
@ -666,3 +759,17 @@ ipcMain.handle("app:quit", () => {
isQuitting = true; isQuitting = true;
app.quit(); 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),
);

View File

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

View File

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

View File

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

View File

@ -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()
}
}
}

View File

@ -331,9 +331,12 @@ export class NginxManager {
return { success: true, message: 'Nginx 已经在运行' } return { success: true, message: 'Nginx 已经在运行' }
} }
// 启动 Nginx // 使用 VBScript 静默启动 Nginx
const child = spawn(nginxExe, [], { const vbsPath = join(nginxPath, 'start_nginx.vbs')
cwd: nginxPath, 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, detached: true,
stdio: 'ignore', stdio: 'ignore',
windowsHide: true windowsHide: true

File diff suppressed because it is too large Load Diff

View File

@ -307,15 +307,16 @@ export class RedisManager {
await this.createDefaultConfig() await this.createDefaultConfig()
} }
// 使用相对路径启动(避免 Cygwin 路径问题) // 使用 VBScript 静默启动 Redis避免黑窗口闪烁
// Redis Windows 版本使用 Cygwin需要在正确的工作目录下用相对路径
const configFileName = 'redis.windows.conf' const configFileName = 'redis.windows.conf'
const child = spawn(redisServer, [configFileName], { const vbsPath = join(redisPath, 'start_redis.vbs')
cwd: redisPath, 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, detached: true,
stdio: 'ignore', stdio: 'ignore',
windowsHide: true, windowsHide: true
shell: false
}) })
child.unref() child.unref()

View File

@ -1,7 +1,7 @@
import { ConfigStore } from './ConfigStore' import { ConfigStore } from './ConfigStore'
import { exec, spawn } from 'child_process' import { exec, spawn } from 'child_process'
import { promisify } from 'util' 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' import { join } from 'path'
const execAsync = promisify(exec) const execAsync = promisify(exec)
@ -562,6 +562,32 @@ export class ServiceManager {
} }
private async startProcess(exe: string, args: string[], cwd: string): Promise<void> { 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, { const child = spawn(exe, args, {
cwd, cwd,
detached: true, detached: true,
@ -571,4 +597,5 @@ export class ServiceManager {
child.unref() child.unref()
} }
} }
}

232
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "phper-dev-manager", "name": "phper-dev-manager",
"version": "1.0.0", "version": "1.0.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "phper-dev-manager", "name": "phper-dev-manager",
"version": "1.0.0", "version": "1.0.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1", "@element-plus/icons-vue": "^2.3.1",
@ -26,12 +26,13 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"electron": "^28.0.0", "electron": "^28.0.0",
"electron-builder": "^24.9.1", "electron-builder": "^24.9.1",
"rcedit": "^5.0.2",
"sass": "^1.69.5", "sass": "^1.69.5",
"typescript": "^5.3.2", "typescript": "^5.9.3",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-plugin-electron": "^0.15.5", "vite-plugin-electron": "^0.15.5",
"vite-plugin-electron-renderer": "^0.14.5", "vite-plugin-electron-renderer": "^0.14.5",
"vue-tsc": "^1.8.25", "vue-tsc": "^3.2.1",
"wait-on": "^7.2.0" "wait-on": "^7.2.0"
} }
}, },
@ -1880,34 +1881,32 @@
} }
}, },
"node_modules/@volar/language-core": { "node_modules/@volar/language-core": {
"version": "1.11.1", "version": "2.4.27",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.27.tgz",
"integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", "integrity": "sha512-DjmjBWZ4tJKxfNC1F6HyYERNHPYS7L7OPFyCrestykNdUZMFYzI9WTyvwPcaNaHlrEUwESHYsfEw3isInncZxQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/source-map": "1.11.1" "@volar/source-map": "2.4.27"
} }
}, },
"node_modules/@volar/source-map": { "node_modules/@volar/source-map": {
"version": "1.11.1", "version": "2.4.27",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.27.tgz",
"integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", "integrity": "sha512-ynlcBReMgOZj2i6po+qVswtDUeeBRCTgDurjMGShbm8WYZgJ0PA4RmtebBJ0BCYol1qPv3GQF6jK7C9qoVc7lg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"dependencies": {
"muggle-string": "^0.3.1"
}
}, },
"node_modules/@volar/typescript": { "node_modules/@volar/typescript": {
"version": "1.11.1", "version": "2.4.27",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.27.tgz",
"integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", "integrity": "sha512-eWaYCcl/uAPInSK2Lze6IqVWaBu/itVqR5InXcHXFyles4zO++Mglt3oxdgj75BDcv1Knr9Y93nowS8U3wqhxg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/language-core": "1.11.1", "@volar/language-core": "2.4.27",
"path-browserify": "^1.0.1" "path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8"
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
@ -1967,45 +1966,32 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/language-core": { "node_modules/@vue/language-core": {
"version": "1.8.27", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.1.tgz",
"integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", "integrity": "sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/language-core": "~1.11.1", "@volar/language-core": "2.4.27",
"@volar/source-map": "~1.11.1", "@vue/compiler-dom": "^3.5.0",
"@vue/compiler-dom": "^3.3.0", "@vue/shared": "^3.5.0",
"@vue/shared": "^3.3.0", "alien-signals": "^3.0.0",
"computeds": "^0.0.1", "muggle-string": "^0.4.1",
"minimatch": "^9.0.3",
"muggle-string": "^0.3.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"vue-template-compiler": "^2.7.14" "picomatch": "^4.0.2"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
} }
}, },
"node_modules/@vue/language-core/node_modules/minimatch": { "node_modules/@vue/language-core/node_modules/picomatch": {
"version": "9.0.5", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "ISC", "license": "MIT",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
@ -2191,6 +2177,13 @@
"ajv": "^6.9.1" "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": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@ -2931,13 +2924,6 @@
"node": ">= 10" "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": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -3148,6 +3134,31 @@
"node": ">= 8" "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": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@ -3177,13 +3188,6 @@
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT" "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": { "node_modules/debounce-fn": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz",
@ -4466,16 +4470,6 @@
"node": ">= 0.4" "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": { "node_modules/hosted-git-info": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz",
@ -4629,6 +4623,22 @@
"is-ci": "bin.js" "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": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -4683,6 +4693,19 @@
"node": ">=8" "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": { "node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@ -5167,9 +5190,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/muggle-string": { "node_modules/muggle-string": {
"version": "0.3.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
"integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -5576,6 +5599,19 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/read-config-file": {
"version": "6.3.2", "version": "6.3.2",
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz", "resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.3.2.tgz",
@ -6491,6 +6527,13 @@
"license": "MIT", "license": "MIT",
"peer": true "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": { "node_modules/vue": {
"version": "3.5.26", "version": "3.5.26",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
@ -6554,46 +6597,21 @@
"vue": "^3.5.0" "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": { "node_modules/vue-tsc": {
"version": "1.8.27", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.1.tgz",
"integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", "integrity": "sha512-I23Rk8dkQfmcSbxDO0dmg9ioMLjKA1pjlU3Lz6Jfk2pMGu3Uryu9810XkcZH24IzPbhzPCnkKo2rEMRX0skSrw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/typescript": "~1.11.1", "@volar/typescript": "2.4.27",
"@vue/language-core": "1.8.27", "@vue/language-core": "3.2.1"
"semver": "^7.5.4"
}, },
"bin": { "bin": {
"vue-tsc": "bin/vue-tsc.js" "vue-tsc": "bin/vue-tsc.js"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": ">=5.0.0"
}
},
"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"
} }
}, },
"node_modules/wait-on": { "node_modules/wait-on": {

View File

@ -1,14 +1,20 @@
{ {
"name": "phper-dev-manager", "name": "phper-dev-manager",
"version": "1.0.0", "version": "1.0.10",
"description": "PHP开发环境管理器 - 管理PHP、MySQL、Nginx、Redis服务", "description": "PHP开发环境管理器 - 管理PHP、MySQL、Nginx、Redis服务",
"main": "dist-electron/main.js", "main": "dist-electron/main.js",
"scripts": { "scripts": {
"dev": "vite", "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", "preview": "vite preview",
"electron:dev": "vite", "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", "author": "PHPer",
"license": "MIT", "license": "MIT",
@ -18,12 +24,13 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"electron": "^28.0.0", "electron": "^28.0.0",
"electron-builder": "^24.9.1", "electron-builder": "^24.9.1",
"rcedit": "^5.0.2",
"sass": "^1.69.5", "sass": "^1.69.5",
"typescript": "^5.3.2", "typescript": "^5.9.3",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-plugin-electron": "^0.15.5", "vite-plugin-electron": "^0.15.5",
"vite-plugin-electron-renderer": "^0.14.5", "vite-plugin-electron-renderer": "^0.14.5",
"vue-tsc": "^1.8.25", "vue-tsc": "^3.2.1",
"wait-on": "^7.2.0" "wait-on": "^7.2.0"
}, },
"dependencies": { "dependencies": {
@ -68,7 +75,9 @@
} }
], ],
"icon": "build/icon.ico", "icon": "build/icon.ico",
"requestedExecutionLevel": "requireAdministrator" "executableName": "PHPer开发环境管理器",
"requestedExecutionLevel": "requireAdministrator",
"signAndEditExecutable": true
}, },
"nsis": { "nsis": {
"oneClick": false, "oneClick": false,
@ -78,6 +87,8 @@
"createDesktopShortcut": true, "createDesktopShortcut": true,
"createStartMenuShortcut": true, "createStartMenuShortcut": true,
"shortcutName": "PHPer开发环境管理器", "shortcutName": "PHPer开发环境管理器",
"installerIcon": "build/icon.ico",
"uninstallerIcon": "build/icon.ico",
"installerLanguages": [ "installerLanguages": [
"zh_CN", "zh_CN",
"en_US" "en_US"

5
public/version.json Normal file
View 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
View 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`)

View File

@ -34,15 +34,16 @@
:key="item.path" :key="item.path"
:to="item.path" :to="item.path"
class="nav-item" 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> <el-icon class="nav-icon"><component :is="item.icon" /></el-icon>
<span class="nav-label">{{ item.label }}</span> <span class="nav-label">{{ item.label }}</span>
<span <span
v-if="item.service" v-if="item.service"
class="status-dot" class="status-dot"
:class="{ running: serviceStatus[item.service as keyof typeof serviceStatus] }" :class="{
></span> running:
serviceStatus[item.service as keyof typeof serviceStatus],
}"></span>
</router-link> </router-link>
</nav> </nav>
@ -62,10 +63,10 @@
<!-- 内容区 --> <!-- 内容区 -->
<main class="content"> <main class="content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component, route }">
<transition name="fade" mode="out-in"> <keep-alive :include="cachedViews">
<component :is="Component" /> <component :is="Component" :key="route.path" />
</transition> </keep-alive>
</router-view> </router-view>
</main> </main>
</div> </div>
@ -73,100 +74,127 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from "vue";
import { ElMessage } from 'element-plus' import { ElMessage } from "element-plus";
import { useServiceStore } from './stores/serviceStore' import { useServiceStore } from "./stores/serviceStore";
const store = useServiceStore() const store = useServiceStore();
const isDark = ref(true) const isDark = ref(true);
const startingAll = ref(false) const startingAll = ref(false);
const stoppingAll = ref(false) const stoppingAll = ref(false);
// -
const cachedViews = [
"Dashboard",
"PhpManager",
"MysqlManager",
"NginxManager",
"RedisManager",
"NodeManager",
"GoManager",
"PythonManager",
"GitManager",
"SitesManager",
"HostsManager",
"Settings",
];
// store // store
const serviceStatus = computed(() => ({ const serviceStatus = computed(() => ({
nginx: store.serviceStatus.nginx, nginx: store.serviceStatus.nginx,
mysql: store.serviceStatus.mysql, mysql: store.serviceStatus.mysql,
redis: store.serviceStatus.redis redis: store.serviceStatus.redis,
})) }));
const menuItems = [ const menuItems = [
{ path: '/', label: '仪表盘', icon: 'Odometer', service: null }, { path: "/", label: "仪表盘", icon: "Odometer", service: null },
{ path: '/php', label: 'PHP 管理', icon: 'Files', service: null }, { path: "/php", label: "PHP 管理", icon: "Files", service: null },
{ path: '/mysql', label: 'MySQL 管理', icon: 'Coin', service: 'mysql' }, { 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: "/nginx",
{ path: '/nodejs', label: 'Node.js 管理', icon: 'Promotion', service: null }, label: "Nginx 管理",
{ path: '/python', label: 'Python 管理', icon: 'Platform', service: null }, icon: "Connection",
{ path: '/git', label: 'Git 管理', icon: 'Share', service: null }, service: "nginx",
{ path: '/sites', label: '站点管理', icon: 'Monitor', service: null }, },
{ path: '/hosts', label: 'Hosts 管理', icon: 'Document', service: null }, { path: "/redis", label: "Redis 管理", icon: "Grid", service: "redis" },
{ path: '/settings', label: '设置', icon: 'Setting', service: null } {
] 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 minimize = () => window.electronAPI?.minimize();
const maximize = () => window.electronAPI?.maximize() const maximize = () => window.electronAPI?.maximize();
const close = () => window.electronAPI?.close() const close = () => window.electronAPI?.close();
// //
const toggleDark = () => { const toggleDark = () => {
isDark.value = !isDark.value isDark.value = !isDark.value;
document.documentElement.classList.toggle('dark', isDark.value) document.documentElement.classList.toggle("dark", isDark.value);
} };
// //
const startAll = async () => { const startAll = async () => {
startingAll.value = true startingAll.value = true;
try { try {
const result = await window.electronAPI?.service.startAll() const result = await window.electronAPI?.service.startAll();
if (result?.success) { if (result?.success) {
ElMessage.success(result.message) ElMessage.success(result.message);
// //
setTimeout(() => store.refreshServiceStatus(), 2000) setTimeout(() => store.refreshServiceStatus(), 2000);
} else { } else {
ElMessage.error(result?.message || '启动失败') ElMessage.error(result?.message || "启动失败");
} }
} catch (error: any) { } catch (error: any) {
ElMessage.error(error.message) ElMessage.error(error.message);
} finally { } finally {
startingAll.value = false startingAll.value = false;
}
} }
};
// //
const stopAll = async () => { const stopAll = async () => {
stoppingAll.value = true stoppingAll.value = true;
try { try {
const result = await window.electronAPI?.service.stopAll() const result = await window.electronAPI?.service.stopAll();
if (result?.success) { if (result?.success) {
ElMessage.success(result.message) ElMessage.success(result.message);
await store.refreshServiceStatus() await store.refreshServiceStatus();
} else { } else {
ElMessage.error(result?.message || '停止失败') ElMessage.error(result?.message || "停止失败");
} }
} catch (error: any) { } catch (error: any) {
ElMessage.error(error.message) ElMessage.error(error.message);
} finally { } finally {
stoppingAll.value = false stoppingAll.value = false;
}
} }
};
onMounted(() => { onMounted(() => {
document.documentElement.classList.add('dark') document.documentElement.classList.add("dark");
// //
store.refreshAll() store.refreshAll();
// 5 // 5
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000) statusInterval = setInterval(() => store.refreshServiceStatus(), 5000);
}) });
onUnmounted(() => { onUnmounted(() => {
if (statusInterval) { if (statusInterval) {
clearInterval(statusInterval) clearInterval(statusInterval);
} }
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -211,7 +239,7 @@ onUnmounted(() => {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif; font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
} }
} }
@ -351,15 +379,4 @@ onUnmounted(() => {
overflow-y: auto; overflow-y: auto;
background: var(--bg-content); background: var(--bg-content);
} }
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.15s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style> </style>

View 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>

View File

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

View File

@ -248,10 +248,9 @@
class="mini-site-card" class="mini-site-card"
> >
<a <a
:href="(site.ssl ? 'https://' : 'http://') + site.domain" href="#"
target="_blank"
class="site-domain-link" class="site-domain-link"
@click.stop @click.prevent="openSite(site)"
> >
{{ site.domain }} {{ site.domain }}
<el-icon class="link-icon"><Link /></el-icon> <el-icon class="link-icon"><Link /></el-icon>
@ -277,11 +276,17 @@
<el-icon><InfoFilled /></el-icon> <el-icon><InfoFilled /></el-icon>
安装路径 安装路径
</span> </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-button size="small" @click="openBasePath">
<el-icon><FolderOpened /></el-icon> <el-icon><FolderOpened /></el-icon>
打开目录 打开目录
</el-button> </el-button>
</div> </div>
</div>
<div class="card-content"> <div class="card-content">
<div class="path-display"> <div class="path-display">
<span class="path-label">基础路径</span> <span class="path-label">基础路径</span>
@ -289,14 +294,23 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 日志查看器 -->
<LogViewer v-model="showLogViewer" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, onActivated } from 'vue'
import { ElMessage } from 'element-plus' 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 { useServiceStore } from '@/stores/serviceStore'
import LogViewer from '@/components/LogViewer.vue'
// 便 KeepAlive
defineOptions({
name: 'Dashboard'
})
const store = useServiceStore() const store = useServiceStore()
@ -352,6 +366,7 @@ const basePath = computed(() => store.basePath)
const settingPhp = ref('') const settingPhp = ref('')
const settingNode = ref('') const settingNode = ref('')
const showLogViewer = ref(false)
const startService = async (service: Service) => { const startService = async (service: Service) => {
serviceLoadingState.value[service.name] = true 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) => { const setActivePhp = async (version: string) => {
settingPhp.value = version settingPhp.value = version
try { try {
@ -570,8 +591,15 @@ const setActiveNode = async (version: string) => {
} }
onMounted(async () => { onMounted(async () => {
// //
if (store.phpVersions.length === 0 && store.nodeVersions.length === 0) {
await store.refreshAll() await store.refreshAll()
}
})
//
onActivated(async () => {
await store.refreshServiceStatus()
}) })
</script> </script>

View File

@ -177,6 +177,11 @@
import { ref, reactive, onMounted, onUnmounted } from 'vue' import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
// 便 KeepAlive
defineOptions({
name: 'GitManager'
})
interface GitVersion { interface GitVersion {
version: string version: string
path: string path: string

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

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

View File

@ -16,6 +16,10 @@
Hosts 条目 Hosts 条目
</span> </span>
<div class="card-actions"> <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-button @click="flushDns">
<el-icon><Refresh /></el-icon> <el-icon><Refresh /></el-icon>
刷新 DNS 刷新 DNS
@ -89,6 +93,11 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
// 便 KeepAlive
defineOptions({
name: 'HostsManager'
})
interface HostEntry { interface HostEntry {
ip: string ip: string
domain: string domain: string
@ -96,6 +105,7 @@ interface HostEntry {
} }
const loading = ref(false) const loading = ref(false)
const refreshing = ref(false)
const hosts = ref<HostEntry[]>([]) const hosts = ref<HostEntry[]>([])
const showAddDialog = ref(false) const showAddDialog = ref(false)
const adding = 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 () => { const addHost = async () => {
if (!hostForm.ip || !hostForm.domain) { if (!hostForm.ip || !hostForm.domain) {
ElMessage.warning('请填写 IP 地址和域名') ElMessage.warning('请填写 IP 地址和域名')

View File

@ -87,9 +87,13 @@
密码 密码
</el-button> </el-button>
<el-button size="small" @click="showConfig(version)"> <el-button size="small" @click="showConfig(version)">
<el-icon><Document /></el-icon> <el-icon><Setting /></el-icon>
配置 配置
</el-button> </el-button>
<el-button size="small" @click="showLogViewerDialog">
<el-icon><Document /></el-icon>
日志
</el-button>
<el-button <el-button
type="danger" type="danger"
size="small" size="small"
@ -222,14 +226,23 @@
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 日志查看器 -->
<LogViewer v-model="showLogViewer" initial-tab="mysql" />
</div> </div>
</template> </template>
<script setup lang="ts"> <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 { 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 { useServiceStore } from '@/stores/serviceStore'
import LogViewer from '@/components/LogViewer.vue'
// 便 KeepAlive
defineOptions({
name: 'MysqlManager'
})
const store = useServiceStore() const store = useServiceStore()
@ -271,6 +284,13 @@ const configContent = ref('')
const savingConfig = ref(false) const savingConfig = ref(false)
const currentVersion = ref('') const currentVersion = ref('')
//
const showLogViewer = ref(false)
const showLogViewerDialog = () => {
showLogViewer.value = true
}
const loadVersions = async () => { const loadVersions = async () => {
try { try {
installedVersions.value = await window.electronAPI?.mysql.getVersions() || [] installedVersions.value = await window.electronAPI?.mysql.getVersions() || []

View File

@ -74,8 +74,12 @@
<el-icon><Refresh /></el-icon> <el-icon><Refresh /></el-icon>
重载配置 重载配置
</el-button> </el-button>
<el-button @click="showConfig"> <el-button @click="showLogViewerDialog('nginx')">
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
查看日志
</el-button>
<el-button @click="showConfig">
<el-icon><Setting /></el-icon>
编辑配置 编辑配置
</el-button> </el-button>
</div> </div>
@ -180,14 +184,23 @@
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 日志查看器 -->
<LogViewer v-model="showLogViewer" :initial-tab="logViewerTab" />
</div> </div>
</template> </template>
<script setup lang="ts"> <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 { 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 { useServiceStore } from '@/stores/serviceStore'
import LogViewer from '@/components/LogViewer.vue'
// 便 KeepAlive
defineOptions({
name: 'NginxManager'
})
const store = useServiceStore() const store = useServiceStore()
@ -219,6 +232,15 @@ const showConfigDialog = ref(false)
const configContent = ref('') const configContent = ref('')
const savingConfig = ref(false) 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 () => { const loadData = async () => {
try { try {
const versions = await window.electronAPI?.nginx.getVersions() || [] const versions = await window.electronAPI?.nginx.getVersions() || []

View File

@ -140,6 +140,11 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Promotion, Box, InfoFilled, Loading } from '@element-plus/icons-vue' import { Plus, Promotion, Box, InfoFilled, Loading } from '@element-plus/icons-vue'
// 便 KeepAlive
defineOptions({
name: 'NodeManager'
})
interface NodeVersion { interface NodeVersion {
version: string version: string
path: string path: string

View File

@ -45,11 +45,47 @@
<div class="version-name"> <div class="version-name">
PHP {{ version.version }} PHP {{ version.version }}
<el-tag v-if="version.isActive" type="success" size="small" class="ml-2">当前使用</el-tag> <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>
<div class="version-path">{{ version.path }}</div> <div class="version-path">{{ version.path }}</div>
</div> </div>
</div> </div>
<div class="version-actions"> <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 <el-button
v-if="!version.isActive" v-if="!version.isActive"
type="primary" type="primary"
@ -63,9 +99,13 @@
扩展 扩展
</el-button> </el-button>
<el-button size="small" @click="showConfig(version)"> <el-button size="small" @click="showConfig(version)">
<el-icon><Document /></el-icon> <el-icon><EditPen /></el-icon>
配置 配置
</el-button> </el-button>
<el-button size="small" @click="showLogViewerDialog">
<el-icon><Document /></el-icon>
日志
</el-button>
<el-button <el-button
type="danger" type="danger"
size="small" size="small"
@ -312,21 +352,34 @@
</div> </div>
<div v-else class="extensions-list"> <div v-else class="extensions-list">
<div class="extensions-count"> <div class="extensions-count">
找到 {{ availableExtensions.length }} 适用于 PHP {{ currentVersion }} 扩展 找到 {{ availableExtensions.length }} 扩展
</div> </div>
<div <div
v-for="ext in availableExtensions" v-for="ext in availableExtensions"
:key="ext.name" :key="ext.name"
class="extension-item" class="extension-item"
:class="{ 'not-available': ext.notAvailableReason }"
> >
<div class="ext-info"> <div class="ext-info">
<div class="ext-main"> <div class="ext-main">
<span class="ext-name" v-html="highlightKeyword(ext.name)"></span> <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> </div>
<span class="ext-desc" v-if="ext.description">{{ ext.description }}</span> <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> </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 <el-button
v-else
type="primary" type="primary"
size="small" size="small"
@click="installExtension(ext)" @click="installExtension(ext)"
@ -366,14 +419,23 @@
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 日志查看器 -->
<LogViewer v-model="showLogViewer" initial-tab="php" />
</div> </div>
</template> </template>
<script setup lang="ts"> <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 { 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 { useServiceStore } from '@/stores/serviceStore'
import LogViewer from '@/components/LogViewer.vue'
// 便 KeepAlive
defineOptions({
name: 'PhpManager'
})
const store = useServiceStore() const store = useServiceStore()
@ -402,9 +464,12 @@ interface AvailableExtension {
downloadUrl: string downloadUrl: string
description?: string description?: string
packageName?: string // Packagist PIE packageName?: string // Packagist PIE
supportedPhpVersions?: string[] // PHP
notAvailableReason?: string //
} }
const loading = ref(false) const loading = ref(false)
const initialLoaded = ref(false) //
const installedVersions = ref<PhpVersion[]>([]) const installedVersions = ref<PhpVersion[]>([])
const availableVersions = ref<AvailableVersion[]>([]) const availableVersions = ref<AvailableVersion[]>([])
const showInstallDialog = ref(false) const showInstallDialog = ref(false)
@ -416,6 +481,30 @@ const downloadProgress = reactive({
total: 0 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 showExtensionsDialog = ref(false)
const loadingExtensions = ref(false) const loadingExtensions = ref(false)
const extensions = ref<Extension[]>([]) const extensions = ref<Extension[]>([])
@ -465,6 +554,13 @@ const showConfigDialog = ref(false)
const configContent = ref('') const configContent = ref('')
const savingConfig = ref(false) const savingConfig = ref(false)
//
const showLogViewer = ref(false)
const showLogViewerDialog = () => {
showLogViewer.value = true
}
// Composer // Composer
const composerStatus = ref<{ const composerStatus = ref<{
installed: boolean 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 // Composer
const loadComposerStatus = async () => { const loadComposerStatus = async () => {
try { try {
@ -812,10 +953,30 @@ const formatSize = (bytes: number) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i] return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
} }
onMounted(() => { onMounted(async () => {
loadVersions() // 使 store
loadAvailableVersions() 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() loadComposerStatus()
])
loading.value = false
initialLoaded.value = true
//
loadAvailableVersions()
// //
window.electronAPI?.onDownloadProgress((data: any) => { window.electronAPI?.onDownloadProgress((data: any) => {
@ -831,6 +992,13 @@ onMounted(() => {
}) })
}) })
// CGI
onActivated(async () => {
if (initialLoaded.value) {
await loadCgiStatus()
}
})
onUnmounted(() => { onUnmounted(() => {
window.electronAPI?.removeDownloadProgressListener() window.electronAPI?.removeDownloadProgressListener()
}) })
@ -928,6 +1096,11 @@ onUnmounted(() => {
border-bottom: none; border-bottom: none;
} }
&.not-available {
opacity: 0.7;
background-color: rgba(0, 0, 0, 0.02);
}
.ext-info { .ext-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -948,6 +1121,14 @@ onUnmounted(() => {
font-size: 12px; font-size: 12px;
color: var(--text-muted); color: var(--text-muted);
} }
.ext-not-available {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--warning-color, #e6a23c);
}
} }
} }

View File

@ -182,6 +182,11 @@ import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { InfoFilled } from '@element-plus/icons-vue' import { InfoFilled } from '@element-plus/icons-vue'
// 便 KeepAlive
defineOptions({
name: 'PythonManager'
})
interface PythonVersion { interface PythonVersion {
version: string version: string
path: string path: string
@ -528,6 +533,10 @@ onUnmounted(() => {
.el-form-item { .el-form-item {
margin-bottom: 16px; margin-bottom: 16px;
} }
.el-select {
width: 160px;
}
} }
.pip-hint { .pip-hint {

View File

@ -75,9 +75,13 @@
重启 重启
</el-button> </el-button>
<el-button @click="showConfig"> <el-button @click="showConfig">
<el-icon><Document /></el-icon> <el-icon><Setting /></el-icon>
编辑配置 编辑配置
</el-button> </el-button>
<el-button @click="openLogDirectory">
<el-icon><Document /></el-icon>
日志
</el-button>
</div> </div>
</div> </div>
</div> </div>
@ -184,11 +188,16 @@
</template> </template>
<script setup lang="ts"> <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 { 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 { useServiceStore } from '@/stores/serviceStore'
// 便 KeepAlive
defineOptions({
name: 'RedisManager'
})
const store = useServiceStore() const store = useServiceStore()
interface RedisStatus { 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) => { const formatSize = (bytes: number) => {
if (bytes === 0) return '0 B' if (bytes === 0) return '0 B'
const k = 1024 const k = 1024

View File

@ -111,7 +111,13 @@
</div> </div>
<div class="app-details"> <div class="app-details">
<h2 class="app-title">PHPer 开发环境管理器</h2> <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"> <p class="app-desc">
一站式 PHP 开发环境管理工具支持 PHPMySQLNginxRedisNode.js 的安装和管理 一站式 PHP 开发环境管理工具支持 PHPMySQLNginxRedisNode.js 的安装和管理
</p> </p>
@ -145,6 +151,11 @@ import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Monitor } from '@element-plus/icons-vue' import { Monitor } from '@element-plus/icons-vue'
// 便 KeepAlive
defineOptions({
name: 'Settings'
})
interface ServiceAutoStart { interface ServiceAutoStart {
name: string name: string
displayName: string displayName: string
@ -169,6 +180,13 @@ const services = reactive<ServiceAutoStart[]>([
{ name: 'redis', displayName: 'Redis', description: '开机时自动启动 Redis 服务(独立于应用)', autoStart: false } { name: 'redis', displayName: 'Redis', description: '开机时自动启动 Redis 服务(独立于应用)', autoStart: false }
]) ])
const appVersion = reactive({
version: '1.0.0',
buildTime: '',
buildDate: '',
isPackaged: false
})
const loadSettings = async () => { const loadSettings = async () => {
try { try {
basePath.value = await window.electronAPI?.config.getBasePath() || '' basePath.value = await window.electronAPI?.config.getBasePath() || ''
@ -177,6 +195,15 @@ const loadSettings = async () => {
appSettings.autoLaunch = await window.electronAPI?.app?.getAutoLaunch() || false appSettings.autoLaunch = await window.electronAPI?.app?.getAutoLaunch() || false
appSettings.startMinimized = await window.electronAPI?.app?.getStartMinimized() || false appSettings.startMinimized = await window.electronAPI?.app?.getStartMinimized() || false
//
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) { for (const service of services) {
service.autoStart = await window.electronAPI?.service.getAutoStart(service.name) || false service.autoStart = await window.electronAPI?.service.getAutoStart(service.name) || false
@ -315,6 +342,15 @@ onMounted(() => {
font-size: 14px; font-size: 14px;
color: var(--text-muted); color: var(--text-muted);
margin-bottom: 8px; margin-bottom: 8px;
display: flex;
align-items: center;
gap: 4px;
.build-date {
font-size: 12px;
color: var(--text-muted);
opacity: 0.8;
}
} }
.app-desc { .app-desc {

View File

@ -16,6 +16,10 @@
站点列表 站点列表
</span> </span>
<div class="header-actions"> <div class="header-actions">
<el-button @click="showLogViewer = true">
<el-icon><Document /></el-icon>
站点日志
</el-button>
<el-button type="success" @click="showCreateLaravelDialog = true"> <el-button type="success" @click="showCreateLaravelDialog = true">
<el-icon><Promotion /></el-icon> <el-icon><Promotion /></el-icon>
创建 Laravel 项目 创建 Laravel 项目
@ -328,14 +332,23 @@
</el-button> </el-button>
</template> </template>
</el-dialog> </el-dialog>
<!-- 日志查看器 -->
<LogViewer v-model="showLogViewer" initial-tab="sites" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' 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 { useServiceStore } from '@/stores/serviceStore'
import LogViewer from '@/components/LogViewer.vue'
// 便 KeepAlive
defineOptions({
name: 'SitesManager'
})
const store = useServiceStore() const store = useServiceStore()
@ -407,6 +420,8 @@ const editForm = reactive<SiteConfig>({
// Laravel // Laravel
const showCreateLaravelDialog = ref(false) const showCreateLaravelDialog = ref(false)
const creatingLaravel = ref(false) const creatingLaravel = ref(false)
const showLogViewer = ref(false)
const laravelForm = reactive({ const laravelForm = reactive({
projectName: '', projectName: '',
targetDir: '', targetDir: '',