diff --git a/package.json b/package.json index 8bbeece2..432486dd 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "image-conversion": "2.1.1", "js-cookie": "3.0.5", "jsencrypt": "3.5.4", + "mitt": "^3.0.1", "nprogress": "0.2.0", "pinia": "3.0.4", "screenfull": "6.0.2", diff --git a/src/utils/sse.ts b/src/utils/sse.ts index 059c8f82..dbd3b500 100644 --- a/src/utils/sse.ts +++ b/src/utils/sse.ts @@ -1,42 +1,102 @@ import { getToken } from '@/utils/auth'; import { ElNotification } from 'element-plus'; import { useNoticeStore } from '@/store/modules/notice'; +import { getSseEventBus } from '@/utils/sseEventBus'; + +/** + * Strongly-typed SSE envelope shared by every event: + * { msgType: "...", data: { ...business fields... } } + * Frontend dispatches by msgType, then casts data to the corresponding shape. + */ +interface SseEnvelope { + msgType: string; + data: T; +} + +/** data shape under msgType in {LOGIN_WELCOME, WORKFLOW_TASK, SYSTEM_NOTICE} */ +interface NoticeData { + title: string; + message: string; +} + +// Business-level SSE event names forwarded to the global event bus. +// Feature modules subscribe via getSseEventBus().on('xxx', handler). +const FORWARD_EVENTS = ['customerservice']; + +const MAX_RETRIES = 5; +const RETRY_DELAY = 5000; + +let eventSource: EventSource | null = null; +let retries = 0; -// 初始化 export const initSSE = (url: any) => { if (import.meta.env.VITE_APP_SSE === 'false') { return; } + connect(url); +}; - url = url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID; - const { data, error } = useEventSource(url, [], { - autoReconnect: { - retries: 5, - delay: 5000, - onFailed() { - console.log('Failed to connect after 5 retries'); - } +function parseEnvelope(raw: string): SseEnvelope | null { + try { + const env = JSON.parse(raw); + if (env && typeof env === 'object' && typeof env.msgType === 'string') { + return env as SseEnvelope; } - }); + console.warn('SSE: payload missing msgType', raw); + return null; + } catch (err) { + console.warn('SSE: invalid JSON envelope', raw, err); + return null; + } +} - watch(error, () => { - console.log('SSE connection error:', error.value); - error.value = null; - }); +function connect(url: string) { + const fullUrl = url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID; + eventSource = new EventSource(fullUrl); - watch(data, () => { - if (!data.value) return; + // "notice" event: login welcome / workflow task / system announcement. + // All three share NoticeData shape; msgType differentiates the source. + eventSource.addEventListener('notice', (e: MessageEvent) => { + if (!e.data) return; + const env = parseEnvelope(e.data); + if (!env) return; + const { title, message } = env.data; useNoticeStore().addNotice({ - message: data.value, + title, + message, read: false, time: new Date().toLocaleString() }); ElNotification({ - title: '消息', - message: data.value, + title, + message, type: 'success', duration: 3000 }); - data.value = null; }); -}; + + // Business events: forward to global bus, feature modules subscribe themselves + const bus = getSseEventBus(); + FORWARD_EVENTS.forEach((name) => { + eventSource!.addEventListener(name, (e: MessageEvent) => { + bus.emit(name, e.data); + }); + }); + + eventSource.onopen = () => { + retries = 0; + console.log('SSE connected'); + }; + + eventSource.onerror = (err) => { + console.log('SSE connection error:', err); + eventSource?.close(); + eventSource = null; + if (retries < MAX_RETRIES) { + retries += 1; + setTimeout(() => connect(url), RETRY_DELAY); + } else { + console.log('SSE failed after', MAX_RETRIES, 'retries'); + } + }; +} diff --git a/src/utils/sseEventBus.ts b/src/utils/sseEventBus.ts new file mode 100644 index 00000000..9fea3314 --- /dev/null +++ b/src/utils/sseEventBus.ts @@ -0,0 +1,21 @@ +import mitt, { Emitter } from 'mitt'; + +/** + * SSE event bus events shape: protocol-level event name -> raw data string + * Consumers JSON.parse the payload themselves + */ +export type SseBusEvents = Record; + +let bus: Emitter | null = null; + +/** + * Lazily-initialized singleton event bus + * Used by sse.ts to forward business event names (e.g. "customerservice") + * to feature modules without creating a second EventSource + */ +export function getSseEventBus(): Emitter { + if (!bus) { + bus = mitt(); + } + return bus; +}