Compare commits
1 Commits
main
...
feature/go
| Author | SHA1 | Date | |
|---|---|---|---|
| 9614a3d234 |
647
.kiro/specs/go-version-management/design.md
Normal file
647
.kiro/specs/go-version-management/design.md
Normal file
@ -0,0 +1,647 @@
|
|||||||
|
# Design Document: Go Version Management
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
本设计文档描述了为 PHPer 开发环境管理器添加 Go 版本管理功能的技术实现方案。该功能将允许用户安装、管理和切换不同版本的 Go 语言开发环境,与现有的 Node.js 和 Python 版本管理功能保持一致的架构和用户体验。
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### 系统架构图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Frontend (Vue 3)"
|
||||||
|
A[GoManager.vue] --> B[Service Store]
|
||||||
|
A --> C[UI Components]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Electron Main Process"
|
||||||
|
D[IPC Handler] --> E[GoManager.ts]
|
||||||
|
E --> F[ConfigStore.ts]
|
||||||
|
E --> G[File System]
|
||||||
|
E --> H[Process Management]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "External Services"
|
||||||
|
I[golang.org API] --> E
|
||||||
|
J[Go Downloads] --> E
|
||||||
|
end
|
||||||
|
|
||||||
|
A -.->|IPC| D
|
||||||
|
E --> K[Environment Variables]
|
||||||
|
E --> L[Local Storage]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 核心架构原则
|
||||||
|
|
||||||
|
1. **一致性**: 与现有 NodeManager 和 PythonManager 保持相同的架构模式
|
||||||
|
2. **模块化**: 独立的 GoManager 服务类,便于维护和测试
|
||||||
|
3. **可扩展性**: 支持未来添加更多 Go 相关功能
|
||||||
|
4. **用户体验**: 统一的界面风格和交互模式
|
||||||
|
|
||||||
|
## Components and Interfaces
|
||||||
|
|
||||||
|
### 1. GoManager Service (Backend)
|
||||||
|
|
||||||
|
**文件位置**: `electron/services/GoManager.ts`
|
||||||
|
|
||||||
|
**主要职责**:
|
||||||
|
- 管理 Go 版本的安装、卸载和切换
|
||||||
|
- 处理环境变量配置
|
||||||
|
- 与 golang.org API 交互获取版本信息
|
||||||
|
- 文件系统操作和下载管理
|
||||||
|
|
||||||
|
**核心接口**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface GoVersion {
|
||||||
|
version: string // 版本号,如 "1.21.5"
|
||||||
|
path: string // 安装路径
|
||||||
|
isActive: boolean // 是否为当前活动版本
|
||||||
|
goroot: string // GOROOT 路径
|
||||||
|
gopath?: string // GOPATH 路径(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvailableGoVersion {
|
||||||
|
version: string // 版本号
|
||||||
|
stable: boolean // 是否为稳定版本
|
||||||
|
downloadUrl: string // Windows 64位下载链接
|
||||||
|
size: number // 文件大小(字节)
|
||||||
|
sha256: string // SHA256 校验和
|
||||||
|
}
|
||||||
|
|
||||||
|
class GoManager {
|
||||||
|
// 获取已安装版本
|
||||||
|
async getInstalledVersions(): Promise<GoVersion[]>
|
||||||
|
|
||||||
|
// 获取可用版本列表
|
||||||
|
async getAvailableVersions(): Promise<AvailableGoVersion[]>
|
||||||
|
|
||||||
|
// 安装指定版本
|
||||||
|
async install(version: string, downloadUrl: string): Promise<{success: boolean, message: string}>
|
||||||
|
|
||||||
|
// 卸载指定版本
|
||||||
|
async uninstall(version: string): Promise<{success: boolean, message: string}>
|
||||||
|
|
||||||
|
// 设置活动版本
|
||||||
|
async setActive(version: string): Promise<{success: boolean, message: string}>
|
||||||
|
|
||||||
|
// 验证 Go 安装
|
||||||
|
async validateInstallation(version: string): Promise<boolean>
|
||||||
|
|
||||||
|
// 获取 Go 环境信息
|
||||||
|
async getGoInfo(version: string): Promise<GoInfo>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. GoManager Vue Component (Frontend)
|
||||||
|
|
||||||
|
**文件位置**: `src/views/GoManager.vue`
|
||||||
|
|
||||||
|
**主要功能**:
|
||||||
|
- 显示已安装的 Go 版本列表
|
||||||
|
- 提供版本安装、卸载和切换操作
|
||||||
|
- 显示下载进度和操作状态
|
||||||
|
- 版本信息展示和管理
|
||||||
|
|
||||||
|
**组件结构**:
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Go 管理</h1>
|
||||||
|
<p>管理本地 Go 版本,支持多版本切换</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 下载进度条 -->
|
||||||
|
<div v-if="downloadProgress.percent > 0" class="download-progress">
|
||||||
|
<!-- 进度显示 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已安装版本卡片 -->
|
||||||
|
<div class="version-grid">
|
||||||
|
<div v-for="version in versions" class="version-card">
|
||||||
|
<!-- 版本信息和操作按钮 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 安装新版本对话框 -->
|
||||||
|
<el-dialog v-model="showInstallDialog">
|
||||||
|
<!-- 可用版本列表 -->
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. IPC Communication Layer
|
||||||
|
|
||||||
|
**主进程注册**:
|
||||||
|
```typescript
|
||||||
|
// electron/main.ts
|
||||||
|
ipcMain.handle('go:getVersions', () => goManager.getInstalledVersions())
|
||||||
|
ipcMain.handle('go:getAvailableVersions', () => goManager.getAvailableVersions())
|
||||||
|
ipcMain.handle('go:install', (_, version, url) => goManager.install(version, url))
|
||||||
|
ipcMain.handle('go:uninstall', (_, version) => goManager.uninstall(version))
|
||||||
|
ipcMain.handle('go:setActive', (_, version) => goManager.setActive(version))
|
||||||
|
```
|
||||||
|
|
||||||
|
**预加载脚本**:
|
||||||
|
```typescript
|
||||||
|
// electron/preload.ts
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
go: {
|
||||||
|
getVersions: () => ipcRenderer.invoke('go:getVersions'),
|
||||||
|
getAvailableVersions: () => ipcRenderer.invoke('go:getAvailableVersions'),
|
||||||
|
install: (version: string, url: string) => ipcRenderer.invoke('go:install', version, url),
|
||||||
|
uninstall: (version: string) => ipcRenderer.invoke('go:uninstall', version),
|
||||||
|
setActive: (version: string) => ipcRenderer.invoke('go:setActive', version)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Models
|
||||||
|
|
||||||
|
### 1. Go Version Data Model
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface GoVersion {
|
||||||
|
version: string // 版本号,格式如 "1.21.5"
|
||||||
|
path: string // 本地安装路径
|
||||||
|
isActive: boolean // 是否为当前活动版本
|
||||||
|
goroot: string // GOROOT 环境变量值
|
||||||
|
gopath?: string // GOPATH 环境变量值(可选)
|
||||||
|
installDate?: Date // 安装日期
|
||||||
|
size?: number // 安装大小(字节)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GoInfo {
|
||||||
|
version: string // Go 版本
|
||||||
|
goroot: string // GOROOT 路径
|
||||||
|
gopath: string // GOPATH 路径
|
||||||
|
goversion: string // go version 命令输出
|
||||||
|
goos: string // 目标操作系统
|
||||||
|
goarch: string // 目标架构
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Available Version Data Model
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AvailableGoVersion {
|
||||||
|
version: string // 版本号
|
||||||
|
stable: boolean // 是否为稳定版本
|
||||||
|
downloadUrl: string // Windows 64位 ZIP 下载链接
|
||||||
|
size: number // 文件大小(字节)
|
||||||
|
sha256: string // SHA256 校验和
|
||||||
|
releaseDate?: string // 发布日期
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GoRelease {
|
||||||
|
version: string
|
||||||
|
stable: boolean
|
||||||
|
files: GoFile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GoFile {
|
||||||
|
filename: string
|
||||||
|
os: string
|
||||||
|
arch: string
|
||||||
|
version: string
|
||||||
|
sha256: string
|
||||||
|
size: number
|
||||||
|
kind: 'archive' | 'installer' | 'source'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configuration Data Model
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface GoConfig {
|
||||||
|
activeVersion: string // 当前活动版本
|
||||||
|
installedVersions: string[] // 已安装版本列表
|
||||||
|
gopath: string // 全局 GOPATH 设置
|
||||||
|
downloadSource: 'official' // 下载源(目前仅支持官方)
|
||||||
|
autoSetGopath: boolean // 是否自动设置 GOPATH
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. Version Discovery and Download
|
||||||
|
|
||||||
|
**版本获取策略**:
|
||||||
|
1. **主要来源**: 使用 `https://golang.org/dl/?mode=json` API
|
||||||
|
2. **备用来源**: 硬编码的最新稳定版本列表
|
||||||
|
3. **缓存机制**: 5分钟本地缓存,减少 API 调用
|
||||||
|
|
||||||
|
**下载实现**:
|
||||||
|
```typescript
|
||||||
|
private async fetchGoVersions(): Promise<AvailableGoVersion[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://golang.org/dl/?mode=json')
|
||||||
|
const releases: GoRelease[] = await response.json()
|
||||||
|
|
||||||
|
return releases
|
||||||
|
.filter(release => release.stable)
|
||||||
|
.map(release => {
|
||||||
|
const windowsFile = release.files.find(f =>
|
||||||
|
f.os === 'windows' && f.arch === 'amd64' && f.kind === 'archive'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!windowsFile) return null
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: release.version,
|
||||||
|
stable: release.stable,
|
||||||
|
downloadUrl: `https://golang.org/dl/${windowsFile.filename}`,
|
||||||
|
size: windowsFile.size,
|
||||||
|
sha256: windowsFile.sha256
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 20) // 限制显示最新 20 个版本
|
||||||
|
} catch (error) {
|
||||||
|
return this.getFallbackVersions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Installation Process
|
||||||
|
|
||||||
|
**安装流程**:
|
||||||
|
1. 验证版本是否已安装
|
||||||
|
2. 创建临时下载目录
|
||||||
|
3. 下载 Go ZIP 文件(显示进度)
|
||||||
|
4. 验证 SHA256 校验和
|
||||||
|
5. 解压到目标目录
|
||||||
|
6. 验证安装完整性
|
||||||
|
7. 更新配置文件
|
||||||
|
8. 清理临时文件
|
||||||
|
|
||||||
|
**目录结构**:
|
||||||
|
```
|
||||||
|
[PHPer安装目录]/go/
|
||||||
|
├── go-1.21.5/ # Go 1.21.5 安装目录
|
||||||
|
│ ├── bin/
|
||||||
|
│ │ ├── go.exe
|
||||||
|
│ │ ├── gofmt.exe
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── src/
|
||||||
|
│ ├── pkg/
|
||||||
|
│ └── ...
|
||||||
|
├── go-1.20.12/ # Go 1.20.12 安装目录
|
||||||
|
└── workspace/ # 默认 GOPATH 工作空间
|
||||||
|
├── src/
|
||||||
|
├── pkg/
|
||||||
|
└── bin/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Environment Variable Management
|
||||||
|
|
||||||
|
**环境变量配置**:
|
||||||
|
```typescript
|
||||||
|
private async updateEnvironmentVariables(goVersion: string): Promise<void> {
|
||||||
|
const goRoot = this.getGoPath(goVersion)
|
||||||
|
const goBin = join(goRoot, 'bin')
|
||||||
|
const goPath = this.getDefaultGoPath()
|
||||||
|
|
||||||
|
// 使用 PowerShell 脚本更新用户环境变量
|
||||||
|
const psScript = `
|
||||||
|
# 设置 GOROOT
|
||||||
|
[Environment]::SetEnvironmentVariable('GOROOT', '${goRoot}', 'User')
|
||||||
|
|
||||||
|
# 设置 GOPATH
|
||||||
|
[Environment]::SetEnvironmentVariable('GOPATH', '${goPath}', 'User')
|
||||||
|
|
||||||
|
# 更新 PATH
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||||
|
$pathArray = $userPath -split ';' | Where-Object { $_ -ne '' }
|
||||||
|
|
||||||
|
# 移除旧的 Go 路径
|
||||||
|
$filteredPaths = $pathArray | Where-Object {
|
||||||
|
-not ($_ -like '*\\go\\go-*\\bin' -or $_ -like '*\\go-*\\bin')
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加新的 Go 路径
|
||||||
|
$newPathArray = @('${goBin}') + $filteredPaths
|
||||||
|
$finalPath = ($newPathArray | Select-Object -Unique) -join ';'
|
||||||
|
|
||||||
|
[Environment]::SetEnvironmentVariable('PATH', $finalPath, 'User')
|
||||||
|
`
|
||||||
|
|
||||||
|
await this.executePowerShellScript(psScript)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Installation Validation
|
||||||
|
|
||||||
|
**验证检查项**:
|
||||||
|
1. `go.exe` 文件存在且可执行
|
||||||
|
2. `go version` 命令返回正确版本
|
||||||
|
3. `go env` 命令正常工作
|
||||||
|
4. 标准库文件完整性检查
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private async validateInstallation(version: string): Promise<boolean> {
|
||||||
|
const goPath = this.getGoPath(version)
|
||||||
|
const goExe = join(goPath, 'bin', 'go.exe')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查可执行文件
|
||||||
|
if (!existsSync(goExe)) return false
|
||||||
|
|
||||||
|
// 验证版本
|
||||||
|
const { stdout } = await execAsync(`"${goExe}" version`)
|
||||||
|
if (!stdout.includes(version)) return false
|
||||||
|
|
||||||
|
// 验证环境
|
||||||
|
await execAsync(`"${goExe}" env GOROOT`)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Correctness Properties
|
||||||
|
|
||||||
|
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||||||
|
|
||||||
|
### Property Reflection
|
||||||
|
|
||||||
|
After analyzing all acceptance criteria, I identified several areas where properties can be consolidated to eliminate redundancy:
|
||||||
|
|
||||||
|
- **Installation validation properties** (5.1, 5.2, 5.3, 5.4) can be combined into a comprehensive installation validation property
|
||||||
|
- **Environment variable management properties** (6.1, 6.2, 6.3) can be consolidated into a single environment update property
|
||||||
|
- **Error handling properties** (8.1, 8.2, 8.3, 8.4, 8.5) share common patterns and can be grouped
|
||||||
|
- **Version list management properties** (2.4, 4.1, 4.2) can be combined into state consistency properties
|
||||||
|
|
||||||
|
### Core Properties
|
||||||
|
|
||||||
|
Property 1: **API Version Fetching Consistency**
|
||||||
|
*For any* valid API response from golang.org, the Go_Manager should correctly parse and return version information with all required fields (version, downloadUrl, size, sha256)
|
||||||
|
**Validates: Requirements 1.1**
|
||||||
|
|
||||||
|
Property 2: **Download URL Construction**
|
||||||
|
*For any* valid Go version, the Go_Manager should construct the correct Windows 64-bit download URL and initiate the download process
|
||||||
|
**Validates: Requirements 1.2**
|
||||||
|
|
||||||
|
Property 3: **Download Progress Reporting**
|
||||||
|
*For any* download operation, progress events should be emitted with valid data structures containing percentage (0-100), downloaded bytes, and total bytes
|
||||||
|
**Validates: Requirements 1.3**
|
||||||
|
|
||||||
|
Property 4: **Installation Validation Completeness**
|
||||||
|
*For any* successfully installed Go version, the validation should confirm that go.exe exists, go version returns the correct version, gofmt.exe exists, and go mod commands work
|
||||||
|
**Validates: Requirements 1.4, 5.1, 5.2, 5.3, 5.4**
|
||||||
|
|
||||||
|
Property 5: **Duplicate Installation Prevention**
|
||||||
|
*For any* Go version that is already installed, attempting to install it again should return an error message indicating the version is already installed
|
||||||
|
**Validates: Requirements 1.5**
|
||||||
|
|
||||||
|
Property 6: **File System Cleanup on Uninstall**
|
||||||
|
*For any* installed Go version, uninstalling it should completely remove the version directory and all its contents
|
||||||
|
**Validates: Requirements 2.2**
|
||||||
|
|
||||||
|
Property 7: **Active Version Environment Management**
|
||||||
|
*For any* Go version being set as active, the environment variables (PATH, GOROOT, GOPATH) should be updated correctly, and old Go paths should be removed from PATH
|
||||||
|
**Validates: Requirements 3.1, 6.1, 6.2, 6.3**
|
||||||
|
|
||||||
|
Property 8: **Version State Consistency**
|
||||||
|
*For any* operation that changes installed versions (install/uninstall), the installed versions list should be updated to reflect the current state accurately
|
||||||
|
**Validates: Requirements 2.4, 4.1**
|
||||||
|
|
||||||
|
Property 9: **Version Information Completeness**
|
||||||
|
*For any* installed Go version, the version information should include all required fields: version number, installation path, active status, GOROOT, and GOPATH
|
||||||
|
**Validates: Requirements 4.2, 4.3**
|
||||||
|
|
||||||
|
Property 10: **System Go Detection**
|
||||||
|
*For any* system with an existing Go installation, the Go_Manager should detect and display the system version information
|
||||||
|
**Validates: Requirements 4.5**
|
||||||
|
|
||||||
|
Property 11: **Environment Variable Validation**
|
||||||
|
*For any* Go version set as active, executing 'go version' in a new process should return the expected version string
|
||||||
|
**Validates: Requirements 3.2, 3.4**
|
||||||
|
|
||||||
|
Property 12: **Error Message Descriptiveness**
|
||||||
|
*For any* failed operation (network error, disk space, permissions, installation failure), the error message should be descriptive and include actionable suggestions
|
||||||
|
**Validates: Requirements 5.5, 6.5, 8.1, 8.2, 8.3, 8.5**
|
||||||
|
|
||||||
|
Property 13: **Success Message Completeness**
|
||||||
|
*For any* successful operation, the success message should include relevant details about what was accomplished
|
||||||
|
**Validates: Requirements 8.4**
|
||||||
|
|
||||||
|
Property 14: **GOPATH Default Configuration**
|
||||||
|
*For any* Go installation, when configuring GOPATH, it should be set to the user's home directory + '/go' workspace
|
||||||
|
**Validates: Requirements 6.4**
|
||||||
|
|
||||||
|
<function_calls>
|
||||||
|
<invoke name="prework">
|
||||||
|
<parameter name="featureName">go-version-management
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Error Categories and Handling Strategies
|
||||||
|
|
||||||
|
#### 1. Network Errors
|
||||||
|
- **Connection failures**: Retry mechanism with exponential backoff
|
||||||
|
- **API unavailable**: Fallback to cached version list
|
||||||
|
- **Download interruption**: Resume capability where possible
|
||||||
|
- **Timeout errors**: Configurable timeout with user feedback
|
||||||
|
|
||||||
|
#### 2. File System Errors
|
||||||
|
- **Insufficient disk space**: Pre-installation space check
|
||||||
|
- **Permission denied**: Clear guidance for administrator privileges
|
||||||
|
- **File corruption**: SHA256 verification and re-download
|
||||||
|
- **Path conflicts**: Automatic path resolution
|
||||||
|
|
||||||
|
#### 3. Installation Errors
|
||||||
|
- **Incomplete installation**: Automatic cleanup and retry option
|
||||||
|
- **Version conflicts**: Clear conflict resolution guidance
|
||||||
|
- **Environment variable failures**: Manual configuration instructions
|
||||||
|
- **Tool validation failures**: Detailed diagnostic information
|
||||||
|
|
||||||
|
#### 4. User Input Errors
|
||||||
|
- **Invalid version selection**: Input validation and user feedback
|
||||||
|
- **Concurrent operations**: Operation queuing and status indication
|
||||||
|
- **Configuration errors**: Validation with helpful error messages
|
||||||
|
|
||||||
|
### Error Recovery Mechanisms
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ErrorRecovery {
|
||||||
|
// 自动重试机制
|
||||||
|
autoRetry: {
|
||||||
|
maxAttempts: number
|
||||||
|
backoffStrategy: 'exponential' | 'linear'
|
||||||
|
retryableErrors: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户引导
|
||||||
|
userGuidance: {
|
||||||
|
errorCode: string
|
||||||
|
message: string
|
||||||
|
suggestedActions: string[]
|
||||||
|
documentationLink?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态恢复
|
||||||
|
stateRecovery: {
|
||||||
|
rollbackOnFailure: boolean
|
||||||
|
cleanupTempFiles: boolean
|
||||||
|
restoreEnvironment: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Dual Testing Approach
|
||||||
|
|
||||||
|
本项目采用单元测试和基于属性的测试相结合的方法,确保全面的代码覆盖和正确性验证。
|
||||||
|
|
||||||
|
#### Unit Testing
|
||||||
|
- **特定示例验证**: 测试具体的用例和边界条件
|
||||||
|
- **集成点测试**: 验证组件间的交互
|
||||||
|
- **错误条件测试**: 测试各种错误场景的处理
|
||||||
|
- **UI 交互测试**: 验证用户界面的响应和状态更新
|
||||||
|
|
||||||
|
#### Property-Based Testing
|
||||||
|
- **通用属性验证**: 验证在所有输入下都应该成立的属性
|
||||||
|
- **随机输入覆盖**: 通过随机化输入发现边界情况
|
||||||
|
- **状态一致性检查**: 验证系统状态的一致性
|
||||||
|
- **不变量验证**: 确保系统不变量在所有操作中保持
|
||||||
|
|
||||||
|
### Testing Framework Configuration
|
||||||
|
|
||||||
|
**Property-Based Testing Library**: 使用 `fast-check` (JavaScript/TypeScript 的属性测试库)
|
||||||
|
|
||||||
|
**测试配置要求**:
|
||||||
|
- 每个属性测试最少运行 100 次迭代
|
||||||
|
- 每个测试必须引用对应的设计文档属性
|
||||||
|
- 标签格式: `**Feature: go-version-management, Property {number}: {property_text}**`
|
||||||
|
|
||||||
|
**示例测试结构**:
|
||||||
|
```typescript
|
||||||
|
// 属性测试示例
|
||||||
|
describe('Go Version Management Properties', () => {
|
||||||
|
test('Property 1: API Version Fetching Consistency', async () => {
|
||||||
|
// **Feature: go-version-management, Property 1: API Version Fetching Consistency**
|
||||||
|
await fc.assert(fc.asyncProperty(
|
||||||
|
fc.array(fc.record({
|
||||||
|
version: fc.string(),
|
||||||
|
files: fc.array(fc.record({
|
||||||
|
os: fc.constantFrom('windows'),
|
||||||
|
arch: fc.constantFrom('amd64'),
|
||||||
|
kind: fc.constantFrom('archive'),
|
||||||
|
filename: fc.string(),
|
||||||
|
size: fc.nat(),
|
||||||
|
sha256: fc.hexaString({ minLength: 64, maxLength: 64 })
|
||||||
|
}))
|
||||||
|
})),
|
||||||
|
async (mockApiResponse) => {
|
||||||
|
const result = await goManager.parseApiResponse(mockApiResponse)
|
||||||
|
// 验证所有返回的版本都包含必需字段
|
||||||
|
result.forEach(version => {
|
||||||
|
expect(version).toHaveProperty('version')
|
||||||
|
expect(version).toHaveProperty('downloadUrl')
|
||||||
|
expect(version).toHaveProperty('size')
|
||||||
|
expect(version).toHaveProperty('sha256')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
), { numRuns: 100 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Coverage Requirements
|
||||||
|
|
||||||
|
- **代码覆盖率**: 最低 85% 的行覆盖率
|
||||||
|
- **分支覆盖率**: 最低 80% 的分支覆盖率
|
||||||
|
- **属性覆盖率**: 所有定义的正确性属性都必须有对应的测试
|
||||||
|
- **集成测试**: 覆盖主要的用户工作流程
|
||||||
|
|
||||||
|
### Continuous Integration
|
||||||
|
|
||||||
|
- **自动化测试**: 每次代码提交都运行完整测试套件
|
||||||
|
- **性能测试**: 监控关键操作的性能指标
|
||||||
|
- **兼容性测试**: 在不同 Windows 版本上验证功能
|
||||||
|
- **回归测试**: 确保新功能不破坏现有功能
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Core Backend Implementation
|
||||||
|
1. 创建 `GoManager.ts` 服务类
|
||||||
|
2. 实现版本获取和解析逻辑
|
||||||
|
3. 实现下载和安装功能
|
||||||
|
4. 添加环境变量管理
|
||||||
|
5. 实现基本的错误处理
|
||||||
|
|
||||||
|
### Phase 2: Frontend Integration
|
||||||
|
1. 创建 `GoManager.vue` 组件
|
||||||
|
2. 实现版本列表显示
|
||||||
|
3. 添加安装/卸载操作界面
|
||||||
|
4. 集成下载进度显示
|
||||||
|
5. 实现状态管理和用户反馈
|
||||||
|
|
||||||
|
### Phase 3: Advanced Features
|
||||||
|
1. 添加系统 Go 检测
|
||||||
|
2. 实现高级错误处理和恢复
|
||||||
|
3. 添加配置选项和偏好设置
|
||||||
|
4. 优化性能和用户体验
|
||||||
|
5. 完善文档和帮助信息
|
||||||
|
|
||||||
|
### Phase 4: Testing and Polish
|
||||||
|
1. 实现完整的测试套件
|
||||||
|
2. 进行性能优化
|
||||||
|
3. 用户体验改进
|
||||||
|
4. 文档完善
|
||||||
|
5. 发布准备
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Download Security
|
||||||
|
- **HTTPS 强制**: 所有下载必须使用 HTTPS
|
||||||
|
- **SHA256 验证**: 验证下载文件的完整性
|
||||||
|
- **签名验证**: 验证 Go 官方签名(如果可用)
|
||||||
|
- **沙箱下载**: 在临时目录中处理下载文件
|
||||||
|
|
||||||
|
### Environment Security
|
||||||
|
- **权限最小化**: 仅请求必要的系统权限
|
||||||
|
- **用户级配置**: 优先使用用户级环境变量
|
||||||
|
- **路径验证**: 验证所有文件路径的安全性
|
||||||
|
- **清理机制**: 及时清理临时文件和敏感数据
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
- **版本号验证**: 严格验证版本号格式
|
||||||
|
- **路径注入防护**: 防止路径遍历攻击
|
||||||
|
- **命令注入防护**: 安全地执行系统命令
|
||||||
|
- **配置验证**: 验证所有用户配置输入
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Optimization Strategies
|
||||||
|
- **缓存机制**: 缓存 API 响应和版本信息
|
||||||
|
- **懒加载**: 按需加载版本详细信息
|
||||||
|
- **并发控制**: 限制同时进行的下载数量
|
||||||
|
- **内存管理**: 及时释放大文件的内存占用
|
||||||
|
|
||||||
|
### Monitoring and Metrics
|
||||||
|
- **操作耗时**: 监控关键操作的执行时间
|
||||||
|
- **内存使用**: 跟踪内存使用情况
|
||||||
|
- **网络性能**: 监控下载速度和成功率
|
||||||
|
- **错误率**: 跟踪各类操作的错误率
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Potential Features
|
||||||
|
1. **Go 模块管理**: 集成 Go 模块和依赖管理
|
||||||
|
2. **开发工具集成**: 集成常用的 Go 开发工具
|
||||||
|
3. **项目模板**: 提供 Go 项目模板和脚手架
|
||||||
|
4. **性能分析**: 集成 Go 性能分析工具
|
||||||
|
5. **云端同步**: 同步配置和偏好设置
|
||||||
|
|
||||||
|
### Extensibility Points
|
||||||
|
- **插件系统**: 支持第三方插件扩展
|
||||||
|
- **自定义下载源**: 支持企业内部下载源
|
||||||
|
- **配置导入导出**: 支持配置的备份和恢复
|
||||||
|
- **API 扩展**: 提供扩展 API 供其他工具使用
|
||||||
112
.kiro/specs/go-version-management/requirements.md
Normal file
112
.kiro/specs/go-version-management/requirements.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
# Requirements Document
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
为 PHPer 开发环境管理器添加 Go 版本管理功能,使用户能够轻松安装、管理和切换不同版本的 Go 语言开发环境。该功能将与现有的 Node.js 和 Python 版本管理功能保持一致的用户体验。
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
- **Go_Manager**: Go 版本管理器,负责处理 Go 版本的安装、卸载和切换
|
||||||
|
- **Go_Version**: 特定版本的 Go 语言环境,包含编译器和标准库
|
||||||
|
- **Active_Version**: 当前系统环境变量中配置的默认 Go 版本
|
||||||
|
- **GOPATH**: Go 工作空间路径,用于存放 Go 代码和依赖包
|
||||||
|
- **GOROOT**: Go 安装根目录,包含 Go 编译器和标准库
|
||||||
|
- **Go_Binary**: Go 可执行文件,包括 go.exe、gofmt.exe 等工具
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement 1: Go 版本安装管理
|
||||||
|
|
||||||
|
**User Story:** 作为开发者,我想要安装不同版本的 Go 语言环境,以便在不同项目中使用合适的 Go 版本。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 用户查看可用版本列表 THEN THE Go_Manager SHALL 从 golang.org 官方 API 获取最新的 Go 版本信息
|
||||||
|
2. WHEN 用户选择安装特定版本 THEN THE Go_Manager SHALL 下载对应的 Windows 64位安装包
|
||||||
|
3. WHEN 下载过程中 THEN THE Go_Manager SHALL 显示下载进度和文件大小信息
|
||||||
|
4. WHEN 安装完成后 THEN THE Go_Manager SHALL 验证安装是否成功并显示安装结果
|
||||||
|
5. WHEN 用户尝试安装已存在的版本 THEN THE Go_Manager SHALL 提示版本已安装并阻止重复安装
|
||||||
|
|
||||||
|
### Requirement 2: Go 版本卸载管理
|
||||||
|
|
||||||
|
**User Story:** 作为开发者,我想要卸载不需要的 Go 版本,以便释放磁盘空间和保持系统整洁。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 用户选择卸载某个版本 THEN THE Go_Manager SHALL 显示确认对话框
|
||||||
|
2. WHEN 用户确认卸载 THEN THE Go_Manager SHALL 删除该版本的所有文件和目录
|
||||||
|
3. WHEN 卸载的是当前活动版本 THEN THE Go_Manager SHALL 清除环境变量配置
|
||||||
|
4. WHEN 卸载完成后 THEN THE Go_Manager SHALL 更新已安装版本列表
|
||||||
|
5. WHEN 用户尝试卸载不存在的版本 THEN THE Go_Manager SHALL 提示版本未安装
|
||||||
|
|
||||||
|
### Requirement 3: Go 版本切换管理
|
||||||
|
|
||||||
|
**User Story:** 作为开发者,我想要在不同的 Go 版本之间快速切换,以便在不同项目中使用不同版本的 Go。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 用户选择设置默认版本 THEN THE Go_Manager SHALL 更新系统环境变量 PATH
|
||||||
|
2. WHEN 环境变量更新后 THEN THE Go_Manager SHALL 验证新版本是否生效
|
||||||
|
3. WHEN 切换成功后 THEN THE Go_Manager SHALL 在界面上标识当前活动版本
|
||||||
|
4. WHEN 用户在新终端中执行 go version THEN THE System SHALL 显示新设置的 Go 版本
|
||||||
|
5. WHEN 切换版本失败 THEN THE Go_Manager SHALL 显示详细的错误信息
|
||||||
|
|
||||||
|
### Requirement 4: Go 版本信息展示
|
||||||
|
|
||||||
|
**User Story:** 作为开发者,我想要查看已安装的 Go 版本详细信息,以便了解每个版本的状态和配置。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 用户打开 Go 管理页面 THEN THE Go_Manager SHALL 显示所有已安装版本的列表
|
||||||
|
2. WHEN 显示版本信息时 THEN THE Go_Manager SHALL 包含版本号、安装路径和是否为活动版本
|
||||||
|
3. WHEN 显示版本信息时 THEN THE Go_Manager SHALL 显示每个版本的 GOROOT 和 GOPATH 配置
|
||||||
|
4. WHEN 版本列表为空时 THEN THE Go_Manager SHALL 显示友好的空状态提示
|
||||||
|
5. WHEN 检测到系统已安装的 Go THEN THE Go_Manager SHALL 显示系统版本信息
|
||||||
|
|
||||||
|
### Requirement 5: Go 工具链集成
|
||||||
|
|
||||||
|
**User Story:** 作为开发者,我想要确保安装的 Go 版本包含完整的工具链,以便进行完整的 Go 开发工作。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN Go 版本安装完成后 THEN THE Go_Manager SHALL 验证 go.exe 可执行文件存在
|
||||||
|
2. WHEN Go 版本安装完成后 THEN THE Go_Manager SHALL 验证 gofmt.exe 格式化工具存在
|
||||||
|
3. WHEN Go 版本安装完成后 THEN THE Go_Manager SHALL 验证 go build 命令可正常执行
|
||||||
|
4. WHEN Go 版本安装完成后 THEN THE Go_Manager SHALL 验证 go mod 模块管理功能可用
|
||||||
|
5. WHEN 工具链验证失败 THEN THE Go_Manager SHALL 提示安装不完整并提供修复建议
|
||||||
|
|
||||||
|
### Requirement 6: 配置文件管理
|
||||||
|
|
||||||
|
**User Story:** 作为开发者,我想要系统自动管理 Go 相关的配置,以便无需手动配置复杂的环境变量。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 安装新的 Go 版本时 THEN THE Go_Manager SHALL 自动配置 GOROOT 环境变量
|
||||||
|
2. WHEN 切换 Go 版本时 THEN THE Go_Manager SHALL 更新用户级别的环境变量
|
||||||
|
3. WHEN 设置环境变量时 THEN THE Go_Manager SHALL 清理旧版本的路径配置
|
||||||
|
4. WHEN 配置 GOPATH 时 THEN THE Go_Manager SHALL 使用用户工作目录下的 go 文件夹
|
||||||
|
5. WHEN 环境变量配置失败 THEN THE Go_Manager SHALL 提供手动配置的指导信息
|
||||||
|
|
||||||
|
### Requirement 7: 用户界面集成
|
||||||
|
|
||||||
|
**User Story:** 作为开发者,我想要 Go 管理功能与现有界面风格保持一致,以便获得统一的用户体验。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 用户访问 Go 管理页面 THEN THE System SHALL 显示与其他服务管理页面一致的界面布局
|
||||||
|
2. WHEN 显示版本卡片时 THEN THE System SHALL 使用与 Node.js 管理页面相同的卡片样式
|
||||||
|
3. WHEN 显示操作按钮时 THEN THE System SHALL 使用统一的按钮样式和颜色方案
|
||||||
|
4. WHEN 显示状态信息时 THEN THE System SHALL 使用一致的图标和标签样式
|
||||||
|
5. WHEN 页面加载时 THEN THE System SHALL 支持 KeepAlive 缓存以提升切换体验
|
||||||
|
|
||||||
|
### Requirement 8: 错误处理和用户反馈
|
||||||
|
|
||||||
|
**User Story:** 作为开发者,我想要在操作过程中获得清晰的反馈信息,以便了解操作状态和处理可能的错误。
|
||||||
|
|
||||||
|
#### Acceptance Criteria
|
||||||
|
|
||||||
|
1. WHEN 网络连接失败时 THEN THE Go_Manager SHALL 显示网络错误提示并提供重试选项
|
||||||
|
2. WHEN 磁盘空间不足时 THEN THE Go_Manager SHALL 显示空间不足警告并阻止安装
|
||||||
|
3. WHEN 权限不足时 THEN THE Go_Manager SHALL 提示需要管理员权限
|
||||||
|
4. WHEN 操作成功时 THEN THE Go_Manager SHALL 显示成功消息和相关详情
|
||||||
|
5. WHEN 操作失败时 THEN THE Go_Manager SHALL 显示具体的错误原因和解决建议
|
||||||
176
.kiro/specs/go-version-management/tasks.md
Normal file
176
.kiro/specs/go-version-management/tasks.md
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
# Implementation Plan: Go Version Management
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
本实现计划将 Go 版本管理功能集成到 PHPer 开发环境管理器中。实现将遵循现有的 NodeManager 和 PythonManager 架构模式,确保代码一致性和可维护性。所有代码将使用 TypeScript 编写,与项目现有技术栈保持一致。
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [x] 1. 创建 Go 管理器后端服务
|
||||||
|
- [x] 1.1 创建 GoManager.ts 服务类
|
||||||
|
- 创建 `electron/services/GoManager.ts` 文件
|
||||||
|
- 定义核心接口和类型(GoVersion, AvailableGoVersion, GoInfo)
|
||||||
|
- 实现基础类结构和 ConfigStore 集成
|
||||||
|
- _Requirements: 1.1, 4.1, 6.1_
|
||||||
|
|
||||||
|
- [x] 1.2 为核心结构编写单元测试
|
||||||
|
- 测试基础类初始化和配置集成
|
||||||
|
- _Requirements: 1.1, 4.1, 6.1_
|
||||||
|
|
||||||
|
- [x] 2. 实现版本获取和管理功能
|
||||||
|
- [x] 2.1 实现 golang.org API 集成
|
||||||
|
- 从 `https://golang.org/dl/?mode=json` 获取版本列表
|
||||||
|
- 解析 API 响应并提取 Windows 64位版本
|
||||||
|
- 实现缓存机制(5分钟缓存)
|
||||||
|
- 添加备用版本列表
|
||||||
|
- _Requirements: 1.1_
|
||||||
|
|
||||||
|
- [x] 2.2 为 API 集成编写属性测试
|
||||||
|
- **Property 1: API Version Fetching Consistency**
|
||||||
|
- **Validates: Requirements 1.1**
|
||||||
|
|
||||||
|
- [x] 2.3 实现已安装版本检测
|
||||||
|
- 扫描本地 Go 安装目录
|
||||||
|
- 验证安装完整性(go.exe, gofmt.exe 等)
|
||||||
|
- 检测当前活动版本
|
||||||
|
- 获取版本详细信息
|
||||||
|
- _Requirements: 4.1, 4.2, 4.5, 5.1, 5.2_
|
||||||
|
|
||||||
|
- [x] 2.4 为版本检测编写属性测试
|
||||||
|
- **Property 9: Version Information Completeness**
|
||||||
|
- **Validates: Requirements 4.2, 4.3**
|
||||||
|
|
||||||
|
- [x] 3. 实现下载和安装功能
|
||||||
|
- [x] 3.1 实现文件下载管理器
|
||||||
|
- 支持 HTTPS 下载和进度跟踪
|
||||||
|
- 实现 SHA256 校验和验证
|
||||||
|
- 处理下载中断和重试
|
||||||
|
- 集成到现有的下载进度系统
|
||||||
|
- _Requirements: 1.2, 1.3_
|
||||||
|
|
||||||
|
- [x] 3.2 为下载功能编写属性测试
|
||||||
|
- **Property 2: Download URL Construction**
|
||||||
|
- **Property 3: Download Progress Reporting**
|
||||||
|
- **Validates: Requirements 1.2, 1.3**
|
||||||
|
|
||||||
|
- [x] 3.3 实现 Go 版本安装逻辑
|
||||||
|
- ZIP 文件解压到目标目录
|
||||||
|
- 安装后验证和完整性检查
|
||||||
|
- 重复安装检测和防护
|
||||||
|
- 清理临时文件
|
||||||
|
- _Requirements: 1.4, 1.5, 5.1, 5.2, 5.3, 5.4_
|
||||||
|
|
||||||
|
- [x] 3.4 为安装功能编写属性测试
|
||||||
|
- **Property 4: Installation Validation Completeness**
|
||||||
|
- **Property 5: Duplicate Installation Prevention**
|
||||||
|
- **Validates: Requirements 1.4, 1.5, 5.1, 5.2, 5.3, 5.4**
|
||||||
|
|
||||||
|
- [x] 4. 实现环境变量管理
|
||||||
|
- [x] 4.1 实现环境变量更新逻辑
|
||||||
|
- 使用 PowerShell 脚本更新用户环境变量
|
||||||
|
- 设置 GOROOT、GOPATH 和 PATH
|
||||||
|
- 清理旧版本的路径配置
|
||||||
|
- _Requirements: 3.1, 6.1, 6.2, 6.3, 6.4_
|
||||||
|
|
||||||
|
- [x] 4.2 为环境变量管理编写属性测试
|
||||||
|
- **Property 7: Active Version Environment Management**
|
||||||
|
- **Property 14: GOPATH Default Configuration**
|
||||||
|
- **Validates: Requirements 3.1, 6.1, 6.2, 6.3, 6.4**
|
||||||
|
|
||||||
|
- [x] 4.3 实现版本切换和验证
|
||||||
|
- 版本激活和环境变量更新
|
||||||
|
- 切换后的功能验证
|
||||||
|
- 系统级 Go 命令验证
|
||||||
|
- _Requirements: 3.2, 3.3, 3.4_
|
||||||
|
|
||||||
|
- [x] 4.4 为版本切换编写属性测试
|
||||||
|
- **Property 11: Environment Variable Validation**
|
||||||
|
- **Validates: Requirements 3.2, 3.4**
|
||||||
|
|
||||||
|
- [x] 5. 实现卸载和清理功能
|
||||||
|
- [x] 5.1 实现版本卸载逻辑
|
||||||
|
- 文件系统清理和目录删除
|
||||||
|
- 活动版本的环境变量清理
|
||||||
|
- 配置状态更新
|
||||||
|
- _Requirements: 2.2, 2.3, 2.4, 2.5_
|
||||||
|
|
||||||
|
- [x] 5.2 为卸载功能编写属性测试
|
||||||
|
- **Property 6: File System Cleanup on Uninstall**
|
||||||
|
- **Property 8: Version State Consistency**
|
||||||
|
- **Validates: Requirements 2.2, 2.4**
|
||||||
|
|
||||||
|
- [x] 6. 实现错误处理和用户反馈
|
||||||
|
- [x] 6.1 实现综合错误处理系统
|
||||||
|
- 网络错误、磁盘空间、权限检查
|
||||||
|
- 描述性错误消息和解决建议
|
||||||
|
- 成功操作的详细反馈
|
||||||
|
- _Requirements: 8.1, 8.2, 8.3, 8.4, 8.5_
|
||||||
|
|
||||||
|
- [x] 7. 实现 IPC 通信层
|
||||||
|
- [x] 7.1 在主进程中注册 Go 管理 IPC 处理器
|
||||||
|
- 在 `electron/main.ts` 中添加 Go 相关的 IPC 处理函数
|
||||||
|
- 注册 go:getVersions, go:getAvailableVersions, go:install, go:uninstall, go:setActive 等
|
||||||
|
- 集成 GoManager 实例
|
||||||
|
- _Requirements: 所有后端功能_
|
||||||
|
|
||||||
|
- [x] 7.2 更新预加载脚本
|
||||||
|
- 在 `electron/preload.ts` 中暴露 Go 管理 API
|
||||||
|
- 添加 go 对象到 electronAPI
|
||||||
|
- 确保类型安全的 IPC 通信
|
||||||
|
- _Requirements: 所有后端功能_
|
||||||
|
|
||||||
|
- [x] 8. 实现前端 Vue 组件
|
||||||
|
- [x] 8.1 创建 GoManager.vue 组件
|
||||||
|
- 创建 `src/views/GoManager.vue` 文件
|
||||||
|
- 设置组件模板和基本布局(参考 NodeManager.vue 和 PythonManager.vue)
|
||||||
|
- 实现 KeepAlive 缓存支持
|
||||||
|
- 添加响应式数据管理
|
||||||
|
- _Requirements: 7.1, 7.5_
|
||||||
|
|
||||||
|
- [x] 8.2 实现版本列表显示功能
|
||||||
|
- 已安装版本的卡片式展示
|
||||||
|
- 版本信息显示(版本号、路径、状态)
|
||||||
|
- 活动版本的视觉标识
|
||||||
|
- 空状态处理和友好提示
|
||||||
|
- _Requirements: 4.1, 4.2, 4.3, 4.4_
|
||||||
|
|
||||||
|
- [x] 8.3 实现版本操作界面
|
||||||
|
- 安装新版本对话框
|
||||||
|
- 可用版本列表和选择
|
||||||
|
- 卸载确认对话框
|
||||||
|
- 版本切换操作按钮
|
||||||
|
- _Requirements: 1.1, 2.1, 3.2_
|
||||||
|
|
||||||
|
- [x] 8.4 实现下载进度和状态反馈
|
||||||
|
- 下载进度条和状态显示
|
||||||
|
- 操作加载状态和按钮禁用
|
||||||
|
- 成功/错误消息提示
|
||||||
|
- 集成到现有的下载进度监听系统
|
||||||
|
- _Requirements: 1.3, 8.4, 8.5_
|
||||||
|
|
||||||
|
- [x] 9. 集成到主应用程序
|
||||||
|
- [x] 9.1 添加 Go 管理页面路由
|
||||||
|
- 在 `src/router/index.ts` 中添加 Go 管理路由
|
||||||
|
- 路径: `/go`, 名称: `go`, 组件: `GoManager.vue`
|
||||||
|
- 添加页面标题元数据
|
||||||
|
- _Requirements: 7.1_
|
||||||
|
|
||||||
|
- [x] 9.2 更新应用程序导航
|
||||||
|
- 在主导航中添加 Go 管理入口
|
||||||
|
- 添加 Go 相关的图标
|
||||||
|
- 确保视觉一致性
|
||||||
|
- _Requirements: 7.2, 7.3, 7.4_
|
||||||
|
|
||||||
|
- [x] 10. 检查点 - 完整功能验证
|
||||||
|
- 确保所有功能正常工作
|
||||||
|
- 验证用户体验和界面一致性
|
||||||
|
- 询问用户是否有问题或需要调整
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- 后端 GoManager 服务已完全实现,包含所有核心功能和错误处理
|
||||||
|
- 属性测试已实现,覆盖所有关键正确性属性
|
||||||
|
- 需要完成 IPC 通信层、前端组件和应用集成
|
||||||
|
- 每个任务都引用了具体的需求条目以确保可追溯性
|
||||||
|
- 所有代码将使用 TypeScript 编写,与项目技术栈保持一致
|
||||||
|
- 实现将遵循现有的 NodeManager 和 PythonManager 架构模式
|
||||||
24
.kiro/steering/product.md
Normal file
24
.kiro/steering/product.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# PHPer 开发环境管理器
|
||||||
|
|
||||||
|
PHPer 开发环境管理器是一款专为 Windows 平台设计的 PHP 开发环境管理工具,提供可视化界面来管理多种开发服务和工具。
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
- **多版本 PHP 管理**: 支持 PHP 8.1-8.5 多版本并行安装,独立 CGI 进程控制
|
||||||
|
- **数据库服务**: MySQL 5.7/8.0 版本管理,自动初始化和配置
|
||||||
|
- **Web 服务器**: Nginx 管理,支持虚拟主机和 SSL 证书
|
||||||
|
- **缓存服务**: Redis Windows 版本管理
|
||||||
|
- **开发工具**: Node.js、Python、Git 版本管理
|
||||||
|
- **站点管理**: 可视化创建和管理开发站点,支持 Laravel 项目
|
||||||
|
- **系统集成**: Hosts 文件管理,开机自启动配置
|
||||||
|
|
||||||
|
## 目标用户
|
||||||
|
|
||||||
|
主要面向 Windows 平台的 PHP 开发者,提供类似 XAMPP/WAMP 但更现代化和可定制的开发环境解决方案。
|
||||||
|
|
||||||
|
## 技术特点
|
||||||
|
|
||||||
|
- 基于 Electron 的桌面应用
|
||||||
|
- 使用 Vue 3 + TypeScript 构建前端界面
|
||||||
|
- 服务管理采用 Windows 原生进程控制
|
||||||
|
- 支持静默启动和系统托盘运行
|
||||||
105
.kiro/steering/structure.md
Normal file
105
.kiro/steering/structure.md
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# 项目结构和组织
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
### 核心目录
|
||||||
|
|
||||||
|
```
|
||||||
|
phper/
|
||||||
|
├── electron/ # Electron 主进程代码
|
||||||
|
│ ├── main.ts # 主进程入口,IPC 处理
|
||||||
|
│ ├── preload.ts # 预加载脚本,安全的 IPC 桥接
|
||||||
|
│ └── services/ # 服务管理模块
|
||||||
|
│ ├── ConfigStore.ts # 配置存储管理
|
||||||
|
│ ├── ServiceManager.ts # 统一服务管理
|
||||||
|
│ ├── *Manager.ts # 各服务专用管理器
|
||||||
|
│ └── __tests__/ # 服务层单元测试
|
||||||
|
│
|
||||||
|
├── src/ # Vue 前端源码
|
||||||
|
│ ├── App.vue # 根组件,包含布局和导航
|
||||||
|
│ ├── main.ts # 前端入口文件
|
||||||
|
│ ├── router/ # Vue Router 配置
|
||||||
|
│ ├── stores/ # Pinia 状态管理
|
||||||
|
│ ├── components/ # 可复用组件
|
||||||
|
│ ├── views/ # 页面组件
|
||||||
|
│ └── styles/ # 全局样式和主题
|
||||||
|
│
|
||||||
|
├── data/ # 运行时数据目录
|
||||||
|
├── service/ # 服务安装目录
|
||||||
|
├── build/ # 构建资源
|
||||||
|
├── release/ # 打包输出
|
||||||
|
└── scripts/ # 构建脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
## 架构模式
|
||||||
|
|
||||||
|
### Electron 架构
|
||||||
|
- **主进程** (`electron/main.ts`): 窗口管理、系统集成、IPC 处理
|
||||||
|
- **渲染进程** (`src/`): Vue 应用,用户界面
|
||||||
|
- **预加载脚本** (`electron/preload.ts`): 安全的 API 暴露
|
||||||
|
|
||||||
|
### 服务管理架构
|
||||||
|
- **ServiceManager**: 统一的服务生命周期管理
|
||||||
|
- **专用管理器**: 每个服务(PHP、MySQL、Nginx等)有独立的管理类
|
||||||
|
- **ConfigStore**: 集中的配置管理,基于 electron-store
|
||||||
|
|
||||||
|
### 前端架构
|
||||||
|
- **组件化**: 使用 Vue 3 Composition API
|
||||||
|
- **状态管理**: Pinia stores 管理应用状态
|
||||||
|
- **路由管理**: Vue Router 4 单页面应用
|
||||||
|
- **UI 组件**: Element Plus 提供一致的界面体验
|
||||||
|
|
||||||
|
## 命名约定
|
||||||
|
|
||||||
|
### 文件命名
|
||||||
|
- **组件文件**: PascalCase (如 `PhpManager.vue`)
|
||||||
|
- **服务文件**: PascalCase + Manager 后缀 (如 `MysqlManager.ts`)
|
||||||
|
- **工具文件**: camelCase (如 `configStore.ts`)
|
||||||
|
- **测试文件**: `*.test.ts` 后缀
|
||||||
|
|
||||||
|
### 代码约定
|
||||||
|
- **接口**: `I` 前缀 (如 `IServiceStatus`)
|
||||||
|
- **类型**: PascalCase (如 `ServiceConfig`)
|
||||||
|
- **常量**: UPPER_SNAKE_CASE
|
||||||
|
- **函数/变量**: camelCase
|
||||||
|
|
||||||
|
## 关键文件说明
|
||||||
|
|
||||||
|
### 配置文件
|
||||||
|
- `package.json`: 项目依赖和构建脚本
|
||||||
|
- `vite.config.ts`: Vite 构建配置,包含 Electron 插件
|
||||||
|
- `tsconfig.json`: TypeScript 编译配置
|
||||||
|
- `jest.config.js`: 测试框架配置
|
||||||
|
|
||||||
|
### 入口文件
|
||||||
|
- `index.html`: HTML 模板
|
||||||
|
- `src/main.ts`: Vue 应用入口
|
||||||
|
- `electron/main.ts`: Electron 主进程入口
|
||||||
|
|
||||||
|
### 服务管理
|
||||||
|
- 每个服务管理器负责:下载、安装、配置、启动/停止、状态检查
|
||||||
|
- 使用 Windows 原生命令和进程管理
|
||||||
|
- 支持多版本并存(PHP、MySQL、Node.js)
|
||||||
|
|
||||||
|
## 数据流
|
||||||
|
|
||||||
|
1. **用户操作** → Vue 组件
|
||||||
|
2. **组件事件** → Pinia Store 或直接 IPC 调用
|
||||||
|
3. **IPC 通信** → Electron 主进程
|
||||||
|
4. **服务调用** → 对应的 Manager 类
|
||||||
|
5. **系统操作** → Windows 命令/进程
|
||||||
|
6. **状态更新** → 通过 IPC 返回到前端
|
||||||
|
|
||||||
|
## 扩展指南
|
||||||
|
|
||||||
|
### 添加新服务
|
||||||
|
1. 创建 `*Manager.ts` 在 `electron/services/`
|
||||||
|
2. 在 `main.ts` 中注册 IPC 处理程序
|
||||||
|
3. 创建对应的 Vue 页面组件
|
||||||
|
4. 添加路由和导航菜单项
|
||||||
|
5. 更新 `ServiceManager.ts` 集成新服务
|
||||||
|
|
||||||
|
### 添加新功能
|
||||||
|
- 前端功能:在 `src/views/` 添加页面组件
|
||||||
|
- 后端功能:在对应的 Manager 类中添加方法
|
||||||
|
- 通用组件:在 `src/components/` 创建可复用组件
|
||||||
84
.kiro/steering/tech.md
Normal file
84
.kiro/steering/tech.md
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# 技术栈和构建系统
|
||||||
|
|
||||||
|
## 核心技术栈
|
||||||
|
|
||||||
|
### 前端框架
|
||||||
|
- **Vue 3**: 使用 Composition API 和 `<script setup>` 语法
|
||||||
|
- **TypeScript**: 严格类型检查,配置在 `tsconfig.json`
|
||||||
|
- **Element Plus**: UI 组件库,提供现代化界面组件
|
||||||
|
- **Vue Router 4**: 单页面应用路由管理
|
||||||
|
- **Pinia**: 状态管理,替代 Vuex
|
||||||
|
|
||||||
|
### 桌面应用框架
|
||||||
|
- **Electron**: 主进程和渲染进程架构
|
||||||
|
- **IPC 通信**: 主进程与渲染进程间的安全通信
|
||||||
|
- **electron-store**: 应用配置持久化存储
|
||||||
|
|
||||||
|
### 构建工具
|
||||||
|
- **Vite**: 现代化构建工具,支持热重载
|
||||||
|
- **vite-plugin-electron**: Electron 集成插件
|
||||||
|
- **electron-builder**: 应用打包和分发
|
||||||
|
|
||||||
|
### 开发工具
|
||||||
|
- **Jest**: 单元测试框架,配置在 `jest.config.js`
|
||||||
|
- **vue-tsc**: Vue 组件类型检查
|
||||||
|
- **Sass**: CSS 预处理器,支持主题变量
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
```bash
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
npm run typecheck
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
npm run test
|
||||||
|
npm run test:watch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建和打包
|
||||||
|
```bash
|
||||||
|
# 构建生产版本(自动更新 patch 版本)
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 指定版本更新类型
|
||||||
|
npm run build:patch # 1.0.0 -> 1.0.1
|
||||||
|
npm run build:minor # 1.0.0 -> 1.1.0
|
||||||
|
npm run build:major # 1.0.0 -> 2.0.0
|
||||||
|
|
||||||
|
# 不更新版本号直接打包
|
||||||
|
npm run build:nobump
|
||||||
|
|
||||||
|
# 构建前进行类型检查
|
||||||
|
npm run build:check
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键依赖
|
||||||
|
|
||||||
|
### 生产依赖
|
||||||
|
- `axios`: HTTP 客户端
|
||||||
|
- `unzipper`: 文件解压缩
|
||||||
|
- `node-windows`: Windows 服务管理
|
||||||
|
- `sudo-prompt`: 管理员权限提升
|
||||||
|
|
||||||
|
### 开发依赖
|
||||||
|
- `concurrently`: 并行运行多个命令
|
||||||
|
- `fast-check`: 属性测试库
|
||||||
|
- `ts-jest`: TypeScript Jest 支持
|
||||||
|
|
||||||
|
## 构建配置
|
||||||
|
|
||||||
|
- **输出目录**: `dist` (前端), `dist-electron` (Electron)
|
||||||
|
- **图标**: `build/icon.ico`
|
||||||
|
- **安装包**: 生成到 `release` 目录
|
||||||
|
- **目标平台**: Windows x64 (NSIS 和 Portable)
|
||||||
|
|
||||||
|
## 开发注意事项
|
||||||
|
|
||||||
|
- 使用 `windowsHide: true` 隐藏命令行窗口
|
||||||
|
- 服务启动使用 VBScript 实现静默启动
|
||||||
|
- 需要管理员权限进行服务管理和 hosts 文件修改
|
||||||
|
- 支持开机自启动(任务计划程序)
|
||||||
@ -7,11 +7,10 @@ exports.default = async function(context) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Running afterPack hook to set icon and version info...');
|
console.log('Running afterPack hook to set icon...');
|
||||||
|
|
||||||
const appOutDir = context.appOutDir;
|
const appOutDir = context.appOutDir;
|
||||||
const productName = context.packager.appInfo.productName;
|
const productName = context.packager.appInfo.productName;
|
||||||
const version = context.packager.appInfo.version;
|
|
||||||
const exePath = path.join(appOutDir, `${productName}.exe`);
|
const exePath = path.join(appOutDir, `${productName}.exe`);
|
||||||
const iconPath = path.join(__dirname, 'icon.ico');
|
const iconPath = path.join(__dirname, 'icon.ico');
|
||||||
|
|
||||||
@ -26,29 +25,18 @@ exports.default = async function(context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// rcedit 是默认导出
|
// 使用 npm 安装的 rcedit 模块
|
||||||
const rcedit = require('rcedit');
|
const { rcedit } = require('rcedit');
|
||||||
|
|
||||||
console.log(`Setting icon and version info for: ${exePath}`);
|
console.log(`Setting icon for: ${exePath}`);
|
||||||
console.log(`Using icon: ${iconPath}`);
|
console.log(`Using icon: ${iconPath}`);
|
||||||
|
|
||||||
await rcedit(exePath, {
|
await rcedit(exePath, {
|
||||||
icon: iconPath,
|
icon: iconPath
|
||||||
'version-string': {
|
|
||||||
'ProductName': productName,
|
|
||||||
'FileDescription': productName,
|
|
||||||
'CompanyName': 'PHPer',
|
|
||||||
'LegalCopyright': 'Copyright © 2024 PHPer',
|
|
||||||
'OriginalFilename': `${productName}.exe`,
|
|
||||||
'InternalName': productName
|
|
||||||
},
|
|
||||||
'file-version': version,
|
|
||||||
'product-version': version
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Icon and version info set successfully!');
|
console.log('Icon set successfully!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to set icon:', error.message);
|
console.error('Failed to set icon:', error.message);
|
||||||
// 不阻止打包继续
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
213
electron/main.ts
213
electron/main.ts
@ -13,11 +13,11 @@ import { MysqlManager } from "./services/MysqlManager";
|
|||||||
import { NginxManager } from "./services/NginxManager";
|
import { NginxManager } from "./services/NginxManager";
|
||||||
import { RedisManager } from "./services/RedisManager";
|
import { RedisManager } from "./services/RedisManager";
|
||||||
import { NodeManager } from "./services/NodeManager";
|
import { NodeManager } from "./services/NodeManager";
|
||||||
import { GoManager } from "./services/GoManager";
|
|
||||||
import { ServiceManager } from "./services/ServiceManager";
|
import { ServiceManager } from "./services/ServiceManager";
|
||||||
import { HostsManager } from "./services/HostsManager";
|
import { HostsManager } from "./services/HostsManager";
|
||||||
import { GitManager } from "./services/GitManager";
|
import { GitManager } from "./services/GitManager";
|
||||||
import { PythonManager } from "./services/PythonManager";
|
import { PythonManager } from "./services/PythonManager";
|
||||||
|
import { GoManager } from "./services/GoManager";
|
||||||
import { LogManager } from "./services/LogManager";
|
import { LogManager } from "./services/LogManager";
|
||||||
import { ConfigStore } from "./services/ConfigStore";
|
import { ConfigStore } from "./services/ConfigStore";
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ export function sendDownloadProgress(
|
|||||||
type: string,
|
type: string,
|
||||||
progress: number,
|
progress: number,
|
||||||
downloaded: number,
|
downloaded: number,
|
||||||
total: number,
|
total: number
|
||||||
) {
|
) {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.webContents.send("download-progress", {
|
mainWindow.webContents.send("download-progress", {
|
||||||
@ -118,11 +118,11 @@ const mysqlManager = new MysqlManager(configStore);
|
|||||||
const nginxManager = new NginxManager(configStore);
|
const nginxManager = new NginxManager(configStore);
|
||||||
const redisManager = new RedisManager(configStore);
|
const redisManager = new RedisManager(configStore);
|
||||||
const nodeManager = new NodeManager(configStore);
|
const nodeManager = new NodeManager(configStore);
|
||||||
const goManager = new GoManager(configStore);
|
|
||||||
const serviceManager = new ServiceManager(configStore);
|
const serviceManager = new ServiceManager(configStore);
|
||||||
const hostsManager = new HostsManager();
|
const hostsManager = new HostsManager();
|
||||||
const gitManager = new GitManager(configStore);
|
const gitManager = new GitManager(configStore);
|
||||||
const pythonManager = new PythonManager(configStore);
|
const pythonManager = new PythonManager(configStore);
|
||||||
|
const goManager = new GoManager(configStore);
|
||||||
const logManager = new LogManager(configStore);
|
const logManager = new LogManager(configStore);
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
@ -243,15 +243,6 @@ function createTray() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置应用名称和 Windows AppUserModelId(用于任务栏图标分组和进程名称显示)
|
|
||||||
const APP_NAME = "PHPer开发环境管理器";
|
|
||||||
const APP_ID = "com.phper.devmanager";
|
|
||||||
|
|
||||||
app.setName(APP_NAME);
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
app.setAppUserModelId(APP_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 单实例锁定
|
// 单实例锁定
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
@ -317,7 +308,7 @@ ipcMain.handle("window:close", () => mainWindow?.close());
|
|||||||
|
|
||||||
// 打开外部链接
|
// 打开外部链接
|
||||||
ipcMain.handle("shell:openExternal", (_, url: string) =>
|
ipcMain.handle("shell:openExternal", (_, url: string) =>
|
||||||
shell.openExternal(url),
|
shell.openExternal(url)
|
||||||
);
|
);
|
||||||
ipcMain.handle("shell:openPath", (_, path: string) => shell.openPath(path));
|
ipcMain.handle("shell:openPath", (_, path: string) => shell.openPath(path));
|
||||||
|
|
||||||
@ -334,33 +325,33 @@ ipcMain.handle("dialog:selectDirectory", async () => {
|
|||||||
// ==================== PHP 管理 ====================
|
// ==================== PHP 管理 ====================
|
||||||
ipcMain.handle("php:getVersions", () => phpManager.getInstalledVersions());
|
ipcMain.handle("php:getVersions", () => phpManager.getInstalledVersions());
|
||||||
ipcMain.handle("php:getAvailableVersions", () =>
|
ipcMain.handle("php:getAvailableVersions", () =>
|
||||||
phpManager.getAvailableVersions(),
|
phpManager.getAvailableVersions()
|
||||||
);
|
);
|
||||||
ipcMain.handle("php:install", (_, version: string) =>
|
ipcMain.handle("php:install", (_, version: string) =>
|
||||||
phpManager.install(version),
|
phpManager.install(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("php:uninstall", (_, version: string) =>
|
ipcMain.handle("php:uninstall", (_, version: string) =>
|
||||||
phpManager.uninstall(version),
|
phpManager.uninstall(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("php:setActive", (_, version: string) =>
|
ipcMain.handle("php:setActive", (_, version: string) =>
|
||||||
phpManager.setActive(version),
|
phpManager.setActive(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("php:getExtensions", (_, version: string) =>
|
ipcMain.handle("php:getExtensions", (_, version: string) =>
|
||||||
phpManager.getExtensions(version),
|
phpManager.getExtensions(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("php:openExtensionDir", (_, version: string) =>
|
ipcMain.handle("php:openExtensionDir", (_, version: string) =>
|
||||||
phpManager.openExtensionDir(version),
|
phpManager.openExtensionDir(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"php:getAvailableExtensions",
|
"php:getAvailableExtensions",
|
||||||
(_, version: string, searchKeyword?: string) =>
|
(_, version: string, searchKeyword?: string) =>
|
||||||
phpManager.getAvailableExtensions(version, searchKeyword),
|
phpManager.getAvailableExtensions(version, searchKeyword)
|
||||||
);
|
);
|
||||||
ipcMain.handle("php:enableExtension", (_, version: string, ext: string) =>
|
ipcMain.handle("php:enableExtension", (_, version: string, ext: string) =>
|
||||||
phpManager.enableExtension(version, ext),
|
phpManager.enableExtension(version, ext)
|
||||||
);
|
);
|
||||||
ipcMain.handle("php:disableExtension", (_, version: string, ext: string) =>
|
ipcMain.handle("php:disableExtension", (_, version: string, ext: string) =>
|
||||||
phpManager.disableExtension(version, ext),
|
phpManager.disableExtension(version, ext)
|
||||||
);
|
);
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"php:installExtension",
|
"php:installExtension",
|
||||||
@ -369,14 +360,14 @@ ipcMain.handle(
|
|||||||
version: string,
|
version: string,
|
||||||
ext: string,
|
ext: string,
|
||||||
downloadUrl?: string,
|
downloadUrl?: string,
|
||||||
packageName?: string,
|
packageName?: string
|
||||||
) => phpManager.installExtension(version, ext, downloadUrl, packageName),
|
) => phpManager.installExtension(version, ext, downloadUrl, packageName)
|
||||||
);
|
);
|
||||||
ipcMain.handle("php:getConfig", (_, version: string) =>
|
ipcMain.handle("php:getConfig", (_, version: string) =>
|
||||||
phpManager.getConfig(version),
|
phpManager.getConfig(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("php:saveConfig", (_, version: string, config: string) =>
|
ipcMain.handle("php:saveConfig", (_, version: string, config: string) =>
|
||||||
phpManager.saveConfig(version, config),
|
phpManager.saveConfig(version, config)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== Composer 管理 ====================
|
// ==================== Composer 管理 ====================
|
||||||
@ -384,62 +375,62 @@ ipcMain.handle("composer:getStatus", () => phpManager.getComposerStatus());
|
|||||||
ipcMain.handle("composer:install", () => phpManager.installComposer());
|
ipcMain.handle("composer:install", () => phpManager.installComposer());
|
||||||
ipcMain.handle("composer:uninstall", () => phpManager.uninstallComposer());
|
ipcMain.handle("composer:uninstall", () => phpManager.uninstallComposer());
|
||||||
ipcMain.handle("composer:setMirror", (_, mirror: string) =>
|
ipcMain.handle("composer:setMirror", (_, mirror: string) =>
|
||||||
phpManager.setComposerMirror(mirror),
|
phpManager.setComposerMirror(mirror)
|
||||||
);
|
);
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"composer:createLaravelProject",
|
"composer:createLaravelProject",
|
||||||
(_, projectName: string, targetDir: string) =>
|
(_, projectName: string, targetDir: string) =>
|
||||||
phpManager.createLaravelProject(projectName, targetDir),
|
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", () =>
|
||||||
mysqlManager.getAvailableVersions(),
|
mysqlManager.getAvailableVersions()
|
||||||
);
|
);
|
||||||
ipcMain.handle("mysql:install", (_, version: string) =>
|
ipcMain.handle("mysql:install", (_, version: string) =>
|
||||||
mysqlManager.install(version),
|
mysqlManager.install(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("mysql:uninstall", (_, version: string) =>
|
ipcMain.handle("mysql:uninstall", (_, version: string) =>
|
||||||
mysqlManager.uninstall(version),
|
mysqlManager.uninstall(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("mysql:start", (_, version: string) =>
|
ipcMain.handle("mysql:start", (_, version: string) =>
|
||||||
mysqlManager.start(version),
|
mysqlManager.start(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("mysql:stop", (_, version: string) =>
|
ipcMain.handle("mysql:stop", (_, version: string) =>
|
||||||
mysqlManager.stop(version),
|
mysqlManager.stop(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("mysql:restart", (_, version: string) =>
|
ipcMain.handle("mysql:restart", (_, version: string) =>
|
||||||
mysqlManager.restart(version),
|
mysqlManager.restart(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("mysql:getStatus", (_, version: string) =>
|
ipcMain.handle("mysql:getStatus", (_, version: string) =>
|
||||||
mysqlManager.getStatus(version),
|
mysqlManager.getStatus(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"mysql:changePassword",
|
"mysql:changePassword",
|
||||||
(_, version: string, newPassword: string, currentPassword?: string) =>
|
(_, version: string, newPassword: string, currentPassword?: string) =>
|
||||||
mysqlManager.changeRootPassword(version, newPassword, currentPassword),
|
mysqlManager.changeRootPassword(version, newPassword, currentPassword)
|
||||||
);
|
);
|
||||||
ipcMain.handle("mysql:getConfig", (_, version: string) =>
|
ipcMain.handle("mysql:getConfig", (_, version: string) =>
|
||||||
mysqlManager.getConfig(version),
|
mysqlManager.getConfig(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("mysql:saveConfig", (_, version: string, config: string) =>
|
ipcMain.handle("mysql:saveConfig", (_, version: string, config: string) =>
|
||||||
mysqlManager.saveConfig(version, config),
|
mysqlManager.saveConfig(version, config)
|
||||||
);
|
);
|
||||||
ipcMain.handle("mysql:reinitialize", (_, version: string) =>
|
ipcMain.handle("mysql:reinitialize", (_, version: string) =>
|
||||||
mysqlManager.reinitialize(version),
|
mysqlManager.reinitialize(version)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== Nginx 管理 ====================
|
// ==================== Nginx 管理 ====================
|
||||||
ipcMain.handle("nginx:getVersions", () => nginxManager.getInstalledVersions());
|
ipcMain.handle("nginx:getVersions", () => nginxManager.getInstalledVersions());
|
||||||
ipcMain.handle("nginx:getAvailableVersions", () =>
|
ipcMain.handle("nginx:getAvailableVersions", () =>
|
||||||
nginxManager.getAvailableVersions(),
|
nginxManager.getAvailableVersions()
|
||||||
);
|
);
|
||||||
ipcMain.handle("nginx:install", (_, version: string) =>
|
ipcMain.handle("nginx:install", (_, version: string) =>
|
||||||
nginxManager.install(version),
|
nginxManager.install(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("nginx:uninstall", (_, version: string) =>
|
ipcMain.handle("nginx:uninstall", (_, version: string) =>
|
||||||
nginxManager.uninstall(version),
|
nginxManager.uninstall(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("nginx:start", () => nginxManager.start());
|
ipcMain.handle("nginx:start", () => nginxManager.start());
|
||||||
ipcMain.handle("nginx:stop", () => nginxManager.stop());
|
ipcMain.handle("nginx:stop", () => nginxManager.stop());
|
||||||
@ -448,39 +439,39 @@ ipcMain.handle("nginx:reload", () => nginxManager.reload());
|
|||||||
ipcMain.handle("nginx:getStatus", () => nginxManager.getStatus());
|
ipcMain.handle("nginx:getStatus", () => nginxManager.getStatus());
|
||||||
ipcMain.handle("nginx:getConfig", () => nginxManager.getConfig());
|
ipcMain.handle("nginx:getConfig", () => nginxManager.getConfig());
|
||||||
ipcMain.handle("nginx:saveConfig", (_, config: string) =>
|
ipcMain.handle("nginx:saveConfig", (_, config: string) =>
|
||||||
nginxManager.saveConfig(config),
|
nginxManager.saveConfig(config)
|
||||||
);
|
);
|
||||||
ipcMain.handle("nginx:getSites", () => nginxManager.getSites());
|
ipcMain.handle("nginx:getSites", () => nginxManager.getSites());
|
||||||
ipcMain.handle("nginx:addSite", (_, site: any) => nginxManager.addSite(site));
|
ipcMain.handle("nginx:addSite", (_, site: any) => nginxManager.addSite(site));
|
||||||
ipcMain.handle("nginx:removeSite", (_, name: string) =>
|
ipcMain.handle("nginx:removeSite", (_, name: string) =>
|
||||||
nginxManager.removeSite(name),
|
nginxManager.removeSite(name)
|
||||||
);
|
);
|
||||||
ipcMain.handle("nginx:updateSite", (_, originalName: string, site: any) =>
|
ipcMain.handle("nginx:updateSite", (_, originalName: string, site: any) =>
|
||||||
nginxManager.updateSite(originalName, site),
|
nginxManager.updateSite(originalName, site)
|
||||||
);
|
);
|
||||||
ipcMain.handle("nginx:enableSite", (_, name: string) =>
|
ipcMain.handle("nginx:enableSite", (_, name: string) =>
|
||||||
nginxManager.enableSite(name),
|
nginxManager.enableSite(name)
|
||||||
);
|
);
|
||||||
ipcMain.handle("nginx:disableSite", (_, name: string) =>
|
ipcMain.handle("nginx:disableSite", (_, name: string) =>
|
||||||
nginxManager.disableSite(name),
|
nginxManager.disableSite(name)
|
||||||
);
|
);
|
||||||
ipcMain.handle("nginx:generateLaravelConfig", (_, site: any) =>
|
ipcMain.handle("nginx:generateLaravelConfig", (_, site: any) =>
|
||||||
nginxManager.generateLaravelConfig(site),
|
nginxManager.generateLaravelConfig(site)
|
||||||
);
|
);
|
||||||
ipcMain.handle("nginx:requestSSL", (_, domain: string, email: string) =>
|
ipcMain.handle("nginx:requestSSL", (_, domain: string, email: string) =>
|
||||||
nginxManager.requestSSLCertificate(domain, email),
|
nginxManager.requestSSLCertificate(domain, email)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== Redis 管理 ====================
|
// ==================== Redis 管理 ====================
|
||||||
ipcMain.handle("redis:getVersions", () => redisManager.getInstalledVersions());
|
ipcMain.handle("redis:getVersions", () => redisManager.getInstalledVersions());
|
||||||
ipcMain.handle("redis:getAvailableVersions", () =>
|
ipcMain.handle("redis:getAvailableVersions", () =>
|
||||||
redisManager.getAvailableVersions(),
|
redisManager.getAvailableVersions()
|
||||||
);
|
);
|
||||||
ipcMain.handle("redis:install", (_, version: string) =>
|
ipcMain.handle("redis:install", (_, version: string) =>
|
||||||
redisManager.install(version),
|
redisManager.install(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("redis:uninstall", (_, version: string) =>
|
ipcMain.handle("redis:uninstall", (_, version: string) =>
|
||||||
redisManager.uninstall(version),
|
redisManager.uninstall(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("redis:start", () => redisManager.start());
|
ipcMain.handle("redis:start", () => redisManager.start());
|
||||||
ipcMain.handle("redis:stop", () => redisManager.stop());
|
ipcMain.handle("redis:stop", () => redisManager.stop());
|
||||||
@ -488,131 +479,127 @@ ipcMain.handle("redis:restart", () => redisManager.restart());
|
|||||||
ipcMain.handle("redis:getStatus", () => redisManager.getStatus());
|
ipcMain.handle("redis:getStatus", () => redisManager.getStatus());
|
||||||
ipcMain.handle("redis:getConfig", () => redisManager.getConfig());
|
ipcMain.handle("redis:getConfig", () => redisManager.getConfig());
|
||||||
ipcMain.handle("redis:saveConfig", (_, config: string) =>
|
ipcMain.handle("redis:saveConfig", (_, config: string) =>
|
||||||
redisManager.saveConfig(config),
|
redisManager.saveConfig(config)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== Node.js 管理 ====================
|
// ==================== Node.js 管理 ====================
|
||||||
ipcMain.handle("node:getVersions", () => nodeManager.getInstalledVersions());
|
ipcMain.handle("node:getVersions", () => nodeManager.getInstalledVersions());
|
||||||
ipcMain.handle("node:getAvailableVersions", () =>
|
ipcMain.handle("node:getAvailableVersions", () =>
|
||||||
nodeManager.getAvailableVersions(),
|
nodeManager.getAvailableVersions()
|
||||||
);
|
);
|
||||||
ipcMain.handle("node:install", (_, version: string, downloadUrl: string) =>
|
ipcMain.handle("node:install", (_, version: string, downloadUrl: string) =>
|
||||||
nodeManager.install(version, downloadUrl),
|
nodeManager.install(version, downloadUrl)
|
||||||
);
|
);
|
||||||
ipcMain.handle("node:uninstall", (_, version: string) =>
|
ipcMain.handle("node:uninstall", (_, version: string) =>
|
||||||
nodeManager.uninstall(version),
|
nodeManager.uninstall(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("node:setActive", (_, version: string) =>
|
ipcMain.handle("node:setActive", (_, version: string) =>
|
||||||
nodeManager.setActive(version),
|
nodeManager.setActive(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("node:getInfo", (_, version: string) =>
|
ipcMain.handle("node:getInfo", (_, version: string) =>
|
||||||
nodeManager.getNodeInfo(version),
|
nodeManager.getNodeInfo(version)
|
||||||
);
|
|
||||||
|
|
||||||
// ==================== Go 管理 ====================
|
|
||||||
ipcMain.handle("go:getVersions", () => goManager.getInstalledVersions());
|
|
||||||
ipcMain.handle("go:getAvailableVersions", () =>
|
|
||||||
goManager.getAvailableVersions(),
|
|
||||||
);
|
|
||||||
ipcMain.handle("go:install", (_, version: string, downloadUrl: string) =>
|
|
||||||
goManager.install(version, downloadUrl),
|
|
||||||
);
|
|
||||||
ipcMain.handle("go:uninstall", (_, version: string) =>
|
|
||||||
goManager.uninstall(version),
|
|
||||||
);
|
|
||||||
ipcMain.handle("go:setActive", (_, version: string) =>
|
|
||||||
goManager.setActive(version),
|
|
||||||
);
|
|
||||||
ipcMain.handle("go:getInfo", (_, version: string) =>
|
|
||||||
goManager.getGoInfo(version),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== 服务管理 ====================
|
// ==================== 服务管理 ====================
|
||||||
ipcMain.handle("service:getAll", () => serviceManager.getAllServices());
|
ipcMain.handle("service:getAll", () => serviceManager.getAllServices());
|
||||||
ipcMain.handle("service:setAutoStart", (_, service: string, enabled: boolean) =>
|
ipcMain.handle("service:setAutoStart", (_, service: string, enabled: boolean) =>
|
||||||
serviceManager.setAutoStart(service, enabled),
|
serviceManager.setAutoStart(service, enabled)
|
||||||
);
|
);
|
||||||
ipcMain.handle("service:getAutoStart", (_, service: string) =>
|
ipcMain.handle("service:getAutoStart", (_, service: string) =>
|
||||||
serviceManager.getAutoStart(service),
|
serviceManager.getAutoStart(service)
|
||||||
);
|
);
|
||||||
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 管理 - 支持多版本
|
// PHP-CGI 管理 - 支持多版本
|
||||||
ipcMain.handle("service:getPhpCgiStatus", () =>
|
ipcMain.handle("service:getPhpCgiStatus", () => serviceManager.getPhpCgiStatus());
|
||||||
serviceManager.getPhpCgiStatus(),
|
|
||||||
);
|
|
||||||
ipcMain.handle("service:startPhpCgi", () => serviceManager.startPhpCgi());
|
ipcMain.handle("service:startPhpCgi", () => serviceManager.startPhpCgi());
|
||||||
ipcMain.handle("service:stopPhpCgi", () => serviceManager.stopPhpCgi());
|
ipcMain.handle("service:stopPhpCgi", () => serviceManager.stopPhpCgi());
|
||||||
ipcMain.handle("service:startAllPhpCgi", () => serviceManager.startAllPhpCgi());
|
ipcMain.handle("service:startAllPhpCgi", () => serviceManager.startAllPhpCgi());
|
||||||
ipcMain.handle("service:stopAllPhpCgi", () => serviceManager.stopAllPhpCgi());
|
ipcMain.handle("service:stopAllPhpCgi", () => serviceManager.stopAllPhpCgi());
|
||||||
ipcMain.handle("service:startPhpCgiVersion", (_, version: string) =>
|
ipcMain.handle("service:startPhpCgiVersion", (_, version: string) => serviceManager.startPhpCgiVersion(version));
|
||||||
serviceManager.startPhpCgiVersion(version),
|
ipcMain.handle("service:stopPhpCgiVersion", (_, version: string) => serviceManager.stopPhpCgiVersion(version));
|
||||||
);
|
ipcMain.handle("service:getPhpCgiPort", (_, version: string) => serviceManager.getPhpCgiPort(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());
|
||||||
ipcMain.handle("hosts:add", (_, domain: string, ip: string) =>
|
ipcMain.handle("hosts:add", (_, domain: string, ip: string) =>
|
||||||
hostsManager.addHost(domain, ip),
|
hostsManager.addHost(domain, ip)
|
||||||
);
|
);
|
||||||
ipcMain.handle("hosts:remove", (_, domain: string) =>
|
ipcMain.handle("hosts:remove", (_, domain: string) =>
|
||||||
hostsManager.removeHost(domain),
|
hostsManager.removeHost(domain)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== Git 管理 ====================
|
// ==================== Git 管理 ====================
|
||||||
ipcMain.handle("git:getVersions", () => gitManager.getInstalledVersions());
|
ipcMain.handle("git:getVersions", () => gitManager.getInstalledVersions());
|
||||||
ipcMain.handle("git:getAvailableVersions", () =>
|
ipcMain.handle("git:getAvailableVersions", () =>
|
||||||
gitManager.getAvailableVersions(),
|
gitManager.getAvailableVersions()
|
||||||
);
|
);
|
||||||
ipcMain.handle("git:install", (_, version: string) =>
|
ipcMain.handle("git:install", (_, version: string) =>
|
||||||
gitManager.install(version),
|
gitManager.install(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("git:uninstall", () => gitManager.uninstall());
|
ipcMain.handle("git:uninstall", () => gitManager.uninstall());
|
||||||
ipcMain.handle("git:checkSystem", () => gitManager.checkSystemGit());
|
ipcMain.handle("git:checkSystem", () => gitManager.checkSystemGit());
|
||||||
ipcMain.handle("git:getConfig", () => gitManager.getGitConfig());
|
ipcMain.handle("git:getConfig", () => gitManager.getGitConfig());
|
||||||
ipcMain.handle("git:setConfig", (_, name: string, email: string) =>
|
ipcMain.handle("git:setConfig", (_, name: string, email: string) =>
|
||||||
gitManager.setGitConfig(name, email),
|
gitManager.setGitConfig(name, email)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== Python 管理 ====================
|
// ==================== Python 管理 ====================
|
||||||
ipcMain.handle("python:getVersions", () =>
|
ipcMain.handle("python:getVersions", () => pythonManager.getInstalledVersions());
|
||||||
pythonManager.getInstalledVersions(),
|
|
||||||
);
|
|
||||||
ipcMain.handle("python:getAvailableVersions", () =>
|
ipcMain.handle("python:getAvailableVersions", () =>
|
||||||
pythonManager.getAvailableVersions(),
|
pythonManager.getAvailableVersions()
|
||||||
);
|
);
|
||||||
ipcMain.handle("python:install", (_, version: string) =>
|
ipcMain.handle("python:install", (_, version: string) =>
|
||||||
pythonManager.install(version),
|
pythonManager.install(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("python:uninstall", (_, version: string) =>
|
ipcMain.handle("python:uninstall", (_, version: string) =>
|
||||||
pythonManager.uninstall(version),
|
pythonManager.uninstall(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("python:setActive", (_, version: string) =>
|
ipcMain.handle("python:setActive", (_, version: string) =>
|
||||||
pythonManager.setActive(version),
|
pythonManager.setActive(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle("python:checkSystem", () => pythonManager.checkSystemPython());
|
ipcMain.handle("python:checkSystem", () => pythonManager.checkSystemPython());
|
||||||
ipcMain.handle("python:getPipInfo", (_, version: string) =>
|
ipcMain.handle("python:getPipInfo", (_, version: string) =>
|
||||||
pythonManager.getPipInfo(version),
|
pythonManager.getPipInfo(version)
|
||||||
);
|
);
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"python:installPackage",
|
"python:installPackage",
|
||||||
(_, version: string, packageName: string) =>
|
(_, version: string, packageName: string) =>
|
||||||
pythonManager.installPackage(version, packageName),
|
pythonManager.installPackage(version, packageName)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== Go 管理 ====================
|
||||||
|
ipcMain.handle("go:getVersions", () => goManager.getInstalledVersions());
|
||||||
|
ipcMain.handle("go:getAvailableVersions", () =>
|
||||||
|
goManager.getAvailableVersions()
|
||||||
|
);
|
||||||
|
ipcMain.handle("go:install", (_, version: string, downloadUrl: string, expectedSha256?: string) =>
|
||||||
|
goManager.install(version, downloadUrl, expectedSha256)
|
||||||
|
);
|
||||||
|
ipcMain.handle("go:uninstall", (_, version: string) =>
|
||||||
|
goManager.uninstall(version)
|
||||||
|
);
|
||||||
|
ipcMain.handle("go:setActive", (_, version: string) =>
|
||||||
|
goManager.setActive(version)
|
||||||
|
);
|
||||||
|
ipcMain.handle("go:validateInstallation", (_, version: string) =>
|
||||||
|
goManager.validateInstallation(version)
|
||||||
|
);
|
||||||
|
ipcMain.handle("go:getInfo", (_, version: string) =>
|
||||||
|
goManager.getGoInfo(version)
|
||||||
|
);
|
||||||
|
ipcMain.handle("go:detectSystemVersion", () =>
|
||||||
|
goManager.detectSystemGoVersion()
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== 配置管理 ====================
|
// ==================== 配置管理 ====================
|
||||||
ipcMain.handle("config:get", (_, key: string) => configStore.get(key));
|
ipcMain.handle("config:get", (_, key: string) => configStore.get(key));
|
||||||
ipcMain.handle("config:set", (_, key: string, value: any) =>
|
ipcMain.handle("config:set", (_, key: string, value: any) =>
|
||||||
configStore.set(key, value),
|
configStore.set(key, value)
|
||||||
);
|
);
|
||||||
ipcMain.handle("config:getBasePath", () => configStore.getBasePath());
|
ipcMain.handle("config:getBasePath", () => configStore.getBasePath());
|
||||||
ipcMain.handle("config:setBasePath", (_, path: string) =>
|
ipcMain.handle("config:setBasePath", (_, path: string) =>
|
||||||
configStore.setBasePath(path),
|
configStore.setBasePath(path)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ==================== 应用设置 ====================
|
// ==================== 应用设置 ====================
|
||||||
@ -739,7 +726,7 @@ ipcMain.handle("app:getVersion", async () => {
|
|||||||
version,
|
version,
|
||||||
buildTime,
|
buildTime,
|
||||||
buildDate,
|
buildDate,
|
||||||
isPackaged: app.isPackaged,
|
isPackaged: app.isPackaged
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -763,13 +750,9 @@ ipcMain.handle("app:quit", () => {
|
|||||||
// ==================== 日志管理 ====================
|
// ==================== 日志管理 ====================
|
||||||
ipcMain.handle("log:getFiles", () => logManager.getLogFiles());
|
ipcMain.handle("log:getFiles", () => logManager.getLogFiles());
|
||||||
ipcMain.handle("log:read", (_, logPath: string, lines?: number) =>
|
ipcMain.handle("log:read", (_, logPath: string, lines?: number) =>
|
||||||
logManager.readLog(logPath, lines),
|
logManager.readLog(logPath, lines)
|
||||||
);
|
);
|
||||||
ipcMain.handle("log:clear", (_, logPath: string) =>
|
ipcMain.handle("log:clear", (_, logPath: string) => logManager.clearLog(logPath));
|
||||||
logManager.clearLog(logPath),
|
ipcMain.handle("log:getDirectory", (_, type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string) =>
|
||||||
);
|
logManager.getLogDirectory(type, version)
|
||||||
ipcMain.handle(
|
|
||||||
"log:getDirectory",
|
|
||||||
(_, type: "nginx" | "php" | "mysql" | "sites", version?: string) =>
|
|
||||||
logManager.getLogDirectory(type, version),
|
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,318 +1,233 @@
|
|||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
|
||||||
// 暴露安全的 API 到渲染进程
|
// 暴露安全的 API 到渲染进程
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
// 窗口控制
|
// 窗口控制
|
||||||
minimize: () => ipcRenderer.invoke("window:minimize"),
|
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||||
maximize: () => ipcRenderer.invoke("window:maximize"),
|
maximize: () => ipcRenderer.invoke('window:maximize'),
|
||||||
close: () => ipcRenderer.invoke("window:close"),
|
close: () => ipcRenderer.invoke('window:close'),
|
||||||
|
|
||||||
// Shell
|
// Shell
|
||||||
openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
|
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
|
||||||
openPath: (path: string) => ipcRenderer.invoke("shell:openPath", path),
|
openPath: (path: string) => ipcRenderer.invoke('shell:openPath', path),
|
||||||
|
|
||||||
// Dialog
|
// Dialog
|
||||||
selectDirectory: () => ipcRenderer.invoke("dialog:selectDirectory"),
|
selectDirectory: () => ipcRenderer.invoke('dialog:selectDirectory'),
|
||||||
|
|
||||||
// 下载进度监听
|
// 下载进度监听
|
||||||
onDownloadProgress: (
|
onDownloadProgress: (callback: (data: { type: string; progress: number; downloaded: number; total: number }) => void) => {
|
||||||
callback: (data: {
|
ipcRenderer.on('download-progress', (_, data) => callback(data))
|
||||||
type: string;
|
|
||||||
progress: number;
|
|
||||||
downloaded: number;
|
|
||||||
total: number;
|
|
||||||
}) => void,
|
|
||||||
) => {
|
|
||||||
ipcRenderer.on("download-progress", (_, data) => callback(data));
|
|
||||||
},
|
},
|
||||||
removeDownloadProgressListener: () => {
|
removeDownloadProgressListener: () => {
|
||||||
ipcRenderer.removeAllListeners("download-progress");
|
ipcRenderer.removeAllListeners('download-progress')
|
||||||
},
|
},
|
||||||
|
|
||||||
// PHP 管理
|
// PHP 管理
|
||||||
php: {
|
php: {
|
||||||
getVersions: () => ipcRenderer.invoke("php:getVersions"),
|
getVersions: () => ipcRenderer.invoke('php:getVersions'),
|
||||||
getAvailableVersions: () => ipcRenderer.invoke("php:getAvailableVersions"),
|
getAvailableVersions: () => ipcRenderer.invoke('php:getAvailableVersions'),
|
||||||
install: (version: string) => ipcRenderer.invoke("php:install", version),
|
install: (version: string) => ipcRenderer.invoke('php:install', version),
|
||||||
uninstall: (version: string) =>
|
uninstall: (version: string) => ipcRenderer.invoke('php:uninstall', version),
|
||||||
ipcRenderer.invoke("php:uninstall", version),
|
setActive: (version: string) => ipcRenderer.invoke('php:setActive', version),
|
||||||
setActive: (version: string) =>
|
getExtensions: (version: string) => ipcRenderer.invoke('php:getExtensions', version),
|
||||||
ipcRenderer.invoke("php:setActive", version),
|
openExtensionDir: (version: string) => ipcRenderer.invoke('php:openExtensionDir', version),
|
||||||
getExtensions: (version: string) =>
|
getAvailableExtensions: (version: string, searchKeyword?: string) => ipcRenderer.invoke('php:getAvailableExtensions', version, searchKeyword),
|
||||||
ipcRenderer.invoke("php:getExtensions", version),
|
enableExtension: (version: string, ext: string) => ipcRenderer.invoke('php:enableExtension', version, ext),
|
||||||
openExtensionDir: (version: string) =>
|
disableExtension: (version: string, ext: string) => ipcRenderer.invoke('php:disableExtension', version, ext),
|
||||||
ipcRenderer.invoke("php:openExtensionDir", version),
|
installExtension: (version: string, ext: string, downloadUrl?: string, packageName?: string) => ipcRenderer.invoke('php:installExtension', version, ext, downloadUrl, packageName),
|
||||||
getAvailableExtensions: (version: string, searchKeyword?: string) =>
|
getConfig: (version: string) => ipcRenderer.invoke('php:getConfig', version),
|
||||||
ipcRenderer.invoke("php:getAvailableExtensions", version, searchKeyword),
|
saveConfig: (version: string, config: string) => ipcRenderer.invoke('php:saveConfig', version, config)
|
||||||
enableExtension: (version: string, ext: string) =>
|
|
||||||
ipcRenderer.invoke("php:enableExtension", version, ext),
|
|
||||||
disableExtension: (version: string, ext: string) =>
|
|
||||||
ipcRenderer.invoke("php:disableExtension", version, ext),
|
|
||||||
installExtension: (
|
|
||||||
version: string,
|
|
||||||
ext: string,
|
|
||||||
downloadUrl?: string,
|
|
||||||
packageName?: string,
|
|
||||||
) =>
|
|
||||||
ipcRenderer.invoke(
|
|
||||||
"php:installExtension",
|
|
||||||
version,
|
|
||||||
ext,
|
|
||||||
downloadUrl,
|
|
||||||
packageName,
|
|
||||||
),
|
|
||||||
getConfig: (version: string) =>
|
|
||||||
ipcRenderer.invoke("php:getConfig", version),
|
|
||||||
saveConfig: (version: string, config: string) =>
|
|
||||||
ipcRenderer.invoke("php:saveConfig", version, config),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Composer 管理
|
// Composer 管理
|
||||||
composer: {
|
composer: {
|
||||||
getStatus: () => ipcRenderer.invoke("composer:getStatus"),
|
getStatus: () => ipcRenderer.invoke('composer:getStatus'),
|
||||||
install: () => ipcRenderer.invoke("composer:install"),
|
install: () => ipcRenderer.invoke('composer:install'),
|
||||||
uninstall: () => ipcRenderer.invoke("composer:uninstall"),
|
uninstall: () => ipcRenderer.invoke('composer:uninstall'),
|
||||||
setMirror: (mirror: string) =>
|
setMirror: (mirror: string) => ipcRenderer.invoke('composer:setMirror', mirror),
|
||||||
ipcRenderer.invoke("composer:setMirror", mirror),
|
createLaravelProject: (projectName: string, targetDir: string) => ipcRenderer.invoke('composer:createLaravelProject', projectName, targetDir)
|
||||||
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'),
|
||||||
getAvailableVersions: () =>
|
getAvailableVersions: () => ipcRenderer.invoke('mysql:getAvailableVersions'),
|
||||||
ipcRenderer.invoke("mysql:getAvailableVersions"),
|
install: (version: string) => ipcRenderer.invoke('mysql:install', version),
|
||||||
install: (version: string) => ipcRenderer.invoke("mysql:install", version),
|
uninstall: (version: string) => ipcRenderer.invoke('mysql:uninstall', version),
|
||||||
uninstall: (version: string) =>
|
start: (version: string) => ipcRenderer.invoke('mysql:start', version),
|
||||||
ipcRenderer.invoke("mysql:uninstall", version),
|
stop: (version: string) => ipcRenderer.invoke('mysql:stop', version),
|
||||||
start: (version: string) => ipcRenderer.invoke("mysql:start", version),
|
restart: (version: string) => ipcRenderer.invoke('mysql:restart', version),
|
||||||
stop: (version: string) => ipcRenderer.invoke("mysql:stop", version),
|
getStatus: (version: string) => ipcRenderer.invoke('mysql:getStatus', version),
|
||||||
restart: (version: string) => ipcRenderer.invoke("mysql:restart", version),
|
changePassword: (version: string, newPassword: string, currentPassword?: string) => ipcRenderer.invoke('mysql:changePassword', version, newPassword, currentPassword),
|
||||||
getStatus: (version: string) =>
|
getConfig: (version: string) => ipcRenderer.invoke('mysql:getConfig', version),
|
||||||
ipcRenderer.invoke("mysql:getStatus", version),
|
saveConfig: (version: string, config: string) => ipcRenderer.invoke('mysql:saveConfig', version, config)
|
||||||
changePassword: (
|
|
||||||
version: string,
|
|
||||||
newPassword: string,
|
|
||||||
currentPassword?: string,
|
|
||||||
) =>
|
|
||||||
ipcRenderer.invoke(
|
|
||||||
"mysql:changePassword",
|
|
||||||
version,
|
|
||||||
newPassword,
|
|
||||||
currentPassword,
|
|
||||||
),
|
|
||||||
getConfig: (version: string) =>
|
|
||||||
ipcRenderer.invoke("mysql:getConfig", version),
|
|
||||||
saveConfig: (version: string, config: string) =>
|
|
||||||
ipcRenderer.invoke("mysql:saveConfig", version, config),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Nginx 管理
|
// Nginx 管理
|
||||||
nginx: {
|
nginx: {
|
||||||
getVersions: () => ipcRenderer.invoke("nginx:getVersions"),
|
getVersions: () => ipcRenderer.invoke('nginx:getVersions'),
|
||||||
getAvailableVersions: () =>
|
getAvailableVersions: () => ipcRenderer.invoke('nginx:getAvailableVersions'),
|
||||||
ipcRenderer.invoke("nginx:getAvailableVersions"),
|
install: (version: string) => ipcRenderer.invoke('nginx:install', version),
|
||||||
install: (version: string) => ipcRenderer.invoke("nginx:install", version),
|
uninstall: (version: string) => ipcRenderer.invoke('nginx:uninstall', version),
|
||||||
uninstall: (version: string) =>
|
start: () => ipcRenderer.invoke('nginx:start'),
|
||||||
ipcRenderer.invoke("nginx:uninstall", version),
|
stop: () => ipcRenderer.invoke('nginx:stop'),
|
||||||
start: () => ipcRenderer.invoke("nginx:start"),
|
restart: () => ipcRenderer.invoke('nginx:restart'),
|
||||||
stop: () => ipcRenderer.invoke("nginx:stop"),
|
reload: () => ipcRenderer.invoke('nginx:reload'),
|
||||||
restart: () => ipcRenderer.invoke("nginx:restart"),
|
getStatus: () => ipcRenderer.invoke('nginx:getStatus'),
|
||||||
reload: () => ipcRenderer.invoke("nginx:reload"),
|
getConfig: () => ipcRenderer.invoke('nginx:getConfig'),
|
||||||
getStatus: () => ipcRenderer.invoke("nginx:getStatus"),
|
saveConfig: (config: string) => ipcRenderer.invoke('nginx:saveConfig', config),
|
||||||
getConfig: () => ipcRenderer.invoke("nginx:getConfig"),
|
getSites: () => ipcRenderer.invoke('nginx:getSites'),
|
||||||
saveConfig: (config: string) =>
|
addSite: (site: any) => ipcRenderer.invoke('nginx:addSite', site),
|
||||||
ipcRenderer.invoke("nginx:saveConfig", config),
|
removeSite: (name: string) => ipcRenderer.invoke('nginx:removeSite', name),
|
||||||
getSites: () => ipcRenderer.invoke("nginx:getSites"),
|
updateSite: (originalName: string, site: any) => ipcRenderer.invoke('nginx:updateSite', originalName, site),
|
||||||
addSite: (site: any) => ipcRenderer.invoke("nginx:addSite", site),
|
enableSite: (name: string) => ipcRenderer.invoke('nginx:enableSite', name),
|
||||||
removeSite: (name: string) => ipcRenderer.invoke("nginx:removeSite", name),
|
disableSite: (name: string) => ipcRenderer.invoke('nginx:disableSite', name),
|
||||||
updateSite: (originalName: string, site: any) =>
|
generateLaravelConfig: (site: any) => ipcRenderer.invoke('nginx:generateLaravelConfig', site),
|
||||||
ipcRenderer.invoke("nginx:updateSite", originalName, site),
|
requestSSL: (domain: string, email: string) => ipcRenderer.invoke('nginx:requestSSL', domain, email)
|
||||||
enableSite: (name: string) => ipcRenderer.invoke("nginx:enableSite", name),
|
|
||||||
disableSite: (name: string) =>
|
|
||||||
ipcRenderer.invoke("nginx:disableSite", name),
|
|
||||||
generateLaravelConfig: (site: any) =>
|
|
||||||
ipcRenderer.invoke("nginx:generateLaravelConfig", site),
|
|
||||||
requestSSL: (domain: string, email: string) =>
|
|
||||||
ipcRenderer.invoke("nginx:requestSSL", domain, email),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Redis 管理
|
// Redis 管理
|
||||||
redis: {
|
redis: {
|
||||||
getVersions: () => ipcRenderer.invoke("redis:getVersions"),
|
getVersions: () => ipcRenderer.invoke('redis:getVersions'),
|
||||||
getAvailableVersions: () =>
|
getAvailableVersions: () => ipcRenderer.invoke('redis:getAvailableVersions'),
|
||||||
ipcRenderer.invoke("redis:getAvailableVersions"),
|
install: (version: string) => ipcRenderer.invoke('redis:install', version),
|
||||||
install: (version: string) => ipcRenderer.invoke("redis:install", version),
|
uninstall: (version: string) => ipcRenderer.invoke('redis:uninstall', version),
|
||||||
uninstall: (version: string) =>
|
start: () => ipcRenderer.invoke('redis:start'),
|
||||||
ipcRenderer.invoke("redis:uninstall", version),
|
stop: () => ipcRenderer.invoke('redis:stop'),
|
||||||
start: () => ipcRenderer.invoke("redis:start"),
|
restart: () => ipcRenderer.invoke('redis:restart'),
|
||||||
stop: () => ipcRenderer.invoke("redis:stop"),
|
getStatus: () => ipcRenderer.invoke('redis:getStatus'),
|
||||||
restart: () => ipcRenderer.invoke("redis:restart"),
|
getConfig: () => ipcRenderer.invoke('redis:getConfig'),
|
||||||
getStatus: () => ipcRenderer.invoke("redis:getStatus"),
|
saveConfig: (config: string) => ipcRenderer.invoke('redis:saveConfig', config)
|
||||||
getConfig: () => ipcRenderer.invoke("redis:getConfig"),
|
|
||||||
saveConfig: (config: string) =>
|
|
||||||
ipcRenderer.invoke("redis:saveConfig", config),
|
|
||||||
},
|
|
||||||
|
|
||||||
// Go 管理
|
|
||||||
go: {
|
|
||||||
getVersions: () => ipcRenderer.invoke("go:getVersions"),
|
|
||||||
getAvailableVersions: () => ipcRenderer.invoke("go:getAvailableVersions"),
|
|
||||||
install: (version: string, downloadUrl: string) =>
|
|
||||||
ipcRenderer.invoke("go:install", version, downloadUrl),
|
|
||||||
uninstall: (version: string) => ipcRenderer.invoke("go:uninstall", version),
|
|
||||||
setActive: (version: string) => ipcRenderer.invoke("go:setActive", version),
|
|
||||||
getInfo: (version: string) => ipcRenderer.invoke("go:getInfo", version),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Node.js 管理
|
// Node.js 管理
|
||||||
node: {
|
node: {
|
||||||
getVersions: () => ipcRenderer.invoke("node:getVersions"),
|
getVersions: () => ipcRenderer.invoke('node:getVersions'),
|
||||||
getAvailableVersions: () => ipcRenderer.invoke("node:getAvailableVersions"),
|
getAvailableVersions: () => ipcRenderer.invoke('node:getAvailableVersions'),
|
||||||
install: (version: string, downloadUrl: string) =>
|
install: (version: string, downloadUrl: string) => ipcRenderer.invoke('node:install', version, downloadUrl),
|
||||||
ipcRenderer.invoke("node:install", version, downloadUrl),
|
uninstall: (version: string) => ipcRenderer.invoke('node:uninstall', version),
|
||||||
uninstall: (version: string) =>
|
setActive: (version: string) => ipcRenderer.invoke('node:setActive', version),
|
||||||
ipcRenderer.invoke("node:uninstall", version),
|
getInfo: (version: string) => ipcRenderer.invoke('node:getInfo', version)
|
||||||
setActive: (version: string) =>
|
|
||||||
ipcRenderer.invoke("node:setActive", version),
|
|
||||||
getInfo: (version: string) => ipcRenderer.invoke("node:getInfo", version),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Git 管理
|
// Git 管理
|
||||||
git: {
|
git: {
|
||||||
getVersions: () => ipcRenderer.invoke("git:getVersions"),
|
getVersions: () => ipcRenderer.invoke('git:getVersions'),
|
||||||
getAvailableVersions: () => ipcRenderer.invoke("git:getAvailableVersions"),
|
getAvailableVersions: () => ipcRenderer.invoke('git:getAvailableVersions'),
|
||||||
install: (version: string) => ipcRenderer.invoke("git:install", version),
|
install: (version: string) => ipcRenderer.invoke('git:install', version),
|
||||||
uninstall: () => ipcRenderer.invoke("git:uninstall"),
|
uninstall: () => ipcRenderer.invoke('git:uninstall'),
|
||||||
checkSystem: () => ipcRenderer.invoke("git:checkSystem"),
|
checkSystem: () => ipcRenderer.invoke('git:checkSystem'),
|
||||||
getConfig: () => ipcRenderer.invoke("git:getConfig"),
|
getConfig: () => ipcRenderer.invoke('git:getConfig'),
|
||||||
setConfig: (name: string, email: string) =>
|
setConfig: (name: string, email: string) => ipcRenderer.invoke('git:setConfig', name, email)
|
||||||
ipcRenderer.invoke("git:setConfig", name, email),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Python 管理
|
// Python 管理
|
||||||
python: {
|
python: {
|
||||||
getVersions: () => ipcRenderer.invoke("python:getVersions"),
|
getVersions: () => ipcRenderer.invoke('python:getVersions'),
|
||||||
getAvailableVersions: () =>
|
getAvailableVersions: () => ipcRenderer.invoke('python:getAvailableVersions'),
|
||||||
ipcRenderer.invoke("python:getAvailableVersions"),
|
install: (version: string) => ipcRenderer.invoke('python:install', version),
|
||||||
install: (version: string) => ipcRenderer.invoke("python:install", version),
|
uninstall: (version: string) => ipcRenderer.invoke('python:uninstall', version),
|
||||||
uninstall: (version: string) =>
|
setActive: (version: string) => ipcRenderer.invoke('python:setActive', version),
|
||||||
ipcRenderer.invoke("python:uninstall", version),
|
checkSystem: () => ipcRenderer.invoke('python:checkSystem'),
|
||||||
setActive: (version: string) =>
|
getPipInfo: (version: string) => ipcRenderer.invoke('python:getPipInfo', version),
|
||||||
ipcRenderer.invoke("python:setActive", version),
|
installPackage: (version: string, packageName: string) => ipcRenderer.invoke('python:installPackage', version, packageName)
|
||||||
checkSystem: () => ipcRenderer.invoke("python:checkSystem"),
|
},
|
||||||
getPipInfo: (version: string) =>
|
|
||||||
ipcRenderer.invoke("python:getPipInfo", version),
|
// Go 管理
|
||||||
installPackage: (version: string, packageName: string) =>
|
go: {
|
||||||
ipcRenderer.invoke("python:installPackage", version, packageName),
|
getVersions: () => ipcRenderer.invoke('go:getVersions'),
|
||||||
|
getAvailableVersions: () => ipcRenderer.invoke('go:getAvailableVersions'),
|
||||||
|
install: (version: string, downloadUrl: string, expectedSha256?: string) => ipcRenderer.invoke('go:install', version, downloadUrl, expectedSha256),
|
||||||
|
uninstall: (version: string) => ipcRenderer.invoke('go:uninstall', version),
|
||||||
|
setActive: (version: string) => ipcRenderer.invoke('go:setActive', version),
|
||||||
|
validateInstallation: (version: string) => ipcRenderer.invoke('go:validateInstallation', version),
|
||||||
|
getInfo: (version: string) => ipcRenderer.invoke('go:getInfo', version),
|
||||||
|
detectSystemVersion: () => ipcRenderer.invoke('go:detectSystemVersion')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 服务管理
|
// 服务管理
|
||||||
service: {
|
service: {
|
||||||
getAll: () => ipcRenderer.invoke("service:getAll"),
|
getAll: () => ipcRenderer.invoke('service:getAll'),
|
||||||
setAutoStart: (service: string, enabled: boolean) =>
|
setAutoStart: (service: string, enabled: boolean) => ipcRenderer.invoke('service:setAutoStart', service, enabled),
|
||||||
ipcRenderer.invoke("service:setAutoStart", service, enabled),
|
getAutoStart: (service: string) => ipcRenderer.invoke('service:getAutoStart', service),
|
||||||
getAutoStart: (service: string) =>
|
startAll: () => ipcRenderer.invoke('service:startAll'),
|
||||||
ipcRenderer.invoke("service:getAutoStart", service),
|
stopAll: () => ipcRenderer.invoke('service:stopAll'),
|
||||||
startAll: () => ipcRenderer.invoke("service:startAll"),
|
|
||||||
stopAll: () => ipcRenderer.invoke("service:stopAll"),
|
|
||||||
// PHP-CGI 多版本管理
|
// PHP-CGI 多版本管理
|
||||||
getPhpCgiStatus: () => ipcRenderer.invoke("service:getPhpCgiStatus"),
|
getPhpCgiStatus: () => ipcRenderer.invoke('service:getPhpCgiStatus'),
|
||||||
startPhpCgi: () => ipcRenderer.invoke("service:startPhpCgi"),
|
startPhpCgi: () => ipcRenderer.invoke('service:startPhpCgi'),
|
||||||
stopPhpCgi: () => ipcRenderer.invoke("service:stopPhpCgi"),
|
stopPhpCgi: () => ipcRenderer.invoke('service:stopPhpCgi'),
|
||||||
startAllPhpCgi: () => ipcRenderer.invoke("service:startAllPhpCgi"),
|
startAllPhpCgi: () => ipcRenderer.invoke('service:startAllPhpCgi'),
|
||||||
stopAllPhpCgi: () => ipcRenderer.invoke("service:stopAllPhpCgi"),
|
stopAllPhpCgi: () => ipcRenderer.invoke('service:stopAllPhpCgi'),
|
||||||
startPhpCgiVersion: (version: string) =>
|
startPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:startPhpCgiVersion', version),
|
||||||
ipcRenderer.invoke("service:startPhpCgiVersion", version),
|
stopPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:stopPhpCgiVersion', version),
|
||||||
stopPhpCgiVersion: (version: string) =>
|
getPhpCgiPort: (version: string) => ipcRenderer.invoke('service:getPhpCgiPort', version)
|
||||||
ipcRenderer.invoke("service:stopPhpCgiVersion", version),
|
|
||||||
getPhpCgiPort: (version: string) =>
|
|
||||||
ipcRenderer.invoke("service:getPhpCgiPort", version),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Hosts 管理
|
// Hosts 管理
|
||||||
hosts: {
|
hosts: {
|
||||||
get: () => ipcRenderer.invoke("hosts:get"),
|
get: () => ipcRenderer.invoke('hosts:get'),
|
||||||
add: (domain: string, ip: string) =>
|
add: (domain: string, ip: string) => ipcRenderer.invoke('hosts:add', domain, ip),
|
||||||
ipcRenderer.invoke("hosts:add", domain, ip),
|
remove: (domain: string) => ipcRenderer.invoke('hosts:remove', domain)
|
||||||
remove: (domain: string) => ipcRenderer.invoke("hosts:remove", domain),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 配置管理
|
// 配置管理
|
||||||
config: {
|
config: {
|
||||||
get: (key: string) => ipcRenderer.invoke("config:get", key),
|
get: (key: string) => ipcRenderer.invoke('config:get', key),
|
||||||
set: (key: string, value: any) =>
|
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value),
|
||||||
ipcRenderer.invoke("config:set", key, value),
|
getBasePath: () => ipcRenderer.invoke('config:getBasePath'),
|
||||||
getBasePath: () => ipcRenderer.invoke("config:getBasePath"),
|
setBasePath: (path: string) => ipcRenderer.invoke('config:setBasePath', path)
|
||||||
setBasePath: (path: string) =>
|
|
||||||
ipcRenderer.invoke("config:setBasePath", path),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 日志管理
|
// 日志管理
|
||||||
log: {
|
log: {
|
||||||
getFiles: () => ipcRenderer.invoke("log:getFiles"),
|
getFiles: () => ipcRenderer.invoke('log:getFiles'),
|
||||||
read: (logPath: string, lines?: number) =>
|
read: (logPath: string, lines?: number) => ipcRenderer.invoke('log:read', logPath, lines),
|
||||||
ipcRenderer.invoke("log:read", logPath, lines),
|
clear: (logPath: string) => ipcRenderer.invoke('log:clear', logPath),
|
||||||
clear: (logPath: string) => ipcRenderer.invoke("log:clear", logPath),
|
getDirectory: (type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string) =>
|
||||||
getDirectory: (
|
ipcRenderer.invoke('log:getDirectory', type, version)
|
||||||
type: "nginx" | "php" | "mysql" | "sites",
|
|
||||||
version?: string,
|
|
||||||
) => ipcRenderer.invoke("log:getDirectory", type, version),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 应用设置
|
// 应用设置
|
||||||
app: {
|
app: {
|
||||||
setAutoLaunch: (enabled: boolean) =>
|
setAutoLaunch: (enabled: boolean) => ipcRenderer.invoke('app:setAutoLaunch', enabled),
|
||||||
ipcRenderer.invoke("app:setAutoLaunch", enabled),
|
getAutoLaunch: () => ipcRenderer.invoke('app:getAutoLaunch'),
|
||||||
getAutoLaunch: () => ipcRenderer.invoke("app:getAutoLaunch"),
|
setStartMinimized: (enabled: boolean) => ipcRenderer.invoke('app:setStartMinimized', enabled),
|
||||||
setStartMinimized: (enabled: boolean) =>
|
getStartMinimized: () => ipcRenderer.invoke('app:getStartMinimized'),
|
||||||
ipcRenderer.invoke("app:setStartMinimized", enabled),
|
getVersion: () => ipcRenderer.invoke('app:getVersion') as Promise<{ version: string; buildTime: string; buildDate: string; isPackaged: boolean }>,
|
||||||
getStartMinimized: () => ipcRenderer.invoke("app:getStartMinimized"),
|
setAutoStartServices: (enabled: boolean) => ipcRenderer.invoke('app:setAutoStartServices', enabled),
|
||||||
getVersion: () =>
|
getAutoStartServices: () => ipcRenderer.invoke('app:getAutoStartServices'),
|
||||||
ipcRenderer.invoke("app:getVersion") as Promise<{
|
quit: () => ipcRenderer.invoke('app:quit')
|
||||||
version: string;
|
|
||||||
buildTime: string;
|
|
||||||
buildDate: string;
|
|
||||||
isPackaged: boolean;
|
|
||||||
}>,
|
|
||||||
setAutoStartServices: (enabled: boolean) =>
|
|
||||||
ipcRenderer.invoke("app:setAutoStartServices", enabled),
|
|
||||||
getAutoStartServices: () => ipcRenderer.invoke("app:getAutoStartServices"),
|
|
||||||
quit: () => ipcRenderer.invoke("app:quit"),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 监听服务状态变化
|
// 监听服务状态变化
|
||||||
onServiceStatusChanged: (callback: () => void) => {
|
onServiceStatusChanged: (callback: () => void) => {
|
||||||
ipcRenderer.on("service-status-changed", callback);
|
ipcRenderer.on('service-status-changed', callback)
|
||||||
},
|
},
|
||||||
removeServiceStatusChangedListener: (callback: () => void) => {
|
removeServiceStatusChangedListener: (callback: () => void) => {
|
||||||
ipcRenderer.removeListener("service-status-changed", callback);
|
ipcRenderer.removeListener('service-status-changed', callback)
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// 声明 Window 接口扩展
|
// 声明 Window 接口扩展
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI: typeof api;
|
electronAPI: typeof api
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
minimize: () => ipcRenderer.invoke("window:minimize"),
|
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||||
maximize: () => ipcRenderer.invoke("window:maximize"),
|
maximize: () => ipcRenderer.invoke('window:maximize'),
|
||||||
close: () => ipcRenderer.invoke("window:close"),
|
close: () => ipcRenderer.invoke('window:close'),
|
||||||
openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
|
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
|
||||||
openPath: (path: string) => ipcRenderer.invoke("shell:openPath", path),
|
openPath: (path: string) => ipcRenderer.invoke('shell:openPath', path),
|
||||||
php: {} as any,
|
php: {} as any,
|
||||||
mysql: {} as any,
|
mysql: {} as any,
|
||||||
nginx: {} as any,
|
nginx: {} as any,
|
||||||
redis: {} as any,
|
redis: {} as any,
|
||||||
|
go: {} as any,
|
||||||
service: {} as any,
|
service: {} as any,
|
||||||
hosts: {} as any,
|
hosts: {} as any,
|
||||||
config: {} as any,
|
config: {} as any
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
1617
electron/services/GoManager.ts.backup
Normal file
1617
electron/services/GoManager.ts.backup
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1065
electron/services/__tests__/GoManager.test.ts
Normal file
1065
electron/services/__tests__/GoManager.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
16
jest.config.js
Normal file
16
jest.config.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
roots: ['<rootDir>/electron'],
|
||||||
|
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'electron/services/**/*.ts',
|
||||||
|
'!electron/services/**/*.test.ts',
|
||||||
|
'!electron/services/__tests__/**'
|
||||||
|
],
|
||||||
|
moduleFileExtensions: ['ts', 'js'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.ts$': 'ts-jest'
|
||||||
|
},
|
||||||
|
testTimeout: 30000
|
||||||
|
}
|
||||||
3691
package-lock.json
generated
3691
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "phper-dev-manager",
|
"name": "phper-dev-manager",
|
||||||
"version": "1.0.10",
|
"version": "1.0.7",
|
||||||
"description": "PHP开发环境管理器 - 管理PHP、MySQL、Nginx、Redis服务",
|
"description": "PHP开发环境管理器 - 管理PHP、MySQL、Nginx、Redis服务",
|
||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -14,18 +14,24 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"electron:dev": "vite",
|
"electron:dev": "vite",
|
||||||
"electron:build": "node scripts/bump-version.js && vite build && electron-builder",
|
"electron:build": "node scripts/bump-version.js && vite build && electron-builder",
|
||||||
"typecheck": "vue-tsc --noEmit"
|
"typecheck": "vue-tsc --noEmit",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"author": "PHPer",
|
"author": "PHPer",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
"@vitejs/plugin-vue": "^4.5.0",
|
"@vitejs/plugin-vue": "^4.5.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"electron": "^28.0.0",
|
"electron": "^28.0.0",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
|
"fast-check": "^4.5.3",
|
||||||
|
"jest": "^30.2.0",
|
||||||
"rcedit": "^5.0.2",
|
"rcedit": "^5.0.2",
|
||||||
"sass": "^1.69.5",
|
"sass": "^1.69.5",
|
||||||
|
"ts-jest": "^29.4.6",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vite-plugin-electron": "^0.15.5",
|
"vite-plugin-electron": "^0.15.5",
|
||||||
@ -75,9 +81,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "build/icon.ico",
|
"icon": "build/icon.ico",
|
||||||
"executableName": "PHPer开发环境管理器",
|
|
||||||
"requestedExecutionLevel": "requireAdministrator",
|
"requestedExecutionLevel": "requireAdministrator",
|
||||||
"signAndEditExecutable": true
|
"signAndEditExecutable": false
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.10",
|
"version": "1.0.7",
|
||||||
"buildTime": "2026-02-05T01:05:37.725Z",
|
"buildTime": "2025-12-31T07:09:02.287Z",
|
||||||
"buildDate": "2026/2/5"
|
"buildDate": "2025/12/31"
|
||||||
}
|
}
|
||||||
231
src/App.vue
231
src/App.vue
@ -34,16 +34,15 @@
|
|||||||
:key="item.path"
|
:key="item.path"
|
||||||
:to="item.path"
|
:to="item.path"
|
||||||
class="nav-item"
|
class="nav-item"
|
||||||
:class="{ active: $route.path === item.path }">
|
:class="{ active: $route.path === item.path }"
|
||||||
|
>
|
||||||
<el-icon class="nav-icon"><component :is="item.icon" /></el-icon>
|
<el-icon class="nav-icon"><component :is="item.icon" /></el-icon>
|
||||||
<span class="nav-label">{{ item.label }}</span>
|
<span class="nav-label">{{ item.label }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="item.service"
|
v-if="item.service"
|
||||||
class="status-dot"
|
class="status-dot"
|
||||||
:class="{
|
:class="{ running: serviceStatus[item.service as keyof typeof serviceStatus] }"
|
||||||
running:
|
></span>
|
||||||
serviceStatus[item.service as keyof typeof serviceStatus],
|
|
||||||
}"></span>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -74,131 +73,121 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, 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";
|
import { useServiceStore } from './stores/serviceStore'
|
||||||
|
|
||||||
const store = useServiceStore();
|
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)
|
||||||
|
|
||||||
// 缓存的视图列表 - 避免页面切换闪烁
|
// 缓存的视图列表 - 避免页面切换闪烁
|
||||||
const cachedViews = [
|
const cachedViews = [
|
||||||
"Dashboard",
|
'Dashboard',
|
||||||
"PhpManager",
|
'PhpManager',
|
||||||
"MysqlManager",
|
'MysqlManager',
|
||||||
"NginxManager",
|
'NginxManager',
|
||||||
"RedisManager",
|
'RedisManager',
|
||||||
"NodeManager",
|
'NodeManager',
|
||||||
"GoManager",
|
'PythonManager',
|
||||||
"PythonManager",
|
'GoManager',
|
||||||
"GitManager",
|
'GitManager',
|
||||||
"SitesManager",
|
'SitesManager',
|
||||||
"HostsManager",
|
'HostsManager',
|
||||||
"Settings",
|
'Settings'
|
||||||
];
|
]
|
||||||
|
|
||||||
// 从 store 获取服务状态
|
// 从 store 获取服务状态
|
||||||
const serviceStatus = computed(() => ({
|
const serviceStatus = computed(() => ({
|
||||||
nginx: store.serviceStatus.nginx,
|
nginx: store.serviceStatus.nginx,
|
||||||
mysql: store.serviceStatus.mysql,
|
mysql: store.serviceStatus.mysql,
|
||||||
redis: store.serviceStatus.redis,
|
redis: store.serviceStatus.redis
|
||||||
}));
|
}))
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ path: "/", label: "仪表盘", icon: "Odometer", service: null },
|
{ path: '/', label: '仪表盘', icon: 'Odometer', service: null },
|
||||||
{ path: "/php", label: "PHP 管理", icon: "Files", service: null },
|
{ path: '/php', label: 'PHP 管理', icon: 'Files', service: null },
|
||||||
{ path: "/mysql", label: "MySQL 管理", icon: "Coin", service: "mysql" },
|
{ path: '/mysql', label: 'MySQL 管理', icon: 'Coin', service: 'mysql' },
|
||||||
{
|
{ path: '/nginx', label: 'Nginx 管理', icon: 'Connection', service: 'nginx' },
|
||||||
path: "/nginx",
|
{ path: '/redis', label: 'Redis 管理', icon: 'Grid', service: 'redis' },
|
||||||
label: "Nginx 管理",
|
{ path: '/nodejs', label: 'Node.js 管理', icon: 'Promotion', service: null },
|
||||||
icon: "Connection",
|
{ path: '/python', label: 'Python 管理', icon: 'Platform', service: null },
|
||||||
service: "nginx",
|
{ path: '/go', label: 'Go 管理', icon: 'Box', service: null },
|
||||||
},
|
{ path: '/git', label: 'Git 管理', icon: 'Share', service: null },
|
||||||
{ path: "/redis", label: "Redis 管理", icon: "Grid", service: "redis" },
|
{ path: '/sites', label: '站点管理', icon: 'Monitor', service: null },
|
||||||
{
|
{ path: '/hosts', label: 'Hosts 管理', icon: 'Document', service: null },
|
||||||
path: "/nodejs",
|
{ path: '/settings', label: '设置', icon: 'Setting', service: null }
|
||||||
label: "Node.js 管理",
|
]
|
||||||
icon: "Promotion",
|
|
||||||
service: null,
|
|
||||||
},
|
|
||||||
{ path: "/go", label: "Go 管理", icon: "Aim", service: null },
|
|
||||||
{ path: "/python", label: "Python 管理", icon: "Platform", service: null },
|
|
||||||
{ path: "/git", label: "Git 管理", icon: "Share", service: null },
|
|
||||||
{ path: "/sites", label: "站点管理", icon: "Monitor", service: null },
|
|
||||||
{ path: "/hosts", label: "Hosts 管理", icon: "Document", service: null },
|
|
||||||
{ path: "/settings", label: "设置", icon: "Setting", service: null },
|
|
||||||
];
|
|
||||||
|
|
||||||
let statusInterval: ReturnType<typeof setInterval> | null = null;
|
let statusInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
// 窗口控制
|
// 窗口控制
|
||||||
const minimize = () => window.electronAPI?.minimize();
|
const minimize = () => window.electronAPI?.minimize()
|
||||||
const maximize = () => window.electronAPI?.maximize();
|
const maximize = () => window.electronAPI?.maximize()
|
||||||
const close = () => window.electronAPI?.close();
|
const close = () => window.electronAPI?.close()
|
||||||
|
|
||||||
// 主题切换
|
// 主题切换
|
||||||
const toggleDark = () => {
|
const toggleDark = () => {
|
||||||
isDark.value = !isDark.value;
|
isDark.value = !isDark.value
|
||||||
document.documentElement.classList.toggle("dark", isDark.value);
|
document.documentElement.classList.toggle('dark', isDark.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
// 启动所有服务
|
// 启动所有服务
|
||||||
const startAll = async () => {
|
const startAll = async () => {
|
||||||
startingAll.value = true;
|
startingAll.value = true
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI?.service.startAll();
|
const result = await window.electronAPI?.service.startAll()
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
ElMessage.success(result.message);
|
ElMessage.success(result.message)
|
||||||
// 延迟刷新状态,等待服务启动
|
// 延迟刷新状态,等待服务启动
|
||||||
setTimeout(() => store.refreshServiceStatus(), 2000);
|
setTimeout(() => store.refreshServiceStatus(), 2000)
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || "启动失败");
|
ElMessage.error(result?.message || '启动失败')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error.message);
|
ElMessage.error(error.message)
|
||||||
} finally {
|
} finally {
|
||||||
startingAll.value = false;
|
startingAll.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// 停止所有服务
|
// 停止所有服务
|
||||||
const stopAll = async () => {
|
const stopAll = async () => {
|
||||||
stoppingAll.value = true;
|
stoppingAll.value = true
|
||||||
try {
|
try {
|
||||||
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 store.refreshServiceStatus();
|
await store.refreshServiceStatus()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || "停止失败");
|
ElMessage.error(result?.message || '停止失败')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error.message);
|
ElMessage.error(error.message)
|
||||||
} finally {
|
} finally {
|
||||||
stoppingAll.value = false;
|
stoppingAll.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.documentElement.classList.add("dark");
|
document.documentElement.classList.add('dark')
|
||||||
// 初始化加载所有状态
|
// 初始化加载所有状态
|
||||||
store.refreshAll();
|
store.refreshAll()
|
||||||
// 每 5 秒刷新一次状态
|
// 每 5 秒刷新一次状态
|
||||||
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000);
|
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000)
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (statusInterval) {
|
if (statusInterval) {
|
||||||
clearInterval(statusInterval);
|
clearInterval(statusInterval)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.app-container {
|
.app-container {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -206,9 +195,9 @@
|
|||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-bar {
|
.title-bar {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -217,15 +206,15 @@
|
|||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-bar-left {
|
.title-bar-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-logo {
|
.app-logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@ -239,17 +228,17 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title-bar-right {
|
.title-bar-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-btn {
|
.title-btn {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -271,31 +260,31 @@
|
|||||||
background: #e81123;
|
background: #e81123;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-container {
|
.main-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 220px;
|
width: 220px;
|
||||||
background: var(--bg-sidebar);
|
background: var(--bg-sidebar);
|
||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 16px 12px;
|
padding: 16px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-menu {
|
.nav-menu {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@ -343,14 +332,14 @@
|
|||||||
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
|
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
border-top: 1px solid var(--border-color);
|
border-top: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-actions {
|
.quick-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@ -371,12 +360,14 @@
|
|||||||
:deep(.el-button + .el-button) {
|
:deep(.el-button + .el-button) {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: var(--bg-content);
|
background: var(--bg-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -1,81 +1,82 @@
|
|||||||
import { createRouter, createWebHashHistory } from "vue-router";
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: '/',
|
||||||
name: "dashboard",
|
name: 'dashboard',
|
||||||
component: () => import("@/views/Dashboard.vue"),
|
component: () => import('@/views/Dashboard.vue'),
|
||||||
meta: { title: "仪表盘" },
|
meta: { title: '仪表盘' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/php",
|
path: '/php',
|
||||||
name: "php",
|
name: 'php',
|
||||||
component: () => import("@/views/PhpManager.vue"),
|
component: () => import('@/views/PhpManager.vue'),
|
||||||
meta: { title: "PHP 管理" },
|
meta: { title: 'PHP 管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/mysql",
|
path: '/mysql',
|
||||||
name: "mysql",
|
name: 'mysql',
|
||||||
component: () => import("@/views/MysqlManager.vue"),
|
component: () => import('@/views/MysqlManager.vue'),
|
||||||
meta: { title: "MySQL 管理" },
|
meta: { title: 'MySQL 管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/nginx",
|
path: '/nginx',
|
||||||
name: "nginx",
|
name: 'nginx',
|
||||||
component: () => import("@/views/NginxManager.vue"),
|
component: () => import('@/views/NginxManager.vue'),
|
||||||
meta: { title: "Nginx 管理" },
|
meta: { title: 'Nginx 管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/redis",
|
path: '/redis',
|
||||||
name: "redis",
|
name: 'redis',
|
||||||
component: () => import("@/views/RedisManager.vue"),
|
component: () => import('@/views/RedisManager.vue'),
|
||||||
meta: { title: "Redis 管理" },
|
meta: { title: 'Redis 管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/nodejs",
|
path: '/nodejs',
|
||||||
name: "nodejs",
|
name: 'nodejs',
|
||||||
component: () => import("@/views/NodeManager.vue"),
|
component: () => import('@/views/NodeManager.vue'),
|
||||||
meta: { title: "Node.js 管理" },
|
meta: { title: 'Node.js 管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/go",
|
path: '/sites',
|
||||||
name: "go",
|
name: 'sites',
|
||||||
component: () => import("@/views/GoManager.vue"),
|
component: () => import('@/views/SitesManager.vue'),
|
||||||
meta: { title: "Go 管理" },
|
meta: { title: '站点管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/sites",
|
path: '/hosts',
|
||||||
name: "sites",
|
name: 'hosts',
|
||||||
component: () => import("@/views/SitesManager.vue"),
|
component: () => import('@/views/HostsManager.vue'),
|
||||||
meta: { title: "站点管理" },
|
meta: { title: 'Hosts 管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/hosts",
|
path: '/git',
|
||||||
name: "hosts",
|
name: 'git',
|
||||||
component: () => import("@/views/HostsManager.vue"),
|
component: () => import('@/views/GitManager.vue'),
|
||||||
meta: { title: "Hosts 管理" },
|
meta: { title: 'Git 管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/git",
|
path: '/python',
|
||||||
name: "git",
|
name: 'python',
|
||||||
component: () => import("@/views/GitManager.vue"),
|
component: () => import('@/views/PythonManager.vue'),
|
||||||
meta: { title: "Git 管理" },
|
meta: { title: 'Python 管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/python",
|
path: '/go',
|
||||||
name: "python",
|
name: 'go',
|
||||||
component: () => import("@/views/PythonManager.vue"),
|
component: () => import('@/views/GoManager.vue'),
|
||||||
meta: { title: "Python 管理" },
|
meta: { title: 'Go 管理' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/settings",
|
path: '/settings',
|
||||||
name: "settings",
|
name: 'settings',
|
||||||
component: () => import("@/views/Settings.vue"),
|
component: () => import('@/views/Settings.vue'),
|
||||||
meta: { title: "设置" },
|
meta: { title: '设置' }
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
});
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
||||||
export default router;
|
|
||||||
|
|||||||
@ -30,6 +30,15 @@ interface NodeVersion {
|
|||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Go 版本信息
|
||||||
|
interface GoVersion {
|
||||||
|
version: string
|
||||||
|
path: string
|
||||||
|
isActive: boolean
|
||||||
|
goroot: string
|
||||||
|
gopath?: string
|
||||||
|
}
|
||||||
|
|
||||||
// 站点信息
|
// 站点信息
|
||||||
interface SiteConfig {
|
interface SiteConfig {
|
||||||
name: string
|
name: string
|
||||||
@ -56,6 +65,9 @@ export const useServiceStore = defineStore('service', () => {
|
|||||||
// Node.js 版本列表
|
// Node.js 版本列表
|
||||||
const nodeVersions = ref<NodeVersion[]>([])
|
const nodeVersions = ref<NodeVersion[]>([])
|
||||||
|
|
||||||
|
// Go 版本列表
|
||||||
|
const goVersions = ref<GoVersion[]>([])
|
||||||
|
|
||||||
// 站点列表
|
// 站点列表
|
||||||
const sites = ref<SiteConfig[]>([])
|
const sites = ref<SiteConfig[]>([])
|
||||||
|
|
||||||
@ -99,6 +111,11 @@ export const useServiceStore = defineStore('service', () => {
|
|||||||
return nodeVersions.value.find(v => v.isActive)
|
return nodeVersions.value.find(v => v.isActive)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 计算属性:当前活动的 Go 版本
|
||||||
|
const activeGoVersion = computed(() => {
|
||||||
|
return goVersions.value.find(v => v.isActive)
|
||||||
|
})
|
||||||
|
|
||||||
// 刷新所有状态
|
// 刷新所有状态
|
||||||
async function refreshAll() {
|
async function refreshAll() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@ -107,6 +124,7 @@ export const useServiceStore = defineStore('service', () => {
|
|||||||
refreshServiceStatus(),
|
refreshServiceStatus(),
|
||||||
refreshPhpVersions(),
|
refreshPhpVersions(),
|
||||||
refreshNodeVersions(),
|
refreshNodeVersions(),
|
||||||
|
refreshGoVersions(),
|
||||||
refreshSites(),
|
refreshSites(),
|
||||||
refreshBasePath()
|
refreshBasePath()
|
||||||
])
|
])
|
||||||
@ -175,6 +193,18 @@ export const useServiceStore = defineStore('service', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新 Go 版本列表
|
||||||
|
async function refreshGoVersions() {
|
||||||
|
try {
|
||||||
|
const versions = await window.electronAPI?.go.getVersions()
|
||||||
|
if (versions) {
|
||||||
|
goVersions.value = versions
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新 Go 版本失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新站点列表
|
// 刷新站点列表
|
||||||
async function refreshSites() {
|
async function refreshSites() {
|
||||||
try {
|
try {
|
||||||
@ -217,6 +247,7 @@ export const useServiceStore = defineStore('service', () => {
|
|||||||
serviceStatus,
|
serviceStatus,
|
||||||
phpVersions,
|
phpVersions,
|
||||||
nodeVersions,
|
nodeVersions,
|
||||||
|
goVersions,
|
||||||
sites,
|
sites,
|
||||||
basePath,
|
basePath,
|
||||||
loading,
|
loading,
|
||||||
@ -227,11 +258,13 @@ export const useServiceStore = defineStore('service', () => {
|
|||||||
runningServiceCount,
|
runningServiceCount,
|
||||||
activePhpVersion,
|
activePhpVersion,
|
||||||
activeNodeVersion,
|
activeNodeVersion,
|
||||||
|
activeGoVersion,
|
||||||
// 方法
|
// 方法
|
||||||
refreshAll,
|
refreshAll,
|
||||||
refreshServiceStatus,
|
refreshServiceStatus,
|
||||||
refreshPhpVersions,
|
refreshPhpVersions,
|
||||||
refreshNodeVersions,
|
refreshNodeVersions,
|
||||||
|
refreshGoVersions,
|
||||||
refreshSites,
|
refreshSites,
|
||||||
refreshBasePath,
|
refreshBasePath,
|
||||||
updateServiceStatus,
|
updateServiceStatus,
|
||||||
|
|||||||
@ -2,24 +2,17 @@
|
|||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1 class="page-title">
|
<h1 class="page-title">
|
||||||
<span class="title-icon"
|
<span class="title-icon"><el-icon><Platform /></el-icon></span>
|
||||||
><el-icon><Aim /></el-icon
|
|
||||||
></span>
|
|
||||||
Go 管理
|
Go 管理
|
||||||
</h1>
|
</h1>
|
||||||
<p class="page-description">管理本地 Go 版本,支持多版本切换</p>
|
<p class="page-description">管理本地 Go 版本,支持多版本切换</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 下载进度 -->
|
<!-- 下载进度 -->
|
||||||
<div
|
<div v-if="downloadProgress.percent > 0 && downloadProgress.percent < 100" class="download-progress">
|
||||||
v-if="downloadProgress.percent > 0 && downloadProgress.percent < 100"
|
|
||||||
class="download-progress">
|
|
||||||
<div class="progress-info">
|
<div class="progress-info">
|
||||||
<span>正在下载 Go...</span>
|
<span>正在下载 Go...</span>
|
||||||
<span
|
<span>{{ formatSize(downloadProgress.downloaded) }} / {{ formatSize(downloadProgress.total) }}</span>
|
||||||
>{{ formatSize(downloadProgress.downloaded) }} /
|
|
||||||
{{ formatSize(downloadProgress.total) }}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<el-progress :percentage="downloadProgress.percent" :stroke-width="10" />
|
<el-progress :percentage="downloadProgress.percent" :stroke-width="10" />
|
||||||
</div>
|
</div>
|
||||||
@ -34,26 +27,44 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div v-if="versions.length > 0" class="version-grid">
|
<div v-if="loading" class="loading-state">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="versions.length === 0" class="empty-state">
|
||||||
|
<el-icon class="empty-icon"><Platform /></el-icon>
|
||||||
|
<h3 class="empty-title">暂未安装 Go</h3>
|
||||||
|
<p class="empty-description">点击上方按钮安装第一个 Go 版本</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="version-grid">
|
||||||
<div
|
<div
|
||||||
v-for="version in versions"
|
v-for="version in versions"
|
||||||
:key="version.version"
|
:key="version.version"
|
||||||
class="version-card"
|
class="version-card"
|
||||||
:class="{ active: version.isActive }">
|
:class="{ active: version.isActive }"
|
||||||
|
>
|
||||||
<div class="version-main">
|
<div class="version-main">
|
||||||
<div class="version-icon">
|
<div class="version-icon">
|
||||||
<el-icon :size="32"><Aim /></el-icon>
|
<el-icon :size="32"><Platform /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-content">
|
<div class="version-content">
|
||||||
<div class="version-title">
|
<div class="version-title">
|
||||||
<span class="version-number">Go {{ version.version }}</span>
|
<span class="version-number">Go {{ version.version }}</span>
|
||||||
<el-tag
|
<el-tag v-if="version.isActive" type="success" size="small" effect="dark">当前版本</el-tag>
|
||||||
v-if="version.isActive"
|
</div>
|
||||||
type="success"
|
<div class="version-meta">
|
||||||
size="small"
|
<div class="version-path">
|
||||||
effect="dark"
|
<el-icon><FolderOpened /></el-icon>
|
||||||
>当前版本</el-tag
|
<span>{{ version.path }}</span>
|
||||||
>
|
</div>
|
||||||
|
<div v-if="version.goroot" class="goroot-info">
|
||||||
|
<el-icon><Files /></el-icon>
|
||||||
|
<span>GOROOT: {{ version.goroot }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="version.gopath" class="gopath-info">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
<span>GOPATH: {{ version.gopath }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -63,7 +74,8 @@
|
|||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@click="setActiveVersion(version.version)"
|
@click="setActiveVersion(version.version)"
|
||||||
:loading="settingActive === version.version">
|
:loading="settingActive === version.version"
|
||||||
|
>
|
||||||
设为默认
|
设为默认
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
@ -71,26 +83,28 @@
|
|||||||
size="small"
|
size="small"
|
||||||
plain
|
plain
|
||||||
@click="uninstallVersion(version.version)"
|
@click="uninstallVersion(version.version)"
|
||||||
:loading="uninstalling === version.version">
|
:loading="uninstalling === version.version"
|
||||||
|
>
|
||||||
卸载
|
卸载
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else description="暂未安装 Go" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 安装新版本对话框 -->
|
<!-- 安装新版本对话框 -->
|
||||||
<el-dialog v-model="showInstallDialog" title="安装 Go" width="700px">
|
<el-dialog
|
||||||
|
v-model="showInstallDialog"
|
||||||
|
title="安装 Go"
|
||||||
|
width="700px"
|
||||||
|
>
|
||||||
<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>
|
<el-icon><InfoFilled /></el-icon>
|
||||||
下载源说明
|
下载源说明
|
||||||
</template>
|
</template>
|
||||||
Go 将从官方网站
|
Go 将从官方网站 <a href="https://golang.org/dl/" target="_blank">golang.org</a> 下载 Windows 64位版本。
|
||||||
<a href="https://go.dev/dl/" target="_blank">go.dev/dl</a> 下载 Windows
|
|
||||||
amd64 版本。
|
|
||||||
</el-alert>
|
</el-alert>
|
||||||
<div v-if="loadingAvailableVersions" 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>
|
||||||
@ -100,27 +114,28 @@
|
|||||||
<span>暂无可用版本</span>
|
<span>暂无可用版本</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="available-versions">
|
<div v-else class="available-versions">
|
||||||
<el-table
|
<el-table :data="availableVersions" style="width: 100%" max-height="400">
|
||||||
:data="availableVersions"
|
<el-table-column prop="version" label="版本" width="120" />
|
||||||
style="width: 100%"
|
<el-table-column label="类型" width="120">
|
||||||
max-height="400">
|
|
||||||
<el-table-column prop="version" label="版本" width="140" />
|
|
||||||
<el-table-column label="类型" width="100">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.stable" type="success" size="small"
|
<el-tag v-if="row.stable" type="success" size="small">稳定版</el-tag>
|
||||||
>Stable</el-tag
|
<el-tag v-else type="warning" size="small">开发版</el-tag>
|
||||||
>
|
|
||||||
<el-tag v-else type="warning" size="small">Unstable</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="120">
|
<el-table-column label="大小" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatSize(row.size) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="!isInstalled(row.version)"
|
v-if="!isInstalled(row.version)"
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@click="installVersion(row)"
|
@click="installVersion(row)"
|
||||||
:loading="installing === row.version">
|
:loading="installing === row.version"
|
||||||
|
>
|
||||||
安装
|
安装
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-tag v-else type="info" size="small">已安装</el-tag>
|
<el-tag v-else type="info" size="small">已安装</el-tag>
|
||||||
@ -136,166 +151,224 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<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 { Plus, Aim, InfoFilled, Loading } from "@element-plus/icons-vue";
|
import { Plus, Platform, InfoFilled, Loading, FolderOpened, Files, Box } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
defineOptions({
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
name: "GoManager",
|
defineOptions({
|
||||||
});
|
name: 'GoManager'
|
||||||
|
})
|
||||||
|
|
||||||
interface GoVersion {
|
interface GoVersion {
|
||||||
version: string;
|
version: string
|
||||||
path: string;
|
path: string
|
||||||
isActive: boolean;
|
isActive: boolean
|
||||||
}
|
goroot: string
|
||||||
|
gopath?: string
|
||||||
|
installDate?: Date
|
||||||
|
size?: number
|
||||||
|
}
|
||||||
|
|
||||||
interface AvailableGoVersion {
|
interface AvailableGoVersion {
|
||||||
version: string;
|
version: string
|
||||||
stable: boolean;
|
stable: boolean
|
||||||
downloadUrl: string;
|
downloadUrl: string
|
||||||
filename: string;
|
size: number
|
||||||
}
|
sha256: string
|
||||||
|
releaseDate?: string
|
||||||
|
}
|
||||||
|
|
||||||
const versions = ref<GoVersion[]>([]);
|
const loading = ref(false)
|
||||||
const availableVersions = ref<AvailableGoVersion[]>([]);
|
const versions = ref<GoVersion[]>([])
|
||||||
const showInstallDialog = ref(false);
|
const availableVersions = ref<AvailableGoVersion[]>([])
|
||||||
const installing = ref("");
|
const showInstallDialog = ref(false)
|
||||||
const uninstalling = ref("");
|
const installing = ref('')
|
||||||
const settingActive = ref("");
|
const uninstalling = ref('')
|
||||||
|
const settingActive = ref('')
|
||||||
|
|
||||||
const downloadProgress = reactive({
|
const downloadProgress = reactive({
|
||||||
percent: 0,
|
percent: 0,
|
||||||
downloaded: 0,
|
downloaded: 0,
|
||||||
total: 0,
|
total: 0
|
||||||
});
|
})
|
||||||
|
|
||||||
const loadVersions = async () => {
|
const loadVersions = async () => {
|
||||||
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
versions.value = (await window.electronAPI?.go.getVersions()) || [];
|
versions.value = await window.electronAPI?.go.getVersions() || []
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("加载版本失败:", error);
|
console.error('加载版本失败:', error)
|
||||||
}
|
ElMessage.error('加载已安装版本失败: ' + error.message)
|
||||||
};
|
|
||||||
|
|
||||||
const loadingAvailableVersions = ref(false);
|
|
||||||
|
|
||||||
const loadAvailableVersions = async () => {
|
|
||||||
loadingAvailableVersions.value = true;
|
|
||||||
try {
|
|
||||||
availableVersions.value =
|
|
||||||
(await window.electronAPI?.go.getAvailableVersions()) || [];
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("加载可用版本失败:", error);
|
|
||||||
} finally {
|
} finally {
|
||||||
loadingAvailableVersions.value = false;
|
loading.value = false
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const isInstalled = (version: string) => {
|
const loadingAvailableVersions = ref(false)
|
||||||
return versions.value.some((v) => v.version === version);
|
|
||||||
};
|
|
||||||
|
|
||||||
const installVersion = async (row: AvailableGoVersion) => {
|
const loadAvailableVersions = async () => {
|
||||||
installing.value = row.version;
|
loadingAvailableVersions.value = true
|
||||||
downloadProgress.percent = 0;
|
try {
|
||||||
downloadProgress.downloaded = 0;
|
availableVersions.value = await window.electronAPI?.go.getAvailableVersions() || []
|
||||||
downloadProgress.total = 0;
|
} catch (error: any) {
|
||||||
|
console.error('加载可用版本失败:', error)
|
||||||
|
ElMessage.error('加载可用版本失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
loadingAvailableVersions.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInstalled = (version: string) => {
|
||||||
|
return versions.value.some(v => v.version === version)
|
||||||
|
}
|
||||||
|
|
||||||
|
const installVersion = async (row: AvailableGoVersion) => {
|
||||||
|
installing.value = row.version
|
||||||
|
downloadProgress.percent = 0
|
||||||
|
downloadProgress.downloaded = 0
|
||||||
|
downloadProgress.total = 0
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI?.go.install(
|
const result = await window.electronAPI?.go.install(row.version, row.downloadUrl, row.sha256)
|
||||||
row.version,
|
|
||||||
row.downloadUrl,
|
|
||||||
);
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
ElMessage.success(result.message);
|
ElMessage.success(result.message)
|
||||||
await loadVersions();
|
await loadVersions()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || "安装失败");
|
ElMessage.error(result?.message || '安装失败')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error.message);
|
ElMessage.error(error.message)
|
||||||
} finally {
|
} finally {
|
||||||
installing.value = "";
|
installing.value = ''
|
||||||
downloadProgress.percent = 0;
|
downloadProgress.percent = 0
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const uninstallVersion = async (version: string) => {
|
const uninstallVersion = async (version: string) => {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`确定要卸载 Go ${version} 吗?`, "确认卸载", {
|
await ElMessageBox.confirm(
|
||||||
type: "warning",
|
`确定要卸载 Go ${version} 吗?`,
|
||||||
});
|
'确认卸载',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
|
||||||
uninstalling.value = version;
|
uninstalling.value = version
|
||||||
const result = await window.electronAPI?.go.uninstall(version);
|
const result = await window.electronAPI?.go.uninstall(version)
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
ElMessage.success(result.message);
|
ElMessage.success(result.message)
|
||||||
await loadVersions();
|
await loadVersions()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || "卸载失败");
|
ElMessage.error(result?.message || '卸载失败')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error !== "cancel") {
|
if (error !== 'cancel') {
|
||||||
ElMessage.error(error.message);
|
ElMessage.error(error.message)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
uninstalling.value = "";
|
uninstalling.value = ''
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const setActiveVersion = async (version: string) => {
|
const setActiveVersion = async (version: string) => {
|
||||||
settingActive.value = version;
|
settingActive.value = version
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI?.go.setActive(version);
|
const result = await window.electronAPI?.go.setActive(version)
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
ElMessage.success(result.message);
|
ElMessage.success(result.message)
|
||||||
await loadVersions();
|
await loadVersions()
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(result?.message || "设置失败");
|
ElMessage.error(result?.message || '设置失败')
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(error.message);
|
ElMessage.error(error.message)
|
||||||
} finally {
|
} finally {
|
||||||
settingActive.value = "";
|
settingActive.value = ''
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const formatSize = (bytes: number) => {
|
const formatSize = (bytes: number) => {
|
||||||
if (bytes === 0) return "0 B";
|
if (bytes === 0) return '0 B'
|
||||||
const k = 1024;
|
const k = 1024
|
||||||
const sizes = ["B", "KB", "MB", "GB"];
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
};
|
}
|
||||||
|
|
||||||
const onDownloadProgress = (data: any) => {
|
// 监听下载进度
|
||||||
if (data.type === "go") {
|
const onDownloadProgress = (_event: any, data: any) => {
|
||||||
downloadProgress.percent = data.progress;
|
if (data.type === 'go') {
|
||||||
downloadProgress.downloaded = data.downloaded;
|
downloadProgress.percent = data.progress
|
||||||
downloadProgress.total = data.total;
|
downloadProgress.downloaded = data.downloaded
|
||||||
|
downloadProgress.total = data.total
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadVersions();
|
loadVersions()
|
||||||
loadAvailableVersions();
|
loadAvailableVersions()
|
||||||
window.electronAPI?.onDownloadProgress(onDownloadProgress);
|
window.electronAPI?.onDownloadProgress(onDownloadProgress)
|
||||||
});
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.electronAPI?.removeDownloadProgressListener(onDownloadProgress);
|
window.electronAPI?.removeDownloadProgressListener(onDownloadProgress)
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.version-grid {
|
.loading-state {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
align-items: center;
|
||||||
gap: 20px;
|
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); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 64px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-card {
|
.empty-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-card {
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
@ -313,14 +386,10 @@
|
|||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border-color: var(--success-color);
|
border-color: var(--success-color);
|
||||||
background: linear-gradient(
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.08) 0%, rgba(16, 185, 129, 0.02) 100%);
|
||||||
135deg,
|
|
||||||
rgba(16, 185, 129, 0.08) 0%,
|
|
||||||
rgba(16, 185, 129, 0.02) 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
.version-icon {
|
.version-icon {
|
||||||
background: linear-gradient(135deg, #00add8 0%, #00add8 100%);
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,7 +403,7 @@
|
|||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: linear-gradient(135deg, #00add8 0%, #00add8 100%);
|
background: linear-gradient(135deg, #00add8 0%, #007d9c 100%);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -351,7 +420,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 12px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,6 +431,37 @@
|
|||||||
letter-spacing: -0.5px;
|
letter-spacing: -0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.version-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-path,
|
||||||
|
.goroot-info,
|
||||||
|
.gopath-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-card);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: 'Fira Code', monospace;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
color: var(--accent-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.version-actions {
|
.version-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@ -372,17 +472,17 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.available-versions {
|
.available-versions {
|
||||||
.el-table {
|
.el-table {
|
||||||
--el-table-bg-color: transparent;
|
--el-table-bg-color: transparent;
|
||||||
--el-table-tr-bg-color: transparent;
|
--el-table-tr-bg-color: transparent;
|
||||||
--el-table-header-bg-color: var(--bg-input);
|
--el-table-header-bg-color: var(--bg-input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.download-progress {
|
.download-progress {
|
||||||
background: var(--bg-card);
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -396,29 +496,15 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-state {
|
.empty-hint {
|
||||||
display: flex;
|
text-align: center;
|
||||||
align-items: center;
|
padding: 40px 20px;
|
||||||
justify-content: center;
|
color: var(--text-muted);
|
||||||
gap: 12px;
|
}
|
||||||
padding: 40px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
|
|
||||||
.is-loading {
|
.mb-4 {
|
||||||
font-size: 24px;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-4 {
|
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@ -429,11 +515,5 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-hint {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@ -352,34 +352,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="extensions-list">
|
<div v-else class="extensions-list">
|
||||||
<div class="extensions-count">
|
<div class="extensions-count">
|
||||||
找到 {{ availableExtensions.length }} 个扩展
|
找到 {{ availableExtensions.length }} 个适用于 PHP {{ currentVersion }} 的扩展
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="ext in availableExtensions"
|
v-for="ext in availableExtensions"
|
||||||
:key="ext.name"
|
:key="ext.name"
|
||||||
class="extension-item"
|
class="extension-item"
|
||||||
:class="{ 'not-available': ext.notAvailableReason }"
|
|
||||||
>
|
>
|
||||||
<div class="ext-info">
|
<div class="ext-info">
|
||||||
<div class="ext-main">
|
<div class="ext-main">
|
||||||
<span class="ext-name" v-html="highlightKeyword(ext.name)"></span>
|
<span class="ext-name" v-html="highlightKeyword(ext.name)"></span>
|
||||||
<el-tag type="info" size="small">{{ ext.version === 'latest' ? '最新版' : 'v' + ext.version }}</el-tag>
|
<el-tag type="warning" size="small">v{{ ext.version }}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<span class="ext-desc" v-if="ext.description">{{ ext.description }}</span>
|
<span class="ext-desc" v-if="ext.description">{{ ext.description }}</span>
|
||||||
<span class="ext-not-available" v-if="ext.notAvailableReason">
|
|
||||||
<el-icon><Warning /></el-icon>
|
|
||||||
{{ ext.notAvailableReason }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 有明确不可用原因时显示不支持 -->
|
|
||||||
<el-tooltip v-if="ext.notAvailableReason" :content="ext.notAvailableReason" placement="top">
|
|
||||||
<el-button type="info" size="small" disabled>
|
|
||||||
不支持
|
|
||||||
</el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
<!-- 否则显示安装按钮 -->
|
|
||||||
<el-button
|
<el-button
|
||||||
v-else
|
|
||||||
type="primary"
|
type="primary"
|
||||||
size="small"
|
size="small"
|
||||||
@click="installExtension(ext)"
|
@click="installExtension(ext)"
|
||||||
@ -428,7 +415,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted, onActivated } from 'vue'
|
import { ref, reactive, computed, onMounted, onUnmounted, onActivated } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { FolderOpened, InfoFilled, VideoPlay, VideoPause, EditPen, Warning } from '@element-plus/icons-vue'
|
import { FolderOpened, InfoFilled, VideoPlay, VideoPause, EditPen } from '@element-plus/icons-vue'
|
||||||
import { useServiceStore } from '@/stores/serviceStore'
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
import LogViewer from '@/components/LogViewer.vue'
|
import LogViewer from '@/components/LogViewer.vue'
|
||||||
|
|
||||||
@ -464,8 +451,6 @@ interface AvailableExtension {
|
|||||||
downloadUrl: string
|
downloadUrl: string
|
||||||
description?: string
|
description?: string
|
||||||
packageName?: string // Packagist 包名,用于 PIE 安装
|
packageName?: string // Packagist 包名,用于 PIE 安装
|
||||||
supportedPhpVersions?: string[] // 支持的 PHP 版本
|
|
||||||
notAvailableReason?: string // 不可用原因
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -1096,11 +1081,6 @@ onUnmounted(() => {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.not-available {
|
|
||||||
opacity: 0.7;
|
|
||||||
background-color: rgba(0, 0, 0, 0.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ext-info {
|
.ext-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -1121,14 +1101,6 @@ onUnmounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ext-not-available {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--warning-color, #e6a23c);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user