Skip to content

Commit d8cdafe

Browse files
committed
✨ websocket 推送支持
1 parent 1f4862e commit d8cdafe

File tree

8 files changed

+265
-21
lines changed

8 files changed

+265
-21
lines changed

src/components/GlobalHeader/GlobalHeader.vue

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
:class="[fixedHeader && 'ant-header-fixedHeader', sidebarOpened ? 'ant-header-side-opened' : 'ant-header-side-closed', ]"
77
:style="{ padding: '0'}">
88
<div v-if="mode === 'sidemenu'" class="header">
9-
<a-icon v-if="device==='mobile'" class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle"/>
10-
<a-icon v-else class="trigger" :type="collapsed ? 'menu-unfold' : 'menu-fold'" @click="toggle"/>
9+
<a-icon class="trigger" :type="collapsedButtonIconType" @click="toggle"/>
1110
<user-menu></user-menu>
1211
</div>
1312
<div v-else :class="['top-nav-header-index', theme]">
1413
<div class="header-index-wide">
1514
<div class="header-index-left">
1615
<img class="top-nav-header" src="@/assets/logo.svg" alt="logo"/>
17-
<s-menu v-if="device !== 'mobile'" mode="horizontal" :menu="menus" :theme="theme" />
18-
<a-icon v-else class="trigger" :type="collapsed ? 'menu-fold' : 'menu-unfold'" @click="toggle" />
16+
<s-menu v-if="device !== 'mobile'" mode="horizontal" :menu="menus" :theme="theme"/>
17+
<a-icon v-else class="trigger" :type="collapsedButtonIconType" @click="toggle"/>
1918
</div>
2019
<user-menu class="header-index-right"></user-menu>
2120
</div>
@@ -34,7 +33,7 @@ export default {
3433
name: 'GlobalHeader',
3534
components: {
3635
UserMenu,
37-
SMenu,
36+
SMenu
3837
},
3938
mixins: [mixin],
4039
props: {
@@ -69,9 +68,23 @@ export default {
6968
oldScrollTop: 0
7069
}
7170
},
71+
computed: {
72+
collapsedButtonIconType () {
73+
const isMobile = this.device === 'mobile'
74+
const collapsed = this.collapsed
75+
if ((collapsed && isMobile) || (!collapsed && !isMobile)) {
76+
return 'menu-fold'
77+
} else {
78+
return 'menu-unfold'
79+
}
80+
}
81+
},
7282
mounted () {
7383
document.addEventListener('scroll', this.handleScroll, { passive: true })
7484
},
85+
beforeDestroy () {
86+
document.body.removeEventListener('scroll', this.handleScroll, true)
87+
},
7588
methods: {
7689
handleScroll () {
7790
if (!this.autoHideHeader) {
@@ -97,26 +110,26 @@ export default {
97110
toggle () {
98111
this.$emit('toggle')
99112
}
100-
},
101-
beforeDestroy () {
102-
document.body.removeEventListener('scroll', this.handleScroll, true)
103113
}
104114
}
105115
</script>
106116

107117
<style lang="less">
108118
@import '../index.less';
109119
110-
.header-animat{
120+
.header-animat {
111121
position: relative;
112122
z-index: @ant-global-header-zindex;
113123
}
124+
114125
.showHeader-enter-active {
115126
transition: all 0.25s ease;
116127
}
128+
117129
.showHeader-leave-active {
118130
transition: all 0.5s ease;
119131
}
132+
120133
.showHeader-enter, .showHeader-leave-to {
121134
opacity: 0;
122135
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<template>
2+
<global-web-socket-listener/>
3+
</template>
4+
<script>
5+
import { ACCESS_TOKEN } from '@/store/storage-types'
6+
import Vue from 'vue'
7+
import GlobalWebSocketListener from '@/components/WebSocket/GlobalWebSocketListener'
8+
9+
export default {
10+
components: { GlobalWebSocketListener },
11+
data () {
12+
return {
13+
webSocket: null, // webSocket实例
14+
lockReconnect: false, // 重连锁,避免多次重连
15+
maxReconnect: -1, // 最大重连次数, -1 标识无限重连
16+
reconnectTime: 0, // 重连尝试次数
17+
heartbeat: {
18+
interval: 30 * 1000, // 心跳间隔时间
19+
timeout: 10 * 1000, // 响应超时时间
20+
pingTimeoutObj: null, // 延时发送心跳的定时器
21+
pongTimeoutObj: null, // 接收心跳响应的定时器
22+
pingMessage: JSON.stringify({ type: 'ping' }) // 心跳请求信息
23+
}
24+
}
25+
},
26+
created () {
27+
this.initWebSocket()
28+
},
29+
destroyed: function () {
30+
this.webSocket.close()
31+
this.clearTimeoutObj(this.heartbeat)
32+
},
33+
methods: {
34+
/**
35+
* 初始化 weoSocket
36+
*/
37+
initWebSocket () {
38+
// ws地址
39+
const token = Vue.ls.get(ACCESS_TOKEN)
40+
const wsUri = 'ws://127.0.0.1:8080/ws?access_token=' + token
41+
// 建立连接
42+
this.webSocket = new WebSocket(wsUri)
43+
// 连接成功
44+
this.webSocket.onopen = this.onOpen
45+
// 连接错误
46+
this.webSocket.onerror = this.onError
47+
// 接收信息
48+
this.webSocket.onmessage = this.onMessage
49+
// 连接关闭
50+
this.webSocket.onclose = this.onClose
51+
},
52+
/**
53+
* 重新连接
54+
*/
55+
reconnect () {
56+
if (this.lockReconnect || (this.maxReconnect !== -1 && this.reconnectTime > this.maxReconnect)) {
57+
return
58+
}
59+
this.lockReconnect = true
60+
setTimeout(() => {
61+
this.reconnectTime++
62+
// 建立新连接
63+
this.initWebSocket()
64+
this.lockReconnect = false
65+
}, 5000)
66+
},
67+
/**
68+
* 清空定时器
69+
*/
70+
clearTimeoutObj: function (heartbeat) {
71+
heartbeat.pingTimeoutObj && clearTimeout(heartbeat.pingTimeoutObj)
72+
heartbeat.pongTimeoutObj && clearTimeout(heartbeat.pongTimeoutObj)
73+
},
74+
/**
75+
* 开启心跳
76+
*/
77+
startHeartbeat () {
78+
const webSocket = this.webSocket
79+
const heartbeat = this.heartbeat
80+
// 清空定时器
81+
this.clearTimeoutObj(heartbeat)
82+
// 延时发送下一次心跳
83+
heartbeat.pingTimeoutObj = setTimeout(() => {
84+
// 如果连接正常
85+
if (webSocket.readyState === 1) {
86+
//这里发送一个心跳,后端收到后,返回一个心跳消息,
87+
webSocket.send(heartbeat.pingMessage)
88+
// 心跳发送后,如果服务器超时未响应则断开,如果响应了会被重置心跳定时器
89+
heartbeat.pongTimeoutObj = setTimeout(() => {
90+
webSocket.close()
91+
}, this.timeout)
92+
} else {
93+
// 否则重连
94+
this.reconnect()
95+
}
96+
}, heartbeat.interval)
97+
},
98+
/**
99+
* 连接成功事件
100+
*/
101+
onOpen () {
102+
console.log('WebSocket connection success')
103+
//开启心跳
104+
this.startHeartbeat()
105+
this.reconnectTime = 0
106+
},
107+
/**
108+
* 连接失败事件
109+
* @param e
110+
*/
111+
onError (e) {
112+
//错误
113+
console.log('WebSocket connection error (' + e.reason + ')')
114+
//重连
115+
this.reconnect()
116+
},
117+
/**
118+
* 连接关闭事件
119+
* @param e
120+
*/
121+
onClose (e) {
122+
//关闭
123+
console.log('WebSocket connection closed (' + e.reason + ')')
124+
//重连
125+
this.reconnect()
126+
},
127+
/**
128+
* 接收服务器推送的信息
129+
* @param msgEvent
130+
*/
131+
onMessage (msgEvent) {
132+
//收到服务器信息,心跳重置并发送
133+
this.startHeartbeat()
134+
let event;
135+
let data;
136+
const text = msgEvent.data
137+
try {
138+
data = JSON.parse(text)
139+
event = data.type
140+
// 心跳响应跳过发布
141+
if(event === 'pong'){
142+
return
143+
}
144+
}catch (e) {
145+
// 纯文本消息
146+
event = 'plaintext'
147+
data = text
148+
}
149+
this.$bus.$emit(event, data);
150+
},
151+
/**
152+
* 数据发送
153+
* @param msg
154+
*/
155+
send (msg) {
156+
//数据发送
157+
this.webSocket.send(msg)
158+
}
159+
}
160+
}
161+
162+
</script>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<template>
2+
3+
</template>
4+
5+
<script>
6+
import { mapActions } from 'vuex'
7+
8+
export default {
9+
name: 'GlobalWebSocketListener',
10+
data () {
11+
return {
12+
handlers: [
13+
{
14+
// 字典更新事件
15+
type: 'dict-change',
16+
handle: () => {
17+
this.checkDictStatus()
18+
}
19+
}
20+
]
21+
}
22+
},
23+
created () {
24+
// 注册监听事件
25+
this.handlers.forEach((handler) => {
26+
this.$bus.$on(handler.type, handler.handle)
27+
})
28+
},
29+
destroyed () {
30+
// 取消监听事件
31+
this.handlers.forEach((handler) => {
32+
this.$bus.$off(handler.type, handler.handle)
33+
})
34+
},
35+
methods: {
36+
...mapActions(['checkDictStatus'])
37+
}
38+
}
39+
</script>

src/components/notify/AnnouncementModal.vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,29 @@
33

44
<script>
55
import { Modal } from 'ant-design-vue'
6+
import { readAnnouncement } from '@/api/notify/announcement'
67
78
export default {
89
name: 'AnnouncementModal',
910
methods: {
10-
show(announcement){
11+
show (announcement, isPreview = false) {
1112
Modal.info({
1213
title: announcement.title,
1314
width: 800,
1415
// JSX support
15-
icon: function (){
16+
icon: function () {
1617
return (
1718
<a-icon type="notification" theme="filled"/>
1819
)
1920
},
2021
content: (
2122
<div domPropsInnerHTML={announcement.content}/>
22-
)
23+
),
24+
onOk: function () {
25+
if (!isPreview) {
26+
return readAnnouncement(announcement.id)
27+
}
28+
}
2329
})
2430
}
2531
}

src/components/notify/AnnouncementRibbon.vue

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313
<script>
1414
import AnnouncementModal from '@/components/notify/AnnouncementModal'
1515
import { getUserAnnouncements, readAnnouncement } from '@/api/notify/announcement'
16-
import { mapGetters } from 'vuex'
1716
1817
export default {
1918
name: 'AnnouncementRibbon',
20-
components: {AnnouncementModal},
19+
components: { AnnouncementModal },
2120
data () {
2221
return {
2322
activeIndex: 0, // 当前索引
@@ -27,7 +26,7 @@ export default {
2726
}
2827
},
2928
computed: {
30-
announcementNum() {
29+
announcementNum () {
3130
return this.announcements.length
3231
},
3332
announcement () {
@@ -47,18 +46,34 @@ export default {
4746
this.activeIndex = 0
4847
}
4948
}, this.playTime)
49+
// 注册监听事件
50+
this.$bus.$on('announcement-push', this.onAnnouncementPush)
51+
this.$bus.$on('announcement-close', this.onAnnouncementClose)
5052
5153
},
5254
destroyed () {
5355
// 清除定时器
5456
clearInterval(this.intervalId)
57+
// 删除事件监听
58+
this.$bus.$off('announcement-push', this.onAnnouncementPush)
59+
this.$bus.$off('announcement-close', this.onAnnouncementClose)
5560
},
5661
methods: {
57-
readAnnouncement(){
62+
readAnnouncement () {
5863
// 展示公告
5964
this.$refs.announcementModal.show(this.announcement)
60-
// 已读上报
61-
readAnnouncement(this.announcement.id).resolve();
65+
},
66+
onAnnouncementPush: function (data) {
67+
// 添加公告
68+
let announcement = {
69+
id: data.id,
70+
title: data.title,
71+
content: data.content
72+
}
73+
this.announcements.push(announcement)
74+
},
75+
onAnnouncementClose: function (data){
76+
this.announcements.splice( this.announcements.findIndex(item => item.id === data.id), 1)
6277
}
6378
}
6479
}

0 commit comments

Comments
 (0)