feat(electron): 添加系统级对话框功能
- 在 ElectronAPI 中新增对话框相关方法:showQuestionDialog、dialogResponse、resizeDialogWindow - 实现独立的对话框窗口 dialog.html,支持自定义标题、消息和选项- 添加对话框窗口的键盘导航和焦点管理功能 - 在主进程中实现对话框窗口的创建、显示和响应处理 - 更新 WebSocket 消息监听器组件,使用系统级对话框替代应用内弹窗- 优化进程清理逻辑,增加多种清理方法并行执行 - 重构日志工具类,改进 Electron API 调用方式 - 调整 Git 更新检查逻辑,避免直接访问 GitHub - 移除冗余的类型定义文件,统一 Electron API 接口定义
This commit is contained in:
@@ -1,18 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
ipcMain,
|
|
||||||
dialog,
|
dialog,
|
||||||
shell,
|
ipcMain,
|
||||||
Tray,
|
|
||||||
Menu,
|
Menu,
|
||||||
nativeImage,
|
nativeImage,
|
||||||
screen,
|
screen,
|
||||||
|
shell,
|
||||||
|
Tray,
|
||||||
} from 'electron'
|
} from 'electron'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import { spawn, exec } from 'child_process'
|
import { exec, spawn } from 'child_process'
|
||||||
import { getAppRoot, checkEnvironment } from './services/environmentService'
|
import { checkEnvironment, getAppRoot } from './services/environmentService'
|
||||||
|
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
|
||||||
|
import {
|
||||||
|
downloadPython,
|
||||||
|
installDependencies,
|
||||||
|
installPipPackage,
|
||||||
|
setMainWindow as setPythonMainWindow,
|
||||||
|
startBackend,
|
||||||
|
} from './services/pythonService'
|
||||||
|
import { cloneBackend, downloadGit, setMainWindow as setGitMainWindow } from './services/gitService'
|
||||||
|
import { cleanOldLogs, getLogFiles, getLogPath, log, setupLogger } from './services/logService'
|
||||||
|
|
||||||
// 强制清理相关进程的函数
|
// 强制清理相关进程的函数
|
||||||
async function forceKillRelatedProcesses(): Promise<void> {
|
async function forceKillRelatedProcesses(): Promise<void> {
|
||||||
@@ -28,9 +38,9 @@ async function forceKillRelatedProcesses(): Promise<void> {
|
|||||||
const appRoot = getAppRoot()
|
const appRoot = getAppRoot()
|
||||||
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise(resolve => {
|
||||||
// 使用更简单的命令强制结束相关进程
|
// 使用更简单的命令强制结束相关进程
|
||||||
exec(`taskkill /f /im python.exe`, (error) => {
|
exec(`taskkill /f /im python.exe`, error => {
|
||||||
if (error) {
|
if (error) {
|
||||||
log.warn('备用清理方法失败:', error.message)
|
log.warn('备用清理方法失败:', error.message)
|
||||||
} else {
|
} else {
|
||||||
@@ -42,16 +52,6 @@ async function forceKillRelatedProcesses(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
|
|
||||||
import {
|
|
||||||
setMainWindow as setPythonMainWindow,
|
|
||||||
downloadPython,
|
|
||||||
installPipPackage,
|
|
||||||
installDependencies,
|
|
||||||
startBackend,
|
|
||||||
} from './services/pythonService'
|
|
||||||
import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService'
|
|
||||||
import { setupLogger, log, getLogPath, getLogFiles, cleanOldLogs } from './services/logService'
|
|
||||||
|
|
||||||
// 检查是否以管理员权限运行
|
// 检查是否以管理员权限运行
|
||||||
function isRunningAsAdmin(): boolean {
|
function isRunningAsAdmin(): boolean {
|
||||||
@@ -113,6 +113,7 @@ interface AppConfig {
|
|||||||
IfMinimizeDirectly: boolean
|
IfMinimizeDirectly: boolean
|
||||||
IfSelfStart: boolean
|
IfSelfStart: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +305,9 @@ function updateTrayVisibility(config: AppConfig) {
|
|||||||
log.info('托盘图标已销毁')
|
log.info('托盘图标已销毁')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mainWindow: Electron.BrowserWindow | null = null
|
let mainWindow: Electron.BrowserWindow | null = null
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
log.info('开始创建主窗口')
|
log.info('开始创建主窗口')
|
||||||
|
|
||||||
@@ -746,6 +749,137 @@ ipcMain.handle('stop-backend', async () => {
|
|||||||
return stopBackend()
|
return stopBackend()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 全局存储对话框窗口引用和回调
|
||||||
|
let dialogWindows = new Map<string, BrowserWindow>()
|
||||||
|
let dialogCallbacks = new Map<string, (result: boolean) => void>()
|
||||||
|
|
||||||
|
// 创建对话框窗口
|
||||||
|
function createQuestionDialog(questionData: any): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const messageId = questionData.messageId || 'dialog_' + Date.now()
|
||||||
|
|
||||||
|
// 存储回调函数
|
||||||
|
dialogCallbacks.set(messageId, resolve)
|
||||||
|
|
||||||
|
// 准备对话框数据
|
||||||
|
const dialogData = {
|
||||||
|
title: questionData.title || '操作确认',
|
||||||
|
message: questionData.message || '是否要执行此操作?',
|
||||||
|
options: questionData.options || ['确定', '取消'],
|
||||||
|
messageId: messageId
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建对话框窗口
|
||||||
|
const dialogWindow = new BrowserWindow({
|
||||||
|
width: 450,
|
||||||
|
height: 200,
|
||||||
|
minWidth: 350,
|
||||||
|
minHeight: 150,
|
||||||
|
maxWidth: 600,
|
||||||
|
maxHeight: 400,
|
||||||
|
resizable: true,
|
||||||
|
minimizable: false,
|
||||||
|
maximizable: false,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
show: false,
|
||||||
|
frame: false,
|
||||||
|
modal: mainWindow ? true : false,
|
||||||
|
parent: mainWindow || undefined,
|
||||||
|
icon: path.join(__dirname, '../public/AUTO-MAS.ico'),
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 存储窗口引用
|
||||||
|
dialogWindows.set(messageId, dialogWindow)
|
||||||
|
|
||||||
|
// 编码对话框数据
|
||||||
|
const encodedData = encodeURIComponent(JSON.stringify(dialogData))
|
||||||
|
|
||||||
|
// 加载对话框页面
|
||||||
|
const dialogUrl = `file://${path.join(__dirname, '../public/dialog.html')}?data=${encodedData}`
|
||||||
|
dialogWindow.loadURL(dialogUrl)
|
||||||
|
|
||||||
|
// 窗口准备好后显示并居中
|
||||||
|
dialogWindow.once('ready-to-show', () => {
|
||||||
|
// 计算居中位置
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
const mainBounds = mainWindow.getBounds()
|
||||||
|
const dialogBounds = dialogWindow.getBounds()
|
||||||
|
const x = Math.round(mainBounds.x + (mainBounds.width - dialogBounds.width) / 2)
|
||||||
|
const y = Math.round(mainBounds.y + (mainBounds.height - dialogBounds.height) / 2)
|
||||||
|
dialogWindow.setPosition(x, y)
|
||||||
|
} else {
|
||||||
|
dialogWindow.center()
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogWindow.show()
|
||||||
|
dialogWindow.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 窗口关闭时清理
|
||||||
|
dialogWindow.on('closed', () => {
|
||||||
|
dialogWindows.delete(messageId)
|
||||||
|
const callback = dialogCallbacks.get(messageId)
|
||||||
|
if (callback) {
|
||||||
|
dialogCallbacks.delete(messageId)
|
||||||
|
callback(false) // 默认返回 false (取消)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
log.info(`对话框窗口已创建: ${messageId}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示问题对话框
|
||||||
|
ipcMain.handle('show-question-dialog', async (_event, questionData) => {
|
||||||
|
log.info('收到显示对话框请求:', questionData)
|
||||||
|
try {
|
||||||
|
const result = await createQuestionDialog(questionData)
|
||||||
|
log.info(`对话框结果: ${result}`)
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
log.error('创建对话框失败:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理对话框响应
|
||||||
|
ipcMain.handle('dialog-response', async (_event, messageId: string, choice: boolean) => {
|
||||||
|
log.info(`收到对话框响应: ${messageId} = ${choice}`)
|
||||||
|
|
||||||
|
const callback = dialogCallbacks.get(messageId)
|
||||||
|
if (callback) {
|
||||||
|
dialogCallbacks.delete(messageId)
|
||||||
|
callback(choice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭对话框窗口
|
||||||
|
const dialogWindow = dialogWindows.get(messageId)
|
||||||
|
if (dialogWindow && !dialogWindow.isDestroyed()) {
|
||||||
|
dialogWindow.close()
|
||||||
|
}
|
||||||
|
dialogWindows.delete(messageId)
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 调整对话框窗口大小
|
||||||
|
ipcMain.handle('resize-dialog-window', async (_event, height: number) => {
|
||||||
|
// 获取当前活动的对话框窗口(最后创建的)
|
||||||
|
const dialogWindow = Array.from(dialogWindows.values()).pop()
|
||||||
|
if (dialogWindow && !dialogWindow.isDestroyed()) {
|
||||||
|
const bounds = dialogWindow.getBounds()
|
||||||
|
dialogWindow.setBounds({
|
||||||
|
...bounds,
|
||||||
|
height: Math.max(150, Math.min(400, height))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Git相关
|
// Git相关
|
||||||
ipcMain.handle('download-git', async () => {
|
ipcMain.handle('download-git', async () => {
|
||||||
const appRoot = getAppRoot()
|
const appRoot = getAppRoot()
|
||||||
@@ -818,7 +952,6 @@ ipcMain.handle('check-git-update', async () => {
|
|||||||
// 如果没有更新,pull操作会很快完成且不会有实际变化
|
// 如果没有更新,pull操作会很快完成且不会有实际变化
|
||||||
log.info('跳过远程检查,返回hasUpdate=true以触发镜像站更新流程')
|
log.info('跳过远程检查,返回hasUpdate=true以触发镜像站更新流程')
|
||||||
return { hasUpdate: true, skipReason: 'avoided_github_access' }
|
return { hasUpdate: true, skipReason: 'avoided_github_access' }
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('检查Git更新失败:', error)
|
log.error('检查Git更新失败:', error)
|
||||||
// 如果检查失败,返回true以触发更新流程,确保代码是最新的
|
// 如果检查失败,返回true以触发更新流程,确保代码是最新的
|
||||||
@@ -1092,13 +1225,13 @@ app.on('before-quit', async event => {
|
|||||||
forceKillRelatedProcesses(),
|
forceKillRelatedProcesses(),
|
||||||
|
|
||||||
// 方法2: 直接使用 taskkill 命令
|
// 方法2: 直接使用 taskkill 命令
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>(resolve => {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const appRoot = getAppRoot()
|
const appRoot = getAppRoot()
|
||||||
const commands = [
|
const commands = [
|
||||||
`taskkill /f /im python.exe`,
|
`taskkill /f /im python.exe`,
|
||||||
`wmic process where "CommandLine like '%main.py%'" delete`,
|
`wmic process where "CommandLine like '%main.py%'" delete`,
|
||||||
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`
|
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`,
|
||||||
]
|
]
|
||||||
|
|
||||||
let completed = 0
|
let completed = 0
|
||||||
@@ -1116,7 +1249,7 @@ app.on('before-quit', async event => {
|
|||||||
} else {
|
} else {
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
// 最多等待3秒
|
// 最多等待3秒
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
|
openFile: (filePath: string) => ipcRenderer.invoke('open-file', filePath),
|
||||||
showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', filePath),
|
showItemInFolder: (filePath: string) => ipcRenderer.invoke('show-item-in-folder', filePath),
|
||||||
|
|
||||||
|
// 对话框相关
|
||||||
|
showQuestionDialog: (questionData: any) => ipcRenderer.invoke('show-question-dialog', questionData),
|
||||||
|
dialogResponse: (messageId: string, choice: boolean) => ipcRenderer.invoke('dialog-response', messageId, choice),
|
||||||
|
resizeDialogWindow: (height: number) => ipcRenderer.invoke('resize-dialog-window', height),
|
||||||
|
|
||||||
// 监听下载进度
|
// 监听下载进度
|
||||||
onDownloadProgress: (callback: (progress: any) => void) => {
|
onDownloadProgress: (callback: (progress: any) => void) => {
|
||||||
ipcRenderer.on('download-progress', (_, progress) => callback(progress))
|
ipcRenderer.on('download-progress', (_, progress) => callback(progress))
|
||||||
|
|||||||
276
frontend/public/dialog.html
Normal file
276
frontend/public/dialog.html
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>操作确认</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
border-bottom: 1px solid #e1e5e9;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #212529;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
padding: 24px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-message {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #495057;
|
||||||
|
margin: 0;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-actions {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
border-top: 1px solid #e1e5e9;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
color: #495057;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 80px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
border-color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button:focus {
|
||||||
|
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25);
|
||||||
|
border-color: #86b7fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button.primary {
|
||||||
|
background: #0d6efd;
|
||||||
|
color: white;
|
||||||
|
border-color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button.primary:hover {
|
||||||
|
background: #0b5ed7;
|
||||||
|
border-color: #0a58ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button.danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button.danger:hover {
|
||||||
|
background: #c82333;
|
||||||
|
border-color: #bd2130;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 键盘导航样式 */
|
||||||
|
.dialog-button:focus-visible {
|
||||||
|
outline: 2px solid #0d6efd;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题支持 */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: #212529;
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-container {
|
||||||
|
background: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header,
|
||||||
|
.dialog-actions {
|
||||||
|
background: #2c3236;
|
||||||
|
border-color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-title {
|
||||||
|
color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-message {
|
||||||
|
color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button {
|
||||||
|
background: #495057;
|
||||||
|
color: #f8f9fa;
|
||||||
|
border-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-button:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
border-color: #7c848a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 动画效果 */
|
||||||
|
.dialog-container {
|
||||||
|
animation: dialogSlideIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dialogSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="dialog-container">
|
||||||
|
<div class="dialog-header">
|
||||||
|
<h3 class="dialog-title" id="dialog-title">操作确认</h3>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<p class="dialog-message" id="dialog-message">是否要执行此操作?</p>
|
||||||
|
</div>
|
||||||
|
<div class="dialog-actions" id="dialog-actions">
|
||||||
|
<!-- 按钮将通过 JavaScript 动态生成 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 获取传递的参数
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const data = JSON.parse(decodeURIComponent(urlParams.get('data') || '{}'));
|
||||||
|
|
||||||
|
// 设置对话框内容
|
||||||
|
document.getElementById('dialog-title').textContent = data.title || '操作确认';
|
||||||
|
document.getElementById('dialog-message').textContent = data.message || '是否要执行此操作?';
|
||||||
|
|
||||||
|
// 创建按钮
|
||||||
|
const actionsContainer = document.getElementById('dialog-actions');
|
||||||
|
const options = data.options || ['确定', '取消'];
|
||||||
|
|
||||||
|
options.forEach((option, index) => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'dialog-button';
|
||||||
|
button.textContent = option;
|
||||||
|
|
||||||
|
// 第一个按钮设为主要按钮
|
||||||
|
if (index === 0) {
|
||||||
|
button.className += ' primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定点击事件
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
// 发送结果到主进程
|
||||||
|
if (window.electronAPI && window.electronAPI.dialogResponse) {
|
||||||
|
const choice = index === 0; // 第一个选项为 true
|
||||||
|
window.electronAPI.dialogResponse(data.messageId, choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不需要手动关闭窗口,主进程会处理
|
||||||
|
// 移除 window.electronAPI.windowClose() 调用
|
||||||
|
});
|
||||||
|
|
||||||
|
actionsContainer.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 自动聚焦第一个按钮
|
||||||
|
setTimeout(() => {
|
||||||
|
const firstButton = actionsContainer.querySelector('.dialog-button');
|
||||||
|
if (firstButton) {
|
||||||
|
firstButton.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 键盘事件处理
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
// ESC 键相当于取消
|
||||||
|
if (window.electronAPI && window.electronAPI.dialogResponse) {
|
||||||
|
window.electronAPI.dialogResponse(data.messageId, false);
|
||||||
|
}
|
||||||
|
// 不需要手动关闭窗口,主进程会处理
|
||||||
|
} else if (event.key === 'Enter') {
|
||||||
|
// Enter 键相当于确定
|
||||||
|
const focusedButton = document.activeElement;
|
||||||
|
if (focusedButton && focusedButton.classList.contains('dialog-button')) {
|
||||||
|
focusedButton.click();
|
||||||
|
} else {
|
||||||
|
// 如果没有聚焦按钮,默认点击第一个
|
||||||
|
const firstButton = actionsContainer.querySelector('.dialog-button');
|
||||||
|
if (firstButton) {
|
||||||
|
firstButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 窗口加载完成后调整大小
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
if (window.electronAPI && window.electronAPI.resizeDialogWindow) {
|
||||||
|
// 获取内容高度
|
||||||
|
const container = document.querySelector('.dialog-container');
|
||||||
|
const height = container.offsetHeight + 40; // 加上一些边距
|
||||||
|
window.electronAPI.resizeDialogWindow(Math.min(height, 400));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,51 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="display: none">
|
<div style="display: none">
|
||||||
<!-- 这是一个隐藏的监听组件,不需要UI -->
|
<!-- 这是一个隐藏的监听组件,不需要UI -->
|
||||||
</div>
|
<!-- 现在使用系统级对话框窗口而不是应用内弹窗 -->
|
||||||
|
|
||||||
<!-- 简单的自定义对话框 -->
|
|
||||||
<div v-if="showDialog" class="dialog-overlay" @click.self="showDialog = false">
|
|
||||||
<div class="dialog-container">
|
|
||||||
<div class="dialog-header">
|
|
||||||
<h3>{{ dialogData.title }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-content">
|
|
||||||
<p>{{ dialogData.message }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="dialog-actions">
|
|
||||||
<button
|
|
||||||
v-for="(option, index) in dialogData.options"
|
|
||||||
:key="index"
|
|
||||||
class="dialog-button"
|
|
||||||
@click="handleChoice(index)"
|
|
||||||
>
|
|
||||||
{{ option }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
|
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
|
||||||
import { logger } from '@/utils/logger'
|
import { logger } from '@/utils/logger'
|
||||||
|
|
||||||
// WebSocket hook
|
// WebSocket hook
|
||||||
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
|
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
|
||||||
|
|
||||||
// 对话框状态
|
|
||||||
const showDialog = ref(false)
|
|
||||||
const dialogData = ref({
|
|
||||||
title: '',
|
|
||||||
message: '',
|
|
||||||
options: ['确定', '取消'],
|
|
||||||
messageId: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// 存储订阅ID用于取消订阅
|
// 存储订阅ID用于取消订阅
|
||||||
let subscriptionId: string
|
let subscriptionId: string
|
||||||
|
|
||||||
|
// 检查是否在 Electron 环境中
|
||||||
|
const isElectron = () => {
|
||||||
|
return typeof window !== 'undefined' && (window as any).electronAPI
|
||||||
|
}
|
||||||
|
|
||||||
// 发送用户选择结果到后端
|
// 发送用户选择结果到后端
|
||||||
const sendResponse = (messageId: string, choice: boolean) => {
|
const sendResponse = (messageId: string, choice: boolean) => {
|
||||||
const response = {
|
const response = {
|
||||||
@@ -59,39 +34,40 @@ const sendResponse = (messageId: string, choice: boolean) => {
|
|||||||
sendRaw('Response', response)
|
sendRaw('Response', response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理用户选择
|
// 显示系统级问题对话框
|
||||||
const handleChoice = (choiceIndex: number) => {
|
const showQuestion = async (questionData: any) => {
|
||||||
const choice = choiceIndex === 0 // 第一个选项为true,其他为false
|
|
||||||
sendResponse(dialogData.value.messageId, choice)
|
|
||||||
showDialog.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示问题对话框
|
|
||||||
const showQuestion = (questionData: any) => {
|
|
||||||
const title = questionData.title || '操作提示'
|
const title = questionData.title || '操作提示'
|
||||||
const message = questionData.message || ''
|
const message = questionData.message || ''
|
||||||
const options = questionData.options || ['确定', '取消']
|
const options = questionData.options || ['确定', '取消']
|
||||||
const messageId = questionData.message_id || 'fallback_' + Date.now()
|
const messageId = questionData.message_id || 'fallback_' + Date.now()
|
||||||
|
|
||||||
logger.info('[WebSocket消息监听器] 显示自定义对话框:', questionData)
|
logger.info('[WebSocket消息监听器] 显示系统级对话框:', questionData)
|
||||||
|
|
||||||
// 设置对话框数据
|
if (!isElectron()) {
|
||||||
dialogData.value = {
|
logger.error('[WebSocket消息监听器] 不在 Electron 环境中,无法显示系统级对话框')
|
||||||
|
// 在非 Electron 环境中,使用默认响应
|
||||||
|
sendResponse(messageId, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用 Electron API 显示系统级对话框
|
||||||
|
const result = await (window as any).electronAPI.showQuestionDialog({
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
options,
|
options,
|
||||||
messageId
|
messageId
|
||||||
}
|
|
||||||
|
|
||||||
showDialog.value = true
|
|
||||||
|
|
||||||
// 在下一个tick自动聚焦第一个按钮
|
|
||||||
nextTick(() => {
|
|
||||||
const firstButton = document.querySelector('.dialog-button:first-child') as HTMLButtonElement
|
|
||||||
if (firstButton) {
|
|
||||||
firstButton.focus()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logger.info('[WebSocket消息监听器] 系统级对话框返回结果:', result)
|
||||||
|
|
||||||
|
// 发送结果到后端
|
||||||
|
sendResponse(messageId, result)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[WebSocket消息监听器] 显示系统级对话框失败:', error)
|
||||||
|
// 出错时发送默认响应
|
||||||
|
sendResponse(messageId, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息处理函数
|
// 消息处理函数
|
||||||
@@ -137,14 +113,14 @@ const handleObjectMessage = (data: any) => {
|
|||||||
logger.info('[WebSocket消息监听器] 发现Question类型消息')
|
logger.info('[WebSocket消息监听器] 发现Question类型消息')
|
||||||
|
|
||||||
if (data.message_id) {
|
if (data.message_id) {
|
||||||
logger.info('[WebSocket消息监听器] message_id存在,显示选择弹窗')
|
logger.info('[WebSocket消息监听器] message_id存在,显示系统级对话框')
|
||||||
showQuestion(data)
|
showQuestion(data)
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
|
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
|
||||||
// 即使缺少message_id,也尝试显示弹窗,使用当前时间戳作为ID
|
// 即使缺少message_id,也尝试显示对话框,使用当前时间戳作为ID
|
||||||
const fallbackId = 'fallback_' + Date.now()
|
const fallbackId = 'fallback_' + Date.now()
|
||||||
logger.info('[WebSocket消息监听器] 使用备用ID显示弹窗:', fallbackId)
|
logger.info('[WebSocket消息监听器] 使用备用ID显示对话框:', fallbackId)
|
||||||
showQuestion({
|
showQuestion({
|
||||||
...data,
|
...data,
|
||||||
message_id: fallbackId
|
message_id: fallbackId
|
||||||
@@ -210,150 +186,4 @@ onUnmounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 对话框遮罩层 */
|
|
||||||
.dialog-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对话框容器 */
|
|
||||||
.dialog-container {
|
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
||||||
min-width: 300px;
|
|
||||||
max-width: 500px;
|
|
||||||
width: 90%;
|
|
||||||
animation: dialogAppear 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对话框头部 */
|
|
||||||
.dialog-header {
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 对话框内容 */
|
|
||||||
.dialog-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-content p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #666;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮区域 */
|
|
||||||
.dialog-actions {
|
|
||||||
padding: 12px 20px 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 按钮样式 */
|
|
||||||
.dialog-button {
|
|
||||||
padding: 8px 16px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: white;
|
|
||||||
color: #333;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-button:hover {
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-button:focus {
|
|
||||||
outline: 2px solid #007bff;
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-button:first-child {
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
border-color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-button:first-child:hover {
|
|
||||||
background: #0056b3;
|
|
||||||
border-color: #0056b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 出现动画 */
|
|
||||||
@keyframes dialogAppear {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 暗色主题适配 */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.dialog-container {
|
|
||||||
background: #2d2d2d;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header {
|
|
||||||
border-bottom-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-header h3 {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-content p {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-button {
|
|
||||||
background: #444;
|
|
||||||
color: #fff;
|
|
||||||
border-color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-button:hover {
|
|
||||||
background: #555;
|
|
||||||
border-color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-button:first-child {
|
|
||||||
background: #0d6efd;
|
|
||||||
border-color: #0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-button:first-child:hover {
|
|
||||||
background: #0b5ed7;
|
|
||||||
border-color: #0b5ed7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
16
frontend/src/types/electron.d.ts
vendored
16
frontend/src/types/electron.d.ts
vendored
@@ -1,4 +1,5 @@
|
|||||||
export interface ElectronAPI {
|
declare global {
|
||||||
|
interface ElectronAPI {
|
||||||
openDevTools: () => Promise<void>
|
openDevTools: () => Promise<void>
|
||||||
selectFolder: () => Promise<string | null>
|
selectFolder: () => Promise<string | null>
|
||||||
selectFile: (filters?: any[]) => Promise<string[]>
|
selectFile: (filters?: any[]) => Promise<string[]>
|
||||||
@@ -53,12 +54,21 @@ export interface ElectronAPI {
|
|||||||
openFile: (filePath: string) => Promise<void>
|
openFile: (filePath: string) => Promise<void>
|
||||||
showItemInFolder: (filePath: string) => Promise<void>
|
showItemInFolder: (filePath: string) => Promise<void>
|
||||||
|
|
||||||
|
// 对话框相关
|
||||||
|
showQuestionDialog: (questionData: {
|
||||||
|
title?: string
|
||||||
|
message?: string
|
||||||
|
options?: string[]
|
||||||
|
messageId?: string
|
||||||
|
}) => Promise<boolean>
|
||||||
|
dialogResponse: (messageId: string, choice: boolean) => Promise<boolean>
|
||||||
|
resizeDialogWindow: (height: number) => Promise<void>
|
||||||
|
|
||||||
// 监听下载进度
|
// 监听下载进度
|
||||||
onDownloadProgress: (callback: (progress: any) => void) => void
|
onDownloadProgress: (callback: (progress: any) => void) => void
|
||||||
removeDownloadProgressListener: () => void
|
removeDownloadProgressListener: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI: ElectronAPI
|
electronAPI: ElectronAPI
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
// Electron API 类型定义
|
|
||||||
export interface ElectronAPI {
|
|
||||||
// 开发工具
|
|
||||||
openDevTools: () => Promise<void>
|
|
||||||
selectFolder: () => Promise<string | null>
|
|
||||||
selectFile: (filters?: Array<{ name: string; extensions: string[] }>) => Promise<string | null>
|
|
||||||
|
|
||||||
// 窗口控制
|
|
||||||
windowMinimize: () => Promise<void>
|
|
||||||
windowMaximize: () => Promise<void>
|
|
||||||
windowClose: () => Promise<void>
|
|
||||||
windowIsMaximized: () => Promise<boolean>
|
|
||||||
|
|
||||||
// 管理员权限检查
|
|
||||||
checkAdmin: () => Promise<boolean>
|
|
||||||
|
|
||||||
// 重启为管理员
|
|
||||||
restartAsAdmin: () => Promise<void>
|
|
||||||
appQuit: () => Promise<void>
|
|
||||||
|
|
||||||
// 进程管理
|
|
||||||
getRelatedProcesses: () => Promise<any[]>
|
|
||||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
|
||||||
forceExit: () => Promise<{ success: boolean }>
|
|
||||||
// 环境检查
|
|
||||||
checkEnvironment: () => Promise<{
|
|
||||||
pythonExists: boolean
|
|
||||||
gitExists: boolean
|
|
||||||
backendExists: boolean
|
|
||||||
dependenciesInstalled: boolean
|
|
||||||
isInitialized: boolean
|
|
||||||
}>
|
|
||||||
|
|
||||||
// 关键文件检查
|
|
||||||
checkCriticalFiles: () => Promise<{
|
|
||||||
pythonExists: boolean
|
|
||||||
pipExists: boolean
|
|
||||||
gitExists: boolean
|
|
||||||
mainPyExists: boolean
|
|
||||||
}>
|
|
||||||
|
|
||||||
// Python相关
|
|
||||||
downloadPython: (mirror: string) => Promise<{ success: boolean; error?: string }>
|
|
||||||
deletePython: () => Promise<{ success: boolean; error?: string }>
|
|
||||||
|
|
||||||
// pip相关
|
|
||||||
installPip: () => Promise<{ success: boolean; error?: string }>
|
|
||||||
deletePip: () => Promise<{ success: boolean; error?: string }>
|
|
||||||
|
|
||||||
// Git相关
|
|
||||||
downloadGit: () => Promise<{ success: boolean; error?: string }>
|
|
||||||
deleteGit: () => Promise<{ success: boolean; error?: string }>
|
|
||||||
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
|
|
||||||
|
|
||||||
// 后端代码相关
|
|
||||||
cloneBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
|
|
||||||
updateBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
|
|
||||||
|
|
||||||
// 依赖安装
|
|
||||||
installDependencies: (mirror: string) => Promise<{ success: boolean; error?: string }>
|
|
||||||
|
|
||||||
// 后端服务
|
|
||||||
startBackend: () => Promise<{ success: boolean; error?: string }>
|
|
||||||
|
|
||||||
// 下载进度监听
|
|
||||||
onDownloadProgress: (
|
|
||||||
callback: (progress: { progress: number; status: string; message: string }) => void
|
|
||||||
) => void
|
|
||||||
removeDownloadProgressListener: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
electronAPI: ElectronAPI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {}
|
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
// 渲染进程日志工具
|
// 渲染进程日志工具
|
||||||
interface ElectronAPI {
|
const LogLevel = {
|
||||||
getLogPath: () => Promise<string>
|
DEBUG: 'DEBUG',
|
||||||
getLogFiles: () => Promise<string[]>
|
INFO: 'INFO',
|
||||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
WARN: 'WARN',
|
||||||
clearLogs: (fileName?: string) => Promise<void>
|
ERROR: 'ERROR'
|
||||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
} as const
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
export type LogLevel = typeof LogLevel[keyof typeof LogLevel]
|
||||||
interface Window {
|
export { LogLevel }
|
||||||
electronAPI: ElectronAPI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LogLevel {
|
|
||||||
DEBUG = 'DEBUG',
|
|
||||||
INFO = 'INFO',
|
|
||||||
WARN = 'WARN',
|
|
||||||
ERROR = 'ERROR'
|
|
||||||
}
|
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
// 直接使用原生console,主进程会自动处理日志记录
|
// 直接使用原生console,主进程会自动处理日志记录
|
||||||
@@ -40,32 +29,32 @@ class Logger {
|
|||||||
|
|
||||||
// 获取日志文件路径
|
// 获取日志文件路径
|
||||||
async getLogPath(): Promise<string> {
|
async getLogPath(): Promise<string> {
|
||||||
if (window.electronAPI) {
|
if ((window as any).electronAPI) {
|
||||||
return await window.electronAPI.getLogPath()
|
return await (window as any).electronAPI.getLogPath()
|
||||||
}
|
}
|
||||||
throw new Error('Electron API not available')
|
throw new Error('Electron API not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取日志文件列表
|
// 获取日志文件列表
|
||||||
async getLogFiles(): Promise<string[]> {
|
async getLogFiles(): Promise<string[]> {
|
||||||
if (window.electronAPI) {
|
if ((window as any).electronAPI) {
|
||||||
return await window.electronAPI.getLogFiles()
|
return await (window as any).electronAPI.getLogFiles()
|
||||||
}
|
}
|
||||||
throw new Error('Electron API not available')
|
throw new Error('Electron API not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取日志内容
|
// 获取日志内容
|
||||||
async getLogs(lines?: number, fileName?: string): Promise<string> {
|
async getLogs(lines?: number, fileName?: string): Promise<string> {
|
||||||
if (window.electronAPI) {
|
if ((window as any).electronAPI) {
|
||||||
return await window.electronAPI.getLogs(lines, fileName)
|
return await (window as any).electronAPI.getLogs(lines, fileName)
|
||||||
}
|
}
|
||||||
throw new Error('Electron API not available')
|
throw new Error('Electron API not available')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空日志
|
// 清空日志
|
||||||
async clearLogs(fileName?: string): Promise<void> {
|
async clearLogs(fileName?: string): Promise<void> {
|
||||||
if (window.electronAPI) {
|
if ((window as any).electronAPI) {
|
||||||
await window.electronAPI.clearLogs(fileName)
|
await (window as any).electronAPI.clearLogs(fileName)
|
||||||
console.info(`日志已清空: ${fileName || '当前文件'}`)
|
console.info(`日志已清空: ${fileName || '当前文件'}`)
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Electron API not available')
|
throw new Error('Electron API not available')
|
||||||
@@ -74,8 +63,8 @@ class Logger {
|
|||||||
|
|
||||||
// 清理旧日志
|
// 清理旧日志
|
||||||
async cleanOldLogs(daysToKeep: number = 7): Promise<void> {
|
async cleanOldLogs(daysToKeep: number = 7): Promise<void> {
|
||||||
if (window.electronAPI) {
|
if ((window as any).electronAPI) {
|
||||||
await window.electronAPI.cleanOldLogs(daysToKeep)
|
await (window as any).electronAPI.cleanOldLogs(daysToKeep)
|
||||||
console.info(`已清理${daysToKeep}天前的旧日志`)
|
console.info(`已清理${daysToKeep}天前的旧日志`)
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Electron API not available')
|
throw new Error('Electron API not available')
|
||||||
|
|||||||
225
websocket_test.html
Normal file
225
websocket_test.html
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebSocket消息测试</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.test-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.status.connected {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.status.disconnected {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.log {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>WebSocket消息测试工具</h1>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>连接状态</h3>
|
||||||
|
<button onclick="connectWebSocket()">连接WebSocket</button>
|
||||||
|
<button onclick="disconnectWebSocket()">断开连接</button>
|
||||||
|
<div id="status" class="status disconnected">未连接</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试消息</h3>
|
||||||
|
<button onclick="sendQuestionMessage()">发送Question消息</button>
|
||||||
|
<button onclick="sendObjectMessage()">发送普通对象消息</button>
|
||||||
|
<button onclick="sendStringMessage()">发送字符串消息</button>
|
||||||
|
<button onclick="sendMalformedMessage()">发送格式错误消息</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>消息日志</h3>
|
||||||
|
<button onclick="clearLog()">清空日志</button>
|
||||||
|
<div id="log" class="log"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let ws = null;
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
const logDiv = document.getElementById('log');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
logDiv.innerHTML += `[${timestamp}] ${message}\n`;
|
||||||
|
logDiv.scrollTop = logDiv.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(connected) {
|
||||||
|
const statusDiv = document.getElementById('status');
|
||||||
|
if (connected) {
|
||||||
|
statusDiv.textContent = 'WebSocket已连接';
|
||||||
|
statusDiv.className = 'status connected';
|
||||||
|
} else {
|
||||||
|
statusDiv.textContent = 'WebSocket未连接';
|
||||||
|
statusDiv.className = 'status disconnected';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
log('WebSocket已经连接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws = new WebSocket('ws://localhost:36163/api/core/ws');
|
||||||
|
|
||||||
|
ws.onopen = function() {
|
||||||
|
log('WebSocket连接已建立');
|
||||||
|
updateStatus(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
log('收到消息: ' + event.data);
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
log('解析后的消息: ' + JSON.stringify(data, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
log('消息解析失败: ' + e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function() {
|
||||||
|
log('WebSocket连接已关闭');
|
||||||
|
updateStatus(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = function(error) {
|
||||||
|
log('WebSocket错误: ' + error);
|
||||||
|
updateStatus(false);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectWebSocket() {
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
ws = null;
|
||||||
|
log('主动断开WebSocket连接');
|
||||||
|
updateStatus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMessage(message) {
|
||||||
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||||
|
log('错误: WebSocket未连接');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageStr = JSON.stringify(message);
|
||||||
|
ws.send(messageStr);
|
||||||
|
log('发送消息: ' + messageStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendQuestionMessage() {
|
||||||
|
const message = {
|
||||||
|
id: "test_id_" + Date.now(),
|
||||||
|
type: "message",
|
||||||
|
data: {
|
||||||
|
type: "Question",
|
||||||
|
message_id: "q_" + Date.now(),
|
||||||
|
title: "测试问题",
|
||||||
|
message: "这是一个测试问题,请选择是否继续?"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendObjectMessage() {
|
||||||
|
const message = {
|
||||||
|
id: "test_obj_" + Date.now(),
|
||||||
|
type: "message",
|
||||||
|
data: {
|
||||||
|
action: "test_action",
|
||||||
|
status: "running",
|
||||||
|
content: "这是一个普通的对象消息"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendStringMessage() {
|
||||||
|
const message = {
|
||||||
|
id: "test_str_" + Date.now(),
|
||||||
|
type: "message",
|
||||||
|
data: "这是一个字符串消息"
|
||||||
|
};
|
||||||
|
sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendMalformedMessage() {
|
||||||
|
const message = {
|
||||||
|
id: "test_malformed_" + Date.now(),
|
||||||
|
type: "message",
|
||||||
|
data: {
|
||||||
|
type: "Question",
|
||||||
|
// 缺少 message_id
|
||||||
|
title: "格式错误的问题",
|
||||||
|
message: "这个消息缺少message_id字段"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sendMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLog() {
|
||||||
|
document.getElementById('log').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时自动连接
|
||||||
|
window.onload = function() {
|
||||||
|
log('页面已加载,准备测试WebSocket连接');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user