fix: 调度中心ws订阅的相关方法在初始化时暴露
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: '调度中心' },
|
||||
},
|
||||
{
|
||||
|
||||
136
frontend/src/views/scheduler/schedulerHandlers.ts
Normal file
136
frontend/src/views/scheduler/schedulerHandlers.ts
Normal file
@@ -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,
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user