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 {
|
||||
app,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
dialog,
|
||||
shell,
|
||||
Tray,
|
||||
ipcMain,
|
||||
Menu,
|
||||
nativeImage,
|
||||
screen,
|
||||
shell,
|
||||
Tray,
|
||||
} from 'electron'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
import { spawn, exec } from 'child_process'
|
||||
import { getAppRoot, checkEnvironment } from './services/environmentService'
|
||||
import { exec, spawn } from 'child_process'
|
||||
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> {
|
||||
@@ -22,15 +32,15 @@ async function forceKillRelatedProcesses(): Promise<void> {
|
||||
log.info('所有相关进程已清理')
|
||||
} catch (error) {
|
||||
log.error('清理进程时出错:', error)
|
||||
|
||||
|
||||
// 备用清理方法
|
||||
if (process.platform === 'win32') {
|
||||
const appRoot = getAppRoot()
|
||||
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) {
|
||||
log.warn('备用清理方法失败:', error.message)
|
||||
} 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 {
|
||||
@@ -113,6 +113,7 @@ interface AppConfig {
|
||||
IfMinimizeDirectly: boolean
|
||||
IfSelfStart: boolean
|
||||
}
|
||||
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -304,7 +305,9 @@ function updateTrayVisibility(config: AppConfig) {
|
||||
log.info('托盘图标已销毁')
|
||||
}
|
||||
}
|
||||
|
||||
let mainWindow: Electron.BrowserWindow | null = null
|
||||
|
||||
function createWindow() {
|
||||
log.info('开始创建主窗口')
|
||||
|
||||
@@ -442,7 +445,7 @@ function createWindow() {
|
||||
screen.removeListener('display-metrics-changed', recomputeMinSize)
|
||||
// 置空模块级引用
|
||||
mainWindow = null
|
||||
|
||||
|
||||
// 如果是正在退出,立即执行进程清理
|
||||
if (isQuitting) {
|
||||
log.info('窗口关闭,执行最终清理')
|
||||
@@ -598,19 +601,19 @@ ipcMain.handle('kill-all-processes', async () => {
|
||||
ipcMain.handle('force-exit', async () => {
|
||||
log.info('收到强制退出命令')
|
||||
isQuitting = true
|
||||
|
||||
|
||||
// 立即清理进程
|
||||
try {
|
||||
await forceKillRelatedProcesses()
|
||||
} catch (e) {
|
||||
log.error('强制清理失败:', e)
|
||||
}
|
||||
|
||||
|
||||
// 强制退出
|
||||
setTimeout(() => {
|
||||
process.exit(0)
|
||||
}, 500)
|
||||
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
@@ -746,6 +749,137 @@ ipcMain.handle('stop-backend', async () => {
|
||||
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相关
|
||||
ipcMain.handle('download-git', async () => {
|
||||
const appRoot = getAppRoot()
|
||||
@@ -785,7 +919,7 @@ ipcMain.handle('check-git-update', async () => {
|
||||
|
||||
// 不执行fetch,直接检查本地状态
|
||||
// 这样避免了直接访问GitHub,而是在后续的pull操作中使用镜像站
|
||||
|
||||
|
||||
// 获取当前HEAD的commit hash
|
||||
const currentCommit = await new Promise<string>((resolve, reject) => {
|
||||
const revParseProc = spawn(gitPath, ['rev-parse', 'HEAD'], {
|
||||
@@ -811,14 +945,13 @@ ipcMain.handle('check-git-update', async () => {
|
||||
})
|
||||
|
||||
log.info(`当前本地commit: ${currentCommit}`)
|
||||
|
||||
|
||||
// 由于我们跳过了fetch步骤(避免直接访问GitHub),
|
||||
// 我们无法准确知道远程是否有更新
|
||||
// 因此返回true,让后续的pull操作通过镜像站来检查和获取更新
|
||||
// 如果没有更新,pull操作会很快完成且不会有实际变化
|
||||
log.info('跳过远程检查,返回hasUpdate=true以触发镜像站更新流程')
|
||||
return { hasUpdate: true, skipReason: 'avoided_github_access' }
|
||||
|
||||
} catch (error) {
|
||||
log.error('检查Git更新失败:', error)
|
||||
// 如果检查失败,返回true以触发更新流程,确保代码是最新的
|
||||
@@ -1084,23 +1217,23 @@ app.on('before-quit', async event => {
|
||||
|
||||
// 立即开始强制清理,不等待优雅关闭
|
||||
log.info('开始强制清理所有相关进程')
|
||||
|
||||
|
||||
try {
|
||||
// 并行执行多种清理方法
|
||||
const cleanupPromises = [
|
||||
// 方法1: 使用我们的进程管理器
|
||||
forceKillRelatedProcesses(),
|
||||
|
||||
|
||||
// 方法2: 直接使用 taskkill 命令
|
||||
new Promise<void>((resolve) => {
|
||||
new Promise<void>(resolve => {
|
||||
if (process.platform === 'win32') {
|
||||
const appRoot = getAppRoot()
|
||||
const commands = [
|
||||
`taskkill /f /im python.exe`,
|
||||
`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
|
||||
commands.forEach(cmd => {
|
||||
exec(cmd, () => {
|
||||
@@ -1110,26 +1243,26 @@ app.on('before-quit', async event => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
// 2秒超时
|
||||
setTimeout(resolve, 2000)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}),
|
||||
]
|
||||
|
||||
|
||||
// 最多等待3秒
|
||||
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 3000))
|
||||
await Promise.race([Promise.all(cleanupPromises), timeoutPromise])
|
||||
|
||||
|
||||
log.info('进程清理完成')
|
||||
} catch (e) {
|
||||
log.error('进程清理时出错:', e)
|
||||
}
|
||||
|
||||
log.info('应用强制退出')
|
||||
|
||||
|
||||
// 使用 process.exit 而不是 app.exit,更加强制
|
||||
setTimeout(() => {
|
||||
process.exit(0)
|
||||
|
||||
@@ -63,6 +63,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
openFile: (filePath: string) => ipcRenderer.invoke('open-file', 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) => {
|
||||
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>
|
||||
<div style="display: none">
|
||||
<!-- 这是一个隐藏的监听组件,不需要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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
// WebSocket hook
|
||||
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
|
||||
|
||||
// 对话框状态
|
||||
const showDialog = ref(false)
|
||||
const dialogData = ref({
|
||||
title: '',
|
||||
message: '',
|
||||
options: ['确定', '取消'],
|
||||
messageId: ''
|
||||
})
|
||||
|
||||
// 存储订阅ID用于取消订阅
|
||||
let subscriptionId: string
|
||||
|
||||
// 检查是否在 Electron 环境中
|
||||
const isElectron = () => {
|
||||
return typeof window !== 'undefined' && (window as any).electronAPI
|
||||
}
|
||||
|
||||
// 发送用户选择结果到后端
|
||||
const sendResponse = (messageId: string, choice: boolean) => {
|
||||
const response = {
|
||||
@@ -59,39 +34,40 @@ const sendResponse = (messageId: string, choice: boolean) => {
|
||||
sendRaw('Response', response)
|
||||
}
|
||||
|
||||
// 处理用户选择
|
||||
const handleChoice = (choiceIndex: number) => {
|
||||
const choice = choiceIndex === 0 // 第一个选项为true,其他为false
|
||||
sendResponse(dialogData.value.messageId, choice)
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
// 显示问题对话框
|
||||
const showQuestion = (questionData: any) => {
|
||||
// 显示系统级问题对话框
|
||||
const showQuestion = async (questionData: any) => {
|
||||
const title = questionData.title || '操作提示'
|
||||
const message = questionData.message || ''
|
||||
const options = questionData.options || ['确定', '取消']
|
||||
const messageId = questionData.message_id || 'fallback_' + Date.now()
|
||||
|
||||
logger.info('[WebSocket消息监听器] 显示自定义对话框:', questionData)
|
||||
logger.info('[WebSocket消息监听器] 显示系统级对话框:', questionData)
|
||||
|
||||
// 设置对话框数据
|
||||
dialogData.value = {
|
||||
title,
|
||||
message,
|
||||
options,
|
||||
messageId
|
||||
if (!isElectron()) {
|
||||
logger.error('[WebSocket消息监听器] 不在 Electron 环境中,无法显示系统级对话框')
|
||||
// 在非 Electron 环境中,使用默认响应
|
||||
sendResponse(messageId, false)
|
||||
return
|
||||
}
|
||||
|
||||
showDialog.value = true
|
||||
|
||||
// 在下一个tick自动聚焦第一个按钮
|
||||
nextTick(() => {
|
||||
const firstButton = document.querySelector('.dialog-button:first-child') as HTMLButtonElement
|
||||
if (firstButton) {
|
||||
firstButton.focus()
|
||||
}
|
||||
})
|
||||
try {
|
||||
// 调用 Electron API 显示系统级对话框
|
||||
const result = await (window as any).electronAPI.showQuestionDialog({
|
||||
title,
|
||||
message,
|
||||
options,
|
||||
messageId
|
||||
})
|
||||
|
||||
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类型消息')
|
||||
|
||||
if (data.message_id) {
|
||||
logger.info('[WebSocket消息监听器] message_id存在,显示选择弹窗')
|
||||
logger.info('[WebSocket消息监听器] message_id存在,显示系统级对话框')
|
||||
showQuestion(data)
|
||||
return
|
||||
} else {
|
||||
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
|
||||
// 即使缺少message_id,也尝试显示弹窗,使用当前时间戳作为ID
|
||||
// 即使缺少message_id,也尝试显示对话框,使用当前时间戳作为ID
|
||||
const fallbackId = 'fallback_' + Date.now()
|
||||
logger.info('[WebSocket消息监听器] 使用备用ID显示弹窗:', fallbackId)
|
||||
logger.info('[WebSocket消息监听器] 使用备用ID显示对话框:', fallbackId)
|
||||
showQuestion({
|
||||
...data,
|
||||
message_id: fallbackId
|
||||
@@ -210,150 +186,4 @@ onUnmounted(() => {
|
||||
})
|
||||
</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>
|
||||
|
||||
130
frontend/src/types/electron.d.ts
vendored
130
frontend/src/types/electron.d.ts
vendored
@@ -1,64 +1,74 @@
|
||||
export interface ElectronAPI {
|
||||
openDevTools: () => Promise<void>
|
||||
selectFolder: () => Promise<string | null>
|
||||
selectFile: (filters?: any[]) => Promise<string[]>
|
||||
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
|
||||
// 初始化相关API
|
||||
checkEnvironment: () => Promise<any>
|
||||
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
|
||||
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
|
||||
downloadPython: (mirror?: string) => Promise<any>
|
||||
installPip: () => Promise<any>
|
||||
downloadGit: () => Promise<any>
|
||||
installDependencies: (mirror?: string) => Promise<any>
|
||||
cloneBackend: (repoUrl?: string) => Promise<any>
|
||||
updateBackend: (repoUrl?: string) => Promise<any>
|
||||
startBackend: () => Promise<{ success: boolean; error?: string }>
|
||||
stopBackend?: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 管理员权限相关
|
||||
checkAdmin: () => Promise<boolean>
|
||||
restartAsAdmin: () => Promise<void>
|
||||
|
||||
// 配置文件操作
|
||||
saveConfig: (config: any) => Promise<void>
|
||||
loadConfig: () => Promise<any>
|
||||
resetConfig: () => Promise<void>
|
||||
|
||||
// 日志文件操作
|
||||
getLogPath: () => Promise<string>
|
||||
getLogFiles: () => Promise<string[]>
|
||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||
clearLogs: (fileName?: string) => Promise<void>
|
||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||
|
||||
// 保留原有方法以兼容现有代码
|
||||
saveLogsToFile: (logs: string) => Promise<void>
|
||||
loadLogsFromFile: () => Promise<string | null>
|
||||
|
||||
// 文件系统操作
|
||||
openFile: (filePath: string) => Promise<void>
|
||||
showItemInFolder: (filePath: string) => Promise<void>
|
||||
|
||||
// 监听下载进度
|
||||
onDownloadProgress: (callback: (progress: any) => void) => void
|
||||
removeDownloadProgressListener: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface ElectronAPI {
|
||||
openDevTools: () => Promise<void>
|
||||
selectFolder: () => Promise<string | null>
|
||||
selectFile: (filters?: any[]) => Promise<string[]>
|
||||
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
|
||||
// 初始化相关API
|
||||
checkEnvironment: () => Promise<any>
|
||||
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
|
||||
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
|
||||
downloadPython: (mirror?: string) => Promise<any>
|
||||
installPip: () => Promise<any>
|
||||
downloadGit: () => Promise<any>
|
||||
installDependencies: (mirror?: string) => Promise<any>
|
||||
cloneBackend: (repoUrl?: string) => Promise<any>
|
||||
updateBackend: (repoUrl?: string) => Promise<any>
|
||||
startBackend: () => Promise<{ success: boolean; error?: string }>
|
||||
stopBackend?: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 管理员权限相关
|
||||
checkAdmin: () => Promise<boolean>
|
||||
restartAsAdmin: () => Promise<void>
|
||||
|
||||
// 配置文件操作
|
||||
saveConfig: (config: any) => Promise<void>
|
||||
loadConfig: () => Promise<any>
|
||||
resetConfig: () => Promise<void>
|
||||
|
||||
// 日志文件操作
|
||||
getLogPath: () => Promise<string>
|
||||
getLogFiles: () => Promise<string[]>
|
||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||
clearLogs: (fileName?: string) => Promise<void>
|
||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||
|
||||
// 保留原有方法以兼容现有代码
|
||||
saveLogsToFile: (logs: string) => Promise<void>
|
||||
loadLogsFromFile: () => Promise<string | null>
|
||||
|
||||
// 文件系统操作
|
||||
openFile: (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
|
||||
removeDownloadProgressListener: () => void
|
||||
}
|
||||
|
||||
interface Window {
|
||||
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 {
|
||||
getLogPath: () => Promise<string>
|
||||
getLogFiles: () => Promise<string[]>
|
||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||
clearLogs: (fileName?: string) => Promise<void>
|
||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||
}
|
||||
const LogLevel = {
|
||||
DEBUG: 'DEBUG',
|
||||
INFO: 'INFO',
|
||||
WARN: 'WARN',
|
||||
ERROR: 'ERROR'
|
||||
} as const
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI
|
||||
}
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 'DEBUG',
|
||||
INFO = 'INFO',
|
||||
WARN = 'WARN',
|
||||
ERROR = 'ERROR'
|
||||
}
|
||||
export type LogLevel = typeof LogLevel[keyof typeof LogLevel]
|
||||
export { LogLevel }
|
||||
|
||||
class Logger {
|
||||
// 直接使用原生console,主进程会自动处理日志记录
|
||||
@@ -40,32 +29,32 @@ class Logger {
|
||||
|
||||
// 获取日志文件路径
|
||||
async getLogPath(): Promise<string> {
|
||||
if (window.electronAPI) {
|
||||
return await window.electronAPI.getLogPath()
|
||||
if ((window as any).electronAPI) {
|
||||
return await (window as any).electronAPI.getLogPath()
|
||||
}
|
||||
throw new Error('Electron API not available')
|
||||
}
|
||||
|
||||
// 获取日志文件列表
|
||||
async getLogFiles(): Promise<string[]> {
|
||||
if (window.electronAPI) {
|
||||
return await window.electronAPI.getLogFiles()
|
||||
if ((window as any).electronAPI) {
|
||||
return await (window as any).electronAPI.getLogFiles()
|
||||
}
|
||||
throw new Error('Electron API not available')
|
||||
}
|
||||
|
||||
// 获取日志内容
|
||||
async getLogs(lines?: number, fileName?: string): Promise<string> {
|
||||
if (window.electronAPI) {
|
||||
return await window.electronAPI.getLogs(lines, fileName)
|
||||
if ((window as any).electronAPI) {
|
||||
return await (window as any).electronAPI.getLogs(lines, fileName)
|
||||
}
|
||||
throw new Error('Electron API not available')
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
async clearLogs(fileName?: string): Promise<void> {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.clearLogs(fileName)
|
||||
if ((window as any).electronAPI) {
|
||||
await (window as any).electronAPI.clearLogs(fileName)
|
||||
console.info(`日志已清空: ${fileName || '当前文件'}`)
|
||||
} else {
|
||||
throw new Error('Electron API not available')
|
||||
@@ -74,8 +63,8 @@ class Logger {
|
||||
|
||||
// 清理旧日志
|
||||
async cleanOldLogs(daysToKeep: number = 7): Promise<void> {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.cleanOldLogs(daysToKeep)
|
||||
if ((window as any).electronAPI) {
|
||||
await (window as any).electronAPI.cleanOldLogs(daysToKeep)
|
||||
console.info(`已清理${daysToKeep}天前的旧日志`)
|
||||
} else {
|
||||
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