Explorar o código

feat: add idle timeout logout and optimize time acquisition

1. 新增多语言文案支持长时间无操作自动退出提示
2. 封装getServerTime方法获取准确服务器时间替代本地时间
3. 为各业务页面添加40分钟无操作自动登出的空闲检测功能
4. 扩展同步班级数据的用户白名单范围
5. 修复pptEasyClass组件的工具按钮状态判断逻辑
lsc hai 4 horas
pai
achega
6eddac458b

+ 3 - 6
src/components/dialog/addClassDialog.vue

@@ -175,7 +175,7 @@
           console.error(err);
         });
     },
-      submit() {
+      async submit() {
         if (
           this.courseDetail.userid == this.$route.query.userid &&
           this.checkboxList2.length &&
@@ -184,11 +184,8 @@
         ) {
 
           // 获取endTime为现在
-          let endDate = new Date();
-          let endTime = endDate.toLocaleString("zh-CN", {
-            hour12: false,
-            timeZone: "Asia/Shanghai"
-          }).replace(/\//g, "-");
+          // let endDate = new Date();
+          let endTime = await this.getServerTime()
 
           // 随机20~50分钟
           let randomMinutes = Math.floor(Math.random() * 31) + 20;

+ 37 - 11
src/components/easy2/studyStudent.vue

@@ -16202,6 +16202,8 @@ export default {
       getPickLoading:false,
       selectPzLoading:false,
       getSplitScreenDataLoading:false,
+      timeoutPan: null,
+      isTimeOutPan: false,
     };
   },
   watch:{
@@ -25767,12 +25769,10 @@ export default {
           console.error(err);
         });
     },
-    doSyncClassData(){
-      if(this.courseDetail.userid == this.userid && this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840'){
-        let endTime = new Date().toLocaleString("zh-CN", { 
-            hour12: false, 
-            timeZone: "Asia/Shanghai" 
-          }).replace(/\//g, "-")
+    async doSyncClassData(){
+      let userArray = ['6fbc6471-1d48-11ed-8c78-005056b86db5','333d0dfc-1cd9-11ef-bee5-005056b86db5','6fbce5ef-1d48-11ed-8c78-005056b86db5','66feffcc-ad35-11ed-b13d-005056b86db5']
+      if (this.courseDetail.userid == this.userid && (this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840' || userArray.includes(this.userid))) {
+        let endTime = await this.getServerTime()
         let courseTime = Math.floor((new Date(endTime) - new Date(this.startTime)) / (1000 * 60))
         this.syncClassData({
           courseId: this.id,
@@ -25786,6 +25786,28 @@ export default {
         console.log('同步数据')
       }
     },
+    resetIdleOp(){
+      this.$message({
+        message: this.lang.ssLongTimeNoOperation,
+        type: "warning",
+      });
+      this.goTo(
+                    '/courseDetail?userid=' +
+                      this.userid +
+                      '&oid=' +
+                      this.oid +
+                      '&org=' +
+                      this.org +
+                      '&cid=' +
+                      this.id +
+                      '&courseId=' +
+                      this.id +
+                      '&tType=' +
+                      this.tType +
+                      '&screenType=' +
+                      this.screenType
+                  )
+    }
   },
   directives: {
     // 使用局部注册指令的方式
@@ -25835,6 +25857,10 @@ export default {
     this.opertimer = null;
     this.updateSplitScreenData(1);
     this.doSyncClassData();
+
+    clearTimeout(this.timeoutPan);
+    this.timeoutPan = null;
+    this.stopIdleDetection(this.resetIdleOp);
   },
   computed: {
     renderedFormula2() {
@@ -26045,12 +26071,12 @@ export default {
     },
     
   },
-  mounted() {
+  async mounted() {
     this.setoTime("1");
-    this.startTime = new Date().toLocaleString("zh-CN", { 
-      hour12: false, 
-      timeZone: "Asia/Shanghai" 
-    }).replace(/\//g, "-")
+    this.startTime = await this.getServerTime()
+    this.timeoutPan = setTimeout(() => {
+      this.startIdleDetection(this.resetIdleOp);
+    }, 40 * 60 * 1000)
     this.updateSplitScreenData(2);
     this.splitScreenData.myUid = uuidv4();
     document.body.addEventListener("click", e => {

+ 36 - 11
src/components/easy3/studyStudent.vue

@@ -12523,6 +12523,8 @@ export default {
       getPickLoading:false,
       selectPzLoading:false,
       getSplitScreenDataLoading:false,
+      timeoutPan: null,
+      isTimeOutPan: false,
     };
   },
   methods: {
@@ -21089,12 +21091,10 @@ export default {
       this.IsStulook = flag;
       this.updateIsStulook();
     },
-    doSyncClassData(){
-      if(this.courseDetail.userid == this.userid && this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840'){
-        let endTime = new Date().toLocaleString("zh-CN", { 
-            hour12: false, 
-            timeZone: "Asia/Shanghai" 
-          }).replace(/\//g, "-")
+    async doSyncClassData(){
+      let userArray = ['6fbc6471-1d48-11ed-8c78-005056b86db5','333d0dfc-1cd9-11ef-bee5-005056b86db5','6fbce5ef-1d48-11ed-8c78-005056b86db5','66feffcc-ad35-11ed-b13d-005056b86db5']
+      if (this.courseDetail.userid == this.userid && (this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840' || userArray.includes(this.userid))) {
+        let endTime = await this.getServerTime()
         let courseTime = Math.floor((new Date(endTime) - new Date(this.startTime)) / (1000 * 60))
         this.syncClassData({
           courseId: this.id,
@@ -21108,6 +21108,28 @@ export default {
         console.log('同步数据')
       }
     },
+    resetIdleOp(){
+      this.$message({
+        message: this.lang.ssLongTimeNoOperation,
+        type: "warning",
+      });
+      this.goTo(
+                    '/courseDetail?userid=' +
+                      this.userid +
+                      '&oid=' +
+                      this.oid +
+                      '&org=' +
+                      this.org +
+                      '&cid=' +
+                      this.id +
+                      '&courseId=' +
+                      this.id +
+                      '&tType=' +
+                      this.tType +
+                      '&screenType=' +
+                      this.screenType
+                  )
+    }    
   },
   directives: {
     // 使用局部注册指令的方式
@@ -21155,6 +21177,9 @@ export default {
     this.opertimer = null;
 		this.updateSplitScreenData(1);
     this.doSyncClassData();
+    clearTimeout(this.timeoutPan);
+    this.timeoutPan = null;
+    this.stopIdleDetection(this.resetIdleOp);
   },
   computed: {
     renderedFormula2() {
@@ -21360,12 +21385,12 @@ export default {
     },
 
   },
-  mounted() {
+  async mounted() {
     this.setoTime("1");
-    this.startTime = new Date().toLocaleString("zh-CN", { 
-      hour12: false, 
-      timeZone: "Asia/Shanghai" 
-    }).replace(/\//g, "-")
+    this.startTime = await this.getServerTime()
+    this.timeoutPan = setTimeout(() => {
+      this.startIdleDetection(this.resetIdleOp);
+    }, 40 * 60 * 1000)
     // if (this.tType == 1) {
     //    // 开局关闭学生查看内容
     //   this.StulookMode(false)

+ 36 - 19
src/components/pptEasyClass/index.vue

@@ -192,6 +192,8 @@ export default {
       recordingEndTime: "", // 结束录音时间
       isResultArray: [],
       isCan: false,
+      timeoutPan: null,
+      isTimeOutPan: false,
     };
   },
   computed: {
@@ -443,10 +445,7 @@ export default {
               // 存储文件和文本到全局对象
               await this.storeRecordingData(file);
               // 记录结束录音时间
-              this.recordingEndTime = new Date().toLocaleString("zh-CN", {
-                hour12: false,
-                timeZone: "Asia/Shanghai"
-              }).replace(/\//g, "-");
+              this.recordingEndTime = await this.getServerTime()
               // 调用 addPPTClass 接口
               this.addPPTClass(file);
               iiframe.contentWindow.onSessionStopped = null;
@@ -465,10 +464,7 @@ export default {
             // 存储文件和文本到全局对象
             await this.storeRecordingData(file);
             // 记录结束录音时间
-            this.recordingEndTime = new Date().toLocaleString("zh-CN", {
-              hour12: false,
-              timeZone: "Asia/Shanghai"
-            }).replace(/\//g, "-");
+            this.recordingEndTime = await this.getServerTime()
             // 调用 addPPTClass 接口
             this.addPPTClass(file);
             resolve(true)
@@ -774,6 +770,7 @@ export default {
               cancelText: this.lang.ssCancel,
               submitText: this.lang.ssConfirm,
               submitCallback: () => {
+                this.isTimeOutPan = false;
                 this.$refs.ppt.contentWindow.PPTistStudent.forceLogout();
                 this.$refs.messageInstructionRef.pptMessage(this.lang.ssStudentLoggedOut)
                 setTimeout(() => {
@@ -782,6 +779,7 @@ export default {
                 }, 1000)
               },
               cancelCallback: () => {
+                this.isTimeOutPan = false;
                 console.log("取消")
               }
             })
@@ -889,12 +887,10 @@ export default {
         this.jArray = Array;
       })
     },
-    doSyncClassData() {
-      if (this.courseDetail.userid == this.userid && this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840') {
-        let endTime = new Date().toLocaleString("zh-CN", {
-          hour12: false,
-          timeZone: "Asia/Shanghai"
-        }).replace(/\//g, "-")
+    async doSyncClassData() {
+      let userArray = ['6fbc6471-1d48-11ed-8c78-005056b86db5','333d0dfc-1cd9-11ef-bee5-005056b86db5','6fbce5ef-1d48-11ed-8c78-005056b86db5','66feffcc-ad35-11ed-b13d-005056b86db5']
+      if (this.courseDetail.userid == this.userid && (this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840' || userArray.includes(this.userid))) {
+        let endTime = await this.getServerTime()
         let courseTime = Math.floor((new Date(endTime) - new Date(this.startTime)) / (1000 * 60))
         this.syncClassData({
           courseId: this.id,
@@ -954,20 +950,41 @@ export default {
     openShareDialog() {
       let code = this.userJson.oidCode || this.userJson.orgCode
       this.$refs.shareDialogRef.open(code, this.inviteCode)
+    },
+    resetIdleOp(){
+      if(this.courseDetail.userId == this.userid){
+        this.isTimeOutPan = true;
+        this.afterClass()
+        setTimeout(() => {
+          if(this.isTimeOutPan){
+            this.$refs.ppt.contentWindow.PPTistStudent.forceLogout();
+            this.$refs.messageInstructionRef.pptMessage(this.lang.ssStudentLoggedOut)
+            setTimeout(() => {
+              this.tcid2 = ""
+              this.refreshCourse()
+            }, 1000)
+          }
+        }, 5000);
+      }
+      // console.log('resetIdleOp','重置空闲定时器')
     }
-    
   },
   destroyed() {
     clearInterval(this.opertimer);
     this.opertimer = null;
+    clearTimeout(this.timeoutPan);
+    this.timeoutPan = null;
     this.doSyncClassData();
+    this.stopIdleDetection(this.resetIdleOp);
   },
   async mounted() {
     this.setoTime("1");
-    this.startTime = new Date().toLocaleString("zh-CN", {
-      hour12: false,
-      timeZone: "Asia/Shanghai"
-    }).replace(/\//g, "-")
+    this.startTime = await this.getServerTime()
+    console.log('this.startTime', this.startTime)
+    this.timeoutPan = setTimeout(() => {
+      this.startIdleDetection(this.resetIdleOp);
+    }, 40 * 60 * 1000)
+    // this.startIdleDetection(this.resetIdleOp);
     this.getClassName()
     this.getAIJ();
     this.getCourseDetail();

+ 37 - 20
src/components/pptEasyClass/indexPS.vue

@@ -192,6 +192,8 @@ export default {
       recordingEndTime: "", // 结束录音时间
       isResultArray: [],
       isCan: false,
+      timeoutPan: null,
+      isTimeOutPan: false,
     };
   },
   computed: {
@@ -443,10 +445,7 @@ export default {
               // 存储文件和文本到全局对象
               await this.storeRecordingData(file);
               // 记录结束录音时间
-              this.recordingEndTime = new Date().toLocaleString("zh-CN", {
-                hour12: false,
-                timeZone: "Asia/Shanghai"
-              }).replace(/\//g, "-");
+              this.recordingEndTime = await this.getServerTime()
               // 调用 addPPTClass 接口
               this.addPPTClass(file);
               iiframe.contentWindow.onSessionStopped = null;
@@ -465,10 +464,7 @@ export default {
             // 存储文件和文本到全局对象
             await this.storeRecordingData(file);
             // 记录结束录音时间
-            this.recordingEndTime = new Date().toLocaleString("zh-CN", {
-              hour12: false,
-              timeZone: "Asia/Shanghai"
-            }).replace(/\//g, "-");
+            this.recordingEndTime = await this.getServerTime()
             // 调用 addPPTClass 接口
             this.addPPTClass(file);
             resolve(true)
@@ -774,6 +770,7 @@ export default {
               cancelText: this.lang.ssCancel,
               submitText: this.lang.ssConfirm,
               submitCallback: () => {
+                this.isTimeOutPan = false;
                 this.$refs.ppt.contentWindow.PPTistStudent.forceLogout();
                 this.$refs.messageInstructionRef.pptMessage(this.lang.ssStudentLoggedOut)
                 setTimeout(() => {
@@ -782,6 +779,7 @@ export default {
                 }, 1000)
               },
               cancelCallback: () => {
+                this.isTimeOutPan = false;
                 console.log("取消")
               }
             })
@@ -846,7 +844,7 @@ export default {
         let item = this.isResultArray[i];
         if(value && item.isTool){
           item.can = true
-        }else if(!item.isTool){
+        }else if(item.isTool){
           item.can = false
           item.like = false
         }
@@ -889,12 +887,10 @@ export default {
         this.jArray = Array;
       })
     },
-    doSyncClassData() {
-      if (this.courseDetail.userid == this.userid && this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840') {
-        let endTime = new Date().toLocaleString("zh-CN", {
-          hour12: false,
-          timeZone: "Asia/Shanghai"
-        }).replace(/\//g, "-")
+    async doSyncClassData() {
+      let userArray = ['6fbc6471-1d48-11ed-8c78-005056b86db5','333d0dfc-1cd9-11ef-bee5-005056b86db5','6fbce5ef-1d48-11ed-8c78-005056b86db5','66feffcc-ad35-11ed-b13d-005056b86db5']
+      if (this.courseDetail.userid == this.userid && (this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840' || userArray.includes(this.userid))) {
+        let endTime = await this.getServerTime()
         let courseTime = Math.floor((new Date(endTime) - new Date(this.startTime)) / (1000 * 60))
         this.syncClassData({
           courseId: this.id,
@@ -954,20 +950,41 @@ export default {
     openShareDialog() {
       let code = this.userJson.oidCode || this.userJson.orgCode
       this.$refs.shareDialogRef.open(code, this.inviteCode)
+    },
+    resetIdleOp(){
+      if(this.courseDetail.userId == this.userid){
+        this.isTimeOutPan = true;
+        this.afterClass()
+        setTimeout(() => {
+          if(this.isTimeOutPan){
+            this.$refs.ppt.contentWindow.PPTistStudent.forceLogout();
+            this.$refs.messageInstructionRef.pptMessage(this.lang.ssStudentLoggedOut)
+            setTimeout(() => {
+              this.tcid2 = ""
+              this.refreshCourse()
+            }, 1000)
+          }
+        }, 5000);
+      }
+      // console.log('resetIdleOp','重置空闲定时器')
     }
-    
   },
   destroyed() {
     clearInterval(this.opertimer);
     this.opertimer = null;
+    clearTimeout(this.timeoutPan);
+    this.timeoutPan = null;
     this.doSyncClassData();
+    this.stopIdleDetection(this.resetIdleOp);
   },
   async mounted() {
     this.setoTime("1");
-    this.startTime = new Date().toLocaleString("zh-CN", {
-      hour12: false,
-      timeZone: "Asia/Shanghai"
-    }).replace(/\//g, "-")
+    this.startTime = await this.getServerTime()
+    console.log('this.startTime', this.startTime)
+    this.timeoutPan = setTimeout(() => {
+      this.startIdleDetection(this.resetIdleOp);
+    }, 40 * 60 * 1000)
+    // this.startIdleDetection(this.resetIdleOp);
     this.getClassName()
     this.getAIJ();
     this.getCourseDetail();

+ 36 - 13
src/components/studyStudent.vue

@@ -12449,6 +12449,8 @@ export default {
       getPickLoading:false,
       selectPzLoading:false,
       getSplitScreenDataLoading:false,
+      timeoutPan: null,
+      isTimeOutPan: false,
     };
   },
   methods: {
@@ -21040,12 +21042,10 @@ export default {
       this.IsStulook = flag;
       this.updateIsStulook();
     },
-    doSyncClassData(){
-      if(this.courseDetail.userid == this.userid && this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840'){
-        let endTime = new Date().toLocaleString("zh-CN", {
-            hour12: false,
-            timeZone: "Asia/Shanghai"
-          }).replace(/\//g, "-")
+    async doSyncClassData(){
+      let userArray = ['6fbc6471-1d48-11ed-8c78-005056b86db5','333d0dfc-1cd9-11ef-bee5-005056b86db5','6fbce5ef-1d48-11ed-8c78-005056b86db5','66feffcc-ad35-11ed-b13d-005056b86db5']
+      if (this.courseDetail.userid == this.userid && (this.org == '16ace517-b5c7-4168-a9bb-a9e0035df840' || userArray.includes(this.userid))) {
+        let endTime = await this.getServerTime()
         let courseTime = Math.floor((new Date(endTime) - new Date(this.startTime)) / (1000 * 60))
         this.syncClassData({
           courseId: this.id,
@@ -21059,6 +21059,28 @@ export default {
         console.log('同步数据')
       }
     },
+    resetIdleOp(){
+      this.$message({
+        message: this.lang.ssLongTimeNoOperation,
+        type: "warning",
+      });
+      this.goTo(
+                    '/courseDetail?userid=' +
+                      this.userid +
+                      '&oid=' +
+                      this.oid +
+                      '&org=' +
+                      this.org +
+                      '&cid=' +
+                      this.id +
+                      '&courseId=' +
+                      this.id +
+                      '&tType=' +
+                      this.tType +
+                      '&screenType=' +
+                      this.screenType
+                  )
+    }
   },
   directives: {
     // 使用局部注册指令的方式
@@ -21106,6 +21128,9 @@ export default {
     this.opertimer = null;
 		this.updateSplitScreenData(1);
     this.doSyncClassData();
+    clearTimeout(this.timeoutPan);
+    this.timeoutPan = null;
+    this.stopIdleDetection(this.resetIdleOp);
   },
   computed: {
     renderedFormula2() {
@@ -21305,15 +21330,13 @@ export default {
         return c;
       };
     },
-
   },
-  mounted() {
+  async mounted() {
     this.setoTime("1");
-    this.startTime = new Date().toLocaleString("zh-CN", {
-      hour12: false,
-      timeZone: "Asia/Shanghai"
-    }).replace(/\//g, "-");
-
+    this.startTime = await this.getServerTime()
+    this.timeoutPan = setTimeout(() => {
+      this.startIdleDetection(this.resetIdleOp);
+    }, 40 * 60 * 1000)
     // if (this.tType == 1) {
     //    // 开局关闭学生查看内容
     //   this.StulookMode(false)

+ 2 - 1
src/lang/cn.json

@@ -906,5 +906,6 @@
   "ssPleaseGenerateLink": "请先生成链接",
   "ssCopyFailed": "复制失败",
   "ssShowResult": "展示结果",
-  "ssHideResult": "隐藏结果"
+  "ssHideResult": "隐藏结果",
+  "ssLongTimeNoOperation": "长时间无操作,将自动退出"
 }

+ 2 - 1
src/lang/en.json

@@ -906,5 +906,6 @@
   "ssPleaseGenerateLink": "Please generate link first",
   "ssCopyFailed": "Copy failed",
   "ssShowResult": "Show Result",
-  "ssHideResult": "Hide Result"
+  "ssHideResult": "Hide Result",
+  "ssLongTimeNoOperation": "Long time no operation, will be logged out"
 }

+ 2 - 1
src/lang/hk.json

@@ -906,5 +906,6 @@
   "ssPleaseGenerateLink": "請先生成鏈接",
   "ssCopyFailed": "複製失敗",
   "ssShowResult": "展示結果",
-  "ssHideResult": "隱藏結果"
+  "ssHideResult": "隱藏結果",
+  "ssLongTimeNoOperation": "長時間無操作,將自動退出"
 }

+ 55 - 0
src/mixins/mixin.js

@@ -3,9 +3,64 @@ export const myMixin = {
     return {
         userJson: {},
         packageArray: [],
+        idleTimer: null,
+        idleTimeout: 600000,
     };
   },
+  mounted() {
+    // this.startIdleDetection();
+  },
+  beforeUnmount() {
+    // this.stopIdleDetection();
+  },
   methods: {
+    async getServerTime() {
+      try {
+        let res = await this.ajax.get(this.$store.state.api + 'getServerTime'); 
+        console.log(res)
+        let time = res.data.time
+        return new Date(time).toLocaleString("zh-CN", {
+          hour12: false,
+          timeZone: "Asia/Shanghai"
+        }).replace(/\//g, "-")
+      } catch (error) {
+        return new Date().toLocaleString("zh-CN", {
+          hour12: false,
+          timeZone: "Asia/Shanghai"
+        }).replace(/\//g, "-")
+      }
+    },
+    startIdleDetection(callback) {
+      const events = ['mousedown', 'mousemove', 'keydown', 'touchstart', 'scroll'];
+      events.forEach(event => {
+        document.addEventListener(event, this.handleUserActivity.bind(null, callback));
+      });
+    },
+    stopIdleDetection(callback) {
+      const events = ['mousedown', 'mousemove', 'keydown', 'touchstart', 'scroll'];
+      if (this.idleTimer) {
+        clearTimeout(this.idleTimer);
+        this.idleTimer = null;
+      }
+      events.forEach(event => {
+        document.removeEventListener(event, this.handleUserActivity.bind(null, callback));
+      });
+    },
+    resetIdleTimer(callback) {
+      if (this.idleTimer) {
+        clearTimeout(this.idleTimer);
+        this.idleTimer = null;
+      }
+      this.idleTimer = setTimeout(() => {
+        callback ? callback() : this.onIdle();
+      }, this.idleTimeout);
+    },
+    handleUserActivity(callback) {
+      this.resetIdleTimer(callback);
+    },
+    onIdle() {
+      console.log('用户已空闲10秒,执行空闲回调函数');
+    },
     detectBrowser() {
       const ua = navigator.userAgent;