Răsfoiți Sursa

增加打包命令配置文件

dengshaobo 1 lună în urmă
părinte
comite
33c2eee795

+ 2 - 0
.env.development

@@ -2,3 +2,5 @@
 
 VITE_API_BASE_URL =
 VITE_RSA_PUBLIC_KEY = MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDmkP5hUxr7x3t76s9kiVrP1acGVp3JFyPsSoZzV+FvFSc78rfLSwEhj0WkUC0O+entDZ/hnvX1pSPsY8VwXtc20n5pSuo/G1HzrFSUCmFD9HWX3RRrFTHGn+G01CQswO/2JZokJQzbTsNDUnlK/w//NYQvXhxOhF9fM/S1L73CkYPIabHmIM1Rc8uik66XlTXrb9+gDQ8Uon+QZTGo3BzxWceONYYnVZZXdFQtzvaHLI5av8LSU5MLp03faX8fYlfB5FwyBL+dQZRVyTP09pbESn30487tsa0/+0pRZ1D+4Tsc2cjIpuXEg1IKGLrs0TSX78T1fROt/JcZ/lt3Tn9xAgMBAAECggEADEAD4/PgaSQuIWVWY4cQth4p46JSe86o7/L9tb8jkR1UmlDJBxoTE09jadmAq10H2rpwljI16zk88WBTqya+1IDWio2aaIPxFLtBOyRaCpxAazMp1I6puF3iRhNHYMFXfoJ88BKv3i8PHNKS8zMeDHcxcLrVUi6iSpKeG8pPkLjEqY0eccYjTUOyh2Q8YmLLwO3+lOOYrD9TcUZ1NqsZCqOI3pzLNONYatqaPprqiuZLUcloiO7D1OJ52LtTzlP2z7/jXM4fwUZK1/9U79zMYmjJifIORY9UiildkGaKzYs3G1ZAKiLKBehf7tGzorU0+tSIF99voyjhed286fMeqQKBgQDn7wH6oadnfZL5NknEaioua4GJ0LhBeZCiaLReYs116554zzBubb4Y752DTE4ka8LKsSIcnCqNbc/b6q0Kw+yThJJ5F/nFgaCzKHfhImqtslolqW5K1UVwUZ9RdFUeeqt8w+CtcnrJs/jIswv7XOnfHm6VKd0gSlpbnAPEVua75wKBgQD+farEb4PCNsOTaLQYcy0dSDh68lBskc5k+kMYmDsUloaOQel2S9ufEEYxZgy4Y2b0U9Y8nEJByFBoKZW+Mplp+hH9OWaK/QpMylh1uwoN44jUyRAKhR211kBZMXcgbIjr49PxG261OydxIyu9ntcJApVpHK30qzR+DBRD7H8+5wKBgQDGz/hQUZXgfqIoAkNFnQO/euQ1sLbhWUWEEmDar7MTq//R6zjG0EettGi/Df/F9KGrgh+NishnJ4SQLSBcJAp9gZzVNJoklbOdH8lzMT9k2Yew1QX4G81ENJNvDVuRnvG1J2tHAuUCVcWitOhGdiT732hHcPVeIp5F/Py1pxBubQKBgQDlVvJxm90tRJTzXsQN1J2vacocYgpADRXmwfF9VJLJdu1DffqadLoymkPneIO2Fz5MqNDERj0fcxmjBPbBNHA0pPtZLEVQs8B4e1FEp43j/kztFVSzZkrj93R97KniOm0Zx3LUMViPUgO1XXCprV8z63QiCYpql27yuIf6vkHduQKBgEe/BLwVHxAxmGzt/k0t9TZYn4MMQd5nx1UWlFzI+mNkNQqNC5cLGWarazeHC2YdcaYAsSMfi11NTvnVY79EbXYomxSBoN3Y63ML7W6Cj9fInVtUF6j002sdRyfey+e2BQtYiUHb9xW6A7sf4LyLZJ64LGYIh1uqWQc2iUeUsSWQ
+# webSocket环境地址配置
+VUE_APP_WEB_SOCKET_URL= 'wss://dev3.k12100.net/teaching/api/v1/teaching/websocket/'

+ 4 - 2
.env.production

@@ -1,4 +1,6 @@
-# 生成环境配置
+# 生产环境配置文件
 
 VITE_API_BASE_URL = https://dev3.k12100.net/teaching
-VITE_RSA_PUBLIC_KEY = MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDmkP5hUxr7x3t76s9kiVrP1acGVp3JFyPsSoZzV+FvFSc78rfLSwEhj0WkUC0O+entDZ/hnvX1pSPsY8VwXtc20n5pSuo/G1HzrFSUCmFD9HWX3RRrFTHGn+G01CQswO/2JZokJQzbTsNDUnlK/w//NYQvXhxOhF9fM/S1L73CkYPIabHmIM1Rc8uik66XlTXrb9+gDQ8Uon+QZTGo3BzxWceONYYnVZZXdFQtzvaHLI5av8LSU5MLp03faX8fYlfB5FwyBL+dQZRVyTP09pbESn30487tsa0/+0pRZ1D+4Tsc2cjIpuXEg1IKGLrs0TSX78T1fROt/JcZ/lt3Tn9xAgMBAAECggEADEAD4/PgaSQuIWVWY4cQth4p46JSe86o7/L9tb8jkR1UmlDJBxoTE09jadmAq10H2rpwljI16zk88WBTqya+1IDWio2aaIPxFLtBOyRaCpxAazMp1I6puF3iRhNHYMFXfoJ88BKv3i8PHNKS8zMeDHcxcLrVUi6iSpKeG8pPkLjEqY0eccYjTUOyh2Q8YmLLwO3+lOOYrD9TcUZ1NqsZCqOI3pzLNONYatqaPprqiuZLUcloiO7D1OJ52LtTzlP2z7/jXM4fwUZK1/9U79zMYmjJifIORY9UiildkGaKzYs3G1ZAKiLKBehf7tGzorU0+tSIF99voyjhed286fMeqQKBgQDn7wH6oadnfZL5NknEaioua4GJ0LhBeZCiaLReYs116554zzBubb4Y752DTE4ka8LKsSIcnCqNbc/b6q0Kw+yThJJ5F/nFgaCzKHfhImqtslolqW5K1UVwUZ9RdFUeeqt8w+CtcnrJs/jIswv7XOnfHm6VKd0gSlpbnAPEVua75wKBgQD+farEb4PCNsOTaLQYcy0dSDh68lBskc5k+kMYmDsUloaOQel2S9ufEEYxZgy4Y2b0U9Y8nEJByFBoKZW+Mplp+hH9OWaK/QpMylh1uwoN44jUyRAKhR211kBZMXcgbIjr49PxG261OydxIyu9ntcJApVpHK30qzR+DBRD7H8+5wKBgQDGz/hQUZXgfqIoAkNFnQO/euQ1sLbhWUWEEmDar7MTq//R6zjG0EettGi/Df/F9KGrgh+NishnJ4SQLSBcJAp9gZzVNJoklbOdH8lzMT9k2Yew1QX4G81ENJNvDVuRnvG1J2tHAuUCVcWitOhGdiT732hHcPVeIp5F/Py1pxBubQKBgQDlVvJxm90tRJTzXsQN1J2vacocYgpADRXmwfF9VJLJdu1DffqadLoymkPneIO2Fz5MqNDERj0fcxmjBPbBNHA0pPtZLEVQs8B4e1FEp43j/kztFVSzZkrj93R97KniOm0Zx3LUMViPUgO1XXCprV8z63QiCYpql27yuIf6vkHduQKBgEe/BLwVHxAxmGzt/k0t9TZYn4MMQd5nx1UWlFzI+mNkNQqNC5cLGWarazeHC2YdcaYAsSMfi11NTvnVY79EbXYomxSBoN3Y63ML7W6Cj9fInVtUF6j002sdRyfey+e2BQtYiUHb9xW6A7sf4LyLZJ64LGYIh1uqWQc2iUeUsSWQ
+VITE_RSA_PUBLIC_KEY = MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDmkP5hUxr7x3t76s9kiVrP1acGVp3JFyPsSoZzV+FvFSc78rfLSwEhj0WkUC0O+entDZ/hnvX1pSPsY8VwXtc20n5pSuo/G1HzrFSUCmFD9HWX3RRrFTHGn+G01CQswO/2JZokJQzbTsNDUnlK/w//NYQvXhxOhF9fM/S1L73CkYPIabHmIM1Rc8uik66XlTXrb9+gDQ8Uon+QZTGo3BzxWceONYYnVZZXdFQtzvaHLI5av8LSU5MLp03faX8fYlfB5FwyBL+dQZRVyTP09pbESn30487tsa0/+0pRZ1D+4Tsc2cjIpuXEg1IKGLrs0TSX78T1fROt/JcZ/lt3Tn9xAgMBAAECggEADEAD4/PgaSQuIWVWY4cQth4p46JSe86o7/L9tb8jkR1UmlDJBxoTE09jadmAq10H2rpwljI16zk88WBTqya+1IDWio2aaIPxFLtBOyRaCpxAazMp1I6puF3iRhNHYMFXfoJ88BKv3i8PHNKS8zMeDHcxcLrVUi6iSpKeG8pPkLjEqY0eccYjTUOyh2Q8YmLLwO3+lOOYrD9TcUZ1NqsZCqOI3pzLNONYatqaPprqiuZLUcloiO7D1OJ52LtTzlP2z7/jXM4fwUZK1/9U79zMYmjJifIORY9UiildkGaKzYs3G1ZAKiLKBehf7tGzorU0+tSIF99voyjhed286fMeqQKBgQDn7wH6oadnfZL5NknEaioua4GJ0LhBeZCiaLReYs116554zzBubb4Y752DTE4ka8LKsSIcnCqNbc/b6q0Kw+yThJJ5F/nFgaCzKHfhImqtslolqW5K1UVwUZ9RdFUeeqt8w+CtcnrJs/jIswv7XOnfHm6VKd0gSlpbnAPEVua75wKBgQD+farEb4PCNsOTaLQYcy0dSDh68lBskc5k+kMYmDsUloaOQel2S9ufEEYxZgy4Y2b0U9Y8nEJByFBoKZW+Mplp+hH9OWaK/QpMylh1uwoN44jUyRAKhR211kBZMXcgbIjr49PxG261OydxIyu9ntcJApVpHK30qzR+DBRD7H8+5wKBgQDGz/hQUZXgfqIoAkNFnQO/euQ1sLbhWUWEEmDar7MTq//R6zjG0EettGi/Df/F9KGrgh+NishnJ4SQLSBcJAp9gZzVNJoklbOdH8lzMT9k2Yew1QX4G81ENJNvDVuRnvG1J2tHAuUCVcWitOhGdiT732hHcPVeIp5F/Py1pxBubQKBgQDlVvJxm90tRJTzXsQN1J2vacocYgpADRXmwfF9VJLJdu1DffqadLoymkPneIO2Fz5MqNDERj0fcxmjBPbBNHA0pPtZLEVQs8B4e1FEp43j/kztFVSzZkrj93R97KniOm0Zx3LUMViPUgO1XXCprV8z63QiCYpql27yuIf6vkHduQKBgEe/BLwVHxAxmGzt/k0t9TZYn4MMQd5nx1UWlFzI+mNkNQqNC5cLGWarazeHC2YdcaYAsSMfi11NTvnVY79EbXYomxSBoN3Y63ML7W6Cj9fInVtUF6j002sdRyfey+e2BQtYiUHb9xW6A7sf4LyLZJ64LGYIh1uqWQc2iUeUsSWQ
+# webSocket环境地址配置
+VITE_WEB_SOCKET_URL=wss://www.k12100.com/teaching/api/v1/teaching/websocket/

+ 7 - 0
.env.test

@@ -0,0 +1,7 @@
+# 测试环境配置文件
+
+VITE_API_BASE_URL = https://dev3.k12100.net/teaching
+VITE_RSA_PUBLIC_KEY = MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDmkP5hUxr7x3t76s9kiVrP1acGVp3JFyPsSoZzV+FvFSc78rfLSwEhj0WkUC0O+entDZ/hnvX1pSPsY8VwXtc20n5pSuo/G1HzrFSUCmFD9HWX3RRrFTHGn+G01CQswO/2JZokJQzbTsNDUnlK/w//NYQvXhxOhF9fM/S1L73CkYPIabHmIM1Rc8uik66XlTXrb9+gDQ8Uon+QZTGo3BzxWceONYYnVZZXdFQtzvaHLI5av8LSU5MLp03faX8fYlfB5FwyBL+dQZRVyTP09pbESn30487tsa0/+0pRZ1D+4Tsc2cjIpuXEg1IKGLrs0TSX78T1fROt/JcZ/lt3Tn9xAgMBAAECggEADEAD4/PgaSQuIWVWY4cQth4p46JSe86o7/L9tb8jkR1UmlDJBxoTE09jadmAq10H2rpwljI16zk88WBTqya+1IDWio2aaIPxFLtBOyRaCpxAazMp1I6puF3iRhNHYMFXfoJ88BKv3i8PHNKS8zMeDHcxcLrVUi6iSpKeG8pPkLjEqY0eccYjTUOyh2Q8YmLLwO3+lOOYrD9TcUZ1NqsZCqOI3pzLNONYatqaPprqiuZLUcloiO7D1OJ52LtTzlP2z7/jXM4fwUZK1/9U79zMYmjJifIORY9UiildkGaKzYs3G1ZAKiLKBehf7tGzorU0+tSIF99voyjhed286fMeqQKBgQDn7wH6oadnfZL5NknEaioua4GJ0LhBeZCiaLReYs116554zzBubb4Y752DTE4ka8LKsSIcnCqNbc/b6q0Kw+yThJJ5F/nFgaCzKHfhImqtslolqW5K1UVwUZ9RdFUeeqt8w+CtcnrJs/jIswv7XOnfHm6VKd0gSlpbnAPEVua75wKBgQD+farEb4PCNsOTaLQYcy0dSDh68lBskc5k+kMYmDsUloaOQel2S9ufEEYxZgy4Y2b0U9Y8nEJByFBoKZW+Mplp+hH9OWaK/QpMylh1uwoN44jUyRAKhR211kBZMXcgbIjr49PxG261OydxIyu9ntcJApVpHK30qzR+DBRD7H8+5wKBgQDGz/hQUZXgfqIoAkNFnQO/euQ1sLbhWUWEEmDar7MTq//R6zjG0EettGi/Df/F9KGrgh+NishnJ4SQLSBcJAp9gZzVNJoklbOdH8lzMT9k2Yew1QX4G81ENJNvDVuRnvG1J2tHAuUCVcWitOhGdiT732hHcPVeIp5F/Py1pxBubQKBgQDlVvJxm90tRJTzXsQN1J2vacocYgpADRXmwfF9VJLJdu1DffqadLoymkPneIO2Fz5MqNDERj0fcxmjBPbBNHA0pPtZLEVQs8B4e1FEp43j/kztFVSzZkrj93R97KniOm0Zx3LUMViPUgO1XXCprV8z63QiCYpql27yuIf6vkHduQKBgEe/BLwVHxAxmGzt/k0t9TZYn4MMQd5nx1UWlFzI+mNkNQqNC5cLGWarazeHC2YdcaYAsSMfi11NTvnVY79EbXYomxSBoN3Y63ML7W6Cj9fInVtUF6j002sdRyfey+e2BQtYiUHb9xW6A7sf4LyLZJ64LGYIh1uqWQc2iUeUsSWQ
+
+# webSocket环境地址配置
+VITE_WEB_SOCKET_URL= wss://dev3.k12100.net/teaching/api/v1/teaching/websocket/

+ 2 - 1
package.json

@@ -5,7 +5,8 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": "vite build",
+    "build:test": "vite build --mode test",
+    "build:dev": "vite build --mode production",
     "preview": "vite preview"
   },
   "dependencies": {

+ 9 - 0
src/api/exam.ts

@@ -317,4 +317,13 @@ export const correctObjectiveQuestion= (data:any):Promise<ApiResponse> => {
     method: 'post',
     data
   })
+}
+
+// 35. 扫描学生  定位异常 重新识别
+export const reRecognizePosition= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_exam_scan/delete_objective_question',
+    method: 'post',
+    data
+  })
 }

BIN
src/assets/icon/position_icon.png


+ 2 - 2
src/components/PositionCanvas.vue

@@ -10,7 +10,7 @@
 <script setup>
 import { ref, reactive, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
 import { throttle } from 'lodash';
-
+import positionIcon from '../assets/icon/position_icon.png';
 // props定义
 const props = defineProps({
   paperType: {
@@ -394,7 +394,7 @@ const DrawImage = () => {
 // 加载定位图标
 const DrawPosition = () => {
   let positionImg = new Image();
-  positionImg.src = require('../assets/position_icon.png');
+  positionImg.src = positionIcon;
   positionImg.onload = () => {
     // 重新计算坐标的位置点
     const iconW = iconWidth.value * scale.value; 

+ 204 - 0
src/utils/webSocket.ts

@@ -0,0 +1,204 @@
+/**
+ * WebSocket 管理器类
+ * 用于管理 WebSocket 连接、心跳机制、消息收发等功能
+ */
+export default class WebSocketManager {
+    private socket: WebSocket | null = null; // WebSocket 对象
+    private pingTimeout: number | null = null; // 心跳计时器
+    private reconnectTimeout: number | null = null; // 重连计时器
+    private reconnectTimeoutClock: number = 5000; // 重连间隔,单位:毫秒
+    private maxReconnectAttempts: number = 5; // 最大重连尝试次数
+    private reconnectAttempts: number = 0; // 当前重连尝试次数
+    private url: string; // WebSocket 连接地址
+    private receiveMessageCallback: ((data: any) => void) | null = null; // 接收消息回调函数
+
+    /**
+     * 构造函数
+     * @param url WebSocket 连接地址
+     * @param receiveMessageCallback 接收消息的回调函数
+     */
+    constructor(url: string | null = null, receiveMessageCallback: ((data: any) => void) | null = null) {
+        if (!url) {
+            console.error('WebSocketManager error: 请传入有效的连接地址');
+            throw new Error('WebSocket URL is required');
+        }
+        
+        this.url = url;
+        this.receiveMessageCallback = receiveMessageCallback;
+    }
+
+    /**
+     * 启动 WebSocket 连接
+     */
+    async start(): Promise<void> {
+        if (this.url) {
+            // 连接 WebSocket
+            this.connectWebSocket();
+        } else {
+            console.error('WebSocketManager error: 连接地址无效');
+        }
+    }
+
+    /**
+     * 获取 WebSocket 连接状态
+     * @returns WebSocket 的 readyState
+     */
+    getSocketStatus(): number {
+        return this.socket?.readyState ?? WebSocket.CLOSED;
+    }
+
+    /**
+     * 创建 WebSocket 连接
+     */
+    private connectWebSocket(): void {
+        try {
+            // 创建 WebSocket 对象
+            this.socket = new WebSocket(this.url);
+
+            // 处理连接打开事件
+            this.socket.addEventListener('open', () => {
+                console.log('WebSocket 连接成功');
+                // 启动心跳机制
+                this.startHeartbeat();
+                // 重置重连次数
+                this.reconnectAttempts = 0;
+            });
+
+            // 处理接收到消息事件
+            this.socket.addEventListener('message', (event: MessageEvent) => {
+                this.receiveMessage(event);
+            });
+
+            // 处理连接关闭事件
+            this.socket.addEventListener('close', (event: CloseEvent) => {
+                console.log(`WebSocket 连接关闭, code: ${event.code}, reason: ${event.reason}`);
+                // 清除定时器
+                this.clearTimers();
+                
+                // 尝试重连
+                if (this.reconnectAttempts < this.maxReconnectAttempts) {
+                    this.reconnectAttempts++;
+                    console.log(`尝试第 ${this.reconnectAttempts} 次重连...`);
+                    
+                    this.reconnectTimeout = window.setTimeout(() => {
+                        this.connectWebSocket();
+                    }, this.reconnectTimeoutClock);
+                } else {
+                    // 重置重连次数
+                    this.reconnectAttempts = 0;
+                    console.error('WebSocketManager error: 已达到最大重连尝试次数,无法重新连接');
+                }
+            });
+
+            // 处理 WebSocket 错误事件
+            this.socket.addEventListener('error', (event: Event) => {
+                console.error('WebSocketManager error: 连接发生错误', event);
+            });
+        } catch (error) {
+            console.error('WebSocketManager error: 创建连接失败', error);
+        }
+    }
+
+    /**
+     * 启动心跳机制
+     * 每隔 10 秒发送一次心跳消息,保持连接活跃
+     */
+    private startHeartbeat(): void {
+        // 先清除已存在的心跳定时器
+        this.clearPingTimeout();
+        
+        this.pingTimeout = window.setInterval(() => {
+            // 发送心跳消息
+            this.sendOrderMessage('heartCheck');
+        }, 10000); // 每隔 10 秒发送一次心跳
+    }
+
+    /**
+     * 发送消息
+     * @param message 消息内容
+     */
+    sendOrderMessage(message: string): void {
+        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
+            this.socket.send(message);
+        } else {
+            console.error('WebSocketManager error: WebSocket 连接未打开,无法发送消息');
+        }
+    }
+
+    /**
+     * 处理接收到的消息
+     * @param event MessageEvent 对象
+     */
+    private receiveMessage(event: MessageEvent): void {
+        const data = event.data;
+        
+        // 处理心跳消息
+        if (data === 'heartCheck') {
+            // 收到心跳响应,无需处理
+            return;
+        }
+        
+        // 调用回调函数处理业务消息
+        if (this.receiveMessageCallback && typeof this.receiveMessageCallback === 'function') {
+            try {
+                this.receiveMessageCallback(data);
+            } catch (error) {
+                console.error('WebSocketManager error: 消息回调处理失败', error);
+            }
+        }
+    }
+
+    /**
+     * 关闭 WebSocket 连接
+     */
+    closeWebSocket(): void {
+        if (this.socket) {
+            this.socket.close();
+            this.socket = null;
+        }
+        
+        // 清除所有定时器
+        this.clearTimers();
+        
+        // 重置重连次数
+        this.reconnectAttempts = 0;
+        
+        console.log('WebSocket 连接已手动关闭');
+    }
+
+    /**
+     * 清除心跳定时器
+     */
+    private clearPingTimeout(): void {
+        if (this.pingTimeout !== null) {
+            clearInterval(this.pingTimeout);
+            this.pingTimeout = null;
+        }
+    }
+
+    /**
+     * 清除重连定时器
+     */
+    private clearReconnectTimeout(): void {
+        if (this.reconnectTimeout !== null) {
+            clearTimeout(this.reconnectTimeout);
+            this.reconnectTimeout = null;
+        }
+    }
+
+    /**
+     * 清除所有定时器
+     */
+    private clearTimers(): void {
+        this.clearPingTimeout();
+        this.clearReconnectTimeout();
+    }
+
+    /**
+     * 销毁实例,清理资源
+     */
+    destroy(): void {
+        this.closeWebSocket();
+        this.receiveMessageCallback = null;
+    }
+}

+ 194 - 11
src/views/exam/abnormal/abnormal.vue

@@ -327,7 +327,7 @@ import { onMounted,onUnmounted, ref, computed, nextTick, watch } from 'vue';
 import { ElMessage,ElMessageBox } from 'element-plus'
 
 // 引入可能需要的 API (请根据实际路径调整)
-import { getExamAbnormalList,getSearchScanStudents,claimAnswerCard, correctObjectiveQuestion,markPositionAbnormal, deletePaper } from '@/api/exam'
+import { getExamAbnormalList,getSearchScanStudents,claimAnswerCard, correctObjectiveQuestion,markPositionAbnormal, deletePaper ,reRecognizePosition} from '@/api/exam'
 
 // 引入子组件 (确保路径正确)
 import PositionCanvas from '@/components/PositionCanvas.vue' // 假设路径
@@ -396,7 +396,7 @@ const currentTemplateUrl = ref('')//试卷图片模板地址
 const currentPaperUrl = ref('')//试卷图片地址
 const currentImagePageNo = ref('')//当前图片页码
 const currentPaperIndex = ref(0)//当前图片索引
-const currentTemplatePageIndex = ref(0)////当前模板页索引
+const currentTemplatePageIndex = ref(1)////当前模板页索引
 const currentDrawData= ref<any[]>([]);//画框数据
 const currentPaperList = ref<any[]>([]) // 底部缩略图列表
 const currentPageInfo = ref<any>({})    // 当前页面信息
@@ -437,10 +437,12 @@ const objectTableHeight = ref(200);//客观题异常处理表格高度
 const subjectiveTableHeight = ref(200)
 const tableKey = ref(0)
 
+const rotationAngle=ref(0);//旋转角度
+
 // 其他状态
 const rightTab = ref('pending') // pending | processed
 const reCutLoading = ref(false)
-const reCutText = ref('重新切割')
+const reCutText = ref('重新识别')
 const correctionLoading = ref(false)
 const subjectStudentIndex = ref(-1)
 const subjectStudentAbnormal = ref<any[]>([])
@@ -476,6 +478,7 @@ const AbnormalChanage = (type: number) => {
             currentIndex.value=0;
             currentItem.value=positionData.value.pendingList[currentIndex.value];//当前学生数据
             currentPaperList.value=currentItem.value.scanPictureVOS || [];//试卷图片列表
+            console.log('试卷图片列表',currentPaperList.value)
             currentPaperUrl.value=currentPaperList.value[0].recognizeUrl;//当前图片地址
         }
     }
@@ -579,6 +582,69 @@ const MarkBlankPage = () => {
 const SavePositionImage = () => {
     // TODO: 提交 sourcePoints 和 targetPoints 到后端
     console.log('保存定位')
+
+    if(currentPaperList.value.length==0)
+    {
+        ElMessage.error('没有定位异常哦!');
+        return
+    }
+    
+    console.log("打印当前的模板页",currentTemplateList.value);
+    console.log("打印当前的模板页码",currentTemplatePageIndex.value);
+    console.log("打印当前currentPaperList",currentPaperList.value);
+    console.log("打印当前图片的地址",currentPaperUrl.value);
+    console.log("打印当前currentPaperNumber",currentPaperIndex.value);
+    console.log("打印当前的系统卡类型",usedCardType.value);
+    
+    let hasError = false;
+    currentPaperList.value.forEach((item,index) => {
+        console.log("打印当前图片的索引",item);
+        if(index==currentPaperIndex.value)
+        {
+            
+            if(usedCardType.value==1)
+            {
+                //系统卡 需要把两个点转成4个点
+                item.sourcePoints=sourcePoints.value;//模板上的四个点的坐标
+                item.targetPoints=targetPoints.value;//坐标位置 左上 右上 右下 左下
+            }
+            else
+            {
+                item.sourcePoints=sourcePoints.value;//模板上的四个点的坐标
+                item.targetPoints=targetPoints.value;//学生图片上的四个点的坐标
+            }
+            
+            if(item.targetPoints.length==0)
+            {
+                //定位点未设置 不可保存
+                ElMessage.error('请选择定位点!');
+                hasError = true;
+                return; //跳出当前循环
+            }
+            else
+            {
+                
+                item.pageNo=currentTemplatePageIndex.value; 
+                item.rotate=rotationAngle.value;//旋转角度
+                currentImagePageNo.value=item.pageNo;
+                console.log("打印当前的图片页码",currentImagePageNo);
+                item.isSave=true;//增加是否处理保存图片页码的标识
+            }
+            
+        }
+    });
+    
+    if(hasError) {
+        return; //如果有错误,不执行后续的保存成功提示
+    }
+    
+    console.log("打印保存后currentPaperList",currentPaperList.value);
+    
+
+    
+    
+    console.log("打印保存后currentPaperList",currentPaperList.value);
+    ElMessage.success('保存成功');
 }
 
 // 12. 底部缩略图切换
@@ -664,14 +730,131 @@ const GetPositionSelNumber = (row: any) => {
     return row.batchNo || '-'
 }
 
-// 16. 重新切割 (定位异常)
-const ReCutPaper = () => {
-    reCutLoading.value = true
-    // TODO: 调用重新切割接口
-    setTimeout(() => {
-        reCutLoading.value = false
-        ElMessage.success('切割成功')
-    }, 1000)
+// 16. 重新识别 (定位异常)
+const ReCutPaper = async () => {
+    
+    
+    // if(currentPaperList.value.length==0)
+    // {
+        // ElMessage.error('没有定位异常哦!');
+        return
+    // }
+
+    // 先判断是否图片都做过校正对应处理
+    const unsavedItems = currentPaperList.value.filter(item => !item.isSave);
+    console.log("打印未保存的图片", unsavedItems);
+    if (unsavedItems.length > 0) {
+            // 提取未保存图片的索引信息(索引+1)
+        const indices = unsavedItems.map(item => {
+            const index = currentPaperList.value.indexOf(item);
+            return index + 1; // 索引从0开始,显示时加1
+        }).join('、');
+        ElMessage.error(`请给第${indices}张图片设置对应的模板页码!`);
+        return;
+    }
+
+    let pictures=[];
+    let templatePageList=[];
+    console.log("打印当前卡类型",usedCardType.value);
+    console.log("打印当前的卡类型",currentPaperList.value);
+    currentPaperList.value.forEach(item => {
+
+        let sourcePoints= [];
+        let targetPoints= [];
+        if(item.pageNo!=-1)
+        {
+            sourcePoints= item.sourcePoints.map(point => [point.x, point.y]);
+            targetPoints= item.targetPoints.map(point => [point.x, point.y]);
+            
+        }
+        else
+        {
+            sourcePoints= [[0,0],[0,0],[0,0],[0,0]];
+            targetPoints= [[0,0],[0,0],[0,0],[0,0]];
+        }
+        if(usedCardType.value==2)
+        {
+        
+            //三方卡 需要把四个点转成两个点
+            
+        
+            console.log("打印三方卡的定位坐标",sourcePoints,targetPoints);
+            if(item.pageNo==-1)
+            {
+                sourcePoints=[[0,0],[0,0]];
+                targetPoints=[[0,0],[0,0]];
+            }
+            else
+            {
+                console.log("打印当前三方卡的定位坐标",item.sourcePoints,item.targetPoints);
+                sourcePoints=[];
+                sourcePoints.push([item.sourcePoints[0].x,item.sourcePoints[0].y]);
+                sourcePoints.push([item.sourcePoints[2].x,item.sourcePoints[2].y]);
+                targetPoints=[];
+                targetPoints.push([item.targetPoints[0].x,item.targetPoints[0].y]);
+                targetPoints.push([item.targetPoints[2].x,item.targetPoints[2].y]);
+            }
+            
+
+        }
+        
+        
+        pictures.push({
+            picId:item.id,//图片id
+            pageNo:item.pageNo,//图标页码
+            rotate:item.rotate,//旋转角度
+            sourcePoints:sourcePoints,//模板上的四个点的坐标
+            targetPoints:targetPoints,//学生图片上的四个点的坐标
+        });
+        let obj={
+            pageNo:item.pageNo,
+            sourcePoints:item.sourcePoints,//模板上的四个点的坐标
+            targetPoints:item.targetPoints,//学生图片上的四个点的坐标
+        };
+        templatePageList.push(obj)
+    });
+    
+    // 注意:templatePageList 需要在组件中定义为响应式变量
+    // const templatePageList = ref<any[]>([])
+    // paperSchema 也需要定义
+    // const paperSchema = ref(1) // 1-单面 2-双面
+    
+    let param={
+        examSubjectId:examSubjectId.value,
+        schema:paperSchema.value,//双面扫描 1-单面 2-双面
+        pictures:pictures,//图片列表集合 图片id picId  图片页码 pageNo
+    }; 
+    
+    reCutLoading.value=true;//重新识别loading
+    reCutText.value='识别中,请稍候...';
+    
+    try {
+        // 注意:需要在 API 文件中添加 reRecognizePosition 方法
+        // import { reRecognizePosition } from '@/api/exam'
+        const res = await reRecognizePosition(param);
+        
+        reCutLoading.value=false;//重新识别loading
+        reCutText.value='重新识别';
+        
+        if(res.code==200)
+        {
+            
+            console.log("重新识别结果",res);
+            ElMessage.success('重新识别成功!');
+            currentPaperUrl.value='';
+            GetAbnormalList();
+        }
+        else
+        {
+            ElMessage.error(res.msg);
+        
+        }
+    } catch (error) {
+        reCutLoading.value=false;
+        reCutText.value='重新识别';
+        ElMessage.error('重新识别失败');
+        console.error('重新识别错误:', error);
+    }
 }
 
 // 17. 删除当前试卷

+ 1 - 1
src/views/exam/components/stepItem.vue

@@ -65,7 +65,7 @@ const menuList = computed(() => {
     list = [
       { name: '试题结构', number: '1', path: 'question', desc: '建立考试试题结构、设置标答、分组等信息', status: 0 },
       { name: '扫描学生', number: '2', path: 'scanList', desc: '扫描学生,处理扫描异常学生等', status: 0 },
-      { name: '成绩查询', number: '3', path: 'analysis', desc: '设置每个主观题下的划分区信息及位置', status: 0 ,
+      { name: '成绩查询', number: '3', path: 'analysis/score', desc: '设置每个主观题下的划分区信息及位置', status: 0 ,
         menu:[
           { name: '成绩单',number: '4', path: 'score' },
           { name: '错题分析',number: '5', path: 'errorAnalysis' },

+ 49 - 4
src/views/exam/scanList.vue

@@ -51,7 +51,11 @@
                     </el-table-column>
                     <el-table-column prop="uploadNum" label="待上传张数" align="center" >
                         <template v-slot="scope">
-                            
+                            <div class="table_row_answer"  v-if="GetNoUploadNumber(scope.row)>0" @click="OpenUpload(scope.row)">
+                                <span style="color:red;text-decoration:underline;"> {{GetNoUploadNumber(scope.row)}}</span>
+                                    <!-- {{scope.row.failedNumber || 0}} -->
+                            </div>
+                            <div v-else>0</div>
                         </template>
                     </el-table-column>
                     <el-table-column prop="uploadNum" label="已上传张数" align="center" >
@@ -168,7 +172,7 @@ import scanCommon from '@/utils/scanCommon';
 import { ElMessageBox, ElMessage } from 'element-plus'
 import { formatTimestamp } from '@/utils/common';
 import AbnormalDetail from './abnormalDetail.vue';
-
+import WebSocketManager from '@/utils/webSocket'//后端websocket链接js
 // 实例化 Store
 const examStore = useExamStore()
 const userStore = useUserStore()
@@ -209,15 +213,17 @@ const baseUrl=import.meta.env.VITE_API_BASE_URL;
 const uploadUrl=`https://dev3.k12100.net/teaching/api/v1/ai_exam_scan/upload_multi_img`;//图片上传地址
 
 
+const webSocket=ref<InstanceType<typeof WebSocketManager> | null>(null);//websocket
+const websocketUrl=import.meta.env.VITE_WEB_SOCKET_URL;//websocket链接地址
 //  定义数据类型接口
 interface BatchItem {
   id?: string | number;
   batchNo: string;
   batchTypeName?: string;
   scanUserName?: string;
-  scannedPaperNum?: number;
+  scannedPaperNum?: number;//扫描张数
   scannedTime?: number | string;
-  uploadNum?: number;
+  uploadNum?: number;//上传张数
   uploadStatus?: number; // 确保包含此属性
   failedNumber?: number;
   [key: string]: any; // 允许其他动态属性
@@ -267,6 +273,45 @@ const Refresh = () => {
     GetScanBatchList();
 }
 
+//获取未上传图片的数量
+const GetNoUploadNumber=(item:BatchItem)=>
+{
+    const scannedPaperNum = item.scannedPaperNum ?? 0;
+    const uploadNum = item.uploadNum ?? 0;
+    
+    console.log("打印item", item);
+    
+    const result = scannedPaperNum - uploadNum;
+    
+    return result > 0 ? result : 0;
+}
+
+// 跟后端websocket连接  1.建立socket连接 用户ID_考试科目ID_学校ID_批次号
+const WebSocketLink = () => {
+    if(webSocket.value && webSocket.value.getSocketStatus() === WebSocket.OPEN) 
+    {
+        webSocket.value.closeWebSocket();
+    }
+    let userCourseId='';
+    let schoolIdValue = userInfo.value.schoolId; // 这里逻辑是 单校都取登录用户的学校ID 如果是联校账号 给单校扫描 传单校的学校id
+
+    if(schoolType.value == 1) {
+        //联校 盟主学校
+        schoolIdValue = selectSchoolId.value;
+    } else {
+        schoolIdValue = userInfo.value.schoolId;
+    }
+    
+    console.log("打印用户信息", userInfo.value);
+    userCourseId = `${userInfo.value.id}_${examSubjectId.value}_${schoolIdValue}_${currentBatchNo.value}`; //用户 科目 批次号
+    
+    const url = `${websocketUrl.value}${userCourseId}`;
+
+    console.log("打印websocket链接地址url", url);
+    webSocket.value = new WebSocketManager(url, MessageBack);
+    webSocket.value.start();
+}
+
 //重新识别弹窗
 const OpenReIdentify=() => {
 

+ 17 - 5
src/views/layout/components/header.vue

@@ -9,7 +9,7 @@
       </div>
       <div class="header_menu">
         <el-menu :default-active="activeMenuName" class="el-menu-wrap" mode="horizontal" :router="true"
-          @select="menuSelect" background-color="#2E64FA" text-color="#fff" active-text-color="#fff">
+          background-color="#2E64FA" text-color="#fff" active-text-color="#fff">
           <el-menu-item :index="item.externalLink" v-for="(item, index) in menuList" :key="index">
             {{ item.menuName }}
           </el-menu-item>
@@ -62,6 +62,7 @@
 
 import { ArrowDown } from '@element-plus/icons-vue'
 import { ref, reactive, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
 import { loginOut, changePassWord } from '@/api/login'
 import { ElMessage } from 'element-plus'
 import type { FormInstance, FormRules } from 'element-plus'
@@ -71,6 +72,10 @@ import { useUserStore } from '@/store/user'
 // 2. 初始化 store
 const userStore = useUserStore()
 
+// 3. 获取路由实例
+const route = useRoute()
+const router = useRouter()
+
 // 修改 schoolLogo 计算属性,直接从 store 获取
 const schoolLogo = computed(() => userStore.schoolLogo)
 
@@ -90,10 +95,17 @@ const menuList=ref([
     externalLink: '/main/essay'
   },
 ])
-const activeMenuName=ref('');
-const menuSelect=(menuName: string) => {
-  activeMenuName.value = menuName
-}
+// 使用计算属性根据当前路由自动设置激活菜单
+const activeMenuName = computed(() => {
+  const currentPath = route.path
+  // 查找匹配的菜单项
+  const matchedMenu = menuList.value.find(item => item.externalLink === currentPath)
+  return matchedMenu ? matchedMenu.externalLink : ''
+})
+
+// const menuSelect=(menuName: string) => {
+//   activeMenuName.value = menuName
+// }
 const userInfo = computed(() => {
   const user = localStorage.getItem('userInfo')
   if (user) {