|
|
@@ -0,0 +1,449 @@
|
|
|
+<template>
|
|
|
+ <div class="page_dialog">
|
|
|
+ <el-dialog
|
|
|
+ title="重新识别"
|
|
|
+ class="page_dialog"
|
|
|
+ :model-value="showDialog"
|
|
|
+ width="728px"
|
|
|
+ @close="handleClose"
|
|
|
+ append-to-body
|
|
|
+ >
|
|
|
+ <div class="dialog_center padding_20">
|
|
|
+ <div class="re_identify">
|
|
|
+ <div class="identify_message">
|
|
|
+ 已扫描{{ reIdentifyTotal }}张,点击"开始识别"进行重新识别。
|
|
|
+ <span style="color:#FF0000;">识别中过程不能关闭弹窗或离开页面。</span>
|
|
|
+ </div>
|
|
|
+ <div class="identify_center">
|
|
|
+ <div class="center_number_1">
|
|
|
+ <div class="center_title">
|
|
|
+ 需识别张数<br>
|
|
|
+ <span class="span_number">{{ reIdentifyNumber }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="center_number_1">
|
|
|
+ <div class="center_title">
|
|
|
+ 已识别张数<br>
|
|
|
+ <span class="span_number">{{ recognizedNumber }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="identify_progress">
|
|
|
+ <span>识别进度:</span>
|
|
|
+ <el-progress
|
|
|
+ :percentage="reIdentifyProgress"
|
|
|
+ :stroke-width="16"
|
|
|
+ :color="customColor"
|
|
|
+ :show-text="true"
|
|
|
+ ></el-progress>
|
|
|
+ </div>
|
|
|
+ <div class="identify_button">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ @click="StartReIdentify"
|
|
|
+ :loading="reIdentifyLoading"
|
|
|
+ >
|
|
|
+ {{ reIdentifyText }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
|
|
|
+// 1. 引入 Pinia store
|
|
|
+import { useUserStore } from '@/store/user'
|
|
|
+import { useExamStore } from '@/store/exam'
|
|
|
+
|
|
|
+import WebSocketManager from '@/utils/webSocket.ts';
|
|
|
+import { recognizeAgain } from '@/api/exam';
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
+
|
|
|
+// ==================== Props 定义 ====================
|
|
|
+interface SchoolInfo {
|
|
|
+ schoolId: string; // 学校ID
|
|
|
+ schoolName: string; // 学校名称
|
|
|
+}
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ /**
|
|
|
+ * 控制弹窗显示/隐藏(v-model)
|
|
|
+ */
|
|
|
+ modelValue: {
|
|
|
+ type: Boolean,
|
|
|
+ required: true,
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 需要重新识别的总张数
|
|
|
+ */
|
|
|
+ reIdentifyTotal: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: '0'
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 是否只识别客观题
|
|
|
+ * 0 - 否(全部识别)
|
|
|
+ * 1 - 是(仅客观题)
|
|
|
+ */
|
|
|
+ justObjective: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: 1
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 学校信息对象
|
|
|
+ */
|
|
|
+ schoolInfo: {
|
|
|
+ type: Object as () => SchoolInfo,
|
|
|
+ default: () => ({
|
|
|
+ schoolId: '',
|
|
|
+ schoolName: ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+// ==================== Emits 定义 ====================
|
|
|
+const emit = defineEmits([
|
|
|
+ 'update:modelValue', // v-model 更新事件
|
|
|
+ 'ReIdentifyComplete' // 重新识别完成事件
|
|
|
+]);
|
|
|
+
|
|
|
+// ==================== Store 实例化 ====================
|
|
|
+const userStore = useUserStore()
|
|
|
+const examStore = useExamStore()
|
|
|
+// ==================== 响应式数据 ====================
|
|
|
+
|
|
|
+// --- 上传相关(当前组件未使用,已注释)---
|
|
|
+// const loading = ref(false); // 上传加载状态
|
|
|
+// const selectedFiles = ref<any[]>([]); // 选择的文件列表
|
|
|
+// const previewImageList = ref<any[]>([]); // 预览弹窗的图片列表
|
|
|
+// const batchNumber = ref(''); // 批次编号
|
|
|
+// const batchNOID = ref(''); // 批次号ID
|
|
|
+// const sensitive = ref('165'); // 灵敏度参数(0-255之间的奇数)
|
|
|
+// const onnx = ref(0); // 算法类型:0-正常算法,1-废资源算法
|
|
|
+// const isUploading = ref(false); // 是否正在上传(防止重复点击)
|
|
|
+// const debounceTimer = ref<any>(null); // 防抖定时器
|
|
|
+// const scanLoading = ref(false); // 扫描按钮loading状态
|
|
|
+
|
|
|
+// --- 学校信息 ---
|
|
|
+const schoolId = ref(''); // 当前学校ID
|
|
|
+const schoolName = ref(''); // 当前学校名称(带冒号后缀)
|
|
|
+
|
|
|
+// --- 重新识别相关 ---
|
|
|
+const reIdentifyUserId = ref(''); // 重新识别的用户ID(WebSocket连接标识)
|
|
|
+const isReIdentify = ref(false); // 是否处于重新识别状态
|
|
|
+const reIdentifyNumber = ref(0); // 需要识别的张数(剩余)
|
|
|
+const recognizedNumber = ref(0); // 已识别的张数
|
|
|
+const reIdentifyProgress = ref(0); // 重新识别进度百分比(0-100)
|
|
|
+const reIdentifyText = ref('开始识别'); // 重新识别按钮文案
|
|
|
+const reIdentifyLoading = ref(false); // 重新识别按钮loading状态
|
|
|
+
|
|
|
+// --- WebSocket 相关 ---
|
|
|
+const websocketUrl = ref(import.meta.env.VUE_APP_WEB_SOCKET_URL || ''); // WebSocket链接地址
|
|
|
+const socket = ref<any>(null); // WebSocket实例对象
|
|
|
+
|
|
|
+// --- 进度条配置 ---
|
|
|
+/**
|
|
|
+ * 自定义进度条颜色配置
|
|
|
+ * 不同进度阶段显示不同颜色(当前统一为蓝色 #2E64FA)
|
|
|
+ */
|
|
|
+const customColor = [
|
|
|
+ { color: '#2E64FA', percentage: 20 },
|
|
|
+ { color: '#2E64FA', percentage: 40 },
|
|
|
+ { color: '#2E64FA', percentage: 60 },
|
|
|
+ { color: '#2E64FA', percentage: 80 },
|
|
|
+ { color: '#2E64FA', percentage: 100 }
|
|
|
+];
|
|
|
+
|
|
|
+// ==================== 计算属性 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 弹窗显示状态(双向绑定)
|
|
|
+ */
|
|
|
+const showDialog = computed({
|
|
|
+ get: () => props.modelValue,
|
|
|
+ set: (val) => emit('update:modelValue', val)
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * 当前考试科目ID
|
|
|
+ * 从 Vuex Store 中获取
|
|
|
+ */
|
|
|
+const examSubjectId = computed(() => {
|
|
|
+ return examStore.currentExam?.id
|
|
|
+})//计算属性
|
|
|
+
|
|
|
+/**
|
|
|
+ * 剩余未上传完成的文件数量
|
|
|
+ * 当前组件未使用,已注释
|
|
|
+ */
|
|
|
+// const remainCount = computed(() => {
|
|
|
+// return selectedFiles.value.filter(file => file.uploadStatus !== 0).length;
|
|
|
+// });
|
|
|
+
|
|
|
+/**
|
|
|
+ * 账号学校类型
|
|
|
+ * 0 - 单校版
|
|
|
+ * 1 - 联校版
|
|
|
+ * 当前组件未使用,已注释
|
|
|
+ */
|
|
|
+// const schoolType = computed(() => {
|
|
|
+// return store.state.user?.schoolType;
|
|
|
+// });
|
|
|
+
|
|
|
+/**
|
|
|
+ * 考试学校类型
|
|
|
+ * 0 - 单校版
|
|
|
+ * 1 - 联校版
|
|
|
+ * 当前组件未使用,已注释
|
|
|
+ */
|
|
|
+// const examSchoolType = computed(() => {
|
|
|
+// return store.state.makeTemplate?.examDetail?.schoolType;
|
|
|
+// });
|
|
|
+
|
|
|
+// ==================== 监听器 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 监听 reIdentifyTotal 变化
|
|
|
+ * 当父组件传入的总张数变化时,同步更新本地变量
|
|
|
+ */
|
|
|
+watch(
|
|
|
+ () => props.reIdentifyTotal,
|
|
|
+ (newVal) => {
|
|
|
+ reIdentifyNumber.value = Number(newVal);
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
+/**
|
|
|
+ * 监听 schoolInfo 变化
|
|
|
+ * 当学校信息变化时,更新本地的 schoolId 和 schoolName
|
|
|
+ */
|
|
|
+watch(
|
|
|
+ () => props.schoolInfo,
|
|
|
+ (newVal) => {
|
|
|
+ console.log("监听到学校信息的改变了", newVal);
|
|
|
+ if (newVal) {
|
|
|
+ schoolId.value = newVal.schoolId || '';
|
|
|
+ schoolName.value = newVal.schoolName ? newVal.schoolName + ':' : '';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { deep: true, immediate: true }
|
|
|
+);
|
|
|
+
|
|
|
+// ==================== 生命周期 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 组件挂载时执行
|
|
|
+ * 初始化重新识别张数
|
|
|
+ */
|
|
|
+onMounted(() => {
|
|
|
+ console.log("打印学校id", props.schoolInfo);
|
|
|
+ reIdentifyNumber.value = Number(props.reIdentifyTotal);
|
|
|
+});
|
|
|
+
|
|
|
+/**
|
|
|
+ * 组件卸载前执行
|
|
|
+ * 清理资源:关闭WebSocket连接、清除定时器
|
|
|
+ */
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ // 清理 WebSocket 连接
|
|
|
+ if (socket.value && socket.value.getSocketStatus() === WebSocket.OPEN) {
|
|
|
+ socket.value.closeWebSocket();
|
|
|
+ }
|
|
|
+ // 清理定时器(当前未使用,保留以防后续扩展)
|
|
|
+ // if (debounceTimer.value) {
|
|
|
+ // clearTimeout(debounceTimer.value);
|
|
|
+ // }
|
|
|
+});
|
|
|
+
|
|
|
+// ==================== 方法定义 ====================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 建立重新识别的 WebSocket 连接
|
|
|
+ * @description 用于接收后端实时推送的识别进度消息
|
|
|
+ */
|
|
|
+const ReIdentifyWebSocketLink = () => {
|
|
|
+ // 如果已有连接且处于打开状态,先关闭
|
|
|
+ if (socket.value && socket.value.getSocketStatus() === WebSocket.OPEN) {
|
|
|
+ socket.value.closeWebSocket();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 构建 WebSocket 用户标识:AGAIN_ + 科目ID
|
|
|
+ let userCourseId = '';
|
|
|
+ console.log("打印用户信息", store.state.user.userInfo);
|
|
|
+ userCourseId = "AGAIN_" + examSubjectId.value;
|
|
|
+ reIdentifyUserId.value = userCourseId;
|
|
|
+ console.log("打印重新识别的userCourseId", userCourseId);
|
|
|
+
|
|
|
+ // 根据环境设置 WebSocket URL
|
|
|
+ if (process.env.NODE_ENV === 'development') {
|
|
|
+ // 开发环境使用固定地址
|
|
|
+ websocketUrl.value = 'wss://dev3.k12100.net/teaching/api/v1/teaching/websocket/';
|
|
|
+ } else if (process.env.NODE_ENV === 'production') {
|
|
|
+ // 生产环境使用环境变量
|
|
|
+ websocketUrl.value = process.env.VUE_APP_WEB_SOCKET_URL || '';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 拼接完整的 WebSocket URL
|
|
|
+ const url = websocketUrl.value + userCourseId;
|
|
|
+ console.log("打印websocket链接地址url", url);
|
|
|
+
|
|
|
+ // 创建并启动 WebSocket 连接
|
|
|
+ socket.value = new WebSocketManager(url, MessageBack);
|
|
|
+ socket.value.start();
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 开始重新识别
|
|
|
+ * @description 调用后端接口触发重新识别,并建立 WebSocket 连接接收进度
|
|
|
+ */
|
|
|
+const StartReIdentify = () => {
|
|
|
+ // 如果已经识别完成,点击按钮则关闭弹窗并重置状态
|
|
|
+ if (reIdentifyText.value === '识别完成') {
|
|
|
+ showDialog.value = false;
|
|
|
+ reIdentifyLoading.value = false;
|
|
|
+ reIdentifyProgress.value = 0;
|
|
|
+ reIdentifyText.value = '开始识别';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置进度状态
|
|
|
+ reIdentifyProgress.value = 0;
|
|
|
+ reIdentifyLoading.value = true;
|
|
|
+ reIdentifyText.value = '识别中';
|
|
|
+
|
|
|
+ // 建立 WebSocket 连接
|
|
|
+ ReIdentifyWebSocketLink();
|
|
|
+
|
|
|
+ // 构建请求参数
|
|
|
+ const param = {
|
|
|
+ examSubjectId: examSubjectId.value, // 考试科目ID
|
|
|
+ recognizeNumber: props.reIdentifyTotal, // 需要识别的总张数
|
|
|
+ justObjective: props.justObjective, // 是否只识别客观题
|
|
|
+ schoolId: props.schoolInfo.schoolId // 学校ID
|
|
|
+ };
|
|
|
+
|
|
|
+ // 调用重新识别接口
|
|
|
+ recognizeAgain(param).then(res => {
|
|
|
+ console.log("重新识别结果", res);
|
|
|
+ if (res.code !== 200) {
|
|
|
+ ElMessage.error("重新识别失败!" + res.msg);
|
|
|
+ }
|
|
|
+ }).catch(err => {
|
|
|
+ console.error("重新识别请求失败", err);
|
|
|
+ ElMessage.error("网络请求失败");
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * WebSocket 消息回调处理
|
|
|
+ * @param data - WebSocket 返回的消息字符串(JSON格式)
|
|
|
+ * @description 接收后端推送的识别进度,更新UI显示
|
|
|
+ */
|
|
|
+const MessageBack = (data: string) => {
|
|
|
+ const msg = JSON.parse(data);
|
|
|
+ console.log("Websocket消息返回处理", msg);
|
|
|
+
|
|
|
+ // code: 0-建立成功, 200-业务成功, 400-业务失败/异常
|
|
|
+ if (msg.code === 200) {
|
|
|
+ // 已识别张数 +1
|
|
|
+ recognizedNumber.value++;
|
|
|
+ // 待识别张数 -1
|
|
|
+ reIdentifyNumber.value--;
|
|
|
+
|
|
|
+ // 计算进度百分比
|
|
|
+ const process = Math.round((Number(recognizedNumber.value) / Number(props.reIdentifyTotal)) * 100);
|
|
|
+ reIdentifyProgress.value = Math.min(process, 100); // 确保不超过100%
|
|
|
+
|
|
|
+ // 如果进度达到100%,表示识别完成
|
|
|
+ if (reIdentifyProgress.value === 100) {
|
|
|
+ ElMessage.success("重新识别完成!");
|
|
|
+
|
|
|
+ // 重置所有状态
|
|
|
+ reIdentifyNumber.value = 0;
|
|
|
+ recognizedNumber.value = 0;
|
|
|
+ reIdentifyProgress.value = 0;
|
|
|
+ reIdentifyText.value = '识别完成';
|
|
|
+ reIdentifyLoading.value = false;
|
|
|
+
|
|
|
+ // 关闭弹窗
|
|
|
+ showDialog.value = false;
|
|
|
+
|
|
|
+ // 通知父组件识别完成
|
|
|
+ emit('ReIdentifyComplete');
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 格式化文件名
|
|
|
+ * @param fileName - 原始文件名
|
|
|
+ * @returns 格式化后的文件名(超过17位时显示前5位+...+后12位)
|
|
|
+ * @description 当前组件未使用,保留以备后续扩展
|
|
|
+ */
|
|
|
+// const FormatFileName = (fileName: string): string => {
|
|
|
+// if (fileName.length <= 17) {
|
|
|
+// return fileName;
|
|
|
+// }
|
|
|
+// const prefix = fileName.substring(0, 5);
|
|
|
+// const suffix = fileName.substring(fileName.length - 12);
|
|
|
+// return prefix + '...' + suffix;
|
|
|
+// };
|
|
|
+
|
|
|
+/**
|
|
|
+ * 检查文件是否为图片
|
|
|
+ * @param fileName - 文件名
|
|
|
+ * @returns 是否为图片文件
|
|
|
+ * @description 当前组件未使用,保留以备后续扩展
|
|
|
+ */
|
|
|
+// const isImageFile = (fileName: string): boolean => {
|
|
|
+// const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
|
|
|
+// const lowerFileName = fileName.toLowerCase();
|
|
|
+// return imageExtensions.some(ext => lowerFileName.endsWith(ext));
|
|
|
+// };
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取文件预览URL
|
|
|
+ * @param file - 文件对象
|
|
|
+ * @returns 预览URL
|
|
|
+ * @description 当前组件未使用,保留以备后续扩展
|
|
|
+ */
|
|
|
+// const getFilePreviewUrl = (file: File): string => {
|
|
|
+// if (window.URL && file) {
|
|
|
+// return window.URL.createObjectURL(file);
|
|
|
+// }
|
|
|
+// return '';
|
|
|
+// };
|
|
|
+
|
|
|
+/**
|
|
|
+ * 关闭弹窗
|
|
|
+ * @description 重置状态并通知父组件
|
|
|
+ */
|
|
|
+const handleClose = () => {
|
|
|
+ showDialog.value = false;
|
|
|
+ // selectedFiles.value = []; // 当前未使用
|
|
|
+ emit('close');
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 取消上传
|
|
|
+ * @description 当前组件未使用,保留以备后续扩展
|
|
|
+ */
|
|
|
+// const CancelUpload = () => {
|
|
|
+// selectedFiles.value = [];
|
|
|
+// emit('CancelUpload', batchNumber.value);
|
|
|
+// showDialog.value = false;
|
|
|
+// };
|
|
|
+
|
|
|
+// ==================== 暴露方法 ====================
|
|
|
+defineExpose({
|
|
|
+ handleClose
|
|
|
+ // CancelUpload // 当前未使用
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+// 样式保持不变
|
|
|
+</style>
|