Vue+Notification 自定义消息通知组件 支持数据分页 实时更新
效果图:
message.vue 消息组件 子组件
<template><div class="custom-notification"><div class="content"><span @click="gotoMessageList(currentMessage.split('=')[1])">{{ currentMessage.split('=')[0] }}</span></div><div class="footer"><div class="left-buttons"><el-button size="mini" type="text" @click="handleIgnoreAll">忽略全部</el-button></div><div class="pagination"><el-paginationsmall:key="paginationKey":current-page.sync="currentPage":page-size="1":total="localNoticeList.length"layout="prev,slot, next"prev-text="<"next-text=">"@prev-click="currentPage--"@next-click="currentPage++"><span class="pagination-text">{{ currentPage }} / {{ totalPages }}</span></el-pagination></div><div class="right-button"><el-button type="primary" size="mini" @click="handleAccept(currentMessage.split('=')[1])">接受</el-button></div></div></div>
</template><script>export default {props: {messages: {type: Array,default: () => []},total:{type:Number,default:0},noticeList:{type:Array,default:()=>[]}},data() {return {localNoticeList: [], // 新增本地副本currentPage: 1,totalPage: 5,paginationKey: 0 // 分页组件独立key}},watch: {noticeList: {deep:true,immediate: true,handler(newVal) {this.localNoticeList = JSON.parse(JSON.stringify(newVal))// 当消息更新时自动重置页码this.currentPage = Math.min(this.currentPage, newVal.length)this.paginationKey++ // 强制分页组件重置// this.updateList(this.localNoticeList)}}},computed: {totalPages() {return this.localNoticeList.length || 1},currentMessage() {return this.localNoticeList[this.currentPage - 1]?.messageTitle +'='+this.localNoticeList[this.currentPage - 1]?.messageId || ''}},methods: {handleLater() {// 稍后提醒逻辑this.$emit('later')},handleIgnoreAll() {// 忽略全部 将消息全部设为已读this.$emit('ignore-all',this.localNoticeList) // 触发父级关闭事件},handleAccept(msgId) {//接收this.$emit('gotoMessageList',msgId)},gotoMessageList(msgId){this.$emit('gotoMessageList',msgId)},// 通过事件通知父组件updateList(newList) {this.$emit('update-list', newList)}}}
</script><style scoped>.custom-notification {padding: 15px 0px 15px 0px;width: 270px;}.header {display: flex;align-items: center;margin-bottom: 12px;}.content{cursor: pointer;font-weight: bold;font-size: 13px;}.header i {margin-right: 8px;font-size: 16px;}.footer {display: flex;justify-content: space-between;align-items: center;margin-top: 15px;}.pagination {display: flex;align-items: center;gap: 8px;}.pagination i {cursor: pointer;color: #606266;}.left-buttons .el-button--text {color: #909399;}.pagination-text{text-align: center;}
</style>
消息父组件:
<template><div><el-badge :value="total" class="item" style="line-height: 40px; position: relative;"><span @click="bellClick"><el-iconclass="el-icon-bell"style="font-size: 20px; cursor: pointer; transform: translateY(1.1px)"></el-icon></span></el-badge><!-- 添加或修改我的消息对话框 --><el-dialog title="我的消息" :visible.sync="open" width="500px" append-to-body><el-form ref="form" :model="form" :rules="rules" label-width="80px"><el-form-item label="消息标题" prop="messageTitle"><el-input disabled v-model="form.messageTitle" placeholder="请输入消息标题" /></el-form-item><el-form-item label="消息内容"><editor :readOnly="true" v-model="form.messageContent" :min-height="192" /></el-form-item></el-form><div slot="footer" class="dialog-footer"><el-button @click="cancel">关闭</el-button></div></el-dialog><!-- word预览弹窗 --><el-dialog fullscreen title="word预览" :visible.sync="dialogVisibleWord" append-to-body><vue-office-docx :src="previewWordOffice" @rendered="renderedHandler" @error="errorHandler" /></el-dialog><!-- doc预览 --><el-dialog fullscreen title="word预览" :visible.sync="DocSync" append-to-body><VueOfficePdf :src="docSrc" /></el-dialog><el-dialog:title="msg.messageTitle":visible.sync="messageVisible"width="800px"append-to-bodyclass="my-message__dialog"@close="closeMsgModel"><div><span class="contentImg" v-html="msg.messageContent"></span></div><div class="my-message__filebox" v-if="fileList.length > 0"><div>附件:</div><ul class="my-message__ul"><li class="my-message__li" v-for="(item, index) in fileList" :key="index"><div class="my-message__li--title">{{ item.name }}</div><div class="my-message__li--opt"><el-buttonstyle="color: green"@click.stop="handleShow(index, item)"type="text"icon="el-icon-view"size="mini"v-if="getFileExtension(item.name) !== 'zip' && getFileExtension(item.name) !== 'rar'">查看</el-button></div></li></ul></div><div slot="footer" class="dialog-footer"><el-button @click.stop="closeMsgModel">关闭</el-button></div></el-dialog>
<!-- <custom-notification></custom-notification>--></div>
</template><script>
import Vue from 'vue'
import { mapGetters } from 'vuex';//引入VueOfficeDocx组件
import VueOfficeDocx from '@vue-office/docx';
import { listMyUnreadNotice, getSysmessage, listSysmessage } from '@/api/system/sysmessage';
import { fileDownload, previewDocumentAPI } from '@/api/files/files';
import XLSX from 'xlsx';
import events from '@/utils/events';
import VueOfficePdf from '@vue-office/pdf';
import Notification from 'element-ui/lib/notification'
import CustomNotification from '@/components/Bell/component/message'
export default {name: 'index',components: { VueOfficePdf, VueOfficeDocx,CustomNotification },data() {return {DocSync:false,docSrc:'',baseUrl: 'http://' + window.location.host + process.env.VUE_APP_BASE_API,dialogVisibleWord: false,previewWordOffice: '',noticeList: [],noticeListNew:[],//pollInterval: null,// 轮询定时器lastCheckTime: null,// 最后检查时间notificationKey:0,isFirstCheck: true,// 首次检查标志templateVisible: false,messageVisible:false,showAllBtn: false,msg:{},form: {},rules: {},open: false,queryParams: {id: null,status: '0'},fileList: [],total: 0,totalNew:0,notificationInstance:null //通知示例};},computed: {...mapGetters(['isDot', 'id'])},created() {// on接收 emit发送this.clearPolling()this.getUnRead(); //查询是否有未读消息通知this.getUnReadNew(); //初始化调用获取未读消息events.$on('noticePush', this.getUnRead);events.$on('noticePushByMsg', this.getUnRead);this.$nextTick(() => {events.$on('noticeCheckByMsg', this.getUnRead);// events.$on('noticeCancel',this.cancel);});events.$on('noticePush',this.getUnReadNew)},mounted() {},beforeDestroy() {// this.clearPolling()},// 关闭定时器deactivated(){// this.clearPolling()},methods: {// 跳转链接clickSpan() {// 点击跳转我的消息页this.$router.push('/task/myMessage');},/**word预览组件回调函数 */renderedHandler() {console.log('渲染完成');},errorHandler() {console.log('渲染失败');},/** 查询是否存在未读消息 根据当前用户匹配消息表中得ID */getUnRead() {this.queryParams.id = this.id;// 查询未读消息listSysmessage(this.queryParams).then((res) => {if (res.code == 200) {if (res.rows.length > 0) {// 这里的total 要请求我的代办的接口 然后加起来this.total = res.total;this.noticeList = res.rows;this.$store.dispatch('app/setIsDot', true);} else {this.total = null;this.noticeList = [];this.$store.dispatch('app/setIsDot', false);}this.$emit('getUnRead', this.noticeList);}});},//查询未读消息重构 添加定时任务 每隔20分钟调用一次/** 核心查询方法 */async getUnReadNew() {try {const params = {id: this.id, // 用户IDstatus:0,}const { code, rows, total } = await listSysmessage(params)if (code === 200) {this.handleNewMessages(rows, total)this.lastCheckTime = new Date()}} catch (error) {console.error('消息检查失败:', error)}},/** 处理新消息逻辑 */handleNewMessages(rows, total) {// 数据同步this.noticeListNew = rowsthis.totalNew = rows.length//total// 仅当有新消息时触发通知if (rows.length > 0) {if (this.notificationInstance) {// 手动创建组件实例// 通过 key 变化强制更新组件this.notificationKey = Date.now()// const NotificationComponent = new Vue({// render: h => h(CustomNotification, {// props: {// noticeList: [...this.noticeListNew],// total: this.noticeListNew.length// },// on: {// // 事件监听...// }// })// }).$mount()// // 保存组件实例引用// this.customNotificationInstance = NotificationComponent.$children[0]// this.customNotificationInstance = this.notificationInstance.$children[0]//已有弹窗时仅更新数据this.customNotificationInstance.$set(this.customNotificationInstance,'noticeList',[...rows])this.customNotificationInstance.$forceUpdate()} else {this.showNotification()}}this.$emit('update:noticeList', rows)window.clearInterval(this.pollInterval)this.setupPolling()// 启动轮询 后台发送消息时 自动触发},/** 判断是否有新消息 */hasNewMessages(newRows) {return newRows.some(item =>!this.noticeListNew.find(old => old.messageId === item.messageId))},/** 轮询控制 */setupPolling() {// 5分钟调用一次 60000 * 5 300000this.clearPolling() // 清除已有定时器this.pollInterval = window.setInterval(() => {this.getUnReadNew()},300000)},/** 销毁定时器*/clearPolling() {if (this.pollInterval) {window.clearInterval(this.pollInterval)this.pollInterval = null}},bellClick() {this.getUnRead();},cancel() {this.open = false;this.getUnRead();},cancelTempDialog() {this.templateVisible = false;this.getUnRead();},// 查看更多showAll() {this.$router.push({ path: '/monitor/myMessage' });},//查看消息 弹窗显示handleRead(data) {const messageId = data.messageId;getSysmessage(messageId).then((response) => {this.form = response.data;// this.form.messageTitle = data.messageTitle// this.form.messageContent = data.messageContentif (this.form.fileJSON && JSON.parse(this.form.fileJSON)) {this.fileList = JSON.parse(this.form.fileJSON);} else {this.fileList = [];}if (this.form.messageType === '3') {this.templateVisible = true;} else {this.open = true;this.title = '查看我的消息';}});},toLink(url) {if (url) {if (url.indexOf('http://') > -1 || url.indexOf('https://') > -1) {window.open(url);} else {this.$router.push({ path: url });}}this.cancelTempDialog();},handleDownLoad(index, file) {const param = {code: file.code,fileName: file.name};fileDownload(param).then((response) => {if (response) {const url = window.URL.createObjectURL(new Blob([response]));const link = document.createElement('a');link.href = url;link.setAttribute('download', file.name); // 设置下载的文件名document.body.appendChild(link);link.click();document.body.removeChild(link);}}).catch((error) => {});},// 预览async handleShow(index, file) {const param = {code: file.code,fileName: file.name};const type = file.name.split('.')[file.name.split('.').length - 1];await fileDownload(param).then(async (response) => {if (type === 'pdf' || type === 'PDF') {//pdf预览let blob = new Blob([response], {type: 'application/pdf'});let fileURL = window.URL.createObjectURL(blob);window.open(fileURL, '_blank'); //这里是直接打开新窗口} else if (type === 'txt' || type === 'TXT') {//txt预览// 将文件流转换为文本const text = await response.text();// 在新窗口中打开文本内容const newWindow = window.open();newWindow.document.write(`<pre>${text}</pre>`);} else if (type === 'xls' || type === 'xlsx') {//excel预览// 将文件流转换为ArrayBufferconst arrayBuffer = await response.arrayBuffer();// 使用xlsx插件解析ArrayBuffer为Workbook对象const workbook = XLSX.read(arrayBuffer, { type: 'array' });// 获取第一个Sheetconst sheetName = workbook.SheetNames[0];const sheet = workbook.Sheets[sheetName];// 将Sheet转换为HTML表格const html = XLSX.utils.sheet_to_html(sheet);// 添加表格线样式const styledHtml = `<style>table {border-collapse: collapse;}td, th {border: 1px solid black;padding: 8px;}</style>${html}`;// 在新窗口中打开HTML内容const newWindow = window.open();setTimeout(() => {newWindow.document.title = sheetName;}, 0);newWindow.document.write(styledHtml);} else if (type === 'doc' || type === 'docx') {if (type === 'doc') {// doc预览 需要转码 定时任务this.handleMany(file.code,type)}else{// docx预览const fileUrl = window.URL.createObjectURL(response);this.previewWordOffice = fileUrl;this.dialogVisibleWord = true;}}else if (type === 'png' || type === 'jpg') {const fileURL = window.URL.createObjectURL(response);this.qrCode = fileURL;this.dialogVisible = true;console.log(fileURL, 'fileURL的值为-----------');this.saveFile = file;}}).catch((error) => {});},// doc 预览handleMany(code,type) {previewDocumentAPI({code: code}).then(response => {let fileURLfileURL = this.baseUrl + response.msgif (type === 'doc' || type === 'docx') {this.docSrc = fileURLthis.DocSync = true}})},showNotification() {// 如果已有通知实例且未关闭,先关闭if (this.notificationInstance) {this.$notify.closeAll()}const h = this.$createElementconst notificationNode = h(CustomNotification, {key: this.notificationKey, // 添加唯一keyprops: {noticeList: [...this.noticeListNew], // 传递消息列表total:this.noticeListNew.length},on: {// 添加事件监听'update-list': this.handleListUpdate,'close-notification': () => this.$notify.closeAll(),'later': () => {this.$notify.closeAll()},'ignore-all': (localNoticeList) => {const msgList = localNoticeListmsgList & msgList.forEach((item)=>{this.readMessageAll(item.messageId)})this.$notify.closeAll()this.getUnRead()},'accept': () => {console.log('接受处理逻辑')},close: () => {this.notificationInstance = null},'gotoMessageList': (msgId) => {this.readMessage(msgId)this.getUnRead()//阅读了那一条 根据ID匹配然后进行过滤this.noticeListNew = this.noticeListNew.filter(item=> item.messageId !== Number(msgId))if(this.noticeListNew.length > 0){this.customNotificationInstance.$set(this.customNotificationInstance,'noticeList',[...this.noticeListNew])this.customNotificationInstance.$forceUpdate()}else{this.$notify.closeAll()}}}})this.notificationInstance = this.$notify.info({title: '消息通知',message: notificationNode,customClass:'bellClass',position: 'bottom-right',duration:60000,onClose: () => {this.notificationInstance = null}});// 获取组件实例this.customNotificationInstance = notificationNode.componentInstance},// 处理列表更新事件handleListUpdate(newList) {this.noticeListNew = newList},//查看消息 弹窗显示readMessage(msgId) {const messageId = msgId;getSysmessage(messageId).then((response) => {this.messageVisible = truethis.msg = response.data;// this.form.messageTitle = data.messageTitle// this.form.messageContent = data.messageContentif (this.msg.fileJSON && JSON.parse(this.msg.fileJSON)) {this.fileList = JSON.parse(this.msg.fileJSON);} else {this.fileList = [];}});},//批量已读readMessageAll(msgId) {const messageId = msgId;getSysmessage(messageId).then((response) => {});},getFileExtension(filename) {// 获取最后一个点的位置const lastDotIndex = filename.lastIndexOf('.')// 如果没有点或者点在第一个位置,返回空字符串if (lastDotIndex === -1 || lastDotIndex === 0) {return ''}// 截取点后的字符串作为后缀return filename.substring(lastDotIndex + 1)},closeMsgModel(){this.messageVisible = false}}
};
</script><style rel="stylesheet/scss" lang="scss" scoped>
.my-message {::v-deep .el-checkbox__label {vertical-align: middle;}&__editor {::v-deep .ql-toolbar {display: none;}::v-deep .ql-container {// border-top: 1px solid #ccc !important;border: none !important;}::v-deep .ql-editor {max-height: 500px;}}&__ul {list-style-type: none;// border: 1px solid #ccc;// border-radius: 5px;}&__filebox {margin-top: 10px;padding-left: 20px;padding-right: 20px;background: #e6f7ff;border-radius: 5px;}&__dialog {::v-deep .el-dialog__body {padding-bottom: 0px !important;}}&__li {width: 100%;display: flex;justify-content: space-between;margin-bottom: 5px;&--opt {padding-right: 10px;}}
}::v-deep .el-badge__content.is-fixed {transform: translateY(-22%) translateX(100%);
}::v-deep .el-badge__content {font-size: 11px;padding: 0 5px;
}
::v-deep .is-fullscreen {.el-dialog__body {/* padding: 15px 20px !important; */color: #606266;font-size: 14px;word-break: break-all;height: calc(100vh - 40px);overflow: auto;}
}</style>
<style>.bellClass .el-notification__icon::before {content: '\e7ba'; /* 需要更换的图标编码 */color: #8BC34A; /* 图标颜色 */}.bellClass {/*border: 1px solid #8bc34a57; !* 修改边框样式 f55e00 *!*/box-shadow: 0 2px 10px 0 rgb(0 0 0 / 48%) !important;}.el-notification__title {color: #000; /* 修改标题字体颜色 */font-size:12px;font-weight: 100;}.bellClass .el-notification__icon {height: 20px;width: 20px;font-size: 18px;transform: translateY(-2px);}.bellClass .el-notification__group {margin-left: 8px;margin-right: 8px;}
</style>
核心代码:
这个显示通知的核心方法, 自定义消息组件 通过$createElement创建Vnode挂载到message中,
数据传递的话通过props传递,通过on自定义事件
showNotification() {// 如果已有通知实例且未关闭,先关闭if (this.notificationInstance) {this.$notify.closeAll()}const h = this.$createElementconst notificationNode = h(CustomNotification, {key: this.notificationKey, // 添加唯一keyprops: {noticeList: [...this.noticeListNew], // 传递消息列表total:this.noticeListNew.length},on: {// 添加事件监听'update-list': this.handleListUpdate,'close-notification': () => this.$notify.closeAll(),'later': () => {this.$notify.closeAll()},'ignore-all': (localNoticeList) => {const msgList = localNoticeListmsgList & msgList.forEach((item)=>{this.readMessageAll(item.messageId)})this.$notify.closeAll()this.getUnRead()},'accept': () => {console.log('接受处理逻辑')},close: () => {this.notificationInstance = null},'gotoMessageList': (msgId) => {this.readMessage(msgId)this.getUnRead()//阅读了那一条 根据ID匹配然后进行过滤this.noticeListNew = this.noticeListNew.filter(item=> item.messageId !== Number(msgId))if(this.noticeListNew.length > 0){this.customNotificationInstance.$set(this.customNotificationInstance,'noticeList',[...this.noticeListNew])this.customNotificationInstance.$forceUpdate()}else{this.$notify.closeAll()}}}})this.notificationInstance = this.$notify.info({title: '消息通知',message: notificationNode,customClass:'bellClass',position: 'bottom-right',duration:60000,onClose: () => {this.notificationInstance = null}});// 获取组件实例this.customNotificationInstance = notificationNode.componentInstance},
获取当前弹窗的实例时数据通信的关键!
/** 处理新消息逻辑 */handleNewMessages(rows, total) {// 数据同步this.noticeListNew = rowsthis.totalNew = rows.length//total// 仅当有新消息时触发通知if (rows.length > 0) {if (this.notificationInstance) {// 手动创建组件实例// 通过 key 变化强制更新组件this.notificationKey = Date.now()// const NotificationComponent = new Vue({// render: h => h(CustomNotification, {// props: {// noticeList: [...this.noticeListNew],// total: this.noticeListNew.length// },// on: {// // 事件监听...// }// })// }).$mount()// // 保存组件实例引用// this.customNotificationInstance = NotificationComponent.$children[0]// this.customNotificationInstance = this.notificationInstance.$children[0]//已有弹窗时仅更新数据this.customNotificationInstance.$set(this.customNotificationInstance,'noticeList',[...rows])this.customNotificationInstance.$forceUpdate()} else {this.showNotification()}}this.$emit('update:noticeList', rows)window.clearInterval(this.pollInterval)this.setupPolling()// 启动轮询 后台发送消息时 自动触发},
已有弹窗只更新数据 不弹窗