From 9a17d63de2ca64b33c059a4584bdeaccb1ee3c75 Mon Sep 17 00:00:00 2001 From: i548450 Date: Tue, 9 Jun 2026 11:23:23 +0800 Subject: [PATCH 1/4] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20mitt=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E4=BE=9B=20SSE=20=E4=BA=8B=E4=BB=B6=E6=80=BB?= =?UTF-8?q?=E7=BA=BF=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + 1 file changed, 1 insertion(+) 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", From 0d03699fcf3a922e296e9144402c9dc510abf3f6 Mon Sep 17 00:00:00 2001 From: i548450 Date: Tue, 9 Jun 2026 11:27:39 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor(sse):=20=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E5=8E=9F=E7=94=9F=20EventSource=20+=20=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E6=80=BB=E7=BA=BF,=E6=94=AF=E6=8C=81=E5=A4=9A=20event=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 utils/sseEventBus.ts mitt 单例 - sse.ts 从 useEventSource 改为原生 EventSource,显式 addEventListener - 'message' event 沿用旧通知行为(noticeStore + ElNotification) - FORWARD_EVENTS(目前 customerservice)转发到事件总线供业务模块订阅 --- src/utils/sse.ts | 69 +++++++++++++++++++++++++++------------- src/utils/sseEventBus.ts | 21 ++++++++++++ 2 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 src/utils/sseEventBus.ts diff --git a/src/utils/sse.ts b/src/utils/sse.ts index 059c8f82..5669c477 100644 --- a/src/utils/sse.ts +++ b/src/utils/sse.ts @@ -1,42 +1,67 @@ import { getToken } from '@/utils/auth'; import { ElNotification } from 'element-plus'; import { useNoticeStore } from '@/store/modules/notice'; +import { getSseEventBus } from '@/utils/sseEventBus'; + +// 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 connect(url: string) { + const fullUrl = url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID; + eventSource = new EventSource(fullUrl); - watch(error, () => { - console.log('SSE connection error:', error.value); - error.value = null; - }); - - watch(data, () => { - if (!data.value) return; + // Legacy "message" event: keep existing notification behavior + eventSource.addEventListener('message', (e: MessageEvent) => { + if (!e.data) return; useNoticeStore().addNotice({ - message: data.value, + message: e.data, read: false, time: new Date().toLocaleString() }); ElNotification({ title: '消息', - message: data.value, + message: e.data, 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; +} From 6ac60c720164504db78ccf14c20a14230a17441e Mon Sep 17 00:00:00 2001 From: i548450 Date: Tue, 9 Jun 2026 12:21:21 +0800 Subject: [PATCH 3/4] =?UTF-8?q?refactor(sse)!:=20'message'=20event=20?= =?UTF-8?q?=E2=86=92=20'notice'=20event=20with=20strongly-typed=20JSON=20p?= =?UTF-8?q?ayload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 'notice' event handler parses JSON {type, title, message} → noticeStore + ElNotification - Backend 老 3 处调用全部改为发 NoticePayload 强类型(LOGIN_WELCOME/WORKFLOW_TASK/SYSTEM_NOTICE),前端按 type 字段二次分发预留 --- src/utils/sse.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/utils/sse.ts b/src/utils/sse.ts index 5669c477..d1463cc2 100644 --- a/src/utils/sse.ts +++ b/src/utils/sse.ts @@ -3,6 +3,17 @@ import { ElNotification } from 'element-plus'; import { useNoticeStore } from '@/store/modules/notice'; import { getSseEventBus } from '@/utils/sseEventBus'; +/** + * Strongly-typed payload of the unified "notice" SSE event. + * Backend sends one of: LOGIN_WELCOME, WORKFLOW_TASK, SYSTEM_NOTICE + * (all share the same shape — type field for dispatching, title + message for display). + */ +interface NoticePayload { + type: string; + 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']; @@ -24,17 +35,25 @@ function connect(url: string) { const fullUrl = url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID; eventSource = new EventSource(fullUrl); - // Legacy "message" event: keep existing notification behavior - eventSource.addEventListener('message', (e: MessageEvent) => { + // "notice" event: login welcome / workflow / system announcement — strongly-typed JSON payload + eventSource.addEventListener('notice', (e: MessageEvent) => { if (!e.data) return; + let payload: NoticePayload; + try { + payload = JSON.parse(e.data); + } catch (err) { + console.warn('SSE notice: invalid JSON payload', e.data, err); + return; + } useNoticeStore().addNotice({ - message: e.data, + title: payload.title, + message: payload.message, read: false, time: new Date().toLocaleString() }); ElNotification({ - title: '消息', - message: e.data, + title: payload.title, + message: payload.message, type: 'success', duration: 3000 }); From 5d2d9a6f61b74e694fd953c3180058212cd2309e Mon Sep 17 00:00:00 2001 From: i548450 Date: Tue, 9 Jun 2026 13:04:08 +0800 Subject: [PATCH 4/4] =?UTF-8?q?refactor(sse):=20wire=20=E5=BD=A2=E6=80=81?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20SseEnvelope{msgType,=20data}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 'notice' event handler 解析 envelope,从 data 取 title/message - 抽 parseEnvelope 通用函数,校验 msgType 必填 --- src/utils/sse.ts | 50 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/utils/sse.ts b/src/utils/sse.ts index d1463cc2..dbd3b500 100644 --- a/src/utils/sse.ts +++ b/src/utils/sse.ts @@ -4,12 +4,17 @@ import { useNoticeStore } from '@/store/modules/notice'; import { getSseEventBus } from '@/utils/sseEventBus'; /** - * Strongly-typed payload of the unified "notice" SSE event. - * Backend sends one of: LOGIN_WELCOME, WORKFLOW_TASK, SYSTEM_NOTICE - * (all share the same shape — type field for dispatching, title + message for display). + * Strongly-typed SSE envelope shared by every event: + * { msgType: "...", data: { ...business fields... } } + * Frontend dispatches by msgType, then casts data to the corresponding shape. */ -interface NoticePayload { - type: string; +interface SseEnvelope { + msgType: string; + data: T; +} + +/** data shape under msgType in {LOGIN_WELCOME, WORKFLOW_TASK, SYSTEM_NOTICE} */ +interface NoticeData { title: string; message: string; } @@ -31,29 +36,40 @@ export const initSSE = (url: any) => { connect(url); }; +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; + } +} + function connect(url: string) { const fullUrl = url + '?Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID; eventSource = new EventSource(fullUrl); - // "notice" event: login welcome / workflow / system announcement — strongly-typed JSON payload + // "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; - let payload: NoticePayload; - try { - payload = JSON.parse(e.data); - } catch (err) { - console.warn('SSE notice: invalid JSON payload', e.data, err); - return; - } + const env = parseEnvelope(e.data); + if (!env) return; + const { title, message } = env.data; useNoticeStore().addNotice({ - title: payload.title, - message: payload.message, + title, + message, read: false, time: new Date().toLocaleString() }); ElNotification({ - title: payload.title, - message: payload.message, + title, + message, type: 'success', duration: 3000 });