Merge branch 'feature/refactor' into v5.0.0-alpha.3
This commit is contained in:
427
docs/useWebSocket_API_Reference.md
Normal file
427
docs/useWebSocket_API_Reference.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# useWebSocket API 参考文档
|
||||
|
||||
## 概述
|
||||
|
||||
`useWebSocket()` 组合式函数提供了完整的 WebSocket 通信接口,包含消息订阅、连接管理、状态监控等核心功能。
|
||||
|
||||
## 导出函数详解
|
||||
|
||||
### 1. subscribe() - 订阅消息
|
||||
|
||||
```typescript
|
||||
const subscribe = (
|
||||
filter: SubscriptionFilter,
|
||||
handler: (message: WebSocketBaseMessage) => void
|
||||
): string
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
|
||||
**filter: SubscriptionFilter**
|
||||
```typescript
|
||||
interface SubscriptionFilter {
|
||||
type?: string // 消息类型过滤器(可选)
|
||||
id?: string // 消息ID过滤器(可选)
|
||||
needCache?: boolean // 是否启用缓存回放(可选)
|
||||
}
|
||||
```
|
||||
|
||||
**handler: Function**
|
||||
- 消息处理回调函数
|
||||
- 参数: `message: WebSocketBaseMessage`
|
||||
- 无返回值
|
||||
|
||||
#### 返回值
|
||||
- `string`: 唯一的订阅ID,用于后续取消订阅
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// 订阅所有消息
|
||||
const allMsgSub = subscribe({}, (msg) => {
|
||||
console.log('收到消息:', msg)
|
||||
})
|
||||
|
||||
// 订阅特定类型消息
|
||||
const taskSub = subscribe(
|
||||
{ type: 'TaskUpdate', needCache: true },
|
||||
(msg) => {
|
||||
console.log('任务更新:', msg.data)
|
||||
}
|
||||
)
|
||||
|
||||
// 订阅特定ID消息
|
||||
const specificSub = subscribe(
|
||||
{ id: 'TaskManager' },
|
||||
(msg) => {
|
||||
console.log('任务管理器消息:', msg)
|
||||
}
|
||||
)
|
||||
|
||||
// 精确订阅(同时匹配type和id)
|
||||
const preciseSub = subscribe(
|
||||
{ type: 'Progress', id: 'task_001', needCache: true },
|
||||
(msg) => {
|
||||
console.log('特定任务进度:', msg.data)
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### 特殊功能
|
||||
- **自动回放**: 如果 `needCache: true`,会立即回放匹配的历史消息
|
||||
- **过滤优先级**: type + id > type > id > 全部
|
||||
- **引用计数**: 多个订阅共享缓存,自动管理内存
|
||||
|
||||
---
|
||||
|
||||
### 2. unsubscribe() - 取消订阅
|
||||
|
||||
```typescript
|
||||
const unsubscribe = (subscriptionId: string): void
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
|
||||
**subscriptionId: string**
|
||||
- 由 `subscribe()` 返回的订阅ID
|
||||
- 必须是有效的订阅ID
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
const subId = subscribe({ type: 'TaskUpdate' }, handleTaskUpdate)
|
||||
|
||||
// 取消订阅
|
||||
unsubscribe(subId)
|
||||
|
||||
// Vue 组件中的最佳实践
|
||||
import { onUnmounted } from 'vue'
|
||||
|
||||
const setupSubscription = () => {
|
||||
const subId = subscribe({ type: 'TaskUpdate' }, handleTaskUpdate)
|
||||
|
||||
onUnmounted(() => {
|
||||
unsubscribe(subId)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 自动清理
|
||||
- 自动减少缓存引用计数
|
||||
- 引用计数为0时清理相关缓存
|
||||
- 不会影响其他订阅者
|
||||
|
||||
---
|
||||
|
||||
### 3. sendRaw() - 发送消息
|
||||
|
||||
```typescript
|
||||
const sendRaw = (type: string, data?: any, id?: string): void
|
||||
```
|
||||
|
||||
#### 参数说明
|
||||
|
||||
**type: string** (必需)
|
||||
- 消息类型标识
|
||||
- 后端用于路由消息
|
||||
|
||||
**data: any** (可选)
|
||||
- 消息负载数据
|
||||
- 可以是任何可序列化的对象
|
||||
|
||||
**id: string** (可选)
|
||||
- 消息标识符
|
||||
- 用于消息跟踪和响应匹配
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
// 发送简单消息
|
||||
sendRaw('Hello')
|
||||
|
||||
// 发送带数据的消息
|
||||
sendRaw('TaskStart', {
|
||||
taskId: '12345',
|
||||
config: { timeout: 30000 }
|
||||
})
|
||||
|
||||
// 发送带ID的消息(便于追踪响应)
|
||||
sendRaw('GetTaskStatus', { taskId: '12345' }, 'query_001')
|
||||
|
||||
// 发送控制信号
|
||||
sendRaw('Signal', {
|
||||
command: 'pause',
|
||||
reason: '用户手动暂停'
|
||||
}, 'TaskManager')
|
||||
```
|
||||
|
||||
#### 发送条件
|
||||
- 仅在 WebSocket 连接为 `OPEN` 状态时发送
|
||||
- 连接异常时静默失败(不抛出异常)
|
||||
- 自动JSON序列化
|
||||
|
||||
---
|
||||
|
||||
### 4. getConnectionInfo() - 获取连接信息
|
||||
|
||||
```typescript
|
||||
const getConnectionInfo = (): ConnectionInfo
|
||||
```
|
||||
|
||||
#### 返回值类型
|
||||
|
||||
```typescript
|
||||
interface ConnectionInfo {
|
||||
connectionId: string // 连接唯一标识
|
||||
status: WebSocketStatus // 当前连接状态
|
||||
subscriberCount: number // 当前订阅者数量
|
||||
moduleLoadCount: number // 模块加载计数
|
||||
wsReadyState: number | null // WebSocket原生状态
|
||||
isConnecting: boolean // 是否正在连接
|
||||
hasHeartbeat: boolean // 是否启用心跳
|
||||
hasEverConnected: boolean // 是否曾经连接成功
|
||||
reconnectAttempts: number // 重连尝试次数
|
||||
isPersistentMode: boolean // 是否持久化模式
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
const info = getConnectionInfo()
|
||||
|
||||
console.log('连接ID:', info.connectionId)
|
||||
console.log('连接状态:', info.status)
|
||||
console.log('订阅者数量:', info.subscriberCount)
|
||||
|
||||
// 检查连接是否健康
|
||||
const isHealthy = info.status === '已连接' &&
|
||||
info.hasHeartbeat &&
|
||||
info.wsReadyState === WebSocket.OPEN
|
||||
|
||||
// 监控重连情况
|
||||
if (info.reconnectAttempts > 0) {
|
||||
console.log(`已重连 ${info.reconnectAttempts} 次`)
|
||||
}
|
||||
```
|
||||
|
||||
#### 调试用途
|
||||
- 诊断连接问题
|
||||
- 监控连接质量
|
||||
- 统计使用情况
|
||||
|
||||
---
|
||||
|
||||
### 5. status - 连接状态
|
||||
|
||||
```typescript
|
||||
const status: Ref<WebSocketStatus>
|
||||
```
|
||||
|
||||
#### 状态类型
|
||||
|
||||
```typescript
|
||||
type WebSocketStatus = '连接中' | '已连接' | '已断开' | '连接错误'
|
||||
```
|
||||
|
||||
#### 状态说明
|
||||
|
||||
| 状态 | 描述 | 触发条件 |
|
||||
|------|------|----------|
|
||||
| `'连接中'` | 正在建立连接 | WebSocket.CONNECTING |
|
||||
| `'已连接'` | 连接成功 | WebSocket.OPEN |
|
||||
| `'已断开'` | 连接断开 | WebSocket.CLOSED |
|
||||
| `'连接错误'` | 连接异常 | WebSocket.onerror |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
import { watch } from 'vue'
|
||||
|
||||
const { status } = useWebSocket()
|
||||
|
||||
// 监听状态变化
|
||||
watch(status, (newStatus) => {
|
||||
console.log('连接状态变化:', newStatus)
|
||||
|
||||
switch (newStatus) {
|
||||
case '已连接':
|
||||
console.log('✅ WebSocket 连接成功')
|
||||
break
|
||||
case '已断开':
|
||||
console.log('❌ WebSocket 连接断开')
|
||||
break
|
||||
case '连接错误':
|
||||
console.log('⚠️ WebSocket 连接错误')
|
||||
break
|
||||
case '连接中':
|
||||
console.log('🔄 WebSocket 连接中...')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// 在模板中显示状态
|
||||
// <div>连接状态: {{ status }}</div>
|
||||
```
|
||||
|
||||
#### 响应式特性
|
||||
- Vue 响应式 Ref 对象
|
||||
- 自动更新 UI
|
||||
- 可用于计算属性和监听器
|
||||
|
||||
---
|
||||
|
||||
### 6. backendStatus - 后端状态
|
||||
|
||||
```typescript
|
||||
const backendStatus: Ref<BackendStatus>
|
||||
```
|
||||
|
||||
#### 状态类型
|
||||
|
||||
```typescript
|
||||
type BackendStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error'
|
||||
```
|
||||
|
||||
#### 状态说明
|
||||
|
||||
| 状态 | 描述 | 含义 |
|
||||
|------|------|------|
|
||||
| `'unknown'` | 未知状态 | 初始状态,尚未检测 |
|
||||
| `'starting'` | 启动中 | 后端服务正在启动 |
|
||||
| `'running'` | 运行中 | 后端服务正常运行 |
|
||||
| `'stopped'` | 已停止 | 后端服务已停止 |
|
||||
| `'error'` | 错误状态 | 后端服务异常 |
|
||||
|
||||
#### 使用示例
|
||||
|
||||
```typescript
|
||||
const { backendStatus, restartBackend } = useWebSocket()
|
||||
|
||||
// 监听后端状态
|
||||
watch(backendStatus, (newStatus) => {
|
||||
console.log('后端状态:', newStatus)
|
||||
|
||||
switch (newStatus) {
|
||||
case 'running':
|
||||
console.log('✅ 后端服务运行正常')
|
||||
break
|
||||
case 'stopped':
|
||||
console.log('⏹️ 后端服务已停止')
|
||||
break
|
||||
case 'error':
|
||||
console.log('❌ 后端服务异常')
|
||||
// 可以提示用户或自动重启
|
||||
break
|
||||
case 'starting':
|
||||
console.log('🚀 后端服务启动中...')
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// 根据状态显示不同UI
|
||||
const statusColor = computed(() => {
|
||||
switch (backendStatus.value) {
|
||||
case 'running': return 'green'
|
||||
case 'error': return 'red'
|
||||
case 'starting': return 'orange'
|
||||
default: return 'gray'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
#### 自动管理
|
||||
- 每3秒自动检测一次
|
||||
- 异常时自动尝试重启(最多3次)
|
||||
- 集成 Electron 进程管理
|
||||
|
||||
---
|
||||
|
||||
## 完整使用示例
|
||||
|
||||
```typescript
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const {
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
sendRaw,
|
||||
getConnectionInfo,
|
||||
status,
|
||||
backendStatus
|
||||
} = useWebSocket()
|
||||
|
||||
let taskSubscription: string
|
||||
let systemSubscription: string
|
||||
|
||||
onMounted(() => {
|
||||
// 订阅任务消息
|
||||
taskSubscription = subscribe(
|
||||
{ type: 'TaskUpdate', needCache: true },
|
||||
(message) => {
|
||||
console.log('任务更新:', message.data)
|
||||
}
|
||||
)
|
||||
|
||||
// 订阅系统消息
|
||||
systemSubscription = subscribe(
|
||||
{ id: 'System' },
|
||||
(message) => {
|
||||
console.log('系统消息:', message)
|
||||
}
|
||||
)
|
||||
|
||||
// 发送初始化消息
|
||||
sendRaw('ClientReady', {
|
||||
timestamp: Date.now()
|
||||
}, 'System')
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理订阅
|
||||
if (taskSubscription) unsubscribe(taskSubscription)
|
||||
if (systemSubscription) unsubscribe(systemSubscription)
|
||||
})
|
||||
|
||||
// 监听连接状态
|
||||
watch([status, backendStatus], ([wsStatus, beStatus]) => {
|
||||
console.log(`WS: ${wsStatus}, Backend: ${beStatus}`)
|
||||
})
|
||||
|
||||
// 获取连接信息
|
||||
const connectionInfo = getConnectionInfo()
|
||||
|
||||
return {
|
||||
status,
|
||||
backendStatus,
|
||||
connectionInfo,
|
||||
sendMessage: (type: string, data: any) => sendRaw(type, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 订阅管理
|
||||
- 总是在组件卸载时取消订阅
|
||||
- 使用 `needCache: true` 确保不丢失消息
|
||||
- 避免重复订阅相同的消息类型
|
||||
|
||||
### 2. 错误处理
|
||||
- 监听连接状态变化
|
||||
- 根据后端状态调整UI显示
|
||||
- 实现重连提示和手动重启
|
||||
|
||||
### 3. 性能优化
|
||||
- 精确的过滤条件减少不必要的处理
|
||||
- 合理使用缓存避免消息丢失
|
||||
- 及时取消不需要的订阅
|
||||
|
||||
### 4. 调试技巧
|
||||
- 使用 `getConnectionInfo()` 诊断问题
|
||||
- 开发环境下查看控制台日志
|
||||
- 监控订阅者数量避免内存泄漏
|
||||
336
docs/useWebSocket_Analysis.md
Normal file
336
docs/useWebSocket_Analysis.md
Normal file
@@ -0,0 +1,336 @@
|
||||
## 核心架构设计
|
||||
|
||||
### 1. 全局持久化存储
|
||||
|
||||
```typescript
|
||||
const WS_STORAGE_KEY = Symbol.for('GLOBAL_WEBSOCKET_PERSISTENT')
|
||||
```
|
||||
|
||||
- 使用 `Symbol.for()` 确保全局唯一性
|
||||
- 存储在 `window` 对象上,实现跨组件共享
|
||||
- 避免多次实例化,确保连接唯一性
|
||||
|
||||
### 2. 状态管理结构
|
||||
|
||||
```typescript
|
||||
interface GlobalWSStorage {
|
||||
wsRef: WebSocket | null // WebSocket 实例
|
||||
status: Ref<WebSocketStatus> // 连接状态
|
||||
subscriptions: Ref<Map<string, WebSocketSubscription>> // 订阅管理
|
||||
cacheMarkers: Ref<Map<string, CacheMarker>> // 缓存标记
|
||||
cachedMessages: Ref<Array<CachedMessage>> // 消息缓存
|
||||
// ... 其他状态
|
||||
}
|
||||
```
|
||||
|
||||
## 核心功能模块
|
||||
|
||||
### 1. 配置管理
|
||||
|
||||
```typescript
|
||||
const BASE_WS_URL = 'ws://localhost:36163/api/core/ws'
|
||||
const HEARTBEAT_INTERVAL = 15000 // 心跳间隔
|
||||
const HEARTBEAT_TIMEOUT = 5000 // 心跳超时
|
||||
const BACKEND_CHECK_INTERVAL = 3000 // 后端检查间隔
|
||||
const MAX_RESTART_ATTEMPTS = 3 // 最大重启尝试次数
|
||||
const RESTART_DELAY = 2000 // 重启延迟
|
||||
const MAX_QUEUE_SIZE = 50 // 最大队列大小
|
||||
const MESSAGE_TTL = 60000 // 消息过期时间
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 所有时间配置使用毫秒为单位
|
||||
- 可根据网络环境调整超时时间
|
||||
- 队列大小限制防止内存泄漏
|
||||
|
||||
### 2. 消息订阅系统
|
||||
|
||||
#### 订阅过滤器
|
||||
```typescript
|
||||
interface SubscriptionFilter {
|
||||
type?: string // 消息类型过滤
|
||||
id?: string // 消息ID过滤
|
||||
needCache?: boolean // 是否需要缓存
|
||||
}
|
||||
```
|
||||
|
||||
#### 订阅机制
|
||||
```typescript
|
||||
export const subscribe = (
|
||||
filter: SubscriptionFilter,
|
||||
handler: (message: WebSocketBaseMessage) => void
|
||||
): string => {
|
||||
// 1. 生成唯一订阅ID
|
||||
// 2. 创建订阅记录
|
||||
// 3. 添加缓存标记
|
||||
// 4. 回放匹配的缓存消息
|
||||
}
|
||||
```
|
||||
|
||||
**要点**:
|
||||
- 支持按 `type` 和 `id` 的组合过滤
|
||||
- 自动回放缓存消息,确保不丢失历史数据
|
||||
- 返回订阅ID用于后续取消订阅
|
||||
|
||||
### 3. 智能缓存系统
|
||||
|
||||
#### 缓存标记机制
|
||||
```typescript
|
||||
interface CacheMarker {
|
||||
type?: string
|
||||
id?: string
|
||||
refCount: number // 引用计数
|
||||
}
|
||||
```
|
||||
|
||||
#### 缓存策略
|
||||
- **引用计数**: 订阅时 +1,取消订阅时 -1
|
||||
- **自动清理**: 引用计数为 0 时删除标记
|
||||
- **TTL机制**: 消息超过 60 秒自动过期
|
||||
- **大小限制**: 每个队列最多保留 50 条消息
|
||||
|
||||
|
||||
### 4. 心跳检测机制
|
||||
|
||||
```typescript
|
||||
const startGlobalHeartbeat = (ws: WebSocket) => {
|
||||
global.heartbeatTimer = window.setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
const pingTime = Date.now()
|
||||
global.lastPingTime = pingTime
|
||||
ws.send(JSON.stringify({
|
||||
type: 'Signal',
|
||||
data: { Ping: pingTime, connectionId: global.connectionId }
|
||||
}))
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 5. 后端服务监控
|
||||
|
||||
#### 状态检测
|
||||
```typescript
|
||||
type BackendStatus = 'unknown' | 'starting' | 'running' | 'stopped' | 'error'
|
||||
```
|
||||
|
||||
#### 自动重启逻辑
|
||||
```typescript
|
||||
const restartBackend = async (): Promise<boolean> => {
|
||||
// 1. 防重入检查
|
||||
// 2. 递增重启计数
|
||||
// 3. 调用 Electron API 启动后端
|
||||
// 4. 更新状态
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 6. 连接控制机制
|
||||
|
||||
#### 连接权限控制
|
||||
```typescript
|
||||
const allowedConnectionReasons = ['后端启动后连接', '后端重启后重连']
|
||||
const checkConnectionPermission = () => getGlobalStorage().allowNewConnection
|
||||
```
|
||||
|
||||
#### 连接锁机制
|
||||
```typescript
|
||||
let isGlobalConnectingLock = false
|
||||
const acquireConnectionLock = () => {
|
||||
if (isGlobalConnectingLock) return false
|
||||
isGlobalConnectingLock = true
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
**AI 开发要点**:
|
||||
- 防止并发连接导致的竞态条件
|
||||
- 只允许特定原因的连接请求
|
||||
- 确保全局唯一连接
|
||||
|
||||
## 消息流处理
|
||||
|
||||
### 1. 消息匹配算法
|
||||
|
||||
```typescript
|
||||
const messageMatchesFilter = (message: WebSocketBaseMessage, filter: SubscriptionFilter): boolean => {
|
||||
// 如果都不指定,匹配所有消息
|
||||
if (!filter.type && !filter.id) return true
|
||||
|
||||
// 如果只指定type
|
||||
if (filter.type && !filter.id) return message.type === filter.type
|
||||
|
||||
// 如果只指定id
|
||||
if (!filter.type && filter.id) return message.id === filter.id
|
||||
|
||||
// 如果同时指定type和id,必须都匹配
|
||||
return message.type === filter.type && message.id === filter.id
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 消息分发流程
|
||||
|
||||
```
|
||||
WebSocket 接收消息
|
||||
↓
|
||||
JSON 解析
|
||||
↓
|
||||
遍历所有订阅者
|
||||
↓
|
||||
匹配过滤条件
|
||||
↓
|
||||
调用处理器函数
|
||||
↓
|
||||
检查是否需要缓存
|
||||
↓
|
||||
添加到缓存队列
|
||||
```
|
||||
|
||||
## 外部接口设计
|
||||
|
||||
### 1. 主要导出函数
|
||||
|
||||
```typescript
|
||||
export function useWebSocket() {
|
||||
return {
|
||||
subscribe, // 订阅消息
|
||||
unsubscribe, // 取消订阅
|
||||
sendRaw, // 发送消息
|
||||
getConnectionInfo, // 获取连接信息
|
||||
status, // 连接状态
|
||||
backendStatus, // 后端状态
|
||||
restartBackend, // 重启后端
|
||||
getBackendStatus, // 获取后端状态
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 特殊接口
|
||||
|
||||
```typescript
|
||||
export const connectAfterBackendStart = async (): Promise<boolean>
|
||||
```
|
||||
- 后端启动后的连接入口
|
||||
- 启动后端监控
|
||||
- 设置连接权限
|
||||
|
||||
## 错误处理策略
|
||||
|
||||
### 1. 连接错误处理
|
||||
- WebSocket 连接失败时设置状态为 '连接错误'
|
||||
- 通过后端监控检测服务状态
|
||||
- 自动重启后端服务
|
||||
|
||||
### 2. 消息处理错误
|
||||
- 订阅处理器异常时记录警告但不中断其他订阅者
|
||||
- JSON 解析失败时静默忽略
|
||||
- 发送消息失败时静默处理
|
||||
|
||||
### 3. 后端故障处理
|
||||
```typescript
|
||||
const handleBackendFailure = async () => {
|
||||
if (global.backendRestartAttempts >= MAX_RESTART_ATTEMPTS) {
|
||||
// 显示错误对话框,提示重启应用
|
||||
Modal.error({
|
||||
title: '后端服务异常',
|
||||
content: '后端服务多次重启失败,请重启整个应用程序。'
|
||||
})
|
||||
return
|
||||
}
|
||||
// 自动重启逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 调试和监控
|
||||
|
||||
### 1. 调试模式
|
||||
```typescript
|
||||
const DEBUG = process.env.NODE_ENV === 'development'
|
||||
const log = (...args: any[]) => {
|
||||
if (DEBUG) console.log('[WebSocket]', ...args)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 连接信息监控
|
||||
```typescript
|
||||
const getConnectionInfo = () => ({
|
||||
connectionId: global.connectionId,
|
||||
status: global.status.value,
|
||||
subscriberCount: global.subscriptions.value.size,
|
||||
moduleLoadCount: global.moduleLoadCount,
|
||||
wsReadyState: global.wsRef ? global.wsRef.readyState : null,
|
||||
isConnecting: global.isConnecting,
|
||||
hasHeartbeat: !!global.heartbeatTimer,
|
||||
hasEverConnected: global.hasEverConnected,
|
||||
reconnectAttempts: global.reconnectAttempts,
|
||||
isPersistentMode: true,
|
||||
})
|
||||
```
|
||||
|
||||
## AI 开发建议
|
||||
|
||||
### 1. 使用模式
|
||||
```typescript
|
||||
// 在 Vue 组件中使用
|
||||
const { subscribe, unsubscribe, sendRaw, status } = useWebSocket()
|
||||
|
||||
// 订阅特定类型消息
|
||||
const subId = subscribe(
|
||||
{ type: 'TaskUpdate', needCache: true },
|
||||
(message) => {
|
||||
console.log('收到任务更新:', message.data)
|
||||
}
|
||||
)
|
||||
|
||||
// 组件卸载时取消订阅
|
||||
onUnmounted(() => {
|
||||
unsubscribe(subId)
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 扩展建议
|
||||
- 添加消息重试机制
|
||||
- 实现消息优先级队列
|
||||
- 支持消息压缩
|
||||
- 添加连接质量监控
|
||||
|
||||
### 3. 性能优化点
|
||||
- 使用 `Object.freeze()` 冻结配置对象
|
||||
- 考虑使用 Web Worker 处理大量消息
|
||||
- 实现消息批处理机制
|
||||
- 添加消息去重功能
|
||||
|
||||
### 4. 安全考虑
|
||||
- 验证消息来源
|
||||
- 实现消息签名机制
|
||||
- 添加连接认证
|
||||
- 防止消息注入攻击
|
||||
|
||||
## 依赖关系
|
||||
|
||||
### 1. 外部依赖
|
||||
- `vue`: 响应式系统和组合式API
|
||||
- `ant-design-vue`: UI组件库(Modal)
|
||||
- `schedulerHandlers`: 默认消息处理器
|
||||
|
||||
### 2. 运行时依赖
|
||||
- `window.electronAPI`: Electron主进程通信
|
||||
- WebSocket API: 浏览器原生支持
|
||||
|
||||
## 总结
|
||||
|
||||
这个 WebSocket 组合式函数是一个功能完整、设计精良的实时通信解决方案。它不仅解决了基本的 WebSocket 连接问题,还提供了高级功能如智能缓存、自动重连、后端监控等。
|
||||
|
||||
**核心优势**:
|
||||
1. 全局持久化连接,避免重复建立
|
||||
2. 智能订阅系统,支持精确过滤
|
||||
3. 自动缓存回放,确保数据完整性
|
||||
4. 完善的错误处理和自动恢复
|
||||
5. 详细的调试和监控信息
|
||||
|
||||
**适用场景**:
|
||||
- 实时数据展示
|
||||
- 任务状态监控
|
||||
- 系统通知推送
|
||||
- 双向通信应用
|
||||
@@ -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