feat(websocket): 实现WebSocket消息监听和调试功能
- 添加WebSocketMessageListener组件用于全局消息监听- 新增WebSocketDebugPanel调试面板(仅开发环境显示) - 在多个组件中实现基于subscriptionId的WebSocket连接管理 -为MAA和通用配置添加更可靠的连接生命周期控制 - 引入消息测试页面用于调试WebSocket消息处理- 更新调度器逻辑以支持新的WebSocket订阅机制 - 优化WebSocket连接的创建、维护和断开流程 - 添加开发环境下的调度中心调试工具导入- 重构WebSocket相关组件的导入和注册方式 - 移除冗余的电源倒计时相关状态和方法
This commit is contained in:
@@ -44,7 +44,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
saveConfig: (config: any) => ipcRenderer.invoke('save-config', config),
|
||||
loadConfig: () => ipcRenderer.invoke('load-config'),
|
||||
resetConfig: () => ipcRenderer.invoke('reset-config'),
|
||||
|
||||
|
||||
// 托盘设置实时更新
|
||||
updateTraySettings: (uiSettings: any) => ipcRenderer.invoke('update-tray-settings', uiSettings),
|
||||
|
||||
@@ -54,7 +54,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getLogs: (lines?: number, fileName?: string) => ipcRenderer.invoke('get-logs', lines, fileName),
|
||||
clearLogs: (fileName?: string) => ipcRenderer.invoke('clear-logs', fileName),
|
||||
cleanOldLogs: (daysToKeep?: number) => ipcRenderer.invoke('clean-old-logs', daysToKeep),
|
||||
|
||||
|
||||
// 保留原有方法以兼容现有代码
|
||||
saveLogsToFile: (logs: string) => ipcRenderer.invoke('save-logs-to-file', logs),
|
||||
loadLogsFromFile: () => ipcRenderer.invoke('load-logs-from-file'),
|
||||
|
||||
@@ -9,6 +9,8 @@ import TitleBar from './components/TitleBar.vue'
|
||||
import UpdateModal from './components/UpdateModal.vue'
|
||||
import DevDebugPanel from './components/DevDebugPanel.vue'
|
||||
import GlobalPowerCountdown from './components/GlobalPowerCountdown.vue'
|
||||
import WebSocketMessageListener from './components/WebSocketMessageListener.vue'
|
||||
import WebSocketDebugPanel from './components/WebSocketDebugPanel.vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
@@ -53,6 +55,12 @@ onMounted(() => {
|
||||
|
||||
<!-- 全局电源倒计时弹窗 -->
|
||||
<GlobalPowerCountdown />
|
||||
|
||||
<!-- WebSocket 消息监听组件 -->
|
||||
<WebSocketMessageListener />
|
||||
|
||||
<!-- WebSocket 调试面板 (仅开发环境) -->
|
||||
<WebSocketDebugPanel v-if="$route.query.debug === 'true'" />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
ghost-class="user-ghost"
|
||||
chosen-class="user-chosen"
|
||||
drag-class="user-drag"
|
||||
@end="evt => onUserDragEnd(evt, script)"
|
||||
@end="(evt: any) => onUserDragEnd(evt, script)"
|
||||
class="users-list"
|
||||
>
|
||||
<template #item="{ element: user }">
|
||||
@@ -326,7 +326,7 @@ import { message } from 'ant-design-vue'
|
||||
|
||||
interface Props {
|
||||
scripts: Script[]
|
||||
activeConnections: Map<string, string>
|
||||
activeConnections: Map<string, { subscriptionId: string; websocketId: string }>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -464,7 +464,7 @@ const onScriptDragEnd = async () => {
|
||||
}
|
||||
|
||||
// 处理用户拖拽结束
|
||||
const onUserDragEnd = async (evt: any, script: Script) => {
|
||||
const onUserDragEnd = async (_evt: any, script: Script) => {
|
||||
try {
|
||||
const userIds = script.users?.map(user => user.id) || []
|
||||
await Service.reorderUserApiScriptsUserOrderPost({
|
||||
|
||||
275
frontend/src/components/WebSocketDebugPanel.vue
Normal file
275
frontend/src/components/WebSocketDebugPanel.vue
Normal file
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<div class="websocket-debug">
|
||||
<h3>WebSocket 调试面板</h3>
|
||||
|
||||
<div class="debug-section">
|
||||
<h4>连接状态</h4>
|
||||
<p>状态: {{ wsStatus }}</p>
|
||||
<p>订阅数量: {{ subscriberCount }}</p>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<h4>测试消息</h4>
|
||||
<button @click="testQuestionMessage" class="test-btn">测试 Question 消息</button>
|
||||
<button @click="testNormalMessage" class="test-btn">测试普通消息</button>
|
||||
<button @click="testMalformedMessage" class="test-btn">测试格式错误消息</button>
|
||||
</div>
|
||||
|
||||
<div class="debug-section">
|
||||
<h4>最近接收的消息</h4>
|
||||
<div class="message-log">
|
||||
<div v-for="(msg, index) in recentMessages" :key="index" class="message-item">
|
||||
<div class="message-timestamp">{{ msg.timestamp }}</div>
|
||||
<div class="message-content">{{ JSON.stringify(msg.data, null, 2) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
const { subscribe, unsubscribe, sendRaw, getConnectionInfo } = useWebSocket()
|
||||
|
||||
// 状态
|
||||
const wsStatus = ref('')
|
||||
const subscriberCount = ref(0)
|
||||
const recentMessages = ref<Array<{timestamp: string, data: any}>>([])
|
||||
|
||||
// 订阅ID
|
||||
let debugSubscriptionId: string
|
||||
|
||||
// 更新状态
|
||||
const updateStatus = () => {
|
||||
const connInfo = getConnectionInfo()
|
||||
wsStatus.value = connInfo.status
|
||||
subscriberCount.value = connInfo.subscriberCount
|
||||
}
|
||||
|
||||
// 处理接收到的消息
|
||||
const handleDebugMessage = (message: WebSocketBaseMessage) => {
|
||||
logger.info('[WebSocket调试] 收到消息:', message)
|
||||
|
||||
// 添加到最近消息列表
|
||||
recentMessages.value.unshift({
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
data: message
|
||||
})
|
||||
|
||||
// 保持最近10条消息
|
||||
if (recentMessages.value.length > 10) {
|
||||
recentMessages.value = recentMessages.value.slice(0, 10)
|
||||
}
|
||||
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
// 测试发送Question消息
|
||||
const testQuestionMessage = () => {
|
||||
const message = {
|
||||
id: "debug_test_" + Date.now(),
|
||||
type: "message",
|
||||
data: {
|
||||
type: "Question",
|
||||
message_id: "q_" + Date.now(),
|
||||
title: "调试测试问题",
|
||||
message: "这是一个来自调试面板的测试问题,请选择是否继续?"
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('[WebSocket调试] 发送Question消息:', message)
|
||||
sendRaw('message', message.data)
|
||||
}
|
||||
|
||||
// 测试发送普通消息
|
||||
const testNormalMessage = () => {
|
||||
const message = {
|
||||
id: "debug_normal_" + Date.now(),
|
||||
type: "message",
|
||||
data: {
|
||||
action: "test_action",
|
||||
status: "running",
|
||||
content: "这是一个来自调试面板的普通消息"
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('[WebSocket调试] 发送普通消息:', message)
|
||||
sendRaw('message', message.data)
|
||||
}
|
||||
|
||||
// 测试发送格式错误的消息
|
||||
const testMalformedMessage = () => {
|
||||
const message = {
|
||||
id: "debug_malformed_" + Date.now(),
|
||||
type: "message",
|
||||
data: {
|
||||
type: "Question",
|
||||
// 故意缺少 message_id
|
||||
title: "格式错误的问题",
|
||||
message: "这个消息缺少message_id字段,测试容错处理"
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('[WebSocket调试] 发送格式错误消息:', message)
|
||||
sendRaw('message', message.data)
|
||||
}
|
||||
|
||||
// 组件挂载
|
||||
onMounted(() => {
|
||||
logger.info('[WebSocket调试] 调试面板挂载')
|
||||
|
||||
// 订阅所有类型的消息进行调试
|
||||
debugSubscriptionId = subscribe({}, handleDebugMessage)
|
||||
|
||||
updateStatus()
|
||||
|
||||
// 定期更新状态
|
||||
const statusTimer = setInterval(updateStatus, 2000)
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
clearInterval(statusTimer)
|
||||
})
|
||||
})
|
||||
|
||||
// 组件卸载
|
||||
onUnmounted(() => {
|
||||
logger.info('[WebSocket调试] 调试面板卸载')
|
||||
if (debugSubscriptionId) {
|
||||
unsubscribe(debugSubscriptionId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.websocket-debug {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
width: 350px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
font-size: 12px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.debug-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 4px 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.test-btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.message-log {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px;
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
border-left: 3px solid #007bff;
|
||||
}
|
||||
|
||||
.message-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.message-timestamp {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.websocket-debug {
|
||||
background: #2a2a2a;
|
||||
border-color: #444;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.debug-section {
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
|
||||
h3, h4 {
|
||||
color: #e8e8e8;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.message-log {
|
||||
background: #333;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
color: #e8e8e8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
359
frontend/src/components/WebSocketMessageListener.vue
Normal file
359
frontend/src/components/WebSocketMessageListener.vue
Normal file
@@ -0,0 +1,359 @@
|
||||
<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 { 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
|
||||
|
||||
// 发送用户选择结果到后端
|
||||
const sendResponse = (messageId: string, choice: boolean) => {
|
||||
const response = {
|
||||
message_id: messageId,
|
||||
choice: choice,
|
||||
}
|
||||
|
||||
logger.info('[WebSocket消息监听器] 发送用户选择结果:', response)
|
||||
|
||||
// 发送响应消息到后端
|
||||
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 title = questionData.title || '操作提示'
|
||||
const message = questionData.message || ''
|
||||
const options = questionData.options || ['确定', '取消']
|
||||
const messageId = questionData.message_id || 'fallback_' + Date.now()
|
||||
|
||||
logger.info('[WebSocket消息监听器] 显示自定义对话框:', questionData)
|
||||
|
||||
// 设置对话框数据
|
||||
dialogData.value = {
|
||||
title,
|
||||
message,
|
||||
options,
|
||||
messageId
|
||||
}
|
||||
|
||||
showDialog.value = true
|
||||
|
||||
// 在下一个tick自动聚焦第一个按钮
|
||||
nextTick(() => {
|
||||
const firstButton = document.querySelector('.dialog-button:first-child') as HTMLButtonElement
|
||||
if (firstButton) {
|
||||
firstButton.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 消息处理函数
|
||||
const handleMessage = (message: WebSocketBaseMessage) => {
|
||||
try {
|
||||
logger.info('[WebSocket消息监听器] 收到Message类型消息:', message)
|
||||
logger.info('[WebSocket消息监听器] 消息详情 - type:', message.type, 'id:', message.id, 'data:', message.data)
|
||||
|
||||
// 解析消息数据
|
||||
if (message.data) {
|
||||
console.log('[WebSocket消息监听器] 消息数据:', message.data)
|
||||
|
||||
// 根据具体的消息内容进行处理
|
||||
if (typeof message.data === 'object') {
|
||||
// 处理对象类型的数据
|
||||
handleObjectMessage(message.data)
|
||||
} else if (typeof message.data === 'string') {
|
||||
// 处理字符串类型的数据
|
||||
handleStringMessage(message.data)
|
||||
} else {
|
||||
// 处理其他类型的数据
|
||||
handleOtherMessage(message.data)
|
||||
}
|
||||
} else {
|
||||
logger.warn('[WebSocket消息监听器] 收到空数据的消息')
|
||||
}
|
||||
|
||||
// 这里可以添加具体的业务逻辑
|
||||
// 例如:更新状态、触发事件、显示通知等
|
||||
} catch (error) {
|
||||
logger.error('[WebSocket消息监听器] 处理消息时发生错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理对象类型的消息
|
||||
const handleObjectMessage = (data: any) => {
|
||||
logger.info('[WebSocket消息监听器] 处理对象消息:', data)
|
||||
|
||||
// 检查是否为Question类型的消息
|
||||
logger.info('[WebSocket消息监听器] 检查消息类型 - data.type:', data.type, 'data.message_id:', data.message_id)
|
||||
|
||||
if (data.type === 'Question') {
|
||||
logger.info('[WebSocket消息监听器] 发现Question类型消息')
|
||||
|
||||
if (data.message_id) {
|
||||
logger.info('[WebSocket消息监听器] message_id存在,显示选择弹窗')
|
||||
showQuestion(data)
|
||||
return
|
||||
} else {
|
||||
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
|
||||
// 即使缺少message_id,也尝试显示弹窗,使用当前时间戳作为ID
|
||||
const fallbackId = 'fallback_' + Date.now()
|
||||
logger.info('[WebSocket消息监听器] 使用备用ID显示弹窗:', fallbackId)
|
||||
showQuestion({
|
||||
...data,
|
||||
message_id: fallbackId
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 根据对象的属性进行不同处理
|
||||
if (data.action) {
|
||||
logger.info('[WebSocket消息监听器] 消息动作:', data.action)
|
||||
}
|
||||
|
||||
if (data.status) {
|
||||
logger.info('[WebSocket消息监听器] 消息状态:', data.status)
|
||||
}
|
||||
|
||||
if (data.content) {
|
||||
logger.info('[WebSocket消息监听器] 消息内容:', data.content)
|
||||
}
|
||||
|
||||
// 可以根据具体需求添加更多处理逻辑
|
||||
}
|
||||
|
||||
// 处理字符串类型的消息
|
||||
const handleStringMessage = (data: string) => {
|
||||
logger.info('[WebSocket消息监听器] 处理字符串消息:', data)
|
||||
|
||||
try {
|
||||
// 尝试解析JSON字符串
|
||||
const parsed = JSON.parse(data)
|
||||
logger.info('[WebSocket消息监听器] 解析后的JSON:', parsed)
|
||||
handleObjectMessage(parsed)
|
||||
} catch (error) {
|
||||
// 不是JSON格式,作为普通字符串处理
|
||||
logger.info('[WebSocket消息监听器] 普通字符串消息:', data)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他类型的消息
|
||||
const handleOtherMessage = (data: any) => {
|
||||
logger.info('[WebSocket消息监听器] 处理其他类型消息:', typeof data, data)
|
||||
}
|
||||
|
||||
// 组件挂载时订阅消息
|
||||
onMounted(() => {
|
||||
logger.info('[WebSocket消息监听器~~] 组件挂载,开始监听Message类型的消息')
|
||||
|
||||
// 使用新的 subscribe API,订阅 Message 类型的消息(注意大写M)
|
||||
subscriptionId = subscribe({ type: 'Message' }, handleMessage)
|
||||
|
||||
logger.info('[WebSocket消息监听器~~] 订阅ID:', subscriptionId)
|
||||
logger.info('[WebSocket消息监听器~~] 订阅过滤器:', { type: 'Message' })
|
||||
})
|
||||
|
||||
// 组件卸载时取消订阅
|
||||
onUnmounted(() => {
|
||||
logger.info('[WebSocket消息监听器~~] 组件卸载,停止监听Message类型的消息')
|
||||
// 使用新的 unsubscribe API
|
||||
if (subscriptionId) {
|
||||
unsubscribe(subscriptionId)
|
||||
}
|
||||
})
|
||||
</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>
|
||||
1
frontend/src/components/devtools/BackendLaunchPage.vue
Normal file
1
frontend/src/components/devtools/BackendLaunchPage.vue
Normal file
@@ -0,0 +1 @@
|
||||
.log-entry.error .log-message {
|
||||
654
frontend/src/components/devtools/MessageTestPage.vue
Normal file
654
frontend/src/components/devtools/MessageTestPage.vue
Normal file
@@ -0,0 +1,654 @@
|
||||
<template>
|
||||
<div class="test-page">
|
||||
<h3 class="page-title">🔧 消息弹窗测试</h3>
|
||||
|
||||
<div class="test-section">
|
||||
<h4>测试消息弹窗</h4>
|
||||
<div class="test-controls">
|
||||
<button class="test-btn primary" @click="triggerQuestionModal" :disabled="isTesting">
|
||||
{{ isTesting ? '测试中...' : '触发Question弹窗' }}
|
||||
</button>
|
||||
|
||||
<button class="test-btn secondary" @click="triggerCustomModal" :disabled="isTesting">
|
||||
自定义消息测试
|
||||
</button>
|
||||
|
||||
<button class="test-btn warning" @click="directTriggerModal" :disabled="isTesting">
|
||||
直接触发测试
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="test-info">
|
||||
<p>点击按钮测试全屏消息选择弹窗功能</p>
|
||||
<p>最后响应: {{ lastResponse || '暂无' }}</p>
|
||||
<p>连接状态: <span :class="connectionStatusClass">{{ connectionStatus }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h4>自定义测试消息</h4>
|
||||
<div class="custom-form">
|
||||
<div class="form-group">
|
||||
<label>标题:</label>
|
||||
<input
|
||||
v-model="customMessage.title"
|
||||
type="text"
|
||||
placeholder="请输入弹窗标题"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>消息内容:</label>
|
||||
<textarea
|
||||
v-model="customMessage.message"
|
||||
placeholder="请输入消息内容"
|
||||
class="form-textarea"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
<button
|
||||
class="test-btn primary"
|
||||
@click="sendCustomMessage"
|
||||
:disabled="!customMessage.title || !customMessage.message"
|
||||
>
|
||||
发送自定义消息
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h4>测试历史</h4>
|
||||
<div class="test-history">
|
||||
<div v-for="(test, index) in testHistory" :key="index" class="history-item">
|
||||
<div class="history-time">{{ test.time }}</div>
|
||||
<div class="history-content">{{ test.title }} - {{ test.result }}</div>
|
||||
</div>
|
||||
<div v-if="testHistory.length === 0" class="no-history">暂无测试历史</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
const { subscribe, unsubscribe, sendRaw, getConnectionInfo } = useWebSocket()
|
||||
|
||||
// 测试状态
|
||||
const isTesting = ref(false)
|
||||
const lastResponse = ref('')
|
||||
const testHistory = ref<Array<{ time: string; title: string; result: string }>>([])
|
||||
const connectionStatus = ref('检查中...')
|
||||
const connectionStatusClass = ref('status-checking')
|
||||
|
||||
// 自定义消息
|
||||
const customMessage = ref({
|
||||
title: '操作确认',
|
||||
message: '请确认是否继续执行此操作?',
|
||||
})
|
||||
|
||||
// 更新连接状态
|
||||
const updateConnectionStatus = () => {
|
||||
try {
|
||||
const connInfo = getConnectionInfo()
|
||||
connectionStatus.value = connInfo.status
|
||||
|
||||
switch (connInfo.status) {
|
||||
case '已连接':
|
||||
connectionStatusClass.value = 'status-connected'
|
||||
break
|
||||
case '连接中':
|
||||
connectionStatusClass.value = 'status-connecting'
|
||||
break
|
||||
case '已断开':
|
||||
connectionStatusClass.value = 'status-disconnected'
|
||||
break
|
||||
case '连接错误':
|
||||
connectionStatusClass.value = 'status-error'
|
||||
break
|
||||
default:
|
||||
connectionStatusClass.value = 'status-unknown'
|
||||
}
|
||||
} catch (error) {
|
||||
connectionStatus.value = '获取失败'
|
||||
connectionStatusClass.value = 'status-error'
|
||||
}
|
||||
}
|
||||
|
||||
// 存储订阅ID用于监听响应
|
||||
let responseSubscriptionId: string
|
||||
|
||||
// 生成唯一ID
|
||||
const generateId = () => {
|
||||
return 'test-' + Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = () => {
|
||||
return new Date().toLocaleTimeString()
|
||||
}
|
||||
|
||||
// 添加测试历史
|
||||
const addTestHistory = (title: string, result: string) => {
|
||||
testHistory.value.unshift({
|
||||
time: formatTime(),
|
||||
title,
|
||||
result,
|
||||
})
|
||||
// 保持最多10条历史记录
|
||||
if (testHistory.value.length > 10) {
|
||||
testHistory.value = testHistory.value.slice(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
// 直接触发弹窗(备用方法)
|
||||
const directTriggerModal = () => {
|
||||
isTesting.value = true
|
||||
|
||||
try {
|
||||
// 直接触发浏览器的confirm对话框作为备用测试
|
||||
const result = confirm('这是直接触发的测试弹窗。\n\n如果WebSocket消息弹窗无法正常工作,这个方法可以用来验证基本功能。\n\n点击"确定"继续,点击"取消"退出。')
|
||||
|
||||
lastResponse.value = result ? '用户选择: 确认 (直接触发)' : '用户选择: 取消 (直接触发)'
|
||||
addTestHistory('直接触发测试', result ? '确认' : '取消')
|
||||
|
||||
logger.info('[调试工具] 直接触发测试完成,结果:', result)
|
||||
} catch (error: any) {
|
||||
logger.error('[调试工具] 直接触发测试失败:', error)
|
||||
lastResponse.value = '直接触发失败: ' + (error?.message || '未知错误')
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
isTesting.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 发送WebSocket消息来模拟接收消息
|
||||
const simulateMessage = (messageData: any) => {
|
||||
logger.info('[调试工具] 发送模拟消息:', messageData)
|
||||
|
||||
// 检查连接状态
|
||||
const connInfo = getConnectionInfo()
|
||||
if (connInfo.status !== '已连接') {
|
||||
logger.warn('[调试工具] WebSocket未连接,无法发送消息')
|
||||
lastResponse.value = '发送失败: WebSocket未连接'
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用sendRaw直接发送Message类型的消息
|
||||
sendRaw('Message', messageData)
|
||||
|
||||
logger.info('[调试工具] 消息已发送到WebSocket')
|
||||
lastResponse.value = '消息已发送,等待弹窗显示...'
|
||||
} catch (error: any) {
|
||||
logger.error('[调试工具] 发送消息失败:', error)
|
||||
lastResponse.value = '发送失败: ' + (error?.message || '未知错误')
|
||||
}
|
||||
}
|
||||
|
||||
// 触发标准Question弹窗
|
||||
const triggerQuestionModal = () => {
|
||||
isTesting.value = true
|
||||
|
||||
const testMessageData = {
|
||||
message_id: generateId(),
|
||||
type: 'Question',
|
||||
title: '测试提示',
|
||||
message: '这是一个测试消息,请选择您的操作。',
|
||||
}
|
||||
|
||||
logger.info('[调试工具] 发送测试Question消息:', testMessageData)
|
||||
|
||||
// 直接模拟接收消息
|
||||
simulateMessage(testMessageData)
|
||||
|
||||
lastResponse.value = '已发送测试Question消息'
|
||||
addTestHistory('标准Question测试', '已发送')
|
||||
|
||||
setTimeout(() => {
|
||||
isTesting.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 触发自定义弹窗
|
||||
const triggerCustomModal = () => {
|
||||
isTesting.value = true
|
||||
|
||||
const testMessageData = {
|
||||
message_id: generateId(),
|
||||
type: 'Question',
|
||||
title: '自定义测试',
|
||||
message:
|
||||
'这是一个自定义的测试消息,用于验证弹窗的不同内容显示。您可以测试长文本、特殊字符等情况。',
|
||||
}
|
||||
|
||||
logger.info('[调试工具] 发送自定义测试消息:', testMessageData)
|
||||
|
||||
simulateMessage(testMessageData)
|
||||
|
||||
lastResponse.value = '已发送自定义测试消息'
|
||||
addTestHistory('自定义内容测试', '已发送')
|
||||
|
||||
setTimeout(() => {
|
||||
isTesting.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 发送完全自定义的消息
|
||||
const sendCustomMessage = () => {
|
||||
if (!customMessage.value.title || !customMessage.value.message) {
|
||||
return
|
||||
}
|
||||
|
||||
isTesting.value = true
|
||||
|
||||
const testMessageData = {
|
||||
message_id: generateId(),
|
||||
type: 'Question',
|
||||
title: customMessage.value.title,
|
||||
message: customMessage.value.message,
|
||||
}
|
||||
|
||||
logger.info('[调试工具] 发送用户自定义消息:', testMessageData)
|
||||
|
||||
simulateMessage(testMessageData)
|
||||
|
||||
lastResponse.value = `已发送自定义消息: ${customMessage.value.title}`
|
||||
addTestHistory(`自定义: ${customMessage.value.title}`, '已发送')
|
||||
|
||||
setTimeout(() => {
|
||||
isTesting.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 监听响应消息
|
||||
const handleResponseMessage = (message: any) => {
|
||||
logger.info('[调试工具] 收到响应消息:', message)
|
||||
|
||||
if (message.data && message.data.choice !== undefined) {
|
||||
const choice = message.data.choice ? '确认' : '取消'
|
||||
lastResponse.value = `用户选择: ${choice}`
|
||||
addTestHistory('用户响应', choice)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时订阅响应消息
|
||||
onMounted(() => {
|
||||
logger.info('[调试工具] 初始化消息测试页面')
|
||||
|
||||
// 订阅Response类型的消息来监听用户的选择结果
|
||||
responseSubscriptionId = subscribe({ type: 'Response' }, handleResponseMessage)
|
||||
|
||||
// 初始化连接状态
|
||||
updateConnectionStatus()
|
||||
|
||||
// 定期更新连接状态
|
||||
const statusTimer = setInterval(updateConnectionStatus, 2000)
|
||||
|
||||
logger.info('[调试工具] 已订阅Response消息,订阅ID:', responseSubscriptionId)
|
||||
|
||||
// 清理定时器
|
||||
onUnmounted(() => {
|
||||
clearInterval(statusTimer)
|
||||
})
|
||||
})
|
||||
|
||||
// 组件卸载时清理订阅
|
||||
onUnmounted(() => {
|
||||
if (responseSubscriptionId) {
|
||||
unsubscribe(responseSubscriptionId)
|
||||
logger.info('[调试工具] 已取消Response消息订阅')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.test-page {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.test-section h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 12px;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.test-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.test-btn.primary {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn.primary:hover:not(:disabled) {
|
||||
background: #45a049;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.test-btn.secondary {
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn.secondary:hover:not(:disabled) {
|
||||
background: #1976d2;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.test-btn.warning {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.test-btn.warning:hover:not(:disabled) {
|
||||
background: #f57c00;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3);
|
||||
}
|
||||
|
||||
.test-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.test-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.test-info {
|
||||
font-size: 10px;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.test-info p {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* 连接状态样式 */
|
||||
.status-connected {
|
||||
color: #4caf50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-connecting {
|
||||
color: #ff9800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
color: #f44336;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: #e91e63;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-checking,
|
||||
.status-unknown {
|
||||
color: #9e9e9e;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.custom-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 10px;
|
||||
color: #ccc;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #4caf50;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
|
||||
}
|
||||
|
||||
.form-input::placeholder,
|
||||
.form-textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.test-history {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.test-history::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.test-history::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.test-history::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.test-history::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
font-size: 10px;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.history-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.history-time {
|
||||
color: #888;
|
||||
min-width: 60px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.history-content {
|
||||
color: #ccc;
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.no-history {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 10px;
|
||||
padding: 16px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 暗色主题专用样式增强 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.test-page {
|
||||
color: #e8e8e8;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: #66bb6a;
|
||||
text-shadow: 0 0 8px rgba(102, 187, 106, 0.3);
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.test-section h4 {
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
.test-btn.primary {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #66bb6a 100%);
|
||||
border: 1px solid rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
|
||||
.test-btn.primary:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #45a049 0%, #5cb85c 100%);
|
||||
border-color: rgba(76, 175, 80, 0.5);
|
||||
}
|
||||
|
||||
.test-btn.secondary {
|
||||
background: linear-gradient(135deg, #2196f3 0%, #42a5f5 100%);
|
||||
border: 1px solid rgba(33, 150, 243, 0.3);
|
||||
}
|
||||
|
||||
.test-btn.secondary:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #1976d2 0%, #1e88e5 100%);
|
||||
border-color: rgba(33, 150, 243, 0.5);
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-textarea:focus {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-color: #66bb6a;
|
||||
box-shadow: 0 0 0 2px rgba(102, 187, 106, 0.2);
|
||||
}
|
||||
|
||||
.history-item {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-bottom-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.history-time {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.history-content {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
/* 高对比度模式适配 */
|
||||
@media (prefers-contrast: high) {
|
||||
.test-section {
|
||||
border-width: 2px;
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.test-btn {
|
||||
border: 2px solid currentColor;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-textarea {
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-textarea:focus {
|
||||
border-width: 2px;
|
||||
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画模式适配 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.test-btn,
|
||||
.form-input,
|
||||
.form-textarea,
|
||||
.history-item {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.test-btn:hover:not(:disabled) {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -43,12 +43,14 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import RouteInfoPage from './RouteInfoPage.vue'
|
||||
import EnvironmentPage from './EnvironmentPage.vue'
|
||||
import QuickNavPage from './QuickNavPage.vue'
|
||||
import MessageTestPage from './MessageTestPage.vue'
|
||||
|
||||
// 调试页面配置
|
||||
const tabs = [
|
||||
{ key: 'route', title: '路由', icon: '🛣️', component: RouteInfoPage },
|
||||
{ key: 'env', title: '环境', icon: '⚙️', component: EnvironmentPage },
|
||||
{ key: 'nav', title: '导航', icon: '🚀', component: QuickNavPage },
|
||||
{ key: 'message', title: '消息', icon: '💬', component: MessageTestPage },
|
||||
]
|
||||
|
||||
// 开发环境检测
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ import { OpenAPI } from '@/api'
|
||||
|
||||
import Antd from 'ant-design-vue'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
|
||||
@@ -15,11 +14,13 @@ import { logger } from '@/utils/logger'
|
||||
// 导入镜像管理器
|
||||
import { mirrorManager } from '@/utils/mirrorManager'
|
||||
|
||||
// 导入WebSocket消息监听组件
|
||||
import WebSocketMessageListener from '@/components/WebSocketMessageListener.vue'
|
||||
import { API_ENDPOINTS } from '@/config/mirrors'
|
||||
|
||||
// 配置dayjs中文本地化
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
import { API_ENDPOINTS } from '@/config/mirrors'
|
||||
|
||||
// 配置API基础URL
|
||||
OpenAPI.BASE = API_ENDPOINTS.local
|
||||
|
||||
@@ -28,11 +29,14 @@ logger.info('前端应用开始初始化')
|
||||
logger.info(`API基础URL: ${OpenAPI.BASE}`)
|
||||
|
||||
// 初始化镜像管理器(异步)
|
||||
mirrorManager.initialize().then(() => {
|
||||
logger.info('镜像管理器初始化完成')
|
||||
}).catch((error) => {
|
||||
logger.error('镜像管理器初始化失败:', error)
|
||||
})
|
||||
mirrorManager
|
||||
.initialize()
|
||||
.then(() => {
|
||||
logger.info('镜像管理器初始化完成')
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('镜像管理器初始化失败:', error)
|
||||
})
|
||||
|
||||
// 创建应用实例
|
||||
const app = createApp(App)
|
||||
@@ -66,4 +70,7 @@ app.config.errorHandler = (err, instance, info) => {
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
|
||||
// 注册WebSocket消息监听组件
|
||||
app.component('WebSocketMessageListener', WebSocketMessageListener)
|
||||
|
||||
logger.info('前端应用初始化完成')
|
||||
|
||||
77
frontend/src/utils/scheduler-debug.ts
Normal file
77
frontend/src/utils/scheduler-debug.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
// 调度中心调试工具
|
||||
export function debugScheduler() {
|
||||
console.log('=== 调度中心调试信息 ===')
|
||||
|
||||
// 检查WebSocket连接状态
|
||||
const wsStorage = (window as any)[Symbol.for('GLOBAL_WEBSOCKET_PERSISTENT')]
|
||||
if (wsStorage) {
|
||||
console.log('WebSocket状态:', wsStorage.status.value)
|
||||
console.log('WebSocket连接ID:', wsStorage.connectionId)
|
||||
console.log('订阅数量:', wsStorage.subscriptions.value.size)
|
||||
console.log('缓存标记数量:', wsStorage.cacheMarkers.value.size)
|
||||
console.log('缓存消息数量:', wsStorage.cachedMessages.value.length)
|
||||
|
||||
// 列出所有订阅
|
||||
console.log('当前订阅:')
|
||||
wsStorage.subscriptions.value.forEach((sub, id) => {
|
||||
console.log(` - ${id}: type=${sub.filter.type}, id=${sub.filter.id}`)
|
||||
})
|
||||
} else {
|
||||
console.log('WebSocket存储未初始化')
|
||||
}
|
||||
|
||||
// 检查调度中心状态
|
||||
const scheduler = document.querySelector('[data-scheduler-debug]')
|
||||
if (scheduler) {
|
||||
console.log('调度中心组件已挂载')
|
||||
} else {
|
||||
console.log('调度中心组件未找到')
|
||||
}
|
||||
}
|
||||
|
||||
// 测试WebSocket连接
|
||||
export function testWebSocketConnection() {
|
||||
console.log('=== 测试WebSocket连接 ===')
|
||||
|
||||
try {
|
||||
const ws = new WebSocket('ws://localhost:36163/api/core/ws')
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('✅ WebSocket连接成功')
|
||||
ws.send(JSON.stringify({
|
||||
type: 'Signal',
|
||||
data: { Connect: true, connectionId: 'test-connection' }
|
||||
}))
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const message = JSON.parse(event.data)
|
||||
console.log('📩 收到消息:', message)
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.log('❌ WebSocket错误:', error)
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('🔌 WebSocket连接关闭:', event.code, event.reason)
|
||||
}
|
||||
|
||||
// 5秒后关闭测试连接
|
||||
setTimeout(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close()
|
||||
console.log('🔌 测试连接已关闭')
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
} catch (error) {
|
||||
console.log('❌ 无法创建WebSocket连接:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 在控制台中暴露调试函数
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).debugScheduler = debugScheduler;
|
||||
(window as any).testWebSocketConnection = testWebSocketConnection;
|
||||
}
|
||||
@@ -16,7 +16,11 @@
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
|
||||
<a-space size="middle">
|
||||
// 如果已有连接,先断开
|
||||
if (generalSubscriptionId.value) {
|
||||
unsubscribe(generalSubscriptionId.value)
|
||||
generalSubscriptionId.value = null
|
||||
generalWebsocketId.value = nulla-space size="middle">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
@@ -390,6 +394,7 @@ const scriptName = ref('')
|
||||
|
||||
// 通用配置相关
|
||||
const generalConfigLoading = ref(false)
|
||||
const generalSubscriptionId = ref<string | null>(null)
|
||||
const generalWebsocketId = ref<string | null>(null)
|
||||
const showGeneralConfigMask = ref(false)
|
||||
let generalConfigTimeout: number | null = null
|
||||
@@ -599,12 +604,14 @@ const handleGeneralConfig = async () => {
|
||||
console.debug('订阅 websocketId:', wsId)
|
||||
|
||||
// 订阅 websocket
|
||||
subscribe(wsId, {
|
||||
onMessage: (wsMessage: any) => {
|
||||
const subscriptionId = subscribe(
|
||||
{ id: wsId },
|
||||
(wsMessage: any) => {
|
||||
if (wsMessage.type === 'error') {
|
||||
console.error(`用户 ${formData.userName} 通用配置错误:`, wsMessage.data)
|
||||
message.error(`通用配置连接失败: ${wsMessage.data}`)
|
||||
unsubscribe(wsId)
|
||||
unsubscribe(subscriptionId)
|
||||
generalSubscriptionId.value = null
|
||||
generalWebsocketId.value = null
|
||||
showGeneralConfigMask.value = false
|
||||
return
|
||||
@@ -612,13 +619,15 @@ const handleGeneralConfig = async () => {
|
||||
|
||||
if (wsMessage.data && wsMessage.data.Accomplish) {
|
||||
message.success(`用户 ${formData.userName} 的配置已完成`)
|
||||
unsubscribe(wsId)
|
||||
unsubscribe(subscriptionId)
|
||||
generalSubscriptionId.value = null
|
||||
generalWebsocketId.value = null
|
||||
showGeneralConfigMask.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
generalSubscriptionId.value = subscriptionId
|
||||
generalWebsocketId.value = wsId
|
||||
showGeneralConfigMask.value = true
|
||||
message.success(`已开始配置用户 ${formData.userName} 的通用设置`)
|
||||
@@ -626,9 +635,9 @@ const handleGeneralConfig = async () => {
|
||||
// 设置 30 分钟超时自动断开
|
||||
generalConfigTimeout = window.setTimeout(
|
||||
() => {
|
||||
if (generalWebsocketId.value) {
|
||||
const id = generalWebsocketId.value
|
||||
unsubscribe(id)
|
||||
if (generalSubscriptionId.value) {
|
||||
unsubscribe(generalSubscriptionId.value)
|
||||
generalSubscriptionId.value = null
|
||||
generalWebsocketId.value = null
|
||||
showGeneralConfigMask.value = false
|
||||
message.info(`用户 ${formData.userName} 的配置会话已超时断开`)
|
||||
@@ -658,7 +667,10 @@ const handleSaveGeneralConfig = async () => {
|
||||
|
||||
const response = await Service.stopTaskApiDispatchStopPost({ taskId: websocketId })
|
||||
if (response && response.code === 200) {
|
||||
unsubscribe(websocketId)
|
||||
if (generalSubscriptionId.value) {
|
||||
unsubscribe(generalSubscriptionId.value)
|
||||
generalSubscriptionId.value = null
|
||||
}
|
||||
generalWebsocketId.value = null
|
||||
showGeneralConfigMask.value = false
|
||||
if (generalConfigTimeout) {
|
||||
@@ -720,8 +732,9 @@ const handleWebhookChange = () => {
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
if (generalWebsocketId.value) {
|
||||
unsubscribe(generalWebsocketId.value)
|
||||
if (generalSubscriptionId.value) {
|
||||
unsubscribe(generalSubscriptionId.value)
|
||||
generalSubscriptionId.value = null
|
||||
generalWebsocketId.value = null
|
||||
showGeneralConfigMask.value = false
|
||||
if (generalConfigTimeout) {
|
||||
|
||||
@@ -166,6 +166,7 @@ const scriptName = ref('')
|
||||
|
||||
// MAA配置相关
|
||||
const maaConfigLoading = ref(false)
|
||||
const maaSubscriptionId = ref<string | null>(null)
|
||||
const maaWebsocketId = ref<string | null>(null)
|
||||
const showMAAConfigMask = ref(false)
|
||||
let maaConfigTimeout: number | null = null
|
||||
@@ -763,8 +764,9 @@ const handleMAAConfig = async () => {
|
||||
maaConfigLoading.value = true
|
||||
|
||||
// 如果已有连接,先断开
|
||||
if (maaWebsocketId.value) {
|
||||
unsubscribe(maaWebsocketId.value)
|
||||
if (maaSubscriptionId.value) {
|
||||
unsubscribe(maaSubscriptionId.value)
|
||||
maaSubscriptionId.value = null
|
||||
maaWebsocketId.value = null
|
||||
showMAAConfigMask.value = false
|
||||
if (maaConfigTimeout) {
|
||||
@@ -783,15 +785,17 @@ const handleMAAConfig = async () => {
|
||||
const wsId = response.websocketId
|
||||
|
||||
// 订阅 websocket
|
||||
subscribe(wsId, {
|
||||
onMessage: (wsMessage: any) => {
|
||||
const subscriptionId = subscribe(
|
||||
{ id: wsId },
|
||||
(wsMessage: any) => {
|
||||
if (wsMessage.type === 'error') {
|
||||
console.error(
|
||||
`用户 ${formData.Info?.Name || formData.userName} MAA配置错误:`,
|
||||
wsMessage.data
|
||||
)
|
||||
message.error(`MAA配置连接失败: ${wsMessage.data}`)
|
||||
unsubscribe(wsId)
|
||||
unsubscribe(subscriptionId)
|
||||
maaSubscriptionId.value = null
|
||||
maaWebsocketId.value = null
|
||||
showMAAConfigMask.value = false
|
||||
return
|
||||
@@ -799,13 +803,15 @@ const handleMAAConfig = async () => {
|
||||
|
||||
if (wsMessage.data && wsMessage.data.Accomplish) {
|
||||
message.success(`用户 ${formData.Info?.Name || formData.userName} 的配置已完成`)
|
||||
unsubscribe(wsId)
|
||||
unsubscribe(subscriptionId)
|
||||
maaSubscriptionId.value = null
|
||||
maaWebsocketId.value = null
|
||||
showMAAConfigMask.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
maaSubscriptionId.value = subscriptionId
|
||||
maaWebsocketId.value = wsId
|
||||
showMAAConfigMask.value = true
|
||||
message.success(`已开始配置用户 ${formData.Info?.Name || formData.userName} 的MAA设置`)
|
||||
@@ -813,9 +819,9 @@ const handleMAAConfig = async () => {
|
||||
// 设置 30 分钟超时自动断开
|
||||
maaConfigTimeout = window.setTimeout(
|
||||
() => {
|
||||
if (maaWebsocketId.value) {
|
||||
const id = maaWebsocketId.value
|
||||
unsubscribe(id)
|
||||
if (maaSubscriptionId.value) {
|
||||
unsubscribe(maaSubscriptionId.value)
|
||||
maaSubscriptionId.value = null
|
||||
maaWebsocketId.value = null
|
||||
showMAAConfigMask.value = false
|
||||
message.info(`用户 ${formData.Info?.Name || formData.userName} 的配置会话已超时断开`)
|
||||
@@ -845,7 +851,10 @@ const handleSaveMAAConfig = async () => {
|
||||
|
||||
const response = await Service.stopTaskApiDispatchStopPost({ taskId: websocketId })
|
||||
if (response && response.code === 200) {
|
||||
unsubscribe(websocketId)
|
||||
if (maaSubscriptionId.value) {
|
||||
unsubscribe(maaSubscriptionId.value)
|
||||
maaSubscriptionId.value = null
|
||||
}
|
||||
maaWebsocketId.value = null
|
||||
showMAAConfigMask.value = false
|
||||
if (maaConfigTimeout) {
|
||||
@@ -970,8 +979,9 @@ const addCustomStageRemain = (stageName: string) => {
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
if (maaWebsocketId.value) {
|
||||
unsubscribe(maaWebsocketId.value)
|
||||
if (maaSubscriptionId.value) {
|
||||
unsubscribe(maaSubscriptionId.value)
|
||||
maaSubscriptionId.value = null
|
||||
maaWebsocketId.value = null
|
||||
}
|
||||
router.push('/scripts')
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<!-- 增加 loadedOnce 条件,避免初始渲染时闪烁 -->
|
||||
<div v-if="!loading && loadedOnce && scripts.length === 0" class="empty-state">
|
||||
<div v-if="!addLoading && loadedOnce && scripts.length === 0" class="empty-state">
|
||||
<div class="empty-content">
|
||||
<div class="empty-image-container">
|
||||
<img src="@/assets/NoData.png" alt="暂无数据" class="empty-image" />
|
||||
@@ -287,7 +287,7 @@ const showMAAConfigMask = ref(false) // 控制MAA配置遮罩层的显示
|
||||
const currentConfigScript = ref<Script | null>(null) // 当前正在配置的脚本
|
||||
|
||||
// WebSocket连接管理
|
||||
const activeConnections = ref<Map<string, string>>(new Map()) // scriptId -> websocketId
|
||||
const activeConnections = ref<Map<string, { subscriptionId: string; websocketId: string }>>(new Map()) // scriptId -> { subscriptionId, websocketId }
|
||||
|
||||
// 解析模板描述的markdown
|
||||
const parseMarkdown = (text: string) => {
|
||||
@@ -548,8 +548,9 @@ const handleStartMAAConfig = async (script: Script) => {
|
||||
currentConfigScript.value = script
|
||||
|
||||
// 订阅WebSocket消息
|
||||
subscribe(response.websocketId, {
|
||||
onMessage: (wsMessage: any) => {
|
||||
const subscriptionId = subscribe(
|
||||
{ id: response.websocketId },
|
||||
(wsMessage: any) => {
|
||||
// 处理错误消息
|
||||
if (wsMessage.type === 'error') {
|
||||
console.error(`脚本 ${script.name} 连接错误:`, wsMessage.data)
|
||||
@@ -569,20 +570,23 @@ const handleStartMAAConfig = async (script: Script) => {
|
||||
showMAAConfigMask.value = false
|
||||
currentConfigScript.value = null
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// 记录连接和websocketId
|
||||
activeConnections.value.set(script.id, response.websocketId)
|
||||
// 记录连接和subscriptionId
|
||||
activeConnections.value.set(script.id, {
|
||||
subscriptionId,
|
||||
websocketId: response.websocketId
|
||||
})
|
||||
message.success(`已启动 ${script.name} 的MAA配置`)
|
||||
|
||||
// 设置自动断开连接的定时器(30分钟后)
|
||||
setTimeout(
|
||||
() => {
|
||||
if (activeConnections.value.has(script.id)) {
|
||||
const wsId = activeConnections.value.get(script.id)
|
||||
if (wsId) {
|
||||
unsubscribe(wsId)
|
||||
const connection = activeConnections.value.get(script.id)
|
||||
if (connection) {
|
||||
unsubscribe(connection.subscriptionId)
|
||||
}
|
||||
activeConnections.value.delete(script.id)
|
||||
// 超时时隐藏遮罩
|
||||
@@ -604,20 +608,20 @@ const handleStartMAAConfig = async (script: Script) => {
|
||||
|
||||
const handleSaveMAAConfig = async (script: Script) => {
|
||||
try {
|
||||
const websocketId = activeConnections.value.get(script.id)
|
||||
if (!websocketId) {
|
||||
const connection = activeConnections.value.get(script.id)
|
||||
if (!connection) {
|
||||
message.error('未找到活动的配置会话')
|
||||
return
|
||||
}
|
||||
|
||||
// 调用停止配置任务API
|
||||
const response = await Service.stopTaskApiDispatchStopPost({
|
||||
taskId: websocketId,
|
||||
taskId: connection.websocketId,
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
// 取消订阅
|
||||
unsubscribe(websocketId)
|
||||
unsubscribe(connection.subscriptionId)
|
||||
activeConnections.value.delete(script.id)
|
||||
|
||||
// 隐藏遮罩
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { PlayCircleOutlined, StopOutlined } from '@ant-design/icons-vue'
|
||||
import { TaskCreateIn } from '@/api/models/TaskCreateIn'
|
||||
import type { ComboBoxItem } from '@/api/models/ComboBoxItem'
|
||||
@@ -88,11 +88,6 @@ const localSelectedMode = ref(props.selectedMode)
|
||||
// 模式选项
|
||||
const modeOptions = TASK_MODE_OPTIONS
|
||||
|
||||
// 计算属性
|
||||
const canStart = computed(() => {
|
||||
return !!(localSelectedTaskId.value && localSelectedMode.value) && !props.disabled
|
||||
})
|
||||
|
||||
// 监听 props 变化,同步到本地状态
|
||||
watch(
|
||||
() => props.selectedTaskId,
|
||||
|
||||
@@ -120,7 +120,6 @@
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { LockOutlined } from '@ant-design/icons-vue'
|
||||
import {
|
||||
getPowerActionText,
|
||||
POWER_ACTION_TEXT,
|
||||
TAB_STATUS_COLOR,
|
||||
} from './schedulerConstants'
|
||||
@@ -137,8 +136,6 @@ const {
|
||||
taskOptionsLoading,
|
||||
taskOptions,
|
||||
powerAction,
|
||||
powerCountdownVisible,
|
||||
powerCountdownData,
|
||||
messageModalVisible,
|
||||
currentMessage,
|
||||
messageResponse,
|
||||
@@ -160,7 +157,6 @@ const {
|
||||
|
||||
// 电源操作
|
||||
onPowerActionChange,
|
||||
cancelPowerAction,
|
||||
|
||||
// 消息操作
|
||||
sendMessageResponse,
|
||||
@@ -189,6 +185,13 @@ const onSchedulerTabEdit = (targetKey: string | MouseEvent, action: 'add' | 'rem
|
||||
onMounted(() => {
|
||||
initialize() // 初始化TaskManager订阅
|
||||
loadTaskOptions()
|
||||
|
||||
// 开发环境下导入调试工具
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
import('@/utils/scheduler-debug').then(() => {
|
||||
console.log('调度中心调试工具已加载,使用 debugScheduler() 和 testWebSocketConnection() 进行调试')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -78,6 +78,7 @@ export interface SchedulerTab {
|
||||
selectedTaskId: string | null
|
||||
selectedMode: TaskCreateIn.mode | null
|
||||
websocketId: string | null
|
||||
subscriptionId?: string | null
|
||||
taskQueue: QueueItem[]
|
||||
userQueue: QueueItem[]
|
||||
logs: LogEntry[]
|
||||
|
||||
@@ -230,8 +230,8 @@ export function useSchedulerLogic() {
|
||||
if (idx === -1) return
|
||||
|
||||
// 清理 WebSocket 订阅
|
||||
if (tab.websocketId) {
|
||||
ws.unsubscribe(tab.websocketId)
|
||||
if (tab.subscriptionId) {
|
||||
ws.unsubscribe(tab.subscriptionId)
|
||||
}
|
||||
|
||||
// 清理日志引用
|
||||
@@ -310,9 +310,13 @@ export function useSchedulerLogic() {
|
||||
const subscribeToTask = (tab: SchedulerTab) => {
|
||||
if (!tab.websocketId) return
|
||||
|
||||
ws.subscribe(tab.websocketId, {
|
||||
onMessage: (message) => handleWebSocketMessage(tab, message)
|
||||
})
|
||||
const subscriptionId = ws.subscribe(
|
||||
{ id: tab.websocketId },
|
||||
(message) => handleWebSocketMessage(tab, message)
|
||||
)
|
||||
|
||||
// 将订阅ID保存到tab中,以便后续取消订阅
|
||||
tab.subscriptionId = subscriptionId
|
||||
}
|
||||
|
||||
const handleWebSocketMessage = (tab: SchedulerTab, wsMessage: any) => {
|
||||
@@ -548,8 +552,12 @@ export function useSchedulerLogic() {
|
||||
console.log('[Scheduler] 已强制更新schedulerTabs,当前tabs状态:', schedulerTabs.value)
|
||||
}
|
||||
|
||||
if (tab.subscriptionId) {
|
||||
ws.unsubscribe(tab.subscriptionId)
|
||||
tab.subscriptionId = null
|
||||
}
|
||||
|
||||
if (tab.websocketId) {
|
||||
ws.unsubscribe(tab.websocketId)
|
||||
tab.websocketId = null
|
||||
}
|
||||
|
||||
@@ -823,8 +831,8 @@ export function useSchedulerLogic() {
|
||||
// 不再清理或重置导出的处理函数,保持使用者注册的处理逻辑永久有效
|
||||
|
||||
schedulerTabs.value.forEach(tab => {
|
||||
if (tab.websocketId) {
|
||||
ws.unsubscribe(tab.websocketId)
|
||||
if (tab.subscriptionId) {
|
||||
ws.unsubscribe(tab.subscriptionId)
|
||||
}
|
||||
})
|
||||
saveTabsToStorage(schedulerTabs.value)
|
||||
|
||||
Reference in New Issue
Block a user