Compare commits
2 Commits
1cea2b340f
...
d8faf27108
| Author | SHA1 | Date | |
|---|---|---|---|
| d8faf27108 | |||
| dac4cc805e |
52
README.md
52
README.md
@ -63,13 +63,16 @@
|
|||||||
### 🐘 PHP 版本管理
|
### 🐘 PHP 版本管理
|
||||||
|
|
||||||
| 功能 | 说明 |
|
| 功能 | 说明 |
|
||||||
| ---------- | ---------------------------------------------------------- |
|
| ------------ | ---------------------------------------------------------- |
|
||||||
| 多版本管理 | 支持同时安装 PHP 8.1、8.2、8.3、8.4、8.5 等多个版本 |
|
| 多版本管理 | 支持同时安装 PHP 8.1、8.2、8.3、8.4、8.5 等多个版本 |
|
||||||
|
| CGI 独立控制 | 每个 PHP 版本可独立启动/停止 CGI 进程,支持多版本并行运行 |
|
||||||
|
| 端口自动分配 | 各版本自动分配端口(如 8.4→9084, 8.3→9083) |
|
||||||
| 一键切换 | 点击即可切换 PHP 版本,自动配置系统环境变量 |
|
| 一键切换 | 点击即可切换 PHP 版本,自动配置系统环境变量 |
|
||||||
| 扩展管理 | 可视化管理 PHP 扩展,支持在线安装(从 PECL) |
|
| 扩展管理 | 可视化管理 PHP 扩展,支持在线安装(从 PECL) |
|
||||||
| 配置编辑 | 在线编辑 php.ini,无需手动查找配置文件 |
|
| 配置编辑 | 在线编辑 php.ini,无需手动查找配置文件 |
|
||||||
| 自动配置 | 安装时自动启用常用扩展(curl、gd、mbstring、pdo_mysql 等) |
|
| 自动配置 | 安装时自动启用常用扩展(curl、gd、mbstring、pdo_mysql 等) |
|
||||||
| Composer | 集成 Composer 管理,支持镜像源切换(阿里云、腾讯云等) |
|
| Composer | 集成 Composer 管理,支持镜像源切换(阿里云、腾讯云等) |
|
||||||
|
| 日志查看 | 直接查看 PHP 错误日志 |
|
||||||
| 下载源 | 从 [windows.php.net](https://windows.php.net) 官方下载 |
|
| 下载源 | 从 [windows.php.net](https://windows.php.net) 官方下载 |
|
||||||
|
|
||||||
### 🐬 MySQL 管理
|
### 🐬 MySQL 管理
|
||||||
@ -139,15 +142,31 @@
|
|||||||
- 🎯 **Laravel 一键配置** - 自动配置 public 目录和伪静态规则
|
- 🎯 **Laravel 一键配置** - 自动配置 public 目录和伪静态规则
|
||||||
- 🔒 **SSL 证书申请** - 集成 Let's Encrypt 自动申请
|
- 🔒 **SSL 证书申请** - 集成 Let's Encrypt 自动申请
|
||||||
- 📝 **Hosts 自动配置** - 自动添加域名到系统 hosts 文件
|
- 📝 **Hosts 自动配置** - 自动添加域名到系统 hosts 文件
|
||||||
|
- 📋 **站点日志查看** - 查看每个站点的访问日志和错误日志
|
||||||
|
- 🌐 **一键打开站点** - 点击域名在默认浏览器打开
|
||||||
|
|
||||||
|
### 📋 日志查看
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
| ------------ | ---------------------------------------------- |
|
||||||
|
| 多服务日志 | 支持查看 Nginx、PHP、MySQL、Redis 日志 |
|
||||||
|
| 站点日志 | 查看各站点的访问日志和错误日志 |
|
||||||
|
| 实时刷新 | 支持刷新日志内容,查看最新记录 |
|
||||||
|
| 行数控制 | 可配置显示的日志行数(100-5000 行) |
|
||||||
|
| 快速清空 | 一键清空指定日志文件 |
|
||||||
|
| 打开目录 | 快速在文件管理器中打开日志目录 |
|
||||||
|
|
||||||
### ⚙️ 其他功能
|
### ⚙️ 其他功能
|
||||||
|
|
||||||
- 🚀 **开机自启动** - 可配置各服务开机自动启动
|
- 🚀 **开机自启动** - 可配置各服务开机自动启动(静默模式,无弹窗)
|
||||||
|
- 🔇 **静默启动** - 所有服务启动无黑色窗口闪烁
|
||||||
- 📋 **Hosts 管理** - 可视化管理系统 hosts 文件
|
- 📋 **Hosts 管理** - 可视化管理系统 hosts 文件
|
||||||
- 🌙 **深色/浅色主题** - 支持主题切换
|
- 🌙 **深色/浅色主题** - 支持主题切换
|
||||||
- 📊 **服务状态监控** - 实时显示各服务运行状态
|
- 📊 **服务状态监控** - 实时显示各服务运行状态
|
||||||
- ⏳ **加载状态提示** - 版本列表加载时显示 Loading 状态
|
- ⚡ **页面切换优化** - 使用 KeepAlive 缓存页面,切换无闪烁
|
||||||
|
- 🔢 **自动版本号** - 打包时自动更新版本号
|
||||||
- 📥 **下载源说明** - 清晰显示各软件的下载来源
|
- 📥 **下载源说明** - 清晰显示各软件的下载来源
|
||||||
|
- 🌐 **默认浏览器打开** - 站点链接自动在默认浏览器打开
|
||||||
|
|
||||||
## 🛠️ 技术栈
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
@ -186,8 +205,16 @@ npm run electron:dev
|
|||||||
### 构建生产版本
|
### 构建生产版本
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 构建 Windows 安装包
|
# 构建 Windows 安装包(自动更新 patch 版本号 +0.0.1)
|
||||||
npm run electron:build
|
npm run build
|
||||||
|
|
||||||
|
# 指定版本号更新类型
|
||||||
|
npm run build:patch # 1.0.0 -> 1.0.1
|
||||||
|
npm run build:minor # 1.0.0 -> 1.1.0
|
||||||
|
npm run build:major # 1.0.0 -> 2.0.0
|
||||||
|
|
||||||
|
# 不更新版本号直接打包
|
||||||
|
npm run build:nobump
|
||||||
```
|
```
|
||||||
|
|
||||||
构建完成后,安装包将生成在 `release` 目录中。
|
构建完成后,安装包将生成在 `release` 目录中。
|
||||||
@ -209,14 +236,19 @@ phper/
|
|||||||
│ ├── PythonManager.ts # Python 版本管理器
|
│ ├── PythonManager.ts # Python 版本管理器
|
||||||
│ ├── GitManager.ts # Git 管理器
|
│ ├── GitManager.ts # Git 管理器
|
||||||
│ ├── ServiceManager.ts # 开机自启服务管理器
|
│ ├── ServiceManager.ts # 开机自启服务管理器
|
||||||
│ └── HostsManager.ts # Hosts 文件管理器
|
│ ├── HostsManager.ts # Hosts 文件管理器
|
||||||
|
│ └── LogManager.ts # 日志管理器
|
||||||
│
|
│
|
||||||
├── src/ # Vue 前端源码
|
├── src/ # Vue 前端源码
|
||||||
│ ├── App.vue # 根组件
|
│ ├── App.vue # 根组件(含 KeepAlive 缓存)
|
||||||
│ ├── main.ts # 入口文件
|
│ ├── main.ts # 入口文件
|
||||||
│ ├── vite-env.d.ts # 类型声明
|
│ ├── vite-env.d.ts # 类型声明
|
||||||
│ ├── router/ # 路由配置
|
│ ├── router/ # 路由配置
|
||||||
│ │ └── index.ts
|
│ │ └── index.ts
|
||||||
|
│ ├── stores/ # Pinia 状态管理
|
||||||
|
│ │ └── serviceStore.ts # 服务状态存储
|
||||||
|
│ ├── components/ # 公共组件
|
||||||
|
│ │ └── LogViewer.vue # 日志查看器组件
|
||||||
│ ├── styles/ # 样式文件
|
│ ├── styles/ # 样式文件
|
||||||
│ │ └── main.scss # 全局样式(含主题变量)
|
│ │ └── main.scss # 全局样式(含主题变量)
|
||||||
│ └── views/ # 页面视图
|
│ └── views/ # 页面视图
|
||||||
@ -232,8 +264,12 @@ phper/
|
|||||||
│ ├── HostsManager.vue # Hosts 管理
|
│ ├── HostsManager.vue # Hosts 管理
|
||||||
│ └── Settings.vue # 设置
|
│ └── Settings.vue # 设置
|
||||||
│
|
│
|
||||||
|
├── scripts/ # 构建脚本
|
||||||
|
│ └── bump-version.js # 版本号自动更新脚本
|
||||||
|
│
|
||||||
├── public/ # 静态资源
|
├── public/ # 静态资源
|
||||||
│ └── icon.svg # 应用图标
|
│ ├── icon.svg # 应用图标
|
||||||
|
│ └── version.json # 版本信息(构建时生成)
|
||||||
│
|
│
|
||||||
├── index.html # HTML 模板
|
├── index.html # HTML 模板
|
||||||
├── package.json # 项目配置
|
├── package.json # 项目配置
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { ServiceManager } from "./services/ServiceManager";
|
|||||||
import { HostsManager } from "./services/HostsManager";
|
import { HostsManager } from "./services/HostsManager";
|
||||||
import { GitManager } from "./services/GitManager";
|
import { GitManager } from "./services/GitManager";
|
||||||
import { PythonManager } from "./services/PythonManager";
|
import { PythonManager } from "./services/PythonManager";
|
||||||
|
import { LogManager } from "./services/LogManager";
|
||||||
import { ConfigStore } from "./services/ConfigStore";
|
import { ConfigStore } from "./services/ConfigStore";
|
||||||
|
|
||||||
// 获取图标路径
|
// 获取图标路径
|
||||||
@ -120,6 +121,7 @@ const serviceManager = new ServiceManager(configStore);
|
|||||||
const hostsManager = new HostsManager();
|
const hostsManager = new HostsManager();
|
||||||
const gitManager = new GitManager(configStore);
|
const gitManager = new GitManager(configStore);
|
||||||
const pythonManager = new PythonManager(configStore);
|
const pythonManager = new PythonManager(configStore);
|
||||||
|
const logManager = new LogManager(configStore);
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const appIcon = createWindowIcon();
|
const appIcon = createWindowIcon();
|
||||||
@ -575,9 +577,11 @@ ipcMain.handle("config:setBasePath", (_, path: string) =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
// ==================== 应用设置 ====================
|
// ==================== 应用设置 ====================
|
||||||
// 设置开机自启(以管理员模式,使用任务计划程序)
|
// 设置开机自启(以管理员模式,使用任务计划程序,静默启动)
|
||||||
ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
|
ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
|
||||||
const { execSync } = require("child_process");
|
const { execSync, exec } = require("child_process");
|
||||||
|
const { writeFileSync, unlinkSync, existsSync } = require("fs");
|
||||||
|
const { join } = require("path");
|
||||||
const exePath = app.getPath("exe");
|
const exePath = app.getPath("exe");
|
||||||
const taskName = "PHPerDevManager";
|
const taskName = "PHPerDevManager";
|
||||||
|
|
||||||
@ -601,12 +605,18 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
|
|||||||
// 忽略删除失败(可能任务不存在)
|
// 忽略删除失败(可能任务不存在)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建任务计划程序任务,以最高权限运行
|
// 创建 VBS 启动脚本(确保静默启动)
|
||||||
const command = `schtasks /create /tn "${taskName}" /tr "\\"${exePath}\\"" /sc onlogon /rl highest /f`;
|
const appDir = require("path").dirname(exePath);
|
||||||
|
const vbsPath = join(appDir, "silent_start.vbs");
|
||||||
|
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run """${exePath.replace(/\\/g, "\\\\")}""", 0, False`;
|
||||||
|
writeFileSync(vbsPath, vbsContent);
|
||||||
|
|
||||||
|
// 创建任务计划程序任务,运行 VBS 脚本实现静默启动
|
||||||
|
const command = `schtasks /create /tn "${taskName}" /tr "wscript.exe \\"${vbsPath}\\"" /sc onlogon /rl highest /f`;
|
||||||
execSync(command, { encoding: "buffer", windowsHide: true });
|
execSync(command, { encoding: "buffer", windowsHide: true });
|
||||||
|
|
||||||
configStore.set("autoLaunch", true);
|
configStore.set("autoLaunch", true);
|
||||||
return { success: true, message: "已启用开机自启(管理员模式)" };
|
return { success: true, message: "已启用开机自启(静默模式)" };
|
||||||
} else {
|
} else {
|
||||||
// 删除任务计划程序任务
|
// 删除任务计划程序任务
|
||||||
try {
|
try {
|
||||||
@ -617,6 +627,18 @@ ipcMain.handle("app:setAutoLaunch", async (_, enabled: boolean) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 忽略删除失败
|
// 忽略删除失败
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除 VBS 脚本
|
||||||
|
const appDir = require("path").dirname(exePath);
|
||||||
|
const vbsPath = join(appDir, "silent_start.vbs");
|
||||||
|
if (existsSync(vbsPath)) {
|
||||||
|
try {
|
||||||
|
unlinkSync(vbsPath);
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略删除失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
configStore.set("autoLaunch", false);
|
configStore.set("autoLaunch", false);
|
||||||
return { success: true, message: "已禁用开机自启" };
|
return { success: true, message: "已禁用开机自启" };
|
||||||
}
|
}
|
||||||
@ -650,6 +672,38 @@ ipcMain.handle("app:getAutoLaunch", async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取应用版本信息
|
||||||
|
ipcMain.handle("app:getVersion", async () => {
|
||||||
|
const { existsSync, readFileSync } = require("fs");
|
||||||
|
const { join } = require("path");
|
||||||
|
|
||||||
|
const version = app.getVersion();
|
||||||
|
let buildTime = "";
|
||||||
|
let buildDate = "";
|
||||||
|
|
||||||
|
// 尝试读取版本信息文件
|
||||||
|
try {
|
||||||
|
const versionFilePath = app.isPackaged
|
||||||
|
? join(process.resourcesPath, "public", "version.json")
|
||||||
|
: join(__dirname, "..", "public", "version.json");
|
||||||
|
|
||||||
|
if (existsSync(versionFilePath)) {
|
||||||
|
const versionInfo = JSON.parse(readFileSync(versionFilePath, "utf-8"));
|
||||||
|
buildTime = versionInfo.buildTime || "";
|
||||||
|
buildDate = versionInfo.buildDate || "";
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略错误
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
buildTime,
|
||||||
|
buildDate,
|
||||||
|
isPackaged: app.isPackaged
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// 设置启动时最小化到托盘
|
// 设置启动时最小化到托盘
|
||||||
ipcMain.handle("app:setStartMinimized", (_, enabled: boolean) => {
|
ipcMain.handle("app:setStartMinimized", (_, enabled: boolean) => {
|
||||||
configStore.set("startMinimized", enabled);
|
configStore.set("startMinimized", enabled);
|
||||||
@ -666,3 +720,13 @@ ipcMain.handle("app:quit", () => {
|
|||||||
isQuitting = true;
|
isQuitting = true;
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== 日志管理 ====================
|
||||||
|
ipcMain.handle("log:getFiles", () => logManager.getLogFiles());
|
||||||
|
ipcMain.handle("log:read", (_, logPath: string, lines?: number) =>
|
||||||
|
logManager.readLog(logPath, lines)
|
||||||
|
);
|
||||||
|
ipcMain.handle("log:clear", (_, logPath: string) => logManager.clearLog(logPath));
|
||||||
|
ipcMain.handle("log:getDirectory", (_, type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string) =>
|
||||||
|
logManager.getLogDirectory(type, version)
|
||||||
|
);
|
||||||
|
|||||||
@ -166,12 +166,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
setBasePath: (path: string) => ipcRenderer.invoke('config:setBasePath', path)
|
setBasePath: (path: string) => ipcRenderer.invoke('config:setBasePath', path)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 日志管理
|
||||||
|
log: {
|
||||||
|
getFiles: () => ipcRenderer.invoke('log:getFiles'),
|
||||||
|
read: (logPath: string, lines?: number) => ipcRenderer.invoke('log:read', logPath, lines),
|
||||||
|
clear: (logPath: string) => ipcRenderer.invoke('log:clear', logPath),
|
||||||
|
getDirectory: (type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string) =>
|
||||||
|
ipcRenderer.invoke('log:getDirectory', type, version)
|
||||||
|
},
|
||||||
|
|
||||||
// 应用设置
|
// 应用设置
|
||||||
app: {
|
app: {
|
||||||
setAutoLaunch: (enabled: boolean) => ipcRenderer.invoke('app:setAutoLaunch', enabled),
|
setAutoLaunch: (enabled: boolean) => ipcRenderer.invoke('app:setAutoLaunch', enabled),
|
||||||
getAutoLaunch: () => ipcRenderer.invoke('app:getAutoLaunch'),
|
getAutoLaunch: () => ipcRenderer.invoke('app:getAutoLaunch'),
|
||||||
setStartMinimized: (enabled: boolean) => ipcRenderer.invoke('app:setStartMinimized', enabled),
|
setStartMinimized: (enabled: boolean) => ipcRenderer.invoke('app:setStartMinimized', enabled),
|
||||||
getStartMinimized: () => ipcRenderer.invoke('app:getStartMinimized'),
|
getStartMinimized: () => ipcRenderer.invoke('app:getStartMinimized'),
|
||||||
|
getVersion: () => ipcRenderer.invoke('app:getVersion') as Promise<{ version: string; buildTime: string; buildDate: string; isPackaged: boolean }>,
|
||||||
setAutoStartServices: (enabled: boolean) => ipcRenderer.invoke('app:setAutoStartServices', enabled),
|
setAutoStartServices: (enabled: boolean) => ipcRenderer.invoke('app:setAutoStartServices', enabled),
|
||||||
getAutoStartServices: () => ipcRenderer.invoke('app:getAutoStartServices'),
|
getAutoStartServices: () => ipcRenderer.invoke('app:getAutoStartServices'),
|
||||||
quit: () => ipcRenderer.invoke('app:quit')
|
quit: () => ipcRenderer.invoke('app:quit')
|
||||||
|
|||||||
277
electron/services/LogManager.ts
Normal file
277
electron/services/LogManager.ts
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
import { ConfigStore } from './ConfigStore'
|
||||||
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs'
|
||||||
|
import { join, basename } from 'path'
|
||||||
|
|
||||||
|
export interface LogFile {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
size: number
|
||||||
|
modifiedTime: Date
|
||||||
|
type: 'nginx' | 'nginx-error' | 'nginx-access' | 'php' | 'mysql' | 'mysql-error' | 'site-access' | 'site-error'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogContent {
|
||||||
|
content: string
|
||||||
|
totalLines: number
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LogManager {
|
||||||
|
private configStore: ConfigStore
|
||||||
|
|
||||||
|
constructor(configStore: ConfigStore) {
|
||||||
|
this.configStore = configStore
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有可用的日志文件列表
|
||||||
|
*/
|
||||||
|
async getLogFiles(): Promise<{ nginx: LogFile[], php: LogFile[], mysql: LogFile[], sites: LogFile[] }> {
|
||||||
|
const result = {
|
||||||
|
nginx: [] as LogFile[],
|
||||||
|
php: [] as LogFile[],
|
||||||
|
mysql: [] as LogFile[],
|
||||||
|
sites: [] as LogFile[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nginx 日志
|
||||||
|
const nginxPath = this.configStore.getNginxPath()
|
||||||
|
const nginxLogsDir = join(nginxPath, 'logs')
|
||||||
|
if (existsSync(nginxLogsDir)) {
|
||||||
|
const files = this.scanLogDir(nginxLogsDir)
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.name.includes('error')) {
|
||||||
|
result.nginx.push({ ...file, type: 'nginx-error' })
|
||||||
|
} else if (file.name.includes('access')) {
|
||||||
|
result.nginx.push({ ...file, type: 'nginx-access' })
|
||||||
|
} else {
|
||||||
|
result.nginx.push({ ...file, type: 'nginx' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP 日志 - 检查每个 PHP 版本的日志
|
||||||
|
const phpVersions = this.configStore.get('phpVersions') || []
|
||||||
|
for (const version of phpVersions) {
|
||||||
|
const phpPath = this.configStore.getPhpPath(version)
|
||||||
|
const phpLogsDir = join(phpPath, 'logs')
|
||||||
|
if (existsSync(phpLogsDir)) {
|
||||||
|
const files = this.scanLogDir(phpLogsDir)
|
||||||
|
for (const file of files) {
|
||||||
|
result.php.push({ ...file, type: 'php', name: `[${version}] ${file.name}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 也检查 php.ini 中配置的 error_log
|
||||||
|
const phpErrorLog = join(phpPath, 'php_errors.log')
|
||||||
|
if (existsSync(phpErrorLog)) {
|
||||||
|
const stat = statSync(phpErrorLog)
|
||||||
|
result.php.push({
|
||||||
|
name: `[${version}] php_errors.log`,
|
||||||
|
path: phpErrorLog,
|
||||||
|
size: stat.size,
|
||||||
|
modifiedTime: stat.mtime,
|
||||||
|
type: 'php'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQL 日志
|
||||||
|
const mysqlVersions = this.configStore.get('mysqlVersions') || []
|
||||||
|
for (const version of mysqlVersions) {
|
||||||
|
const mysqlPath = this.configStore.getMysqlPath(version)
|
||||||
|
// MySQL 日志通常在 data 目录下
|
||||||
|
const mysqlDataDir = join(mysqlPath, 'data')
|
||||||
|
if (existsSync(mysqlDataDir)) {
|
||||||
|
const files = readdirSync(mysqlDataDir)
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith('.err') || file.endsWith('.log')) {
|
||||||
|
const filePath = join(mysqlDataDir, file)
|
||||||
|
const stat = statSync(filePath)
|
||||||
|
const logType = file.includes('error') || file.endsWith('.err') ? 'mysql-error' : 'mysql'
|
||||||
|
result.mysql.push({
|
||||||
|
name: `[${version}] ${file}`,
|
||||||
|
path: filePath,
|
||||||
|
size: stat.size,
|
||||||
|
modifiedTime: stat.mtime,
|
||||||
|
type: logType
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 也检查 logs 目录
|
||||||
|
const mysqlLogsDir = join(mysqlPath, 'logs')
|
||||||
|
if (existsSync(mysqlLogsDir)) {
|
||||||
|
const files = this.scanLogDir(mysqlLogsDir)
|
||||||
|
for (const file of files) {
|
||||||
|
result.mysql.push({ ...file, type: 'mysql', name: `[${version}] ${file.name}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点日志 - Nginx sites logs
|
||||||
|
const sites = this.configStore.get('sites') || []
|
||||||
|
for (const site of sites) {
|
||||||
|
// 站点日志通常在 nginx/logs 目录下,以域名命名
|
||||||
|
const siteAccessLog = join(nginxLogsDir, `${site.domain}.access.log`)
|
||||||
|
const siteErrorLog = join(nginxLogsDir, `${site.domain}.error.log`)
|
||||||
|
|
||||||
|
if (existsSync(siteAccessLog)) {
|
||||||
|
const stat = statSync(siteAccessLog)
|
||||||
|
result.sites.push({
|
||||||
|
name: `${site.domain} - 访问日志`,
|
||||||
|
path: siteAccessLog,
|
||||||
|
size: stat.size,
|
||||||
|
modifiedTime: stat.mtime,
|
||||||
|
type: 'site-access'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(siteErrorLog)) {
|
||||||
|
const stat = statSync(siteErrorLog)
|
||||||
|
result.sites.push({
|
||||||
|
name: `${site.domain} - 错误日志`,
|
||||||
|
path: siteErrorLog,
|
||||||
|
size: stat.size,
|
||||||
|
modifiedTime: stat.mtime,
|
||||||
|
type: 'site-error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取日志文件内容
|
||||||
|
* @param logPath 日志文件路径
|
||||||
|
* @param lines 读取的行数(从末尾开始),默认 500 行
|
||||||
|
*/
|
||||||
|
async readLog(logPath: string, lines: number = 500): Promise<LogContent> {
|
||||||
|
if (!existsSync(logPath)) {
|
||||||
|
return { content: '日志文件不存在', totalLines: 0, fileSize: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stat = statSync(logPath)
|
||||||
|
const fileSize = stat.size
|
||||||
|
|
||||||
|
// 如果文件小于 1MB,直接读取全部内容
|
||||||
|
if (fileSize < 1024 * 1024) {
|
||||||
|
const content = readFileSync(logPath, 'utf-8')
|
||||||
|
const allLines = content.split('\n')
|
||||||
|
const totalLines = allLines.length
|
||||||
|
|
||||||
|
// 取最后 N 行
|
||||||
|
const lastLines = allLines.slice(-lines).join('\n')
|
||||||
|
return { content: lastLines, totalLines, fileSize }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 大文件:从末尾读取
|
||||||
|
const content = await this.readLastLines(logPath, lines)
|
||||||
|
return { content, totalLines: lines, fileSize }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { content: `读取日志失败: ${error.message}`, totalLines: 0, fileSize: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文件末尾读取指定行数
|
||||||
|
*/
|
||||||
|
private async readLastLines(filePath: string, lines: number): Promise<string> {
|
||||||
|
const fs = await import('fs/promises')
|
||||||
|
const stat = await fs.stat(filePath)
|
||||||
|
const fileSize = stat.size
|
||||||
|
|
||||||
|
// 估算需要读取的字节数(假设每行约 200 字节)
|
||||||
|
const bytesToRead = Math.min(fileSize, lines * 200)
|
||||||
|
const startPosition = Math.max(0, fileSize - bytesToRead)
|
||||||
|
|
||||||
|
const buffer = Buffer.alloc(bytesToRead)
|
||||||
|
const fd = await fs.open(filePath, 'r')
|
||||||
|
await fd.read(buffer, 0, bytesToRead, startPosition)
|
||||||
|
await fd.close()
|
||||||
|
|
||||||
|
const content = buffer.toString('utf-8')
|
||||||
|
const allLines = content.split('\n')
|
||||||
|
|
||||||
|
// 第一行可能不完整,跳过
|
||||||
|
const completeLines = startPosition === 0 ? allLines : allLines.slice(1)
|
||||||
|
|
||||||
|
return completeLines.slice(-lines).join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志文件
|
||||||
|
*/
|
||||||
|
async clearLog(logPath: string): Promise<{ success: boolean, message: string }> {
|
||||||
|
if (!existsSync(logPath)) {
|
||||||
|
return { success: false, message: '日志文件不存在' }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fs = await import('fs/promises')
|
||||||
|
await fs.writeFile(logPath, '')
|
||||||
|
return { success: true, message: '日志已清空' }
|
||||||
|
} catch (error: any) {
|
||||||
|
return { success: false, message: `清空日志失败: ${error.message}` }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扫描目录中的日志文件
|
||||||
|
*/
|
||||||
|
private scanLogDir(dir: string): LogFile[] {
|
||||||
|
const files: LogFile[] = []
|
||||||
|
|
||||||
|
if (!existsSync(dir)) {
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = readdirSync(dir)
|
||||||
|
for (const item of items) {
|
||||||
|
const filePath = join(dir, item)
|
||||||
|
const stat = statSync(filePath)
|
||||||
|
|
||||||
|
if (stat.isFile() && (item.endsWith('.log') || item.endsWith('.err'))) {
|
||||||
|
files.push({
|
||||||
|
name: item,
|
||||||
|
path: filePath,
|
||||||
|
size: stat.size,
|
||||||
|
modifiedTime: stat.mtime,
|
||||||
|
type: 'nginx' // 默认类型,调用方会覆盖
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('扫描日志目录失败:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files.sort((a, b) => b.modifiedTime.getTime() - a.modifiedTime.getTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取日志文件路径(用于在文件管理器中打开)
|
||||||
|
*/
|
||||||
|
getLogDirectory(type: 'nginx' | 'php' | 'mysql' | 'sites', version?: string): string {
|
||||||
|
switch (type) {
|
||||||
|
case 'nginx':
|
||||||
|
return join(this.configStore.getNginxPath(), 'logs')
|
||||||
|
case 'php':
|
||||||
|
if (version) {
|
||||||
|
return join(this.configStore.getPhpPath(version), 'logs')
|
||||||
|
}
|
||||||
|
return join(this.configStore.getBasePath(), 'php')
|
||||||
|
case 'mysql':
|
||||||
|
if (version) {
|
||||||
|
return join(this.configStore.getMysqlPath(version), 'data')
|
||||||
|
}
|
||||||
|
return join(this.configStore.getBasePath(), 'mysql')
|
||||||
|
case 'sites':
|
||||||
|
return join(this.configStore.getNginxPath(), 'logs')
|
||||||
|
default:
|
||||||
|
return this.configStore.getBasePath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -331,9 +331,12 @@ export class NginxManager {
|
|||||||
return { success: true, message: 'Nginx 已经在运行' }
|
return { success: true, message: 'Nginx 已经在运行' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 启动 Nginx
|
// 使用 VBScript 静默启动 Nginx
|
||||||
const child = spawn(nginxExe, [], {
|
const vbsPath = join(nginxPath, 'start_nginx.vbs')
|
||||||
cwd: nginxPath,
|
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.CurrentDirectory = "${nginxPath.replace(/\\/g, '\\\\')}"\nWshShell.Run """${nginxExe.replace(/\\/g, '\\\\')}""", 0, False`
|
||||||
|
writeFileSync(vbsPath, vbsContent)
|
||||||
|
|
||||||
|
const child = spawn('wscript.exe', [vbsPath], {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
windowsHide: true
|
windowsHide: true
|
||||||
|
|||||||
@ -307,15 +307,16 @@ export class RedisManager {
|
|||||||
await this.createDefaultConfig()
|
await this.createDefaultConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用相对路径启动(避免 Cygwin 路径问题)
|
// 使用 VBScript 静默启动 Redis(避免黑窗口闪烁)
|
||||||
// Redis Windows 版本使用 Cygwin,需要在正确的工作目录下用相对路径
|
|
||||||
const configFileName = 'redis.windows.conf'
|
const configFileName = 'redis.windows.conf'
|
||||||
const child = spawn(redisServer, [configFileName], {
|
const vbsPath = join(redisPath, 'start_redis.vbs')
|
||||||
cwd: redisPath,
|
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.CurrentDirectory = "${redisPath.replace(/\\/g, '\\\\')}"\nWshShell.Run """${redisServer.replace(/\\/g, '\\\\')}""" & " " & "${configFileName}", 0, False`
|
||||||
|
writeFileSync(vbsPath, vbsContent)
|
||||||
|
|
||||||
|
const child = spawn('wscript.exe', [vbsPath], {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
windowsHide: true,
|
windowsHide: true
|
||||||
shell: false
|
|
||||||
})
|
})
|
||||||
child.unref()
|
child.unref()
|
||||||
|
|
||||||
|
|||||||
@ -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, readdirSync } from 'fs'
|
import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
const execAsync = promisify(exec)
|
const execAsync = promisify(exec)
|
||||||
@ -562,6 +562,32 @@ export class ServiceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async startProcess(exe: string, args: string[], cwd: string): Promise<void> {
|
private async startProcess(exe: string, args: string[], cwd: string): Promise<void> {
|
||||||
|
// 使用 VBScript 来完全隐藏窗口启动进程
|
||||||
|
const argsStr = args.map(a => `"${a}"`).join(' ')
|
||||||
|
const command = args.length > 0 ? `"${exe}" ${argsStr}` : `"${exe}"`
|
||||||
|
|
||||||
|
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\nWshShell.Run ${JSON.stringify(command)}, 0, False`
|
||||||
|
const vbsPath = join(cwd, `start_${Date.now()}.vbs`)
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(vbsPath, vbsContent)
|
||||||
|
await execAsync(`cscript //nologo "${vbsPath}"`, {
|
||||||
|
cwd,
|
||||||
|
windowsHide: true,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
// 延迟删除 VBS 文件
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
if (existsSync(vbsPath)) {
|
||||||
|
unlinkSync(vbsPath)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略删除失败
|
||||||
|
}
|
||||||
|
}, 2000)
|
||||||
|
} catch (error) {
|
||||||
|
// 如果 VBS 方式失败,回退到 spawn
|
||||||
const child = spawn(exe, args, {
|
const child = spawn(exe, args, {
|
||||||
cwd,
|
cwd,
|
||||||
detached: true,
|
detached: true,
|
||||||
@ -571,4 +597,5 @@ export class ServiceManager {
|
|||||||
child.unref()
|
child.unref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,10 +5,14 @@
|
|||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc --noEmit && vite build && electron-builder",
|
"build": "node scripts/bump-version.js && vue-tsc --noEmit && vite build && electron-builder",
|
||||||
|
"build:patch": "node scripts/bump-version.js patch && vue-tsc --noEmit && vite build && electron-builder",
|
||||||
|
"build:minor": "node scripts/bump-version.js minor && vue-tsc --noEmit && vite build && electron-builder",
|
||||||
|
"build:major": "node scripts/bump-version.js major && vue-tsc --noEmit && vite build && electron-builder",
|
||||||
|
"build:nobump": "vue-tsc --noEmit && vite build && electron-builder",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"electron:dev": "vite",
|
"electron:dev": "vite",
|
||||||
"electron:build": "vite build && electron-builder"
|
"electron:build": "node scripts/bump-version.js && vite build && electron-builder"
|
||||||
},
|
},
|
||||||
"author": "PHPer",
|
"author": "PHPer",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
58
scripts/bump-version.js
Normal file
58
scripts/bump-version.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* 自动更新版本号脚本
|
||||||
|
* 每次打包时自动增加 patch 版本号
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* node scripts/bump-version.js # patch: 1.0.0 -> 1.0.1
|
||||||
|
* node scripts/bump-version.js minor # minor: 1.0.0 -> 1.1.0
|
||||||
|
* node scripts/bump-version.js major # major: 1.0.0 -> 2.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
const packagePath = path.join(__dirname, '..', 'package.json')
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf-8'))
|
||||||
|
|
||||||
|
const currentVersion = pkg.version
|
||||||
|
const [major, minor, patch] = currentVersion.split('.').map(Number)
|
||||||
|
|
||||||
|
// 获取命令行参数
|
||||||
|
const bumpType = process.argv[2] || 'patch'
|
||||||
|
|
||||||
|
let newVersion
|
||||||
|
switch (bumpType) {
|
||||||
|
case 'major':
|
||||||
|
newVersion = `${major + 1}.0.0`
|
||||||
|
break
|
||||||
|
case 'minor':
|
||||||
|
newVersion = `${major}.${minor + 1}.0`
|
||||||
|
break
|
||||||
|
case 'patch':
|
||||||
|
default:
|
||||||
|
newVersion = `${major}.${minor}.${patch + 1}`
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 package.json
|
||||||
|
pkg.version = newVersion
|
||||||
|
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
|
||||||
|
|
||||||
|
// 生成构建时间戳
|
||||||
|
const buildTime = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
||||||
|
|
||||||
|
console.log(`✅ 版本号已更新: ${currentVersion} -> ${newVersion}`)
|
||||||
|
console.log(`📦 构建时间: ${buildTime}`)
|
||||||
|
|
||||||
|
// 将版本信息写入一个文件,供应用读取
|
||||||
|
const versionInfo = {
|
||||||
|
version: newVersion,
|
||||||
|
buildTime: new Date().toISOString(),
|
||||||
|
buildDate: new Date().toLocaleDateString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionFilePath = path.join(__dirname, '..', 'public', 'version.json')
|
||||||
|
fs.writeFileSync(versionFilePath, JSON.stringify(versionInfo, null, 2))
|
||||||
|
|
||||||
|
console.log(`📄 版本信息已写入: public/version.json`)
|
||||||
|
|
||||||
32
src/App.vue
32
src/App.vue
@ -62,10 +62,10 @@
|
|||||||
|
|
||||||
<!-- 内容区 -->
|
<!-- 内容区 -->
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component, route }">
|
||||||
<transition name="fade" mode="out-in">
|
<keep-alive :include="cachedViews">
|
||||||
<component :is="Component" />
|
<component :is="Component" :key="route.path" />
|
||||||
</transition>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@ -83,6 +83,21 @@ const isDark = ref(true)
|
|||||||
const startingAll = ref(false)
|
const startingAll = ref(false)
|
||||||
const stoppingAll = ref(false)
|
const stoppingAll = ref(false)
|
||||||
|
|
||||||
|
// 缓存的视图列表 - 避免页面切换闪烁
|
||||||
|
const cachedViews = [
|
||||||
|
'Dashboard',
|
||||||
|
'PhpManager',
|
||||||
|
'MysqlManager',
|
||||||
|
'NginxManager',
|
||||||
|
'RedisManager',
|
||||||
|
'NodeManager',
|
||||||
|
'PythonManager',
|
||||||
|
'GitManager',
|
||||||
|
'SitesManager',
|
||||||
|
'HostsManager',
|
||||||
|
'Settings'
|
||||||
|
]
|
||||||
|
|
||||||
// 从 store 获取服务状态
|
// 从 store 获取服务状态
|
||||||
const serviceStatus = computed(() => ({
|
const serviceStatus = computed(() => ({
|
||||||
nginx: store.serviceStatus.nginx,
|
nginx: store.serviceStatus.nginx,
|
||||||
@ -352,14 +367,5 @@ onUnmounted(() => {
|
|||||||
background: var(--bg-content);
|
background: var(--bg-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-active,
|
|
||||||
.fade-leave-active {
|
|
||||||
transition: opacity 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter-from,
|
|
||||||
.fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
512
src/components/LogViewer.vue
Normal file
512
src/components/LogViewer.vue
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="900px"
|
||||||
|
class="log-viewer-dialog"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@closed="onClosed"
|
||||||
|
>
|
||||||
|
<!-- 日志文件选择器 -->
|
||||||
|
<div class="log-selector">
|
||||||
|
<el-tabs v-model="activeTab" @tab-change="onTabChange">
|
||||||
|
<el-tab-pane label="Nginx" name="nginx">
|
||||||
|
<div class="log-list" v-if="logFiles.nginx.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="file in logFiles.nginx"
|
||||||
|
:key="file.path"
|
||||||
|
class="log-item"
|
||||||
|
:class="{ active: selectedLog?.path === file.path }"
|
||||||
|
@click="selectLog(file)"
|
||||||
|
>
|
||||||
|
<div class="log-item-info">
|
||||||
|
<el-icon :class="getLogIcon(file.type)"><Document /></el-icon>
|
||||||
|
<span class="log-name">{{ file.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="log-item-meta">
|
||||||
|
<span class="log-size">{{ formatSize(file.size) }}</span>
|
||||||
|
<span class="log-time">{{ formatTime(file.modifiedTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-log">暂无 Nginx 日志文件</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="PHP" name="php">
|
||||||
|
<div class="log-list" v-if="logFiles.php.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="file in logFiles.php"
|
||||||
|
:key="file.path"
|
||||||
|
class="log-item"
|
||||||
|
:class="{ active: selectedLog?.path === file.path }"
|
||||||
|
@click="selectLog(file)"
|
||||||
|
>
|
||||||
|
<div class="log-item-info">
|
||||||
|
<el-icon class="php-icon"><Document /></el-icon>
|
||||||
|
<span class="log-name">{{ file.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="log-item-meta">
|
||||||
|
<span class="log-size">{{ formatSize(file.size) }}</span>
|
||||||
|
<span class="log-time">{{ formatTime(file.modifiedTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-log">暂无 PHP 日志文件</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="MySQL" name="mysql">
|
||||||
|
<div class="log-list" v-if="logFiles.mysql.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="file in logFiles.mysql"
|
||||||
|
:key="file.path"
|
||||||
|
class="log-item"
|
||||||
|
:class="{ active: selectedLog?.path === file.path }"
|
||||||
|
@click="selectLog(file)"
|
||||||
|
>
|
||||||
|
<div class="log-item-info">
|
||||||
|
<el-icon class="mysql-icon"><Document /></el-icon>
|
||||||
|
<span class="log-name">{{ file.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="log-item-meta">
|
||||||
|
<span class="log-size">{{ formatSize(file.size) }}</span>
|
||||||
|
<span class="log-time">{{ formatTime(file.modifiedTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-log">暂无 MySQL 日志文件</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="站点" name="sites">
|
||||||
|
<div class="log-list" v-if="logFiles.sites.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="file in logFiles.sites"
|
||||||
|
:key="file.path"
|
||||||
|
class="log-item"
|
||||||
|
:class="{ active: selectedLog?.path === file.path }"
|
||||||
|
@click="selectLog(file)"
|
||||||
|
>
|
||||||
|
<div class="log-item-info">
|
||||||
|
<el-icon :class="file.type === 'site-error' ? 'error-icon' : 'access-icon'">
|
||||||
|
<Document />
|
||||||
|
</el-icon>
|
||||||
|
<span class="log-name">{{ file.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="log-item-meta">
|
||||||
|
<span class="log-size">{{ formatSize(file.size) }}</span>
|
||||||
|
<span class="log-time">{{ formatTime(file.modifiedTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-log">暂无站点日志文件</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 日志内容区域 -->
|
||||||
|
<div class="log-content-area" v-if="selectedLog">
|
||||||
|
<div class="log-toolbar">
|
||||||
|
<div class="toolbar-left">
|
||||||
|
<span class="selected-log-name">{{ selectedLog.name }}</span>
|
||||||
|
<el-tag size="small" type="info">{{ formatSize(logContent.fileSize) }}</el-tag>
|
||||||
|
<el-tag size="small" type="info">{{ logContent.totalLines }} 行</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<el-button size="small" @click="refreshLog" :loading="loading">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="openInExplorer">
|
||||||
|
<el-icon><FolderOpened /></el-icon>
|
||||||
|
打开目录
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="clearLog">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
清空
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="log-content-wrapper">
|
||||||
|
<pre class="log-content" ref="logContentRef">{{ logContent.content || '(日志为空)' }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="log-content-area empty" v-else>
|
||||||
|
<el-icon class="empty-icon"><Document /></el-icon>
|
||||||
|
<p>请选择左侧的日志文件查看</p>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch, nextTick } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Document, Refresh, FolderOpened, Delete } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
interface LogFile {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
size: number
|
||||||
|
modifiedTime: string | Date
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogContent {
|
||||||
|
content: string
|
||||||
|
totalLines: number
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
initialTab?: 'nginx' | 'php' | 'mysql' | 'sites'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const activeTab = ref<'nginx' | 'php' | 'mysql' | 'sites'>('nginx')
|
||||||
|
const loading = ref(false)
|
||||||
|
const logContentRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const logFiles = reactive<{
|
||||||
|
nginx: LogFile[]
|
||||||
|
php: LogFile[]
|
||||||
|
mysql: LogFile[]
|
||||||
|
sites: LogFile[]
|
||||||
|
}>({
|
||||||
|
nginx: [],
|
||||||
|
php: [],
|
||||||
|
mysql: [],
|
||||||
|
sites: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedLog = ref<LogFile | null>(null)
|
||||||
|
const logContent = reactive<LogContent>({
|
||||||
|
content: '',
|
||||||
|
totalLines: 0,
|
||||||
|
fileSize: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const dialogTitle = ref('日志查看器')
|
||||||
|
|
||||||
|
// 监听外部 modelValue 变化
|
||||||
|
watch(() => props.modelValue, async (newVal) => {
|
||||||
|
visible.value = newVal
|
||||||
|
if (newVal) {
|
||||||
|
if (props.initialTab) {
|
||||||
|
activeTab.value = props.initialTab
|
||||||
|
}
|
||||||
|
await loadLogFiles()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 同步内部状态到外部
|
||||||
|
watch(visible, (newVal) => {
|
||||||
|
emit('update:modelValue', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载日志文件列表
|
||||||
|
const loadLogFiles = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const files = await window.electronAPI?.log.getFiles()
|
||||||
|
if (files) {
|
||||||
|
logFiles.nginx = files.nginx || []
|
||||||
|
logFiles.php = files.php || []
|
||||||
|
logFiles.mysql = files.mysql || []
|
||||||
|
logFiles.sites = files.sites || []
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error('加载日志列表失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择日志文件
|
||||||
|
const selectLog = async (file: LogFile) => {
|
||||||
|
selectedLog.value = file
|
||||||
|
await loadLogContent(file.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载日志内容
|
||||||
|
const loadLogContent = async (logPath: string) => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI?.log.read(logPath, 1000)
|
||||||
|
if (result) {
|
||||||
|
logContent.content = result.content
|
||||||
|
logContent.totalLines = result.totalLines
|
||||||
|
logContent.fileSize = result.fileSize
|
||||||
|
|
||||||
|
// 滚动到底部
|
||||||
|
await nextTick()
|
||||||
|
if (logContentRef.value) {
|
||||||
|
logContentRef.value.scrollTop = logContentRef.value.scrollHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error('读取日志失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新日志
|
||||||
|
const refreshLog = async () => {
|
||||||
|
if (selectedLog.value) {
|
||||||
|
await loadLogContent(selectedLog.value.path)
|
||||||
|
}
|
||||||
|
await loadLogFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空日志
|
||||||
|
const clearLog = async () => {
|
||||||
|
if (!selectedLog.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要清空日志 "${selectedLog.value.name}" 吗?此操作不可恢复。`,
|
||||||
|
'确认清空',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = await window.electronAPI?.log.clear(selectedLog.value.path)
|
||||||
|
if (result?.success) {
|
||||||
|
ElMessage.success(result.message)
|
||||||
|
await refreshLog()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '清空失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('操作失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在文件管理器中打开
|
||||||
|
const openInExplorer = async () => {
|
||||||
|
if (!selectedLog.value) return
|
||||||
|
|
||||||
|
// 获取文件所在目录
|
||||||
|
const path = selectedLog.value.path
|
||||||
|
const dir = path.substring(0, path.lastIndexOf('\\'))
|
||||||
|
await window.electronAPI?.openPath(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tab 切换
|
||||||
|
const onTabChange = () => {
|
||||||
|
selectedLog.value = null
|
||||||
|
logContent.content = ''
|
||||||
|
logContent.totalLines = 0
|
||||||
|
logContent.fileSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对话框关闭
|
||||||
|
const onClosed = () => {
|
||||||
|
selectedLog.value = null
|
||||||
|
logContent.content = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取日志图标样式
|
||||||
|
const getLogIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'nginx-error':
|
||||||
|
case 'mysql-error':
|
||||||
|
case 'site-error':
|
||||||
|
return 'error-icon'
|
||||||
|
case 'nginx-access':
|
||||||
|
case 'site-access':
|
||||||
|
return 'access-icon'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatSize = (bytes: number): string => {
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (time: string | Date): string => {
|
||||||
|
const date = typeof time === 'string' ? new Date(time) : time
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now.getTime() - date.getTime()
|
||||||
|
|
||||||
|
if (diff < 60000) return '刚刚'
|
||||||
|
if (diff < 3600000) return Math.floor(diff / 60000) + ' 分钟前'
|
||||||
|
if (diff < 86400000) return Math.floor(diff / 3600000) + ' 小时前'
|
||||||
|
if (diff < 604800000) return Math.floor(diff / 86400000) + ' 天前'
|
||||||
|
|
||||||
|
return date.toLocaleDateString()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.log-viewer-dialog {
|
||||||
|
:deep(.el-dialog__body) {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-selector {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding: 16px 20px 0;
|
||||||
|
|
||||||
|
:deep(.el-tabs__nav-wrap) {
|
||||||
|
&::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__item) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-list {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgba(124, 58, 237, 0.1);
|
||||||
|
border-left: 3px solid var(--accent-color);
|
||||||
|
padding-left: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
|
||||||
|
&.error-icon {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.access-icon {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.php-icon {
|
||||||
|
color: #777BB4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mysql-icon {
|
||||||
|
color: #00758F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-name {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-log {
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 20px;
|
||||||
|
background: var(--bg-input);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
|
||||||
|
.toolbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.selected-log-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
background: #0d1117;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-content {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
font-family: 'Fira Code', 'Consolas', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #c9d1d9;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -248,10 +248,9 @@
|
|||||||
class="mini-site-card"
|
class="mini-site-card"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
:href="(site.ssl ? 'https://' : 'http://') + site.domain"
|
href="#"
|
||||||
target="_blank"
|
|
||||||
class="site-domain-link"
|
class="site-domain-link"
|
||||||
@click.stop
|
@click.prevent="openSite(site)"
|
||||||
>
|
>
|
||||||
{{ site.domain }}
|
{{ site.domain }}
|
||||||
<el-icon class="link-icon"><Link /></el-icon>
|
<el-icon class="link-icon"><Link /></el-icon>
|
||||||
@ -277,11 +276,17 @@
|
|||||||
<el-icon><InfoFilled /></el-icon>
|
<el-icon><InfoFilled /></el-icon>
|
||||||
安装路径
|
安装路径
|
||||||
</span>
|
</span>
|
||||||
|
<div class="card-actions">
|
||||||
|
<el-button size="small" @click="showLogViewer = true">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
查看日志
|
||||||
|
</el-button>
|
||||||
<el-button size="small" @click="openBasePath">
|
<el-button size="small" @click="openBasePath">
|
||||||
<el-icon><FolderOpened /></el-icon>
|
<el-icon><FolderOpened /></el-icon>
|
||||||
打开目录
|
打开目录
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="path-display">
|
<div class="path-display">
|
||||||
<span class="path-label">基础路径:</span>
|
<span class="path-label">基础路径:</span>
|
||||||
@ -289,14 +294,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 日志查看器 -->
|
||||||
|
<LogViewer v-model="showLogViewer" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, onActivated } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Link, Promotion } from '@element-plus/icons-vue'
|
import { Link, Promotion, Document } from '@element-plus/icons-vue'
|
||||||
import { useServiceStore } from '@/stores/serviceStore'
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
import LogViewer from '@/components/LogViewer.vue'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'Dashboard'
|
||||||
|
})
|
||||||
|
|
||||||
const store = useServiceStore()
|
const store = useServiceStore()
|
||||||
|
|
||||||
@ -352,6 +366,7 @@ const basePath = computed(() => store.basePath)
|
|||||||
|
|
||||||
const settingPhp = ref('')
|
const settingPhp = ref('')
|
||||||
const settingNode = ref('')
|
const settingNode = ref('')
|
||||||
|
const showLogViewer = ref(false)
|
||||||
|
|
||||||
const startService = async (service: Service) => {
|
const startService = async (service: Service) => {
|
||||||
serviceLoadingState.value[service.name] = true
|
serviceLoadingState.value[service.name] = true
|
||||||
@ -533,6 +548,12 @@ const openBasePath = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在默认浏览器中打开站点
|
||||||
|
const openSite = (site: { domain: string, ssl: boolean }) => {
|
||||||
|
const protocol = site.ssl ? 'https' : 'http'
|
||||||
|
window.electronAPI?.openExternal(`${protocol}://${site.domain}`)
|
||||||
|
}
|
||||||
|
|
||||||
const setActivePhp = async (version: string) => {
|
const setActivePhp = async (version: string) => {
|
||||||
settingPhp.value = version
|
settingPhp.value = version
|
||||||
try {
|
try {
|
||||||
@ -570,8 +591,15 @@ const setActiveNode = async (version: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 总是刷新数据以确保数据最新
|
// 首次加载:如果没有数据则全量刷新
|
||||||
|
if (store.phpVersions.length === 0 && store.nodeVersions.length === 0) {
|
||||||
await store.refreshAll()
|
await store.refreshAll()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 从缓存激活时静默刷新状态(不会闪烁因为已有数据)
|
||||||
|
onActivated(async () => {
|
||||||
|
await store.refreshServiceStatus()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -177,6 +177,11 @@
|
|||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'GitManager'
|
||||||
|
})
|
||||||
|
|
||||||
interface GitVersion {
|
interface GitVersion {
|
||||||
version: string
|
version: string
|
||||||
path: string
|
path: string
|
||||||
|
|||||||
@ -89,6 +89,11 @@
|
|||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'HostsManager'
|
||||||
|
})
|
||||||
|
|
||||||
interface HostEntry {
|
interface HostEntry {
|
||||||
ip: string
|
ip: string
|
||||||
domain: string
|
domain: string
|
||||||
|
|||||||
@ -87,9 +87,13 @@
|
|||||||
密码
|
密码
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" @click="showConfig(version)">
|
<el-button size="small" @click="showConfig(version)">
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><Setting /></el-icon>
|
||||||
配置
|
配置
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button size="small" @click="showLogViewerDialog">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
日志
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
type="danger"
|
||||||
size="small"
|
size="small"
|
||||||
@ -222,14 +226,23 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 日志查看器 -->
|
||||||
|
<LogViewer v-model="showLogViewer" initial-tab="mysql" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted, onActivated } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
import { InfoFilled, Setting } from '@element-plus/icons-vue'
|
||||||
import { useServiceStore } from '@/stores/serviceStore'
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
import LogViewer from '@/components/LogViewer.vue'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'MysqlManager'
|
||||||
|
})
|
||||||
|
|
||||||
const store = useServiceStore()
|
const store = useServiceStore()
|
||||||
|
|
||||||
@ -271,6 +284,13 @@ const configContent = ref('')
|
|||||||
const savingConfig = ref(false)
|
const savingConfig = ref(false)
|
||||||
const currentVersion = ref('')
|
const currentVersion = ref('')
|
||||||
|
|
||||||
|
// 日志查看器
|
||||||
|
const showLogViewer = ref(false)
|
||||||
|
|
||||||
|
const showLogViewerDialog = () => {
|
||||||
|
showLogViewer.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const loadVersions = async () => {
|
const loadVersions = async () => {
|
||||||
try {
|
try {
|
||||||
installedVersions.value = await window.electronAPI?.mysql.getVersions() || []
|
installedVersions.value = await window.electronAPI?.mysql.getVersions() || []
|
||||||
|
|||||||
@ -74,8 +74,12 @@
|
|||||||
<el-icon><Refresh /></el-icon>
|
<el-icon><Refresh /></el-icon>
|
||||||
重载配置
|
重载配置
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="showConfig">
|
<el-button @click="showLogViewerDialog('nginx')">
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><Document /></el-icon>
|
||||||
|
查看日志
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="showConfig">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
编辑配置
|
编辑配置
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -180,14 +184,23 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 日志查看器 -->
|
||||||
|
<LogViewer v-model="showLogViewer" :initial-tab="logViewerTab" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted, onActivated } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
import { InfoFilled, Setting } from '@element-plus/icons-vue'
|
||||||
import { useServiceStore } from '@/stores/serviceStore'
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
import LogViewer from '@/components/LogViewer.vue'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'NginxManager'
|
||||||
|
})
|
||||||
|
|
||||||
const store = useServiceStore()
|
const store = useServiceStore()
|
||||||
|
|
||||||
@ -219,6 +232,15 @@ const showConfigDialog = ref(false)
|
|||||||
const configContent = ref('')
|
const configContent = ref('')
|
||||||
const savingConfig = ref(false)
|
const savingConfig = ref(false)
|
||||||
|
|
||||||
|
// 日志查看器
|
||||||
|
const showLogViewer = ref(false)
|
||||||
|
const logViewerTab = ref<'nginx' | 'php' | 'mysql' | 'sites'>('nginx')
|
||||||
|
|
||||||
|
const showLogViewerDialog = (tab: 'nginx' | 'sites') => {
|
||||||
|
logViewerTab.value = tab
|
||||||
|
showLogViewer.value = true
|
||||||
|
}
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const versions = await window.electronAPI?.nginx.getVersions() || []
|
const versions = await window.electronAPI?.nginx.getVersions() || []
|
||||||
|
|||||||
@ -140,6 +140,11 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Promotion, Box, InfoFilled, Loading } from '@element-plus/icons-vue'
|
import { Plus, Promotion, Box, InfoFilled, Loading } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'NodeManager'
|
||||||
|
})
|
||||||
|
|
||||||
interface NodeVersion {
|
interface NodeVersion {
|
||||||
version: string
|
version: string
|
||||||
path: string
|
path: string
|
||||||
|
|||||||
@ -45,11 +45,47 @@
|
|||||||
<div class="version-name">
|
<div class="version-name">
|
||||||
PHP {{ version.version }}
|
PHP {{ version.version }}
|
||||||
<el-tag v-if="version.isActive" type="success" size="small" class="ml-2">当前使用</el-tag>
|
<el-tag v-if="version.isActive" type="success" size="small" class="ml-2">当前使用</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="isCgiRunning(version.version)"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
CGI:{{ getCgiPort(version.version) }}
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-path">{{ version.path }}</div>
|
<div class="version-path">{{ version.path }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="version-actions">
|
<div class="version-actions">
|
||||||
|
<!-- CGI 控制按钮 -->
|
||||||
|
<el-tooltip
|
||||||
|
:content="isCgiRunning(version.version)
|
||||||
|
? `停止 CGI (端口 ${getCgiPort(version.version)})`
|
||||||
|
: `启动 CGI (端口 ${getCgiPort(version.version)})`"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<el-button
|
||||||
|
v-if="isCgiRunning(version.version)"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="stopCgi(version.version)"
|
||||||
|
:loading="cgiLoading[version.version]"
|
||||||
|
>
|
||||||
|
<el-icon><VideoPause /></el-icon>
|
||||||
|
CGI
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="startCgi(version.version)"
|
||||||
|
:loading="cgiLoading[version.version]"
|
||||||
|
>
|
||||||
|
<el-icon><VideoPlay /></el-icon>
|
||||||
|
CGI
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="!version.isActive"
|
v-if="!version.isActive"
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -63,9 +99,13 @@
|
|||||||
扩展
|
扩展
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="small" @click="showConfig(version)">
|
<el-button size="small" @click="showConfig(version)">
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><EditPen /></el-icon>
|
||||||
配置
|
配置
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button size="small" @click="showLogViewerDialog">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
日志
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
type="danger"
|
||||||
size="small"
|
size="small"
|
||||||
@ -366,14 +406,23 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 日志查看器 -->
|
||||||
|
<LogViewer v-model="showLogViewer" initial-tab="php" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, computed, onMounted, onUnmounted, onActivated } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { FolderOpened, InfoFilled } from '@element-plus/icons-vue'
|
import { FolderOpened, InfoFilled, VideoPlay, VideoPause, EditPen } from '@element-plus/icons-vue'
|
||||||
import { useServiceStore } from '@/stores/serviceStore'
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
import LogViewer from '@/components/LogViewer.vue'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'PhpManager'
|
||||||
|
})
|
||||||
|
|
||||||
const store = useServiceStore()
|
const store = useServiceStore()
|
||||||
|
|
||||||
@ -405,6 +454,7 @@ interface AvailableExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const initialLoaded = ref(false) // 标记是否已完成首次加载
|
||||||
const installedVersions = ref<PhpVersion[]>([])
|
const installedVersions = ref<PhpVersion[]>([])
|
||||||
const availableVersions = ref<AvailableVersion[]>([])
|
const availableVersions = ref<AvailableVersion[]>([])
|
||||||
const showInstallDialog = ref(false)
|
const showInstallDialog = ref(false)
|
||||||
@ -416,6 +466,30 @@ const downloadProgress = reactive({
|
|||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// CGI 状态管理
|
||||||
|
interface CgiStatus {
|
||||||
|
version: string
|
||||||
|
port: number
|
||||||
|
running: boolean
|
||||||
|
}
|
||||||
|
const cgiStatus = ref<CgiStatus[]>([])
|
||||||
|
const cgiLoading = ref<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
// 获取指定版本的 CGI 状态
|
||||||
|
const getCgiStatus = (version: string): CgiStatus | undefined => {
|
||||||
|
return cgiStatus.value.find(s => s.version === version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断 CGI 是否运行中
|
||||||
|
const isCgiRunning = (version: string): boolean => {
|
||||||
|
return getCgiStatus(version)?.running ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 CGI 端口
|
||||||
|
const getCgiPort = (version: string): number => {
|
||||||
|
return getCgiStatus(version)?.port ?? 9000
|
||||||
|
}
|
||||||
|
|
||||||
const showExtensionsDialog = ref(false)
|
const showExtensionsDialog = ref(false)
|
||||||
const loadingExtensions = ref(false)
|
const loadingExtensions = ref(false)
|
||||||
const extensions = ref<Extension[]>([])
|
const extensions = ref<Extension[]>([])
|
||||||
@ -465,6 +539,13 @@ const showConfigDialog = ref(false)
|
|||||||
const configContent = ref('')
|
const configContent = ref('')
|
||||||
const savingConfig = ref(false)
|
const savingConfig = ref(false)
|
||||||
|
|
||||||
|
// 日志查看器
|
||||||
|
const showLogViewer = ref(false)
|
||||||
|
|
||||||
|
const showLogViewerDialog = () => {
|
||||||
|
showLogViewer.value = true
|
||||||
|
}
|
||||||
|
|
||||||
// Composer 相关
|
// Composer 相关
|
||||||
const composerStatus = ref<{
|
const composerStatus = ref<{
|
||||||
installed: boolean
|
installed: boolean
|
||||||
@ -489,6 +570,51 @@ const loadVersions = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载 CGI 状态
|
||||||
|
const loadCgiStatus = async () => {
|
||||||
|
try {
|
||||||
|
cgiStatus.value = await window.electronAPI?.service.getPhpCgiStatus() || []
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载 CGI 状态失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动 CGI
|
||||||
|
const startCgi = async (version: string) => {
|
||||||
|
cgiLoading.value[version] = true
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI?.service.startPhpCgiVersion(version)
|
||||||
|
if (result?.success) {
|
||||||
|
ElMessage.success(result.message)
|
||||||
|
await loadCgiStatus()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '启动失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error('启动失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
cgiLoading.value[version] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止 CGI
|
||||||
|
const stopCgi = async (version: string) => {
|
||||||
|
cgiLoading.value[version] = true
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI?.service.stopPhpCgiVersion(version)
|
||||||
|
if (result?.success) {
|
||||||
|
ElMessage.success(result.message)
|
||||||
|
await loadCgiStatus()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result?.message || '停止失败')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error('停止失败: ' + error.message)
|
||||||
|
} finally {
|
||||||
|
cgiLoading.value[version] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Composer 相关方法
|
// Composer 相关方法
|
||||||
const loadComposerStatus = async () => {
|
const loadComposerStatus = async () => {
|
||||||
try {
|
try {
|
||||||
@ -812,10 +938,30 @@ const formatSize = (bytes: number) => {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
loadVersions()
|
// 使用 store 中的缓存数据进行首次渲染(避免闪烁)
|
||||||
loadAvailableVersions()
|
if (store.phpVersions.length > 0) {
|
||||||
|
installedVersions.value = store.phpVersions.map(v => ({
|
||||||
|
version: v.version,
|
||||||
|
path: v.path,
|
||||||
|
isActive: v.isActive
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首次加载数据
|
||||||
|
loading.value = installedVersions.value.length === 0
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
loadVersions(),
|
||||||
|
loadCgiStatus(),
|
||||||
loadComposerStatus()
|
loadComposerStatus()
|
||||||
|
])
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
initialLoaded.value = true
|
||||||
|
|
||||||
|
// 异步加载可用版本列表(不阻塞首屏)
|
||||||
|
loadAvailableVersions()
|
||||||
|
|
||||||
// 监听下载进度
|
// 监听下载进度
|
||||||
window.electronAPI?.onDownloadProgress((data: any) => {
|
window.electronAPI?.onDownloadProgress((data: any) => {
|
||||||
@ -831,6 +977,13 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 从缓存激活时静默刷新 CGI 状态
|
||||||
|
onActivated(async () => {
|
||||||
|
if (initialLoaded.value) {
|
||||||
|
await loadCgiStatus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.electronAPI?.removeDownloadProgressListener()
|
window.electronAPI?.removeDownloadProgressListener()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -182,6 +182,11 @@ import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
|||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
import { InfoFilled } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'PythonManager'
|
||||||
|
})
|
||||||
|
|
||||||
interface PythonVersion {
|
interface PythonVersion {
|
||||||
version: string
|
version: string
|
||||||
path: string
|
path: string
|
||||||
|
|||||||
@ -75,9 +75,13 @@
|
|||||||
重启
|
重启
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="showConfig">
|
<el-button @click="showConfig">
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><Setting /></el-icon>
|
||||||
编辑配置
|
编辑配置
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button @click="openLogDirectory">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
日志
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -184,11 +188,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, onMounted, onUnmounted, onActivated } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { InfoFilled } from '@element-plus/icons-vue'
|
import { InfoFilled, Setting } from '@element-plus/icons-vue'
|
||||||
import { useServiceStore } from '@/stores/serviceStore'
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'RedisManager'
|
||||||
|
})
|
||||||
|
|
||||||
const store = useServiceStore()
|
const store = useServiceStore()
|
||||||
|
|
||||||
interface RedisStatus {
|
interface RedisStatus {
|
||||||
@ -383,6 +392,19 @@ const saveConfig = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开 Redis 日志目录
|
||||||
|
const openLogDirectory = async () => {
|
||||||
|
try {
|
||||||
|
const basePath = await window.electronAPI?.config.getBasePath()
|
||||||
|
if (basePath) {
|
||||||
|
// Redis 日志通常与配置文件在同一目录
|
||||||
|
await window.electronAPI?.openPath(basePath + '\\redis')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error('打开目录失败: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const formatSize = (bytes: number) => {
|
const formatSize = (bytes: number) => {
|
||||||
if (bytes === 0) return '0 B'
|
if (bytes === 0) return '0 B'
|
||||||
const k = 1024
|
const k = 1024
|
||||||
|
|||||||
@ -111,7 +111,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="app-details">
|
<div class="app-details">
|
||||||
<h2 class="app-title">PHPer 开发环境管理器</h2>
|
<h2 class="app-title">PHPer 开发环境管理器</h2>
|
||||||
<p class="app-version">版本 1.0.0</p>
|
<p class="app-version">
|
||||||
|
版本 {{ appVersion.version }}
|
||||||
|
<span v-if="appVersion.buildDate" class="build-date">
|
||||||
|
({{ appVersion.buildDate }})
|
||||||
|
</span>
|
||||||
|
<el-tag v-if="!appVersion.isPackaged" type="warning" size="small" style="margin-left: 8px">开发版</el-tag>
|
||||||
|
</p>
|
||||||
<p class="app-desc">
|
<p class="app-desc">
|
||||||
一站式 PHP 开发环境管理工具,支持 PHP、MySQL、Nginx、Redis、Node.js 的安装和管理。
|
一站式 PHP 开发环境管理工具,支持 PHP、MySQL、Nginx、Redis、Node.js 的安装和管理。
|
||||||
</p>
|
</p>
|
||||||
@ -145,6 +151,11 @@ import { ref, reactive, onMounted } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Monitor } from '@element-plus/icons-vue'
|
import { Monitor } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'Settings'
|
||||||
|
})
|
||||||
|
|
||||||
interface ServiceAutoStart {
|
interface ServiceAutoStart {
|
||||||
name: string
|
name: string
|
||||||
displayName: string
|
displayName: string
|
||||||
@ -169,6 +180,13 @@ const services = reactive<ServiceAutoStart[]>([
|
|||||||
{ name: 'redis', displayName: 'Redis', description: '开机时自动启动 Redis 服务(独立于应用)', autoStart: false }
|
{ name: 'redis', displayName: 'Redis', description: '开机时自动启动 Redis 服务(独立于应用)', autoStart: false }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const appVersion = reactive({
|
||||||
|
version: '1.0.0',
|
||||||
|
buildTime: '',
|
||||||
|
buildDate: '',
|
||||||
|
isPackaged: false
|
||||||
|
})
|
||||||
|
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
try {
|
try {
|
||||||
basePath.value = await window.electronAPI?.config.getBasePath() || ''
|
basePath.value = await window.electronAPI?.config.getBasePath() || ''
|
||||||
@ -177,6 +195,15 @@ const loadSettings = async () => {
|
|||||||
appSettings.autoLaunch = await window.electronAPI?.app?.getAutoLaunch() || false
|
appSettings.autoLaunch = await window.electronAPI?.app?.getAutoLaunch() || false
|
||||||
appSettings.startMinimized = await window.electronAPI?.app?.getStartMinimized() || false
|
appSettings.startMinimized = await window.electronAPI?.app?.getStartMinimized() || false
|
||||||
|
|
||||||
|
// 加载版本信息
|
||||||
|
const versionInfo = await window.electronAPI?.app?.getVersion()
|
||||||
|
if (versionInfo) {
|
||||||
|
appVersion.version = versionInfo.version
|
||||||
|
appVersion.buildTime = versionInfo.buildTime
|
||||||
|
appVersion.buildDate = versionInfo.buildDate
|
||||||
|
appVersion.isPackaged = versionInfo.isPackaged
|
||||||
|
}
|
||||||
|
|
||||||
// 加载服务自启动设置
|
// 加载服务自启动设置
|
||||||
for (const service of services) {
|
for (const service of services) {
|
||||||
service.autoStart = await window.electronAPI?.service.getAutoStart(service.name) || false
|
service.autoStart = await window.electronAPI?.service.getAutoStart(service.name) || false
|
||||||
@ -315,6 +342,15 @@ onMounted(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.build-date {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-desc {
|
.app-desc {
|
||||||
|
|||||||
@ -16,6 +16,10 @@
|
|||||||
站点列表
|
站点列表
|
||||||
</span>
|
</span>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
|
<el-button @click="showLogViewer = true">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
站点日志
|
||||||
|
</el-button>
|
||||||
<el-button type="success" @click="showCreateLaravelDialog = true">
|
<el-button type="success" @click="showCreateLaravelDialog = true">
|
||||||
<el-icon><Promotion /></el-icon>
|
<el-icon><Promotion /></el-icon>
|
||||||
创建 Laravel 项目
|
创建 Laravel 项目
|
||||||
@ -328,14 +332,23 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 日志查看器 -->
|
||||||
|
<LogViewer v-model="showLogViewer" initial-tab="sites" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { FolderOpened } from '@element-plus/icons-vue'
|
import { FolderOpened, Document } from '@element-plus/icons-vue'
|
||||||
import { useServiceStore } from '@/stores/serviceStore'
|
import { useServiceStore } from '@/stores/serviceStore'
|
||||||
|
import LogViewer from '@/components/LogViewer.vue'
|
||||||
|
|
||||||
|
// 定义组件名称以便 KeepAlive 正确缓存
|
||||||
|
defineOptions({
|
||||||
|
name: 'SitesManager'
|
||||||
|
})
|
||||||
|
|
||||||
const store = useServiceStore()
|
const store = useServiceStore()
|
||||||
|
|
||||||
@ -407,6 +420,8 @@ const editForm = reactive<SiteConfig>({
|
|||||||
// 创建 Laravel 项目
|
// 创建 Laravel 项目
|
||||||
const showCreateLaravelDialog = ref(false)
|
const showCreateLaravelDialog = ref(false)
|
||||||
const creatingLaravel = ref(false)
|
const creatingLaravel = ref(false)
|
||||||
|
const showLogViewer = ref(false)
|
||||||
|
|
||||||
const laravelForm = reactive({
|
const laravelForm = reactive({
|
||||||
projectName: '',
|
projectName: '',
|
||||||
targetDir: '',
|
targetDir: '',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user