效果图:
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-pagination
small
: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-icon
class="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-body
class="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-button
style="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, // 用户ID
status: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 = rows
this.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 300000
this.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.messageContent
if (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预览
// 将文件流转换为ArrayBuffer
const arrayBuffer = await response.arrayBuffer();
// 使用xlsx插件解析ArrayBuffer为Workbook对象
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
// 获取第一个Sheet
const 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 fileURL
fileURL = this.baseUrl + response.msg
if (type === 'doc' || type === 'docx') {
this.docSrc = fileURL
this.DocSync = true
}
})
},
showNotification() {
// 如果已有通知实例且未关闭,先关闭
if (this.notificationInstance) {
this.$notify.closeAll()
}
const h = this.$createElement
const notificationNode = h(CustomNotification, {
key: this.notificationKey, // 添加唯一key
props: {
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 = localNoticeList
msgList & 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 = true
this.msg = response.data;
// this.form.messageTitle = data.messageTitle
// this.form.messageContent = data.messageContent
if (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.$createElement
const notificationNode = h(CustomNotification, {
key: this.notificationKey, // 添加唯一key
props: {
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 = localNoticeList
msgList & 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 = rows
this.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()// 启动轮询 后台发送消息时 自动触发
},
已有弹窗只更新数据 不弹窗