Compare commits

..

1 Commits

Author SHA1 Message Date
9614a3d234 feat: add Go version management support
- Add GoManager service for downloading and managing Go versions
- Implement Go version detection and installation
- Add GoManager Vue component with version selection UI
- Update main process to handle Go-related IPC calls
- Add Jest testing configuration and GoManager unit tests
- Update service store to include Go management
- Add routing for Go manager page
- Include Kiro specs and steering documentation
2026-01-13 18:30:26 +08:00
22 changed files with 10974 additions and 2301 deletions

View 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 供其他工具使用

View 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 显示具体的错误原因和解决建议

View 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
View 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
View 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
View 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 文件修改
- 支持开机自启动(任务计划程序)

View File

@ -7,11 +7,10 @@ exports.default = async function(context) {
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 productName = context.packager.appInfo.productName;
const version = context.packager.appInfo.version;
const exePath = path.join(appOutDir, `${productName}.exe`);
const iconPath = path.join(__dirname, 'icon.ico');
@ -26,29 +25,18 @@ exports.default = async function(context) {
}
try {
// rcedit 是默认导出
const rcedit = require('rcedit');
// 使用 npm 安装的 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}`);
await rcedit(exePath, {
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
icon: iconPath
});
console.log('Icon and version info set successfully!');
console.log('Icon set successfully!');
} catch (error) {
console.error('Failed to set icon:', error.message);
// 不阻止打包继续
}
};

View File

@ -13,11 +13,11 @@ import { MysqlManager } from "./services/MysqlManager";
import { NginxManager } from "./services/NginxManager";
import { RedisManager } from "./services/RedisManager";
import { NodeManager } from "./services/NodeManager";
import { GoManager } from "./services/GoManager";
import { ServiceManager } from "./services/ServiceManager";
import { HostsManager } from "./services/HostsManager";
import { GitManager } from "./services/GitManager";
import { PythonManager } from "./services/PythonManager";
import { GoManager } from "./services/GoManager";
import { LogManager } from "./services/LogManager";
import { ConfigStore } from "./services/ConfigStore";
@ -99,7 +99,7 @@ export function sendDownloadProgress(
type: string,
progress: number,
downloaded: number,
total: number,
total: number
) {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send("download-progress", {
@ -118,11 +118,11 @@ const mysqlManager = new MysqlManager(configStore);
const nginxManager = new NginxManager(configStore);
const redisManager = new RedisManager(configStore);
const nodeManager = new NodeManager(configStore);
const goManager = new GoManager(configStore);
const serviceManager = new ServiceManager(configStore);
const hostsManager = new HostsManager();
const gitManager = new GitManager(configStore);
const pythonManager = new PythonManager(configStore);
const goManager = new GoManager(configStore);
const logManager = new LogManager(configStore);
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();
@ -317,7 +308,7 @@ ipcMain.handle("window:close", () => mainWindow?.close());
// 打开外部链接
ipcMain.handle("shell:openExternal", (_, url: string) =>
shell.openExternal(url),
shell.openExternal(url)
);
ipcMain.handle("shell:openPath", (_, path: string) => shell.openPath(path));
@ -334,33 +325,33 @@ ipcMain.handle("dialog:selectDirectory", async () => {
// ==================== PHP 管理 ====================
ipcMain.handle("php:getVersions", () => phpManager.getInstalledVersions());
ipcMain.handle("php:getAvailableVersions", () =>
phpManager.getAvailableVersions(),
phpManager.getAvailableVersions()
);
ipcMain.handle("php:install", (_, version: string) =>
phpManager.install(version),
phpManager.install(version)
);
ipcMain.handle("php:uninstall", (_, version: string) =>
phpManager.uninstall(version),
phpManager.uninstall(version)
);
ipcMain.handle("php:setActive", (_, version: string) =>
phpManager.setActive(version),
phpManager.setActive(version)
);
ipcMain.handle("php:getExtensions", (_, version: string) =>
phpManager.getExtensions(version),
phpManager.getExtensions(version)
);
ipcMain.handle("php:openExtensionDir", (_, version: string) =>
phpManager.openExtensionDir(version),
phpManager.openExtensionDir(version)
);
ipcMain.handle(
"php:getAvailableExtensions",
(_, version: string, searchKeyword?: string) =>
phpManager.getAvailableExtensions(version, searchKeyword),
phpManager.getAvailableExtensions(version, searchKeyword)
);
ipcMain.handle("php:enableExtension", (_, version: string, ext: string) =>
phpManager.enableExtension(version, ext),
phpManager.enableExtension(version, ext)
);
ipcMain.handle("php:disableExtension", (_, version: string, ext: string) =>
phpManager.disableExtension(version, ext),
phpManager.disableExtension(version, ext)
);
ipcMain.handle(
"php:installExtension",
@ -369,14 +360,14 @@ ipcMain.handle(
version: string,
ext: string,
downloadUrl?: string,
packageName?: string,
) => phpManager.installExtension(version, ext, downloadUrl, packageName),
packageName?: string
) => phpManager.installExtension(version, ext, downloadUrl, packageName)
);
ipcMain.handle("php:getConfig", (_, version: string) =>
phpManager.getConfig(version),
phpManager.getConfig(version)
);
ipcMain.handle("php:saveConfig", (_, version: string, config: string) =>
phpManager.saveConfig(version, config),
phpManager.saveConfig(version, config)
);
// ==================== Composer 管理 ====================
@ -384,62 +375,62 @@ ipcMain.handle("composer:getStatus", () => phpManager.getComposerStatus());
ipcMain.handle("composer:install", () => phpManager.installComposer());
ipcMain.handle("composer:uninstall", () => phpManager.uninstallComposer());
ipcMain.handle("composer:setMirror", (_, mirror: string) =>
phpManager.setComposerMirror(mirror),
phpManager.setComposerMirror(mirror)
);
ipcMain.handle(
"composer:createLaravelProject",
(_, projectName: string, targetDir: string) =>
phpManager.createLaravelProject(projectName, targetDir),
phpManager.createLaravelProject(projectName, targetDir)
);
// ==================== MySQL 管理 ====================
ipcMain.handle("mysql:getVersions", () => mysqlManager.getInstalledVersions());
ipcMain.handle("mysql:getAvailableVersions", () =>
mysqlManager.getAvailableVersions(),
mysqlManager.getAvailableVersions()
);
ipcMain.handle("mysql:install", (_, version: string) =>
mysqlManager.install(version),
mysqlManager.install(version)
);
ipcMain.handle("mysql:uninstall", (_, version: string) =>
mysqlManager.uninstall(version),
mysqlManager.uninstall(version)
);
ipcMain.handle("mysql:start", (_, version: string) =>
mysqlManager.start(version),
mysqlManager.start(version)
);
ipcMain.handle("mysql:stop", (_, version: string) =>
mysqlManager.stop(version),
mysqlManager.stop(version)
);
ipcMain.handle("mysql:restart", (_, version: string) =>
mysqlManager.restart(version),
mysqlManager.restart(version)
);
ipcMain.handle("mysql:getStatus", (_, version: string) =>
mysqlManager.getStatus(version),
mysqlManager.getStatus(version)
);
ipcMain.handle(
"mysql:changePassword",
(_, version: string, newPassword: string, currentPassword?: string) =>
mysqlManager.changeRootPassword(version, newPassword, currentPassword),
mysqlManager.changeRootPassword(version, newPassword, currentPassword)
);
ipcMain.handle("mysql:getConfig", (_, version: string) =>
mysqlManager.getConfig(version),
mysqlManager.getConfig(version)
);
ipcMain.handle("mysql:saveConfig", (_, version: string, config: string) =>
mysqlManager.saveConfig(version, config),
mysqlManager.saveConfig(version, config)
);
ipcMain.handle("mysql:reinitialize", (_, version: string) =>
mysqlManager.reinitialize(version),
mysqlManager.reinitialize(version)
);
// ==================== Nginx 管理 ====================
ipcMain.handle("nginx:getVersions", () => nginxManager.getInstalledVersions());
ipcMain.handle("nginx:getAvailableVersions", () =>
nginxManager.getAvailableVersions(),
nginxManager.getAvailableVersions()
);
ipcMain.handle("nginx:install", (_, version: string) =>
nginxManager.install(version),
nginxManager.install(version)
);
ipcMain.handle("nginx:uninstall", (_, version: string) =>
nginxManager.uninstall(version),
nginxManager.uninstall(version)
);
ipcMain.handle("nginx:start", () => nginxManager.start());
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:getConfig", () => nginxManager.getConfig());
ipcMain.handle("nginx:saveConfig", (_, config: string) =>
nginxManager.saveConfig(config),
nginxManager.saveConfig(config)
);
ipcMain.handle("nginx:getSites", () => nginxManager.getSites());
ipcMain.handle("nginx:addSite", (_, site: any) => nginxManager.addSite(site));
ipcMain.handle("nginx:removeSite", (_, name: string) =>
nginxManager.removeSite(name),
nginxManager.removeSite(name)
);
ipcMain.handle("nginx:updateSite", (_, originalName: string, site: any) =>
nginxManager.updateSite(originalName, site),
nginxManager.updateSite(originalName, site)
);
ipcMain.handle("nginx:enableSite", (_, name: string) =>
nginxManager.enableSite(name),
nginxManager.enableSite(name)
);
ipcMain.handle("nginx:disableSite", (_, name: string) =>
nginxManager.disableSite(name),
nginxManager.disableSite(name)
);
ipcMain.handle("nginx:generateLaravelConfig", (_, site: any) =>
nginxManager.generateLaravelConfig(site),
nginxManager.generateLaravelConfig(site)
);
ipcMain.handle("nginx:requestSSL", (_, domain: string, email: string) =>
nginxManager.requestSSLCertificate(domain, email),
nginxManager.requestSSLCertificate(domain, email)
);
// ==================== Redis 管理 ====================
ipcMain.handle("redis:getVersions", () => redisManager.getInstalledVersions());
ipcMain.handle("redis:getAvailableVersions", () =>
redisManager.getAvailableVersions(),
redisManager.getAvailableVersions()
);
ipcMain.handle("redis:install", (_, version: string) =>
redisManager.install(version),
redisManager.install(version)
);
ipcMain.handle("redis:uninstall", (_, version: string) =>
redisManager.uninstall(version),
redisManager.uninstall(version)
);
ipcMain.handle("redis:start", () => redisManager.start());
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:getConfig", () => redisManager.getConfig());
ipcMain.handle("redis:saveConfig", (_, config: string) =>
redisManager.saveConfig(config),
redisManager.saveConfig(config)
);
// ==================== Node.js 管理 ====================
ipcMain.handle("node:getVersions", () => nodeManager.getInstalledVersions());
ipcMain.handle("node:getAvailableVersions", () =>
nodeManager.getAvailableVersions(),
nodeManager.getAvailableVersions()
);
ipcMain.handle("node:install", (_, version: string, downloadUrl: string) =>
nodeManager.install(version, downloadUrl),
nodeManager.install(version, downloadUrl)
);
ipcMain.handle("node:uninstall", (_, version: string) =>
nodeManager.uninstall(version),
nodeManager.uninstall(version)
);
ipcMain.handle("node:setActive", (_, version: string) =>
nodeManager.setActive(version),
nodeManager.setActive(version)
);
ipcMain.handle("node:getInfo", (_, version: string) =>
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),
nodeManager.getNodeInfo(version)
);
// ==================== 服务管理 ====================
ipcMain.handle("service:getAll", () => serviceManager.getAllServices());
ipcMain.handle("service:setAutoStart", (_, service: string, enabled: boolean) =>
serviceManager.setAutoStart(service, enabled),
serviceManager.setAutoStart(service, enabled)
);
ipcMain.handle("service:getAutoStart", (_, service: string) =>
serviceManager.getAutoStart(service),
serviceManager.getAutoStart(service)
);
ipcMain.handle("service:startAll", () => serviceManager.startAll());
ipcMain.handle("service:stopAll", () => serviceManager.stopAll());
// PHP-CGI 管理 - 支持多版本
ipcMain.handle("service:getPhpCgiStatus", () =>
serviceManager.getPhpCgiStatus(),
);
ipcMain.handle("service:getPhpCgiStatus", () => serviceManager.getPhpCgiStatus());
ipcMain.handle("service:startPhpCgi", () => serviceManager.startPhpCgi());
ipcMain.handle("service:stopPhpCgi", () => serviceManager.stopPhpCgi());
ipcMain.handle("service:startAllPhpCgi", () => serviceManager.startAllPhpCgi());
ipcMain.handle("service:stopAllPhpCgi", () => serviceManager.stopAllPhpCgi());
ipcMain.handle("service:startPhpCgiVersion", (_, version: string) =>
serviceManager.startPhpCgiVersion(version),
);
ipcMain.handle("service:stopPhpCgiVersion", (_, version: string) =>
serviceManager.stopPhpCgiVersion(version),
);
ipcMain.handle("service:getPhpCgiPort", (_, version: string) =>
serviceManager.getPhpCgiPort(version),
);
ipcMain.handle("service:startPhpCgiVersion", (_, version: string) => serviceManager.startPhpCgiVersion(version));
ipcMain.handle("service:stopPhpCgiVersion", (_, version: string) => serviceManager.stopPhpCgiVersion(version));
ipcMain.handle("service:getPhpCgiPort", (_, version: string) => serviceManager.getPhpCgiPort(version));
// ==================== Hosts 管理 ====================
ipcMain.handle("hosts:get", () => hostsManager.getHosts());
ipcMain.handle("hosts:add", (_, domain: string, ip: string) =>
hostsManager.addHost(domain, ip),
hostsManager.addHost(domain, ip)
);
ipcMain.handle("hosts:remove", (_, domain: string) =>
hostsManager.removeHost(domain),
hostsManager.removeHost(domain)
);
// ==================== Git 管理 ====================
ipcMain.handle("git:getVersions", () => gitManager.getInstalledVersions());
ipcMain.handle("git:getAvailableVersions", () =>
gitManager.getAvailableVersions(),
gitManager.getAvailableVersions()
);
ipcMain.handle("git:install", (_, version: string) =>
gitManager.install(version),
gitManager.install(version)
);
ipcMain.handle("git:uninstall", () => gitManager.uninstall());
ipcMain.handle("git:checkSystem", () => gitManager.checkSystemGit());
ipcMain.handle("git:getConfig", () => gitManager.getGitConfig());
ipcMain.handle("git:setConfig", (_, name: string, email: string) =>
gitManager.setGitConfig(name, email),
gitManager.setGitConfig(name, email)
);
// ==================== Python 管理 ====================
ipcMain.handle("python:getVersions", () =>
pythonManager.getInstalledVersions(),
);
ipcMain.handle("python:getVersions", () => pythonManager.getInstalledVersions());
ipcMain.handle("python:getAvailableVersions", () =>
pythonManager.getAvailableVersions(),
pythonManager.getAvailableVersions()
);
ipcMain.handle("python:install", (_, version: string) =>
pythonManager.install(version),
pythonManager.install(version)
);
ipcMain.handle("python:uninstall", (_, version: string) =>
pythonManager.uninstall(version),
pythonManager.uninstall(version)
);
ipcMain.handle("python:setActive", (_, version: string) =>
pythonManager.setActive(version),
pythonManager.setActive(version)
);
ipcMain.handle("python:checkSystem", () => pythonManager.checkSystemPython());
ipcMain.handle("python:getPipInfo", (_, version: string) =>
pythonManager.getPipInfo(version),
pythonManager.getPipInfo(version)
);
ipcMain.handle(
"python:installPackage",
(_, 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:set", (_, key: string, value: any) =>
configStore.set(key, value),
configStore.set(key, value)
);
ipcMain.handle("config:getBasePath", () => configStore.getBasePath());
ipcMain.handle("config:setBasePath", (_, path: string) =>
configStore.setBasePath(path),
configStore.setBasePath(path)
);
// ==================== 应用设置 ====================
@ -666,7 +653,7 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
} catch (e) {
// 忽略删除失败
}
// 删除 VBS 脚本
const appDir = require("path").dirname(exePath);
const vbsPath = join(appDir, "silent_start.vbs");
@ -677,7 +664,7 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
// 忽略删除失败
}
}
configStore.set("autoLaunch", false);
return { success: true, message: "已禁用开机自启" };
}
@ -715,17 +702,17 @@ ipcMain.handle("app:getAutoLaunch", async () => {
ipcMain.handle("app:getVersion", async () => {
const { existsSync, readFileSync } = require("fs");
const { join } = require("path");
const version = app.getVersion();
let buildTime = "";
let buildDate = "";
// 尝试读取版本信息文件
try {
const versionFilePath = app.isPackaged
? join(process.resourcesPath, "public", "version.json")
: join(__dirname, "..", "public", "version.json");
if (existsSync(versionFilePath)) {
const versionInfo = JSON.parse(readFileSync(versionFilePath, "utf-8"));
buildTime = versionInfo.buildTime || "";
@ -734,12 +721,12 @@ ipcMain.handle("app:getVersion", async () => {
} catch (e) {
// 忽略错误
}
return {
version,
buildTime,
buildDate,
isPackaged: app.isPackaged,
isPackaged: app.isPackaged
};
});
@ -763,13 +750,9 @@ ipcMain.handle("app:quit", () => {
// ==================== 日志管理 ====================
ipcMain.handle("log:getFiles", () => logManager.getLogFiles());
ipcMain.handle("log:read", (_, logPath: string, lines?: number) =>
logManager.readLog(logPath, lines),
logManager.readLog(logPath, lines)
);
ipcMain.handle("log:clear", (_, logPath: string) =>
logManager.clearLog(logPath),
);
ipcMain.handle(
"log:getDirectory",
(_, type: "nginx" | "php" | "mysql" | "sites", version?: string) =>
logManager.getLogDirectory(type, version),
ipcMain.handle("log:clear", (_, logPath: string) => logManager.clearLog(logPath));
ipcMain.handle("log:getDirectory", (_, type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string) =>
logManager.getLogDirectory(type, version)
);

View File

@ -1,318 +1,233 @@
import { contextBridge, ipcRenderer } from "electron";
import { contextBridge, ipcRenderer } from 'electron'
// 暴露安全的 API 到渲染进程
contextBridge.exposeInMainWorld("electronAPI", {
contextBridge.exposeInMainWorld('electronAPI', {
// 窗口控制
minimize: () => ipcRenderer.invoke("window:minimize"),
maximize: () => ipcRenderer.invoke("window:maximize"),
close: () => ipcRenderer.invoke("window:close"),
minimize: () => ipcRenderer.invoke('window:minimize'),
maximize: () => ipcRenderer.invoke('window:maximize'),
close: () => ipcRenderer.invoke('window:close'),
// Shell
openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
openPath: (path: string) => ipcRenderer.invoke("shell:openPath", path),
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
openPath: (path: string) => ipcRenderer.invoke('shell:openPath', path),
// Dialog
selectDirectory: () => ipcRenderer.invoke("dialog:selectDirectory"),
selectDirectory: () => ipcRenderer.invoke('dialog:selectDirectory'),
// 下载进度监听
onDownloadProgress: (
callback: (data: {
type: string;
progress: number;
downloaded: number;
total: number;
}) => void,
) => {
ipcRenderer.on("download-progress", (_, data) => callback(data));
onDownloadProgress: (callback: (data: { type: string; progress: number; downloaded: number; total: number }) => void) => {
ipcRenderer.on('download-progress', (_, data) => callback(data))
},
removeDownloadProgressListener: () => {
ipcRenderer.removeAllListeners("download-progress");
ipcRenderer.removeAllListeners('download-progress')
},
// PHP 管理
php: {
getVersions: () => ipcRenderer.invoke("php:getVersions"),
getAvailableVersions: () => ipcRenderer.invoke("php:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("php:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("php:uninstall", version),
setActive: (version: string) =>
ipcRenderer.invoke("php:setActive", version),
getExtensions: (version: string) =>
ipcRenderer.invoke("php:getExtensions", version),
openExtensionDir: (version: string) =>
ipcRenderer.invoke("php:openExtensionDir", version),
getAvailableExtensions: (version: string, searchKeyword?: string) =>
ipcRenderer.invoke("php:getAvailableExtensions", version, searchKeyword),
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),
getVersions: () => ipcRenderer.invoke('php:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('php:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('php:install', version),
uninstall: (version: string) => ipcRenderer.invoke('php:uninstall', version),
setActive: (version: string) => ipcRenderer.invoke('php:setActive', version),
getExtensions: (version: string) => ipcRenderer.invoke('php:getExtensions', version),
openExtensionDir: (version: string) => ipcRenderer.invoke('php:openExtensionDir', version),
getAvailableExtensions: (version: string, searchKeyword?: string) => ipcRenderer.invoke('php:getAvailableExtensions', version, searchKeyword),
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: {
getStatus: () => ipcRenderer.invoke("composer:getStatus"),
install: () => ipcRenderer.invoke("composer:install"),
uninstall: () => ipcRenderer.invoke("composer:uninstall"),
setMirror: (mirror: string) =>
ipcRenderer.invoke("composer:setMirror", mirror),
createLaravelProject: (projectName: string, targetDir: string) =>
ipcRenderer.invoke(
"composer:createLaravelProject",
projectName,
targetDir,
),
getStatus: () => ipcRenderer.invoke('composer:getStatus'),
install: () => ipcRenderer.invoke('composer:install'),
uninstall: () => ipcRenderer.invoke('composer:uninstall'),
setMirror: (mirror: string) => ipcRenderer.invoke('composer:setMirror', mirror),
createLaravelProject: (projectName: string, targetDir: string) => ipcRenderer.invoke('composer:createLaravelProject', projectName, targetDir)
},
// MySQL 管理
mysql: {
getVersions: () => ipcRenderer.invoke("mysql:getVersions"),
getAvailableVersions: () =>
ipcRenderer.invoke("mysql:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("mysql:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("mysql:uninstall", version),
start: (version: string) => ipcRenderer.invoke("mysql:start", version),
stop: (version: string) => ipcRenderer.invoke("mysql:stop", version),
restart: (version: string) => ipcRenderer.invoke("mysql:restart", version),
getStatus: (version: string) =>
ipcRenderer.invoke("mysql:getStatus", version),
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),
getVersions: () => ipcRenderer.invoke('mysql:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('mysql:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('mysql:install', version),
uninstall: (version: string) => ipcRenderer.invoke('mysql:uninstall', version),
start: (version: string) => ipcRenderer.invoke('mysql:start', version),
stop: (version: string) => ipcRenderer.invoke('mysql:stop', version),
restart: (version: string) => ipcRenderer.invoke('mysql:restart', version),
getStatus: (version: string) => ipcRenderer.invoke('mysql:getStatus', version),
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: {
getVersions: () => ipcRenderer.invoke("nginx:getVersions"),
getAvailableVersions: () =>
ipcRenderer.invoke("nginx:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("nginx:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("nginx:uninstall", version),
start: () => ipcRenderer.invoke("nginx:start"),
stop: () => ipcRenderer.invoke("nginx:stop"),
restart: () => ipcRenderer.invoke("nginx:restart"),
reload: () => ipcRenderer.invoke("nginx:reload"),
getStatus: () => ipcRenderer.invoke("nginx:getStatus"),
getConfig: () => ipcRenderer.invoke("nginx:getConfig"),
saveConfig: (config: string) =>
ipcRenderer.invoke("nginx:saveConfig", config),
getSites: () => ipcRenderer.invoke("nginx:getSites"),
addSite: (site: any) => ipcRenderer.invoke("nginx:addSite", site),
removeSite: (name: string) => ipcRenderer.invoke("nginx:removeSite", name),
updateSite: (originalName: string, site: any) =>
ipcRenderer.invoke("nginx:updateSite", originalName, site),
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),
getVersions: () => ipcRenderer.invoke('nginx:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('nginx:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('nginx:install', version),
uninstall: (version: string) => ipcRenderer.invoke('nginx:uninstall', version),
start: () => ipcRenderer.invoke('nginx:start'),
stop: () => ipcRenderer.invoke('nginx:stop'),
restart: () => ipcRenderer.invoke('nginx:restart'),
reload: () => ipcRenderer.invoke('nginx:reload'),
getStatus: () => ipcRenderer.invoke('nginx:getStatus'),
getConfig: () => ipcRenderer.invoke('nginx:getConfig'),
saveConfig: (config: string) => ipcRenderer.invoke('nginx:saveConfig', config),
getSites: () => ipcRenderer.invoke('nginx:getSites'),
addSite: (site: any) => ipcRenderer.invoke('nginx:addSite', site),
removeSite: (name: string) => ipcRenderer.invoke('nginx:removeSite', name),
updateSite: (originalName: string, site: any) => ipcRenderer.invoke('nginx:updateSite', originalName, site),
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: {
getVersions: () => ipcRenderer.invoke("redis:getVersions"),
getAvailableVersions: () =>
ipcRenderer.invoke("redis:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("redis:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("redis:uninstall", version),
start: () => ipcRenderer.invoke("redis:start"),
stop: () => ipcRenderer.invoke("redis:stop"),
restart: () => ipcRenderer.invoke("redis:restart"),
getStatus: () => ipcRenderer.invoke("redis:getStatus"),
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),
getVersions: () => ipcRenderer.invoke('redis:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('redis:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('redis:install', version),
uninstall: (version: string) => ipcRenderer.invoke('redis:uninstall', version),
start: () => ipcRenderer.invoke('redis:start'),
stop: () => ipcRenderer.invoke('redis:stop'),
restart: () => ipcRenderer.invoke('redis:restart'),
getStatus: () => ipcRenderer.invoke('redis:getStatus'),
getConfig: () => ipcRenderer.invoke('redis:getConfig'),
saveConfig: (config: string) => ipcRenderer.invoke('redis:saveConfig', config)
},
// Node.js 管理
node: {
getVersions: () => ipcRenderer.invoke("node:getVersions"),
getAvailableVersions: () => ipcRenderer.invoke("node:getAvailableVersions"),
install: (version: string, downloadUrl: string) =>
ipcRenderer.invoke("node:install", version, downloadUrl),
uninstall: (version: string) =>
ipcRenderer.invoke("node:uninstall", version),
setActive: (version: string) =>
ipcRenderer.invoke("node:setActive", version),
getInfo: (version: string) => ipcRenderer.invoke("node:getInfo", version),
getVersions: () => ipcRenderer.invoke('node:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('node:getAvailableVersions'),
install: (version: string, downloadUrl: string) => ipcRenderer.invoke('node:install', version, downloadUrl),
uninstall: (version: string) => ipcRenderer.invoke('node:uninstall', version),
setActive: (version: string) => ipcRenderer.invoke('node:setActive', version),
getInfo: (version: string) => ipcRenderer.invoke('node:getInfo', version)
},
// Git 管理
git: {
getVersions: () => ipcRenderer.invoke("git:getVersions"),
getAvailableVersions: () => ipcRenderer.invoke("git:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("git:install", version),
uninstall: () => ipcRenderer.invoke("git:uninstall"),
checkSystem: () => ipcRenderer.invoke("git:checkSystem"),
getConfig: () => ipcRenderer.invoke("git:getConfig"),
setConfig: (name: string, email: string) =>
ipcRenderer.invoke("git:setConfig", name, email),
getVersions: () => ipcRenderer.invoke('git:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('git:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('git:install', version),
uninstall: () => ipcRenderer.invoke('git:uninstall'),
checkSystem: () => ipcRenderer.invoke('git:checkSystem'),
getConfig: () => ipcRenderer.invoke('git:getConfig'),
setConfig: (name: string, email: string) => ipcRenderer.invoke('git:setConfig', name, email)
},
// Python 管理
python: {
getVersions: () => ipcRenderer.invoke("python:getVersions"),
getAvailableVersions: () =>
ipcRenderer.invoke("python:getAvailableVersions"),
install: (version: string) => ipcRenderer.invoke("python:install", version),
uninstall: (version: string) =>
ipcRenderer.invoke("python:uninstall", version),
setActive: (version: string) =>
ipcRenderer.invoke("python:setActive", version),
checkSystem: () => ipcRenderer.invoke("python:checkSystem"),
getPipInfo: (version: string) =>
ipcRenderer.invoke("python:getPipInfo", version),
installPackage: (version: string, packageName: string) =>
ipcRenderer.invoke("python:installPackage", version, packageName),
getVersions: () => ipcRenderer.invoke('python:getVersions'),
getAvailableVersions: () => ipcRenderer.invoke('python:getAvailableVersions'),
install: (version: string) => ipcRenderer.invoke('python:install', version),
uninstall: (version: string) => ipcRenderer.invoke('python:uninstall', version),
setActive: (version: string) => ipcRenderer.invoke('python:setActive', version),
checkSystem: () => ipcRenderer.invoke('python:checkSystem'),
getPipInfo: (version: string) => ipcRenderer.invoke('python:getPipInfo', version),
installPackage: (version: string, packageName: string) => ipcRenderer.invoke('python:installPackage', version, packageName)
},
// Go 管理
go: {
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: {
getAll: () => ipcRenderer.invoke("service:getAll"),
setAutoStart: (service: string, enabled: boolean) =>
ipcRenderer.invoke("service:setAutoStart", service, enabled),
getAutoStart: (service: string) =>
ipcRenderer.invoke("service:getAutoStart", service),
startAll: () => ipcRenderer.invoke("service:startAll"),
stopAll: () => ipcRenderer.invoke("service:stopAll"),
getAll: () => ipcRenderer.invoke('service:getAll'),
setAutoStart: (service: string, enabled: boolean) => ipcRenderer.invoke('service:setAutoStart', service, enabled),
getAutoStart: (service: string) => ipcRenderer.invoke('service:getAutoStart', service),
startAll: () => ipcRenderer.invoke('service:startAll'),
stopAll: () => ipcRenderer.invoke('service:stopAll'),
// PHP-CGI 多版本管理
getPhpCgiStatus: () => ipcRenderer.invoke("service:getPhpCgiStatus"),
startPhpCgi: () => ipcRenderer.invoke("service:startPhpCgi"),
stopPhpCgi: () => ipcRenderer.invoke("service:stopPhpCgi"),
startAllPhpCgi: () => ipcRenderer.invoke("service:startAllPhpCgi"),
stopAllPhpCgi: () => ipcRenderer.invoke("service:stopAllPhpCgi"),
startPhpCgiVersion: (version: string) =>
ipcRenderer.invoke("service:startPhpCgiVersion", version),
stopPhpCgiVersion: (version: string) =>
ipcRenderer.invoke("service:stopPhpCgiVersion", version),
getPhpCgiPort: (version: string) =>
ipcRenderer.invoke("service:getPhpCgiPort", version),
getPhpCgiStatus: () => ipcRenderer.invoke('service:getPhpCgiStatus'),
startPhpCgi: () => ipcRenderer.invoke('service:startPhpCgi'),
stopPhpCgi: () => ipcRenderer.invoke('service:stopPhpCgi'),
startAllPhpCgi: () => ipcRenderer.invoke('service:startAllPhpCgi'),
stopAllPhpCgi: () => ipcRenderer.invoke('service:stopAllPhpCgi'),
startPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:startPhpCgiVersion', version),
stopPhpCgiVersion: (version: string) => ipcRenderer.invoke('service:stopPhpCgiVersion', version),
getPhpCgiPort: (version: string) => ipcRenderer.invoke('service:getPhpCgiPort', version)
},
// Hosts 管理
hosts: {
get: () => ipcRenderer.invoke("hosts:get"),
add: (domain: string, ip: string) =>
ipcRenderer.invoke("hosts:add", domain, ip),
remove: (domain: string) => ipcRenderer.invoke("hosts:remove", domain),
get: () => ipcRenderer.invoke('hosts:get'),
add: (domain: string, ip: string) => ipcRenderer.invoke('hosts:add', domain, ip),
remove: (domain: string) => ipcRenderer.invoke('hosts:remove', domain)
},
// 配置管理
config: {
get: (key: string) => ipcRenderer.invoke("config:get", key),
set: (key: string, value: any) =>
ipcRenderer.invoke("config:set", key, value),
getBasePath: () => ipcRenderer.invoke("config:getBasePath"),
setBasePath: (path: string) =>
ipcRenderer.invoke("config:setBasePath", path),
get: (key: string) => ipcRenderer.invoke('config:get', key),
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value),
getBasePath: () => ipcRenderer.invoke('config:getBasePath'),
setBasePath: (path: string) => ipcRenderer.invoke('config:setBasePath', path)
},
// 日志管理
log: {
getFiles: () => ipcRenderer.invoke("log:getFiles"),
read: (logPath: string, lines?: number) =>
ipcRenderer.invoke("log:read", logPath, lines),
clear: (logPath: string) => ipcRenderer.invoke("log:clear", logPath),
getDirectory: (
type: "nginx" | "php" | "mysql" | "sites",
version?: string,
) => ipcRenderer.invoke("log:getDirectory", type, version),
getFiles: () => ipcRenderer.invoke('log:getFiles'),
read: (logPath: string, lines?: number) => ipcRenderer.invoke('log:read', logPath, lines),
clear: (logPath: string) => ipcRenderer.invoke('log:clear', logPath),
getDirectory: (type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string) =>
ipcRenderer.invoke('log:getDirectory', type, version)
},
// 应用设置
app: {
setAutoLaunch: (enabled: boolean) =>
ipcRenderer.invoke("app:setAutoLaunch", enabled),
getAutoLaunch: () => ipcRenderer.invoke("app:getAutoLaunch"),
setStartMinimized: (enabled: boolean) =>
ipcRenderer.invoke("app:setStartMinimized", enabled),
getStartMinimized: () => ipcRenderer.invoke("app:getStartMinimized"),
getVersion: () =>
ipcRenderer.invoke("app:getVersion") as Promise<{
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"),
setAutoLaunch: (enabled: boolean) => ipcRenderer.invoke('app:setAutoLaunch', enabled),
getAutoLaunch: () => ipcRenderer.invoke('app:getAutoLaunch'),
setStartMinimized: (enabled: boolean) => ipcRenderer.invoke('app:setStartMinimized', enabled),
getStartMinimized: () => ipcRenderer.invoke('app:getStartMinimized'),
getVersion: () => ipcRenderer.invoke('app:getVersion') as Promise<{ 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) => {
ipcRenderer.on("service-status-changed", callback);
ipcRenderer.on('service-status-changed', callback)
},
removeServiceStatusChangedListener: (callback: () => void) => {
ipcRenderer.removeListener("service-status-changed", callback);
},
});
ipcRenderer.removeListener('service-status-changed', callback)
}
})
// 声明 Window 接口扩展
declare global {
interface Window {
electronAPI: typeof api;
electronAPI: typeof api
}
}
const api = {
minimize: () => ipcRenderer.invoke("window:minimize"),
maximize: () => ipcRenderer.invoke("window:maximize"),
close: () => ipcRenderer.invoke("window:close"),
openExternal: (url: string) => ipcRenderer.invoke("shell:openExternal", url),
openPath: (path: string) => ipcRenderer.invoke("shell:openPath", path),
minimize: () => ipcRenderer.invoke('window:minimize'),
maximize: () => ipcRenderer.invoke('window:maximize'),
close: () => ipcRenderer.invoke('window:close'),
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
openPath: (path: string) => ipcRenderer.invoke('shell:openPath', path),
php: {} as any,
mysql: {} as any,
nginx: {} as any,
redis: {} as any,
go: {} as any,
service: {} as any,
hosts: {} as any,
config: {} as any,
};
config: {} as any
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

16
jest.config.js Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "phper-dev-manager",
"version": "1.0.10",
"version": "1.0.7",
"description": "PHP开发环境管理器 - 管理PHP、MySQL、Nginx、Redis服务",
"main": "dist-electron/main.js",
"scripts": {
@ -14,18 +14,24 @@
"preview": "vite preview",
"electron:dev": "vite",
"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",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^20.10.0",
"@vitejs/plugin-vue": "^4.5.0",
"concurrently": "^8.2.2",
"electron": "^28.0.0",
"electron-builder": "^24.9.1",
"fast-check": "^4.5.3",
"jest": "^30.2.0",
"rcedit": "^5.0.2",
"sass": "^1.69.5",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3",
"vite": "^5.0.0",
"vite-plugin-electron": "^0.15.5",
@ -75,9 +81,8 @@
}
],
"icon": "build/icon.ico",
"executableName": "PHPer开发环境管理器",
"requestedExecutionLevel": "requireAdministrator",
"signAndEditExecutable": true
"signAndEditExecutable": false
},
"nsis": {
"oneClick": false,

View File

@ -1,5 +1,5 @@
{
"version": "1.0.10",
"buildTime": "2026-02-05T01:05:37.725Z",
"buildDate": "2026/2/5"
"version": "1.0.7",
"buildTime": "2025-12-31T07:09:02.287Z",
"buildDate": "2025/12/31"
}

View File

@ -29,21 +29,20 @@
<!-- 侧边栏 -->
<aside class="sidebar">
<nav class="nav-menu">
<router-link
v-for="item in menuItems"
:key="item.path"
<router-link
v-for="item in menuItems"
:key="item.path"
:to="item.path"
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>
<span class="nav-label">{{ item.label }}</span>
<span
v-if="item.service"
<span
v-if="item.service"
class="status-dot"
:class="{
running:
serviceStatus[item.service as keyof typeof serviceStatus],
}"></span>
:class="{ running: serviceStatus[item.service as keyof typeof serviceStatus] }"
></span>
</router-link>
</nav>
@ -74,309 +73,301 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from "vue";
import { ElMessage } from "element-plus";
import { useServiceStore } from "./stores/serviceStore";
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { useServiceStore } from './stores/serviceStore'
const store = useServiceStore();
const store = useServiceStore()
const isDark = ref(true);
const startingAll = ref(false);
const stoppingAll = ref(false);
const isDark = ref(true)
const startingAll = ref(false)
const stoppingAll = ref(false)
// -
const cachedViews = [
"Dashboard",
"PhpManager",
"MysqlManager",
"NginxManager",
"RedisManager",
"NodeManager",
"GoManager",
"PythonManager",
"GitManager",
"SitesManager",
"HostsManager",
"Settings",
];
// -
const cachedViews = [
'Dashboard',
'PhpManager',
'MysqlManager',
'NginxManager',
'RedisManager',
'NodeManager',
'PythonManager',
'GoManager',
'GitManager',
'SitesManager',
'HostsManager',
'Settings'
]
// store
const serviceStatus = computed(() => ({
nginx: store.serviceStatus.nginx,
mysql: store.serviceStatus.mysql,
redis: store.serviceStatus.redis,
}));
// store
const serviceStatus = computed(() => ({
nginx: store.serviceStatus.nginx,
mysql: store.serviceStatus.mysql,
redis: store.serviceStatus.redis
}))
const menuItems = [
{ path: "/", label: "仪表盘", icon: "Odometer", service: null },
{ path: "/php", label: "PHP 管理", icon: "Files", service: null },
{ path: "/mysql", label: "MySQL 管理", icon: "Coin", service: "mysql" },
{
path: "/nginx",
label: "Nginx 管理",
icon: "Connection",
service: "nginx",
},
{ path: "/redis", label: "Redis 管理", icon: "Grid", service: "redis" },
{
path: "/nodejs",
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 },
];
const menuItems = [
{ path: '/', label: '仪表盘', icon: 'Odometer', service: null },
{ path: '/php', label: 'PHP 管理', icon: 'Files', service: null },
{ path: '/mysql', label: 'MySQL 管理', icon: 'Coin', service: 'mysql' },
{ path: '/nginx', label: 'Nginx 管理', icon: 'Connection', service: 'nginx' },
{ path: '/redis', label: 'Redis 管理', icon: 'Grid', service: 'redis' },
{ path: '/nodejs', label: 'Node.js 管理', icon: 'Promotion', service: null },
{ path: '/python', label: 'Python 管理', icon: 'Platform', service: null },
{ path: '/go', label: 'Go 管理', icon: 'Box', 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 maximize = () => window.electronAPI?.maximize();
const close = () => window.electronAPI?.close();
//
const minimize = () => window.electronAPI?.minimize()
const maximize = () => window.electronAPI?.maximize()
const close = () => window.electronAPI?.close()
//
const toggleDark = () => {
isDark.value = !isDark.value;
document.documentElement.classList.toggle("dark", isDark.value);
};
//
const toggleDark = () => {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
}
//
const startAll = async () => {
startingAll.value = true;
try {
const result = await window.electronAPI?.service.startAll();
if (result?.success) {
ElMessage.success(result.message);
//
setTimeout(() => store.refreshServiceStatus(), 2000);
} else {
ElMessage.error(result?.message || "启动失败");
}
} catch (error: any) {
ElMessage.error(error.message);
} finally {
startingAll.value = false;
//
const startAll = async () => {
startingAll.value = true
try {
const result = await window.electronAPI?.service.startAll()
if (result?.success) {
ElMessage.success(result.message)
//
setTimeout(() => store.refreshServiceStatus(), 2000)
} else {
ElMessage.error(result?.message || '启动失败')
}
};
} catch (error: any) {
ElMessage.error(error.message)
} finally {
startingAll.value = false
}
}
//
const stopAll = async () => {
stoppingAll.value = true;
try {
const result = await window.electronAPI?.service.stopAll();
if (result?.success) {
ElMessage.success(result.message);
await store.refreshServiceStatus();
} else {
ElMessage.error(result?.message || "停止失败");
}
} catch (error: any) {
ElMessage.error(error.message);
} finally {
stoppingAll.value = false;
//
const stopAll = async () => {
stoppingAll.value = true
try {
const result = await window.electronAPI?.service.stopAll()
if (result?.success) {
ElMessage.success(result.message)
await store.refreshServiceStatus()
} else {
ElMessage.error(result?.message || '停止失败')
}
};
} catch (error: any) {
ElMessage.error(error.message)
} finally {
stoppingAll.value = false
}
}
onMounted(() => {
document.documentElement.classList.add("dark");
//
store.refreshAll();
// 5
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000);
});
onMounted(() => {
document.documentElement.classList.add('dark')
//
store.refreshAll()
// 5
statusInterval = setInterval(() => store.refreshServiceStatus(), 5000)
})
onUnmounted(() => {
if (statusInterval) {
clearInterval(statusInterval);
}
});
onUnmounted(() => {
if (statusInterval) {
clearInterval(statusInterval)
}
})
</script>
<style lang="scss" scoped>
.app-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
.app-container {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
}
.title-bar {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-titlebar);
border-bottom: 1px solid var(--border-color);
-webkit-app-region: drag;
padding: 0 12px;
}
.title-bar-left {
display: flex;
align-items: center;
gap: 12px;
}
.app-logo {
display: flex;
align-items: center;
gap: 8px;
.logo-icon {
width: 24px;
height: 24px;
}
.app-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
font-family: 'Noto Sans SC', 'Microsoft YaHei', sans-serif;
}
}
.title-bar-right {
display: flex;
gap: 4px;
-webkit-app-region: no-drag;
}
.title-btn {
width: 36px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
&:hover {
background: var(--bg-hover);
color: var(--text-primary);
overflow: hidden;
}
.title-bar {
height: 40px;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--bg-titlebar);
border-bottom: 1px solid var(--border-color);
-webkit-app-region: drag;
padding: 0 12px;
&.close-btn:hover {
background: #e81123;
color: white;
}
}
.main-container {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 220px;
background: var(--bg-sidebar);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 16px 12px;
}
.nav-menu {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 10px;
text-decoration: none;
color: var(--text-secondary);
transition: all 0.2s;
&:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.title-bar-left {
display: flex;
align-items: center;
gap: 12px;
}
.app-logo {
display: flex;
align-items: center;
gap: 8px;
.logo-icon {
width: 24px;
height: 24px;
}
.app-name {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
font-family: "Noto Sans SC", "Microsoft YaHei", sans-serif;
}
}
.title-bar-right {
display: flex;
gap: 4px;
-webkit-app-region: no-drag;
}
.title-btn {
width: 36px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
&:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
&.close-btn:hover {
background: #e81123;
color: white;
}
}
.main-container {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 220px;
background: var(--bg-sidebar);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 16px 12px;
}
.nav-menu {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 10px;
text-decoration: none;
color: var(--text-secondary);
transition: all 0.2s;
&:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
&.active {
background: var(--accent-gradient);
color: white;
box-shadow: 0 4px 12px rgba(123, 97, 255, 0.3);
.status-dot {
border-color: rgba(255, 255, 255, 0.3);
}
}
.nav-icon {
font-size: 20px;
}
.nav-label {
font-size: 14px;
font-weight: 500;
flex: 1;
}
&.active {
background: var(--accent-gradient);
color: white;
box-shadow: 0 4px 12px rgba(123, 97, 255, 0.3);
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6b7280;
border: 2px solid var(--bg-sidebar);
transition: all 0.3s;
&.running {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
}
border-color: rgba(255, 255, 255, 0.3);
}
}
.sidebar-footer {
padding-top: 16px;
border-top: 1px solid var(--border-color);
.nav-icon {
font-size: 20px;
}
.quick-actions {
display: flex;
flex-direction: column;
gap: 10px;
padding: 0 12px;
:deep(.el-button) {
width: 100% !important;
height: 40px !important;
min-width: 100% !important;
max-width: 100% !important;
font-size: 14px !important;
justify-content: center !important;
border-radius: 8px !important;
padding: 0 16px !important;
margin-left: 0 !important;
}
:deep(.el-button + .el-button) {
margin-left: 0 !important;
}
}
.content {
.nav-label {
font-size: 14px;
font-weight: 500;
flex: 1;
padding: 24px;
overflow-y: auto;
background: var(--bg-content);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6b7280;
border: 2px solid var(--bg-sidebar);
transition: all 0.3s;
&.running {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
}
}
}
.sidebar-footer {
padding-top: 16px;
border-top: 1px solid var(--border-color);
}
.quick-actions {
display: flex;
flex-direction: column;
gap: 10px;
padding: 0 12px;
:deep(.el-button) {
width: 100% !important;
height: 40px !important;
min-width: 100% !important;
max-width: 100% !important;
font-size: 14px !important;
justify-content: center !important;
border-radius: 8px !important;
padding: 0 16px !important;
margin-left: 0 !important;
}
:deep(.el-button + .el-button) {
margin-left: 0 !important;
}
}
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
background: var(--bg-content);
}
</style>

View File

@ -1,81 +1,82 @@
import { createRouter, createWebHashHistory } from "vue-router";
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/",
name: "dashboard",
component: () => import("@/views/Dashboard.vue"),
meta: { title: "仪表盘" },
path: '/',
name: 'dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '仪表盘' }
},
{
path: "/php",
name: "php",
component: () => import("@/views/PhpManager.vue"),
meta: { title: "PHP 管理" },
path: '/php',
name: 'php',
component: () => import('@/views/PhpManager.vue'),
meta: { title: 'PHP 管理' }
},
{
path: "/mysql",
name: "mysql",
component: () => import("@/views/MysqlManager.vue"),
meta: { title: "MySQL 管理" },
path: '/mysql',
name: 'mysql',
component: () => import('@/views/MysqlManager.vue'),
meta: { title: 'MySQL 管理' }
},
{
path: "/nginx",
name: "nginx",
component: () => import("@/views/NginxManager.vue"),
meta: { title: "Nginx 管理" },
path: '/nginx',
name: 'nginx',
component: () => import('@/views/NginxManager.vue'),
meta: { title: 'Nginx 管理' }
},
{
path: "/redis",
name: "redis",
component: () => import("@/views/RedisManager.vue"),
meta: { title: "Redis 管理" },
path: '/redis',
name: 'redis',
component: () => import('@/views/RedisManager.vue'),
meta: { title: 'Redis 管理' }
},
{
path: "/nodejs",
name: "nodejs",
component: () => import("@/views/NodeManager.vue"),
meta: { title: "Node.js 管理" },
path: '/nodejs',
name: 'nodejs',
component: () => import('@/views/NodeManager.vue'),
meta: { title: 'Node.js 管理' }
},
{
path: "/go",
name: "go",
component: () => import("@/views/GoManager.vue"),
meta: { title: "Go 管理" },
path: '/sites',
name: 'sites',
component: () => import('@/views/SitesManager.vue'),
meta: { title: '站点管理' }
},
{
path: "/sites",
name: "sites",
component: () => import("@/views/SitesManager.vue"),
meta: { title: "站点管理" },
path: '/hosts',
name: 'hosts',
component: () => import('@/views/HostsManager.vue'),
meta: { title: 'Hosts 管理' }
},
{
path: "/hosts",
name: "hosts",
component: () => import("@/views/HostsManager.vue"),
meta: { title: "Hosts 管理" },
path: '/git',
name: 'git',
component: () => import('@/views/GitManager.vue'),
meta: { title: 'Git 管理' }
},
{
path: "/git",
name: "git",
component: () => import("@/views/GitManager.vue"),
meta: { title: "Git 管理" },
path: '/python',
name: 'python',
component: () => import('@/views/PythonManager.vue'),
meta: { title: 'Python 管理' }
},
{
path: "/python",
name: "python",
component: () => import("@/views/PythonManager.vue"),
meta: { title: "Python 管理" },
path: '/go',
name: 'go',
component: () => import('@/views/GoManager.vue'),
meta: { title: 'Go 管理' }
},
{
path: "/settings",
name: "settings",
component: () => import("@/views/Settings.vue"),
meta: { title: "设置" },
},
],
});
path: '/settings',
name: 'settings',
component: () => import('@/views/Settings.vue'),
meta: { title: '设置' }
}
]
})
export default router
export default router;

View File

@ -30,6 +30,15 @@ interface NodeVersion {
isActive: boolean
}
// Go 版本信息
interface GoVersion {
version: string
path: string
isActive: boolean
goroot: string
gopath?: string
}
// 站点信息
interface SiteConfig {
name: string
@ -56,6 +65,9 @@ export const useServiceStore = defineStore('service', () => {
// Node.js 版本列表
const nodeVersions = ref<NodeVersion[]>([])
// Go 版本列表
const goVersions = ref<GoVersion[]>([])
// 站点列表
const sites = ref<SiteConfig[]>([])
@ -99,6 +111,11 @@ export const useServiceStore = defineStore('service', () => {
return nodeVersions.value.find(v => v.isActive)
})
// 计算属性:当前活动的 Go 版本
const activeGoVersion = computed(() => {
return goVersions.value.find(v => v.isActive)
})
// 刷新所有状态
async function refreshAll() {
loading.value = true
@ -107,6 +124,7 @@ export const useServiceStore = defineStore('service', () => {
refreshServiceStatus(),
refreshPhpVersions(),
refreshNodeVersions(),
refreshGoVersions(),
refreshSites(),
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() {
try {
@ -217,6 +247,7 @@ export const useServiceStore = defineStore('service', () => {
serviceStatus,
phpVersions,
nodeVersions,
goVersions,
sites,
basePath,
loading,
@ -227,11 +258,13 @@ export const useServiceStore = defineStore('service', () => {
runningServiceCount,
activePhpVersion,
activeNodeVersion,
activeGoVersion,
// 方法
refreshAll,
refreshServiceStatus,
refreshPhpVersions,
refreshNodeVersions,
refreshGoVersions,
refreshSites,
refreshBasePath,
updateServiceStatus,

View File

@ -2,24 +2,17 @@
<div class="page-container">
<div class="page-header">
<h1 class="page-title">
<span class="title-icon"
><el-icon><Aim /></el-icon
></span>
<span class="title-icon"><el-icon><Platform /></el-icon></span>
Go 管理
</h1>
<p class="page-description">管理本地 Go 版本支持多版本切换</p>
</div>
<!-- 下载进度 -->
<div
v-if="downloadProgress.percent > 0 && downloadProgress.percent < 100"
class="download-progress">
<div v-if="downloadProgress.percent > 0 && downloadProgress.percent < 100" class="download-progress">
<div class="progress-info">
<span>正在下载 Go...</span>
<span
>{{ formatSize(downloadProgress.downloaded) }} /
{{ formatSize(downloadProgress.total) }}</span
>
<span>{{ formatSize(downloadProgress.downloaded) }} / {{ formatSize(downloadProgress.total) }}</span>
</div>
<el-progress :percentage="downloadProgress.percent" :stroke-width="10" />
</div>
@ -34,63 +27,84 @@
</el-button>
</div>
<div class="card-content">
<div v-if="versions.length > 0" class="version-grid">
<div
v-for="version in versions"
<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
v-for="version in versions"
:key="version.version"
class="version-card"
:class="{ active: version.isActive }">
:class="{ active: version.isActive }"
>
<div class="version-main">
<div class="version-icon">
<el-icon :size="32"><Aim /></el-icon>
<el-icon :size="32"><Platform /></el-icon>
</div>
<div class="version-content">
<div class="version-title">
<span class="version-number">Go {{ version.version }}</span>
<el-tag
v-if="version.isActive"
type="success"
size="small"
effect="dark"
>当前版本</el-tag
>
<el-tag v-if="version.isActive" type="success" size="small" effect="dark">当前版本</el-tag>
</div>
<div class="version-meta">
<div class="version-path">
<el-icon><FolderOpened /></el-icon>
<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 class="version-actions">
<el-button
<el-button
v-if="!version.isActive"
type="primary"
size="small"
type="primary"
size="small"
@click="setActiveVersion(version.version)"
:loading="settingActive === version.version">
:loading="settingActive === version.version"
>
设为默认
</el-button>
<el-button
type="danger"
size="small"
<el-button
type="danger"
size="small"
plain
@click="uninstallVersion(version.version)"
:loading="uninstalling === version.version">
:loading="uninstalling === version.version"
>
卸载
</el-button>
</div>
</div>
</div>
<el-empty v-else description="暂未安装 Go" />
</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">
<template #title>
<el-icon><InfoFilled /></el-icon>
下载源说明
</template>
Go 将从官方网站
<a href="https://go.dev/dl/" target="_blank">go.dev/dl</a> 下载 Windows
amd64 版本
Go 将从官方网站 <a href="https://golang.org/dl/" target="_blank">golang.org</a> 下载 Windows 64位版本
</el-alert>
<div v-if="loadingAvailableVersions" class="loading-state">
<el-icon class="is-loading"><Loading /></el-icon>
@ -100,27 +114,28 @@
<span>暂无可用版本</span>
</div>
<div v-else class="available-versions">
<el-table
:data="availableVersions"
style="width: 100%"
max-height="400">
<el-table-column prop="version" label="版本" width="140" />
<el-table-column label="类型" width="100">
<el-table :data="availableVersions" style="width: 100%" max-height="400">
<el-table-column prop="version" label="版本" width="120" />
<el-table-column label="类型" width="120">
<template #default="{ row }">
<el-tag v-if="row.stable" type="success" size="small"
>Stable</el-tag
>
<el-tag v-else type="warning" size="small">Unstable</el-tag>
<el-tag v-if="row.stable" type="success" size="small">稳定版</el-tag>
<el-tag v-else type="warning" size="small">开发版</el-tag>
</template>
</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 }">
<el-button
v-if="!isInstalled(row.version)"
type="primary"
size="small"
@click="installVersion(row)"
:loading="installing === row.version">
:loading="installing === row.version"
>
安装
</el-button>
<el-tag v-else type="info" size="small">已安装</el-tag>
@ -136,304 +151,369 @@
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Aim, InfoFilled, Loading } from "@element-plus/icons-vue";
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Platform, InfoFilled, Loading, FolderOpened, Files, Box } from '@element-plus/icons-vue'
defineOptions({
name: "GoManager",
});
// 便 KeepAlive
defineOptions({
name: 'GoManager'
})
interface GoVersion {
version: string;
path: string;
isActive: boolean;
interface GoVersion {
version: string
path: string
isActive: boolean
goroot: string
gopath?: string
installDate?: Date
size?: number
}
interface AvailableGoVersion {
version: string
stable: boolean
downloadUrl: string
size: number
sha256: string
releaseDate?: string
}
const loading = ref(false)
const versions = ref<GoVersion[]>([])
const availableVersions = ref<AvailableGoVersion[]>([])
const showInstallDialog = ref(false)
const installing = ref('')
const uninstalling = ref('')
const settingActive = ref('')
const downloadProgress = reactive({
percent: 0,
downloaded: 0,
total: 0
})
const loadVersions = async () => {
loading.value = true
try {
versions.value = await window.electronAPI?.go.getVersions() || []
} catch (error: any) {
console.error('加载版本失败:', error)
ElMessage.error('加载已安装版本失败: ' + error.message)
} finally {
loading.value = false
}
}
interface AvailableGoVersion {
version: string;
stable: boolean;
downloadUrl: string;
filename: string;
const loadingAvailableVersions = ref(false)
const loadAvailableVersions = async () => {
loadingAvailableVersions.value = true
try {
availableVersions.value = await window.electronAPI?.go.getAvailableVersions() || []
} catch (error: any) {
console.error('加载可用版本失败:', error)
ElMessage.error('加载可用版本失败: ' + error.message)
} finally {
loadingAvailableVersions.value = false
}
}
const versions = ref<GoVersion[]>([]);
const availableVersions = ref<AvailableGoVersion[]>([]);
const showInstallDialog = ref(false);
const installing = ref("");
const uninstalling = ref("");
const settingActive = ref("");
const isInstalled = (version: string) => {
return versions.value.some(v => v.version === version)
}
const downloadProgress = reactive({
percent: 0,
downloaded: 0,
total: 0,
});
const loadVersions = async () => {
try {
versions.value = (await window.electronAPI?.go.getVersions()) || [];
} catch (error: any) {
console.error("加载版本失败:", error);
const installVersion = async (row: AvailableGoVersion) => {
installing.value = row.version
downloadProgress.percent = 0
downloadProgress.downloaded = 0
downloadProgress.total = 0
try {
const result = await window.electronAPI?.go.install(row.version, row.downloadUrl, row.sha256)
if (result?.success) {
ElMessage.success(result.message)
await loadVersions()
} else {
ElMessage.error(result?.message || '安装失败')
}
};
} catch (error: any) {
ElMessage.error(error.message)
} finally {
installing.value = ''
downloadProgress.percent = 0
}
}
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 {
loadingAvailableVersions.value = false;
const uninstallVersion = async (version: string) => {
try {
await ElMessageBox.confirm(
`确定要卸载 Go ${version} 吗?`,
'确认卸载',
{ type: 'warning' }
)
uninstalling.value = version
const result = await window.electronAPI?.go.uninstall(version)
if (result?.success) {
ElMessage.success(result.message)
await loadVersions()
} else {
ElMessage.error(result?.message || '卸载失败')
}
};
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 {
const result = await window.electronAPI?.go.install(
row.version,
row.downloadUrl,
);
if (result?.success) {
ElMessage.success(result.message);
await loadVersions();
} else {
ElMessage.error(result?.message || "安装失败");
}
} catch (error: any) {
ElMessage.error(error.message);
} finally {
installing.value = "";
downloadProgress.percent = 0;
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message)
}
};
} finally {
uninstalling.value = ''
}
}
const uninstallVersion = async (version: string) => {
try {
await ElMessageBox.confirm(`确定要卸载 Go ${version} 吗?`, "确认卸载", {
type: "warning",
});
uninstalling.value = version;
const result = await window.electronAPI?.go.uninstall(version);
if (result?.success) {
ElMessage.success(result.message);
await loadVersions();
} else {
ElMessage.error(result?.message || "卸载失败");
}
} catch (error: any) {
if (error !== "cancel") {
ElMessage.error(error.message);
}
} finally {
uninstalling.value = "";
const setActiveVersion = async (version: string) => {
settingActive.value = version
try {
const result = await window.electronAPI?.go.setActive(version)
if (result?.success) {
ElMessage.success(result.message)
await loadVersions()
} else {
ElMessage.error(result?.message || '设置失败')
}
};
} catch (error: any) {
ElMessage.error(error.message)
} finally {
settingActive.value = ''
}
}
const setActiveVersion = async (version: string) => {
settingActive.value = version;
try {
const result = await window.electronAPI?.go.setActive(version);
if (result?.success) {
ElMessage.success(result.message);
await loadVersions();
} else {
ElMessage.error(result?.message || "设置失败");
}
} catch (error: any) {
ElMessage.error(error.message);
} finally {
settingActive.value = "";
}
};
const formatSize = (bytes: number) => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const formatSize = (bytes: number) => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
//
const onDownloadProgress = (_event: any, data: any) => {
if (data.type === 'go') {
downloadProgress.percent = data.progress
downloadProgress.downloaded = data.downloaded
downloadProgress.total = data.total
}
}
const onDownloadProgress = (data: any) => {
if (data.type === "go") {
downloadProgress.percent = data.progress;
downloadProgress.downloaded = data.downloaded;
downloadProgress.total = data.total;
}
};
onMounted(() => {
loadVersions()
loadAvailableVersions()
window.electronAPI?.onDownloadProgress(onDownloadProgress)
})
onMounted(() => {
loadVersions();
loadAvailableVersions();
window.electronAPI?.onDownloadProgress(onDownloadProgress);
});
onUnmounted(() => {
window.electronAPI?.removeDownloadProgressListener(onDownloadProgress);
});
onUnmounted(() => {
window.electronAPI?.removeDownloadProgressListener(onDownloadProgress)
})
</script>
<style lang="scss" scoped>
.version-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
.loading-state {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px;
color: var(--text-secondary);
.is-loading {
font-size: 24px;
animation: spin 1s linear infinite;
}
}
.version-card {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
transition: all 0.3s;
display: flex;
flex-direction: column;
gap: 20px;
@keyframes spin {
to { transform: rotate(360deg); }
}
&:hover {
border-color: var(--accent-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.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;
}
.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;
}
}
&.active {
border-color: var(--success-color);
background: linear-gradient(
135deg,
rgba(16, 185, 129, 0.08) 0%,
rgba(16, 185, 129, 0.02) 100%
);
.version-icon {
background: linear-gradient(135deg, #00add8 0%, #00add8 100%);
}
}
.version-main {
display: flex;
align-items: flex-start;
gap: 16px;
}
.version-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 20px;
}
.version-card {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
transition: all 0.3s;
display: flex;
flex-direction: column;
gap: 20px;
&:hover {
border-color: var(--accent-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
&.active {
border-color: var(--success-color);
background: linear-gradient(135deg, rgba(16, 185, 129, 0.08) 0%, rgba(16, 185, 129, 0.02) 100%);
.version-icon {
width: 56px;
height: 56px;
border-radius: 14px;
background: linear-gradient(135deg, #00add8 0%, #00add8 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
.version-content {
flex: 1;
min-width: 0;
}
.version-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.version-number {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.5px;
}
.version-actions {
display: flex;
gap: 10px;
padding-top: 4px;
border-top: 1px solid var(--border-color);
.el-button {
flex: 1;
}
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
}
.available-versions {
.el-table {
--el-table-bg-color: transparent;
--el-table-tr-bg-color: transparent;
--el-table-header-bg-color: var(--bg-input);
}
.version-main {
display: flex;
align-items: flex-start;
gap: 16px;
}
.download-progress {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
color: var(--text-secondary);
}
}
.loading-state {
.version-icon {
width: 56px;
height: 56px;
border-radius: 14px;
background: linear-gradient(135deg, #00add8 0%, #007d9c 100%);
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 40px;
color: white;
flex-shrink: 0;
}
.version-content {
flex: 1;
min-width: 0;
}
.version-title {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.version-number {
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
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);
.is-loading {
font-size: 24px;
animation: spin 1s linear infinite;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.mb-4 {
margin-bottom: 16px;
a {
background: var(--bg-card);
padding: 6px 12px;
border-radius: 6px;
font-family: 'Fira Code', monospace;
.el-icon {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
flex-shrink: 0;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.empty-hint {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
.version-actions {
display: flex;
gap: 10px;
padding-top: 4px;
border-top: 1px solid var(--border-color);
.el-button {
flex: 1;
}
}
</style>
}
.available-versions {
.el-table {
--el-table-bg-color: transparent;
--el-table-tr-bg-color: transparent;
--el-table-header-bg-color: var(--bg-input);
}
}
.download-progress {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
.progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
color: var(--text-secondary);
}
}
.empty-hint {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.mb-4 {
margin-bottom: 16px;
a {
color: var(--accent-color);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
</style>

View File

@ -352,34 +352,21 @@
</div>
<div v-else class="extensions-list">
<div class="extensions-count">
找到 {{ availableExtensions.length }} 扩展
找到 {{ availableExtensions.length }} 适用于 PHP {{ currentVersion }} 扩展
</div>
<div
v-for="ext in availableExtensions"
:key="ext.name"
class="extension-item"
:class="{ 'not-available': ext.notAvailableReason }"
>
<div class="ext-info">
<div class="ext-main">
<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>
<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>
<!-- 有明确不可用原因时显示不支持 -->
<el-tooltip v-if="ext.notAvailableReason" :content="ext.notAvailableReason" placement="top">
<el-button type="info" size="small" disabled>
不支持
</el-button>
</el-tooltip>
<!-- 否则显示安装按钮 -->
<el-button
v-else
type="primary"
size="small"
@click="installExtension(ext)"
@ -428,7 +415,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted, onActivated } from 'vue'
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 LogViewer from '@/components/LogViewer.vue'
@ -464,8 +451,6 @@ interface AvailableExtension {
downloadUrl: string
description?: string
packageName?: string // Packagist PIE
supportedPhpVersions?: string[] // PHP
notAvailableReason?: string //
}
const loading = ref(false)
@ -1096,11 +1081,6 @@ onUnmounted(() => {
border-bottom: none;
}
&.not-available {
opacity: 0.7;
background-color: rgba(0, 0, 0, 0.02);
}
.ext-info {
display: flex;
flex-direction: column;
@ -1121,14 +1101,6 @@ onUnmounted(() => {
font-size: 12px;
color: var(--text-muted);
}
.ext-not-available {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--warning-color, #e6a23c);
}
}
}