在utils文件下新建一个websocket.js文件
javascript
import { updateMcCenter } from "@/api/mes/mc/mcCenter";
import { Message, Notification } from "element-ui";
// 读取环境变量
const envWsUrl = process.env.VUE_APP_WS_URL;
// 全局配置
const WS_CONFIG = {
URL_PREFIX: envWsUrl || "",
HEARTBEAT_INTERVAL: 30000,
RECONNECT_DELAY: 5000,
};
// 全局通知容器配置
const NOTIFICATION_CONFIG = {
containerId: 'global-notification-container',
maxHeight: '500px',
position: 'bottom-right',
notificationQueue: [] // 存储Notification实例
};
let socket = null;
let lockReconnect = false;
let heartTimer = null;
let routerInstance = null;
let storeInstance = null;
let currentUserId = null;
/**
* 创建全局通知容器
*/
const createNotificationContainer = () => {
// 如果容器已存在,先移除
const existingContainer = document.getElementById(NOTIFICATION_CONFIG.containerId);
if (existingContainer) {
existingContainer.remove();
}
// 创建容器
const container = document.createElement('div');
container.id = NOTIFICATION_CONFIG.containerId;
container.className = 'global-notification-container';
// 设置样式
container.style.cssText = `
position: fixed;
${NOTIFICATION_CONFIG.position.includes('right') ? 'right: 16px;' : 'left: 16px;'}
${NOTIFICATION_CONFIG.position.includes('bottom') ? 'bottom: 16px;' : 'top: 16px;'}
z-index: 9999;
display: flex;
flex-direction: column;
gap: 12px;
max-height: ${NOTIFICATION_CONFIG.maxHeight};
overflow-y: auto;
width: 380px;
`;
document.body.appendChild(container);
return container;
};
/**
* 创建自定义通知元素(替换Element UI的Notification)
*/
const createCustomNotification = (msg, notificationId) => {
const container = document.getElementById(NOTIFICATION_CONFIG.containerId);
if (!container) return null;
// 创建通知元素
const notificationEl = document.createElement('div');
notificationEl.id = notificationId;
notificationEl.className = 'custom-notification';
notificationEl.dataset.msgId = msg.msgId;
// console.log("创建通知:", msg);
// 设置样式
notificationEl.style.cssText = `
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
`;
// 构建HTML内容
notificationEl.innerHTML = `
<div class="notification-header" style="position: relative; margin-bottom: 8px;">
<h4 style="margin: 0 0 8px 0; color: #303133; font-size: 14px;">
${msg.msgType || '系统通知'}
</h4>
<button class="close-btn" style="
background: none;
border: none;
color: #909399;
cursor: pointer;
font-size: 16px;
font-weight: bold;
line-height: 1;
position: absolute;
top: 0px;
right: 0px;
">×</button>
</div>
<div class="notification-body">
<div style="color: #606266; font-size: 13px; line-height: 1.5; margin-bottom: 12px;">
${msg.msgContent || '新消息'}
</div>
<div style="text-align: right;">
<button class="view-btn" style="
padding: 5px 10px;
background: #409EFF;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
">
<i style="margin-right: 4px;">👁️</i>去查看
</button>
</div>
</div>
`;
// 添加到容器顶部
container.insertBefore(notificationEl, container.firstChild);
// 添加事件监听
const closeBtn = notificationEl.querySelector('.close-btn');
const viewBtn = notificationEl.querySelector('.view-btn');
const notificationObj = {
element: notificationEl,
msgId: msg.msgId,
notificationId: notificationId,
close: () => {
// notificationEl.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (notificationEl.parentNode) {
notificationEl.remove();
}
removeFromNotificationQueue(msg.msgId);
}, 300);
}
};
// 关闭按钮点击事件
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
notificationObj.close();
// markAsRead(msg);
});
// 查看按钮点击事件
viewBtn.addEventListener('click', (e) => {
e.stopPropagation();
handleNotificationJump(msg);
// notificationObj.close();
});
// 整个通知点击事件(点击空白处)
notificationEl.addEventListener('click', (e) => {
if (e.target === notificationEl || e.target.classList.contains('notification-body')) {
handleNotificationJump(msg);
// notificationObj.close();
}
});
// 添加到队列
NOTIFICATION_CONFIG.notificationQueue.push(notificationObj);
return notificationObj;
};
/**
* 从通知队列中移除
*/
const removeFromNotificationQueue = (msgId) => {
const index = NOTIFICATION_CONFIG.notificationQueue.findIndex(item => item.msgId === msgId);
if (index !== -1) {
NOTIFICATION_CONFIG.notificationQueue.splice(index, 1);
}
};
/**
* 初始化 WebSocket
*/
export const initWebSocket = (router, store) => {
routerInstance = router;
storeInstance = store;
if (!WS_CONFIG.URL_PREFIX) {
console.error("[WebSocket] 未配置 VUE_APP_WS_URL,无法连接");
return;
}
const user = store.state.user;
if (!user || !user.id) {
console.warn("[WebSocket] 用户未登录,跳过连接");
return;
}
if (socket && currentUserId === user.id) {
return;
}
if (socket) {
closeWebSocket();
}
currentUserId = user.id;
// 创建通知容器
createNotificationContainer();
createWebSocket();
};
/**
* 创建 WebSocket 连接
*/
const createWebSocket = () => {
try {
const wsUrl = WS_CONFIG.URL_PREFIX + currentUserId;
console.log("[WebSocket] 正在连接:", wsUrl);
socket = new WebSocket(wsUrl);
initEventHandle();
} catch (e) {
console.error("[WebSocket] 连接创建失败:", e);
reconnect();
}
};
/**
* 初始化事件监听
*/
const initEventHandle = () => {
socket.onopen = () => {
console.log("[WebSocket] 连接成功");
startHeartbeat();
};
socket.onmessage = (e) => {
startHeartbeat();
if (e.data === "PONG") {
return;
}
try {
const msg = JSON.parse(e.data);
console.log("[WebSocket] 收到消息:", msg);
handleMessage(msg);
} catch (err) {
console.error("[WebSocket] 消息解析失败:", err);
}
};
socket.onclose = (e) => {
console.log("[WebSocket] 连接关闭", e);
reconnect();
};
socket.onerror = (e) => {
console.log("[WebSocket] 连接错误", e);
reconnect();
};
};
/**
* 断线重连
*/
const reconnect = () => {
if (lockReconnect) return;
lockReconnect = true;
clearTimeout(heartTimer);
if (!storeInstance || !storeInstance.state.user || !storeInstance.state.user.id) {
lockReconnect = false;
return;
}
console.log(`[WebSocket] ${WS_CONFIG.RECONNECT_DELAY / 1000}秒后尝试重连...`);
setTimeout(() => {
createWebSocket();
lockReconnect = false;
}, WS_CONFIG.RECONNECT_DELAY);
};
/**
* 心跳检测
*/
const startHeartbeat = () => {
clearTimeout(heartTimer);
heartTimer = setTimeout(() => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send("PING");
}
}, WS_CONFIG.HEARTBEAT_INTERVAL);
};
/**
* 主动关闭连接
*/
export const closeWebSocket = () => {
if (socket) {
socket.close();
socket = null;
}
clearTimeout(heartTimer);
lockReconnect = false;
currentUserId = null;
// 清空所有通知
clearAllNotifications();
console.log("[WebSocket] 主动关闭连接");
};
/**
* 清空所有通知
*/
const clearAllNotifications = () => {
NOTIFICATION_CONFIG.notificationQueue.forEach(item => {
if (item.close) {
item.close();
}
});
NOTIFICATION_CONFIG.notificationQueue = [];
// 移除容器
const container = document.getElementById(NOTIFICATION_CONFIG.containerId);
if (container) {
container.remove();
}
};
/**
* 标记消息为已读
*/
const markAsRead = (msg) => {
return updateMcCenter({
msgType: msg.msgType,
msgStatus: 1,
msgId: msg.msgId,
});
};
/**
* 核心业务逻辑:处理消息弹窗
*/
const handleMessage = (msg) => {
const user = storeInstance.state.user;
// 逻辑判断:是否应该显示通知
// const isTargetUser =
// (msg.msgOrigin === "ERP下发" &&
// (msg.msgType === "下单通知" || msg.msgType === "结构报价单通知")) ||
// user.id === msg.recipientId;
// if (!isTargetUser) return;
// 生成唯一ID
const notificationId = `notify-${Date.now()}`;
// 创建自定义通知(替换Element UI Notification)
createCustomNotification(msg, notificationId);
};
/**
* 跳转逻辑封装
*/
const handleNotificationJump = (row) => {
// console.log(row)
if (!row.msgId) {
Message.warning("消息数据不完整");
return;
}
routerInstance.push(`/index?msgId=${row.msgId}`);
};
在main.js引入
javascript
// 引入刚才创建的 socket 工具
import { initWebSocket } from "@/utils/websocket";
new Vue({
el: "#app",
router,
store,
i18n,
created() {
// 应用启动时尝试连接
initWebSocket(router, store);
},
render: (h) => h(App),
});
还可以加入全局弹窗组件
在websocket.js文件中引入组件
javascript
// 引入刚才创建的 socket 工具
import MessageDetailDialog from '@/components/msgCenter/MessageDialog.vue';
在Notification的点击事件中判断什么时候需要弹窗如:
onClick: () => {
// 打开 MessageDetailDialog 组件
if (msg.msgType === "订单未接单提醒") {
openMessageDetailDialog(msg.msgId);
}
})
// 创建 MessageDetailDialog 组件的实例并挂载到 body 上
function openMessageDetailDialog(msgId) {
const MessageDetailConstructor = Vue.extend(MessageDetailDialog);
const instance = new MessageDetailConstructor({
propsData: {
visible: true,
messageId: msgId
}
});
instance.$mount(); // 挂载组件
document.body.appendChild(instance.$el);
instance.$on('update:visible', (val) => {
if (!val) {
document.body.removeChild(instance.$el);
instance.$destroy();
}
});
}
弹窗组件
javascript
<template>
<el-dialog
title="消息详情"
:visible.sync="dialogVisible"
width="50%"
:before-close="handleClose"
:close-on-click-modal="false"
>
<div v-if="messageDetail" class="message-content">
</div>
<div v-else>
<p>加载中...</p>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="handleClose">关闭</el-button>
</span>
</el-dialog>
</template>
<script>
import { getMessageDetail } from '@/api/msg/msgCenter';
export default {
dicts: ['vehicle_type'],
props: {
visible: {
type: Boolean,
default: false,
},
messageId: {
type: Number,
required: true,
},
},
data() {
return {
dialogVisible: this.visible,
messageDetail: null,
};
},
created() {
this.$watch('visible', (newVal) => {
console.log('watch 监听到 visible 变化:', newVal);
this.dialogVisible = newVal;
if (newVal) {
this.fetchMessageDetail();
}
}, { immediate: true });
},
methods: {
handleClose() {
this.$emit('update:visible', false);
},
fetchMessageDetail() {
console.log("654645");
if (!this.messageId) {
console.error('messageId 为空,无法获取消息详情');
return;
}
console.log("666");
getMessageDetail(this.messageId).then(response => {
this.messageDetail = response.data;
})
.catch(error => {
console.error('获取消息详情失败', error);
this.$message.error('获取消息详情失败');
});
},
},
};
</script>
以此记录