Vue+websocket+stompjs 实时监控坐席状态demo

由于是前后端分离的demo, 程序的后端我不管,我只负责把前端做好,这只是个demo, 还有很多不完善的地方。

2018-01-09新增:
后端的MQ事件结构现在也改了,该demo只能看看了。

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<link href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<table class="table" id="event-queue">
<thead>
<tr>
<th>当前状态</th>
<th>状态改变时间</th>
<th>姓名</th>
<th>工号</th>
<th>分机号</th>
<th>对方号码</th>
<th>呼入数</th>
<th>呼出数</th>
</tr>
</thead>
<tbody>
<tr v-for="item in eventQueue">
<td>{{item.agentStatus | transAgentStatus}}</td>
<td>{{item.agentStatusTime}}</td>
<td>{{item.userName}}</td>
<td>{{item.loginName}}</td>
<td>{{item.deviceId}}</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>


<script src="http://cdn.bootcss.com/vue/1.0.26/vue.js"></script>
<script src="js/websocket-suport.min.js"></script>
<script src="js/main.js"></script>
</body>
</html>

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
var tm = (function(){
var App = function(){};
var app = App.prototype;
var config = {
dest: 'http://xxx.xxx.xxx.xxx:58080/mvc/stomp',
topic: '/topic/csta/namespace/testwdd2.com'
// topic: '/topic/csta/device/8002@testwdd2.com'
};


var eventQueue = [];
var vm = new Vue({
el:'#event-queue',
data:{
eventQueue: eventQueue
}
});

Vue.filter('transAgentStatus', function(status){
switch(status){
case 'NotReady': return '未就绪';
case 'WorkNotReady': return '话后处理状态';
case 'Idle': return '就绪';
case 'OnCallIn': return '呼入通话';
case 'OnCallOut': return '呼出通话';
case 'Logout': return '登出';
case 'Ringing': return '振铃';
case 'OffHook': return '摘机';
case 'CallInternal': return '内部通话';
case 'Dailing': return '外线已经振铃';
case 'Ringback': return '回铃';
case 'Conference': return '会议';
case 'OnHold': return '保持';
case 'Other': return '其他';
}

return '';
});

/**
* [render description]
* @Author Wdd
* @DateTime 2016-12-26T16:06:16+0800
* @param {[string]} tpl [模板字符串]
* @param {[object]} data [data对象]
* @return {[string]} [渲染后的字符串]
*/
app.render = function(tpl,data){
var re = /{{([^}]+)?}}/g;

while(match = re.exec(tpl)){
tpl = tpl.replace(match[0],data[match[1]] || '');
}

return tpl;
};

app.initWebSocket = function(dest, topic){
dest = dest || config.dest;
topic = topic || config.topic;

var socket = new SockJS(dest);
var ws = Stomp.over(socket);

ws.connect({}, function(frame) {

ws.subscribe(topic, function(event) {
// var eventInfo = JSON.parse(event.body);
app.handerEvent(JSON.parse(event.body));
});
}, function(frame) {

console.log(frame);
console.error(new Date() + 'websocket失去连接');
});
};

/**
* [findAgentIndex description]
* @Author Wdd
* @DateTime 2016-12-28T10:34:13+0800
* @param {[string]} agentId [description]
* @return {[int]} [description]
*/
app.findAgentIndex = function(agentId){
for(var i = eventQueue.length - 1; i >= 0; i--){
if(eventQueue[i].agentId === agentId){
return i;
}
}

return -1;
};

/**
* [handerEvent 处理websocket事件]
* @Author Wdd
* @DateTime 2016-12-28T10:33:03+0800
* @param {[object]} data [description]
* @return {[type]} [description]
*/
app.handerEvent = function(data){
if(data.eventType === 'CallEvent'){
return;
}
if(!data.eventSrc){
return;
}

var eventItem = {
agentStatus: '',
eventName: data.eventName,
agentId: '',
loginName: '',
userName: '',
deviceId: data.deviceId,
agentStatusTime: ''
};

var agent = data.eventSrc.agent || '';

if(agent){
eventItem.agentId = agent.agentId;
eventItem.loginName = agent.loginName;
eventItem.userName = agent.userName;
eventItem.agentStatus = agent.agentStatus;
eventItem.agentStatusTime = agent.agentStatusTime;
}
// 针对登出事件的agentId在外层
else if(data.agentMode){
eventItem.agentStatus = data.agentMode;
eventItem.agentId = data.agentId;
}
else if(data.agentStatus){
eventItem.agentStatus = data.agentStatus;
}

if(!eventItem.agentId){
return;
}

var itemIndex = app.findAgentIndex(eventItem.agentId);

// 新的座席加入
if(itemIndex === -1){
eventQueue.push(eventItem);
}
// 更新已有座席的状态
else{
eventQueue[itemIndex].agentStatus = eventItem.agentStatus;
eventQueue[itemIndex].agentStatusTime = eventItem.agentStatusTime;
eventQueue[itemIndex].eventName = eventItem.eventName;
}

};


return new App();
})();

打开控制台,输入tm.initWebsocket()后,websocket连接正常。

之后坐席状态改变,可以看到有事件推送过来。

看下整个页面:

最后,这个小小的监控如果用jQuery写,也可以,不过就是太坑了,每次都要去找到Dom元素,再更新DOM,用了Vue这类的框架,页面的dom操作完全不用关心了,真是太舒服了。\(^o^)/

1. 关于stomp的重连

程序后服务端使用RabbitMQ
这里我直接引用我的另一个项目的部分代码,这个没有使用SockJS, 直接使用浏览器原生的WebSocket。
重连的原理很简单,就是检测到断开时,去调用我的reconnectWs方法,这里我也做了重连的次数限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
initWebSocket: function(callback, errorCallback) {
callback = callback || function(){};

if(ws && ws.connected){
return;
}

Config.isManCloseWs = false;

var url = Config.wsProtocol + Config.SDK + Config.eventPort + Config.eventBasePath + "/websocket";

if(typeof WebSocket != 'function'){
alert('您的浏览器版本太太太老了,请升级你的浏览器到IE11,或使用任何支持原生WebSocket的浏览器');
return;
}

try{
var socket = new WebSocket(url);
}
catch(e){
console.log(e);
return;
}


var wsHeartbeatId = '';

ws = Stomp.over(socket);

if(!Config.useWsLog){
ws.debug = null;
}

ws.connect({}, function(frame) {

Config.currentReconnectTimes = 0;

var dest = Config.newWsTopic + env.loginId.replace(/\./g,'_');

var lastEventSerial = '';

ws.subscribe(dest, function(event) {
var eventInfo = {};

try{
eventInfo = JSON.parse(event.body);
delete eventInfo.params;
delete eventInfo._type;
delete eventInfo.topics;
}
catch(e){
console.log(e);
return;
}

if(lastEventSerial === eventInfo.serial){
util.error('Error: event repeat sent !');
return;
}
else{
lastEventSerial = eventInfo.serial;
}

if(Config.useEventLog){
util.debugout.log(' ' + JSON.stringify(eventInfo));
}

eventHandler.deliverEvent(eventInfo);
});
callback();

}, function(frame) {
// websocket upexpected disconnected
// maybe network disconnection, or browser in offline
// this condition will emit wsDisconnected event
if(Config.isManCloseWs){return;}
errorCallback();

util.log(frame);
util.error(new Date() + 'websocket disconnect');
// clearInterval(wsHeartbeatId);

if(Config.currentReconnectTimes < Config.maxReconnectTimes){
Config.currentReconnectTimes++;
util.reconnectWs();
}
else{
var errorMsg = {
eventName: 'wsDisconnected',
msg: 'websocket disconnect'
};
wellClient.ui.main({
eventName:'wsDisconnected'
});
util.debugout.log('>>> websocket disconnect');

wellClient.triggerInnerOn(errorMsg);
}
});
},

reconnectWs: function(){
setTimeout(function(){
util.log('>>> try to reconnect');
util.debugout.log('>>> try to reconnect');
util.initWebSocket(function(){},function(){});

}, Config.timeout * 1000);
},

2. 参考