补发团队项目04-消息提醒(websocket和springTask结合使用 苍穹外卖改编实现)
这边项目需求对于未审核工单超时提醒
我们也做了简单实现
首先给出前端代码 这边的苍穹外卖前端实现消息提醒是在顶部导航栏 为了完成需求 做了简单改编 这边仅作参考
src\layout\components\Navbar\index.vue
<template>
<div class="navbar">
<div class="statusBox">
<hamburger id="hamburger-container"
:is-active="sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar" />
<!-- 添加面包屑导航 -->
<breadcrumb class="breadcrumb-container" />
<!-- 添加系统标题 -->
<div class="system-title">设备管理系统</div>
</div>
<div :key="restKey"
class="right-menu">
<!-- 添加通知中心 -->
<div class="notification-center">
<el-badge :value="ountUnread > 0 ? ountUnread : ''" :max="99" class="item">
<div class="navicon mesCenter" @click="goToMessageCenter">
<i></i>
<span>消息中心</span>
</div>
</el-badge>
</div>
<!-- 添加帮助中心 -->
<div class="help-center">
<div class="navicon helpCenter" @click="openHelpCenter">
<i class="el-icon-question"></i>
<span>帮助中心</span>
</div>
</div>
<!-- 添加主题切换 -->
<div class="theme-switch">
<div class="navicon themeSwitch" @click="toggleTheme">
<i :class="isDarkTheme ? 'el-icon-sunny' : 'el-icon-moon'"></i>
<span>{{ isDarkTheme ? '浅色模式' : '深色模式' }}</span>
</div>
</div>
<div class="rightStatus">
<audio ref="audioVo"
hidden>
<source src="./../../../assets/preview.mp3" type="audio/mp3" />
</audio>
<audio ref="audioVo2"
hidden>
<source src="./../../../assets/reminder.mp3" type="audio/mp3" />
</audio>
</div>
<div class="avatar-wrapper">
<div :class="shopShow?'userInfo':''"
@mouseenter="toggleShow"
@mouseleave="mouseLeaves">
<el-button type="primary"
:class="shopShow?'active':''">
{{ realName }}<i class="el-icon-arrow-down" />
</el-button>
<div v-if="shopShow"
class="userList">
<p class="userInfoIcon"
@click="viewUserInfo">
个人信息<i class="el-icon-user" />
</p>
<p class="amendPwdIcon"
@click="handlePwd">
修改密码<i />
</p>
<p class="outLogin"
@click="logout">
退出登录<i />
</p>
</div>
</div>
</div>
</div>
<!-- 修改密码 -->
<Password :dialog-form-visible="dialogFormVisible"
@handleclose="handlePwdClose" />
<!-- end -->
<!-- 帮助中心弹窗 -->
<el-dialog
title="帮助中心"
:visible.sync="helpDialogVisible"
width="50%">
<div class="help-content">
<h3>设备管理系统使用指南</h3>
<el-collapse>
<el-collapse-item title="基本操作" name="1">
<div>系统导航:使用左侧菜单进行功能导航</div>
<div>消息通知:点击顶部导航栏的"消息中心"查看系统通知</div>
<div>个人设置:点击右上角用户名可进行密码修改和退出登录</div>
</el-collapse-item>
<el-collapse-item title="设备管理" name="2">
<div>设备登记:记录设备基本信息、规格参数等</div>
<div>设备维护:记录设备维护保养情况</div>
<div>设备报废:处理报废设备的流程</div>
</el-collapse-item>
<el-collapse-item title="常见问题" name="3">
<div>Q: 如何添加新设备?</div>
<div>A: 进入"设备管理"页面,点击"添加设备"按钮,填写相关信息后保存。</div>
<div>Q: 如何处理设备故障?</div>
<div>A: 进入"故障管理"页面,点击"报修"按钮,填写故障信息并提交。</div>
</el-collapse-item>
</el-collapse>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="helpDialogVisible = false">关闭</el-button>
</span>
</el-dialog>
<!-- end -->
<!-- 个人信息弹窗 -->
<el-dialog
title="个人信息"
:visible.sync="userInfoDialogVisible"
width="30%">
<div class="user-info-content">
<el-form label-width="80px">
<el-form-item label="用户名">
<span>{{ realName }}</span>
</el-form-item>
<el-form-item label="角色">
<span>{{ userRoleName }}</span>
</el-form-item>
<el-form-item label="部门">
<span>{{ userDepartment }}</span>
</el-form-item>
<el-form-item label="最后登录">
<span>{{ lastLoginTime }}</span>
</el-form-item>
</el-form>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="userInfoDialogVisible = false">关闭</el-button>
</span>
</el-dialog>
<!-- end -->
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator'
import { AppModule } from '@/store/modules/app'
import { UserModule } from '@/store/modules/user'
import Breadcrumb from '@/components/Breadcrumb/index.vue'
import Hamburger from '@/components/Hamburger/index.vue'
import { setStatus } from '@/api/users'
import Cookies from 'js-cookie'
import { debounce, throttle } from '@/utils/common'
import { setNewData, getNewData } from '@/utils/cookies'
// 修改密码弹层
import Password from '../components/password.vue'
@Component({
name: 'Navbar',
components: {
Breadcrumb,
Hamburger,
Password,
},
})
export default class extends Vue {
private storeId = this.getStoreId
private restKey: number = 0
private websocket = null
private newOrder = ''
private message = ''
private audioIsPlaying = false
private audioPaused = false
private statusValue = true
private audioUrl: './../../../assets/preview.mp3'
private shopShow = false
private dialogVisible = false
private status = 1
private setStatus = 1
private dialogFormVisible = false
private ountUnread = 0
private roleId = parseInt(localStorage.getItem('roleId') || '0'); // 从 localStorage 中读取
private isDarkTheme = false
private helpDialogVisible = false
private userInfoDialogVisible = false
private userRoleName = '管理员' // 示例数据,实际应从用户信息中获取
private userDepartment = '技术部' // 示例数据,实际应从用户信息中获取
private lastLoginTime = new Date().toLocaleString() // 示例数据,实际应从用户信息中获取
get sidebar() {
return AppModule.sidebar
}
get device() {
return AppModule.device.toString()
}
getuserInfo() {
return UserModule.userInfo
}
get realName() {
return (UserModule.userInfo as any).realName
? (UserModule.userInfo as any).realName
: JSON.parse(Cookies.get('user_info') as any).realName
}
get getStoreId() {
let storeId = ''
if (UserModule.storeId) {
storeId = UserModule.storeId
} else if ((UserModule.userInfo as any).stores != null) {
storeId = (UserModule.userInfo as any).stores[0].storeId
}
return storeId
}
mounted() {
document.addEventListener('click', this.handleClose)
}
created() {
this.webSocket()
}
onload() {
}
destroyed() {
this.websocket.close() //离开路由之后断开websocket连接
}
// 添加新订单提示弹窗
// 添加新订单提示弹窗
webSocket() {
const that = this as any
let clientId = Math.random().toString(36).substr(2)
let socketUrl = process.env.VUE_APP_SOCKET_URL + clientId
console.log(socketUrl, 'socketUrl')
if (typeof WebSocket == 'undefined') {
that.$notify({
title: '提示',
message: '当前浏览器无法接收实时报警信息,请使用谷歌浏览器!',
type: 'warning',
duration: 0,
})
} else {
this.websocket = new WebSocket(socketUrl)
// 监听socket打开
this.websocket.onopen = function () {
console.log('浏览器WebSocket已打开')
}
// 监听socket消息接收
this.websocket.onmessage = function (msg) {
// 转换为json对象
that.$refs.audioVo.currentTime = 0
that.$refs.audioVo2.currentTime = 0
console.log(msg, JSON.parse(msg.data), 'msg')
// const h = this.$createElement
const jsonMsg = JSON.parse(msg.data)
if (String(jsonMsg.roleId) !== String(that.roleId)) { // 这里改为that.roleId
return;
}
if (jsonMsg.type === 1) {
that.$refs.audioVo.play()
} else if (jsonMsg.type === 2) {
that.$refs.audioVo2.play()
}
that.$notify({
title: jsonMsg.type === 1 ? '待审核' : '催单',
duration: 0,
dangerouslyUseHTMLString: true,
onClick: () => {
// 根据消息类型跳转到不同页面
switch (jsonMsg.type) {
case 1: // 待审核
if (jsonMsg.faultId) {
that.$router.push(`/teamLeader/deviceReview/faultClear?faultId=${jsonMsg.faultId}`)
} else if (jsonMsg.maintenanceId) {
that.$router.push(`/teamLeader/deviceReview/maintenanceAcceptance?maintenanceId=${jsonMsg.maintenanceId}`)
} else if (jsonMsg.inspectionId) {
that.$router.push(`/teamLeader/deviceReview/inspectionAcceptance?inspectionId=${jsonMsg.inspectionId}`)
} else if (jsonMsg.detectionId) {
that.$router.push(`/teamLeader/deviceReview/testingAcceptance?detectionId=${jsonMsg.detectionId}`)
}
break
case 2: // 催单
if (jsonMsg.purchaseId) {
that.$router.push(`/sectionLeader/purchase?purchaseId=${jsonMsg.purchaseId}`)
} else if (jsonMsg.outboundId) {
that.$router.push(`/sectionLeader/outbound?outboundId=${jsonMsg.outboundId}`)
}
break
default:
// 默认跳转到消息中心
that.$router.push('/sectionLeader/inform')
}
setTimeout(() => {
location.reload()
}, 100)
},
// 这里也可以把返回信息加入到message中显示
message: `${jsonMsg.type === 1
? `<span>您有1个<span style=color:#419EFF>审核处理</span>,${jsonMsg.content},请及时审核</span>`
: `${jsonMsg.content}<span style='color:#419EFF;cursor: pointer'>去处理</span>`
}`,
})
}
// 监听socket错误
this.websocket.onerror = function () {
that.$notify({
title: '错误',
message: '服务器错误,无法接收实时报警信息',
type: 'error',
duration: 0,
})
}
// 监听socket关闭
this.websocket.onclose = function () {
console.log('WebSocket已关闭')
}
}
}
private toggleSideBar() {
AppModule.ToggleSideBar(false)
}
// 退出
private async logout() {
this.$store.dispatch('LogOut').then(() => {
this.$router.replace({ path: '/' })
})
}
// 下拉菜单显示
toggleShow() {
this.shopShow = true
}
// 下拉菜单隐藏
mouseLeaves() {
this.shopShow = false
}
// 触发空白处下来菜单关闭
handleClose() {
// clearTimeout(this.leave)
// this.shopShow = false
}
// 跳转到消息中心
goToMessageCenter() {
// 根据用户角色跳转到不同的消息中心页面
const roleId = parseInt(localStorage.getItem('roleId') || '0');
if (roleId === 2) { // 科长
this.$router.push('/sectionLeader/inform');
} else if (roleId === 3) { // 班组长
this.$router.push('/teamLeader/inform');
} else if (roleId === 4) { // 仓库管理员
this.$router.push('/WarehouseManager/inform');
} else if (roleId === 5) { // 主任
this.$router.push('/director/inform');
} else {
this.$router.push('/sectionLeader/inform'); // 默认跳转
}
this.ountUnread = 0; // 清空未读消息数量
}
// 修改密码
handlePwd() {
this.dialogFormVisible = true
}
// 关闭密码编辑弹层
handlePwdClose() {
this.dialogFormVisible = false
}
// 切换主题
toggleTheme() {
this.isDarkTheme = !this.isDarkTheme;
// 这里可以添加实际的主题切换逻辑
document.body.classList.toggle('dark-theme');
this.$message.success(`已切换到${this.isDarkTheme ? '深色' : '浅色'}模式`);
}
// 打开帮助中心
openHelpCenter() {
this.helpDialogVisible = true;
}
// 查看个人信息
viewUserInfo() {
this.userInfoDialogVisible = true;
}
}
</script>
<style lang="scss" scoped>
.navbar {
height: 80px;
position: relative;
background: #ffffff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.statusBox {
float: left;
height: 100%;
align-items: center;
display: flex;
}
.system-title {
font-size: 20px;
font-weight: bold;
color: #333;
margin-left: 15px;
position: relative;
padding-left: 15px;
&:before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 20px;
background-color: #409EFF;
}
}
.hamburger-container {
padding: 0 12px 0 20px;
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.breadcrumb-container {
float: left;
}
.right-menu {
float: right;
margin-right: 20px;
color: #333333;
font-size: 16px;
padding: 10px 0;
height: auto;
line-height: 20px;
display: flex;
align-items: center;
.notification-center, .help-center, .theme-switch {
margin-right: 20px;
cursor: pointer;
}
span {
padding: 0 10px;
width: 130px;
display: inline-block;
cursor: pointer;
&:hover {
background: rgba(255, 255, 255, 0.52);
}
}
.amendPwdIcon {
i {
width: 18px;
height: 18px;
background: url(./../../../assets/icons/btn_gaimi@2x.png) no-repeat;
background-size: contain;
margin-top: 8px;
}
}
.outLogin {
i {
width: 18px;
height: 18px;
background: url(./../../../assets/icons/btn_close@2x.png) no-repeat 100% 100%;
background-size: contain;
margin-top: 8px;
}
}
.userInfoIcon {
i {
font-size: 18px;
margin-top: 8px;
}
}
.outLogin {
cursor: pointer;
}
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
}
}
.rightStatus {
height: 100%;
line-height: 60px;
display: flex;
align-items: center;
float: left;
}
.avatar-wrapper {
margin-top: 14px;
margin-left: 18px;
position: relative;
float: right;
width: 120px;
text-align: left;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
.el-button--primary {
background: rgba(255, 255, 255, 0.52);
border-radius: 4px;
padding-top: 0px;
padding-bottom: 0px;
position: relative;
width: 120px;
padding-left: 12px;
text-align: left;
border: 0 none;
height: 32px;
line-height: 32px;
&.active {
background: rgba(250, 250, 250, 0);
border: 0 none;
.el-icon-arrow-down {
transform: rotate(-180deg);
}
}
}
}
.navicon {
i {
display: inline-block;
width: 18px;
height: 18px;
vertical-align: sub;
margin: 0 4px 0 0;
}
}
.mesCenter {
i {
background: url('./../../../assets/icons/msg.png') no-repeat;
background-size: contain;
}
}
.helpCenter {
i {
font-size: 18px;
vertical-align: middle;
}
}
.themeSwitch {
i {
font-size: 18px;
vertical-align: middle;
}
}
}
.help-content {
max-height: 400px;
overflow-y: auto;
h3 {
margin-top: 0;
margin-bottom: 20px;
text-align: center;
color: #409EFF;
}
}
.user-info-content {
padding: 20px;
}
// 深色模式样式
:global(.dark-theme) {
.navbar {
background: #1f2d3d;
color: #fff;
.system-title {
color: #fff;
}
.right-menu {
color: #fff;
}
}
}
</style>
<style lang="scss">
.el-notification {
// background: rgba(255, 255, 255, 0.71);
width: 419px !important;
.el-notification__title {
margin-bottom: 14px;
color: #333;
.el-notification__content {
color: #333;
}
}
}
.navbar {
.el-dialog {
min-width: auto !important;
}
.el-dialog__header {
height: 61px;
line-height: 60px;
background: #fbfbfa;
padding: 0 30px;
font-size: 16px;
color: #333;
border: 0 none;
}
.el-dialog__body {
padding: 10px 30px 30px;
.el-radio,
.el-radio__input {
white-space: normal;
}
.el-radio__label {
padding-left: 5px;
color: #333;
font-weight: 700;
span {
display: block;
line-height: 20px;
padding-top: 12px;
color: #666;
font-weight: normal;
}
}
.el-radio__input.is-checked .el-radio__inner {
&::after {
background: #333;
}
}
.el-radio-group {
& > .is-checked {
border: 1px solid #409EFF; // 原值为 #ffc200,修改为蓝色
}
}
.el-radio {
width: 100%;
background: #fbfbfa;
border: 1px solid #e5e4e4;
border-radius: 4px;
padding: 14px 22px;
margin-top: 20px;
}
.el-radio__input.is-checked + .el-radio__label {
span {
}
}
}
.el-badge__content.is-fixed {
top: 24px;
right: 2px;
width: 18px;
height: 18px;
font-size: 10px;
line-height: 16px;
font-size: 10px;
border-radius: 50%;
padding: 0;
}
.badgeW {
.el-badge__content.is-fixed {
width: 30px;
border-radius: 20px;
}
}
}
.el-icon-arrow-down {
background: url('./../../../assets/icons/up.png') no-repeat 50% 50%;
background-size: contain;
width: 8px;
height: 8px;
transform: rotate(0eg);
margin-left: 16px;
position: absolute;
right: 16px;
top: 12px;
&:before {
content: '';
}
}
.userInfo {
background: #fff;
position: absolute;
top: 0px;
left: 0;
z-index: 99;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.14);
width: 100%;
border-radius: 4px;
line-height: 32px;
padding: 0 0 5px;
height: 105px;
// .active {
// top: 0;
// left: 0;
// }
.userList {
width: 95%;
// // margin-top: -5px;
// position: absolute;
// top: 35px;
padding-left: 5px;
}
p {
cursor: pointer;
height: 32px;
line-height: 32px;
padding: 0 5px 0 7px;
i {
margin-left: 10px;
vertical-align: middle;
margin-top: 4px;
float: right;
}
&:hover {
background: #f6f1e1;
}
}
}
.msgTip {
color: #419eff;
padding: 0 5px;
}
// .el-dropdown{
// .el-button--primary{
// height: 32px;
// background: rgba(255,255,255,0.52);
// border-radius: 4px;
// padding-top: 0px;
// padding-bottom: 0px;
// }
// margin-top: 2px;
// }
// .el-popper{
// top: 45px !important;
// padding-top: 50px !important;
// border-radius: 0 0 4px 4px;
// }
// .el-popper[x-placement^=bottom] .popper__arrow::after,.popper__arrow{
// display: none !important;
// }
</style>
对应的跳转页面关键代码为
mounted() {
if (
this.$route.query.faultId &&
this.$route.query.faultId !== 'undefined'
) {
this.goDetail(this.$route.query.faultId, 2)
}
if (this.$route.query.status) {
this.defaultActivity = Number(this.$route.query.status)
}
}
后端实现这边直接给出来
/**
* 查询未审核审核清单
*/
@Scheduled(cron = "0 * * * * ?")
@Transactional
public void listenMaintenanceVerify() {
System.out.println("定期查审核");
LocalDateTime listenTime = LocalDateTime.now().minusMinutes(15);
log.info("定时检查保养任务状态,检测时间(当前时间-15min):{}", listenTime);
//检查超时未审核的 先检查工组长 设置步骤为1
List<Long> maintenanceIds = teamLeaderMapper.getMaintenanceIdByTime(listenTime,1);
//根据id集合获取maintenanceCode集合
List<String> maintenanceCodes = teamLeaderMapper.getMaintenanceCodeByMaintenanceId(maintenanceIds);
//若存在websocket群发消息
if (maintenanceIds.isEmpty()){
return;
}
Map map = new HashMap();
map.put("type", 1);//1 表示审核提醒
map.put("roleId", 3);
for (int i = 0; i < maintenanceIds.size(); i++) {
//发送websocket消息
map.put("maintenanceId", maintenanceIds.get(i));
map.put("content", "请及时审核保养单"+maintenanceCodes.get(i));
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
log.info("已发送消息:{}", json);
}
//检查超时未审核的 先检查设备管理员 步骤为2
maintenanceIds = teamLeaderMapper.getMaintenanceIdByTime(listenTime,2);
maintenanceCodes = teamLeaderMapper.getMaintenanceCodeByMaintenanceId(maintenanceIds);
if (maintenanceIds.isEmpty()){
return;
}
map.put("roleId", 2);
for (int i = 0; i < maintenanceIds.size(); i++) {
//发送websocket消息
map.put("maintenanceId", maintenanceIds.get(i));
map.put("content", "请及时审核保养单"+maintenanceCodes.get(i));
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
log.info("已发送消息:{}", json);
}
}
使用的websokcetserver如下
`package com.device.websocket;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
-
WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();/**
- 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
- 收到客户端消息后调用的方法
- @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
- 连接关闭调用的方法
- @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
- 群发
- @param message
*/
public void sendToAllClient(String message) {
Collectionsessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 连接建立成功调用的方法
}
`
依赖导入参考黑马即可
这边省略
浙公网安备 33010602011771号