Implement Composer management features in Electron app, including installation, uninstallation, status retrieval, and mirror configuration. Enhance PHP-CGI service management with start/stop capabilities for multiple versions and update UI components to reflect these changes.

This commit is contained in:
ethanfly 2025-12-26 08:59:59 +08:00
parent 894c792253
commit a91146c4e9
16 changed files with 1755 additions and 117 deletions

2
.gitignore vendored
View File

@ -33,3 +33,5 @@ Thumbs.db
# Electron
*.asar
data/

View File

@ -362,6 +362,19 @@ ipcMain.handle("php:saveConfig", (_, version: string, config: string) =>
phpManager.saveConfig(version, config)
);
// ==================== Composer 管理 ====================
ipcMain.handle("composer:getStatus", () => phpManager.getComposerStatus());
ipcMain.handle("composer:install", () => phpManager.installComposer());
ipcMain.handle("composer:uninstall", () => phpManager.uninstallComposer());
ipcMain.handle("composer:setMirror", (_, mirror: string) =>
phpManager.setComposerMirror(mirror)
);
ipcMain.handle(
"composer:createLaravelProject",
(_, projectName: string, targetDir: string) =>
phpManager.createLaravelProject(projectName, targetDir)
);
// ==================== MySQL 管理 ====================
ipcMain.handle("mysql:getVersions", () => mysqlManager.getInstalledVersions());
ipcMain.handle("mysql:getAvailableVersions", () =>
@ -489,6 +502,15 @@ ipcMain.handle("service:getAutoStart", (_, service: string) =>
);
ipcMain.handle("service:startAll", () => serviceManager.startAll());
ipcMain.handle("service:stopAll", () => serviceManager.stopAll());
// PHP-CGI 管理 - 支持多版本
ipcMain.handle("service:getPhpCgiStatus", () => serviceManager.getPhpCgiStatus());
ipcMain.handle("service:startPhpCgi", () => serviceManager.startPhpCgi());
ipcMain.handle("service:stopPhpCgi", () => serviceManager.stopPhpCgi());
ipcMain.handle("service:startAllPhpCgi", () => serviceManager.startAllPhpCgi());
ipcMain.handle("service:stopAllPhpCgi", () => serviceManager.stopAllPhpCgi());
ipcMain.handle("service:startPhpCgiVersion", (_, version: string) => serviceManager.startPhpCgiVersion(version));
ipcMain.handle("service:stopPhpCgiVersion", (_, version: string) => serviceManager.stopPhpCgiVersion(version));
ipcMain.handle("service:getPhpCgiPort", (_, version: string) => serviceManager.getPhpCgiPort(version));
// ==================== Hosts 管理 ====================
ipcMain.handle("hosts:get", () => hostsManager.getHosts());

View File

@ -39,6 +39,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
saveConfig: (version: string, config: string) => ipcRenderer.invoke('php:saveConfig', version, config)
},
// Composer 管理
composer: {
getStatus: () => ipcRenderer.invoke('composer:getStatus'),
install: () => ipcRenderer.invoke('composer:install'),
uninstall: () => ipcRenderer.invoke('composer:uninstall'),
setMirror: (mirror: string) => ipcRenderer.invoke('composer:setMirror', mirror),
createLaravelProject: (projectName: string, targetDir: string) => ipcRenderer.invoke('composer:createLaravelProject', projectName, targetDir)
},
// MySQL 管理
mysql: {
getVersions: () => ipcRenderer.invoke('mysql:getVersions'),
@ -107,7 +116,16 @@ contextBridge.exposeInMainWorld('electronAPI', {
setAutoStart: (service: string, enabled: boolean) => ipcRenderer.invoke('service:setAutoStart', service, enabled),
getAutoStart: (service: string) => ipcRenderer.invoke('service:getAutoStart', service),
startAll: () => ipcRenderer.invoke('service:startAll'),
stopAll: () => ipcRenderer.invoke('service:stopAll')
stopAll: () => ipcRenderer.invoke('service:stopAll'),
// PHP-CGI 多版本管理
getPhpCgiStatus: () => ipcRenderer.invoke('service:getPhpCgiStatus'),
startPhpCgi: () => ipcRenderer.invoke('service:startPhpCgi'),
stopPhpCgi: () => ipcRenderer.invoke('service:stopPhpCgi'),
startAllPhpCgi: () => ipcRenderer.invoke('service:startAllPhpCgi'),
stopAllPhpCgi: () => ipcRenderer.invoke('service:stopAllPhpCgi'),
startPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:startPhpCgiVersion', version),
stopPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:stopPhpCgiVersion', version),
getPhpCgiPort: (version: string) => ipcRenderer.invoke('service:getPhpCgiPort', version)
},
// Hosts 管理

View File

@ -19,6 +19,8 @@ interface ConfigSchema {
// 应用设置
autoLaunch: boolean;
startMinimized: boolean;
// Composer 设置
composerMirror: string;
}
export interface SiteConfig {
@ -66,6 +68,8 @@ export class ConfigStore {
// 应用设置默认值
autoLaunch: false,
startMinimized: false,
// Composer 镜像(空为官方源)
composerMirror: "",
},
});

View File

@ -341,12 +341,16 @@ export class MysqlManager {
} catch (pwdError: any) {
console.log('设置默认密码失败root密码为空:', pwdError.message)
}
// 设置密码后停止 MySQL让用户手动启动
console.log('停止 MySQL 服务...')
await this.stop(version)
}
console.log(`MySQL ${version} 安装完成`)
return {
success: true,
message: `MySQL ${version} 安装成功!\n\n连接信息\n• 主机localhost\n• 端口3306\n• 用户root\n• 密码123456`
message: `MySQL ${version} 安装成功!\n\n连接信息\n• 主机localhost\n• 端口3306\n• 用户root\n• 密码123456\n\n注意MySQL 已停止,请手动启动服务。`
}
} catch (error: any) {
console.error('MySQL 安装失败:', error)

View File

@ -33,6 +33,20 @@ export class NginxManager {
this.configStore = configStore
}
/**
* PHP FastCGI
* PHP 8.0.x -> 9080, PHP 8.1.x -> 9081, etc.
*/
private getPhpCgiPort(version: string): number {
const match = version.match(/^(\d+)\.(\d+)/)
if (match) {
const major = parseInt(match[1])
const minor = parseInt(match[2])
return 9000 + major * 10 + minor // 8.5 -> 9085, 8.4 -> 9084, 8.3 -> 9083
}
return 9000
}
/**
* Nginx
*/
@ -612,8 +626,10 @@ http {
private generateSiteConfig(site: SiteConfig): string {
const phpPath = this.configStore.getPhpPath(site.phpVersion)
const logsPath = this.configStore.getLogsPath()
const phpCgiPort = this.getPhpCgiPort(site.phpVersion)
let config = `
# PHP Version: ${site.phpVersion} -> Port ${phpCgiPort}
server {
listen 80;
server_name ${site.domain};
@ -628,7 +644,7 @@ server {
}
location ~ \\.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_pass 127.0.0.1:${phpCgiPort};
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
@ -662,7 +678,7 @@ server {
}
location ~ \\.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_pass 127.0.0.1:${phpCgiPort};
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
@ -682,8 +698,10 @@ server {
const logsPath = this.configStore.getLogsPath()
// Laravel 项目 public 目录
const publicPath = join(site.rootPath, 'public').replace(/\\/g, '/')
const phpCgiPort = this.getPhpCgiPort(site.phpVersion)
let config = `
# Laravel Project - PHP Version: ${site.phpVersion} -> Port ${phpCgiPort}
server {
listen 80;
server_name ${site.domain};
@ -708,7 +726,7 @@ server {
error_page 404 /index.php;
location ~ \\.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_pass 127.0.0.1:${phpCgiPort};
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
@ -752,7 +770,7 @@ server {
error_page 404 /index.php;
location ~ \\.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_pass 127.0.0.1:${phpCgiPort};
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}

View File

@ -2060,4 +2060,463 @@ if ($verifyPath -and $verifyPath.Contains($NewPhpPath)) {
rmdirSync(dir);
}
}
// ==================== Composer 管理 ====================
/**
* Composer
*/
async getComposerStatus(): Promise<{
installed: boolean;
version?: string;
path?: string;
mirror?: string;
}> {
const composerPath = this.getComposerPath();
const composerBatPath = join(this.configStore.getBasePath(), "tools", "composer.bat");
const mirror = this.configStore.get("composerMirror") || "";
console.log("检查 Composer 路径:", composerPath);
if (!existsSync(composerPath)) {
console.log("Composer 未安装");
return { installed: false, mirror };
}
let version: string | undefined;
// 方法1: 尝试直接使用 composer.bat如果在 PATH 中)
try {
console.log("尝试使用 composer --version...");
const { stdout } = await execAsync("composer --version", {
windowsHide: true,
timeout: 15000,
encoding: "utf8",
});
console.log("Composer 输出:", stdout);
// 解析版本号 - 支持多种格式
// "Composer version 2.9-dev+9497eca6e15b115d25833c68b7c5c76589953b65 (2.9-dev)"
// "Composer version 2.7.1 2024-01-01"
const versionMatch = stdout.match(/Composer version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)/);
if (versionMatch) {
version = versionMatch[1];
console.log("解析到版本:", version);
return { installed: true, version, path: composerPath, mirror };
}
} catch (e: any) {
console.log("composer 命令不可用,尝试其他方式:", e.message);
}
// 方法2: 使用 composer.bat
if (existsSync(composerBatPath)) {
try {
console.log("尝试使用 composer.bat...");
const { stdout } = await execAsync(`"${composerBatPath}" --version`, {
windowsHide: true,
timeout: 15000,
encoding: "utf8",
});
const versionMatch = stdout.match(/Composer version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)/);
if (versionMatch) {
version = versionMatch[1];
console.log("解析到版本:", version);
return { installed: true, version, path: composerPath, mirror };
}
} catch (e: any) {
console.log("composer.bat 执行失败:", e.message);
}
}
// 方法3: 使用 PHP 运行 composer.phar
try {
const activePhp = this.configStore.get("activePhpVersion");
if (activePhp) {
const phpPath = this.configStore.getPhpPath(activePhp);
const phpExe = join(phpPath, "php.exe");
if (existsSync(phpExe)) {
console.log("尝试使用 PHP 运行 composer.phar...");
const { stdout } = await execAsync(`"${phpExe}" "${composerPath}" --version`, {
windowsHide: true,
timeout: 15000,
encoding: "utf8",
});
const versionMatch = stdout.match(/Composer version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)/);
if (versionMatch) {
version = versionMatch[1];
console.log("解析到版本:", version);
}
}
}
} catch (e: any) {
console.log("PHP 运行 composer.phar 失败:", e.message);
}
return { installed: true, version, path: composerPath, mirror };
}
/**
* Composer
*/
private getComposerPath(): string {
return join(this.configStore.getBasePath(), "tools", "composer.phar");
}
/**
* Composer
*/
async installComposer(): Promise<{ success: boolean; message: string }> {
try {
const toolsDir = join(this.configStore.getBasePath(), "tools");
const composerPath = join(toolsDir, "composer.phar");
const composerBatPath = join(toolsDir, "composer.bat");
// 确保目录存在
if (!existsSync(toolsDir)) {
mkdirSync(toolsDir, { recursive: true });
}
// 下载 Composer
console.log("正在下载 Composer...");
// 尝试多个下载源
const urls = [
"https://getcomposer.org/composer.phar",
"https://mirrors.aliyun.com/composer/composer.phar",
];
let downloaded = false;
let lastError: Error | null = null;
for (const url of urls) {
try {
console.log(`尝试从 ${url} 下载...`);
await this.downloadFile(url, composerPath);
downloaded = true;
break;
} catch (e: any) {
console.error(`${url} 下载失败:`, e.message);
lastError = e;
}
}
if (!downloaded) {
throw lastError || new Error("所有下载源均失败");
}
// 验证文件是否下载成功
if (!existsSync(composerPath)) {
return { success: false, message: "Composer 下载失败,文件不存在" };
}
// 创建 composer.bat 批处理文件
const batContent = `@echo off\r\nphp "%~dp0composer.phar" %*`;
writeFileSync(composerBatPath, batContent);
console.log("创建 composer.bat:", composerBatPath);
// 添加到环境变量
await this.addComposerToPath(toolsDir);
// 验证安装
const activePhp = this.configStore.get("activePhpVersion");
if (activePhp) {
const phpPath = this.configStore.getPhpPath(activePhp);
const phpExe = join(phpPath, "php.exe");
if (existsSync(phpExe)) {
try {
const { stdout } = await execAsync(`"${phpExe}" "${composerPath}" --version`, {
windowsHide: true,
timeout: 10000,
});
console.log("Composer 安装成功:", stdout);
} catch (e) {
console.log("Composer 已下载,但验证失败(可能是 PHP 问题)");
}
}
}
return { success: true, message: "Composer 安装成功,已添加到系统环境变量" };
} catch (error: any) {
console.error("Composer 安装失败:", error);
return { success: false, message: `安装失败: ${error.message}` };
}
}
/**
* Composer
*/
async uninstallComposer(): Promise<{ success: boolean; message: string }> {
try {
const toolsDir = join(this.configStore.getBasePath(), "tools");
const composerPath = join(toolsDir, "composer.phar");
const composerBatPath = join(toolsDir, "composer.bat");
// 删除文件
if (existsSync(composerPath)) {
unlinkSync(composerPath);
console.log("已删除:", composerPath);
}
if (existsSync(composerBatPath)) {
unlinkSync(composerBatPath);
console.log("已删除:", composerBatPath);
}
// 从环境变量移除
await this.removeComposerFromPath(toolsDir);
return { success: true, message: "Composer 已卸载" };
} catch (error: any) {
console.error("Composer 卸载失败:", error);
return { success: false, message: `卸载失败: ${error.message}` };
}
}
/**
* Composer
*/
private async addComposerToPath(toolsDir: string): Promise<void> {
try {
const tempScriptPath = join(this.configStore.getTempPath(), "add_composer_path.ps1");
mkdirSync(this.configStore.getTempPath(), { recursive: true });
const psScript = `
param([string]$ComposerPath)
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if ($userPath -eq $null) { $userPath = '' }
# Check if already exists
if ($userPath.ToLower().Contains($ComposerPath.ToLower())) {
Write-Host "Composer path already in PATH"
exit 0
}
# Add to PATH
$newPath = $ComposerPath + ";" + $userPath
[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
Write-Host "SUCCESS: Composer path added to user PATH"
`;
writeFileSync(tempScriptPath, psScript, "utf-8");
const { stdout } = await execAsync(
`powershell -ExecutionPolicy Bypass -File "${tempScriptPath}" -ComposerPath "${toolsDir}"`,
{ windowsHide: true, timeout: 30000 }
);
console.log("Composer PATH 更新:", stdout);
if (existsSync(tempScriptPath)) {
unlinkSync(tempScriptPath);
}
} catch (error: any) {
console.error("添加 Composer 到 PATH 失败:", error);
}
}
/**
* Composer
*/
private async removeComposerFromPath(toolsDir: string): Promise<void> {
try {
const tempScriptPath = join(this.configStore.getTempPath(), "remove_composer_path.ps1");
mkdirSync(this.configStore.getTempPath(), { recursive: true });
const psScript = `
param([string]$ComposerPath)
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
if ($userPath -eq $null) { exit 0 }
$paths = $userPath -split ';' | Where-Object { $_ -ne '' -and $_.ToLower() -ne $ComposerPath.ToLower() }
$newPath = $paths -join ';'
[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
Write-Host "SUCCESS: Composer path removed from user PATH"
`;
writeFileSync(tempScriptPath, psScript, "utf-8");
const { stdout } = await execAsync(
`powershell -ExecutionPolicy Bypass -File "${tempScriptPath}" -ComposerPath "${toolsDir}"`,
{ windowsHide: true, timeout: 30000 }
);
console.log("Composer PATH 移除:", stdout);
if (existsSync(tempScriptPath)) {
unlinkSync(tempScriptPath);
}
} catch (error: any) {
console.error("从 PATH 移除 Composer 失败:", error);
}
}
/**
* Composer
*/
async setComposerMirror(
mirror: string
): Promise<{ success: boolean; message: string }> {
try {
const activePhp = this.configStore.get("activePhpVersion");
if (!activePhp) {
return { success: false, message: "请先设置默认 PHP 版本" };
}
const composerPath = this.getComposerPath();
if (!existsSync(composerPath)) {
return { success: false, message: "Composer 未安装" };
}
const phpPath = this.configStore.getPhpPath(activePhp);
const phpExe = join(phpPath, "php.exe");
// 检查 PHP 是否存在
if (!existsSync(phpExe)) {
return { success: false, message: `PHP 未找到: ${phpExe}` };
}
// 先移除旧的镜像配置
try {
await execAsync(
`"${phpExe}" "${composerPath}" config -g --unset repos.packagist`,
{ windowsHide: true, timeout: 30000 }
);
} catch (e) {
// 忽略移除失败
}
if (mirror) {
// 设置新镜像
await execAsync(
`"${phpExe}" "${composerPath}" config -g repos.packagist composer ${mirror}`,
{ windowsHide: true, timeout: 30000 }
);
}
// 保存到配置
this.configStore.set("composerMirror", mirror);
const mirrorName = this.getMirrorName(mirror);
return {
success: true,
message: mirror ? `已设置镜像为: ${mirrorName}` : "已恢复为官方源",
};
} catch (error: any) {
console.error("设置镜像失败:", error);
// 即使命令执行失败,也保存配置(因为镜像配置主要在创建项目时使用)
this.configStore.set("composerMirror", mirror);
const mirrorName = this.getMirrorName(mirror);
return {
success: true,
message: mirror ? `镜像配置已保存: ${mirrorName}` : "已恢复为官方源",
};
}
}
/**
*
*/
private getMirrorName(mirror: string): string {
const mirrors: Record<string, string> = {
"https://mirrors.aliyun.com/composer/": "阿里云镜像",
"https://mirrors.cloud.tencent.com/composer/": "腾讯云镜像",
"https://mirrors.huaweicloud.com/repository/php/": "华为云镜像",
"https://packagist.phpcomposer.com": "中国全量镜像",
};
return mirrors[mirror] || mirror;
}
/**
* Laravel
*/
async createLaravelProject(
projectName: string,
targetDir: string
): Promise<{ success: boolean; message: string; projectPath?: string }> {
try {
const activePhp = this.configStore.get("activePhpVersion");
if (!activePhp) {
return { success: false, message: "请先设置默认 PHP 版本" };
}
const composerPath = this.getComposerPath();
if (!existsSync(composerPath)) {
return { success: false, message: "Composer 未安装,请先安装 Composer" };
}
const phpPath = this.configStore.getPhpPath(activePhp);
const phpExe = join(phpPath, "php.exe");
// 检查 PHP 是否存在
if (!existsSync(phpExe)) {
return { success: false, message: `PHP 未找到: ${phpExe}` };
}
// 确保目标目录存在
if (!existsSync(targetDir)) {
mkdirSync(targetDir, { recursive: true });
}
const projectPath = join(targetDir, projectName);
// 检查项目目录是否已存在
if (existsSync(projectPath)) {
return { success: false, message: `项目目录已存在: ${projectPath}` };
}
console.log(`创建 Laravel 项目: ${projectName}${targetDir}`);
// 获取镜像配置
const mirror = this.configStore.get("composerMirror");
let repoOption = "";
if (mirror) {
// 使用环境变量设置镜像
repoOption = `--repository=${mirror}`;
}
// 使用 composer create-project 创建 Laravel 项目
const cmd = `"${phpExe}" "${composerPath}" create-project --prefer-dist ${repoOption} laravel/laravel "${projectName}"`;
console.log("执行命令:", cmd);
const { stdout, stderr } = await execAsync(cmd, {
cwd: targetDir,
windowsHide: true,
timeout: 600000, // 10 分钟超时
env: {
...process.env,
COMPOSER_PROCESS_TIMEOUT: "600",
COMPOSER_HOME: join(this.configStore.getBasePath(), "tools", "composer"),
},
});
console.log("Composer 输出:", stdout);
if (stderr) console.log("Composer stderr:", stderr);
// 验证项目创建成功
if (existsSync(join(projectPath, "artisan"))) {
return {
success: true,
message: `Laravel 项目 "${projectName}" 创建成功`,
projectPath,
};
} else {
return { success: false, message: "项目创建失败,请查看日志" };
}
} catch (error: any) {
console.error("创建 Laravel 项目失败:", error);
let errorMsg = error.message;
if (error.stderr) {
errorMsg = error.stderr;
}
return { success: false, message: `创建失败: ${errorMsg}` };
}
}
}

View File

@ -213,6 +213,23 @@ export class ServiceManager {
}
}
// 如果 Nginx 启动了,自动启动所有 PHP-CGI
if (autoStart.nginx) {
const phpVersions = this.configStore.get('phpVersions')
for (const version of phpVersions) {
const phpPath = this.configStore.getPhpPath(version)
const phpCgi = join(phpPath, 'php-cgi.exe')
if (existsSync(phpCgi)) {
const port = this.getPhpCgiPort(version)
const isRunning = await this.checkPort(port)
if (!isRunning) {
await this.startProcess(phpCgi, ['-b', `127.0.0.1:${port}`], phpPath)
details.push(`PHP-CGI ${version} 已自动启动 (端口 ${port})`)
}
}
}
}
if (details.length === 0) {
return { success: true, message: '没有需要自动启动的服务', details }
}
@ -277,6 +294,23 @@ export class ServiceManager {
}
}
// 启动所有已安装 PHP 版本的 CGI 进程
const phpVersions = this.configStore.get('phpVersions')
for (const version of phpVersions) {
const phpPath = this.configStore.getPhpPath(version)
const phpCgi = join(phpPath, 'php-cgi.exe')
if (existsSync(phpCgi)) {
const port = this.getPhpCgiPort(version)
const isRunning = await this.checkPort(port)
if (!isRunning) {
await this.startProcess(phpCgi, ['-b', `127.0.0.1:${port}`], phpPath)
details.push(`PHP-CGI ${version} 已启动 (端口 ${port})`)
} else {
details.push(`PHP-CGI ${version} 已在运行 (端口 ${port})`)
}
}
}
if (details.length === 0) {
return { success: true, message: '没有已安装的服务', details }
}
@ -340,37 +374,51 @@ export class ServiceManager {
}
/**
* PHP-CGI FastCGI
* PHP FastCGI
* PHP 8.0.x -> 9080, PHP 8.1.x -> 9081, etc.
*/
async startPhpCgi(): Promise<{ success: boolean; message: string }> {
try {
const activePhp = this.configStore.get('activePhpVersion')
if (!activePhp) {
return { success: false, message: '未设置活动的 PHP 版本' }
}
getPhpCgiPort(version: string): number {
// 提取主版本号,如 "8.5.1" -> "8.5" -> 85
const match = version.match(/^(\d+)\.(\d+)/)
if (match) {
const major = parseInt(match[1])
const minor = parseInt(match[2])
return 9000 + major * 10 + minor // 8.5 -> 9085, 8.4 -> 9084, 8.3 -> 9083
}
return 9000
}
const phpPath = this.configStore.getPhpPath(activePhp)
/**
* PHP-CGI
*/
async startPhpCgiVersion(version: string): Promise<{ success: boolean; message: string }> {
try {
const phpPath = this.configStore.getPhpPath(version)
const phpCgi = join(phpPath, 'php-cgi.exe')
if (!existsSync(phpCgi)) {
return { success: false, message: 'php-cgi.exe 不存在' }
return { success: false, message: `PHP ${version} 的 php-cgi.exe 不存在` }
}
// 检查是否已在运行
if (await this.checkProcess('php-cgi.exe')) {
return { success: true, message: 'PHP-CGI 已经在运行' }
const port = this.getPhpCgiPort(version)
// 检查端口是否已被占用
const isPortInUse = await this.checkPort(port)
if (isPortInUse) {
return { success: true, message: `PHP-CGI ${version} 已在端口 ${port} 运行` }
}
// 启动 PHP-CGI
await this.startProcess(phpCgi, ['-b', '127.0.0.1:9000'], phpPath)
await this.startProcess(phpCgi, ['-b', `127.0.0.1:${port}`], phpPath)
// 等待启动
await new Promise(resolve => setTimeout(resolve, 1000))
if (await this.checkProcess('php-cgi.exe')) {
return { success: true, message: 'PHP-CGI 启动成功' }
const started = await this.checkPort(port)
if (started) {
return { success: true, message: `PHP-CGI ${version} 启动成功 (端口 ${port})` }
} else {
return { success: false, message: 'PHP-CGI 启动失败' }
return { success: false, message: `PHP-CGI ${version} 启动失败` }
}
} catch (error: any) {
return { success: false, message: `启动失败: ${error.message}` }
@ -378,20 +426,104 @@ export class ServiceManager {
}
/**
* PHP-CGI
* PHP-CGI
*/
async stopPhpCgi(): Promise<{ success: boolean; message: string }> {
async stopPhpCgiVersion(version: string): Promise<{ success: boolean; message: string }> {
try {
await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 })
return { success: true, message: 'PHP-CGI 已停止' }
} catch (error: any) {
if (error.message.includes('not found')) {
return { success: true, message: 'PHP-CGI 未运行' }
const port = this.getPhpCgiPort(version)
// 查找并结束监听该端口的进程
try {
const { stdout } = await execAsync(`netstat -ano | findstr ":${port}"`, { windowsHide: true })
const lines = stdout.split('\n').filter(line => line.includes('LISTENING'))
for (const line of lines) {
const parts = line.trim().split(/\s+/)
const pid = parts[parts.length - 1]
if (pid && /^\d+$/.test(pid)) {
await execAsync(`taskkill /F /PID ${pid}`, { windowsHide: true, timeout: 5000 }).catch(() => {})
}
}
} catch (e) {
// 端口可能未被使用
}
return { success: true, message: `PHP-CGI ${version} 已停止` }
} catch (error: any) {
return { success: false, message: `停止失败: ${error.message}` }
}
}
/**
* PHP CGI
*/
async startAllPhpCgi(): Promise<{ success: boolean; message: string; details: string[] }> {
const details: string[] = []
const phpVersions = this.configStore.get('phpVersions')
for (const version of phpVersions) {
const result = await this.startPhpCgiVersion(version)
details.push(`PHP ${version}: ${result.message}`)
}
return { success: true, message: '所有 PHP-CGI 启动完成', details }
}
/**
* PHP-CGI
*/
async stopAllPhpCgi(): Promise<{ success: boolean; message: string }> {
try {
await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 }).catch(() => {})
return { success: true, message: '所有 PHP-CGI 已停止' }
} catch (error: any) {
return { success: true, message: 'PHP-CGI 未运行' }
}
}
/**
*
*/
private async checkPort(port: number): Promise<boolean> {
try {
const { stdout } = await execAsync(`netstat -ano | findstr ":${port}"`, { windowsHide: true })
return stdout.includes('LISTENING')
} catch (e) {
return false
}
}
/**
* PHP-CGI FastCGI-
*/
async startPhpCgi(): Promise<{ success: boolean; message: string }> {
const activePhp = this.configStore.get('activePhpVersion')
if (!activePhp) {
return { success: false, message: '未设置活动的 PHP 版本' }
}
return this.startPhpCgiVersion(activePhp)
}
/**
* PHP-CGI -
*/
async stopPhpCgi(): Promise<{ success: boolean; message: string }> {
return this.stopAllPhpCgi()
}
/**
* PHP-CGI
*/
async getPhpCgiStatus(): Promise<{ version: string; port: number; running: boolean }[]> {
const phpVersions = this.configStore.get('phpVersions')
const status: { version: string; port: number; running: boolean }[] = []
for (const version of phpVersions) {
const port = this.getPhpCgiPort(version)
const running = await this.checkPort(port)
status.push({ version, port, running })
}
return status
}
// ==================== 私有方法 ====================
private async checkProcess(name: string): Promise<boolean> {

View File

@ -73,19 +73,22 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useServiceStore } from './stores/serviceStore'
const store = useServiceStore()
const isDark = ref(true)
const startingAll = ref(false)
const stoppingAll = ref(false)
//
const serviceStatus = reactive({
nginx: false,
mysql: false,
redis: false
})
// store
const serviceStatus = computed(() => ({
nginx: store.serviceStatus.nginx,
mysql: store.serviceStatus.mysql,
redis: store.serviceStatus.redis
}))
const menuItems = [
{ path: '/', label: '仪表盘', icon: 'Odometer', service: null },
@ -99,20 +102,6 @@ const menuItems = [
{ path: '/settings', label: '设置', icon: 'Setting', service: null }
]
//
const loadServiceStatus = async () => {
try {
const services = await window.electronAPI?.service.getAll()
if (services) {
serviceStatus.nginx = services.some(s => s.name === 'nginx' && s.running)
serviceStatus.mysql = services.some(s => s.name.startsWith('mysql') && s.running)
serviceStatus.redis = services.some(s => s.name === 'redis' && s.running)
}
} catch (error) {
console.error('加载服务状态失败:', error)
}
}
let statusInterval: ReturnType<typeof setInterval> | null = null
//
@ -134,7 +123,7 @@ const startAll = async () => {
if (result?.success) {
ElMessage.success(result.message)
//
setTimeout(loadServiceStatus, 2000)
setTimeout(() => store.refreshServiceStatus(), 2000)
} else {
ElMessage.error(result?.message || '启动失败')
}
@ -152,7 +141,7 @@ const stopAll = async () => {
const result = await window.electronAPI?.service.stopAll()
if (result?.success) {
ElMessage.success(result.message)
await loadServiceStatus()
await store.refreshServiceStatus()
} else {
ElMessage.error(result?.message || '停止失败')
}
@ -165,9 +154,10 @@ const stopAll = async () => {
onMounted(() => {
document.documentElement.classList.add('dark')
loadServiceStatus()
//
store.refreshAll()
// 5
statusInterval = setInterval(loadServiceStatus, 5000)
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000)
})
onUnmounted(() => {

241
src/stores/serviceStore.ts Normal file
View File

@ -0,0 +1,241 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// PHP-CGI 服务状态
interface PhpCgiStatus {
version: string
port: number
running: boolean
}
// 服务状态
interface ServiceStatus {
nginx: boolean
mysql: boolean
redis: boolean
phpCgi: PhpCgiStatus[]
}
// PHP 版本信息
interface PhpVersion {
version: string
path: string
isActive: boolean
}
// Node.js 版本信息
interface NodeVersion {
version: string
path: string
isActive: boolean
}
// 站点信息
interface SiteConfig {
name: string
domain: string
rootPath: string
phpVersion: string
isLaravel: boolean
ssl: boolean
enabled: boolean
}
export const useServiceStore = defineStore('service', () => {
// 服务运行状态
const serviceStatus = ref<ServiceStatus>({
nginx: false,
mysql: false,
redis: false,
phpCgi: []
})
// PHP 版本列表
const phpVersions = ref<PhpVersion[]>([])
// Node.js 版本列表
const nodeVersions = ref<NodeVersion[]>([])
// 站点列表
const sites = ref<SiteConfig[]>([])
// 基础路径
const basePath = ref('')
// 加载状态
const loading = ref(false)
// 最后更新时间
const lastUpdated = ref<Date | null>(null)
// 计算属性:所有 PHP-CGI 是否都在运行
const allPhpCgiRunning = computed(() => {
if (serviceStatus.value.phpCgi.length === 0) return false
return serviceStatus.value.phpCgi.every(p => p.running)
})
// 计算属性:是否有任何 PHP-CGI 在运行
const anyPhpCgiRunning = computed(() => {
return serviceStatus.value.phpCgi.some(p => p.running)
})
// 计算属性:运行中的服务数量
const runningServiceCount = computed(() => {
let count = 0
if (serviceStatus.value.nginx) count++
if (serviceStatus.value.mysql) count++
if (serviceStatus.value.redis) count++
count += serviceStatus.value.phpCgi.filter(p => p.running).length
return count
})
// 计算属性:当前活动的 PHP 版本
const activePhpVersion = computed(() => {
return phpVersions.value.find(v => v.isActive)
})
// 计算属性:当前活动的 Node.js 版本
const activeNodeVersion = computed(() => {
return nodeVersions.value.find(v => v.isActive)
})
// 刷新所有状态
async function refreshAll() {
loading.value = true
try {
await Promise.all([
refreshServiceStatus(),
refreshPhpVersions(),
refreshNodeVersions(),
refreshSites(),
refreshBasePath()
])
lastUpdated.value = new Date()
} catch (error) {
console.error('刷新状态失败:', error)
} finally {
loading.value = false
}
}
// 刷新服务状态
async function refreshServiceStatus() {
try {
// 获取基础服务状态
const allServices = await window.electronAPI?.service.getAll()
if (allServices) {
// 重置 MySQL 状态为 false然后检测是否有任一版本在运行
serviceStatus.value.mysql = false
for (const svc of allServices) {
if (svc.name === 'nginx') {
serviceStatus.value.nginx = svc.running
} else if (svc.name.startsWith('mysql-')) {
// MySQL 服务名称格式为 mysql-{version},只要有一个版本在运行就设为 true
if (svc.running) {
serviceStatus.value.mysql = true
}
} else if (svc.name === 'redis') {
serviceStatus.value.redis = svc.running
}
}
}
// 获取 PHP-CGI 状态
const phpCgiStatus = await window.electronAPI?.service.getPhpCgiStatus()
if (phpCgiStatus) {
serviceStatus.value.phpCgi = phpCgiStatus
}
} catch (error) {
console.error('刷新服务状态失败:', error)
}
}
// 刷新 PHP 版本列表
async function refreshPhpVersions() {
try {
const versions = await window.electronAPI?.php.getVersions()
if (versions) {
phpVersions.value = versions
}
} catch (error) {
console.error('刷新 PHP 版本失败:', error)
}
}
// 刷新 Node.js 版本列表
async function refreshNodeVersions() {
try {
const versions = await window.electronAPI?.node.getVersions()
if (versions) {
nodeVersions.value = versions
}
} catch (error) {
console.error('刷新 Node.js 版本失败:', error)
}
}
// 刷新站点列表
async function refreshSites() {
try {
const siteList = await window.electronAPI?.nginx.getSites()
if (siteList) {
sites.value = siteList
}
} catch (error) {
console.error('刷新站点列表失败:', error)
}
}
// 刷新基础路径
async function refreshBasePath() {
try {
const path = await window.electronAPI?.config.getBasePath()
if (path) {
basePath.value = path
}
} catch (error) {
console.error('刷新基础路径失败:', error)
}
}
// 更新单个服务状态
function updateServiceStatus(service: 'nginx' | 'mysql' | 'redis', running: boolean) {
serviceStatus.value[service] = running
}
// 更新 PHP-CGI 状态
function updatePhpCgiStatus(version: string, running: boolean) {
const index = serviceStatus.value.phpCgi.findIndex(p => p.version === version)
if (index !== -1) {
serviceStatus.value.phpCgi[index].running = running
}
}
return {
// 状态
serviceStatus,
phpVersions,
nodeVersions,
sites,
basePath,
loading,
lastUpdated,
// 计算属性
allPhpCgiRunning,
anyPhpCgiRunning,
runningServiceCount,
activePhpVersion,
activeNodeVersion,
// 方法
refreshAll,
refreshServiceStatus,
refreshPhpVersions,
refreshNodeVersions,
refreshSites,
refreshBasePath,
updateServiceStatus,
updatePhpCgiStatus
}
})

View File

@ -62,6 +62,86 @@
</div>
</div>
<!-- PHP-CGI 服务状态卡片 -->
<div v-if="phpCgiServices.length > 0" class="php-cgi-section">
<div class="section-header">
<h2 class="section-title">
<el-icon><Files /></el-icon>
PHP-CGI 进程
</h2>
<div class="section-actions">
<el-button type="success" size="small" @click="startAllPhpCgi">
<el-icon><VideoPlay /></el-icon>
全部启动
</el-button>
<el-button type="danger" size="small" @click="stopAllPhpCgi">
<el-icon><VideoPause /></el-icon>
全部停止
</el-button>
</div>
</div>
<div class="status-grid">
<div
v-for="service in phpCgiServices"
:key="service.name"
class="status-card php-cgi-card"
:class="{ running: service.running }"
>
<div class="status-header">
<div class="service-icon" :style="{ background: service.gradient }">
<el-icon><component :is="service.icon" /></el-icon>
</div>
<div class="service-info">
<h3 class="service-name">{{ service.displayName }}</h3>
<div class="port-info">端口: {{ service.port }}</div>
<span class="status-tag" :class="service.running ? 'running' : 'stopped'">
<span class="status-dot"></span>
{{ service.running ? '运行中' : '已停止' }}
</span>
</div>
</div>
<div class="status-actions">
<el-button
v-if="!service.running"
type="success"
size="small"
@click="startPhpCgi(service)"
:loading="service.loading"
>
<el-icon><VideoPlay /></el-icon>
启动
</el-button>
<el-button
v-else
type="danger"
size="small"
@click="stopPhpCgi(service)"
:loading="service.loading"
>
<el-icon><VideoPause /></el-icon>
停止
</el-button>
<el-button
size="small"
@click="restartPhpCgi(service)"
:loading="service.loading"
:disabled="!service.running"
>
<el-icon><RefreshRight /></el-icon>
重启
</el-button>
</div>
</div>
</div>
</div>
<div v-else class="php-cgi-empty">
<el-alert type="info" :closable="false">
<template #title>
暂未安装 PHP请先到 <router-link to="/php">PHP 管理</router-link> PHP
</template>
</el-alert>
</div>
<!-- 快捷信息 -->
<div class="info-grid">
<!-- PHP 版本 -->
@ -213,9 +293,12 @@
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Link, Promotion } from '@element-plus/icons-vue'
import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore()
interface Service {
name: string
@ -224,52 +307,54 @@ interface Service {
gradient: string
running: boolean
loading: boolean
version?: string // PHP-CGI
port?: number // PHP-CGI
}
const services = reactive<Service[]>([
{ name: 'nginx', displayName: 'Nginx', icon: 'Connection', gradient: 'linear-gradient(135deg, #009639 0%, #0ecc5a 100%)', running: false, loading: false },
{ name: 'mysql', displayName: 'MySQL', icon: 'Coin', gradient: 'linear-gradient(135deg, #00758f 0%, #00b4d8 100%)', running: false, loading: false },
{ name: 'redis', displayName: 'Redis', icon: 'Grid', gradient: 'linear-gradient(135deg, #dc382d 0%, #ff6b6b 100%)', running: false, loading: false }
])
//
const baseServiceConfigs = [
{ name: 'nginx', displayName: 'Nginx', icon: 'Connection', gradient: 'linear-gradient(135deg, #009639 0%, #0ecc5a 100%)' },
{ name: 'mysql', displayName: 'MySQL', icon: 'Coin', gradient: 'linear-gradient(135deg, #00758f 0%, #00b4d8 100%)' },
{ name: 'redis', displayName: 'Redis', icon: 'Grid', gradient: 'linear-gradient(135deg, #dc382d 0%, #ff6b6b 100%)' }
]
//
const serviceLoadingState = ref<Record<string, boolean>>({})
// store
const services = computed<Service[]>(() => {
return baseServiceConfigs.map(config => ({
...config,
running: store.serviceStatus[config.name as keyof typeof store.serviceStatus] as boolean,
loading: serviceLoadingState.value[config.name] || false
}))
})
// store PHP-CGI
const phpCgiServices = computed<Service[]>(() => {
return store.serviceStatus.phpCgi.map(status => ({
name: `php-cgi-${status.version}`,
displayName: `PHP ${status.version}`,
icon: 'Files',
gradient: 'linear-gradient(135deg, #777BB4 0%, #9b8ed4 100%)',
running: status.running,
loading: serviceLoadingState.value[`php-cgi-${status.version}`] || false,
version: status.version,
port: status.port
}))
})
// store
const phpVersions = computed(() => store.phpVersions)
const nodeVersions = computed(() => store.nodeVersions)
const sites = computed(() => store.sites)
const basePath = computed(() => store.basePath)
const phpVersions = ref<any[]>([])
const nodeVersions = ref<any[]>([])
const sites = ref<any[]>([])
const basePath = ref('')
const settingPhp = ref('')
const settingNode = ref('')
const loadData = async () => {
try {
//
const allServices = await window.electronAPI?.service.getAll()
if (allServices) {
for (const svc of allServices) {
const found = services.find(s => s.name === svc.name || svc.name.startsWith(s.name))
if (found) {
found.running = svc.running
}
}
}
// PHP
phpVersions.value = await window.electronAPI?.php.getVersions() || []
// Node.js
nodeVersions.value = await window.electronAPI?.node.getVersions() || []
//
sites.value = await window.electronAPI?.nginx.getSites() || []
//
basePath.value = await window.electronAPI?.config.getBasePath() || ''
} catch (error: any) {
console.error('加载数据失败:', error)
}
}
const startService = async (service: Service) => {
service.loading = true
serviceLoadingState.value[service.name] = true
try {
let result
if (service.name === 'nginx') {
@ -286,20 +371,42 @@ const startService = async (service: Service) => {
}
if (result?.success) {
service.running = true
store.updateServiceStatus(service.name as 'nginx' | 'mysql' | 'redis', true)
ElMessage.success(result.message)
} else {
ElMessage.error(result?.message || '启动失败')
}
//
await store.refreshServiceStatus()
} catch (error: any) {
ElMessage.error(error.message)
} finally {
service.loading = false
serviceLoadingState.value[service.name] = false
}
}
const startPhpCgi = async (service: Service) => {
const key = `php-cgi-${service.version}`
serviceLoadingState.value[key] = true
try {
const result = await window.electronAPI?.service.startPhpCgiVersion(service.version!)
if (result?.success) {
store.updatePhpCgiStatus(service.version!, true)
ElMessage.success(result.message)
} else {
ElMessage.error(result?.message || '启动失败')
}
//
await store.refreshServiceStatus()
} catch (error: any) {
ElMessage.error(error.message)
} finally {
serviceLoadingState.value[key] = false
}
}
const stopService = async (service: Service) => {
service.loading = true
serviceLoadingState.value[service.name] = true
try {
let result
if (service.name === 'nginx') {
@ -314,20 +421,42 @@ const stopService = async (service: Service) => {
}
if (result?.success) {
service.running = false
store.updateServiceStatus(service.name as 'nginx' | 'mysql' | 'redis', false)
ElMessage.success(result.message)
} else {
ElMessage.error(result?.message || '停止失败')
}
//
await store.refreshServiceStatus()
} catch (error: any) {
ElMessage.error(error.message)
} finally {
service.loading = false
serviceLoadingState.value[service.name] = false
}
}
const stopPhpCgi = async (service: Service) => {
const key = `php-cgi-${service.version}`
serviceLoadingState.value[key] = true
try {
const result = await window.electronAPI?.service.stopPhpCgiVersion(service.version!)
if (result?.success) {
store.updatePhpCgiStatus(service.version!, false)
ElMessage.success(result.message)
} else {
ElMessage.error(result?.message || '停止失败')
}
//
await store.refreshServiceStatus()
} catch (error: any) {
ElMessage.error(error.message)
} finally {
serviceLoadingState.value[key] = false
}
}
const restartService = async (service: Service) => {
service.loading = true
serviceLoadingState.value[service.name] = true
try {
let result
if (service.name === 'nginx') {
@ -346,10 +475,67 @@ const restartService = async (service: Service) => {
} else {
ElMessage.error(result?.message || '重启失败')
}
//
await store.refreshServiceStatus()
} catch (error: any) {
ElMessage.error(error.message)
} finally {
service.loading = false
serviceLoadingState.value[service.name] = false
}
}
const restartPhpCgi = async (service: Service) => {
const key = `php-cgi-${service.version}`
serviceLoadingState.value[key] = true
try {
// PHP-CGI
await window.electronAPI?.service.stopPhpCgiVersion(service.version!)
await new Promise(resolve => setTimeout(resolve, 500))
const result = await window.electronAPI?.service.startPhpCgiVersion(service.version!)
if (result?.success) {
store.updatePhpCgiStatus(service.version!, true)
ElMessage.success(result.message)
} else {
ElMessage.error(result?.message || '重启失败')
}
//
await store.refreshServiceStatus()
} catch (error: any) {
ElMessage.error(error.message)
} finally {
serviceLoadingState.value[key] = false
}
}
// PHP-CGI
const startAllPhpCgi = async () => {
try {
const result = await window.electronAPI?.service.startAllPhpCgi()
if (result?.success) {
ElMessage.success('全部 PHP-CGI 已启动')
//
await store.refreshServiceStatus()
} else {
ElMessage.error(result?.message || '启动失败')
}
} catch (error: any) {
ElMessage.error(error.message)
}
}
// PHP-CGI
const stopAllPhpCgi = async () => {
try {
const result = await window.electronAPI?.service.stopAllPhpCgi()
if (result?.success) {
ElMessage.success('全部 PHP-CGI 已停止')
//
await store.refreshServiceStatus()
} else {
ElMessage.error(result?.message || '停止失败')
}
} catch (error: any) {
ElMessage.error(error.message)
}
}
@ -365,8 +551,8 @@ const setActivePhp = async (version: string) => {
const result = await window.electronAPI?.php.setActive(version)
if (result?.success) {
ElMessage.success(result.message)
// PHP
phpVersions.value = await window.electronAPI?.php.getVersions() || []
// PHP
await store.refreshPhpVersions()
} else {
ElMessage.error(result?.message || '设置失败')
}
@ -383,8 +569,8 @@ const setActiveNode = async (version: string) => {
const result = await window.electronAPI?.node.setActive(version)
if (result?.success) {
ElMessage.success(result.message)
// Node.js
nodeVersions.value = await window.electronAPI?.node.getVersions() || []
// Node.js
await store.refreshNodeVersions()
} else {
ElMessage.error(result?.message || '设置失败')
}
@ -396,9 +582,10 @@ const setActiveNode = async (version: string) => {
}
onMounted(() => {
loadData()
// 10
setInterval(loadData, 10000)
// store
if (!store.lastUpdated) {
store.refreshAll()
}
})
</script>
@ -593,5 +780,58 @@ onMounted(() => {
color: var(--accent-color);
}
}
.no-php-hint {
font-size: 13px;
color: var(--text-muted);
font-style: italic;
}
.php-cgi-section {
margin-bottom: 24px;
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.section-actions {
display: flex;
gap: 8px;
}
}
.php-cgi-card {
.port-info {
font-size: 12px;
color: var(--text-muted);
font-family: 'Fira Code', monospace;
margin-bottom: 4px;
}
}
.php-cgi-empty {
margin-bottom: 24px;
a {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
</style>

View File

@ -220,6 +220,9 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore()
interface MysqlVersion {
version: string
@ -262,6 +265,8 @@ const currentVersion = ref('')
const loadVersions = async () => {
try {
installedVersions.value = await window.electronAPI?.mysql.getVersions() || []
//
store.refreshServiceStatus()
} catch (error: any) {
console.error('加载版本失败:', error)
}

View File

@ -172,6 +172,9 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore()
interface NginxStatus {
running: boolean
@ -208,6 +211,8 @@ const loadData = async () => {
currentVersion.value = versions[0].version
}
status.value = await window.electronAPI?.nginx.getStatus() || { running: false }
//
store.refreshServiceStatus()
} catch (error: any) {
console.error('加载数据失败:', error)
} finally {

View File

@ -81,6 +81,91 @@
</div>
</div>
<!-- Composer 管理 -->
<div class="card">
<div class="card-header">
<span class="card-title">
<el-icon><Box /></el-icon>
Composer 管理
</span>
</div>
<div class="card-content">
<div class="composer-status">
<div class="status-info">
<div class="status-icon" :class="{ installed: composerStatus.installed }">
<el-icon v-if="composerStatus.installed"><Check /></el-icon>
<el-icon v-else><Close /></el-icon>
</div>
<div class="status-details">
<div class="status-title">
{{ composerStatus.installed ? 'Composer 已安装' : 'Composer 未安装' }}
<el-tag v-if="composerStatus.version" type="success" size="small" class="ml-2">
v{{ composerStatus.version }}
</el-tag>
<el-tag v-else-if="composerStatus.installed" type="warning" size="small" class="ml-2">
版本未知请设置默认 PHP
</el-tag>
</div>
<div class="status-path" v-if="composerStatus.path">
{{ composerStatus.path }}
</div>
</div>
</div>
<div class="status-actions">
<el-button
v-if="!composerStatus.installed"
type="primary"
@click="installComposer"
:loading="installingComposer"
>
<el-icon><Download /></el-icon>
安装 Composer
</el-button>
<template v-else>
<el-button @click="showMirrorDialog = true">
<el-icon><Setting /></el-icon>
设置镜像
</el-button>
<el-button type="danger" @click="uninstallComposer" :loading="uninstallingComposer">
<el-icon><Delete /></el-icon>
卸载
</el-button>
</template>
</div>
</div>
<!-- 当前镜像显示 -->
<div v-if="composerStatus.installed" class="mirror-info">
<span class="mirror-label">当前镜像源</span>
<span class="mirror-value">{{ getMirrorDisplayName(composerStatus.mirror) }}</span>
</div>
</div>
</div>
<!-- Composer 镜像设置对话框 -->
<el-dialog
v-model="showMirrorDialog"
title="设置 Composer 镜像"
width="500px"
>
<el-form label-width="80px">
<el-form-item label="镜像源">
<el-select v-model="selectedMirror" placeholder="选择镜像源" style="width: 100%">
<el-option label="官方源(默认)" value="" />
<el-option label="阿里云镜像" value="https://mirrors.aliyun.com/composer/" />
<el-option label="腾讯云镜像" value="https://mirrors.cloud.tencent.com/composer/" />
<el-option label="华为云镜像" value="https://mirrors.huaweicloud.com/repository/php/" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showMirrorDialog = false">取消</el-button>
<el-button type="primary" @click="setMirror" :loading="settingMirror">
确定
</el-button>
</template>
</el-dialog>
<!-- 安装对话框 -->
<el-dialog
v-model="showInstallDialog"
@ -283,6 +368,9 @@
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { FolderOpened } from '@element-plus/icons-vue'
import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore()
interface PhpVersion {
version: string
@ -372,14 +460,111 @@ const showConfigDialog = ref(false)
const configContent = ref('')
const savingConfig = ref(false)
// Composer
const composerStatus = ref<{
installed: boolean
version?: string
path?: string
mirror?: string
}>({ installed: false })
const installingComposer = ref(false)
const uninstallingComposer = ref(false)
const showMirrorDialog = ref(false)
const selectedMirror = ref('')
const settingMirror = ref(false)
const loadVersions = async () => {
try {
installedVersions.value = await window.electronAPI?.php.getVersions() || []
//
store.refreshPhpVersions()
store.refreshServiceStatus()
} catch (error: any) {
console.error('加载版本失败:', error)
}
}
// Composer
const loadComposerStatus = async () => {
try {
composerStatus.value = await window.electronAPI?.composer?.getStatus() || { installed: false }
selectedMirror.value = composerStatus.value.mirror || ''
} catch (error: any) {
console.error('加载 Composer 状态失败:', error)
}
}
const installComposer = async () => {
installingComposer.value = true
try {
const result = await window.electronAPI?.composer?.install()
if (result?.success) {
ElMessage.success(result.message)
await loadComposerStatus()
} else {
ElMessage.error(result?.message || '安装失败')
}
} catch (error: any) {
ElMessage.error('安装失败: ' + error.message)
} finally {
installingComposer.value = false
}
}
const uninstallComposer = async () => {
try {
await ElMessageBox.confirm(
'确定要卸载 Composer 吗?',
'确认卸载',
{ type: 'warning' }
)
uninstallingComposer.value = true
const result = await window.electronAPI?.composer?.uninstall()
if (result?.success) {
ElMessage.success(result.message)
await loadComposerStatus()
} else {
ElMessage.error(result?.message || '卸载失败')
}
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error('卸载失败: ' + error.message)
}
} finally {
uninstallingComposer.value = false
}
}
const setMirror = async () => {
settingMirror.value = true
try {
const result = await window.electronAPI?.composer?.setMirror(selectedMirror.value)
if (result?.success) {
ElMessage.success(result.message)
showMirrorDialog.value = false
await loadComposerStatus()
} else {
ElMessage.error(result?.message || '设置失败')
}
} catch (error: any) {
ElMessage.error('设置失败: ' + error.message)
} finally {
settingMirror.value = false
}
}
const getMirrorDisplayName = (mirror?: string) => {
if (!mirror) return '官方源'
const mirrors: Record<string, string> = {
'https://mirrors.aliyun.com/composer/': '阿里云镜像',
'https://mirrors.cloud.tencent.com/composer/': '腾讯云镜像',
'https://mirrors.huaweicloud.com/repository/php/': '华为云镜像',
'https://packagist.phpcomposer.com': '中国全量镜像'
}
return mirrors[mirror] || mirror
}
const loadAvailableVersions = async () => {
try {
availableVersions.value = await window.electronAPI?.php.getAvailableVersions() || []
@ -448,6 +633,8 @@ const setActive = async (version: string) => {
if (result?.success) {
ElMessage.success(result.message)
await loadVersions()
// Composer PHP
await loadComposerStatus()
} else {
ElMessage.error(result?.message || '设置失败')
}
@ -618,6 +805,7 @@ const formatSize = (bytes: number) => {
onMounted(() => {
loadVersions()
loadAvailableVersions()
loadComposerStatus()
//
window.electronAPI?.onDownloadProgress((data: any) => {
@ -831,5 +1019,74 @@ onUnmounted(() => {
width: 100%;
height: 500px;
}
// Composer
.composer-status {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
.status-info {
display: flex;
align-items: center;
gap: 16px;
}
.status-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: var(--bg-card);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: var(--text-muted);
&.installed {
background: rgba(103, 194, 58, 0.1);
color: var(--success-color);
}
}
.status-details {
.status-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
display: flex;
align-items: center;
}
.status-path {
font-size: 12px;
color: var(--text-muted);
font-family: 'Fira Code', monospace;
}
}
.status-actions {
display: flex;
gap: 8px;
}
}
.mirror-info {
padding: 12px 16px;
background: var(--bg-input);
border-radius: 8px;
margin-top: 12px;
.mirror-label {
color: var(--text-secondary);
margin-right: 8px;
}
.mirror-value {
color: var(--accent-color);
font-weight: 500;
}
}
</style>

View File

@ -178,6 +178,9 @@
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore()
interface RedisStatus {
running: boolean
@ -216,6 +219,8 @@ const loadData = async () => {
currentVersion.value = versions[0].version
}
status.value = await window.electronAPI?.redis.getStatus() || { running: false }
//
store.refreshServiceStatus()
} catch (error: any) {
console.error('加载数据失败:', error)
} finally {

View File

@ -15,10 +15,16 @@
<el-icon><Collection /></el-icon>
站点列表
</span>
<el-button type="primary" @click="showAddSiteDialog = true">
<el-icon><Plus /></el-icon>
添加站点
</el-button>
<div class="header-actions">
<el-button type="success" @click="showCreateLaravelDialog = true">
<el-icon><Promotion /></el-icon>
创建 Laravel 项目
</el-button>
<el-button type="primary" @click="showAddSiteDialog = true">
<el-icon><Plus /></el-icon>
添加站点
</el-button>
</div>
</div>
<div class="card-content">
<div v-if="loading" class="loading-state">
@ -56,7 +62,7 @@
</span>
<span class="meta-item">
<el-icon><Files /></el-icon>
PHP {{ site.phpVersion }}
PHP {{ site.phpVersion }} (端口 {{ getPhpCgiPort(site.phpVersion) }})
</span>
</div>
<div class="site-tags">
@ -133,10 +139,11 @@
<el-option
v-for="v in phpVersions"
:key="v.version"
:label="'PHP ' + v.version"
:label="`PHP ${v.version} (端口 ${getPhpCgiPort(v.version)})`"
:value="v.version"
/>
</el-select>
<span class="form-hint">每个 PHP 版本使用独立端口的 FastCGI 进程</span>
</el-form-item>
<el-form-item label="Laravel 项目">
<el-switch v-model="siteForm.isLaravel" />
@ -214,10 +221,11 @@
<el-option
v-for="v in phpVersions"
:key="v.version"
:label="'PHP ' + v.version"
:label="`PHP ${v.version} (端口 ${getPhpCgiPort(v.version)})`"
:value="v.version"
/>
</el-select>
<span class="form-hint">修改后需重新加载 Nginx 配置</span>
</el-form-item>
<el-form-item label="Laravel 项目">
<el-switch v-model="editForm.isLaravel" />
@ -235,6 +243,70 @@
</el-button>
</template>
</el-dialog>
<!-- 创建 Laravel 项目对话框 -->
<el-dialog
v-model="showCreateLaravelDialog"
title="创建 Laravel 项目"
width="600px"
>
<el-alert type="info" :closable="false" class="mb-4">
<template #title>
创建新的 Laravel 项目
</template>
将使用 Composer 创建 Laravel 项目并自动配置站点请确保已安装 Composer
</el-alert>
<el-form :model="laravelForm" label-width="100px">
<el-form-item label="项目名称" required>
<el-input
v-model="laravelForm.projectName"
placeholder="例如: my-app"
@input="autoGenerateDomain"
/>
<span class="form-hint">将作为目录名和站点名称</span>
</el-form-item>
<el-form-item label="项目目录" required>
<div class="directory-input">
<el-input v-model="laravelForm.targetDir" placeholder="点击右侧按钮选择目录" readonly />
<el-button type="primary" @click="selectLaravelDirectory" :icon="FolderOpened">
选择目录
</el-button>
</div>
<span class="form-hint">Laravel 项目将创建在此目录下</span>
</el-form-item>
<el-form-item label="域名" required>
<el-input v-model="laravelForm.domain" placeholder="例如: my-app.test" />
<span class="form-hint">本地开发域名建议使用 .test 后缀</span>
</el-form-item>
<el-form-item label="PHP 版本" required>
<el-select v-model="laravelForm.phpVersion" placeholder="选择 PHP 版本">
<el-option
v-for="v in phpVersions"
:key="v.version"
:label="`PHP ${v.version} (端口 ${getPhpCgiPort(v.version)})`"
:value="v.version"
/>
</el-select>
</el-form-item>
<el-form-item label="添加 Hosts">
<el-switch v-model="laravelForm.addToHosts" />
<span class="form-hint">自动将域名添加到系统 hosts 文件</span>
</el-form-item>
</el-form>
<!-- 创建进度 -->
<div v-if="creatingLaravel" class="creating-progress">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在创建 Laravel 项目这可能需要几分钟...</span>
</div>
<template #footer>
<el-button @click="showCreateLaravelDialog = false" :disabled="creatingLaravel">取消</el-button>
<el-button type="primary" @click="createLaravelProject" :loading="creatingLaravel">
创建项目
</el-button>
</template>
</el-dialog>
</div>
</template>
@ -242,6 +314,9 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { FolderOpened } from '@element-plus/icons-vue'
import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore()
interface SiteConfig {
name: string
@ -260,6 +335,17 @@ const showAddSiteDialog = ref(false)
const adding = ref(false)
const addToHosts = ref(true)
// PHP FastCGI
const getPhpCgiPort = (version: string): number => {
const match = version.match(/^(\d+)\.(\d+)/)
if (match) {
const major = parseInt(match[1])
const minor = parseInt(match[2])
return 9000 + major * 10 + minor // 8.5 -> 9085, 8.4 -> 9084
}
return 9000
}
const siteForm = reactive<SiteConfig>({
name: '',
domain: '',
@ -291,15 +377,35 @@ const editForm = reactive<SiteConfig>({
enabled: true
})
// Laravel
const showCreateLaravelDialog = ref(false)
const creatingLaravel = ref(false)
const laravelForm = reactive({
projectName: '',
targetDir: '',
domain: '',
phpVersion: '',
addToHosts: true
})
const loadData = async () => {
try {
sites.value = await window.electronAPI?.nginx.getSites() || []
phpVersions.value = await window.electronAPI?.php.getVersions() || []
// PHP
if (phpVersions.value.length > 0 && !siteForm.phpVersion) {
siteForm.phpVersion = phpVersions.value[0].version
if (phpVersions.value.length > 0) {
if (!siteForm.phpVersion) {
siteForm.phpVersion = phpVersions.value[0].version
}
if (!laravelForm.phpVersion) {
laravelForm.phpVersion = phpVersions.value[0].version
}
}
//
store.refreshSites()
store.refreshPhpVersions()
} catch (error: any) {
console.error('加载数据失败:', error)
}
@ -553,6 +659,109 @@ const requestSSL = async () => {
}
}
// ==================== Laravel ====================
// Laravel
const selectLaravelDirectory = async () => {
try {
const path = await window.electronAPI?.selectDirectory()
if (path) {
laravelForm.targetDir = path
}
} catch (error: any) {
ElMessage.error('选择目录失败: ' + error.message)
}
}
//
const autoGenerateDomain = () => {
if (laravelForm.projectName) {
// 线
const safeName = laravelForm.projectName.toLowerCase().replace(/[\s_]+/g, '-')
laravelForm.domain = `${safeName}.test`
}
}
// Laravel
const createLaravelProject = async () => {
if (!laravelForm.projectName) {
ElMessage.warning('请输入项目名称')
return
}
if (!laravelForm.targetDir) {
ElMessage.warning('请选择项目目录')
return
}
if (!laravelForm.domain) {
ElMessage.warning('请输入域名')
return
}
if (!laravelForm.phpVersion) {
ElMessage.warning('请选择 PHP 版本')
return
}
creatingLaravel.value = true
try {
// 1. Laravel
const createResult = await window.electronAPI?.composer?.createLaravelProject(
laravelForm.projectName,
laravelForm.targetDir
)
if (!createResult?.success) {
ElMessage.error(createResult?.message || '创建项目失败')
return
}
// 2. Laravel public
const projectPath = createResult.projectPath
const publicPath = `${projectPath}\\public`
const siteData = {
name: laravelForm.projectName,
domain: laravelForm.domain,
rootPath: publicPath,
phpVersion: laravelForm.phpVersion,
isLaravel: true,
ssl: false,
enabled: true
}
const siteResult = await window.electronAPI?.nginx.addSite(siteData)
if (!siteResult?.success) {
ElMessage.warning(`项目创建成功,但站点配置失败: ${siteResult?.message}`)
}
// 3. hosts
if (laravelForm.addToHosts) {
await window.electronAPI?.hosts.add(laravelForm.domain, '127.0.0.1')
}
// 4. Nginx
await window.electronAPI?.nginx.reload()
ElMessage.success(`Laravel 项目 "${laravelForm.projectName}" 创建成功!`)
showCreateLaravelDialog.value = false
//
Object.assign(laravelForm, {
projectName: '',
targetDir: '',
domain: '',
phpVersion: phpVersions.value[0]?.version || '',
addToHosts: true
})
//
await loadData()
} catch (error: any) {
ElMessage.error('创建失败: ' + error.message)
} finally {
creatingLaravel.value = false
}
}
onMounted(() => {
loadData()
})
@ -693,5 +902,32 @@ onMounted(() => {
flex: 1;
}
}
.header-actions {
display: flex;
gap: 10px;
}
.creating-progress {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 20px;
margin-top: 16px;
background: var(--bg-input);
border-radius: 8px;
color: var(--text-secondary);
.is-loading {
font-size: 20px;
animation: spin 1s linear infinite;
color: var(--accent-color);
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>