Enhance application documentation and UI components for service management, adding support for Node.js and Python management features. Update README to reflect new services, improve version retrieval processes, and enhance user experience with loading indicators and download source information across various service managers.

This commit is contained in:
Ethanfly 2025-12-26 12:10:39 +08:00
parent baf26b3b5d
commit 1cea2b340f
20 changed files with 586 additions and 105 deletions

1
.gitignore vendored
View File

@ -35,3 +35,4 @@ Thumbs.db
data/ data/
service/

View File

@ -9,7 +9,7 @@
</p> </p>
<p align="center"> <p align="center">
轻松管理 PHP、MySQL、Nginx、Redis 等服务,告别繁琐的手动配置 轻松管理 PHP、MySQL、Nginx、Redis、Node.js、Python 等服务,告别繁琐的手动配置
</p> </p>
<p align="center"> <p align="center">
@ -26,11 +26,35 @@
<table> <table>
<tr> <tr>
<td><img src="docs/dashboard.png" alt="仪表盘" /></td> <td><img src="docs/dashboard.png" alt="仪表盘" /></td>
<td><img src="docs/php-manager.png" alt="PHP管理" /></td> <td><img src="docs/php.png" alt="PHP管理" /></td>
</tr> </tr>
<tr> <tr>
<td align="center">仪表盘</td> <td align="center">🏠 仪表盘</td>
<td align="center">PHP 版本管理</td> <td align="center">🐘 PHP 版本管理</td>
</tr>
<tr>
<td><img src="docs/mysql.png" alt="MySQL管理" /></td>
<td><img src="docs/nginx.png" alt="Nginx管理" /></td>
</tr>
<tr>
<td align="center">🐬 MySQL 管理</td>
<td align="center">🌐 Nginx 管理</td>
</tr>
<tr>
<td><img src="docs/redis.png" alt="Redis管理" /></td>
<td><img src="docs/nodejs.png" alt="Node.js管理" /></td>
</tr>
<tr>
<td align="center">🔴 Redis 管理</td>
<td align="center">💚 Node.js 管理</td>
</tr>
<tr>
<td><img src="docs/python.png" alt="Python管理" /></td>
<td><img src="docs/setting.png" alt="设置" /></td>
</tr>
<tr>
<td align="center">🐍 Python 管理</td>
<td align="center">⚙️ 设置</td>
</tr> </tr>
</table> </table>
@ -42,39 +66,72 @@
| ---------- | ---------------------------------------------------------- | | ---------- | ---------------------------------------------------------- |
| 多版本管理 | 支持同时安装 PHP 8.1、8.2、8.3、8.4、8.5 等多个版本 | | 多版本管理 | 支持同时安装 PHP 8.1、8.2、8.3、8.4、8.5 等多个版本 |
| 一键切换 | 点击即可切换 PHP 版本,自动配置系统环境变量 | | 一键切换 | 点击即可切换 PHP 版本,自动配置系统环境变量 |
| 扩展管理 | 可视化管理 PHP 扩展,一键启用/禁用 | | 扩展管理 | 可视化管理 PHP 扩展,支持在线安装(从 PECL |
| 配置编辑 | 在线编辑 php.ini无需手动查找配置文件 | | 配置编辑 | 在线编辑 php.ini无需手动查找配置文件 |
| 自动配置 | 安装时自动启用常用扩展curl、gd、mbstring、pdo_mysql 等) | | 自动配置 | 安装时自动启用常用扩展curl、gd、mbstring、pdo_mysql 等) |
| Composer | 集成 Composer 管理,支持镜像源切换(阿里云、腾讯云等) |
| 下载源 | 从 [windows.php.net](https://windows.php.net) 官方下载 |
### 🐬 MySQL 管理 ### 🐬 MySQL 管理
| 功能 | 说明 | | 功能 | 说明 |
| ---------- | -------------------------------- | | ---------- | -------------------------------------------------------------------- |
| 版本支持 | 支持 MySQL 5.7.x 和 8.0.x 系列 | | 版本支持 | 支持 MySQL 5.7.x 和 8.0.x 系列 |
| 服务控制 | 启动、停止、重启 MySQL 服务 | | 服务控制 | 启动、停止、重启 MySQL 服务 |
| 密码管理 | 一键修改 root 密码 | | 密码管理 | 一键修改 root 密码 |
| 配置编辑 | 在线编辑 my.ini 配置文件 | | 配置编辑 | 在线编辑 my.ini 配置文件 |
| 自动初始化 | 安装时自动初始化数据库,开箱即用 | | 自动初始化 | 安装时自动初始化数据库,开箱即用 |
| 下载源 | 从[阿里云镜像站](https://mirrors.aliyun.com/mysql/)下载,速度更快 |
### 🌐 Nginx 管理 ### 🌐 Nginx 管理
| 功能 | 说明 | | 功能 | 说明 |
| ------------ | ------------------------------------ | | ------------ | --------------------------------------------- |
| 版本管理 | 支持多个 Nginx 版本,可随时切换 | | 版本管理 | 支持多个 Nginx 版本,可随时切换 |
| 服务控制 | 启动、停止、重启、热重载配置 | | 服务控制 | 启动、停止、重启、热重载配置 |
| 站点管理 | 可视化添加、删除、启用、禁用虚拟主机 | | 站点管理 | 可视化添加、删除、启用、禁用虚拟主机 |
| Laravel 支持 | 自动生成 Laravel 项目的伪静态配置 | | Laravel 支持 | 自动生成 Laravel 项目的伪静态配置 |
| SSL 证书 | 支持申请 Let's Encrypt 免费 SSL 证书 | | SSL 证书 | 支持申请 Let's Encrypt 免费 SSL 证书 |
| 配置编辑 | 在线编辑 nginx.conf 主配置文件 | | 配置编辑 | 在线编辑 nginx.conf 主配置文件 |
| 下载源 | 从 [nginx.org](https://nginx.org) 官方下载 |
### 🔴 Redis 管理 ### 🔴 Redis 管理
| 功能 | 说明 | | 功能 | 说明 |
| ------------ | -------------------------------- | | ------------ | -------------------------------------------------------------------------- |
| Windows 版本 | 使用 Windows 原生编译版 Redis | | Windows 版本 | 使用 Windows 原生编译版 Redis |
| 服务控制 | 启动、停止、重启 Redis 服务 | | 服务控制 | 启动、停止、重启 Redis 服务 |
| 状态监控 | 实时查看运行状态、内存使用情况 | | 状态监控 | 实时查看运行状态、内存使用情况 |
| 配置编辑 | 在线编辑 redis.windows.conf 配置 | | 配置编辑 | 在线编辑 redis.windows.conf 配置 |
| 下载源 | 从 [GitHub (redis-windows)](https://github.com/redis-windows/redis-windows) 下载 |
### 💚 Node.js 管理
| 功能 | 说明 |
| ---------- | -------------------------------------------------------- |
| 多版本管理 | 支持同时安装多个 Node.js 版本 |
| LTS 支持 | 显示 LTS 版本和 Current 版本标识 |
| npm 集成 | 自动显示对应的 npm 版本 |
| 一键切换 | 快速切换默认 Node.js 版本,自动配置环境变量 |
| 下载源 | 从 [nodejs.org](https://nodejs.org) 官方下载 |
### 🐍 Python 管理
| 功能 | 说明 |
| ---------- | -------------------------------------------------------- |
| 嵌入式版本 | 使用免安装的嵌入式版本,不影响系统环境 |
| pip 集成 | 自动配置 pip支持安装 Python 包 |
| 多版本管理 | 支持同时安装多个 Python 版本 |
| 一键切换 | 快速切换默认 Python 版本 |
| 下载源 | 从 [python.org](https://www.python.org) 官方下载 |
### 🔧 Git 管理
| 功能 | 说明 |
| ---------- | ------------------------------ |
| 版本管理 | 一键安装/卸载 Git for Windows |
| 配置管理 | 可视化配置用户名、邮箱等信息 |
| 环境变量 | 自动配置系统 PATH |
### 🌍 站点管理 ### 🌍 站点管理
@ -89,6 +146,8 @@
- 📋 **Hosts 管理** - 可视化管理系统 hosts 文件 - 📋 **Hosts 管理** - 可视化管理系统 hosts 文件
- 🌙 **深色/浅色主题** - 支持主题切换 - 🌙 **深色/浅色主题** - 支持主题切换
- 📊 **服务状态监控** - 实时显示各服务运行状态 - 📊 **服务状态监控** - 实时显示各服务运行状态
- ⏳ **加载状态提示** - 版本列表加载时显示 Loading 状态
- 📥 **下载源说明** - 清晰显示各软件的下载来源
## 🛠️ 技术栈 ## 🛠️ 技术栈
@ -146,6 +205,9 @@ phper/
│ ├── MysqlManager.ts # MySQL 服务管理器 │ ├── MysqlManager.ts # MySQL 服务管理器
│ ├── NginxManager.ts # Nginx 服务管理器 │ ├── NginxManager.ts # Nginx 服务管理器
│ ├── RedisManager.ts # Redis 服务管理器 │ ├── RedisManager.ts # Redis 服务管理器
│ ├── NodeManager.ts # Node.js 版本管理器
│ ├── PythonManager.ts # Python 版本管理器
│ ├── GitManager.ts # Git 管理器
│ ├── ServiceManager.ts # 开机自启服务管理器 │ ├── ServiceManager.ts # 开机自启服务管理器
│ └── HostsManager.ts # Hosts 文件管理器 │ └── HostsManager.ts # Hosts 文件管理器
@ -163,6 +225,9 @@ phper/
│ ├── MysqlManager.vue # MySQL 管理 │ ├── MysqlManager.vue # MySQL 管理
│ ├── NginxManager.vue # Nginx 管理 │ ├── NginxManager.vue # Nginx 管理
│ ├── RedisManager.vue # Redis 管理 │ ├── RedisManager.vue # Redis 管理
│ ├── NodeManager.vue # Node.js 管理
│ ├── PythonManager.vue # Python 管理
│ ├── GitManager.vue # Git 管理
│ ├── SitesManager.vue # 站点管理 │ ├── SitesManager.vue # 站点管理
│ ├── HostsManager.vue # Hosts 管理 │ ├── HostsManager.vue # Hosts 管理
│ └── Settings.vue # 设置 │ └── Settings.vue # 设置
@ -246,10 +311,15 @@ A: 进入对应服务管理页面,先停止服务,然后点击"卸载"按钮
## 🔗 相关资源 ## 🔗 相关资源
- [PHP for Windows](https://windows.php.net/download/) - PHP Windows 官方下载 | 软件 | 下载源 |
- [MySQL Downloads](https://dev.mysql.com/downloads/) - MySQL 官方下载 | ------- | ---------------------------------------------------------------------- |
- [Nginx](https://nginx.org/) - Nginx 官方网站 | PHP | [windows.php.net](https://windows.php.net/download/) - 官方 Windows 版 |
- [Redis for Windows](https://github.com/redis-windows/redis-windows) - Windows 版 Redis | MySQL | [阿里云镜像](https://mirrors.aliyun.com/mysql/) - 国内高速下载 |
| Nginx | [nginx.org](https://nginx.org/en/download.html) - 官方 Windows 版 |
| Redis | [GitHub redis-windows](https://github.com/redis-windows/redis-windows) |
| Node.js | [nodejs.org](https://nodejs.org/en/download/) - 官方下载 |
| Python | [python.org](https://www.python.org/downloads/windows/) - 嵌入式版本 |
| Git | [git-scm.com](https://git-scm.com/download/win) - 官方 Windows 版 |
## 📄 开源协议 ## 📄 开源协议

BIN
docs/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
docs/mysql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
docs/nginx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
docs/nodejs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
docs/php.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
docs/python.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
docs/redis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
docs/setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@ -9,7 +9,9 @@ interface ConfigSchema {
mysqlVersions: string[]; mysqlVersions: string[];
nginxVersions: string[]; nginxVersions: string[];
redisVersions: string[]; redisVersions: string[];
nodeVersions: string[];
activePhpVersion: string; activePhpVersion: string;
activeNodeVersion: string;
autoStart: { autoStart: {
nginx: boolean; nginx: boolean;
mysql: boolean; mysql: boolean;
@ -63,7 +65,9 @@ export class ConfigStore {
mysqlVersions: [], mysqlVersions: [],
nginxVersions: [], nginxVersions: [],
redisVersions: [], redisVersions: [],
nodeVersions: [],
activePhpVersion: "", activePhpVersion: "",
activeNodeVersion: "",
autoStart: { autoStart: {
nginx: false, nginx: false,
mysql: false, mysql: false,

View File

@ -28,6 +28,8 @@ interface NginxStatus {
export class NginxManager { export class NginxManager {
private configStore: ConfigStore private configStore: ConfigStore
private versionCache: { versions: AvailableNginxVersion[]; timestamp: number } | null = null
private readonly CACHE_TTL = 5 * 60 * 1000 // 5分钟缓存
constructor(configStore: ConfigStore) { constructor(configStore: ConfigStore) {
this.configStore = configStore this.configStore = configStore
@ -88,9 +90,109 @@ export class NginxManager {
/** /**
* Nginx * Nginx
* nginx.org
*/ */
async getAvailableVersions(): Promise<AvailableNginxVersion[]> { async getAvailableVersions(): Promise<AvailableNginxVersion[]> {
const versions: AvailableNginxVersion[] = [ // 检查缓存
if (this.versionCache && (Date.now() - this.versionCache.timestamp) < this.CACHE_TTL) {
console.log('使用缓存的 Nginx 版本列表')
return this.versionCache.versions
}
let versions: AvailableNginxVersion[] = []
try {
console.log('从 nginx.org 获取版本列表...')
versions = await this.fetchNginxVersions()
console.log(`从 nginx.org 获取到 ${versions.length} 个 Nginx 版本`)
} catch (error: any) {
console.error('从 nginx.org 获取 Nginx 版本失败:', error.message)
}
// 如果获取失败或为空,使用备用列表
if (versions.length === 0) {
console.log('使用备用 Nginx 版本列表')
versions = this.getFallbackVersions()
}
// 更新缓存
this.versionCache = { versions, timestamp: Date.now() }
return versions
}
/**
* nginx.org
*/
private async fetchNginxVersions(): Promise<AvailableNginxVersion[]> {
return new Promise((resolve, reject) => {
const options = {
hostname: 'nginx.org',
path: '/download/',
method: 'GET',
headers: {
'User-Agent': 'PHPer-Dev-Manager'
},
timeout: 15000
}
const request = http.request(options, (response) => {
let data = ''
response.on('data', chunk => data += chunk)
response.on('end', () => {
try {
const versions: AvailableNginxVersion[] = []
// 解析 HTML 页面中的 Windows 版本链接
// 格式: nginx-1.27.3.zip
const regex = /href="\/download\/nginx-(\d+\.\d+\.\d+)\.zip"/g
let match
const seen = new Set<string>()
while ((match = regex.exec(data)) !== null) {
const version = match[1]
if (!seen.has(version)) {
seen.add(version)
versions.push({
version,
downloadUrl: `https://nginx.org/download/nginx-${version}.zip`
})
}
}
// 按版本号排序(降序)
versions.sort((a, b) => {
const aParts = a.version.split('.').map(Number)
const bParts = b.version.split('.').map(Number)
for (let i = 0; i < 3; i++) {
if (aParts[i] !== bParts[i]) {
return bParts[i] - aParts[i]
}
}
return 0
})
// 只返回前 10 个版本
resolve(versions.slice(0, 10))
} catch (e) {
reject(e)
}
})
})
request.on('error', reject)
request.on('timeout', () => {
request.destroy()
reject(new Error('请求超时'))
})
request.end()
})
}
/**
*
*/
private getFallbackVersions(): AvailableNginxVersion[] {
return [
{ {
version: '1.27.3', version: '1.27.3',
downloadUrl: 'https://nginx.org/download/nginx-1.27.3.zip' downloadUrl: 'https://nginx.org/download/nginx-1.27.3.zip'
@ -108,8 +210,6 @@ export class NginxManager {
downloadUrl: 'https://nginx.org/download/nginx-1.24.0.zip' downloadUrl: 'https://nginx.org/download/nginx-1.24.0.zip'
} }
] ]
return versions
} }
/** /**

View File

@ -24,6 +24,8 @@ interface AvailablePythonVersion {
export class PythonManager { export class PythonManager {
private configStore: ConfigStore private configStore: ConfigStore
private versionCache: { versions: AvailablePythonVersion[]; timestamp: number } | null = null
private readonly CACHE_TTL = 5 * 60 * 1000 // 5分钟缓存
constructor(configStore: ConfigStore) { constructor(configStore: ConfigStore) {
this.configStore = configStore this.configStore = configStore
@ -78,12 +80,131 @@ export class PythonManager {
/** /**
* Python * Python
* 使 Python * python.org Windows
*/ */
async getAvailableVersions(): Promise<AvailablePythonVersion[]> { async getAvailableVersions(): Promise<AvailablePythonVersion[]> {
// Python 嵌入式版本下载地址 // 检查缓存
// https://www.python.org/downloads/windows/ if (this.versionCache && (Date.now() - this.versionCache.timestamp) < this.CACHE_TTL) {
const versions: AvailablePythonVersion[] = [ console.log('使用缓存的 Python 版本列表')
// 过滤掉已安装的版本
const installed = await this.getInstalledVersions()
const installedVersions = installed.map(v => v.version)
return this.versionCache.versions.filter(v => !installedVersions.includes(v.version))
}
let versions: AvailablePythonVersion[] = []
try {
console.log('从 python.org 获取版本列表...')
versions = await this.fetchPythonVersions()
console.log(`从 python.org 获取到 ${versions.length} 个 Python 版本`)
} catch (error: any) {
console.error('从 python.org 获取 Python 版本失败:', error.message)
}
// 如果获取失败或为空,使用备用列表
if (versions.length === 0) {
console.log('使用备用 Python 版本列表')
versions = this.getFallbackVersions()
}
// 更新缓存
this.versionCache = { versions, timestamp: Date.now() }
// 过滤掉已安装的版本
const installed = await this.getInstalledVersions()
const installedVersions = installed.map(v => v.version)
return versions.filter(v => !installedVersions.includes(v.version))
}
/**
* python.org API
*/
private async fetchPythonVersions(): Promise<AvailablePythonVersion[]> {
return new Promise((resolve, reject) => {
const options = {
hostname: 'www.python.org',
path: '/api/v2/downloads/release/?is_published=true&pre_release=false&page_size=50',
method: 'GET',
headers: {
'User-Agent': 'PHPer-Dev-Manager',
'Accept': 'application/json'
},
timeout: 15000
}
const request = https.request(options, (response) => {
let data = ''
response.on('data', chunk => data += chunk)
response.on('end', () => {
try {
const json = JSON.parse(data)
const versions: AvailablePythonVersion[] = []
const seen = new Set<string>()
if (json.results && Array.isArray(json.results)) {
for (const release of json.results) {
// 解析版本号,如 "Python 3.13.1"
const match = release.name?.match(/Python (\d+\.\d+\.\d+)/)
if (match) {
const version = match[1]
// 只获取 Python 3.8+ 版本
const majorMinor = version.split('.').slice(0, 2).map(Number)
if (majorMinor[0] >= 3 && majorMinor[1] >= 8 && !seen.has(version)) {
seen.add(version)
versions.push({
version,
downloadUrl: `https://www.python.org/ftp/python/${version}/python-${version}-embed-amd64.zip`,
type: 'embed'
})
}
}
}
}
// 按版本号排序(降序)
versions.sort((a, b) => {
const aParts = a.version.split('.').map(Number)
const bParts = b.version.split('.').map(Number)
for (let i = 0; i < 3; i++) {
if (aParts[i] !== bParts[i]) {
return bParts[i] - aParts[i]
}
}
return 0
})
// 每个主版本只保留最新的一个
const latestByMajorMinor = new Map<string, AvailablePythonVersion>()
for (const v of versions) {
const majorMinor = v.version.split('.').slice(0, 2).join('.')
if (!latestByMajorMinor.has(majorMinor)) {
latestByMajorMinor.set(majorMinor, v)
}
}
resolve(Array.from(latestByMajorMinor.values()))
} catch (e) {
reject(e)
}
})
})
request.on('error', reject)
request.on('timeout', () => {
request.destroy()
reject(new Error('请求超时'))
})
request.end()
})
}
/**
*
*/
private getFallbackVersions(): AvailablePythonVersion[] {
return [
{ {
version: '3.13.1', version: '3.13.1',
downloadUrl: 'https://www.python.org/ftp/python/3.13.1/python-3.13.1-embed-amd64.zip', downloadUrl: 'https://www.python.org/ftp/python/3.13.1/python-3.13.1-embed-amd64.zip',
@ -110,12 +231,6 @@ export class PythonManager {
type: 'embed' type: 'embed'
} }
] ]
// 过滤掉已安装的版本
const installed = await this.getInstalledVersions()
const installedVersions = installed.map(v => v.version)
return versions.filter(v => !installedVersions.includes(v.version))
} }
/** /**

View File

@ -376,8 +376,6 @@ const startService = 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 {
@ -396,8 +394,6 @@ const startPhpCgi = 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 {
@ -426,8 +422,6 @@ const stopService = 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 {
@ -446,8 +440,6 @@ const stopPhpCgi = 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 {
@ -475,8 +467,6 @@ 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 {
@ -498,8 +488,6 @@ const restartPhpCgi = 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 {
@ -513,8 +501,8 @@ const startAllPhpCgi = async () => {
const result = await window.electronAPI?.service.startAllPhpCgi() const result = await window.electronAPI?.service.startAllPhpCgi()
if (result?.success) { if (result?.success) {
ElMessage.success('全部 PHP-CGI 已启动') ElMessage.success('全部 PHP-CGI 已启动')
// // PHP-CGI
await store.refreshServiceStatus() store.serviceStatus.phpCgi.forEach(p => p.running = true)
} else { } else {
ElMessage.error(result?.message || '启动失败') ElMessage.error(result?.message || '启动失败')
} }
@ -529,8 +517,8 @@ const stopAllPhpCgi = async () => {
const result = await window.electronAPI?.service.stopAllPhpCgi() const result = await window.electronAPI?.service.stopAllPhpCgi()
if (result?.success) { if (result?.success) {
ElMessage.success('全部 PHP-CGI 已停止') ElMessage.success('全部 PHP-CGI 已停止')
// // PHP-CGI
await store.refreshServiceStatus() store.serviceStatus.phpCgi.forEach(p => p.running = false)
} else { } else {
ElMessage.error(result?.message || '停止失败') ElMessage.error(result?.message || '停止失败')
} }
@ -581,11 +569,9 @@ const setActiveNode = async (version: string) => {
} }
} }
onMounted(() => { onMounted(async () => {
// store //
if (!store.lastUpdated) { await store.refreshAll()
store.refreshAll()
}
}) })
</script> </script>

View File

@ -113,11 +113,19 @@
> >
<el-alert type="info" :closable="false" class="mb-4"> <el-alert type="info" :closable="false" class="mb-4">
<template #title> <template #title>
安装说明 <el-icon><InfoFilled /></el-icon>
下载源说明
</template> </template>
MySQL 将从阿里云镜像站下载安装后自动设置 root 密码123456 MySQL 将从 <a href="https://mirrors.aliyun.com/mysql/" target="_blank">阿里云镜像站</a> 下载速度较快安装后 root 密码默认123456
</el-alert> </el-alert>
<div class="available-versions"> <div v-if="loadingAvailableVersions" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在获取可用版本列表...</span>
</div>
<div v-else-if="availableVersions.length === 0" class="empty-hint">
<span>暂无可用版本</span>
</div>
<div v-else class="available-versions">
<div <div
v-for="version in availableVersions" v-for="version in availableVersions"
:key="version.version" :key="version.version"
@ -220,6 +228,7 @@
<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 { InfoFilled } from '@element-plus/icons-vue'
import { useServiceStore } from '@/stores/serviceStore' import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore() const store = useServiceStore()
@ -272,11 +281,16 @@ const loadVersions = async () => {
} }
} }
const loadingAvailableVersions = ref(false)
const loadAvailableVersions = async () => { const loadAvailableVersions = async () => {
loadingAvailableVersions.value = true
try { try {
availableVersions.value = await window.electronAPI?.mysql.getAvailableVersions() || [] availableVersions.value = await window.electronAPI?.mysql.getAvailableVersions() || []
} catch (error: any) { } catch (error: any) {
ElMessage.error('加载可用版本失败: ' + error.message) ElMessage.error('加载可用版本失败: ' + error.message)
} finally {
loadingAvailableVersions.value = false
} }
} }
@ -556,5 +570,20 @@ onUnmounted(() => {
width: 100%; width: 100%;
height: 500px; height: 500px;
} }
.empty-hint {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.mb-4 a {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
</style> </style>

View File

@ -113,7 +113,21 @@
title="安装/切换 Nginx 版本" title="安装/切换 Nginx 版本"
width="600px" width="600px"
> >
<div class="available-versions"> <el-alert type="info" :closable="false" class="mb-4">
<template #title>
<el-icon><InfoFilled /></el-icon>
下载源说明
</template>
Nginx 将从官方网站 <a href="https://nginx.org/en/download.html" target="_blank">nginx.org</a> 下载 Windows 版本
</el-alert>
<div v-if="loadingAvailableVersions" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在获取可用版本列表...</span>
</div>
<div v-else-if="availableVersions.length === 0" class="empty-hint">
<span>暂无可用版本</span>
</div>
<div v-else class="available-versions">
<div <div
v-for="version in availableVersions" v-for="version in availableVersions"
:key="version.version" :key="version.version"
@ -172,6 +186,7 @@
<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 { InfoFilled } from '@element-plus/icons-vue'
import { useServiceStore } from '@/stores/serviceStore' import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore() const store = useServiceStore()
@ -219,11 +234,16 @@ const loadData = async () => {
} }
} }
const loadingAvailableVersions = ref(false)
const loadAvailableVersions = async () => { const loadAvailableVersions = async () => {
loadingAvailableVersions.value = true
try { try {
availableVersions.value = await window.electronAPI?.nginx.getAvailableVersions() || [] availableVersions.value = await window.electronAPI?.nginx.getAvailableVersions() || []
} catch (error: any) { } catch (error: any) {
console.error('加载可用版本失败:', error) console.error('加载可用版本失败:', error)
} finally {
loadingAvailableVersions.value = false
} }
} }
@ -518,5 +538,24 @@ onUnmounted(() => {
width: 100%; width: 100%;
height: 500px; height: 500px;
} }
.mb-4 {
margin-bottom: 16px;
a {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.empty-hint {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
</style> </style>

View File

@ -83,7 +83,21 @@
title="安装 Node.js" title="安装 Node.js"
width="700px" width="700px"
> >
<div class="available-versions"> <el-alert type="info" :closable="false" class="mb-4">
<template #title>
<el-icon><InfoFilled /></el-icon>
下载源说明
</template>
Node.js 将从官方网站 <a href="https://nodejs.org/en/download/" target="_blank">nodejs.org</a> 下载 Windows 64位版本
</el-alert>
<div v-if="loadingAvailableVersions" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在获取可用版本列表...</span>
</div>
<div v-else-if="availableVersions.length === 0" class="empty-hint">
<span>暂无可用版本</span>
</div>
<div v-else class="available-versions">
<el-table :data="availableVersions" style="width: 100%" max-height="400"> <el-table :data="availableVersions" style="width: 100%" max-height="400">
<el-table-column prop="version" label="版本" width="120" /> <el-table-column prop="version" label="版本" width="120" />
<el-table-column prop="date" label="发布日期" width="120" /> <el-table-column prop="date" label="发布日期" width="120" />
@ -124,7 +138,7 @@
<script setup lang="ts"> <script setup lang="ts">
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 { Plus, Promotion, Box } from '@element-plus/icons-vue' import { Plus, Promotion, Box, InfoFilled, Loading } from '@element-plus/icons-vue'
interface NodeVersion { interface NodeVersion {
version: string version: string
@ -162,11 +176,16 @@ const loadVersions = async () => {
} }
} }
const loadingAvailableVersions = ref(false)
const loadAvailableVersions = async () => { const loadAvailableVersions = async () => {
loadingAvailableVersions.value = true
try { try {
availableVersions.value = await window.electronAPI?.node.getAvailableVersions() || [] availableVersions.value = await window.electronAPI?.node.getAvailableVersions() || []
} catch (error: any) { } catch (error: any) {
console.error('加载可用版本失败:', error) console.error('加载可用版本失败:', error)
} finally {
loadingAvailableVersions.value = false
} }
} }
@ -393,5 +412,42 @@ onUnmounted(() => {
color: var(--text-secondary); color: var(--text-secondary);
} }
} }
.loading-state {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px;
color: var(--text-secondary);
.is-loading {
font-size: 24px;
animation: spin 1s linear infinite;
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.mb-4 {
margin-bottom: 16px;
a {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.empty-hint {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
</style> </style>

View File

@ -172,14 +172,19 @@
title="安装 PHP 版本" title="安装 PHP 版本"
width="600px" width="600px"
> >
<el-alert type="warning" :closable="false" class="mb-4"> <el-alert type="info" :closable="false" class="mb-4">
<template #title>安装说明</template> <template #title>
PHP 从官方网站 (windows.php.net) 下载国内网络可能较慢请耐心等待 <el-icon><InfoFilled /></el-icon>
下载进度可在控制台查看 (F12) 下载源说明
</template>
PHP 将从官方网站 <a href="https://windows.php.net" target="_blank">windows.php.net</a> 下载国内网络可能较慢请耐心等待
</el-alert> </el-alert>
<div v-if="availableVersions.length === 0" class="loading-state"> <div v-if="loadingAvailableVersions" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon> <el-icon class="is-loading"><Loading /></el-icon>
<span>加载可用版本...</span> <span>正在获取可用版本列表...</span>
</div>
<div v-else-if="availableVersions.length === 0" class="empty-hint">
<span>暂无可用版本</span>
</div> </div>
<div v-else class="available-versions"> <div v-else class="available-versions">
<div <div
@ -367,7 +372,7 @@
<script setup lang="ts"> <script setup lang="ts">
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, InfoFilled } from '@element-plus/icons-vue'
import { useServiceStore } from '@/stores/serviceStore' import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore() const store = useServiceStore()
@ -565,11 +570,16 @@ const getMirrorDisplayName = (mirror?: string) => {
return mirrors[mirror] || mirror return mirrors[mirror] || mirror
} }
const loadingAvailableVersions = ref(false)
const loadAvailableVersions = async () => { const loadAvailableVersions = async () => {
loadingAvailableVersions.value = true
try { try {
availableVersions.value = await window.electronAPI?.php.getAvailableVersions() || [] availableVersions.value = await window.electronAPI?.php.getAvailableVersions() || []
} catch (error: any) { } catch (error: any) {
ElMessage.error('加载可用版本失败: ' + error.message) ElMessage.error('加载可用版本失败: ' + error.message)
} finally {
loadingAvailableVersions.value = false
} }
} }
@ -1088,5 +1098,20 @@ onUnmounted(() => {
font-weight: 500; font-weight: 500;
} }
} }
.empty-hint {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.mb-4 a {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
</style> </style>

View File

@ -126,12 +126,18 @@
width="600px" width="600px"
> >
<el-alert type="info" :closable="false" class="mb-4"> <el-alert type="info" :closable="false" class="mb-4">
<template #title>安装说明</template> <template #title>
将下载 Python 嵌入式版本免安装自动配置 pip <el-icon><InfoFilled /></el-icon>
下载源说明
</template>
Python 将从官方网站 <a href="https://www.python.org/downloads/windows/" target="_blank">python.org</a> 下载嵌入式版本免安装并自动配置 pip
</el-alert> </el-alert>
<div v-if="availableVersions.length === 0" class="loading-state"> <div v-if="loadingAvailableVersions" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon> <el-icon class="is-loading"><Loading /></el-icon>
<span>加载可用版本...</span> <span>正在获取可用版本列表...</span>
</div>
<div v-else-if="availableVersions.length === 0" class="empty-hint">
<span>暂无可用版本</span>
</div> </div>
<div v-else class="available-versions"> <div v-else class="available-versions">
<div <div
@ -174,6 +180,7 @@
<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 { InfoFilled } from '@element-plus/icons-vue'
interface PythonVersion { interface PythonVersion {
version: string version: string
@ -231,7 +238,10 @@ const loadVersions = async () => {
} }
} }
const loadingAvailableVersions = ref(false)
const loadAvailableVersions = async () => { const loadAvailableVersions = async () => {
loadingAvailableVersions.value = true
try { try {
availableVersions.value = await window.electronAPI?.python?.getAvailableVersions() || [] availableVersions.value = await window.electronAPI?.python?.getAvailableVersions() || []
if (availableVersions.value.length > 0) { if (availableVersions.value.length > 0) {
@ -239,6 +249,8 @@ const loadAvailableVersions = async () => {
} }
} catch (error: any) { } catch (error: any) {
ElMessage.error('加载可用版本失败: ' + error.message) ElMessage.error('加载可用版本失败: ' + error.message)
} finally {
loadingAvailableVersions.value = false
} }
} }
@ -523,5 +535,20 @@ onUnmounted(() => {
color: var(--text-muted); color: var(--text-muted);
margin-top: 8px; margin-top: 8px;
} }
.empty-hint {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.mb-4 a {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
</style> </style>

View File

@ -115,11 +115,19 @@
> >
<el-alert type="info" :closable="false" class="mb-4"> <el-alert type="info" :closable="false" class="mb-4">
<template #title> <template #title>
Windows Redis <el-icon><InfoFilled /></el-icon>
下载源说明
</template> </template>
将从 GitHub 下载 Windows Redis下载速度可能较慢 Redis 将从 <a href="https://github.com/redis-windows/redis-windows" target="_blank">GitHub (redis-windows)</a> 下载 Windows 编译版国内网络可能较慢
</el-alert> </el-alert>
<div class="available-versions"> <div v-if="loadingAvailableVersions" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon>
<span>正在获取可用版本列表...</span>
</div>
<div v-else-if="availableVersions.length === 0" class="empty-hint">
<span>暂无可用版本</span>
</div>
<div v-else class="available-versions">
<div <div
v-for="version in availableVersions" v-for="version in availableVersions"
:key="version.version" :key="version.version"
@ -178,6 +186,7 @@
<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 { InfoFilled } from '@element-plus/icons-vue'
import { useServiceStore } from '@/stores/serviceStore' import { useServiceStore } from '@/stores/serviceStore'
const store = useServiceStore() const store = useServiceStore()
@ -227,11 +236,16 @@ const loadData = async () => {
} }
} }
const loadingAvailableVersions = ref(false)
const loadAvailableVersions = async () => { const loadAvailableVersions = async () => {
loadingAvailableVersions.value = true
try { try {
availableVersions.value = await window.electronAPI?.redis.getAvailableVersions() || [] availableVersions.value = await window.electronAPI?.redis.getAvailableVersions() || []
} catch (error: any) { } catch (error: any) {
console.error('加载可用版本失败:', error) console.error('加载可用版本失败:', error)
} finally {
loadingAvailableVersions.value = false
} }
} }
@ -530,5 +544,20 @@ onUnmounted(() => {
width: 100%; width: 100%;
height: 500px; height: 500px;
} }
.empty-hint {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.mb-4 a {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
</style> </style>