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:
parent
894c792253
commit
a91146c4e9
2
.gitignore
vendored
2
.gitignore
vendored
@ -33,3 +33,5 @@ Thumbs.db
|
||||
# Electron
|
||||
*.asar
|
||||
|
||||
|
||||
data/
|
||||
@ -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());
|
||||
|
||||
@ -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 管理
|
||||
|
||||
@ -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: "",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
40
src/App.vue
40
src/App.vue
@ -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
241
src/stores/serviceStore.ts
Normal 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
|
||||
}
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user