From 34df37c040363fb676a051382ec9c63dff2f683d Mon Sep 17 00:00:00 2001 From: DLmaster361 Date: Wed, 24 Sep 2025 21:43:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=B0=83=E5=BA=A6=E4=B8=AD=E5=BF=83ws?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E7=9A=84=E7=9B=B8=E5=85=B3=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=9C=A8=E5=88=9D=E5=A7=8B=E5=8C=96=E6=97=B6=E6=9A=B4=E9=9C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/composables/useWebSocket.ts | 61 +++++--- frontend/src/main.ts | 17 +++ frontend/src/router/index.ts | 4 +- .../src/views/scheduler/schedulerHandlers.ts | 136 ++++++++++++++++++ .../src/views/scheduler/useSchedulerLogic.ts | 63 +++++++- 5 files changed, 257 insertions(+), 24 deletions(-) create mode 100644 frontend/src/views/scheduler/schedulerHandlers.ts diff --git a/frontend/src/composables/useWebSocket.ts b/frontend/src/composables/useWebSocket.ts index d44ce09..1adeca6 100644 --- a/frontend/src/composables/useWebSocket.ts +++ b/frontend/src/composables/useWebSocket.ts @@ -1,4 +1,10 @@ import { ref, type Ref } from 'vue' +// 为了在调度中心 UI 未加载时仍能使用具体逻辑,直接同步导入 schedulerHandlers 的默认实现 +// schedulerHandlers 不再依赖 useWebSocket,因此同步导入不会产生致命循环依赖 +import schedulerHandlers from '@/views/scheduler/schedulerHandlers' +let _defaultHandlersLoaded = true +let _defaultTaskManagerHandler: (m: any) => void = schedulerHandlers.handleTaskManagerMessage +let _defaultMainHandler: (m: any) => void = schedulerHandlers.handleMainMessage import { Modal } from 'ant-design-vue' // 基础配置 @@ -576,18 +582,34 @@ window.addEventListener('beforeunload', () => { // 保持连接 }) -// 全局订阅处理函数 - 供调度台逻辑调用 -let globalTaskManagerHandler: ((message: any) => void) | null = null -let globalMainMessageHandler: ((message: any) => void) | null = null - -// 设置TaskManager消息处理函数 -export const setTaskManagerHandler = (handler: (message: any) => void) => { - globalTaskManagerHandler = handler -} - -// 设置Main消息处理函数 -export const setMainMessageHandler = (handler: (message: any) => void) => { - globalMainMessageHandler = handler +// 导出一个长期存在的可变对象,供外部通过 import 直接赋值/替换处理函数 +// 这样可以避免通过 window 或全局函数暴露,同时保证导入方始终能获取到该对象并设置回调,且不会被内部清理 +export const ExternalWSHandlers: { + mainMessage: (message: any) => void + taskManagerMessage: (message: any) => void +} = { + mainMessage: (message: any) => { + // 如果默认实现已加载,则调用之;否则保持空实现 + try { + if (_defaultHandlersLoaded && typeof _defaultMainHandler === 'function') { + _defaultMainHandler(message) + return + } + } catch (e) { + console.warn('[ExternalWSHandlers] default main handler error:', e) + } + // 未加载默认实现时保持空实现 + }, + taskManagerMessage: (message: any) => { + try { + if (_defaultHandlersLoaded && typeof _defaultTaskManagerHandler === 'function') { + _defaultTaskManagerHandler(message) + return + } + } catch (e) { + console.warn('[ExternalWSHandlers] default taskManager handler error:', e) + } + }, } // 初始化全局订阅 @@ -595,8 +617,11 @@ const initializeGlobalSubscriptions = () => { // 订阅TaskManager消息 _subscribe('TaskManager', { onMessage: (message) => { - if (globalTaskManagerHandler) { - globalTaskManagerHandler(message) + try { + ExternalWSHandlers.taskManagerMessage(message) + } catch (e) { + // 防御性处理,确保调用方异常不会影响消息通道 + console.warn('[WebSocket] External taskManagerMessage handler error:', e) } } }) @@ -633,9 +658,11 @@ const initializeGlobalSubscriptions = () => { } } - // 调用外部设置的Main消息处理函数 - if (globalMainMessageHandler) { - globalMainMessageHandler(message) + // 调用外部导入的 Main 消息处理函数(由使用者通过 import 并赋值给 ExternalWSHandlers) + try { + ExternalWSHandlers.mainMessage(message) + } catch (e) { + console.warn('[WebSocket] External mainMessage handler error:', e) } } }) diff --git a/frontend/src/main.ts b/frontend/src/main.ts index a7bd096..6eb571c 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -37,6 +37,23 @@ mirrorManager.initialize().then(() => { // 创建应用实例 const app = createApp(App) +// 提前初始化调度中心逻辑(使 handler 在前端初始化阶段就被注册) +try { + // 动态导入以避免循环引用问题 + const { useSchedulerLogic } = await import('./views/scheduler/useSchedulerLogic') + try { + const scheduler = useSchedulerLogic() + // 初始化并加载任务选项(不阻塞主流程,但希望尽早完成) + scheduler.initialize() + logger.info('Scheduler logic initialized at app startup') + } catch (e) { + logger.warn('Scheduler logic init failed at startup:', e) + } +} catch (e) { + // 如果导入失败(例如构建/路径问题),记录并继续,避免阻塞应用启动 + logger.warn('Failed to pre-import scheduler logic:', e) +} + // 注册插件 app.use(Antd) app.use(router) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 9cf1d0a..b0dcb72 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,5 +1,7 @@ import type { RouteRecordRaw } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router' +// 同步导入调度中心,保证其模块级导出(如 handler 注册点)在应用初始化时可用 +import SchedulerView from '../views/scheduler/index.vue' import { isAppInitialized } from '@/utils/config' let needInitLanding = true @@ -80,7 +82,7 @@ const routes: RouteRecordRaw[] = [ { path: '/scheduler', name: 'Scheduler', - component: () => import('../views/scheduler/index.vue'), + component: SchedulerView, meta: { title: '调度中心' }, }, { diff --git a/frontend/src/views/scheduler/schedulerHandlers.ts b/frontend/src/views/scheduler/schedulerHandlers.ts new file mode 100644 index 0000000..4c50033 --- /dev/null +++ b/frontend/src/views/scheduler/schedulerHandlers.ts @@ -0,0 +1,136 @@ +// schedulerHandlers.ts +// 提供在调度中心 UI 未加载前也能工作的消息处理实现。 +// 核心策略:将重要事件保存到 localStorage(或内存队列),并暴露注册点供 UI 在挂载时接收并回放。 + +// no types needed here to avoid circular/unused imports + +const PENDING_TABS_KEY = 'scheduler-pending-tabs' +const PENDING_COUNTDOWN_KEY = 'scheduler-pending-countdown' +const POWER_ACTION_KEY = 'scheduler-power-action' + +type UIHooks = { + onNewTab?: (tab: any) => void + onCountdown?: (data: any) => void +} + +let uiHooks: UIHooks = {} + +export function registerSchedulerUI(hooks: UIHooks) { + uiHooks = { ...uiHooks, ...hooks } +} + +// helper: push pending tab id to localStorage +function pushPendingTab(taskId: string) { + try { + const raw = localStorage.getItem(PENDING_TABS_KEY) + const arr = raw ? JSON.parse(raw) : [] + if (!arr.includes(taskId)) { + arr.push(taskId) + localStorage.setItem(PENDING_TABS_KEY, JSON.stringify(arr)) + } + } catch (e) { + // ignore + } +} + +function popPendingTabs(): string[] { + try { + const raw = localStorage.getItem(PENDING_TABS_KEY) + if (!raw) return [] + localStorage.removeItem(PENDING_TABS_KEY) + return JSON.parse(raw) + } catch (e) { + return [] + } +} + +function storePendingCountdown(data: any) { + try { + localStorage.setItem(PENDING_COUNTDOWN_KEY, JSON.stringify(data)) + } catch (e) { + // ignore + } +} + +function consumePendingCountdownAndClear(): any | null { + try { + const raw = localStorage.getItem(PENDING_COUNTDOWN_KEY) + if (!raw) return null + localStorage.removeItem(PENDING_COUNTDOWN_KEY) + return JSON.parse(raw) + } catch (e) { + return null + } +} + +function savePowerAction(value: string) { + try { + localStorage.setItem(POWER_ACTION_KEY, value) + } catch (e) { + // ignore + } +} + +// 导出:供 useWebSocket 在模块加载时就能使用的处理函数 +export function handleTaskManagerMessage(wsMessage: any) { + if (!wsMessage || typeof wsMessage !== 'object') return + const { type, data } = wsMessage + try { + if (type === 'Signal' && data && data.newTask) { + const taskId = String(data.newTask) + // 将任务 ID 写入 pending 队列,UI 在挂载时会回放 + pushPendingTab(taskId) + + // 如果 UI 已注册回调,则立即通知 + if (uiHooks.onNewTab) { + try { + uiHooks.onNewTab({ title: `调度台自动-${taskId}`, websocketId: taskId }) + } catch (e) { + console.warn('[SchedulerHandlers] onNewTab handler error:', e) + } + } + } + } catch (e) { + console.warn('[SchedulerHandlers] handleTaskManagerMessage error:', e) + } +} + +export function handleMainMessage(wsMessage: any) { + if (!wsMessage || typeof wsMessage !== 'object') return + const { type, data } = wsMessage + try { + if (type === 'Message' && data && data.type === 'Countdown') { + // 存储倒计时消息,供 UI 回放 + storePendingCountdown(data) + if (uiHooks.onCountdown) { + try { + uiHooks.onCountdown(data) + } catch (e) { + console.warn('[SchedulerHandlers] onCountdown handler error:', e) + } + } + } else if (type === 'Update' && data && data.PowerSign !== undefined) { + // 保存电源操作显示值(供 UI 加载时读取) + savePowerAction(String(data.PowerSign)) + } + } catch (e) { + console.warn('[SchedulerHandlers] handleMainMessage error:', e) + } +} + +// UI 在挂载时调用,消费并回放 pending 数据 +export function consumePendingTabIds(): string[] { + return popPendingTabs() +} + +export function consumePendingCountdown(): any | null { + return consumePendingCountdownAndClear() +} + +export default { + handleTaskManagerMessage, + handleMainMessage, + registerSchedulerUI, + consumePendingTabIds, + consumePendingCountdown, +} diff --git a/frontend/src/views/scheduler/useSchedulerLogic.ts b/frontend/src/views/scheduler/useSchedulerLogic.ts index 387b2cd..01711f0 100644 --- a/frontend/src/views/scheduler/useSchedulerLogic.ts +++ b/frontend/src/views/scheduler/useSchedulerLogic.ts @@ -3,7 +3,8 @@ import { message, Modal, notification } from 'ant-design-vue' import { Service } from '@/api/services/Service' import { TaskCreateIn } from '@/api/models/TaskCreateIn' import { PowerIn } from '@/api/models/PowerIn' -import { useWebSocket, setTaskManagerHandler, setMainMessageHandler } from '@/composables/useWebSocket' +import { useWebSocket, ExternalWSHandlers } from '@/composables/useWebSocket' +import schedulerHandlers from './schedulerHandlers' import type { ComboBoxItem } from '@/api/models/ComboBoxItem' import type { QueueItem, Script } from './schedulerConstants' import { @@ -184,7 +185,7 @@ export function useSchedulerLogic() { title: options?.title || `调度台${tabCounter}`, closable: true, status: validStatus, - selectedTaskId: null, + selectedTaskId: options?.websocketId || null, selectedMode: TaskCreateIn.mode.AutoMode, websocketId: options?.websocketId || null, taskQueue: [], @@ -767,10 +768,61 @@ export function useSchedulerLogic() { // 初始化函数 const initialize = () => { // 设置全局WebSocket的消息处理函数 - setTaskManagerHandler(handleTaskManagerMessage) - setMainMessageHandler(handleMainMessage) + // 通过 import 的 ExternalWSHandlers 直接注册处理函数,保证导入方能够永久引用并调用 + ExternalWSHandlers.taskManagerMessage = handleTaskManagerMessage + ExternalWSHandlers.mainMessage = handleMainMessage console.log('[Scheduler] 已设置全局WebSocket消息处理函数') + // 注册 UI hooks 到 schedulerHandlers,使其能在 schedulerHandlers 检测到 pending 时回放到当前 UI + try { + schedulerHandlers.registerSchedulerUI({ + onNewTab: (tab) => { + try { + // 创建并订阅新调度台 + const newTab = addSchedulerTab({ title: tab.title, status: '运行', websocketId: tab.websocketId }) + subscribeToTask(newTab) + saveTabsToStorage(schedulerTabs.value) + } catch (e) { + console.warn('[Scheduler] registerSchedulerUI onNewTab error:', e) + } + }, + onCountdown: (data) => { + try { + // 直接启动前端倒计时 + startPowerCountdown(data) + } catch (e) { + console.warn('[Scheduler] registerSchedulerUI onCountdown error:', e) + } + } + }) + + // 回放 pending tabs(如果有的话) + const pending = schedulerHandlers.consumePendingTabIds() + if (pending && pending.length > 0) { + pending.forEach((taskId: string) => { + try { + const newTab = addSchedulerTab({ title: `调度台${taskId}`, status: '运行', websocketId: taskId }) + subscribeToTask(newTab) + } catch (e) { + console.warn('[Scheduler] replay pending tab error:', e) + } + }) + saveTabsToStorage(schedulerTabs.value) + } + + // 回放 pending countdown(如果有的话) + const pendingCountdown = schedulerHandlers.consumePendingCountdown() + if (pendingCountdown) { + try { + startPowerCountdown(pendingCountdown) + } catch (e) { + console.warn('[Scheduler] replay pending countdown error:', e) + } + } + } catch (e) { + console.warn('[Scheduler] schedulerHandlers registration failed:', e) + } + // 新增:为已有的“运行中”标签恢复 WebSocket 订阅,防止路由切换返回后不再更新 try { schedulerTabs.value.forEach(tab => { @@ -811,8 +863,7 @@ export function useSchedulerLogic() { } // 清理全局WebSocket的消息处理函数 - setTaskManagerHandler(() => {}) - setMainMessageHandler(() => {}) + // 不再清理或重置导出的处理函数,保持使用者注册的处理逻辑永久有效 schedulerTabs.value.forEach(tab => { if (tab.websocketId) {