New file |
| | |
| | | // src/main.js |
| | | import Vue from "vue"; |
| | | import Cookies from "js-cookie"; |
| | | import "babel-polyfill"; |
| | | import Element from "element-ui"; |
| | | import "./assets/styles/element-variables.scss"; |
| | | import "@/assets/styles/index.scss"; |
| | | import "@/assets/styles/ruoyi.scss"; |
| | | import App from "./App"; |
| | | import store from "./store"; |
| | | import router from "./router"; |
| | | import directive from "./directive"; |
| | | import plugins from "./plugins"; |
| | | import { download } from "@/utils/request"; |
| | | import Print from "vue-print-nb"; |
| | | import JsonExcel from "vue-json-excel"; |
| | | import "./assets/icons"; |
| | | import "./permission"; |
| | | import { getDicts } from "@/api/system/dict/data"; |
| | | import { getConfigKey } from "@/api/system/config"; |
| | | import { |
| | | parseTime, |
| | | resetForm, |
| | | addDateRange, |
| | | selectDictLabel, |
| | | selectDictLabels, |
| | | handleTree, |
| | | } from "@/utils/ruoyi"; |
| | | import Pagination from "@/components/Pagination"; |
| | | import Editor from "@/components/Editor"; |
| | | import FileUpload from "@/components/FileUpload"; |
| | | import ImageUpload from "@/components/ImageUpload"; |
| | | import ImagePreview from "@/components/ImagePreview"; |
| | | import DictTag from "@/components/DictTag"; |
| | | import VueMeta from "vue-meta"; |
| | | import DictData from "@/components/DictData"; |
| | | import * as echarts from "echarts"; |
| | | import VueBarcode from "vue-barcode"; |
| | | |
| | | Vue.component("downloadExcel", JsonExcel); |
| | | Vue.component("barcode", VueBarcode); |
| | | Vue.component("DictTag", DictTag); |
| | | Vue.component("Pagination", Pagination); |
| | | Vue.component("Editor", Editor); |
| | | Vue.component("FileUpload", FileUpload); |
| | | Vue.component("ImageUpload", ImageUpload); |
| | | Vue.component("ImagePreview", ImagePreview); |
| | | |
| | | Vue.prototype.getDicts = getDicts; |
| | | Vue.prototype.getConfigKey = getConfigKey; |
| | | Vue.prototype.parseTime = parseTime; |
| | | Vue.prototype.resetForm = resetForm; |
| | | Vue.prototype.addDateRange = addDateRange; |
| | | Vue.prototype.selectDictLabel = selectDictLabel; |
| | | Vue.prototype.selectDictLabels = selectDictLabels; |
| | | Vue.prototype.download = download; |
| | | Vue.prototype.handleTree = handleTree; |
| | | Vue.prototype.$echarts = echarts; |
| | | |
| | | // 保留 $showNotification,支持手动触发通知 |
| | | Vue.prototype.$showNotification = function (type, title, message, onClick) { |
| | | console.log('触发通知:', { type, title, message }, new Date().toLocaleString()); |
| | | Vue.prototype.$notify({ |
| | | title, |
| | | message, |
| | | type, |
| | | duration: 5000, |
| | | position: 'top-right', |
| | | offset: 50, |
| | | onClick, |
| | | customClass: 'global-notification', |
| | | appendTo: document.body |
| | | }); |
| | | }; |
| | | |
| | | // 监听路由变化 |
| | | router.afterEach(() => { |
| | | console.log('路由切换完成,当前路径:', router.currentRoute.path); |
| | | }); |
| | | |
| | | const app = new Vue({ |
| | | el: "#app", |
| | | router, |
| | | store, |
| | | render: (h) => h(App) |
| | | }); |
| | | |
| | | Vue.use(directive); |
| | | Vue.use(plugins); |
| | | Vue.use(VueMeta); |
| | | Vue.use(Print); |
| | | Vue.use(Element, { |
| | | size: Cookies.get("size") || "medium", |
| | | }); |
| | | DictData.install(); |
| | | |
| | | Vue.config.productionTip = false; |
| | |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // 新增参数配置 |
| | | export function yidu(data) { |
| | | return request({ |
| | | url: '/system/notice/readNotice ', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | // 修改参数配置 |
| | | export function updateConfig(data) { |
| | | return request({ |
| | |
| | | import "./assets/icons"; |
| | | import "./permission"; |
| | | import { getDicts } from "@/api/system/dict/data"; |
| | | import { getConfigKey } from "@/api/system/config"; |
| | | import { getConfigKey, yidu } from "@/api/system/config"; |
| | | import { |
| | | parseTime, |
| | | resetForm, |
| | |
| | | import DictData from "@/components/DictData"; |
| | | import * as echarts from "echarts"; |
| | | import VueBarcode from "vue-barcode"; |
| | | import { initWebSocket, closeWebSocket } from "@/utils/websocket"; |
| | | import RightToolbar from "@/components/RightToolbar" |
| | | |
| | | // 注册全局组件 |
| | | Vue.component("downloadExcel", JsonExcel); |
| | | Vue.component("barcode", VueBarcode); |
| | | Vue.component("DictTag", DictTag); |
| | |
| | | Vue.component("FileUpload", FileUpload); |
| | | Vue.component("ImageUpload", ImageUpload); |
| | | Vue.component("ImagePreview", ImagePreview); |
| | | Vue.component("RightToolbar", RightToolbar); |
| | | |
| | | // 注册全局方法 |
| | | Vue.prototype.getDicts = getDicts; |
| | | Vue.prototype.getConfigKey = getConfigKey; |
| | | Vue.prototype.parseTime = parseTime; |
| | |
| | | Vue.prototype.handleTree = handleTree; |
| | | Vue.prototype.$echarts = echarts; |
| | | |
| | | // 保留 $showNotification,支持手动触发通知 |
| | | Vue.prototype.$showNotification = function (type, title, message, onClick) { |
| | | console.log('触发通知:', { type, title, message }, new Date().toLocaleString()); |
| | | Vue.prototype.$notify({ |
| | | title, |
| | | message, |
| | | // 通知管理:跟踪当前通知和偏移量 |
| | | const notificationManager = { |
| | | notifications: [], // 存储当前显示的通知实例 |
| | | baseOffset: 50, // 基础偏移量 |
| | | notificationHeight: 80, // 每个通知的估计高度(包括间距) |
| | | maxNotifications: 5, // 最大同时显示的通知数量 |
| | | addNotification(notification) { |
| | | if (this.notifications.length >= this.maxNotifications) { |
| | | // 关闭最早的通知 |
| | | const oldest = this.notifications.shift(); |
| | | oldest.close(); |
| | | } |
| | | this.notifications.push(notification); |
| | | // 设置动态 offset 和 z-index |
| | | notification.offset = this.baseOffset + this.notifications.length * this.notificationHeight; |
| | | notification.customClass += ` notification-${this.notifications.length}`; // 为 z-index 添加唯一类 |
| | | // 监听通知关闭 |
| | | notification.onClose = () => { |
| | | const index = this.notifications.indexOf(notification); |
| | | if (index > -1) { |
| | | this.notifications.splice(index, 1); |
| | | // 更新后续通知的 offset |
| | | this.updateOffsets(); |
| | | } |
| | | }; |
| | | }, |
| | | updateOffsets() { |
| | | this.notifications.forEach((notification, index) => { |
| | | notification.offset = this.baseOffset + (index + 1) * this.notificationHeight; |
| | | notification.customClass = notification.customClass.replace(/notification-\d+/, `notification-${index + 1}`); |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 全局通知方法,添加“已读”按钮 |
| | | Vue.prototype.$showNotification = function (type, title, message, onClick, noticeId) { |
| | | console.log('触发通知:', { type, title, message, noticeId, noticeIdType: typeof noticeId }); // 调试:记录 noticeId 和类型 |
| | | const h = this.$createElement; |
| | | const notification = this.$notify({ |
| | | title: title, |
| | | message: h('div', { class: 'notification-content' }, [ |
| | | h('div', { class: 'notification-message' }, message), |
| | | noticeId ? h('el-button', { |
| | | class: 'read-button', |
| | | style: { marginLeft: '10px', float: 'right', cursor: 'pointer' }, |
| | | props: { type: 'primary', size: 'mini' }, |
| | | on: { |
| | | click: async () => { |
| | | console.log('点击“已读”按钮,noticeId:', noticeId, 'type:', typeof noticeId); // 调试:记录点击时的 noticeId |
| | | try { |
| | | await yidu({ noticeId: String(noticeId) }); |
| | | console.log(`通知 ${noticeId} 已标记为已读`); |
| | | this.$message.success('标记为已读成功'); |
| | | notification.close(); |
| | | } catch (error) { |
| | | console.error('标记已读失败:', error, 'noticeId:', noticeId); |
| | | this.$message.error('标记已读失败'); |
| | | } |
| | | } |
| | | } |
| | | }, '已读') : null |
| | | ]), |
| | | type, |
| | | duration: 5000, |
| | | position: 'top-right', |
| | | offset: 50, |
| | | onClick, |
| | | offset: notificationManager.baseOffset, // 初始 offset |
| | | onClick: null, // 不跳转 |
| | | customClass: 'global-notification', |
| | | dangerouslyUseHTMLString: false, |
| | | appendTo: document.body |
| | | }); |
| | | notificationManager.addNotification(notification); // 添加到通知管理 |
| | | }; |
| | | |
| | | // 监听路由变化 |
| | |
| | | console.log('路由切换完成,当前路径:', router.currentRoute.path); |
| | | }); |
| | | |
| | | // 定义 WebSocket 初始化标志,防止重复连接 |
| | | let isWebSocketInitialized = false; |
| | | |
| | | const app = new Vue({ |
| | | el: "#app", |
| | | router, |
| | | store, |
| | | render: (h) => h(App) |
| | | render: (h) => h(App), |
| | | mounted() { |
| | | const token = store.state.user.token || Cookies.get('token') || ''; |
| | | if (token && !isWebSocketInitialized) { |
| | | console.log('初始化 WebSocket,Token:', token); |
| | | isWebSocketInitialized = true; |
| | | initWebSocket(token, (type, data) => { |
| | | console.log('WebSocket 收到消息:', { type, data }); // 调试:记录原始数据 |
| | | if (type === 'error') { |
| | | Vue.prototype.$showNotification.call(this, 'error', '错误', data); |
| | | return; |
| | | } |
| | | try { |
| | | if (typeof data === 'string' && data.trim().startsWith('{')) { |
| | | // 替换大整数字段,防止精度丢失 |
| | | const normalizedData = data.replace(/"(noticeId|notice_id|id)":\s*(\d+)/g, '"$1":"$2"'); |
| | | const message = JSON.parse(normalizedData); |
| | | console.log('WebSocket 解析后消息:', message); // 调试:记录解析后的消息 |
| | | if (message.noticeId || message.notice_id || message.id) { |
| | | const noticeTypeLabel = message.noticeType === '1' ? '通知' : '公告'; |
| | | const noticeTitle = message.noticeTitle ? message.noticeTitle.replace(/<[^>]+>/g, '') : '无标题'; |
| | | const contentPreview = message.noticeContent |
| | | ? message.noticeContent.replace(/<[^>]+>/g, '').substring(0, 20) + '...' |
| | | : '无内容'; |
| | | const noticeId = String(message.noticeId || message.notice_id || message.id); |
| | | console.log('准备触发通知,noticeId:', noticeId, 'type:', typeof noticeId); // 调试:记录传递的 noticeId |
| | | Vue.prototype.$showNotification.call( |
| | | this, |
| | | 'success', |
| | | `新${noticeTypeLabel}`, |
| | | `${noticeTitle} - ${contentPreview}`, |
| | | null, |
| | | noticeId |
| | | ); |
| | | } else { |
| | | console.log('未知消息类型:', message); |
| | | Vue.prototype.$showNotification.call(this, 'info', '消息', '收到未知格式的消息'); |
| | | } |
| | | } else { |
| | | console.log('WebSocket 非 JSON 消息:', data); |
| | | |
| | | } |
| | | } catch (error) { |
| | | console.error('消息解析失败:', error, '原始数据:', data); |
| | | Vue.prototype.$showNotification.call(this, 'error', '消息解析失败', `服务器回应字符串: ${data}`); |
| | | } |
| | | }); |
| | | } else if (!token) { |
| | | console.error('未找到 token,无法初始化 WebSocket'); |
| | | } else { |
| | | console.log('WebSocket 已初始化,跳过重复连接'); |
| | | } |
| | | }, |
| | | beforeDestroy() { |
| | | if (isWebSocketInitialized) { |
| | | closeWebSocket(); |
| | | isWebSocketInitialized = false; |
| | | console.log('Vue 实例销毁,WebSocket 已清理'); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | // 注册插件 |
| | | Vue.use(directive); |
| | | Vue.use(plugins); |
| | | Vue.use(VueMeta); |
| | |
| | | // src/utils/websocket.js |
| | | let ws = null; |
| | | let reconnectAttempts = 0; |
| | | let pingTimer = null; // 用于管理心跳定时器 |
| | | const maxReconnectAttempts = 5; |
| | | const reconnectInterval = 5000; // 5秒重连间隔 |
| | | const pingInterval = 5000; // 5秒发送ping |
| | |
| | | return; |
| | | } |
| | | |
| | | const wsUrl = `ws://192.168.1.2:5011/ws?token=${token}`; |
| | | // 如果 ws 已存在且连接活跃,则跳过初始化 |
| | | if (ws && ws.readyState === WebSocket.OPEN) { |
| | | console.log('WebSocket 已连接,跳过重复初始化'); |
| | | return; |
| | | } |
| | | |
| | | // 如果 ws 存在但未关闭,先关闭旧连接 |
| | | if (ws) { |
| | | console.warn('发现旧 WebSocket 连接,正在关闭...'); |
| | | closeWebSocket(); |
| | | } |
| | | |
| | | const wsUrl = `ws://192.168.1.244:5011/ws?token=${token}`; |
| | | ws = new WebSocket(wsUrl); |
| | | |
| | | ws.onopen = () => { |
| | | console.log('WebSocket 连接成功'); |
| | | reconnectAttempts = 0; |
| | | reconnectAttempts = 0; // 重置重连计数 |
| | | |
| | | // 启动心跳机制 |
| | | const pingTimer = setInterval(() => { |
| | | if (ws.readyState === WebSocket.OPEN) { |
| | | console.log('发送 ping 消息'); |
| | | if (pingTimer) clearInterval(pingTimer); // 清理旧定时器 |
| | | pingTimer = setInterval(() => { |
| | | if (ws && ws.readyState === WebSocket.OPEN) { |
| | | ws.send('ping'); |
| | | } else { |
| | | console.warn('WebSocket 未连接,停止 ping'); |
| | | clearInterval(pingTimer); |
| | | pingTimer = null; |
| | | } |
| | | }, pingInterval); |
| | | }; |
| | |
| | | console.log('WebSocket 收到原始消息:', data); |
| | | if (data === 'pong') { |
| | | console.log('收到 pong 响应,连接活跃'); |
| | | reconnectAttempts = 0; // 重置重连计数,确保活跃连接不触发重连 |
| | | return; |
| | | } |
| | | onMessage('message', data); |
| | |
| | | |
| | | ws.onerror = (error) => { |
| | | console.error('WebSocket 错误:', error); |
| | | onMessage('error', 'WebSocket 连接错误'); |
| | | }; |
| | | |
| | | ws.onclose = () => { |
| | | console.warn('WebSocket 连接关闭'); |
| | | console.warn('WebSocket 连接关闭,时间:', new Date()); |
| | | if (pingTimer) { |
| | | clearInterval(pingTimer); |
| | | pingTimer = null; |
| | | } |
| | | if (reconnectAttempts < maxReconnectAttempts) { |
| | | reconnectAttempts++; |
| | | console.log(`尝试重连 (${reconnectAttempts}/${maxReconnectAttempts})...`); |
| | |
| | | |
| | | // 清理 WebSocket |
| | | window.addEventListener('beforeunload', () => { |
| | | if (ws) { |
| | | ws.close(); |
| | | ws = null; |
| | | console.log('WebSocket 已清理'); |
| | | } |
| | | }); |
| | | closeWebSocket(); |
| | | }, { once: true }); // 确保事件监听只添加一次 |
| | | } |
| | | |
| | | export function closeWebSocket() { |
| | | if (ws) { |
| | | ws.close(); |
| | | ws = null; |
| | | if (pingTimer) { |
| | | clearInterval(pingTimer); |
| | | pingTimer = null; |
| | | } |
| | | console.log('WebSocket 已关闭'); |
| | | } |
| | | } |