From 329acaf68d0c11ad0f314a4b24b061a543618e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Fri, 22 Aug 2025 18:27:26 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E7=9F=A5=E6=9D=BF=E5=9D=97=20?= =?UTF-8?q?=E5=8F=8A=20=E5=8A=9F=E8=83=BD=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 140 ++++- src/api/adminApi.ts | 7 + src/api/commonApi.ts | 38 ++ src/api/crmApi.ts | 8 + src/api/systemApi.ts | 28 + src/api/usersApi.ts | 8 + src/assets/img/common/logox.png | Bin 0 -> 26170 bytes .../core/layouts/art-header-bar/index.vue | 98 ++- .../core/layouts/art-notification/index.vue | 228 ++++--- src/utils/sys/wsClient.ts | 229 ++++--- src/views/admin/customer/index.vue | 25 + src/views/admin/customer/recycle.vue | 573 ++++++++++++++++++ src/views/admin/system/company.vue | 12 +- src/views/admin/system/feishu_app.vue | 12 +- src/views/admin/system/menu/index.vue | 1 - src/views/admin/system/notice/index.vue | 266 ++++++++ src/views/admin/system/systems.vue | 12 +- src/views/common/notice/index.vue | 257 ++++++++ src/views/crmpc/customer/Add.vue | 4 +- src/views/crmpc/customer/Edit.vue | 4 +- src/views/crmpc/customer/Show.vue | 2 +- src/views/crmpc/customer/index.vue | 37 +- 22 files changed, 1771 insertions(+), 218 deletions(-) create mode 100644 src/api/commonApi.ts create mode 100644 src/assets/img/common/logox.png create mode 100644 src/views/admin/customer/recycle.vue create mode 100644 src/views/admin/system/notice/index.vue create mode 100644 src/views/common/notice/index.vue diff --git a/src/App.vue b/src/App.vue index a964565..6169ed4 100644 --- a/src/App.vue +++ b/src/App.vue @@ -12,6 +12,8 @@ import { UserService } from './api/usersApi' import { setThemeTransitionClass } from './utils/theme/animation' import { checkStorageCompatibility } from './utils/storage' + import { mittBus } from '@/utils/sys' + import appLogo from '@/assets/img/common/logox.png' const userStore = useUserStore() const { language } = storeToRefs(userStore) @@ -32,14 +34,130 @@ zh: zh, en: en } + const isSupported = ref(false) + const permissionStatus = ref('default') + // 检查浏览器支持情况 + const checkSupport = () => { + if ('Notification' in window) { + isSupported.value = true + console.log('浏览器支持原生Notification API') + } else { + isSupported.value = false + console.log('⚠️ 浏览器不支持原生Notification API') + } + } + // 请求通知权限 + const requestPermission = async () => { + if (!('Notification' in window)) { + console.log('浏览器不支持原生Notification API') + return + } + try { + const permission = await Notification.requestPermission() + permissionStatus.value = permission + if (permission === 'granted') { + console.log('用户已授予通知权限') + } else if (permission === 'denied') { + console.log('用户拒绝了通知权限') + } else { + console.log('用户暂时未授予通知权限') + } + } catch (error) { + console.log(`请求权限时出错:`) + } + } + + // 显示基本通知(支持自定义标题与内容) + const showBasicNotification = ( + title: string = '基本通知', + body: string = '这是一个使用浏览器原生API的系统通知' + ) => { + if (!('Notification' in window)) { + ElMessage({ + message: '浏览器不支持原生Notification API', + type: 'warning' + }) + return + } + if (Notification.permission !== 'granted') { + ElMessage({ + message: '请先授予通知权限', + type: 'warning' + }) + requestPermission() + return + } + try { + const iconUrl = appLogo || '/favicon.ico' + const notification = new Notification(title, { + body, + requireInteraction: true, + icon: iconUrl + }) + notification.onclick = () => { + notification.close() + } + } 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() }) + // 页面刷新时重置WebSocket重连次数,同时清理事件监听 + onBeforeUnmount(() => { + import('@/utils/sys/wsClient').then(({ resetWebSocketReconnect }) => { + resetWebSocketReconnect() + }) + mittBus.off('ws:message') + }) + + const handleWsMessageNotification = (data: any) => { + if (data && data.event === 'notify.receive') { + const notice = data?.data?.notice_data + const title = notice?.message?.title || '新通知' + const rawContent = notice?.message?.content || '' + const body = typeof rawContent === 'string' ? rawContent.replace(/<[^>]+>/g, '') : '' + showBasicNotification(title, body) + } + } + onMounted(() => { // 检查存储兼容性 checkStorageCompatibility() + checkSupport() + requestPermission() // 提升暗黑主题下页面刷新视觉体验 setThemeTransitionClass(false) // 系统升级 @@ -48,11 +166,23 @@ getUserInfo() // 初始化 WebSocket const { accessToken } = useUserStore() - // if (accessToken) { - // import('@/utils/sys/wsClient').then(({ initWebSocket }) => { - // initWebSocket(accessToken) - // }) - // } + // showEventNotification() + if (accessToken) { + import('@/utils/sys/wsClient').then( + ({ initWebSocket, resetWebSocketReconnect, getWebSocketStatus }) => { + // 页面刷新时重置重连次数 + resetWebSocketReconnect() + initWebSocket(accessToken) + // 添加调试功能,可以在控制台查看WebSocket状态 + // @ts-ignore + window.getWebSocketStatus = getWebSocketStatus + // console.log('WebSocket状态查看: window.getWebSocketStatus()') + } + ) + } + + // 监听ws消息,用于系统级通知 + mittBus.on('ws:message', handleWsMessageNotification) }) // 获取用户信息 diff --git a/src/api/adminApi.ts b/src/api/adminApi.ts index d33b476..f118573 100644 --- a/src/api/adminApi.ts +++ b/src/api/adminApi.ts @@ -198,4 +198,11 @@ export class manageService { // showErrorMessage: false // 不显示错误消息 }) } + //回收站 恢复 + static customerRestore(params: any) { + return request.get({ + url: '/admin/crm/customer/restore', + params + }) + } } diff --git a/src/api/commonApi.ts b/src/api/commonApi.ts new file mode 100644 index 0000000..22a1cf8 --- /dev/null +++ b/src/api/commonApi.ts @@ -0,0 +1,38 @@ +import request from '@/utils/http' + +export class commonService { + //通知列表 + static getNoticeList(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/common/notice/list', + params + }) + } + //通知详情 + static getNoticeDetail(params: any) { + return request.get({ + url: '/common/notice/show', + params + }) + } + //通知已读 + static readNotice(params: any) { + return request.post({ + url: '/common/notice/read', + params + }) + } + //通知删除 + static delNotice(params: any) { + return request.post({ + url: '/common/notice/destroy', + params + }) + } + //通知未读列表 + static getUnReadNoticeList() { + return request.get({ + url: '/common/notice/unread' + }) + } +} diff --git a/src/api/crmApi.ts b/src/api/crmApi.ts index 3727eed..e65c0f4 100644 --- a/src/api/crmApi.ts +++ b/src/api/crmApi.ts @@ -191,4 +191,12 @@ export class CrmService { // showErrorMessage: false // 不显示错误消息 }) } + //客户删除 + static crmCustomerDelete(params: any) { + return request.get({ + url: '/crm/customer/delete', + params + // showErrorMessage: false // 不显示错误消息 + }) + } } diff --git a/src/api/systemApi.ts b/src/api/systemApi.ts index 5284590..b9bbe95 100644 --- a/src/api/systemApi.ts +++ b/src/api/systemApi.ts @@ -217,4 +217,32 @@ export class systemService { params }) } + //通知列表 + static getNoticeList(params: Api.Common.PaginatingParams) { + return request.get({ + url: '/admin/notice/list', + params + }) + } + //通知详情 + static getNoticeDetail(params: any) { + return request.get({ + url: '/admin/notice/show', + params + }) + } + //通知删除 + static delNotice(params: any) { + return request.post({ + url: '/admin/notice/destroy', + params + }) + } + //通知推送 + static sendNotice(params: any) { + return request.post({ + url: '/admin/notice/push', + params + }) + } } diff --git a/src/api/usersApi.ts b/src/api/usersApi.ts index 44fac2e..62b9c20 100644 --- a/src/api/usersApi.ts +++ b/src/api/usersApi.ts @@ -92,4 +92,12 @@ export class UserService { params }) } + //客户删除 + static crmCustomerDelete(params: any) { + return request.get({ + url: '/admin/crm/customer/delete', + params + // showErrorMessage: false // 不显示错误消息 + }) + } } diff --git a/src/assets/img/common/logox.png b/src/assets/img/common/logox.png new file mode 100644 index 0000000000000000000000000000000000000000..8bba14d85313c4985102f8f7d8720363169e29f4 GIT binary patch literal 26170 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�zOTDVTCae2fIkUHzhIgX_*!2>zDBJGjnj{F$(2rg6NO?SzIW` zdnT?}tiG}4$&Y+;Ic5m0xKq07p4x_*j&{Am#FI|2L4uz(48+;zmRO&T?jp52?_d*! zy!koP%*@OS%SvV2F~T(?{Hg+Hl8KY%o|9r;spR~?LmYtv0xUO;PF}s0jO&mt$$?!p z#Z2d>*S7C%geZ{^_!#HWEqp>F#=MgqY1q!Q?`@sj$ZV8c#n4c4e2|(#dIPP#^>U z2~g&A8qwEs=o#oI=e zL7VFoLiE$6k0t6bZ9E&u6{#8Z>6$?j?04w9V{>VgWm3_VDrA)5F&k;d3}Y6q&UV-4 zYA>kb`RZgHJURiiXQ81aF`#-oLQZ3IX6=D;GmOa`&$VCOvR3 zuy9!B4jPWfZ~CP<(%TrSc%nD7CraJJJWh;PUxxHS%acOgNn79ds{Z6yQFE;h<8nV6 zFMD0s{>D;&`}@Sg)fXqV>@GYOK1^4vN? --> + +
+
+ + +
+
@@ -69,13 +81,6 @@ {{ isFullscreen ? '' : '' }}
- - + 消息 + {{ $t('notice.btnRead') }}
  • - {{ item.name }} ({{ item.num }}) + {{ item.name }} ({{ noticeList.length }})
-
    -
  • +
      + +
    • -

      {{ item.title }}

      -

      {{ item.time }}

      +

      {{ item.message.title }}

      +

      {{ item.message.created_at }}

    -
      + -
        + -
        +

        {{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}

        -
        +
        @@ -86,6 +89,25 @@
        + + + +
        +
        +
        发送人:{{ noticeDetail.sender_name }}
        +
        +
        +
        通知标题:{{ noticeDetail.title }}
        +
        +
        +
        + 通知内容: +
        +
        +
        +
        +
        +
        diff --git a/src/utils/sys/wsClient.ts b/src/utils/sys/wsClient.ts index 75c2cc6..5929c62 100644 --- a/src/utils/sys/wsClient.ts +++ b/src/utils/sys/wsClient.ts @@ -3,56 +3,38 @@ import { useUserStore } from '@/store/modules/user' type JsonValue = Record | any[] -const WS_BASE = 'wss://mzl.dev.hnxinyouhe.com/ws?token=' +const WS_BASE = import.meta.env.VITE_API_URL_WSS + '/ws?token=' +const MAX_RECONNECT_ATTEMPTS = 10 +const MAX_RECONNECT_DELAY = 50000 +const HEARTBEAT_INTERVAL = 50000 let socket: WebSocket | null = null let reconnectTimer: any = null let heartbeatTimer: any = null let reconnectAttempts = 0 -const maxReconnectDelayMs = 30000 -const heartbeatIntervalMs = 30000 +let isInitialConnection = true -// 生成全局唯一的 trace_id(优先使用 crypto.randomUUID) +// 生成唯一ID const generateTraceId = (): string => { try { - // @ts-ignore - if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { - // @ts-ignore + if (typeof crypto !== 'undefined' && crypto.randomUUID) { return `req-${crypto.randomUUID()}` } - } catch (_) {} - // Fallback:基于时间戳 + 随机数 - const timestampPart = Date.now().toString(36) - let randomPart = '' - try { - // @ts-ignore - if (typeof crypto !== 'undefined' && crypto.getRandomValues) { - const buf = new Uint8Array(16) - // @ts-ignore - crypto.getRandomValues(buf) - randomPart = Array.from(buf) - .map((b) => b.toString(16).padStart(2, '0')) - .join('') - } else { - randomPart = Array.from({ length: 16 }) - .map(() => - Math.floor(Math.random() * 256) - .toString(16) - .padStart(2, '0') - ) - .join('') - } - } catch (_) { - randomPart = Math.random().toString(36).slice(2) + } catch { + // 忽略错误,使用fallback方案 } - return `req-${timestampPart}-${randomPart}` + const timestamp = Date.now().toString(36) + const random = Math.random().toString(36).slice(2) + return `req-${timestamp}-${random}` } +// 计算重连延迟 const getReconnectDelay = () => { const base = 1000 * Math.pow(2, reconnectAttempts) - return Math.min(base, maxReconnectDelayMs) + return Math.min(base, MAX_RECONNECT_DELAY) } +// 清理定时器 const cleanup = () => { if (reconnectTimer) { clearTimeout(reconnectTimer) @@ -64,107 +46,198 @@ const cleanup = () => { } } +// 启动心跳 const startHeartbeat = () => { - // 清理上一次的心跳 if (heartbeatTimer) { clearTimeout(heartbeatTimer) - heartbeatTimer = null } - // 递归 setTimeout,确保固定间隔执行 heartbeatTimer = setTimeout(() => { try { - if (socket && socket.readyState === WebSocket.OPEN) { + if (socket?.readyState === WebSocket.OPEN) { const msg = { event: 'system.ping', data: {}, - trace_id: generateTraceId() // 可选,用于请求-响应匹配 + trace_id: generateTraceId() } socket.send(JSON.stringify(msg)) } - } catch (_) {} - // 下一次心跳 + } catch { + // 忽略心跳发送错误 + } startHeartbeat() - }, heartbeatIntervalMs) + }, HEARTBEAT_INTERVAL) } +// 发送心跳消息 +const sendPing = () => { + try { + if (socket?.readyState === WebSocket.OPEN) { + const msg = { + event: 'system.ping', + data: {}, + trace_id: generateTraceId() + } + socket.send(JSON.stringify(msg)) + } + } catch { + // 忽略发送错误 + } +} + +// 处理WebSocket消息 +const handleMessage = (event: MessageEvent) => { + let data: any = event.data + try { + data = JSON.parse(event.data) + } catch { + // 忽略解析错误 + } + console.log('WebSocket message:', data) + if (data.event == 'notify.receive') { + ElNotification({ + title: data.data.notice_data.message.title || '通知', + message: data.data.notice_data.message.content, + type: 'primary' + }) + mittBus.emit('notification-data-changed', 1) + } + mittBus.emit('ws:message', data) +} + +// 设置WebSocket事件处理器 +const setupWebSocketHandlers = (ws: WebSocket) => { + ws.onopen = () => { + isInitialConnection = false + startHeartbeat() + mittBus.emit('ws:open') + sendPing() + } + + ws.onmessage = handleMessage + + ws.onerror = (err) => { + mittBus.emit('ws:error', err) + } + + ws.onclose = () => { + mittBus.emit('ws:close') + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + scheduleReconnect() + } + } +} + +// 安排重连 const scheduleReconnect = () => { + if (!isInitialConnection) { + reconnectAttempts += 1 + } + + if (reconnectAttempts > MAX_RECONNECT_ATTEMPTS) { + console.warn(`WebSocket重连失败,已达到最大重连次数(${MAX_RECONNECT_ATTEMPTS}),停止重连`) + mittBus.emit('ws:max-reconnect-reached') + return + } + cleanup() const delay = getReconnectDelay() + console.log(`WebSocket重连尝试 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS},延迟: ${delay}ms`) reconnectTimer = setTimeout(() => { const { accessToken } = useUserStore() - if (accessToken) initWebSocket(accessToken) + if (accessToken) { + try { + if (socket) { + try { + socket.close() + } catch { + // 忽略关闭错误 + } + socket = null + } + + const url = `${WS_BASE}${accessToken}` + socket = new WebSocket(url) + setupWebSocketHandlers(socket) + } catch (e) { + mittBus.emit('ws:error', e) + if (reconnectAttempts <= MAX_RECONNECT_ATTEMPTS) { + scheduleReconnect() + } + } + } }, delay) } +// 初始化WebSocket连接 export const initWebSocket = (token: string) => { try { if (!token) return if (socket) { try { socket.close() - } catch (_) {} + } catch { + // 忽略关闭错误 + } socket = null } cleanup() + reconnectAttempts = 0 + isInitialConnection = true const url = `${WS_BASE}${token}` socket = new WebSocket(url) - - socket.onopen = () => { - reconnectAttempts = 0 - startHeartbeat() - mittBus.emit('ws:open') - try { - const msg = { - event: 'system.ping', - data: {}, - trace_id: generateTraceId() // 可选,用于请求-响应匹配 - } - socket?.send(JSON.stringify(msg)) - } catch (_) {} - } - - socket.onmessage = (event) => { - const dataStr = event.data - let data: any = dataStr - try { - data = JSON.parse(dataStr) - } catch (_) {} - mittBus.emit('ws:message', data) - } - - socket.onerror = (err) => { - mittBus.emit('ws:error', err) - } - - socket.onclose = () => { - mittBus.emit('ws:close') - reconnectAttempts += 1 - scheduleReconnect() - } + setupWebSocketHandlers(socket) } catch (e) { mittBus.emit('ws:error', e) scheduleReconnect() } } +// 关闭WebSocket连接 export const closeWebSocket = () => { cleanup() try { socket?.close() - } catch (_) {} + } catch { + // 忽略关闭错误 + } socket = null + reconnectAttempts = 0 + isInitialConnection = true } +// 发送WebSocket消息 export const sendWebSocket = (payload: string | JsonValue) => { if (!socket || socket.readyState !== WebSocket.OPEN) return false try { const data = typeof payload === 'string' ? payload : JSON.stringify(payload) socket.send(data) return true - } catch (_) { + } catch { return false } } +// 重置重连次数 +export const resetWebSocketReconnect = () => { + reconnectAttempts = 0 + isInitialConnection = true + console.log('WebSocket重连次数已重置') +} +// 获取当前重连次数 +export const getWebSocketReconnectAttempts = () => reconnectAttempts + +// 获取最大重连次数 +export const getMaxReconnectAttempts = () => MAX_RECONNECT_ATTEMPTS + +// 获取WebSocket连接状态 +export const getWebSocketStatus = () => ({ + socket, + readyState: socket?.readyState, + reconnectAttempts, + maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS, + isReconnecting: reconnectTimer !== null, + isHeartbeating: heartbeatTimer !== null, + isInitialConnection +}) \ No newline at end of file diff --git a/src/views/admin/customer/index.vue b/src/views/admin/customer/index.vue index 9ea45d7..c16d1a5 100644 --- a/src/views/admin/customer/index.vue +++ b/src/views/admin/customer/index.vue @@ -175,6 +175,15 @@ > 跟进记录 + + 删除 + @@ -555,6 +564,22 @@ plzyDialogVisible.value = false gettableList() } + const delectfn = (row: any) => { + ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }) + .then(async () => { + const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'list' }) + ElMessage.success(res?.message || '提交成功!') + gettableList() + // 你可以在这里调用接口或做后续处理 + }) + .catch(() => { + // 用户点击了取消 + }) + } diff --git a/src/views/admin/system/company.vue b/src/views/admin/system/company.vue index cf093da..9d965e3 100644 --- a/src/views/admin/system/company.vue +++ b/src/views/admin/system/company.vue @@ -15,6 +15,8 @@ :currentPage="pagination.currentPage" :pageSize="pagination.pageSize" :total="pagination.total" + @size-change="handleSizeChange" + @current-change="handleCurrentChange" >