Browse Source

选择题判分重新识别websocket链接优化 识别完成 关闭弹窗自动断开链接

dengshaobo 1 month ago
parent
commit
e70f2f4337

+ 25 - 3
src/utils/webSocket.ts

@@ -11,6 +11,7 @@ export default class WebSocketManager {
     private reconnectAttempts: number = 0; // 当前重连尝试次数
     private url: string; // WebSocket 连接地址
     private receiveMessageCallback: ((data: any) => void) | null = null; // 接收消息回调函数
+    private isManualClose: boolean = false; // 是否为手动关闭(手动关闭不触发重连)
 
     /**
      * 构造函数
@@ -32,6 +33,8 @@ export default class WebSocketManager {
      */
     async start(): Promise<void> {
         if (this.url) {
+            // 重置手动关闭标志,允许自动重连
+            this.isManualClose = false;
             // 连接 WebSocket
             this.connectWebSocket();
         } else {
@@ -75,6 +78,12 @@ export default class WebSocketManager {
                 // 清除定时器
                 this.clearTimers();
                 
+                // 手动关闭不触发重连
+                if (this.isManualClose) {
+                    console.log('WebSocket 手动关闭,不触发重连');
+                    return;
+                }
+                
                 // 尝试重连
                 if (this.reconnectAttempts < this.maxReconnectAttempts) {
                     this.reconnectAttempts++;
@@ -152,20 +161,33 @@ export default class WebSocketManager {
      * 关闭 WebSocket 连接
      */
     closeWebSocket(): void {
+        // 先标记为手动关闭,防止 close 事件触发重连
+        this.isManualClose = true;
+        
+        // 清除所有定时器(在 socket.close 之前清除,防止重连定时器触发)
+        this.clearTimers();
+        
         if (this.socket) {
             this.socket.close();
             this.socket = null;
         }
         
-        // 清除所有定时器
-        this.clearTimers();
-        
         // 重置重连次数
         this.reconnectAttempts = 0;
         
         console.log('WebSocket 连接已手动关闭');
     }
 
+    /**
+     * 停止重连(不停止当前连接,仅阻止后续自动重连)
+     * 适用于服务端主动关闭连接的场景(如:账号已在其他地方登录)
+     */
+    stopReconnect(): void {
+        this.isManualClose = true;
+        this.clearTimers();
+        console.log('WebSocket 已停止自动重连');
+    }
+
     /**
      * 清除心跳定时器
      */

+ 20 - 39
src/views/exam/abnormal/abnormal.vue

@@ -50,7 +50,7 @@
                             标准模板
                         </div>
                         <div class="header_right">
-                            <div :class="item.pageNum==currentTemplatePageIndex?'page_num_active':'page_num_item'"  v-for="(item,index) in currentTemplateList" :key="'template_'+index" @click="ChangeTemplatePage(item)">{{item.pageNum}}</div>
+                            <div :class="item.pageNum==currentTemplatePageIndex?'page_num_active':'page_num_item'"  v-for="(item,index) in templateList" :key="'template_'+index" @click="ChangeTemplatePage(item)">{{item.pageNum}}</div>
                         </div>
                     </div>
                     <div class="content_canvas">
@@ -329,7 +329,7 @@ import { useExamStore } from '@/store/exam'
 import { useRouter } from 'vue-router'
 import { onMounted,onUnmounted, ref, computed, nextTick, watch } from 'vue';
 import { ElMessage,ElMessageBox } from 'element-plus'
-import { getExamAbnormalList,getSearchScanStudents,claimAnswerCard, correctObjectiveQuestion,markPositionAbnormal, deletePaper ,reRecognizePosition,getTemplateDetail} from '@/api/exam'
+import { getExamAbnormalList,getSearchScanStudents,claimAnswerCard, correctObjectiveQuestion,markPositionAbnormal, deletePaper ,reRecognizePosition} from '@/api/exam'
 
 // 引入子组件 (确保路径正确)
 import PositionCanvas from '@/components/PositionCanvas.vue' 
@@ -341,6 +341,14 @@ import AddStudent from '../components/addStudent.vue' // 新增学生弹窗
 const examStore = useExamStore()
 const router = useRouter()
 
+// 定义 Props
+const props = defineProps({
+    templateList: {
+        type: Array as () => any[],
+        default: () => [],
+    }, // 模板数据(由父组件统一获取并传入)
+})
+
 // ==================== 考试通用数据(可追加)====================
 // 考试科目 ID
 const examSubjectId = computed(() => {
@@ -420,7 +428,7 @@ const showAddStudent=ref(false);//新增学生弹窗
 const usedCardType = ref(2) // 1 系统卡  2 三方卡  默认2
 const sourcePoints = ref<any[]>([]) // 模板点位
 const targetPoints = ref<any[]>([]) // 学生点位
-const currentTemplateList = ref<any[]>([]) // 模板数据
+// 模板数据已提升至父组件 abnormalDetail.vue,通过 props.templateList 传入
 
 //客观题异常相关
 const currentQuestionList= ref<any[]>([]);//客观题题目异常校正列表
@@ -601,8 +609,8 @@ const ShowDrawData = () => {
 const ObjectDataChange = () => {
     // TODO: 根据 isShowDrawType 重新渲染右侧列表或 Canvas
     console.log('客观题答案回显', )
-    console.log("打印模板数据",currentTemplateList.value);
-    let questionList=currentTemplateList.value[0].examCardGroupVOS;//选择题判分就一页 
+    console.log("打印模板数据",props.templateList);
+    let questionList=props.templateList[0].examCardGroupVOS;//选择题判分就一页 
     console.log("打印客观题模板数据",questionList);
     const drawList: any[] = [];
     questionList.forEach((item:any, index:number) => {
@@ -851,7 +859,7 @@ const SavePositionImage = () => {
         return
     }
     
-    console.log("打印当前的模板页",currentTemplateList.value);
+    console.log("打印当前的模板页",props.templateList);
     console.log("打印当前的模板页码",currentTemplatePageIndex.value);
     console.log("打印当前currentPaperList",currentPaperList.value);
     console.log("打印当前图片的地址",currentPaperUrl.value);
@@ -1663,38 +1671,7 @@ const GetAbnormalList = async () => {
 
 }
  
-//获取模板详情
-const GetTemplateDetail = async () => { 
-    const params={
-        examSubjectId: examSubjectId.value,//考试id
-    };
-    const res = await getTemplateDetail(params);
-    console.log("打印模板数据",res);
-    if(res.code == 200)
-    {
-       currentTemplateList.value=[];
-       res.data.forEach((item:any)=>{ 
-        let obj={
-            examCardGroupVOS: item.examCardGroupVOS,
-            pageNo: item.pageNo,
-            paperInfo: item.paperInfo,
-            templateUrl: item.templateUrl,
-            pageNum: item.pageNo,
-
-        };
-        currentTemplateList.value.push(obj);
-        currentTemplatePageIndex.value=currentTemplateList.value[0].pageNum;
-        currentTemplateUrl.value=item.templateUrl;
-       })
-       
-
-
-    }
-    else
-    {
-
-    }
-}
+//获取模板详情(已提升至父组件 abnormalDetail.vue,通过 props.templateList 传入)
 
 // ==================== 生命周期 ====================
 
@@ -1707,7 +1684,11 @@ onMounted(() => {
         // LoadAbnormalList(abnormalType.value)
     }
     GetAbnormalList();
-    GetTemplateDetail();//获取模板数据
+    // 从 props 初始化模板页码和模板图片地址
+    if (props.templateList && props.templateList.length > 0) {
+        currentTemplatePageIndex.value = props.templateList[0].pageNum;
+        currentTemplateUrl.value = props.templateList[0].templateUrl;
+    }
     // 监听窗口变化调整高度
     window.addEventListener('resize', CalculateTableHeight)
     CalculateTableHeight()

+ 8 - 36
src/views/exam/abnormal/tableList.vue

@@ -333,7 +333,7 @@ import { useExamStore } from '@/store/exam'
 import { useUserStore } from '@/store/user'
 import { useRouter } from 'vue-router'
 import { onMounted ,ref,nextTick,onUnmounted,computed, watch} from 'vue';
-import {deleteAnswerCard, getExamStudentList,updateScannedStatus,updateExamNumberException,correctObjectiveQuestion,getTemplateDetail } from '@/api/exam';
+import {deleteAnswerCard, getExamStudentList,updateScannedStatus,updateExamNumberException,correctObjectiveQuestion } from '@/api/exam';
 import { ElMessageBox, ElMessage } from 'element-plus'
 
 // ... existing code ...
@@ -365,6 +365,10 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   }, // 答案
+  templateList: {
+    type: Array as () => any[],
+    default: () => [],
+  }, // 模板数据(由父组件统一获取并传入)
 })
 
 //监听props变化
@@ -427,7 +431,7 @@ const reviewQuestionIndex=ref('');//当前选择的题目索引
 const showPaperPreview=ref(false);//预览试卷
 const previewImageList=ref<any[]>([]);//图片列表
 const previewAnswerData=ref<any[]>([]);//答案数据
-const currentTemplateList=ref<any[]>([]);//模板数据
+
 
 // 客观题重新识别
 const isReIdentify=ref(false);//打开重新识别弹窗
@@ -735,8 +739,8 @@ const RightStudentRowClick = (item: any) => {
 const ObjectDataChange = () => {
     // TODO: 根据 isShowDrawType 重新渲染右侧列表或 Canvas
     console.log('客观题答案回显', )
-    console.log("打印模板数据",currentTemplateList.value);
-    let questionList=currentTemplateList.value[0].examCardGroupVOS;//选择题判分就一页 
+    console.log("打印模板数据",props.templateList);
+    let questionList=props.templateList[0].examCardGroupVOS;//选择题判分就一页 
     console.log("打印客观题模板数据",questionList);
     const drawList: any[] = [];
     questionList.forEach((item:any, index:number) => {
@@ -821,38 +825,7 @@ const ObjectDataChange = () => {
 
 }
 
-//获取模板详情
-const GetTemplateDetail = async () => { 
-    const params={
-        examSubjectId: examSubjectId.value,//考试id
-    };
-    const res = await getTemplateDetail(params);
-    console.log("打印模板数据",res);
-    if(res.code == 200)
-    {
-       currentTemplateList.value=[];
-       res.data.forEach((item:any)=>{ 
-        let obj={
-            examCardGroupVOS: item.examCardGroupVOS,
-            pageNo: item.pageNo,
-            paperInfo: item.paperInfo,
-            templateUrl: item.templateUrl,
-            pageNum: item.pageNo,
-
-        };
-        currentTemplateList.value.push(obj);
-        localStorage.setItem("templateData",JSON.stringify(currentTemplateList.value));//保存模板数据到本地
-       
-       })
-       
-
 
-    }
-    else
-    {
-
-    }
-}
 
 
 /**
@@ -1880,7 +1853,6 @@ onMounted(() => {
     }
     CalculateTableHeight()//计算表格高度
     GetExamStudentList();//获取学生列表
-    GetTemplateDetail();//获取模板数据
     // 监听窗口大小变化
     window.addEventListener('resize', CalculateTableHeight);
 })

+ 30 - 3
src/views/exam/abnormalDetail.vue

@@ -23,9 +23,9 @@
             </div>
             <div class="detail_content" v-if="showDialog">
                 <!-- 异常 -->
-                <Abnormal v-if="scanState == 3"></Abnormal>
+                <Abnormal v-if="scanState == 3" :templateList="templateList"></Abnormal>
                 <!-- 状态  0  未扫描  1 已上传 2  缺考  3 异常 -->
-                <TableList :scanState="scanState" v-else></TableList>
+                <TableList :scanState="scanState" :templateList="templateList" v-else></TableList>
             </div>
         </div>
 
@@ -36,7 +36,7 @@ import { useExamStore } from '@/store/exam'
 import { useUserStore } from '@/store/user'
 import { useRouter } from 'vue-router'
 import { onMounted ,ref,computed,onUnmounted,nextTick,watch } from 'vue';
-import { hasImportStudent,getBatchList,getCurrentBatchNo,deleteBatch,updateScanCount } from '@/api/exam'
+import { hasImportStudent,getBatchList,getCurrentBatchNo,deleteBatch,updateScanCount,getTemplateDetail } from '@/api/exam'
 import { ElMessageBox, ElMessage } from 'element-plus'
 
 import TableList from './abnormal/tableList.vue'
@@ -132,6 +132,9 @@ const isImportStudent=ref(false);//是否导入了学生名单
 const showSelectStudent=ref(false);//是否显示选择学生名单弹窗
 
 const scanState=ref(0);//扫描状态 0 未扫描 1 已上传 2 缺考 3 异常
+
+// 模板数据(两个子组件共用,只在父组件中请求一次)
+const templateList = ref<any[]>([]);
 const scanDataInfo=ref({
     abnormalNum:0,//异常数量
     examMissNum:0,//缺考数量
@@ -537,6 +540,29 @@ const HandleScanResult = (res: any) => {
 
 };
 
+//获取模板详情(提升到父组件,避免子组件重复调用)
+const GetTemplateDetail = async () => { 
+    const params={
+        examSubjectId: examSubjectId.value,//考试id
+    };
+    const res = await getTemplateDetail(params);
+    console.log("打印模板数据",res);
+    if(res.code == 200)
+    {
+       templateList.value=[];
+       res.data.forEach((item:any)=>{ 
+        let obj={
+            examCardGroupVOS: item.examCardGroupVOS,
+            pageNo: item.pageNo,
+            paperInfo: item.paperInfo,
+            templateUrl: item.templateUrl,
+            pageNum: item.pageNo,
+        };
+        templateList.value.push(obj);
+       })
+    }
+}
+
 onMounted(() => {
     // // 初始化连接
     // scanCommon.init(HandleScanResult);
@@ -554,6 +580,7 @@ onMounted(() => {
     // HasImportStudent();//查询是否导入了学生名单
 
     CalculateTableHeight();//初始化计算表格高度
+    GetTemplateDetail();//获取模板数据(只调用一次)
   
     // 监听窗口大小变化
     window.addEventListener('resize', CalculateTableHeight);

+ 48 - 10
src/views/exam/components/ReIdentify.vue

@@ -107,7 +107,8 @@ const props = defineProps({
 // ==================== Emits 定义 ====================
 const emit = defineEmits([
     'update:modelValue',   // v-model 更新事件
-    'ReIdentifyComplete'   // 重新识别完成事件
+    'ReIdentifyComplete',  // 重新识别完成事件
+    'close'                // 关闭弹窗事件
 ]);
 
 // ==================== Store 实例化 ====================
@@ -252,9 +253,10 @@ onMounted(() => {
  * 清理资源:关闭WebSocket连接、清除定时器
  */
 onBeforeUnmount(() => {
-    // 清理 WebSocket 连接
-    if (socket.value && socket.value.getSocketStatus() === WebSocket.OPEN) {
-        socket.value.closeWebSocket();
+    // 无条件清理 WebSocket 连接(不管当前状态如何)
+    if (socket.value) {
+        socket.value.destroy();
+        socket.value = null;
     }
     // 清理定时器(当前未使用,保留以防后续扩展)
     // if (debounceTimer.value) {
@@ -269,9 +271,10 @@ onBeforeUnmount(() => {
  * @description 用于接收后端实时推送的识别进度消息
  */
 const ReIdentifyWebSocketLink = () => {
-    // 如果已有连接且处于打开状态,先关闭
-    if (socket.value && socket.value.getSocketStatus() === WebSocket.OPEN) {
-        socket.value.closeWebSocket();
+    // 如果已有连接,无条件销毁(不管是否 OPEN,CONNECTING 状态也要清理)
+    if (socket.value) {
+        socket.value.destroy();
+        socket.value = null;
     }
 
     // 构建 WebSocket 用户标识:AGAIN_ + 科目ID
@@ -357,7 +360,31 @@ const MessageBack = (data: string) => {
     const msg = JSON.parse(data);
     console.log("Websocket消息返回处理", msg);
 
-    // code: 0-建立成功, 200-业务成功, 400-业务失败/异常
+    // code: 0-建立连接成功, 200-识别进度更新, 400-业务失败/异常(如账号已在其他地方登录)
+    if (msg.code === 0) {
+        // 建立连接成功,无需处理
+        console.log('WebSocket 连接已建立');
+        return;
+    }
+
+    if (msg.code === 400) {
+        // 业务异常(如:账户已在其他地方登录),关闭连接并提示用户
+        console.error('WebSocket 业务异常:', msg.msg);
+        ElMessage.error(msg.msg || '识别服务异常,请稍后重试');
+        
+        // 主动关闭连接,阻止服务端关闭后触发自动重连循环
+        if (socket.value) {
+            socket.value.closeWebSocket();
+            socket.value = null;
+        }
+        
+        // 重置识别状态
+        reIdentifyLoading.value = false;
+        reIdentifyText.value = '开始识别';
+        reIdentifyProgress.value = 0;
+        return;
+    }
+
     if (msg.code === 200) {
         // 已识别张数 +1
         recognizedNumber.value++;
@@ -372,6 +399,13 @@ const MessageBack = (data: string) => {
         if (reIdentifyProgress.value === 100) {
             ElMessage.success("重新识别完成!");
             
+            // 识别完成后主动关闭 WebSocket 连接,释放服务端资源
+            // 避免下次重新识别时因同 userId 在线被踢(code:400)
+            if (socket.value) {
+                socket.value.closeWebSocket();
+                socket.value = null;
+            }
+            
             // 重置所有状态
             reIdentifyNumber.value = 0;
             recognizedNumber.value = 0;
@@ -430,11 +464,15 @@ const MessageBack = (data: string) => {
 
 /**
  * 关闭弹窗
- * @description 重置状态并通知父组件
+ * @description 重置状态并通知父组件,同时关闭 WebSocket 连接
  */
 const handleClose = () => {
+    // 关闭弹窗时同时关闭 WebSocket 连接,避免残留连接
+    if (socket.value) {
+        socket.value.closeWebSocket();
+        socket.value = null;
+    }
     showDialog.value = false;
-    // selectedFiles.value = [];  // 当前未使用
     emit('close');
 };
 

+ 3 - 2
src/views/exam/components/addStudent.vue

@@ -234,8 +234,9 @@ watch(
     if (newVal) {
       if (props.selectSchoolId != -1) {
         addStudentData.schoolId = props.selectSchoolId as any 
-        GetClassListData()
+       
       }
+       GetClassListData()
     }
   }
 )
@@ -452,7 +453,7 @@ defineExpose({
 })
 
 onMounted(() => {
-  GetClassListData()
+  // GetClassListData()
 })
 </script>