From aefd43f79de8995c107b6acb492748f4b79690df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 26 Aug 2025 09:55:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E6=A0=B7=E5=BC=8F=20=20=E5=92=8C=20=E5=8D=B3?= =?UTF-8?q?=E6=97=B6=E9=80=9A=E8=AE=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 58 +++++-------- src/api/menuApi.ts | 2 +- .../core/layouts/art-notification/index.vue | 61 ++++++++++--- src/utils/sys/wsClient.ts | 58 ++++++++++--- src/views/admin/system/notice/index.vue | 61 ++++++++++--- src/views/common/notice/index.vue | 85 ++++++++++++------- src/views/crmpc/customer/Add.vue | 1 + 7 files changed, 222 insertions(+), 104 deletions(-) diff --git a/src/App.vue b/src/App.vue index 6169ed4..3036e35 100644 --- a/src/App.vue +++ b/src/App.vue @@ -16,7 +16,7 @@ import appLogo from '@/assets/img/common/logox.png' const userStore = useUserStore() - const { language } = storeToRefs(userStore) + const { language, accessToken } = storeToRefs(userStore) function setCompanyFromUrl() { const userStore = useUserStore() const params = new URLSearchParams(window.location.search) @@ -99,37 +99,6 @@ } } catch (error) {} } - const showEventNotification = () => { - if (Notification.permission !== 'granted') { - ElMessage({ - message: '请先授予通知权限', - type: 'warning' - }) - return - } - try { - const notification = new Notification('带事件监听的通知', { - body: '这个通知演示各种事件监听', - icon: 'https://element-plus.org/images/element-plus-logo.svg' - }) - notification.onshow = () => { - console.log('通知显示事件触发') - } - notification.onclick = () => { - console.log('通知点击事件触发') - notification.close() - } - notification.onclose = () => { - console.log('通知关闭事件触发') - } - notification.onerror = (error) => { - // console.log(`通知错误事件: ${error.message}`) - } - console.log('已发送带事件监听的通知') - } catch (error) { - // console.log(`发送通知失败: ${error.message}`) - } - } onBeforeMount(() => { setThemeTransitionClass(true) setCompanyFromUrl() @@ -165,14 +134,13 @@ // 获取用户信息 getUserInfo() // 初始化 WebSocket - const { accessToken } = useUserStore() // showEventNotification() - if (accessToken) { + if (accessToken.value) { import('@/utils/sys/wsClient').then( ({ initWebSocket, resetWebSocketReconnect, getWebSocketStatus }) => { // 页面刷新时重置重连次数 resetWebSocketReconnect() - initWebSocket(accessToken) + initWebSocket(accessToken.value) // 添加调试功能,可以在控制台查看WebSocket状态 // @ts-ignore window.getWebSocketStatus = getWebSocketStatus @@ -185,6 +153,26 @@ mittBus.on('ws:message', handleWsMessageNotification) }) + // 登录后,accessToken 变更时立即初始化 WebSocket(无需刷新) + watch(accessToken, async (token) => { + if (token) { + const { initWebSocket, getWebSocketStatus } = await import('@/utils/sys/wsClient') + const status = getWebSocketStatus?.() + // 避免重复初始化:已连接或正在连接时不再初始化 + if ( + !status?.readyState || + (status.readyState !== WebSocket.OPEN && status.readyState !== WebSocket.CONNECTING) + ) { + initWebSocket(token) + } + } else { + // 退出登录时,关闭并重置 + const { closeWebSocket, resetWebSocketReconnect } = await import('@/utils/sys/wsClient') + closeWebSocket() + resetWebSocketReconnect() + } + }) + // 获取用户信息 const getUserInfo = async () => { if (userStore.isLogin) { diff --git a/src/api/menuApi.ts b/src/api/menuApi.ts index ab614f8..03889fc 100644 --- a/src/api/menuApi.ts +++ b/src/api/menuApi.ts @@ -23,7 +23,7 @@ export const menuService = { ElMessage.error('暂无系统菜单权限') return { menuList: [] } } - console.log('menuData', menuData) + // console.log('menuData', menuData) // 处理菜单数据 const menuList = menuData.map((route: any) => menuDataToRouter(route)) // 验证处理后的菜单列表 diff --git a/src/components/core/layouts/art-notification/index.vue b/src/components/core/layouts/art-notification/index.vue index 577748c..41773c6 100644 --- a/src/components/core/layouts/art-notification/index.vue +++ b/src/components/core/layouts/art-notification/index.vue @@ -91,17 +91,21 @@ - -
-
-
发送人:{{ noticeDetail.sender_name }}
+ +
+
+
+
发送人
+
{{ noticeDetail.sender_name || '-' }}
+
+
+
通知标题
+
{{ noticeDetail.title || '-' }}
+
-
-
通知标题:{{ noticeDetail.title }}
-
-
-
- 通知内容: +
+
通知内容
+
@@ -282,7 +286,42 @@ } .message-text { padding: 10px 14px; - line-height: 1.4; + line-height: 1.7; white-space: pre-line; + word-break: break-word; + } + .detail-dialog { + display: flex; + flex-direction: column; + gap: 16px; + } + .section { + background: var(--el-fill-color-blank); + border: 1px solid var(--el-border-color-light); + border-radius: 8px; + padding: 12px 14px; + } + .info-grid { + display: grid; + grid-template-columns: 96px 1fr; + gap: 6px 12px; + align-items: center; + padding: 6px 0; + } + .label { + color: var(--el-text-color-secondary); + font-size: 13px; + } + .value { + color: var(--el-text-color-primary); + font-size: 14px; + } + .content-box { + background: var(--el-fill-color-lighter); + border: 1px dashed var(--el-border-color); + border-radius: 8px; + padding: 10px 12px; + max-height: 320px; + overflow: auto; } diff --git a/src/utils/sys/wsClient.ts b/src/utils/sys/wsClient.ts index 5929c62..18b3381 100644 --- a/src/utils/sys/wsClient.ts +++ b/src/utils/sys/wsClient.ts @@ -1,5 +1,6 @@ import mittBus from './mittBus' import { useUserStore } from '@/store/modules/user' +import { ElNotification } from 'element-plus' type JsonValue = Record | any[] @@ -13,6 +14,10 @@ let reconnectTimer: any = null let heartbeatTimer: any = null let reconnectAttempts = 0 let isInitialConnection = true +let isConnecting = false +let lastInitToken: string | null = null +let lastInitAt = 0 +const INIT_DEBOUNCE_MS = 1200 // 生成唯一ID const generateTraceId = (): string => { @@ -108,6 +113,7 @@ const handleMessage = (event: MessageEvent) => { const setupWebSocketHandlers = (ws: WebSocket) => { ws.onopen = () => { isInitialConnection = false + isConnecting = false startHeartbeat() mittBus.emit('ws:open') sendPing() @@ -117,10 +123,19 @@ const setupWebSocketHandlers = (ws: WebSocket) => { ws.onerror = (err) => { mittBus.emit('ws:error', err) + isConnecting = false + try { + if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { + ws.close() + } + } catch { + // 忽略关闭错误 + } } ws.onclose = () => { mittBus.emit('ws:close') + isConnecting = false if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { scheduleReconnect() } @@ -129,11 +144,10 @@ const setupWebSocketHandlers = (ws: WebSocket) => { // 安排重连 const scheduleReconnect = () => { - if (!isInitialConnection) { - reconnectAttempts += 1 - } + // 每次安排重连都递增(即使从未成功连接过) + reconnectAttempts += 1 - if (reconnectAttempts > MAX_RECONNECT_ATTEMPTS) { + if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { console.warn(`WebSocket重连失败,已达到最大重连次数(${MAX_RECONNECT_ATTEMPTS}),停止重连`) mittBus.emit('ws:max-reconnect-reached') return @@ -144,7 +158,7 @@ const scheduleReconnect = () => { console.log(`WebSocket重连尝试 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS},延迟: ${delay}ms`) reconnectTimer = setTimeout(() => { const { accessToken } = useUserStore() - if (accessToken) { + if (accessToken && !isConnecting) { try { if (socket) { try { @@ -156,11 +170,13 @@ const scheduleReconnect = () => { } const url = `${WS_BASE}${accessToken}` + isConnecting = true socket = new WebSocket(url) setupWebSocketHandlers(socket) } catch (e) { mittBus.emit('ws:error', e) - if (reconnectAttempts <= MAX_RECONNECT_ATTEMPTS) { + isConnecting = false + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { scheduleReconnect() } } @@ -172,23 +188,33 @@ const scheduleReconnect = () => { export const initWebSocket = (token: string) => { try { if (!token) return - if (socket) { - try { - socket.close() - } catch { - // 忽略关闭错误 + const now = Date.now() + // 简单防抖:在短时间内重复触发则跳过 + if (now - lastInitAt < INIT_DEBOUNCE_MS) { + return + } + // 避免并发初始化:已在连接中或已连接且 token 未变化则跳过 + if (isConnecting) return + if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) { + if (lastInitToken === token) { + return } + // token 变更:需要切换连接 + try { socket.close() } catch {} socket = null } + cleanup() - reconnectAttempts = 0 - isInitialConnection = true const url = `${WS_BASE}${token}` + isConnecting = true + lastInitToken = token + lastInitAt = now socket = new WebSocket(url) setupWebSocketHandlers(socket) } catch (e) { mittBus.emit('ws:error', e) + isConnecting = false scheduleReconnect() } } @@ -204,6 +230,8 @@ export const closeWebSocket = () => { socket = null reconnectAttempts = 0 isInitialConnection = true + isConnecting = false + lastInitToken = null } // 发送WebSocket消息 @@ -239,5 +267,7 @@ export const getWebSocketStatus = () => ({ maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS, isReconnecting: reconnectTimer !== null, isHeartbeating: heartbeatTimer !== null, - isInitialConnection + isInitialConnection, + isConnecting, + lastInitToken }) \ No newline at end of file diff --git a/src/views/admin/system/notice/index.vue b/src/views/admin/system/notice/index.vue index fcee459..e701cdb 100644 --- a/src/views/admin/system/notice/index.vue +++ b/src/views/admin/system/notice/index.vue @@ -89,17 +89,21 @@
- -
-
-
发送人:{{ noticeDetail.sender_name }}
+ +
+
+
+
发送人
+
{{ noticeDetail.sender_name || '-' }}
+
+
+
通知标题
+
{{ noticeDetail.title || '-' }}
+
-
-
通知标题:{{ noticeDetail.title }}
-
-
-
- 通知内容: +
+
通知内容
+
@@ -264,7 +268,42 @@ } .message-text { padding: 10px 14px; - line-height: 1.4; + line-height: 1.7; white-space: pre-line; + word-break: break-word; + } + .detail-dialog { + display: flex; + flex-direction: column; + gap: 16px; + } + .section { + background: var(--el-fill-color-blank); + border: 1px solid var(--el-border-color-light); + border-radius: 8px; + padding: 12px 14px; + } + .info-grid { + display: grid; + grid-template-columns: 96px 1fr; + gap: 6px 12px; + align-items: center; + padding: 6px 0; + } + .label { + color: var(--el-text-color-secondary); + font-size: 13px; + } + .value { + color: var(--el-text-color-primary); + font-size: 14px; + } + .content-box { + background: var(--el-fill-color-lighter); + border: 1px dashed var(--el-border-color); + border-radius: 8px; + padding: 10px 12px; + max-height: 320px; + overflow: auto; } diff --git a/src/views/common/notice/index.vue b/src/views/common/notice/index.vue index 851f578..923a254 100644 --- a/src/views/common/notice/index.vue +++ b/src/views/common/notice/index.vue @@ -77,17 +77,21 @@
- -
-
-
发送人:{{ noticeDetail.sender_name }}
+ +
+
+
+
发送人
+
{{ noticeDetail.sender_name || '-' }}
+
+
+
通知标题
+
{{ noticeDetail.title || '-' }}
+
-
-
通知标题:{{ noticeDetail.title }}
-
-
-
- 通知内容: +
+
通知内容
+
@@ -203,23 +207,45 @@ align-items: center; justify-content: center; } - .detail-info-table { - padding-left: 15px; - .row { - display: flex; - justify-content: flex-start; - margin-bottom: 6px; - - > div { - flex: 1; - min-width: 0; - margin-right: 16px; - overflow: hidden; - font-size: 14px; - text-overflow: ellipsis; - white-space: nowrap; - } - } + .message-text { + padding: 10px 14px; + line-height: 1.7; + white-space: pre-line; + word-break: break-word; + } + .detail-dialog { + display: flex; + flex-direction: column; + gap: 16px; + } + .section { + background: var(--el-fill-color-blank); + border: 1px solid var(--el-border-color-light); + border-radius: 8px; + padding: 12px 14px; + } + .info-grid { + display: grid; + grid-template-columns: 96px 1fr; + gap: 6px 12px; + align-items: center; + padding: 6px 0; + } + .label { + color: var(--el-text-color-secondary); + font-size: 13px; + } + .value { + color: var(--el-text-color-primary); + font-size: 14px; + } + .content-box { + background: var(--el-fill-color-lighter); + border: 1px dashed var(--el-border-color); + border-radius: 8px; + padding: 10px 12px; + max-height: 320px; + overflow: auto; } .customer-page { background: var(--art-main-bg-color); @@ -247,9 +273,4 @@ display: flex; gap: 12px; } - .message-text { - padding: 10px 14px; - line-height: 1.4; - white-space: pre-line; - } diff --git a/src/views/crmpc/customer/Add.vue b/src/views/crmpc/customer/Add.vue index 074b479..1b9981e 100644 --- a/src/views/crmpc/customer/Add.vue +++ b/src/views/crmpc/customer/Add.vue @@ -173,6 +173,7 @@ if (valid) { try { const res = await CrmService.customerAdd(form.value) + console.log(res) ElMessage.success(res?.message || '提交成功!') router.push('/crmpc/customer') } catch (e: any) {