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
|
# Electron
|
||||||
*.asar
|
*.asar
|
||||||
|
|
||||||
|
|
||||||
|
data/
|
||||||
@ -362,6 +362,19 @@ ipcMain.handle("php:saveConfig", (_, version: string, config: string) =>
|
|||||||
phpManager.saveConfig(version, config)
|
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 管理 ====================
|
// ==================== MySQL 管理 ====================
|
||||||
ipcMain.handle("mysql:getVersions", () => mysqlManager.getInstalledVersions());
|
ipcMain.handle("mysql:getVersions", () => mysqlManager.getInstalledVersions());
|
||||||
ipcMain.handle("mysql:getAvailableVersions", () =>
|
ipcMain.handle("mysql:getAvailableVersions", () =>
|
||||||
@ -489,6 +502,15 @@ ipcMain.handle("service:getAutoStart", (_, service: string) =>
|
|||||||
);
|
);
|
||||||
ipcMain.handle("service:startAll", () => serviceManager.startAll());
|
ipcMain.handle("service:startAll", () => serviceManager.startAll());
|
||||||
ipcMain.handle("service:stopAll", () => serviceManager.stopAll());
|
ipcMain.handle("service:stopAll", () => serviceManager.stopAll());
|
||||||
|
// PHP-CGI 管理 - 支持多版本
|
||||||
|
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 管理 ====================
|
// ==================== Hosts 管理 ====================
|
||||||
ipcMain.handle("hosts:get", () => hostsManager.getHosts());
|
ipcMain.handle("hosts:get", () => hostsManager.getHosts());
|
||||||
|
|||||||
@ -39,6 +39,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
saveConfig: (version: string, config: string) => ipcRenderer.invoke('php:saveConfig', version, config)
|
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 管理
|
||||||
mysql: {
|
mysql: {
|
||||||
getVersions: () => ipcRenderer.invoke('mysql:getVersions'),
|
getVersions: () => ipcRenderer.invoke('mysql:getVersions'),
|
||||||
@ -107,7 +116,16 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
setAutoStart: (service: string, enabled: boolean) => ipcRenderer.invoke('service:setAutoStart', service, enabled),
|
setAutoStart: (service: string, enabled: boolean) => ipcRenderer.invoke('service:setAutoStart', service, enabled),
|
||||||
getAutoStart: (service: string) => ipcRenderer.invoke('service:getAutoStart', service),
|
getAutoStart: (service: string) => ipcRenderer.invoke('service:getAutoStart', service),
|
||||||
startAll: () => ipcRenderer.invoke('service:startAll'),
|
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 管理
|
// Hosts 管理
|
||||||
|
|||||||
@ -19,6 +19,8 @@ interface ConfigSchema {
|
|||||||
// 应用设置
|
// 应用设置
|
||||||
autoLaunch: boolean;
|
autoLaunch: boolean;
|
||||||
startMinimized: boolean;
|
startMinimized: boolean;
|
||||||
|
// Composer 设置
|
||||||
|
composerMirror: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SiteConfig {
|
export interface SiteConfig {
|
||||||
@ -66,6 +68,8 @@ export class ConfigStore {
|
|||||||
// 应用设置默认值
|
// 应用设置默认值
|
||||||
autoLaunch: false,
|
autoLaunch: false,
|
||||||
startMinimized: false,
|
startMinimized: false,
|
||||||
|
// Composer 镜像(空为官方源)
|
||||||
|
composerMirror: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -341,12 +341,16 @@ export class MysqlManager {
|
|||||||
} catch (pwdError: any) {
|
} catch (pwdError: any) {
|
||||||
console.log('设置默认密码失败,root密码为空:', pwdError.message)
|
console.log('设置默认密码失败,root密码为空:', pwdError.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 设置密码后停止 MySQL,让用户手动启动
|
||||||
|
console.log('停止 MySQL 服务...')
|
||||||
|
await this.stop(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`MySQL ${version} 安装完成`)
|
console.log(`MySQL ${version} 安装完成`)
|
||||||
return {
|
return {
|
||||||
success: true,
|
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) {
|
} catch (error: any) {
|
||||||
console.error('MySQL 安装失败:', error)
|
console.error('MySQL 安装失败:', error)
|
||||||
|
|||||||
@ -33,6 +33,20 @@ export class NginxManager {
|
|||||||
this.configStore = configStore
|
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 版本列表
|
* 获取已安装的 Nginx 版本列表
|
||||||
*/
|
*/
|
||||||
@ -612,8 +626,10 @@ http {
|
|||||||
private generateSiteConfig(site: SiteConfig): string {
|
private generateSiteConfig(site: SiteConfig): string {
|
||||||
const phpPath = this.configStore.getPhpPath(site.phpVersion)
|
const phpPath = this.configStore.getPhpPath(site.phpVersion)
|
||||||
const logsPath = this.configStore.getLogsPath()
|
const logsPath = this.configStore.getLogsPath()
|
||||||
|
const phpCgiPort = this.getPhpCgiPort(site.phpVersion)
|
||||||
|
|
||||||
let config = `
|
let config = `
|
||||||
|
# PHP Version: ${site.phpVersion} -> Port ${phpCgiPort}
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name ${site.domain};
|
server_name ${site.domain};
|
||||||
@ -628,7 +644,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location ~ \\.php$ {
|
location ~ \\.php$ {
|
||||||
fastcgi_pass 127.0.0.1:9000;
|
fastcgi_pass 127.0.0.1:${phpCgiPort};
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
@ -662,7 +678,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location ~ \\.php$ {
|
location ~ \\.php$ {
|
||||||
fastcgi_pass 127.0.0.1:9000;
|
fastcgi_pass 127.0.0.1:${phpCgiPort};
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
@ -682,8 +698,10 @@ server {
|
|||||||
const logsPath = this.configStore.getLogsPath()
|
const logsPath = this.configStore.getLogsPath()
|
||||||
// Laravel 项目 public 目录
|
// Laravel 项目 public 目录
|
||||||
const publicPath = join(site.rootPath, 'public').replace(/\\/g, '/')
|
const publicPath = join(site.rootPath, 'public').replace(/\\/g, '/')
|
||||||
|
const phpCgiPort = this.getPhpCgiPort(site.phpVersion)
|
||||||
|
|
||||||
let config = `
|
let config = `
|
||||||
|
# Laravel Project - PHP Version: ${site.phpVersion} -> Port ${phpCgiPort}
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name ${site.domain};
|
server_name ${site.domain};
|
||||||
@ -708,7 +726,7 @@ server {
|
|||||||
error_page 404 /index.php;
|
error_page 404 /index.php;
|
||||||
|
|
||||||
location ~ \\.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;
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
}
|
}
|
||||||
@ -752,7 +770,7 @@ server {
|
|||||||
error_page 404 /index.php;
|
error_page 404 /index.php;
|
||||||
|
|
||||||
location ~ \\.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;
|
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||||
include fastcgi_params;
|
include fastcgi_params;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2060,4 +2060,463 @@ if ($verifyPath -and $verifyPath.Contains($NewPhpPath)) {
|
|||||||
rmdirSync(dir);
|
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) {
|
if (details.length === 0) {
|
||||||
return { success: true, message: '没有需要自动启动的服务', details }
|
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) {
|
if (details.length === 0) {
|
||||||
return { success: true, message: '没有已安装的服务', details }
|
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 }> {
|
getPhpCgiPort(version: string): number {
|
||||||
try {
|
// 提取主版本号,如 "8.5.1" -> "8.5" -> 85
|
||||||
const activePhp = this.configStore.get('activePhpVersion')
|
const match = version.match(/^(\d+)\.(\d+)/)
|
||||||
if (!activePhp) {
|
if (match) {
|
||||||
return { success: false, message: '未设置活动的 PHP 版本' }
|
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')
|
const phpCgi = join(phpPath, 'php-cgi.exe')
|
||||||
|
|
||||||
if (!existsSync(phpCgi)) {
|
if (!existsSync(phpCgi)) {
|
||||||
return { success: false, message: 'php-cgi.exe 不存在' }
|
return { success: false, message: `PHP ${version} 的 php-cgi.exe 不存在` }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否已在运行
|
const port = this.getPhpCgiPort(version)
|
||||||
if (await this.checkProcess('php-cgi.exe')) {
|
|
||||||
return { success: true, message: 'PHP-CGI 已经在运行' }
|
// 检查端口是否已被占用
|
||||||
|
const isPortInUse = await this.checkPort(port)
|
||||||
|
if (isPortInUse) {
|
||||||
|
return { success: true, message: `PHP-CGI ${version} 已在端口 ${port} 运行` }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动 PHP-CGI
|
// 启动 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))
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
if (await this.checkProcess('php-cgi.exe')) {
|
const started = await this.checkPort(port)
|
||||||
return { success: true, message: 'PHP-CGI 启动成功' }
|
if (started) {
|
||||||
|
return { success: true, message: `PHP-CGI ${version} 启动成功 (端口 ${port})` }
|
||||||
} else {
|
} else {
|
||||||
return { success: false, message: 'PHP-CGI 启动失败' }
|
return { success: false, message: `PHP-CGI ${version} 启动失败` }
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { success: false, message: `启动失败: ${error.message}` }
|
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 {
|
try {
|
||||||
await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 })
|
const port = this.getPhpCgiPort(version)
|
||||||
return { success: true, message: 'PHP-CGI 已停止' }
|
// 查找并结束监听该端口的进程
|
||||||
} catch (error: any) {
|
try {
|
||||||
if (error.message.includes('not found')) {
|
const { stdout } = await execAsync(`netstat -ano | findstr ":${port}"`, { windowsHide: true })
|
||||||
return { success: true, message: 'PHP-CGI 未运行' }
|
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}` }
|
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> {
|
private async checkProcess(name: string): Promise<boolean> {
|
||||||
|
|||||||
40
src/App.vue
40
src/App.vue
@ -73,19 +73,22 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useServiceStore } from './stores/serviceStore'
|
||||||
|
|
||||||
|
const store = useServiceStore()
|
||||||
|
|
||||||
const isDark = ref(true)
|
const isDark = ref(true)
|
||||||
const startingAll = ref(false)
|
const startingAll = ref(false)
|
||||||
const stoppingAll = ref(false)
|
const stoppingAll = ref(false)
|
||||||
|
|
||||||
// 服务状态
|
// 从 store 获取服务状态
|
||||||
const serviceStatus = reactive({
|
const serviceStatus = computed(() => ({
|
||||||
nginx: false,
|
nginx: store.serviceStatus.nginx,
|
||||||
mysql: false,
|
mysql: store.serviceStatus.mysql,
|
||||||
redis: false
|
redis: store.serviceStatus.redis
|
||||||
})
|
}))
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ path: '/', label: '仪表盘', icon: 'Odometer', service: null },
|
{ path: '/', label: '仪表盘', icon: 'Odometer', service: null },
|
||||||
@ -99,20 +102,6 @@ const menuItems = [
|
|||||||
{ path: '/settings', label: '设置', icon: 'Setting', service: null }
|
{ 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
|
let statusInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
// 窗口控制
|
// 窗口控制
|
||||||
@ -134,7 +123,7 @@ const startAll = async () => {
|
|||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
ElMessage.success(result.message)
|
ElMessage.success(result.message)
|
||||||
// 延迟刷新状态,等待服务启动
|
// 延迟刷新状态,等待服务启动
|
||||||
setTimeout(loadServiceStatus, 2000)
|
setTimeout(() => store.refreshServiceStatus(), 2000)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || '启动失败')
|
ElMessage.error(result?.message || '启动失败')
|
||||||
}
|
}
|
||||||
@ -152,7 +141,7 @@ const stopAll = async () => {
|
|||||||
const result = await window.electronAPI?.service.stopAll()
|
const result = await window.electronAPI?.service.stopAll()
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
ElMessage.success(result.message)
|
ElMessage.success(result.message)
|
||||||
await loadServiceStatus()
|
await store.refreshServiceStatus()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || '停止失败')
|
ElMessage.error(result?.message || '停止失败')
|
||||||
}
|
}
|
||||||
@ -165,9 +154,10 @@ const stopAll = async () => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.documentElement.classList.add('dark')
|
document.documentElement.classList.add('dark')
|
||||||
loadServiceStatus()
|
// 初始化加载所有状态
|
||||||
|
store.refreshAll()
|
||||||
// 每 5 秒刷新一次状态
|
// 每 5 秒刷新一次状态
|
||||||
statusInterval = setInterval(loadServiceStatus, 5000)
|
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
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>
|
||||||
</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">
|
<div class="info-grid">
|
||||||
<!-- PHP 版本 -->
|
<!-- PHP 版本 -->
|
||||||
@ -213,9 +293,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, reactive } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Link, Promotion } from '@element-plus/icons-vue'
|
import { Link, Promotion } from '@element-plus/icons-vue'
|
||||||
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
|
||||||
|
const store = useServiceStore()
|
||||||
|
|
||||||
interface Service {
|
interface Service {
|
||||||
name: string
|
name: string
|
||||||
@ -224,52 +307,54 @@ interface Service {
|
|||||||
gradient: string
|
gradient: string
|
||||||
running: boolean
|
running: boolean
|
||||||
loading: 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 },
|
const baseServiceConfigs = [
|
||||||
{ name: 'mysql', displayName: 'MySQL', icon: 'Coin', gradient: 'linear-gradient(135deg, #00758f 0%, #00b4d8 100%)', running: false, loading: false },
|
{ name: 'nginx', displayName: 'Nginx', icon: 'Connection', gradient: 'linear-gradient(135deg, #009639 0%, #0ecc5a 100%)' },
|
||||||
{ name: 'redis', displayName: 'Redis', icon: 'Grid', gradient: 'linear-gradient(135deg, #dc382d 0%, #ff6b6b 100%)', running: false, loading: false }
|
{ 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 settingPhp = ref('')
|
||||||
const settingNode = 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) => {
|
const startService = async (service: Service) => {
|
||||||
service.loading = true
|
serviceLoadingState.value[service.name] = true
|
||||||
try {
|
try {
|
||||||
let result
|
let result
|
||||||
if (service.name === 'nginx') {
|
if (service.name === 'nginx') {
|
||||||
@ -286,20 +371,42 @@ const startService = async (service: Service) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
service.running = true
|
store.updateServiceStatus(service.name as 'nginx' | 'mysql' | 'redis', true)
|
||||||
ElMessage.success(result.message)
|
ElMessage.success(result.message)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || '启动失败')
|
ElMessage.error(result?.message || '启动失败')
|
||||||
}
|
}
|
||||||
|
// 同步刷新全局状态
|
||||||
|
await store.refreshServiceStatus()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error.message)
|
ElMessage.error(error.message)
|
||||||
} finally {
|
} 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) => {
|
const stopService = async (service: Service) => {
|
||||||
service.loading = true
|
serviceLoadingState.value[service.name] = true
|
||||||
try {
|
try {
|
||||||
let result
|
let result
|
||||||
if (service.name === 'nginx') {
|
if (service.name === 'nginx') {
|
||||||
@ -314,20 +421,42 @@ const stopService = async (service: Service) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
service.running = false
|
store.updateServiceStatus(service.name as 'nginx' | 'mysql' | 'redis', false)
|
||||||
ElMessage.success(result.message)
|
ElMessage.success(result.message)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || '停止失败')
|
ElMessage.error(result?.message || '停止失败')
|
||||||
}
|
}
|
||||||
|
// 同步刷新全局状态
|
||||||
|
await store.refreshServiceStatus()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error.message)
|
ElMessage.error(error.message)
|
||||||
} finally {
|
} 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) => {
|
const restartService = async (service: Service) => {
|
||||||
service.loading = true
|
serviceLoadingState.value[service.name] = true
|
||||||
try {
|
try {
|
||||||
let result
|
let result
|
||||||
if (service.name === 'nginx') {
|
if (service.name === 'nginx') {
|
||||||
@ -346,10 +475,67 @@ const restartService = async (service: Service) => {
|
|||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || '重启失败')
|
ElMessage.error(result?.message || '重启失败')
|
||||||
}
|
}
|
||||||
|
// 同步刷新全局状态
|
||||||
|
await store.refreshServiceStatus()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error.message)
|
ElMessage.error(error.message)
|
||||||
} finally {
|
} 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)
|
const result = await window.electronAPI?.php.setActive(version)
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
ElMessage.success(result.message)
|
ElMessage.success(result.message)
|
||||||
// 刷新 PHP 版本列表
|
// 刷新全局 PHP 版本列表
|
||||||
phpVersions.value = await window.electronAPI?.php.getVersions() || []
|
await store.refreshPhpVersions()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || '设置失败')
|
ElMessage.error(result?.message || '设置失败')
|
||||||
}
|
}
|
||||||
@ -383,8 +569,8 @@ const setActiveNode = async (version: string) => {
|
|||||||
const result = await window.electronAPI?.node.setActive(version)
|
const result = await window.electronAPI?.node.setActive(version)
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
ElMessage.success(result.message)
|
ElMessage.success(result.message)
|
||||||
// 刷新 Node.js 版本列表
|
// 刷新全局 Node.js 版本列表
|
||||||
nodeVersions.value = await window.electronAPI?.node.getVersions() || []
|
await store.refreshNodeVersions()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || '设置失败')
|
ElMessage.error(result?.message || '设置失败')
|
||||||
}
|
}
|
||||||
@ -396,9 +582,10 @@ const setActiveNode = async (version: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData()
|
// 如果 store 未初始化,则刷新
|
||||||
// 每 10 秒刷新一次状态
|
if (!store.lastUpdated) {
|
||||||
setInterval(loadData, 10000)
|
store.refreshAll()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -593,5 +780,58 @@ onMounted(() => {
|
|||||||
color: var(--accent-color);
|
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>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -220,6 +220,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
|
||||||
|
const store = useServiceStore()
|
||||||
|
|
||||||
interface MysqlVersion {
|
interface MysqlVersion {
|
||||||
version: string
|
version: string
|
||||||
@ -262,6 +265,8 @@ const currentVersion = ref('')
|
|||||||
const loadVersions = async () => {
|
const loadVersions = async () => {
|
||||||
try {
|
try {
|
||||||
installedVersions.value = await window.electronAPI?.mysql.getVersions() || []
|
installedVersions.value = await window.electronAPI?.mysql.getVersions() || []
|
||||||
|
// 同步更新全局状态
|
||||||
|
store.refreshServiceStatus()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('加载版本失败:', error)
|
console.error('加载版本失败:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -172,6 +172,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
|
||||||
|
const store = useServiceStore()
|
||||||
|
|
||||||
interface NginxStatus {
|
interface NginxStatus {
|
||||||
running: boolean
|
running: boolean
|
||||||
@ -208,6 +211,8 @@ const loadData = async () => {
|
|||||||
currentVersion.value = versions[0].version
|
currentVersion.value = versions[0].version
|
||||||
}
|
}
|
||||||
status.value = await window.electronAPI?.nginx.getStatus() || { running: false }
|
status.value = await window.electronAPI?.nginx.getStatus() || { running: false }
|
||||||
|
// 同步更新全局状态
|
||||||
|
store.refreshServiceStatus()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('加载数据失败:', error)
|
console.error('加载数据失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -81,6 +81,91 @@
|
|||||||
</div>
|
</div>
|
||||||
</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
|
<el-dialog
|
||||||
v-model="showInstallDialog"
|
v-model="showInstallDialog"
|
||||||
@ -283,6 +368,9 @@
|
|||||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { FolderOpened } from '@element-plus/icons-vue'
|
import { FolderOpened } from '@element-plus/icons-vue'
|
||||||
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
|
||||||
|
const store = useServiceStore()
|
||||||
|
|
||||||
interface PhpVersion {
|
interface PhpVersion {
|
||||||
version: string
|
version: string
|
||||||
@ -372,14 +460,111 @@ const showConfigDialog = ref(false)
|
|||||||
const configContent = ref('')
|
const configContent = ref('')
|
||||||
const savingConfig = ref(false)
|
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 () => {
|
const loadVersions = async () => {
|
||||||
try {
|
try {
|
||||||
installedVersions.value = await window.electronAPI?.php.getVersions() || []
|
installedVersions.value = await window.electronAPI?.php.getVersions() || []
|
||||||
|
// 同步更新全局状态
|
||||||
|
store.refreshPhpVersions()
|
||||||
|
store.refreshServiceStatus()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('加载版本失败:', error)
|
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 () => {
|
const loadAvailableVersions = async () => {
|
||||||
try {
|
try {
|
||||||
availableVersions.value = await window.electronAPI?.php.getAvailableVersions() || []
|
availableVersions.value = await window.electronAPI?.php.getAvailableVersions() || []
|
||||||
@ -448,6 +633,8 @@ const setActive = async (version: string) => {
|
|||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
ElMessage.success(result.message)
|
ElMessage.success(result.message)
|
||||||
await loadVersions()
|
await loadVersions()
|
||||||
|
// 刷新 Composer 状态(因为默认 PHP 改变了)
|
||||||
|
await loadComposerStatus()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || '设置失败')
|
ElMessage.error(result?.message || '设置失败')
|
||||||
}
|
}
|
||||||
@ -618,6 +805,7 @@ const formatSize = (bytes: number) => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadVersions()
|
loadVersions()
|
||||||
loadAvailableVersions()
|
loadAvailableVersions()
|
||||||
|
loadComposerStatus()
|
||||||
|
|
||||||
// 监听下载进度
|
// 监听下载进度
|
||||||
window.electronAPI?.onDownloadProgress((data: any) => {
|
window.electronAPI?.onDownloadProgress((data: any) => {
|
||||||
@ -831,5 +1019,74 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 500px;
|
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>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -178,6 +178,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
|
||||||
|
const store = useServiceStore()
|
||||||
|
|
||||||
interface RedisStatus {
|
interface RedisStatus {
|
||||||
running: boolean
|
running: boolean
|
||||||
@ -216,6 +219,8 @@ const loadData = async () => {
|
|||||||
currentVersion.value = versions[0].version
|
currentVersion.value = versions[0].version
|
||||||
}
|
}
|
||||||
status.value = await window.electronAPI?.redis.getStatus() || { running: false }
|
status.value = await window.electronAPI?.redis.getStatus() || { running: false }
|
||||||
|
// 同步更新全局状态
|
||||||
|
store.refreshServiceStatus()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('加载数据失败:', error)
|
console.error('加载数据失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -15,10 +15,16 @@
|
|||||||
<el-icon><Collection /></el-icon>
|
<el-icon><Collection /></el-icon>
|
||||||
站点列表
|
站点列表
|
||||||
</span>
|
</span>
|
||||||
<el-button type="primary" @click="showAddSiteDialog = true">
|
<div class="header-actions">
|
||||||
<el-icon><Plus /></el-icon>
|
<el-button type="success" @click="showCreateLaravelDialog = true">
|
||||||
添加站点
|
<el-icon><Promotion /></el-icon>
|
||||||
</el-button>
|
创建 Laravel 项目
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="showAddSiteDialog = true">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加站点
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div v-if="loading" class="loading-state">
|
<div v-if="loading" class="loading-state">
|
||||||
@ -56,7 +62,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="meta-item">
|
<span class="meta-item">
|
||||||
<el-icon><Files /></el-icon>
|
<el-icon><Files /></el-icon>
|
||||||
PHP {{ site.phpVersion }}
|
PHP {{ site.phpVersion }} (端口 {{ getPhpCgiPort(site.phpVersion) }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="site-tags">
|
<div class="site-tags">
|
||||||
@ -133,10 +139,11 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="v in phpVersions"
|
v-for="v in phpVersions"
|
||||||
:key="v.version"
|
:key="v.version"
|
||||||
:label="'PHP ' + v.version"
|
:label="`PHP ${v.version} (端口 ${getPhpCgiPort(v.version)})`"
|
||||||
:value="v.version"
|
:value="v.version"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<span class="form-hint">每个 PHP 版本使用独立端口的 FastCGI 进程</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Laravel 项目">
|
<el-form-item label="Laravel 项目">
|
||||||
<el-switch v-model="siteForm.isLaravel" />
|
<el-switch v-model="siteForm.isLaravel" />
|
||||||
@ -214,10 +221,11 @@
|
|||||||
<el-option
|
<el-option
|
||||||
v-for="v in phpVersions"
|
v-for="v in phpVersions"
|
||||||
:key="v.version"
|
:key="v.version"
|
||||||
:label="'PHP ' + v.version"
|
:label="`PHP ${v.version} (端口 ${getPhpCgiPort(v.version)})`"
|
||||||
:value="v.version"
|
:value="v.version"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
<span class="form-hint">修改后需重新加载 Nginx 配置</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Laravel 项目">
|
<el-form-item label="Laravel 项目">
|
||||||
<el-switch v-model="editForm.isLaravel" />
|
<el-switch v-model="editForm.isLaravel" />
|
||||||
@ -235,6 +243,70 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -242,6 +314,9 @@
|
|||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { FolderOpened } from '@element-plus/icons-vue'
|
import { FolderOpened } from '@element-plus/icons-vue'
|
||||||
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
|
||||||
|
const store = useServiceStore()
|
||||||
|
|
||||||
interface SiteConfig {
|
interface SiteConfig {
|
||||||
name: string
|
name: string
|
||||||
@ -260,6 +335,17 @@ const showAddSiteDialog = ref(false)
|
|||||||
const adding = ref(false)
|
const adding = ref(false)
|
||||||
const addToHosts = ref(true)
|
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>({
|
const siteForm = reactive<SiteConfig>({
|
||||||
name: '',
|
name: '',
|
||||||
domain: '',
|
domain: '',
|
||||||
@ -291,15 +377,35 @@ const editForm = reactive<SiteConfig>({
|
|||||||
enabled: true
|
enabled: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 创建 Laravel 项目
|
||||||
|
const showCreateLaravelDialog = ref(false)
|
||||||
|
const creatingLaravel = ref(false)
|
||||||
|
const laravelForm = reactive({
|
||||||
|
projectName: '',
|
||||||
|
targetDir: '',
|
||||||
|
domain: '',
|
||||||
|
phpVersion: '',
|
||||||
|
addToHosts: true
|
||||||
|
})
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
sites.value = await window.electronAPI?.nginx.getSites() || []
|
sites.value = await window.electronAPI?.nginx.getSites() || []
|
||||||
phpVersions.value = await window.electronAPI?.php.getVersions() || []
|
phpVersions.value = await window.electronAPI?.php.getVersions() || []
|
||||||
|
|
||||||
// 默认选择第一个 PHP 版本
|
// 默认选择第一个 PHP 版本
|
||||||
if (phpVersions.value.length > 0 && !siteForm.phpVersion) {
|
if (phpVersions.value.length > 0) {
|
||||||
siteForm.phpVersion = phpVersions.value[0].version
|
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) {
|
} catch (error: any) {
|
||||||
console.error('加载数据失败:', error)
|
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(() => {
|
onMounted(() => {
|
||||||
loadData()
|
loadData()
|
||||||
})
|
})
|
||||||
@ -693,5 +902,32 @@ onMounted(() => {
|
|||||||
flex: 1;
|
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>
|
</style>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user