user-behavior-monitor 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "user-behavior-monitor",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Vue component for monitoring user behavior and auto-logout",
5
5
  "main": "dist/user-behavior-monitor.umd.min.js",
6
6
  "files": [
@@ -12,7 +12,8 @@
12
12
  "prepublishOnly": "npm run build"
13
13
  },
14
14
  "dependencies": {
15
- "element-ui": "^2.15.0"
15
+ "element-ui": "^2.15.0",
16
+ "socket.io-client": "^4.8.1"
16
17
  },
17
18
  "peerDependencies": {
18
19
  "vue": "^2.5.2"
@@ -2,24 +2,16 @@
2
2
  <div ref="behaviorMonitor" class="user-behavior-monitor">
3
3
  <!-- 提示框 -->
4
4
  <el-dialog
5
+ title="提示"
5
6
  :visible.sync="showWarning"
6
7
  :show-close="false"
7
8
  :modal="true"
8
9
  width="30%"
9
10
  center
10
11
  custom-class="behavior-warning-dialog"
11
- :append-to-body="true"
12
- :modal-append-to-body="true"
13
- :close-on-click-modal="false"
14
- :close-on-press-escape="false"
15
12
  >
16
13
  <span>{{ warningMessage }}</span>
17
14
  </el-dialog>
18
-
19
-
20
- <button @click="testWarning" style="position: fixed; top: 10px; right: 10px; z-index: 10000;">
21
- 测试警告
22
- </button>
23
15
  </div>
24
16
  </template>
25
17
 
@@ -28,10 +20,10 @@ export default {
28
20
  name: 'UserBehaviorMonitor',
29
21
  props: {
30
22
  // WebSocket服务器地址
31
- websocketUrl: {
32
- type: String,
33
- required: true
34
- },
23
+ // websocketUrl: {
24
+ // type: String,
25
+ // required: true
26
+ // },
35
27
  // 倒计时时长(分钟)
36
28
  timeoutMinutes: {
37
29
  type: Number,
@@ -45,39 +37,104 @@ export default {
45
37
  },
46
38
  data() {
47
39
  return {
48
- mouseMoveThrottled:false,
49
- websocket: null,
40
+ mouseMoveThrottled: false,
41
+ socket: null,
50
42
  countdownTimer: null,
51
43
  warningTimer: null,
52
44
  showWarning: false,
53
- warningMessage: `您已${this.timeoutMinutes}分钟未操作,将在${this.warningMinutes}分钟后自动退出`,
45
+ // warningMinutes
46
+ warningMessage: `您已${this.timeoutMinutes}分钟未操作,将在1分钟后自动退出`,
54
47
  lastActivityTime: null,
55
- isMonitoring: false
48
+ isMonitoring: false,
49
+ currentTimeoutMinutes: 10,
50
+ currentWarningMinutes: 1
56
51
  };
57
52
  },
58
53
  mounted() {
59
- console.log('开始开始! mounted');
60
- this.initMonitor();
54
+ // 检查当前路由是否包含/login,如果不包含则初始化监控
55
+ if (!this.isLoginRoute()) {
56
+ this.initMonitor();
57
+ }
61
58
  },
62
59
  beforeDestroy() {
63
- console.log('开始开始! beforeDestroy');
64
60
  this.destroyMonitor();
65
61
  },
62
+ watch: {
63
+ // 监听路由变化
64
+ '$route'(to, from) {
65
+ // 安全检查 from.href 是否存在
66
+ const isFromLogin = from.path.includes('/login') ||
67
+ (from.href && from.href.includes('/login'));
68
+
69
+ // 检查当前是否为登录路由
70
+ const isToLogin = this.isLoginRoute();
71
+
72
+ // 如果从登录页跳转到非登录页,且监控未启动,则初始化监控
73
+ if (isFromLogin && !isToLogin && !this.isMonitoring) {
74
+ this.initMonitor();
75
+ }
76
+ // 如果从非登录页跳转到登录页,且监控正在运行,则销毁监控
77
+ else if (!isFromLogin && isToLogin && this.isMonitoring) {
78
+ this.destroyMonitor();
79
+ }
80
+ }
81
+ },
66
82
  methods: {
67
- testWarning() {
68
- console.log('Testing warning display');
69
- this.showWarning = true;
83
+ // 检查当前路由是否为登录页面
84
+ isLoginRoute() {
85
+ // 优先检查 Vue Router 路由
86
+ if (this.$route && this.$route.path) {
87
+ if (this.$route.path.includes('/login')) {
88
+ return true;
89
+ }
90
+ }
91
+
92
+ // 检查浏览器地址栏作为后备
93
+ if (typeof window !== 'undefined') {
94
+ return window.location.pathname.includes('/login') ||
95
+ window.location.href.includes('/login');
96
+ }
97
+
98
+ return false;
99
+ },
100
+
101
+ updateTimeoutSettings(timeoutMinutes, warningMinutes) {
102
+ if (timeoutMinutes !== undefined) {
103
+ this.currentTimeoutMinutes = timeoutMinutes;
104
+ localStorage.setItem('userBehavior_timeoutMinutes', timeoutMinutes.toString());
105
+ }
106
+
107
+ if (warningMinutes !== undefined) {
108
+ this.currentWarningMinutes = warningMinutes;
109
+ localStorage.setItem('userBehavior_warningMinutes', warningMinutes.toString());
110
+ }
111
+
112
+ // 更新警告消息
113
+ // ${this.currentWarningMinutes}
114
+ this.warningMessage = `您已${this.currentTimeoutMinutes}分钟未操作,将在1分钟后自动退出`;
115
+
116
+ // 重置计时器
117
+ this.resetTimer();
70
118
  },
71
119
  // 初始化监控
72
120
  initMonitor() {
73
- console.log('Initializing monitor');
121
+ // 再次检查确保不在登录页面
122
+ if (this.isLoginRoute()) {
123
+ // 如果在登录页面,确保清理所有监控资源
124
+ this.destroyMonitor();
125
+ return;
126
+ }
127
+
74
128
  if (this.isMonitoring) return;
75
129
 
76
130
  this.isMonitoring = true;
77
131
  this.lastActivityTime = Date.now();
78
132
 
133
+ // 初始化WebSocket连接
134
+ this.initWebSocket();
135
+
79
136
  // 启动倒计时
80
- this.startCountdown();
137
+ // this.startCountdown();
81
138
 
82
139
  // 绑定事件监听器
83
140
  this.bindEventListeners();
@@ -91,71 +148,183 @@ export default {
91
148
  if (this.countdownTimer) clearInterval(this.countdownTimer);
92
149
  if (this.warningTimer) clearTimeout(this.warningTimer);
93
150
 
151
+ // 关闭WebSocket连接
152
+ if (this.socket) {
153
+ this.socket.close();
154
+ this.socket = null;
155
+ }
156
+
94
157
  // 解绑事件监听器
95
158
  this.unbindEventListeners();
96
159
  },
97
160
 
161
+ // 初始化WebSocket连接
162
+ initWebSocket() {
163
+ try {
164
+ // 使用传入的WebSocket URL
165
+ const websocketUrl = `wss://${location.host}/xy-api/auth-server/ws/user-activity`;
166
+
167
+ // 获取token(假设存储在localStorage中)
168
+ let token = '';
169
+ try {
170
+ const apiHeader = localStorage.getItem('api_header');
171
+ if (apiHeader) {
172
+ token = JSON.parse(apiHeader).Authorization || '';
173
+ }
174
+ } catch (e) {
175
+ token = localStorage.getItem('token') || '';
176
+ }
177
+
178
+ // 创建WebSocket连接
179
+ this.socket = new WebSocket(`${websocketUrl}?token=${encodeURIComponent(token)}`);
180
+
181
+ // 设置连接成功回调
182
+ this.socket.onopen = () => {
183
+ console.log('WebSocket连接已建立');
184
+ this.sendUserBehavior();
185
+ this.$emit('websocket-open');
186
+ };
187
+
188
+ // 设置接收消息回调
189
+ this.socket.onmessage = (event) => {
190
+ try {
191
+ const data = JSON.parse(event.data);
192
+ console.log('收到用户活动状态消息:', data);
193
+ this.handleActivityStatus(data);
194
+ this.$emit('websocket-message', data);
195
+ } catch (e) {
196
+ console.error('解析WebSocket消息失败:', e);
197
+ }
198
+ };
199
+
200
+ // 设置连接关闭回调
201
+ this.socket.onclose = (event) => {
202
+ console.log('WebSocket连接已断开:', event.reason);
203
+ this.$emit('websocket-close');
204
+ };
205
+
206
+ // 设置连接错误回调
207
+ this.socket.onerror = (error) => {
208
+ console.error('WebSocket错误:', error);
209
+ this.$emit('websocket-error', error);
210
+ };
211
+
212
+ } catch (error) {
213
+ console.error('WebSocket初始化失败:', error);
214
+ this.$emit('websocket-error', error);
215
+ }
216
+ },
217
+
218
+ // 处理活动状态信息
219
+ handleActivityStatus(data) {
220
+ if (data.type === 'ACTIVITY_STATUS' && data.data) {
221
+ const activityData = data.data;
222
+
223
+ // 更新超时和警告时间配置
224
+ this.currentTimeoutMinutes = activityData.timeoutMinutes || 10;
225
+ this.currentWarningMinutes = activityData.reminderMinutes || 1;
226
+
227
+ // 更新警告消息
228
+ this.warningMessage = `您已${this.currentTimeoutMinutes}分钟未操作,将在${this.currentWarningMinutes}分钟后自动退出`;
229
+ if(activityData.needReminder){
230
+ this.showWarningWarning(activityData.remainingMillis);
231
+ }
232
+ // remainingMillis
233
+
234
+
235
+ // 检查是否仍然活跃
236
+ //if (!activityData.isActive) {
237
+ //this.handleInactiveStatus();
238
+ //}
239
+ }
240
+ // 检查是否需要显示提醒
241
+ // let dataReminder=data.data;
242
+ // if (dataReminder&&dataReminder.needReminder) {
243
+ // this.showWarningWarning(dataReminder.remainingSeconds);
244
+ // }
245
+ },
246
+
247
+ // 处理非活跃状态
248
+ handleInactiveStatus() {
249
+ // 清空缓存
250
+ localStorage.clear();
251
+ sessionStorage.clear();
252
+
253
+ // 跳转到登录页
254
+ //this.$router.push('/login');
255
+
256
+ // 或者使用window.location
257
+ window.location.href = '/login';
258
+
259
+ // 触发登出事件
260
+ this.$emit('logout');
261
+ },
262
+
98
263
  // 发送用户行为数据到后端
99
264
  sendUserBehavior(data) {
100
- // 用于测试:在控制台输出用户行为信息
101
- console.log('用户行为监测:', data);
265
+ console.log('用户行为监测:')
266
+ if (this.warningTimer) clearTimeout(this.warningTimer);
267
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
268
+ const message = {
269
+ "type": "HEARTBEAT",
270
+ "message": "心跳",
271
+ };
272
+ console.log('用户行为监测:', JSON.stringify(message));
273
+ this.socket.send(JSON.stringify(message));
274
+ }
102
275
  },
103
276
 
104
277
  // 绑定事件监听器
105
278
  bindEventListeners() {
106
- console.log('Binding event listeners');
107
- // 使用箭头函数或bind来保持this上下文
108
279
  // 鼠标事件
109
- document.addEventListener('click', this.handleUserActivity.bind(this), true);
110
- document.addEventListener('dblclick', this.handleUserActivity.bind(this), true);
111
- document.addEventListener('mousedown', this.handleUserActivity.bind(this), true);
112
- document.addEventListener('mouseup', this.handleUserActivity.bind(this), true);
113
- document.addEventListener('mousemove', this.handleMouseMove.bind(this), true);
114
- document.addEventListener('mouseover', this.handleUserActivity.bind(this), true);
115
- document.addEventListener('mouseout', this.handleUserActivity.bind(this), true);
280
+ document.addEventListener('click', this.handleUserActivity, true);
281
+ document.addEventListener('dblclick', this.handleUserActivity, true);
282
+ document.addEventListener('mousedown', this.handleUserActivity, true);
283
+ document.addEventListener('mouseup', this.handleUserActivity, true);
284
+ document.addEventListener('mousemove', this.handleMouseMove, true);
285
+ document.addEventListener('mouseover', this.handleUserActivity, true);
286
+ document.addEventListener('mouseout', this.handleUserActivity, true);
116
287
 
117
288
  // 键盘事件
118
- document.addEventListener('keydown', this.handleUserActivity.bind(this), true);
119
- document.addEventListener('keyup', this.handleUserActivity.bind(this), true);
289
+ document.addEventListener('keydown', this.handleUserActivity, true);
290
+ document.addEventListener('keyup', this.handleUserActivity, true);
120
291
 
121
292
  // 表单事件
122
- document.addEventListener('input', this.handleUserActivity.bind(this), true);
123
- document.addEventListener('change', this.handleUserActivity.bind(this), true);
124
- document.addEventListener('focus', this.handleUserActivity.bind(this), true);
125
- document.addEventListener('blur', this.handleUserActivity.bind(this), true);
293
+ document.addEventListener('input', this.handleUserActivity, true);
294
+ document.addEventListener('change', this.handleUserActivity, true);
295
+ document.addEventListener('focus', this.handleUserActivity, true);
296
+ document.addEventListener('blur', this.handleUserActivity, true);
126
297
 
127
298
  // 滚动事件(防抖处理)
128
- document.addEventListener('scroll', this.debounce(this.handleUserActivity, 300).bind(this), true);
299
+ document.addEventListener('scroll', this.debounce(this.handleUserActivity, 300), true);
129
300
 
130
301
  // 窗口事件
131
- window.addEventListener('resize', this.handleUserActivity.bind(this), true);
132
- window.addEventListener('beforeunload', this.handleBeforeUnload.bind(this));
302
+ window.addEventListener('resize', this.handleUserActivity, true);
303
+ window.addEventListener('beforeunload', this.handleBeforeUnload);
133
304
  },
134
305
 
135
306
  // 解绑事件监听器
136
307
  unbindEventListeners() {
137
- console.log('Unbinding event listeners');
138
- // 解绑时也要使用相同的方式
139
- document.removeEventListener('click', this.handleUserActivity.bind(this), true);
140
- document.removeEventListener('dblclick', this.handleUserActivity.bind(this), true);
141
- document.removeEventListener('mousedown', this.handleUserActivity.bind(this), true);
142
- document.removeEventListener('mouseup', this.handleUserActivity.bind(this), true);
143
- document.removeEventListener('mousemove', this.handleMouseMove.bind(this), true);
144
- document.removeEventListener('mouseover', this.handleUserActivity.bind(this), true);
145
- document.removeEventListener('mouseout', this.handleUserActivity.bind(this), true);
146
-
147
- document.removeEventListener('keydown', this.handleUserActivity.bind(this), true);
148
- document.removeEventListener('keyup', this.handleUserActivity.bind(this), true);
149
-
150
- document.removeEventListener('input', this.handleUserActivity.bind(this), true);
151
- document.removeEventListener('change', this.handleUserActivity.bind(this), true);
152
- document.removeEventListener('focus', this.handleUserActivity.bind(this), true);
153
- document.removeEventListener('blur', this.handleUserActivity.bind(this), true);
154
-
155
- document.removeEventListener('scroll', this.debounce(this.handleUserActivity, 300).bind(this), true);
156
-
157
- window.removeEventListener('resize', this.handleUserActivity.bind(this), true);
158
- window.removeEventListener('beforeunload', this.handleBeforeUnload.bind(this));
308
+ document.removeEventListener('click', this.handleUserActivity, true);
309
+ document.removeEventListener('dblclick', this.handleUserActivity, true);
310
+ document.removeEventListener('mousedown', this.handleUserActivity, true);
311
+ document.removeEventListener('mouseup', this.handleUserActivity, true);
312
+ document.removeEventListener('mousemove', this.handleMouseMove, true);
313
+ document.removeEventListener('mouseover', this.handleUserActivity, true);
314
+ document.removeEventListener('mouseout', this.handleUserActivity, true);
315
+
316
+ document.removeEventListener('keydown', this.handleUserActivity, true);
317
+ document.removeEventListener('keyup', this.handleUserActivity, true);
318
+
319
+ document.removeEventListener('input', this.handleUserActivity, true);
320
+ document.removeEventListener('change', this.handleUserActivity, true);
321
+ document.removeEventListener('focus', this.handleUserActivity, true);
322
+ document.removeEventListener('blur', this.handleUserActivity, true);
323
+
324
+ document.removeEventListener('scroll', this.debounce(this.handleUserActivity, 300), true);
325
+
326
+ window.removeEventListener('resize', this.handleUserActivity, true);
327
+ window.removeEventListener('beforeunload', this.handleBeforeUnload);
159
328
  },
160
329
 
161
330
  // 处理用户活动
@@ -186,7 +355,7 @@ export default {
186
355
 
187
356
  this.sendUserBehavior(behaviorData);
188
357
  },
189
- handleMouseMove(event) {
358
+ handleMouseMove(event) {
190
359
  if (!this.mouseMoveThrottled) {
191
360
  this.handleUserActivity(event);
192
361
  this.mouseMoveThrottled = true;
@@ -196,20 +365,6 @@ export default {
196
365
  }
197
366
  },
198
367
 
199
- // 处理鼠标移动(降低频率)
200
- // handleMouseMove: function() {
201
- // let isThrottled = false;
202
- // return (event) => {
203
- // if (!isThrottled) {
204
- // this.handleUserActivity(event);
205
- // isThrottled = true;
206
- // setTimeout(() => {
207
- // isThrottled = false;
208
- // }, 500);
209
- // }
210
- // };
211
- // }(),
212
-
213
368
  // 判断是否为自动触发事件
214
369
  isAutomaticEvent(event) {
215
370
  // 自动刷新等非用户主动触发的事件
@@ -234,7 +389,6 @@ export default {
234
389
 
235
390
  // 重置计时器
236
391
  resetTimer() {
237
- console.log('Resetting timer');
238
392
  this.lastActivityTime = Date.now();
239
393
  this.showWarning = false;
240
394
 
@@ -245,52 +399,48 @@ export default {
245
399
  }
246
400
 
247
401
  // 重新启动倒计时
248
- this.startCountdown();
402
+ // this.startCountdown();
249
403
 
250
404
  this.$emit('user-active');
251
405
  },
252
406
 
253
407
  // 启动倒计时
254
408
  startCountdown() {
255
- console.log('Starting countdown');
256
409
  // 清除现有定时器
257
410
  if (this.countdownTimer) clearInterval(this.countdownTimer);
258
411
 
259
412
  // 设置新的倒计时
260
413
  this.countdownTimer = setInterval(() => {
261
414
  const now = Date.now();
262
- const elapsedMinutes = (now - this.lastActivityTime) / (1000 * 60);
263
- console.log('Elapsed minutes:', elapsedMinutes);
415
+ const elapsedMinutes = (now - this.lastActivityTime) / (1000 * 50);
264
416
 
265
417
  // 检查是否需要显示警告
266
- if (elapsedMinutes >= (this.timeoutMinutes - this.warningMinutes) && !this.warningTimer) {
267
- console.log('Showing warning');
418
+ if (elapsedMinutes >= (this.currentTimeoutMinutes - this.currentWarningMinutes) && !this.warningTimer) {
268
419
  this.showWarningWarning();
269
420
  }
270
421
 
271
422
  // 检查是否超时
272
- if (elapsedMinutes >= this.timeoutMinutes) {
273
- console.log('Handling timeout');
423
+ if (elapsedMinutes >= this.currentTimeoutMinutes) {
274
424
  this.handleTimeout();
275
425
  }
276
426
  }, 1000);
277
427
  },
278
428
 
279
429
  // 显示超时警告
280
- showWarningWarning() {
430
+ showWarningWarning(timeoutMinutes) {
431
+ let time=timeoutMinutes||50;
281
432
  console.log('Setting showWarning to true');
282
433
  this.showWarning = true;
283
434
  this.$emit('timeout-warning');
284
-
435
+ if (this.warningTimer) clearTimeout(this.warningTimer);
285
436
  // 设置超时处理
286
437
  this.warningTimer = setTimeout(() => {
287
438
  this.handleTimeout();
288
- }, this.warningMinutes * 60 * 1000);
439
+ }, 1 * time * 1000);
289
440
  },
290
441
 
291
442
  // 处理超时
292
443
  handleTimeout() {
293
- console.log('Handling timeout');
294
444
  this.showWarning = false;
295
445
  this.$emit('timeout');
296
446
 
@@ -300,9 +450,24 @@ export default {
300
450
 
301
451
  // 登出操作
302
452
  logout() {
303
- // 用于测试:在控制台输出登出信息
304
- console.log('用户超时登出');
453
+ localStorage.clear();
454
+ sessionStorage.clear();
455
+ location.reload();
456
+ // 跳转到登录页
457
+ //this.$router.push('/login');
305
458
 
459
+ // 或者使用window.location
460
+ // window.location.href = '/login';
461
+ // 发送登出消息到后端
462
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
463
+ const message = {
464
+ type: 'logout',
465
+ timestamp: Date.now()
466
+ };
467
+ this.socket.send(JSON.stringify(message));
468
+ }
469
+
470
+ // 触发登出事件
306
471
  this.$emit('logout');
307
472
 
308
473
  // 清除定时器
@@ -312,32 +477,45 @@ export default {
312
477
 
313
478
  // 处理页面卸载前的操作
314
479
  handleBeforeUnload(event) {
315
- // 用于测试:在控制台输出页面卸载信息
316
- console.log('页面即将卸载');
480
+ // 发送页面关闭消息
481
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
482
+ const message = {
483
+ type: 'page_unload',
484
+ timestamp: Date.now()
485
+ };
486
+ this.socket.send(JSON.stringify(message));
487
+ }
317
488
  },
318
489
 
319
490
  // 手动重置监控
320
491
  reset() {
492
+ // 检查当前路由,如果在登录页面则销毁监控
493
+ if (this.isLoginRoute()) {
494
+ this.destroyMonitor();
495
+ return;
496
+ }
497
+
321
498
  this.resetTimer();
499
+ },
500
+
501
+ // 重新连接WebSocket
502
+ reconnect() {
503
+ // 检查当前路由,如果在登录页面则销毁监控
504
+ if (this.isLoginRoute()) {
505
+ this.destroyMonitor();
506
+ return;
507
+ }
508
+
509
+ if (this.socket) {
510
+ this.socket.close();
511
+ }
512
+ this.initWebSocket();
322
513
  }
323
514
  }
324
515
  };
325
516
  </script>
326
-
327
- <style scoped>
328
- .user-behavior-monitor {
329
- display: none;
330
- }
331
-
332
- /* 使用深度选择器确保样式正确应用 */
333
- .behavior-warning-dialog ::v-deep .el-dialog__body {
334
- text-align: center;
335
- font-size: 16px;
336
- padding: 30px 20px;
337
- }
338
-
339
- /* 确保弹框在最上层 */
340
- .behavior-warning-dialog {
341
- z-index: 9999 !important;
517
+ <style>
518
+ .behavior-warning-dialog.el-dialog.el-dialog--center{
519
+ text-align: left;
342
520
  }
343
521
  </style>