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.
1
.gitignore
vendored
@ -35,3 +35,4 @@ Thumbs.db
|
|||||||
|
|
||||||
|
|
||||||
data/
|
data/
|
||||||
|
service/
|
||||||
130
README.md
@ -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
|
After Width: | Height: | Size: 91 KiB |
BIN
docs/mysql.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
docs/nginx.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/nodejs.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/php.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/python.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/redis.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/setting.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
@ -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,
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
@ -130,7 +138,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-icon v-if="selectedVersion === version.version" class="check-icon"><Check /></el-icon>
|
<el-icon v-if="selectedVersion === version.version" class="check-icon"><Check /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 下载进度条 -->
|
<!-- 下载进度条 -->
|
||||||
<div v-if="installing && downloadProgress.total > 0" class="download-progress">
|
<div v-if="installing && downloadProgress.total > 0" class="download-progress">
|
||||||
<div class="progress-info">
|
<div class="progress-info">
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
@ -125,16 +139,16 @@
|
|||||||
<span class="version-number">Nginx {{ version.version }}</span>
|
<span class="version-number">Nginx {{ version.version }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-icon v-if="selectedVersion === version.version" class="check-icon"><Check /></el-icon>
|
<el-icon v-if="selectedVersion === version.version" class="check-icon"><Check /></el-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- 下载进度条 -->
|
||||||
<!-- 下载进度条 -->
|
<div v-if="installing && downloadProgress.total > 0" class="download-progress">
|
||||||
<div v-if="installing && downloadProgress.total > 0" class="download-progress">
|
<div class="progress-info">
|
||||||
<div class="progress-info">
|
<span>下载中...</span>
|
||||||
<span>下载中...</span>
|
<span>{{ formatSize(downloadProgress.downloaded) }} / {{ formatSize(downloadProgress.total) }}</span>
|
||||||
<span>{{ formatSize(downloadProgress.downloaded) }} / {{ formatSize(downloadProgress.total) }}</span>
|
</div>
|
||||||
|
<el-progress :percentage="downloadProgress.progress" :stroke-width="10" />
|
||||||
</div>
|
</div>
|
||||||
<el-progress :percentage="downloadProgress.progress" :stroke-width="10" />
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showInstallDialog = false" :disabled="installing">取消</el-button>
|
<el-button @click="showInstallDialog = false" :disabled="installing">取消</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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" />
|
||||||
@ -113,7 +127,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showInstallDialog = false">关闭</el-button>
|
<el-button @click="showInstallDialog = false">关闭</el-button>
|
||||||
</template>
|
</template>
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
@ -131,16 +139,16 @@
|
|||||||
<span class="version-number">Redis {{ version.version }}</span>
|
<span class="version-number">Redis {{ version.version }}</span>
|
||||||
</div>
|
</div>
|
||||||
<el-icon v-if="selectedVersion === version.version" class="check-icon"><Check /></el-icon>
|
<el-icon v-if="selectedVersion === version.version" class="check-icon"><Check /></el-icon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<!-- 下载进度条 -->
|
||||||
<!-- 下载进度条 -->
|
<div v-if="installing && downloadProgress.total > 0" class="download-progress">
|
||||||
<div v-if="installing && downloadProgress.total > 0" class="download-progress">
|
<div class="progress-info">
|
||||||
<div class="progress-info">
|
<span>下载中...</span>
|
||||||
<span>下载中...</span>
|
<span>{{ formatSize(downloadProgress.downloaded) }} / {{ formatSize(downloadProgress.total) }}</span>
|
||||||
<span>{{ formatSize(downloadProgress.downloaded) }} / {{ formatSize(downloadProgress.total) }}</span>
|
</div>
|
||||||
|
<el-progress :percentage="downloadProgress.progress" :stroke-width="10" />
|
||||||
</div>
|
</div>
|
||||||
<el-progress :percentage="downloadProgress.progress" :stroke-width="10" />
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showInstallDialog = false" :disabled="installing">取消</el-button>
|
<el-button @click="showInstallDialog = false" :disabled="installing">取消</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||