Implement Git and Python management features in Electron app, including version retrieval, installation, and configuration management. Enhance site configuration to support reverse proxy settings, updating UI components accordingly for better user experience.
This commit is contained in:
parent
a91146c4e9
commit
de6d3b8c51
@ -15,6 +15,8 @@ import { RedisManager } from "./services/RedisManager";
|
|||||||
import { NodeManager } from "./services/NodeManager";
|
import { NodeManager } from "./services/NodeManager";
|
||||||
import { ServiceManager } from "./services/ServiceManager";
|
import { ServiceManager } from "./services/ServiceManager";
|
||||||
import { HostsManager } from "./services/HostsManager";
|
import { HostsManager } from "./services/HostsManager";
|
||||||
|
import { GitManager } from "./services/GitManager";
|
||||||
|
import { PythonManager } from "./services/PythonManager";
|
||||||
import { ConfigStore } from "./services/ConfigStore";
|
import { ConfigStore } from "./services/ConfigStore";
|
||||||
|
|
||||||
// 获取图标路径
|
// 获取图标路径
|
||||||
@ -116,6 +118,8 @@ const redisManager = new RedisManager(configStore);
|
|||||||
const nodeManager = new NodeManager(configStore);
|
const nodeManager = new NodeManager(configStore);
|
||||||
const serviceManager = new ServiceManager(configStore);
|
const serviceManager = new ServiceManager(configStore);
|
||||||
const hostsManager = new HostsManager();
|
const hostsManager = new HostsManager();
|
||||||
|
const gitManager = new GitManager(configStore);
|
||||||
|
const pythonManager = new PythonManager(configStore);
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const appIcon = createWindowIcon();
|
const appIcon = createWindowIcon();
|
||||||
@ -521,6 +525,45 @@ 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()
|
||||||
|
);
|
||||||
|
ipcMain.handle("git:install", (_, version: string) =>
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== Python 管理 ====================
|
||||||
|
ipcMain.handle("python:getVersions", () => pythonManager.getInstalledVersions());
|
||||||
|
ipcMain.handle("python:getAvailableVersions", () =>
|
||||||
|
pythonManager.getAvailableVersions()
|
||||||
|
);
|
||||||
|
ipcMain.handle("python:install", (_, version: string) =>
|
||||||
|
pythonManager.install(version)
|
||||||
|
);
|
||||||
|
ipcMain.handle("python:uninstall", (_, version: string) =>
|
||||||
|
pythonManager.uninstall(version)
|
||||||
|
);
|
||||||
|
ipcMain.handle("python:setActive", (_, version: string) =>
|
||||||
|
pythonManager.setActive(version)
|
||||||
|
);
|
||||||
|
ipcMain.handle("python:checkSystem", () => pythonManager.checkSystemPython());
|
||||||
|
ipcMain.handle("python:getPipInfo", (_, version: string) =>
|
||||||
|
pythonManager.getPipInfo(version)
|
||||||
|
);
|
||||||
|
ipcMain.handle(
|
||||||
|
"python:installPackage",
|
||||||
|
(_, version: string, packageName: string) =>
|
||||||
|
pythonManager.installPackage(version, packageName)
|
||||||
|
);
|
||||||
|
|
||||||
// ==================== 配置管理 ====================
|
// ==================== 配置管理 ====================
|
||||||
ipcMain.handle("config:get", (_, key: string) => configStore.get(key));
|
ipcMain.handle("config:get", (_, key: string) => configStore.get(key));
|
||||||
ipcMain.handle("config:set", (_, key: string, value: any) =>
|
ipcMain.handle("config:set", (_, key: string, value: any) =>
|
||||||
|
|||||||
@ -110,6 +110,29 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getInfo: (version: string) => ipcRenderer.invoke('node:getInfo', 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)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
},
|
||||||
|
|
||||||
// 服务管理
|
// 服务管理
|
||||||
service: {
|
service: {
|
||||||
getAll: () => ipcRenderer.invoke('service:getAll'),
|
getAll: () => ipcRenderer.invoke('service:getAll'),
|
||||||
|
|||||||
@ -31,6 +31,9 @@ export interface SiteConfig {
|
|||||||
isLaravel: boolean;
|
isLaravel: boolean;
|
||||||
ssl: boolean;
|
ssl: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
// 反向代理配置
|
||||||
|
isProxy?: boolean;
|
||||||
|
proxyTarget?: string; // 代理目标地址,如 http://127.0.0.1:3000
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取应用安装目录下的 data 路径
|
// 获取应用安装目录下的 data 路径
|
||||||
|
|||||||
458
electron/services/GitManager.ts
Normal file
458
electron/services/GitManager.ts
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
import { ConfigStore } from './ConfigStore'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import { existsSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, rmdirSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
import https from 'https'
|
||||||
|
import http from 'http'
|
||||||
|
import { createWriteStream } from 'fs'
|
||||||
|
import { sendDownloadProgress } from '../main'
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
interface GitVersion {
|
||||||
|
version: string
|
||||||
|
path: string
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvailableGitVersion {
|
||||||
|
version: string
|
||||||
|
downloadUrl: string
|
||||||
|
type: 'portable' | 'installer'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GitManager {
|
||||||
|
private configStore: ConfigStore
|
||||||
|
|
||||||
|
constructor(configStore: ConfigStore) {
|
||||||
|
this.configStore = configStore
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Git 安装路径
|
||||||
|
*/
|
||||||
|
getGitPath(): string {
|
||||||
|
return join(this.configStore.getBasePath(), 'git')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已安装的 Git 版本
|
||||||
|
*/
|
||||||
|
async getInstalledVersions(): Promise<GitVersion[]> {
|
||||||
|
const versions: GitVersion[] = []
|
||||||
|
const gitPath = this.getGitPath()
|
||||||
|
|
||||||
|
if (!existsSync(gitPath)) {
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否存在 git.exe
|
||||||
|
const gitExe = join(gitPath, 'cmd', 'git.exe')
|
||||||
|
const gitExeAlt = join(gitPath, 'bin', 'git.exe')
|
||||||
|
|
||||||
|
if (existsSync(gitExe) || existsSync(gitExeAlt)) {
|
||||||
|
try {
|
||||||
|
const exePath = existsSync(gitExe) ? gitExe : gitExeAlt
|
||||||
|
const { stdout } = await execAsync(`"${exePath}" --version`, {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
const match = stdout.match(/git version (\d+\.\d+\.\d+)/)
|
||||||
|
if (match) {
|
||||||
|
versions.push({
|
||||||
|
version: match[1],
|
||||||
|
path: gitPath,
|
||||||
|
isActive: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取 Git 版本失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用的 Git 版本列表
|
||||||
|
*/
|
||||||
|
async getAvailableVersions(): Promise<AvailableGitVersion[]> {
|
||||||
|
// Git for Windows 便携版下载地址
|
||||||
|
// https://github.com/git-for-windows/git/releases
|
||||||
|
const versions: AvailableGitVersion[] = [
|
||||||
|
{
|
||||||
|
version: '2.47.1',
|
||||||
|
downloadUrl: 'https://github.com/git-for-windows/git/releases/download/v2.47.1.windows.1/PortableGit-2.47.1-64-bit.7z.exe',
|
||||||
|
type: 'portable'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '2.46.2',
|
||||||
|
downloadUrl: 'https://github.com/git-for-windows/git/releases/download/v2.46.2.windows.1/PortableGit-2.46.2-64-bit.7z.exe',
|
||||||
|
type: 'portable'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '2.45.2',
|
||||||
|
downloadUrl: 'https://github.com/git-for-windows/git/releases/download/v2.45.2.windows.1/PortableGit-2.45.2-64-bit.7z.exe',
|
||||||
|
type: 'portable'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 过滤掉已安装的版本
|
||||||
|
const installed = await this.getInstalledVersions()
|
||||||
|
const installedVersions = installed.map(v => v.version)
|
||||||
|
|
||||||
|
return versions.filter(v => !installedVersions.includes(v.version))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装 Git
|
||||||
|
*/
|
||||||
|
async install(version: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const available = await this.getAvailableVersions()
|
||||||
|
const versionInfo = available.find(v => v.version === version)
|
||||||
|
|
||||||
|
if (!versionInfo) {
|
||||||
|
return { success: false, message: `未找到 Git ${version} 版本` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const gitPath = this.getGitPath()
|
||||||
|
const tempPath = this.configStore.getTempPath()
|
||||||
|
const downloadPath = join(tempPath, `PortableGit-${version}.7z.exe`)
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if (!existsSync(tempPath)) {
|
||||||
|
mkdirSync(tempPath, { recursive: true })
|
||||||
|
}
|
||||||
|
if (!existsSync(gitPath)) {
|
||||||
|
mkdirSync(gitPath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`开始下载 Git ${version} 从 ${versionInfo.downloadUrl}`)
|
||||||
|
|
||||||
|
// 下载 Git
|
||||||
|
await this.downloadFile(versionInfo.downloadUrl, downloadPath)
|
||||||
|
|
||||||
|
console.log('下载完成,开始解压...')
|
||||||
|
|
||||||
|
// 解压便携版 Git(自解压 7z)
|
||||||
|
// 使用命令行运行自解压程序
|
||||||
|
try {
|
||||||
|
await execAsync(`"${downloadPath}" -o"${gitPath}" -y`, {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 300000 // 5分钟超时
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
// 自解压可能不返回正确的退出码,检查文件是否存在
|
||||||
|
const gitExe = join(gitPath, 'cmd', 'git.exe')
|
||||||
|
const gitExeAlt = join(gitPath, 'bin', 'git.exe')
|
||||||
|
if (!existsSync(gitExe) && !existsSync(gitExeAlt)) {
|
||||||
|
throw new Error(`解压失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('解压完成')
|
||||||
|
|
||||||
|
// 删除临时文件
|
||||||
|
if (existsSync(downloadPath)) {
|
||||||
|
unlinkSync(downloadPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到环境变量
|
||||||
|
await this.addToPath()
|
||||||
|
|
||||||
|
return { success: true, message: `Git ${version} 安装成功` }
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Git 安装失败:', error)
|
||||||
|
return { success: false, message: `安装失败: ${error.message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载 Git
|
||||||
|
*/
|
||||||
|
async uninstall(): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const gitPath = this.getGitPath()
|
||||||
|
|
||||||
|
if (!existsSync(gitPath)) {
|
||||||
|
return { success: false, message: 'Git 未安装' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从环境变量移除
|
||||||
|
await this.removeFromPath()
|
||||||
|
|
||||||
|
// 删除目录
|
||||||
|
this.removeDirectory(gitPath)
|
||||||
|
|
||||||
|
return { success: true, message: 'Git 已卸载' }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, message: `卸载失败: ${error.message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查系统是否已安装 Git
|
||||||
|
*/
|
||||||
|
async checkSystemGit(): Promise<{ installed: boolean; version?: string; path?: string }> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('git --version', {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
const match = stdout.match(/git version (\d+\.\d+\.\d+)/)
|
||||||
|
|
||||||
|
// 获取 git 路径
|
||||||
|
try {
|
||||||
|
const { stdout: wherePath } = await execAsync('where git', {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
const gitExePath = wherePath.trim().split('\n')[0]
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
version: match ? match[1] : 'unknown',
|
||||||
|
path: gitExePath
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
version: match ? match[1] : 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { installed: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Git 配置
|
||||||
|
*/
|
||||||
|
async getGitConfig(): Promise<{ name?: string; email?: string }> {
|
||||||
|
try {
|
||||||
|
let name: string | undefined
|
||||||
|
let email: string | undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout: nameOut } = await execAsync('git config --global user.name', {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
name = nameOut.trim()
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout: emailOut } = await execAsync('git config --global user.email', {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
email = emailOut.trim()
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return { name, email }
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Git 配置
|
||||||
|
*/
|
||||||
|
async setGitConfig(name: string, email: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
if (name) {
|
||||||
|
await execAsync(`git config --global user.name "${name}"`, {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email) {
|
||||||
|
await execAsync(`git config --global user.email "${email}"`, {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: 'Git 配置已保存' }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, message: `设置失败: ${error.message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有方法 ====================
|
||||||
|
|
||||||
|
private async downloadFile(url: string, dest: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const file = createWriteStream(dest)
|
||||||
|
const protocol = url.startsWith('https') ? https : http
|
||||||
|
|
||||||
|
const request = protocol.get(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||||
|
}
|
||||||
|
}, (response) => {
|
||||||
|
// 处理重定向
|
||||||
|
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||||
|
const redirectUrl = response.headers.location
|
||||||
|
if (redirectUrl) {
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
this.downloadFile(redirectUrl, dest).then(resolve).catch(reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
reject(new Error(`下载失败,状态码: ${response.statusCode}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = parseInt(response.headers['content-length'] || '0', 10)
|
||||||
|
let downloadedSize = 0
|
||||||
|
let lastProgressTime = Date.now()
|
||||||
|
|
||||||
|
response.on('data', (chunk) => {
|
||||||
|
downloadedSize += chunk.length
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastProgressTime > 500) {
|
||||||
|
const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0
|
||||||
|
sendDownloadProgress('git', progress, downloadedSize, totalSize)
|
||||||
|
lastProgressTime = now
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
response.pipe(file)
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close()
|
||||||
|
sendDownloadProgress('git', 100, totalSize, totalSize)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
file.on('error', (err) => {
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
request.on('error', (err) => {
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
reject(new Error(`网络错误: ${err.message}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
request.setTimeout(600000, () => {
|
||||||
|
request.destroy()
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
reject(new Error('下载超时(10分钟)'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addToPath(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const gitPath = this.getGitPath()
|
||||||
|
const cmdPath = join(gitPath, 'cmd')
|
||||||
|
const binPath = join(gitPath, 'bin')
|
||||||
|
|
||||||
|
const tempScriptPath = join(this.configStore.getTempPath(), 'add_git_path.ps1')
|
||||||
|
mkdirSync(this.configStore.getTempPath(), { recursive: true })
|
||||||
|
|
||||||
|
const psScript = `
|
||||||
|
param([string]$CmdPath, [string]$BinPath)
|
||||||
|
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||||
|
if ($userPath -eq $null) { $userPath = '' }
|
||||||
|
|
||||||
|
$paths = $userPath -split ';' | Where-Object { $_ -ne '' -and $_.Trim() -ne '' }
|
||||||
|
|
||||||
|
# 移除旧的 Git 路径
|
||||||
|
$filteredPaths = @()
|
||||||
|
foreach ($p in $paths) {
|
||||||
|
$pathLower = $p.ToLower()
|
||||||
|
if (-not ($pathLower -like '*\\git\\*' -or $pathLower -like '*\\git-*')) {
|
||||||
|
$filteredPaths += $p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加新路径
|
||||||
|
$allPaths = @($CmdPath, $BinPath) + $filteredPaths
|
||||||
|
$newPath = $allPaths -join ';'
|
||||||
|
|
||||||
|
[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
|
||||||
|
Write-Host "SUCCESS: Git path added"
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(tempScriptPath, psScript, 'utf-8')
|
||||||
|
|
||||||
|
await execAsync(
|
||||||
|
`powershell -ExecutionPolicy Bypass -File "${tempScriptPath}" -CmdPath "${cmdPath}" -BinPath "${binPath}"`,
|
||||||
|
{ windowsHide: true, timeout: 30000 }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existsSync(tempScriptPath)) {
|
||||||
|
unlinkSync(tempScriptPath)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('添加 Git 到 PATH 失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeFromPath(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const gitPath = this.getGitPath()
|
||||||
|
|
||||||
|
const tempScriptPath = join(this.configStore.getTempPath(), 'remove_git_path.ps1')
|
||||||
|
mkdirSync(this.configStore.getTempPath(), { recursive: true })
|
||||||
|
|
||||||
|
const psScript = `
|
||||||
|
param([string]$GitBasePath)
|
||||||
|
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||||
|
if ($userPath -eq $null) { exit 0 }
|
||||||
|
|
||||||
|
$gitPathLower = $GitBasePath.ToLower()
|
||||||
|
$paths = $userPath -split ';' | Where-Object {
|
||||||
|
$_ -ne '' -and -not $_.ToLower().StartsWith($gitPathLower)
|
||||||
|
}
|
||||||
|
$newPath = $paths -join ';'
|
||||||
|
|
||||||
|
[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
|
||||||
|
Write-Host "SUCCESS: Git path removed"
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(tempScriptPath, psScript, 'utf-8')
|
||||||
|
|
||||||
|
await execAsync(
|
||||||
|
`powershell -ExecutionPolicy Bypass -File "${tempScriptPath}" -GitBasePath "${gitPath}"`,
|
||||||
|
{ windowsHide: true, timeout: 30000 }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existsSync(tempScriptPath)) {
|
||||||
|
unlinkSync(tempScriptPath)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('从 PATH 移除 Git 失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeDirectory(dir: string): void {
|
||||||
|
if (existsSync(dir)) {
|
||||||
|
const files = readdirSync(dir, { withFileTypes: true })
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = join(dir, file.name)
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
this.removeDirectory(fullPath)
|
||||||
|
} else {
|
||||||
|
unlinkSync(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rmdirSync(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -389,9 +389,14 @@ export class NginxManager {
|
|||||||
async addSite(site: SiteConfig): Promise<{ success: boolean; message: string }> {
|
async addSite(site: SiteConfig): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
// 生成配置文件
|
// 生成配置文件
|
||||||
const config = site.isLaravel
|
let config: string
|
||||||
? this.generateLaravelSiteConfig(site)
|
if (site.isProxy && site.proxyTarget) {
|
||||||
: this.generateSiteConfig(site)
|
config = this.generateProxySiteConfig(site)
|
||||||
|
} else if (site.isLaravel) {
|
||||||
|
config = this.generateLaravelSiteConfig(site)
|
||||||
|
} else {
|
||||||
|
config = this.generateSiteConfig(site)
|
||||||
|
}
|
||||||
|
|
||||||
const sitesAvailable = this.configStore.getSitesAvailablePath()
|
const sitesAvailable = this.configStore.getSitesAvailablePath()
|
||||||
const configPath = join(sitesAvailable, `${site.name}.conf`)
|
const configPath = join(sitesAvailable, `${site.name}.conf`)
|
||||||
@ -450,9 +455,14 @@ export class NginxManager {
|
|||||||
const wasEnabled = existsSync(enabledPath)
|
const wasEnabled = existsSync(enabledPath)
|
||||||
|
|
||||||
// 生成新的配置内容
|
// 生成新的配置内容
|
||||||
const config = site.isLaravel
|
let config: string
|
||||||
? this.generateLaravelSiteConfig(site)
|
if (site.isProxy && site.proxyTarget) {
|
||||||
: this.generateSiteConfig(site)
|
config = this.generateProxySiteConfig(site)
|
||||||
|
} else if (site.isLaravel) {
|
||||||
|
config = this.generateLaravelSiteConfig(site)
|
||||||
|
} else {
|
||||||
|
config = this.generateSiteConfig(site)
|
||||||
|
}
|
||||||
|
|
||||||
// 写入配置文件
|
// 写入配置文件
|
||||||
writeFileSync(configPath, config)
|
writeFileSync(configPath, config)
|
||||||
@ -694,6 +704,75 @@ server {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成反向代理站点配置
|
||||||
|
*/
|
||||||
|
private generateProxySiteConfig(site: SiteConfig): string {
|
||||||
|
const logsPath = this.configStore.getLogsPath()
|
||||||
|
const proxyTarget = site.proxyTarget || 'http://127.0.0.1:3000'
|
||||||
|
|
||||||
|
let config = `
|
||||||
|
# Reverse Proxy Site
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ${site.domain};
|
||||||
|
|
||||||
|
access_log "${logsPath.replace(/\\/g, '/')}/${site.name}-access.log";
|
||||||
|
error_log "${logsPath.replace(/\\/g, '/')}/${site.name}-error.log";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass ${proxyTarget};
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# WebSocket 支持
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
if (site.ssl) {
|
||||||
|
const sslPath = join(this.configStore.getSSLPath(), site.domain)
|
||||||
|
config += `
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name ${site.domain};
|
||||||
|
|
||||||
|
ssl_certificate "${sslPath.replace(/\\/g, '/')}/${site.domain}-chain.pem";
|
||||||
|
ssl_certificate_key "${sslPath.replace(/\\/g, '/')}/${site.domain}-key.pem";
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
|
access_log "${logsPath.replace(/\\/g, '/')}/${site.name}-ssl-access.log";
|
||||||
|
error_log "${logsPath.replace(/\\/g, '/')}/${site.name}-ssl-error.log";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass ${proxyTarget};
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
|
# WebSocket 支持
|
||||||
|
proxy_read_timeout 86400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
private generateLaravelSiteConfig(site: SiteConfig): string {
|
private generateLaravelSiteConfig(site: SiteConfig): string {
|
||||||
const logsPath = this.configStore.getLogsPath()
|
const logsPath = this.configStore.getLogsPath()
|
||||||
// Laravel 项目 public 目录
|
// Laravel 项目 public 目录
|
||||||
|
|||||||
547
electron/services/PythonManager.ts
Normal file
547
electron/services/PythonManager.ts
Normal file
@ -0,0 +1,547 @@
|
|||||||
|
import { ConfigStore } from './ConfigStore'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import { existsSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, rmdirSync } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
import https from 'https'
|
||||||
|
import http from 'http'
|
||||||
|
import { createWriteStream } from 'fs'
|
||||||
|
import { sendDownloadProgress } from '../main'
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
interface PythonVersion {
|
||||||
|
version: string
|
||||||
|
path: string
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvailablePythonVersion {
|
||||||
|
version: string
|
||||||
|
downloadUrl: string
|
||||||
|
type: 'embed' | 'installer'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PythonManager {
|
||||||
|
private configStore: ConfigStore
|
||||||
|
|
||||||
|
constructor(configStore: ConfigStore) {
|
||||||
|
this.configStore = configStore
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Python 基础安装路径
|
||||||
|
*/
|
||||||
|
getPythonBasePath(): string {
|
||||||
|
return join(this.configStore.getBasePath(), 'python')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定版本的 Python 路径
|
||||||
|
*/
|
||||||
|
getPythonPath(version: string): string {
|
||||||
|
return join(this.getPythonBasePath(), `python-${version}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已安装的 Python 版本
|
||||||
|
*/
|
||||||
|
async getInstalledVersions(): Promise<PythonVersion[]> {
|
||||||
|
const versions: PythonVersion[] = []
|
||||||
|
const pythonBasePath = this.getPythonBasePath()
|
||||||
|
const activeVersion = this.configStore.get('activePythonVersion') as string || ''
|
||||||
|
|
||||||
|
if (!existsSync(pythonBasePath)) {
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirs = readdirSync(pythonBasePath, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const dir of dirs) {
|
||||||
|
if (dir.isDirectory() && dir.name.startsWith('python-')) {
|
||||||
|
const version = dir.name.replace('python-', '')
|
||||||
|
const pythonPath = join(pythonBasePath, dir.name)
|
||||||
|
const pythonExe = join(pythonPath, 'python.exe')
|
||||||
|
|
||||||
|
if (existsSync(pythonExe)) {
|
||||||
|
versions.push({
|
||||||
|
version,
|
||||||
|
path: pythonPath,
|
||||||
|
isActive: version === activeVersion
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions.sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取可用的 Python 版本列表
|
||||||
|
* 使用 Python 嵌入式版本(免安装)
|
||||||
|
*/
|
||||||
|
async getAvailableVersions(): Promise<AvailablePythonVersion[]> {
|
||||||
|
// Python 嵌入式版本下载地址
|
||||||
|
// https://www.python.org/downloads/windows/
|
||||||
|
const versions: AvailablePythonVersion[] = [
|
||||||
|
{
|
||||||
|
version: '3.13.1',
|
||||||
|
downloadUrl: 'https://www.python.org/ftp/python/3.13.1/python-3.13.1-embed-amd64.zip',
|
||||||
|
type: 'embed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '3.12.8',
|
||||||
|
downloadUrl: 'https://www.python.org/ftp/python/3.12.8/python-3.12.8-embed-amd64.zip',
|
||||||
|
type: 'embed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '3.11.11',
|
||||||
|
downloadUrl: 'https://www.python.org/ftp/python/3.11.11/python-3.11.11-embed-amd64.zip',
|
||||||
|
type: 'embed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '3.10.16',
|
||||||
|
downloadUrl: 'https://www.python.org/ftp/python/3.10.16/python-3.10.16-embed-amd64.zip',
|
||||||
|
type: 'embed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '3.9.21',
|
||||||
|
downloadUrl: 'https://www.python.org/ftp/python/3.9.21/python-3.9.21-embed-amd64.zip',
|
||||||
|
type: 'embed'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 过滤掉已安装的版本
|
||||||
|
const installed = await this.getInstalledVersions()
|
||||||
|
const installedVersions = installed.map(v => v.version)
|
||||||
|
|
||||||
|
return versions.filter(v => !installedVersions.includes(v.version))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装 Python
|
||||||
|
*/
|
||||||
|
async install(version: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const available = await this.getAvailableVersions()
|
||||||
|
const versionInfo = available.find(v => v.version === version)
|
||||||
|
|
||||||
|
if (!versionInfo) {
|
||||||
|
return { success: false, message: `未找到 Python ${version} 版本` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pythonPath = this.getPythonPath(version)
|
||||||
|
const tempPath = this.configStore.getTempPath()
|
||||||
|
const zipPath = join(tempPath, `python-${version}.zip`)
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
if (!existsSync(tempPath)) {
|
||||||
|
mkdirSync(tempPath, { recursive: true })
|
||||||
|
}
|
||||||
|
if (!existsSync(pythonPath)) {
|
||||||
|
mkdirSync(pythonPath, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`开始下载 Python ${version} 从 ${versionInfo.downloadUrl}`)
|
||||||
|
|
||||||
|
// 下载 Python
|
||||||
|
await this.downloadFile(versionInfo.downloadUrl, zipPath)
|
||||||
|
|
||||||
|
console.log('下载完成,开始解压...')
|
||||||
|
|
||||||
|
// 解压
|
||||||
|
await this.unzip(zipPath, pythonPath)
|
||||||
|
|
||||||
|
console.log('解压完成')
|
||||||
|
|
||||||
|
// 删除临时文件
|
||||||
|
if (existsSync(zipPath)) {
|
||||||
|
unlinkSync(zipPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置 pip(嵌入式版本需要额外配置)
|
||||||
|
await this.setupPip(pythonPath, version)
|
||||||
|
|
||||||
|
// 如果是第一个安装的版本,设为默认
|
||||||
|
const installed = await this.getInstalledVersions()
|
||||||
|
if (installed.length === 1) {
|
||||||
|
await this.setActive(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: `Python ${version} 安装成功` }
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Python 安装失败:', error)
|
||||||
|
return { success: false, message: `安装失败: ${error.message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载 Python
|
||||||
|
*/
|
||||||
|
async uninstall(version: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const pythonPath = this.getPythonPath(version)
|
||||||
|
|
||||||
|
if (!existsSync(pythonPath)) {
|
||||||
|
return { success: false, message: `Python ${version} 未安装` }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是当前活动版本,清除
|
||||||
|
const activeVersion = this.configStore.get('activePythonVersion')
|
||||||
|
if (activeVersion === version) {
|
||||||
|
await this.removeFromPath(pythonPath)
|
||||||
|
this.configStore.set('activePythonVersion' as any, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除目录
|
||||||
|
this.removeDirectory(pythonPath)
|
||||||
|
|
||||||
|
return { success: true, message: `Python ${version} 已卸载` }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, message: `卸载失败: ${error.message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置活动的 Python 版本
|
||||||
|
*/
|
||||||
|
async setActive(version: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const pythonPath = this.getPythonPath(version)
|
||||||
|
|
||||||
|
if (!existsSync(pythonPath)) {
|
||||||
|
return { success: false, message: `Python ${version} 未安装` }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pythonExe = join(pythonPath, 'python.exe')
|
||||||
|
if (!existsSync(pythonExe)) {
|
||||||
|
return { success: false, message: `Python ${version} 安装不完整` }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到环境变量
|
||||||
|
await this.addToPath(pythonPath)
|
||||||
|
|
||||||
|
// 更新配置
|
||||||
|
this.configStore.set('activePythonVersion' as any, version)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Python ${version} 已设置为默认版本\n\n环境变量已更新,新开的终端窗口中将生效。`
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, message: `设置失败: ${error.message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查系统是否已安装 Python
|
||||||
|
*/
|
||||||
|
async checkSystemPython(): Promise<{ installed: boolean; version?: string; path?: string }> {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('python --version', {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
const match = stdout.match(/Python (\d+\.\d+\.\d+)/)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout: wherePath } = await execAsync('where python', {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
const pythonExePath = wherePath.trim().split('\n')[0]
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
version: match ? match[1] : 'unknown',
|
||||||
|
path: pythonExePath
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
version: match ? match[1] : 'unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { installed: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 pip 信息
|
||||||
|
*/
|
||||||
|
async getPipInfo(version: string): Promise<{ installed: boolean; version?: string }> {
|
||||||
|
try {
|
||||||
|
const pythonPath = this.getPythonPath(version)
|
||||||
|
const pythonExe = join(pythonPath, 'python.exe')
|
||||||
|
|
||||||
|
const { stdout } = await execAsync(`"${pythonExe}" -m pip --version`, {
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
const match = stdout.match(/pip (\d+\.\d+(?:\.\d+)?)/)
|
||||||
|
|
||||||
|
return {
|
||||||
|
installed: true,
|
||||||
|
version: match ? match[1] : 'unknown'
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return { installed: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装 pip 包
|
||||||
|
*/
|
||||||
|
async installPackage(version: string, packageName: string): Promise<{ success: boolean; message: string }> {
|
||||||
|
try {
|
||||||
|
const pythonPath = this.getPythonPath(version)
|
||||||
|
const pythonExe = join(pythonPath, 'python.exe')
|
||||||
|
|
||||||
|
const { stdout, stderr } = await execAsync(
|
||||||
|
`"${pythonExe}" -m pip install ${packageName}`,
|
||||||
|
{
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 300000 // 5分钟
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('pip install output:', stdout)
|
||||||
|
if (stderr) console.log('pip install stderr:', stderr)
|
||||||
|
|
||||||
|
return { success: true, message: `${packageName} 安装成功` }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, message: `安装失败: ${error.message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有方法 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置 pip(嵌入式版本需要额外配置)
|
||||||
|
*/
|
||||||
|
private async setupPip(pythonPath: string, version: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 修改 python*._pth 文件以启用 pip
|
||||||
|
const majorMinor = version.split('.').slice(0, 2).join('')
|
||||||
|
const pthFile = join(pythonPath, `python${majorMinor}._pth`)
|
||||||
|
|
||||||
|
if (existsSync(pthFile)) {
|
||||||
|
const { readFileSync } = require('fs')
|
||||||
|
let content = readFileSync(pthFile, 'utf-8')
|
||||||
|
// 取消注释 import site
|
||||||
|
content = content.replace(/^#import site/m, 'import site')
|
||||||
|
writeFileSync(pthFile, content)
|
||||||
|
console.log('已启用 site 模块')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载并安装 pip
|
||||||
|
const pythonExe = join(pythonPath, 'python.exe')
|
||||||
|
const getPipUrl = 'https://bootstrap.pypa.io/get-pip.py'
|
||||||
|
const getPipPath = join(pythonPath, 'get-pip.py')
|
||||||
|
|
||||||
|
console.log('下载 get-pip.py...')
|
||||||
|
await this.downloadFile(getPipUrl, getPipPath)
|
||||||
|
|
||||||
|
console.log('安装 pip...')
|
||||||
|
try {
|
||||||
|
await execAsync(`"${pythonExe}" "${getPipPath}"`, {
|
||||||
|
cwd: pythonPath,
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 300000
|
||||||
|
})
|
||||||
|
console.log('pip 安装成功')
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log('pip 安装提示:', e.message)
|
||||||
|
// pip 可能已经安装成功,忽略某些错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
if (existsSync(getPipPath)) {
|
||||||
|
unlinkSync(getPipPath)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('pip 配置失败:', error)
|
||||||
|
// 不抛出错误,pip 配置失败不影响 Python 使用
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async downloadFile(url: string, dest: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const file = createWriteStream(dest)
|
||||||
|
const protocol = url.startsWith('https') ? https : http
|
||||||
|
|
||||||
|
const request = protocol.get(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||||
|
}
|
||||||
|
}, (response) => {
|
||||||
|
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||||
|
const redirectUrl = response.headers.location
|
||||||
|
if (redirectUrl) {
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
this.downloadFile(redirectUrl, dest).then(resolve).catch(reject)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
reject(new Error(`下载失败,状态码: ${response.statusCode}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = parseInt(response.headers['content-length'] || '0', 10)
|
||||||
|
let downloadedSize = 0
|
||||||
|
let lastProgressTime = Date.now()
|
||||||
|
|
||||||
|
response.on('data', (chunk) => {
|
||||||
|
downloadedSize += chunk.length
|
||||||
|
const now = Date.now()
|
||||||
|
if (now - lastProgressTime > 500) {
|
||||||
|
const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0
|
||||||
|
sendDownloadProgress('python', progress, downloadedSize, totalSize)
|
||||||
|
lastProgressTime = now
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
response.pipe(file)
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close()
|
||||||
|
sendDownloadProgress('python', 100, totalSize, totalSize)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
file.on('error', (err) => {
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
request.on('error', (err) => {
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
reject(new Error(`网络错误: ${err.message}`))
|
||||||
|
})
|
||||||
|
|
||||||
|
request.setTimeout(600000, () => {
|
||||||
|
request.destroy()
|
||||||
|
file.close()
|
||||||
|
if (existsSync(dest)) unlinkSync(dest)
|
||||||
|
reject(new Error('下载超时'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async unzip(zipPath: string, destPath: string): Promise<void> {
|
||||||
|
const { createReadStream } = await import('fs')
|
||||||
|
const unzipper = await import('unzipper')
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
createReadStream(zipPath)
|
||||||
|
.pipe(unzipper.Extract({ path: destPath }))
|
||||||
|
.on('close', resolve)
|
||||||
|
.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async addToPath(pythonPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const scriptsPath = join(pythonPath, 'Scripts')
|
||||||
|
|
||||||
|
const tempScriptPath = join(this.configStore.getTempPath(), 'add_python_path.ps1')
|
||||||
|
mkdirSync(this.configStore.getTempPath(), { recursive: true })
|
||||||
|
|
||||||
|
const psScript = `
|
||||||
|
param([string]$PythonPath, [string]$ScriptsPath)
|
||||||
|
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||||
|
if ($userPath -eq $null) { $userPath = '' }
|
||||||
|
|
||||||
|
$paths = $userPath -split ';' | Where-Object { $_ -ne '' -and $_.Trim() -ne '' }
|
||||||
|
|
||||||
|
# 移除旧的 Python 路径
|
||||||
|
$filteredPaths = @()
|
||||||
|
foreach ($p in $paths) {
|
||||||
|
$pathLower = $p.ToLower()
|
||||||
|
if (-not ($pathLower -like '*\\python\\python-*' -or $pathLower -like '*\\python-*\\*')) {
|
||||||
|
$filteredPaths += $p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加新路径
|
||||||
|
$allPaths = @($PythonPath, $ScriptsPath) + $filteredPaths
|
||||||
|
$newPath = $allPaths -join ';'
|
||||||
|
|
||||||
|
[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
|
||||||
|
Write-Host "SUCCESS: Python path added"
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(tempScriptPath, psScript, 'utf-8')
|
||||||
|
|
||||||
|
await execAsync(
|
||||||
|
`powershell -ExecutionPolicy Bypass -File "${tempScriptPath}" -PythonPath "${pythonPath}" -ScriptsPath "${scriptsPath}"`,
|
||||||
|
{ windowsHide: true, timeout: 30000 }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existsSync(tempScriptPath)) {
|
||||||
|
unlinkSync(tempScriptPath)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('添加 Python 到 PATH 失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeFromPath(pythonPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tempScriptPath = join(this.configStore.getTempPath(), 'remove_python_path.ps1')
|
||||||
|
mkdirSync(this.configStore.getTempPath(), { recursive: true })
|
||||||
|
|
||||||
|
const psScript = `
|
||||||
|
param([string]$PythonBasePath)
|
||||||
|
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
|
||||||
|
if ($userPath -eq $null) { exit 0 }
|
||||||
|
|
||||||
|
$pythonPathLower = $PythonBasePath.ToLower()
|
||||||
|
$paths = $userPath -split ';' | Where-Object {
|
||||||
|
$_ -ne '' -and -not $_.ToLower().StartsWith($pythonPathLower)
|
||||||
|
}
|
||||||
|
$newPath = $paths -join ';'
|
||||||
|
|
||||||
|
[Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
|
||||||
|
Write-Host "SUCCESS: Python path removed"
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(tempScriptPath, psScript, 'utf-8')
|
||||||
|
|
||||||
|
await execAsync(
|
||||||
|
`powershell -ExecutionPolicy Bypass -File "${tempScriptPath}" -PythonBasePath "${pythonPath}"`,
|
||||||
|
{ windowsHide: true, timeout: 30000 }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (existsSync(tempScriptPath)) {
|
||||||
|
unlinkSync(tempScriptPath)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('从 PATH 移除 Python 失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeDirectory(dir: string): void {
|
||||||
|
if (existsSync(dir)) {
|
||||||
|
const files = readdirSync(dir, { withFileTypes: true })
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = join(dir, file.name)
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
this.removeDirectory(fullPath)
|
||||||
|
} else {
|
||||||
|
unlinkSync(fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rmdirSync(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { ConfigStore } from './ConfigStore'
|
import { ConfigStore } from './ConfigStore'
|
||||||
import { exec, spawn } from 'child_process'
|
import { exec, spawn } from 'child_process'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { existsSync, writeFileSync, readFileSync, mkdirSync } from 'fs'
|
import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync } from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
const execAsync = promisify(exec)
|
const execAsync = promisify(exec)
|
||||||
@ -330,7 +330,7 @@ export class ServiceManager {
|
|||||||
try {
|
try {
|
||||||
// 停止 PHP-CGI
|
// 停止 PHP-CGI
|
||||||
if (await this.checkProcess('php-cgi.exe')) {
|
if (await this.checkProcess('php-cgi.exe')) {
|
||||||
await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 }).catch(() => {})
|
await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 }).catch(() => { })
|
||||||
details.push('PHP-CGI 已停止')
|
details.push('PHP-CGI 已停止')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,14 +340,14 @@ export class ServiceManager {
|
|||||||
try {
|
try {
|
||||||
await execAsync(`"${join(nginxPath, 'nginx.exe')}" -s stop`, { cwd: nginxPath, timeout: 5000 })
|
await execAsync(`"${join(nginxPath, 'nginx.exe')}" -s stop`, { cwd: nginxPath, timeout: 5000 })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await execAsync('taskkill /F /IM nginx.exe', { timeout: 5000 }).catch(() => {})
|
await execAsync('taskkill /F /IM nginx.exe', { timeout: 5000 }).catch(() => { })
|
||||||
}
|
}
|
||||||
details.push('Nginx 已停止')
|
details.push('Nginx 已停止')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止 MySQL
|
// 停止 MySQL
|
||||||
if (await this.checkProcess('mysqld.exe')) {
|
if (await this.checkProcess('mysqld.exe')) {
|
||||||
await execAsync('taskkill /F /IM mysqld.exe', { timeout: 5000 }).catch(() => {})
|
await execAsync('taskkill /F /IM mysqld.exe', { timeout: 5000 }).catch(() => { })
|
||||||
details.push('MySQL 已停止')
|
details.push('MySQL 已停止')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,10 +359,10 @@ export class ServiceManager {
|
|||||||
try {
|
try {
|
||||||
await execAsync(`"${redisCli}" shutdown`, { timeout: 5000 })
|
await execAsync(`"${redisCli}" shutdown`, { timeout: 5000 })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await execAsync('taskkill /F /IM redis-server.exe', { timeout: 5000 }).catch(() => {})
|
await execAsync('taskkill /F /IM redis-server.exe', { timeout: 5000 }).catch(() => { })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await execAsync('taskkill /F /IM redis-server.exe', { timeout: 5000 }).catch(() => {})
|
await execAsync('taskkill /F /IM redis-server.exe', { timeout: 5000 }).catch(() => { })
|
||||||
}
|
}
|
||||||
details.push('Redis 已停止')
|
details.push('Redis 已停止')
|
||||||
}
|
}
|
||||||
@ -439,7 +439,7 @@ export class ServiceManager {
|
|||||||
const parts = line.trim().split(/\s+/)
|
const parts = line.trim().split(/\s+/)
|
||||||
const pid = parts[parts.length - 1]
|
const pid = parts[parts.length - 1]
|
||||||
if (pid && /^\d+$/.test(pid)) {
|
if (pid && /^\d+$/.test(pid)) {
|
||||||
await execAsync(`taskkill /F /PID ${pid}`, { windowsHide: true, timeout: 5000 }).catch(() => {})
|
await execAsync(`taskkill /F /PID ${pid}`, { windowsHide: true, timeout: 5000 }).catch(() => { })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -471,7 +471,7 @@ export class ServiceManager {
|
|||||||
*/
|
*/
|
||||||
async stopAllPhpCgi(): Promise<{ success: boolean; message: string }> {
|
async stopAllPhpCgi(): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 }).catch(() => {})
|
await execAsync('taskkill /F /IM php-cgi.exe', { timeout: 5000 }).catch(() => { })
|
||||||
return { success: true, message: '所有 PHP-CGI 已停止' }
|
return { success: true, message: '所有 PHP-CGI 已停止' }
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return { success: true, message: 'PHP-CGI 未运行' }
|
return { success: true, message: 'PHP-CGI 未运行' }
|
||||||
@ -510,17 +510,38 @@ export class ServiceManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有 PHP-CGI 状态
|
* 获取所有 PHP-CGI 状态
|
||||||
|
* 只返回实际安装的 PHP 版本(php-cgi.exe 存在的版本)
|
||||||
*/
|
*/
|
||||||
async getPhpCgiStatus(): Promise<{ version: string; port: number; running: boolean }[]> {
|
async getPhpCgiStatus(): Promise<{ version: string; port: number; running: boolean }[]> {
|
||||||
const phpVersions = this.configStore.get('phpVersions')
|
|
||||||
const status: { version: string; port: number; running: boolean }[] = []
|
const status: { version: string; port: number; running: boolean }[] = []
|
||||||
|
const phpDir = join(this.configStore.getBasePath(), 'php')
|
||||||
|
|
||||||
for (const version of phpVersions) {
|
// 检查 PHP 目录是否存在
|
||||||
const port = this.getPhpCgiPort(version)
|
if (!existsSync(phpDir)) {
|
||||||
const running = await this.checkPort(port)
|
return status
|
||||||
status.push({ version, port, running })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 扫描实际安装的 PHP 版本
|
||||||
|
const dirs = readdirSync(phpDir, { withFileTypes: true })
|
||||||
|
|
||||||
|
for (const dir of dirs) {
|
||||||
|
if (dir.isDirectory() && dir.name.startsWith('php-')) {
|
||||||
|
const version = dir.name.replace('php-', '')
|
||||||
|
const phpPath = join(phpDir, dir.name)
|
||||||
|
const phpCgiExe = join(phpPath, 'php-cgi.exe')
|
||||||
|
|
||||||
|
// 只有当 php-cgi.exe 存在时才添加到列表
|
||||||
|
if (existsSync(phpCgiExe)) {
|
||||||
|
const port = this.getPhpCgiPort(version)
|
||||||
|
const running = await this.checkPort(port)
|
||||||
|
status.push({ version, port, running })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按版本号排序(降序)
|
||||||
|
status.sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }))
|
||||||
|
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
package-lock.json
generated
65
package-lock.json
generated
@ -1796,6 +1796,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/lodash": "*"
|
"@types/lodash": "*"
|
||||||
}
|
}
|
||||||
@ -2129,6 +2130,7 @@
|
|||||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@ -2320,7 +2322,6 @@
|
|||||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^2.1.0",
|
"archiver-utils": "^2.1.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
@ -2340,7 +2341,6 @@
|
|||||||
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.1.4",
|
"glob": "^7.1.4",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@ -2363,7 +2363,6 @@
|
|||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@ -2379,8 +2378,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@ -2388,7 +2386,6 @@
|
|||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@ -2515,7 +2512,6 @@
|
|||||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
"inherits": "^2.0.4",
|
"inherits": "^2.0.4",
|
||||||
@ -2925,7 +2921,6 @@
|
|||||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"crc32-stream": "^4.0.2",
|
"crc32-stream": "^4.0.2",
|
||||||
@ -3117,7 +3112,6 @@
|
|||||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"crc32": "bin/crc32.njs"
|
"crc32": "bin/crc32.njs"
|
||||||
},
|
},
|
||||||
@ -3131,7 +3125,6 @@
|
|||||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crc-32": "^1.2.0",
|
"crc-32": "^1.2.0",
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
@ -3373,6 +3366,7 @@
|
|||||||
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
|
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"builder-util": "24.13.1",
|
"builder-util": "24.13.1",
|
||||||
@ -3610,7 +3604,6 @@
|
|||||||
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
@ -3624,7 +3617,6 @@
|
|||||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^6.0.1",
|
"jsonfile": "^6.0.1",
|
||||||
@ -3640,7 +3632,6 @@
|
|||||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
@ -3654,7 +3645,6 @@
|
|||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
@ -4136,8 +4126,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
@ -4855,7 +4844,6 @@
|
|||||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readable-stream": "^2.0.5"
|
"readable-stream": "^2.0.5"
|
||||||
},
|
},
|
||||||
@ -4869,7 +4857,6 @@
|
|||||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@ -4885,8 +4872,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lazystream/node_modules/string_decoder": {
|
"node_modules/lazystream/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@ -4894,7 +4880,6 @@
|
|||||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@ -4916,13 +4901,15 @@
|
|||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash-es": {
|
"node_modules/lodash-es": {
|
||||||
"version": "4.17.22",
|
"version": "4.17.22",
|
||||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
|
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
|
||||||
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
|
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash-unified": {
|
"node_modules/lodash-unified": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
@ -4940,40 +4927,35 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.difference": {
|
"node_modules/lodash.difference": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.flatten": {
|
"node_modules/lodash.flatten": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isplainobject": {
|
"node_modules/lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.union": {
|
"node_modules/lodash.union": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@ -5239,7 +5221,6 @@
|
|||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@ -5619,7 +5600,6 @@
|
|||||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"string_decoder": "^1.1.1",
|
"string_decoder": "^1.1.1",
|
||||||
@ -5635,7 +5615,6 @@
|
|||||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimatch": "^5.1.0"
|
"minimatch": "^5.1.0"
|
||||||
}
|
}
|
||||||
@ -5792,8 +5771,7 @@
|
|||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@ -5818,6 +5796,7 @@
|
|||||||
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"immutable": "^5.0.2",
|
"immutable": "^5.0.2",
|
||||||
@ -6052,7 +6031,6 @@
|
|||||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
@ -6173,7 +6151,6 @@
|
|||||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
"end-of-stream": "^1.4.1",
|
"end-of-stream": "^1.4.1",
|
||||||
@ -6313,6 +6290,7 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@ -6431,6 +6409,7 @@
|
|||||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@ -6509,13 +6488,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-electron-renderer/-/vite-plugin-electron-renderer-0.14.6.tgz",
|
||||||
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
|
"integrity": "sha512-oqkWFa7kQIkvHXG7+Mnl1RTroA4sP0yesKatmAy0gjZC4VwUqlvF9IvOpHd1fpLWsqYX/eZlVxlhULNtaQ78Jw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.26",
|
"version": "3.5.26",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.26",
|
"@vue/compiler-dom": "3.5.26",
|
||||||
"@vue/compiler-sfc": "3.5.26",
|
"@vue/compiler-sfc": "3.5.26",
|
||||||
@ -6770,7 +6751,6 @@
|
|||||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^3.0.4",
|
"archiver-utils": "^3.0.4",
|
||||||
"compress-commons": "^4.1.2",
|
"compress-commons": "^4.1.2",
|
||||||
@ -6786,7 +6766,6 @@
|
|||||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.2.3",
|
"glob": "^7.2.3",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
|
|||||||
@ -97,6 +97,8 @@ const menuItems = [
|
|||||||
{ path: '/nginx', label: 'Nginx 管理', icon: 'Connection', service: 'nginx' },
|
{ path: '/nginx', label: 'Nginx 管理', icon: 'Connection', service: 'nginx' },
|
||||||
{ path: '/redis', label: 'Redis 管理', icon: 'Grid', service: 'redis' },
|
{ path: '/redis', label: 'Redis 管理', icon: 'Grid', service: 'redis' },
|
||||||
{ path: '/nodejs', label: 'Node.js 管理', icon: 'Promotion', service: null },
|
{ path: '/nodejs', label: 'Node.js 管理', icon: 'Promotion', 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: '/sites', label: '站点管理', icon: 'Monitor', service: null },
|
||||||
{ path: '/hosts', label: 'Hosts 管理', icon: 'Document', service: null },
|
{ path: '/hosts', label: 'Hosts 管理', icon: 'Document', service: null },
|
||||||
{ path: '/settings', label: '设置', icon: 'Setting', service: null }
|
{ path: '/settings', label: '设置', icon: 'Setting', service: null }
|
||||||
|
|||||||
@ -51,6 +51,18 @@ const router = createRouter({
|
|||||||
component: () => import('@/views/HostsManager.vue'),
|
component: () => import('@/views/HostsManager.vue'),
|
||||||
meta: { title: 'Hosts 管理' }
|
meta: { title: 'Hosts 管理' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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: '/settings',
|
path: '/settings',
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
|
|||||||
544
src/views/GitManager.vue
Normal file
544
src/views/GitManager.vue
Normal file
@ -0,0 +1,544 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">
|
||||||
|
<span class="title-icon"><el-icon><Share /></el-icon></span>
|
||||||
|
Git 管理
|
||||||
|
</h1>
|
||||||
|
<p class="page-description">安装和管理 Git 版本控制工具</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 系统 Git 状态 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
Git 状态
|
||||||
|
</span>
|
||||||
|
<el-button size="small" @click="refreshStatus" :loading="loading">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div v-if="loading" class="loading-state">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="git-status">
|
||||||
|
<div class="status-info">
|
||||||
|
<div class="status-icon" :class="{ installed: gitStatus.installed }">
|
||||||
|
<el-icon v-if="gitStatus.installed"><Check /></el-icon>
|
||||||
|
<el-icon v-else><Close /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="status-details">
|
||||||
|
<div class="status-title">
|
||||||
|
{{ gitStatus.installed ? 'Git 已安装' : 'Git 未安装' }}
|
||||||
|
<el-tag v-if="gitStatus.version" type="success" size="small" class="ml-2">
|
||||||
|
v{{ gitStatus.version }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="status-path" v-if="gitStatus.path">
|
||||||
|
{{ gitStatus.path }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="!gitStatus.installed && !localInstalled"
|
||||||
|
type="primary"
|
||||||
|
@click="showInstallDialog = true"
|
||||||
|
>
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
安装 Git
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="localInstalled"
|
||||||
|
type="danger"
|
||||||
|
@click="uninstallGit"
|
||||||
|
:loading="uninstalling"
|
||||||
|
>
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
卸载
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Git 配置 -->
|
||||||
|
<div class="card" v-if="gitStatus.installed">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
Git 全局配置
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<el-form :model="gitConfig" label-width="100px" class="config-form">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="gitConfig.name" placeholder="请输入 Git 用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱">
|
||||||
|
<el-input v-model="gitConfig.email" placeholder="请输入 Git 邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="saveConfig" :loading="savingConfig">
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 本地安装的 Git 版本 -->
|
||||||
|
<div class="card" v-if="installedVersions.length > 0">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
本地安装版本
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div
|
||||||
|
v-for="version in installedVersions"
|
||||||
|
:key="version.version"
|
||||||
|
class="version-card active"
|
||||||
|
>
|
||||||
|
<div class="version-info">
|
||||||
|
<div class="version-icon">
|
||||||
|
<el-icon><Share /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="version-details">
|
||||||
|
<div class="version-name">
|
||||||
|
Git {{ version.version }}
|
||||||
|
<el-tag type="success" size="small" class="ml-2">已安装</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="version-path">{{ version.path }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 安装对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showInstallDialog"
|
||||||
|
title="安装 Git"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-alert type="info" :closable="false" class="mb-4">
|
||||||
|
<template #title>安装说明</template>
|
||||||
|
将下载 Git for Windows 便携版,无需管理员权限即可使用。
|
||||||
|
</el-alert>
|
||||||
|
<div v-if="loadingVersions" class="loading-state">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>加载可用版本...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="available-versions">
|
||||||
|
<div
|
||||||
|
v-for="version in availableVersions"
|
||||||
|
:key="version.version"
|
||||||
|
class="available-version-item"
|
||||||
|
:class="{ selected: selectedVersion === version.version }"
|
||||||
|
@click="selectedVersion = version.version"
|
||||||
|
>
|
||||||
|
<div class="version-select-info">
|
||||||
|
<span class="version-number">Git {{ version.version }}</span>
|
||||||
|
<span class="version-type">{{ version.type === 'portable' ? '便携版' : '安装版' }}</span>
|
||||||
|
</div>
|
||||||
|
<el-icon v-if="selectedVersion === version.version" class="check-icon"><Check /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 下载进度条 -->
|
||||||
|
<div v-if="installing && downloadProgress.total > 0" class="download-progress">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span>下载中...</span>
|
||||||
|
<span>{{ formatSize(downloadProgress.downloaded) }} / {{ formatSize(downloadProgress.total) }}</span>
|
||||||
|
</div>
|
||||||
|
<el-progress :percentage="downloadProgress.progress" :stroke-width="10" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showInstallDialog = false" :disabled="installing">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="installGit"
|
||||||
|
:loading="installing"
|
||||||
|
:disabled="!selectedVersion"
|
||||||
|
>
|
||||||
|
{{ installing ? '安装中...' : '安装' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
interface GitVersion {
|
||||||
|
version: string
|
||||||
|
path: string
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvailableVersion {
|
||||||
|
version: string
|
||||||
|
downloadUrl: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingVersions = ref(false)
|
||||||
|
const installing = ref(false)
|
||||||
|
const uninstalling = ref(false)
|
||||||
|
const savingConfig = ref(false)
|
||||||
|
const showInstallDialog = ref(false)
|
||||||
|
const selectedVersion = ref('')
|
||||||
|
|
||||||
|
const gitStatus = ref<{
|
||||||
|
installed: boolean
|
||||||
|
version?: string
|
||||||
|
path?: string
|
||||||
|
}>({ installed: false })
|
||||||
|
|
||||||
|
const localInstalled = ref(false)
|
||||||
|
const installedVersions = ref<GitVersion[]>([])
|
||||||
|
const availableVersions = ref<AvailableVersion[]>([])
|
||||||
|
|
||||||
|
const gitConfig = reactive({
|
||||||
|
name: '',
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadProgress = reactive({
|
||||||
|
progress: 0,
|
||||||
|
downloaded: 0,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const refreshStatus = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 检查系统 Git
|
||||||
|
gitStatus.value = await window.electronAPI?.git?.checkSystem() || { installed: false }
|
||||||
|
|
||||||
|
// 获取本地安装的版本
|
||||||
|
installedVersions.value = await window.electronAPI?.git?.getVersions() || []
|
||||||
|
localInstalled.value = installedVersions.value.length > 0
|
||||||
|
|
||||||
|
// 如果有 Git,加载配置
|
||||||
|
if (gitStatus.value.installed) {
|
||||||
|
const config = await window.electronAPI?.git?.getConfig() || {}
|
||||||
|
gitConfig.name = config.name || ''
|
||||||
|
gitConfig.email = config.email || ''
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载状态失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAvailableVersions = async () => {
|
||||||
|
loadingVersions.value = true
|
||||||
|
try {
|
||||||
|
availableVersions.value = await window.electronAPI?.git?.getAvailableVersions() || []
|
||||||
|
if (availableVersions.value.length > 0) {
|
||||||
|
selectedVersion.value = availableVersions.value[0].version
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error('加载可用版本失败')
|
||||||
|
} finally {
|
||||||
|
loadingVersions.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const installGit = async () => {
|
||||||
|
if (!selectedVersion.value) return
|
||||||
|
|
||||||
|
downloadProgress.progress = 0
|
||||||
|
downloadProgress.downloaded = 0
|
||||||
|
downloadProgress.total = 0
|
||||||
|
|
||||||
|
installing.value = true
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI?.git?.install(selectedVersion.value)
|
||||||
|
if (result?.success) {
|
||||||
|
ElMessage.success(result.message)
|
||||||
|
showInstallDialog.value = false
|
||||||
|
await refreshStatus()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '安装失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message)
|
||||||
|
} finally {
|
||||||
|
installing.value = false
|
||||||
|
downloadProgress.progress = 0
|
||||||
|
downloadProgress.downloaded = 0
|
||||||
|
downloadProgress.total = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uninstallGit = async () => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要卸载本地安装的 Git 吗?',
|
||||||
|
'确认卸载',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
|
||||||
|
uninstalling.value = true
|
||||||
|
const result = await window.electronAPI?.git?.uninstall()
|
||||||
|
if (result?.success) {
|
||||||
|
ElMessage.success(result.message)
|
||||||
|
await refreshStatus()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '卸载失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error(error.message)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
uninstalling.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveConfig = async () => {
|
||||||
|
savingConfig.value = true
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI?.git?.setConfig(gitConfig.name, gitConfig.email)
|
||||||
|
if (result?.success) {
|
||||||
|
ElMessage.success(result.message)
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '保存失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message)
|
||||||
|
} finally {
|
||||||
|
savingConfig.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(1)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshStatus()
|
||||||
|
loadAvailableVersions()
|
||||||
|
|
||||||
|
// 监听下载进度
|
||||||
|
window.electronAPI?.onDownloadProgress((data: any) => {
|
||||||
|
if (data.type === 'git') {
|
||||||
|
downloadProgress.progress = data.progress
|
||||||
|
downloadProgress.downloaded = data.downloaded
|
||||||
|
downloadProgress.total = data.total
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.electronAPI?.removeDownloadProgressListener()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
.is-loading {
|
||||||
|
font-size: 24px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ml-2 {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.git-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 0;
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--bg-card);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
|
||||||
|
&.installed {
|
||||||
|
background: rgba(103, 194, 58, 0.1);
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-details {
|
||||||
|
.status-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-path {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: var(--success-color);
|
||||||
|
background: rgba(103, 194, 58, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #f05033 0%, #ff6b6b 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-details {
|
||||||
|
.version-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-path {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-versions {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-version-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--accent-light);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
background: rgba(124, 58, 237, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-select-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.version-number {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-type {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-progress {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
527
src/views/PythonManager.vue
Normal file
527
src/views/PythonManager.vue
Normal file
@ -0,0 +1,527 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">
|
||||||
|
<span class="title-icon"><el-icon><Platform /></el-icon></span>
|
||||||
|
Python 管理
|
||||||
|
</h1>
|
||||||
|
<p class="page-description">安装、切换和管理 Python 版本</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 已安装版本 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
已安装版本
|
||||||
|
</span>
|
||||||
|
<el-button type="primary" @click="showInstallDialog = true">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
安装新版本
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<div v-if="loading" class="loading-state">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="installedVersions.length === 0" class="empty-state">
|
||||||
|
<el-icon class="empty-icon"><Platform /></el-icon>
|
||||||
|
<h3 class="empty-title">暂未安装 Python</h3>
|
||||||
|
<p class="empty-description">点击上方按钮安装第一个 Python 版本</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-for="version in installedVersions"
|
||||||
|
:key="version.version"
|
||||||
|
class="version-card"
|
||||||
|
:class="{ active: version.isActive }"
|
||||||
|
>
|
||||||
|
<div class="version-info">
|
||||||
|
<div class="version-icon">
|
||||||
|
<el-icon><Platform /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="version-details">
|
||||||
|
<div class="version-name">
|
||||||
|
Python {{ version.version }}
|
||||||
|
<el-tag v-if="version.isActive" type="success" size="small" class="ml-2">当前使用</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="version-path">{{ version.path }}</div>
|
||||||
|
<div class="pip-info" v-if="pipInfo[version.version]">
|
||||||
|
<el-tag type="info" size="small">
|
||||||
|
pip {{ pipInfo[version.version] }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="version-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="!version.isActive"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="setActive(version.version)"
|
||||||
|
:loading="settingActive === version.version"
|
||||||
|
>
|
||||||
|
设为默认
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="uninstall(version.version)"
|
||||||
|
:disabled="version.isActive"
|
||||||
|
>
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
卸载
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- pip 包管理 -->
|
||||||
|
<div class="card" v-if="installedVersions.length > 0">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">
|
||||||
|
<el-icon><Box /></el-icon>
|
||||||
|
pip 包管理
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<el-form inline class="pip-form">
|
||||||
|
<el-form-item label="Python 版本">
|
||||||
|
<el-select v-model="selectedPythonVersion" placeholder="选择版本">
|
||||||
|
<el-option
|
||||||
|
v-for="v in installedVersions"
|
||||||
|
:key="v.version"
|
||||||
|
:label="`Python ${v.version}`"
|
||||||
|
:value="v.version"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="包名">
|
||||||
|
<el-input v-model="packageName" placeholder="输入包名,如 requests" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="installPackage"
|
||||||
|
:loading="installingPackage"
|
||||||
|
:disabled="!selectedPythonVersion || !packageName"
|
||||||
|
>
|
||||||
|
安装包
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="pip-hint">
|
||||||
|
常用包:requests, flask, django, numpy, pandas, pillow
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 安装对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showInstallDialog"
|
||||||
|
title="安装 Python 版本"
|
||||||
|
width="600px"
|
||||||
|
>
|
||||||
|
<el-alert type="info" :closable="false" class="mb-4">
|
||||||
|
<template #title>安装说明</template>
|
||||||
|
将下载 Python 嵌入式版本(免安装),自动配置 pip。
|
||||||
|
</el-alert>
|
||||||
|
<div v-if="availableVersions.length === 0" class="loading-state">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>加载可用版本...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="available-versions">
|
||||||
|
<div
|
||||||
|
v-for="version in availableVersions"
|
||||||
|
:key="version.version"
|
||||||
|
class="available-version-item"
|
||||||
|
:class="{ selected: selectedVersion === version.version }"
|
||||||
|
@click="selectedVersion = version.version"
|
||||||
|
>
|
||||||
|
<div class="version-select-info">
|
||||||
|
<span class="version-number">Python {{ version.version }}</span>
|
||||||
|
<span class="version-type">{{ version.type === 'embed' ? '嵌入式版本' : '安装版' }}</span>
|
||||||
|
</div>
|
||||||
|
<el-icon v-if="selectedVersion === version.version" class="check-icon"><Check /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 下载进度条 -->
|
||||||
|
<div v-if="installing && downloadProgress.total > 0" class="download-progress">
|
||||||
|
<div class="progress-info">
|
||||||
|
<span>下载中...</span>
|
||||||
|
<span>{{ formatSize(downloadProgress.downloaded) }} / {{ formatSize(downloadProgress.total) }}</span>
|
||||||
|
</div>
|
||||||
|
<el-progress :percentage="downloadProgress.progress" :stroke-width="10" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showInstallDialog = false" :disabled="installing">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="install"
|
||||||
|
:loading="installing"
|
||||||
|
:disabled="!selectedVersion"
|
||||||
|
>
|
||||||
|
{{ installing ? '安装中...' : '安装' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
interface PythonVersion {
|
||||||
|
version: string
|
||||||
|
path: string
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AvailableVersion {
|
||||||
|
version: string
|
||||||
|
downloadUrl: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const installedVersions = ref<PythonVersion[]>([])
|
||||||
|
const availableVersions = ref<AvailableVersion[]>([])
|
||||||
|
const showInstallDialog = ref(false)
|
||||||
|
const selectedVersion = ref('')
|
||||||
|
const installing = ref(false)
|
||||||
|
const settingActive = ref('')
|
||||||
|
const pipInfo = ref<Record<string, string>>({})
|
||||||
|
|
||||||
|
// pip 包管理
|
||||||
|
const selectedPythonVersion = ref('')
|
||||||
|
const packageName = ref('')
|
||||||
|
const installingPackage = ref(false)
|
||||||
|
|
||||||
|
const downloadProgress = reactive({
|
||||||
|
progress: 0,
|
||||||
|
downloaded: 0,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadVersions = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
installedVersions.value = await window.electronAPI?.python?.getVersions() || []
|
||||||
|
|
||||||
|
// 如果有安装的版本,加载 pip 信息
|
||||||
|
for (const v of installedVersions.value) {
|
||||||
|
const info = await window.electronAPI?.python?.getPipInfo(v.version)
|
||||||
|
if (info?.installed) {
|
||||||
|
pipInfo.value[v.version] = info.version || 'installed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认选择第一个版本用于 pip
|
||||||
|
if (installedVersions.value.length > 0 && !selectedPythonVersion.value) {
|
||||||
|
selectedPythonVersion.value = installedVersions.value[0].version
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载版本失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadAvailableVersions = async () => {
|
||||||
|
try {
|
||||||
|
availableVersions.value = await window.electronAPI?.python?.getAvailableVersions() || []
|
||||||
|
if (availableVersions.value.length > 0) {
|
||||||
|
selectedVersion.value = availableVersions.value[0].version
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error('加载可用版本失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const install = async () => {
|
||||||
|
if (!selectedVersion.value) return
|
||||||
|
|
||||||
|
downloadProgress.progress = 0
|
||||||
|
downloadProgress.downloaded = 0
|
||||||
|
downloadProgress.total = 0
|
||||||
|
|
||||||
|
installing.value = true
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI?.python?.install(selectedVersion.value)
|
||||||
|
if (result?.success) {
|
||||||
|
ElMessage.success(result.message)
|
||||||
|
showInstallDialog.value = false
|
||||||
|
selectedVersion.value = ''
|
||||||
|
await loadVersions()
|
||||||
|
await loadAvailableVersions()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '安装失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message)
|
||||||
|
} finally {
|
||||||
|
installing.value = false
|
||||||
|
downloadProgress.progress = 0
|
||||||
|
downloadProgress.downloaded = 0
|
||||||
|
downloadProgress.total = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uninstall = async (version: string) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要卸载 Python ${version} 吗?此操作不可恢复。`,
|
||||||
|
'确认卸载',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await window.electronAPI?.python?.uninstall(version)
|
||||||
|
if (result?.success) {
|
||||||
|
ElMessage.success(result.message)
|
||||||
|
await loadVersions()
|
||||||
|
await loadAvailableVersions()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '卸载失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setActive = async (version: string) => {
|
||||||
|
settingActive.value = version
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI?.python?.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 installPackage = async () => {
|
||||||
|
if (!selectedPythonVersion.value || !packageName.value) return
|
||||||
|
|
||||||
|
installingPackage.value = true
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI?.python?.installPackage(
|
||||||
|
selectedPythonVersion.value,
|
||||||
|
packageName.value
|
||||||
|
)
|
||||||
|
if (result?.success) {
|
||||||
|
ElMessage.success(result.message)
|
||||||
|
packageName.value = ''
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '安装失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message)
|
||||||
|
} finally {
|
||||||
|
installingPackage.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(1)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadVersions()
|
||||||
|
loadAvailableVersions()
|
||||||
|
|
||||||
|
// 监听下载进度
|
||||||
|
window.electronAPI?.onDownloadProgress((data: any) => {
|
||||||
|
if (data.type === 'python') {
|
||||||
|
downloadProgress.progress = data.progress
|
||||||
|
downloadProgress.downloaded = data.downloaded
|
||||||
|
downloadProgress.total = data.total
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.electronAPI?.removeDownloadProgressListener()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
.is-loading {
|
||||||
|
font-size: 24px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ml-2 {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--accent-light);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: var(--success-color);
|
||||||
|
background: rgba(103, 194, 58, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, #3776ab 0%, #ffd43b 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-details {
|
||||||
|
.version-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-path {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'Fira Code', monospace;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip-info {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-versions {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-version-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--accent-light);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
background: rgba(124, 58, 237, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-select-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.version-number {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-type {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-progress {
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip-form {
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pip-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -56,16 +56,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="site-meta">
|
<div class="site-meta">
|
||||||
<span class="meta-item">
|
<span class="meta-item" v-if="site.isProxy">
|
||||||
|
<el-icon><Link /></el-icon>
|
||||||
|
{{ site.proxyTarget }}
|
||||||
|
</span>
|
||||||
|
<span class="meta-item" v-else>
|
||||||
<el-icon><Folder /></el-icon>
|
<el-icon><Folder /></el-icon>
|
||||||
{{ site.rootPath }}
|
{{ site.rootPath }}
|
||||||
</span>
|
</span>
|
||||||
<span class="meta-item">
|
<span class="meta-item" v-if="!site.isProxy">
|
||||||
<el-icon><Files /></el-icon>
|
<el-icon><Files /></el-icon>
|
||||||
PHP {{ site.phpVersion }} (端口 {{ getPhpCgiPort(site.phpVersion) }})
|
PHP {{ site.phpVersion }} (端口 {{ getPhpCgiPort(site.phpVersion) }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="site-tags">
|
<div class="site-tags">
|
||||||
|
<el-tag v-if="site.isProxy" type="primary" size="small">反向代理</el-tag>
|
||||||
<el-tag v-if="site.isLaravel" type="warning" size="small">Laravel</el-tag>
|
<el-tag v-if="site.isLaravel" type="warning" size="small">Laravel</el-tag>
|
||||||
<el-tag v-if="site.ssl" type="success" size="small">SSL</el-tag>
|
<el-tag v-if="site.ssl" type="success" size="small">SSL</el-tag>
|
||||||
<el-tag :type="site.enabled ? 'success' : 'info'" size="small">
|
<el-tag :type="site.enabled ? 'success' : 'info'" size="small">
|
||||||
@ -126,7 +131,7 @@
|
|||||||
<el-input v-model="siteForm.name" placeholder="留空则使用域名作为名称" />
|
<el-input v-model="siteForm.name" placeholder="留空则使用域名作为名称" />
|
||||||
<span class="form-hint">可选,默认使用域名</span>
|
<span class="form-hint">可选,默认使用域名</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="根目录" required>
|
<el-form-item label="根目录" required v-if="!siteForm.isProxy">
|
||||||
<div class="directory-input">
|
<div class="directory-input">
|
||||||
<el-input v-model="siteForm.rootPath" placeholder="点击右侧按钮选择目录" readonly />
|
<el-input v-model="siteForm.rootPath" placeholder="点击右侧按钮选择目录" readonly />
|
||||||
<el-button type="primary" @click="selectDirectory" :icon="FolderOpened">
|
<el-button type="primary" @click="selectDirectory" :icon="FolderOpened">
|
||||||
@ -134,7 +139,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="PHP 版本" required>
|
<el-form-item label="PHP 版本" required v-if="!siteForm.isProxy">
|
||||||
<el-select v-model="siteForm.phpVersion" placeholder="选择 PHP 版本">
|
<el-select v-model="siteForm.phpVersion" placeholder="选择 PHP 版本">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="v in phpVersions"
|
v-for="v in phpVersions"
|
||||||
@ -145,7 +150,15 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
<span class="form-hint">每个 PHP 版本使用独立端口的 FastCGI 进程</span>
|
<span class="form-hint">每个 PHP 版本使用独立端口的 FastCGI 进程</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Laravel 项目">
|
<el-form-item label="反向代理">
|
||||||
|
<el-switch v-model="siteForm.isProxy" @change="onProxyChange" />
|
||||||
|
<span class="form-hint">开启后将作为反向代理服务器(用于 Node.js、Go 等应用)</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="代理目标" v-if="siteForm.isProxy" required>
|
||||||
|
<el-input v-model="siteForm.proxyTarget" placeholder="例如: http://127.0.0.1:3000" />
|
||||||
|
<span class="form-hint">后端服务地址,支持 WebSocket</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Laravel 项目" v-if="!siteForm.isProxy">
|
||||||
<el-switch v-model="siteForm.isLaravel" />
|
<el-switch v-model="siteForm.isLaravel" />
|
||||||
<span class="form-hint">开启后将自动配置 Laravel 伪静态规则</span>
|
<span class="form-hint">开启后将自动配置 Laravel 伪静态规则</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -208,7 +221,15 @@
|
|||||||
<el-input v-model="editForm.name" disabled />
|
<el-input v-model="editForm.name" disabled />
|
||||||
<span class="form-hint">站点名称不可修改</span>
|
<span class="form-hint">站点名称不可修改</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="根目录" required>
|
<el-form-item label="反向代理">
|
||||||
|
<el-switch v-model="editForm.isProxy" @change="onEditProxyChange" />
|
||||||
|
<span class="form-hint">开启后将作为反向代理服务器</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="代理目标" v-if="editForm.isProxy" required>
|
||||||
|
<el-input v-model="editForm.proxyTarget" placeholder="例如: http://127.0.0.1:3000" />
|
||||||
|
<span class="form-hint">后端服务地址,支持 WebSocket</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="根目录" required v-if="!editForm.isProxy">
|
||||||
<div class="directory-input">
|
<div class="directory-input">
|
||||||
<el-input v-model="editForm.rootPath" placeholder="点击右侧按钮选择目录" readonly />
|
<el-input v-model="editForm.rootPath" placeholder="点击右侧按钮选择目录" readonly />
|
||||||
<el-button type="primary" @click="selectEditDirectory" :icon="FolderOpened">
|
<el-button type="primary" @click="selectEditDirectory" :icon="FolderOpened">
|
||||||
@ -216,7 +237,7 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="PHP 版本" required>
|
<el-form-item label="PHP 版本" required v-if="!editForm.isProxy">
|
||||||
<el-select v-model="editForm.phpVersion" placeholder="选择 PHP 版本">
|
<el-select v-model="editForm.phpVersion" placeholder="选择 PHP 版本">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="v in phpVersions"
|
v-for="v in phpVersions"
|
||||||
@ -227,7 +248,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
<span class="form-hint">修改后需重新加载 Nginx 配置</span>
|
<span class="form-hint">修改后需重新加载 Nginx 配置</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Laravel 项目">
|
<el-form-item label="Laravel 项目" v-if="!editForm.isProxy">
|
||||||
<el-switch v-model="editForm.isLaravel" />
|
<el-switch v-model="editForm.isLaravel" />
|
||||||
<span class="form-hint">开启后将自动配置 Laravel 伪静态规则</span>
|
<span class="form-hint">开启后将自动配置 Laravel 伪静态规则</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -326,6 +347,8 @@ interface SiteConfig {
|
|||||||
isLaravel: boolean
|
isLaravel: boolean
|
||||||
ssl: boolean
|
ssl: boolean
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
isProxy?: boolean
|
||||||
|
proxyTarget?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -353,7 +376,9 @@ const siteForm = reactive<SiteConfig>({
|
|||||||
phpVersion: '',
|
phpVersion: '',
|
||||||
isLaravel: false,
|
isLaravel: false,
|
||||||
ssl: false,
|
ssl: false,
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
isProxy: false,
|
||||||
|
proxyTarget: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const showSSLDialogVisible = ref(false)
|
const showSSLDialogVisible = ref(false)
|
||||||
@ -374,7 +399,9 @@ const editForm = reactive<SiteConfig>({
|
|||||||
phpVersion: '',
|
phpVersion: '',
|
||||||
isLaravel: false,
|
isLaravel: false,
|
||||||
ssl: false,
|
ssl: false,
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
isProxy: false,
|
||||||
|
proxyTarget: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 创建 Laravel 项目
|
// 创建 Laravel 项目
|
||||||
@ -423,6 +450,31 @@ const selectDirectory = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 反向代理开关变化
|
||||||
|
const onProxyChange = (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
// 开启反向代理时,清空不需要的字段
|
||||||
|
siteForm.rootPath = ''
|
||||||
|
siteForm.phpVersion = ''
|
||||||
|
siteForm.isLaravel = false
|
||||||
|
// 设置默认代理目标
|
||||||
|
if (!siteForm.proxyTarget) {
|
||||||
|
siteForm.proxyTarget = 'http://127.0.0.1:3000'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEditProxyChange = (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
editForm.rootPath = ''
|
||||||
|
editForm.phpVersion = ''
|
||||||
|
editForm.isLaravel = false
|
||||||
|
if (!editForm.proxyTarget) {
|
||||||
|
editForm.proxyTarget = 'http://127.0.0.1:3000'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 自动填充站点名称(当域名输入完成后)
|
// 自动填充站点名称(当域名输入完成后)
|
||||||
const autoFillName = () => {
|
const autoFillName = () => {
|
||||||
if (!siteForm.name && siteForm.domain) {
|
if (!siteForm.name && siteForm.domain) {
|
||||||
@ -437,9 +489,17 @@ const addSite = async () => {
|
|||||||
siteForm.name = siteForm.domain.replace(/\.(test|local|dev|localhost)$/i, '')
|
siteForm.name = siteForm.domain.replace(/\.(test|local|dev|localhost)$/i, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!siteForm.domain || !siteForm.rootPath || !siteForm.phpVersion) {
|
// 根据站点类型验证必填字段
|
||||||
ElMessage.warning('请填写所有必填字段(域名、根目录、PHP版本)')
|
if (siteForm.isProxy) {
|
||||||
return
|
if (!siteForm.domain || !siteForm.proxyTarget) {
|
||||||
|
ElMessage.warning('请填写所有必填字段(域名、代理目标)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!siteForm.domain || !siteForm.rootPath || !siteForm.phpVersion) {
|
||||||
|
ElMessage.warning('请填写所有必填字段(域名、根目录、PHP版本)')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最终确保有站点名称
|
// 最终确保有站点名称
|
||||||
@ -457,7 +517,9 @@ const addSite = async () => {
|
|||||||
phpVersion: siteForm.phpVersion,
|
phpVersion: siteForm.phpVersion,
|
||||||
isLaravel: siteForm.isLaravel,
|
isLaravel: siteForm.isLaravel,
|
||||||
ssl: siteForm.ssl,
|
ssl: siteForm.ssl,
|
||||||
enabled: siteForm.enabled
|
enabled: siteForm.enabled,
|
||||||
|
isProxy: siteForm.isProxy,
|
||||||
|
proxyTarget: siteForm.proxyTarget
|
||||||
}
|
}
|
||||||
const result = await window.electronAPI?.nginx.addSite(siteData)
|
const result = await window.electronAPI?.nginx.addSite(siteData)
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
@ -477,7 +539,9 @@ const addSite = async () => {
|
|||||||
phpVersion: phpVersions.value[0]?.version || '',
|
phpVersion: phpVersions.value[0]?.version || '',
|
||||||
isLaravel: false,
|
isLaravel: false,
|
||||||
ssl: false,
|
ssl: false,
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
isProxy: false,
|
||||||
|
proxyTarget: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
// 重新加载 Nginx 配置
|
// 重新加载 Nginx 配置
|
||||||
@ -568,7 +632,9 @@ const showEditDialog = (site: SiteConfig) => {
|
|||||||
phpVersion: site.phpVersion,
|
phpVersion: site.phpVersion,
|
||||||
isLaravel: site.isLaravel,
|
isLaravel: site.isLaravel,
|
||||||
ssl: site.ssl,
|
ssl: site.ssl,
|
||||||
enabled: site.enabled
|
enabled: site.enabled,
|
||||||
|
isProxy: site.isProxy || false,
|
||||||
|
proxyTarget: site.proxyTarget || ''
|
||||||
})
|
})
|
||||||
showEditSiteDialog.value = true
|
showEditSiteDialog.value = true
|
||||||
}
|
}
|
||||||
@ -587,9 +653,17 @@ const selectEditDirectory = async () => {
|
|||||||
|
|
||||||
// 更新站点
|
// 更新站点
|
||||||
const updateSite = async () => {
|
const updateSite = async () => {
|
||||||
if (!editForm.domain || !editForm.rootPath || !editForm.phpVersion) {
|
// 根据站点类型验证必填字段
|
||||||
ElMessage.warning('请填写所有必填字段')
|
if (editForm.isProxy) {
|
||||||
return
|
if (!editForm.domain || !editForm.proxyTarget) {
|
||||||
|
ElMessage.warning('请填写所有必填字段(域名、代理目标)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!editForm.domain || !editForm.rootPath || !editForm.phpVersion) {
|
||||||
|
ElMessage.warning('请填写所有必填字段')
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updating.value = true
|
updating.value = true
|
||||||
@ -602,7 +676,9 @@ const updateSite = async () => {
|
|||||||
phpVersion: editForm.phpVersion,
|
phpVersion: editForm.phpVersion,
|
||||||
isLaravel: editForm.isLaravel,
|
isLaravel: editForm.isLaravel,
|
||||||
ssl: editForm.ssl,
|
ssl: editForm.ssl,
|
||||||
enabled: editForm.enabled
|
enabled: editForm.enabled,
|
||||||
|
isProxy: editForm.isProxy,
|
||||||
|
proxyTarget: editForm.proxyTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await window.electronAPI?.nginx.updateSite(editingOriginalName.value, siteData)
|
const result = await window.electronAPI?.nginx.updateSite(editingOriginalName.value, siteData)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user