ReIdentify.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <template>
  2. <div class="page_dialog">
  3. <el-dialog
  4. title="重新识别"
  5. class="page_dialog"
  6. :model-value="showDialog"
  7. width="728px"
  8. @close="handleClose"
  9. append-to-body
  10. >
  11. <div class="dialog_center padding_20">
  12. <div class="re_identify">
  13. <div class="identify_message">
  14. 已扫描{{ reIdentifyTotal }}张,点击"开始识别"进行重新识别。
  15. <span style="color:#FF0000;">识别中过程不能关闭弹窗或离开页面。</span>
  16. </div>
  17. <div class="identify_center">
  18. <div class="center_number_1">
  19. <div class="center_title">
  20. 需识别张数<br>
  21. <span class="span_number">{{ reIdentifyNumber }}</span>
  22. </div>
  23. </div>
  24. <div class="center_number_1">
  25. <div class="center_title">
  26. 已识别张数<br>
  27. <span class="span_number">{{ recognizedNumber }}</span>
  28. </div>
  29. </div>
  30. </div>
  31. <div class="identify_progress">
  32. <span>识别进度:</span>
  33. <el-progress
  34. :percentage="reIdentifyProgress"
  35. :stroke-width="16"
  36. :color="customColor"
  37. :show-text="true"
  38. ></el-progress>
  39. </div>
  40. <div class="identify_button">
  41. <el-button
  42. type="primary"
  43. @click="StartReIdentify"
  44. :loading="reIdentifyLoading"
  45. >
  46. {{ reIdentifyText }}
  47. </el-button>
  48. </div>
  49. </div>
  50. </div>
  51. </el-dialog>
  52. </div>
  53. </template>
  54. <script lang="ts" setup>
  55. import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue';
  56. // 1. 引入 Pinia store
  57. import { useUserStore } from '@/store/user'
  58. import { useExamStore } from '@/store/exam'
  59. import WebSocketManager from '@/utils/webSocket.ts';
  60. import { recognizeAgain } from '@/api/exam';
  61. import { ElMessage } from 'element-plus';
  62. // ==================== Props 定义 ====================
  63. interface SchoolInfo {
  64. schoolId: string; // 学校ID
  65. schoolName: string; // 学校名称
  66. }
  67. const props = defineProps({
  68. /**
  69. * 控制弹窗显示/隐藏(v-model)
  70. */
  71. modelValue: {
  72. type: Boolean,
  73. required: true,
  74. },
  75. /**
  76. * 需要重新识别的总张数
  77. */
  78. reIdentifyTotal: {
  79. type: [String, Number],
  80. default: '0'
  81. },
  82. /**
  83. * 是否只识别客观题
  84. * 0 - 否(全部识别)
  85. * 1 - 是(仅客观题)
  86. */
  87. justObjective: {
  88. type: [String, Number],
  89. default: 1
  90. },
  91. /**
  92. * 学校信息对象
  93. */
  94. schoolInfo: {
  95. type: Object as () => SchoolInfo,
  96. default: () => ({
  97. schoolId: '',
  98. schoolName: ''
  99. })
  100. }
  101. });
  102. // ==================== Emits 定义 ====================
  103. const emit = defineEmits([
  104. 'update:modelValue', // v-model 更新事件
  105. 'ReIdentifyComplete' // 重新识别完成事件
  106. ]);
  107. // ==================== Store 实例化 ====================
  108. const userStore = useUserStore()
  109. const examStore = useExamStore()
  110. // ==================== 响应式数据 ====================
  111. // --- 上传相关(当前组件未使用,已注释)---
  112. // const loading = ref(false); // 上传加载状态
  113. // const selectedFiles = ref<any[]>([]); // 选择的文件列表
  114. // const previewImageList = ref<any[]>([]); // 预览弹窗的图片列表
  115. // const batchNumber = ref(''); // 批次编号
  116. // const batchNOID = ref(''); // 批次号ID
  117. // const sensitive = ref('165'); // 灵敏度参数(0-255之间的奇数)
  118. // const onnx = ref(0); // 算法类型:0-正常算法,1-废资源算法
  119. // const isUploading = ref(false); // 是否正在上传(防止重复点击)
  120. // const debounceTimer = ref<any>(null); // 防抖定时器
  121. // const scanLoading = ref(false); // 扫描按钮loading状态
  122. // --- 学校信息 ---
  123. // const schoolId = ref(''); // 当前学校ID
  124. const schoolName = ref(''); // 当前学校名称(带冒号后缀)
  125. // --- 重新识别相关 ---
  126. const reIdentifyUserId = ref(''); // 重新识别的用户ID(WebSocket连接标识)
  127. const isReIdentify = ref(false); // 是否处于重新识别状态
  128. const reIdentifyNumber = ref(0); // 需要识别的张数(剩余)
  129. const recognizedNumber = ref(0); // 已识别的张数
  130. const reIdentifyProgress = ref(0); // 重新识别进度百分比(0-100)
  131. const reIdentifyText = ref('开始识别'); // 重新识别按钮文案
  132. const reIdentifyLoading = ref(false); // 重新识别按钮loading状态
  133. // --- WebSocket 相关 ---
  134. const websocketUrl = ref(import.meta.env.VUE_APP_WEB_SOCKET_URL || ''); // WebSocket链接地址
  135. const socket = ref<any>(null); // WebSocket实例对象
  136. // --- 进度条配置 ---
  137. /**
  138. * 自定义进度条颜色配置
  139. * 不同进度阶段显示不同颜色(当前统一为蓝色 #2E64FA)
  140. */
  141. const customColor = [
  142. { color: '#2E64FA', percentage: 20 },
  143. { color: '#2E64FA', percentage: 40 },
  144. { color: '#2E64FA', percentage: 60 },
  145. { color: '#2E64FA', percentage: 80 },
  146. { color: '#2E64FA', percentage: 100 }
  147. ];
  148. // ==================== 计算属性 ====================
  149. /**
  150. * 弹窗显示状态(双向绑定)
  151. */
  152. const showDialog = computed({
  153. get: () => props.modelValue,
  154. set: (val) => emit('update:modelValue', val)
  155. });
  156. /**
  157. * 当前考试科目ID
  158. * 从 Vuex Store 中获取
  159. */
  160. const examSubjectId = computed(() => {
  161. return examStore.currentExam?.id
  162. })//计算属性
  163. const schoolId = computed(() => {
  164. return userStore.userInfo.schoolId;
  165. });
  166. /**
  167. * 剩余未上传完成的文件数量
  168. * 当前组件未使用,已注释
  169. */
  170. // const remainCount = computed(() => {
  171. // return selectedFiles.value.filter(file => file.uploadStatus !== 0).length;
  172. // });
  173. /**
  174. * 账号学校类型
  175. * 0 - 单校版
  176. * 1 - 联校版
  177. * 当前组件未使用,已注释
  178. */
  179. // const schoolType = computed(() => {
  180. // return store.state.user?.schoolType;
  181. // });
  182. /**
  183. * 考试学校类型
  184. * 0 - 单校版
  185. * 1 - 联校版
  186. * 当前组件未使用,已注释
  187. */
  188. // const examSchoolType = computed(() => {
  189. // return store.state.makeTemplate?.examDetail?.schoolType;
  190. // });
  191. // ==================== 监听器 ====================
  192. /**
  193. * 监听 reIdentifyTotal 变化
  194. * 当父组件传入的总张数变化时,同步更新本地变量
  195. */
  196. watch(
  197. () => props.reIdentifyTotal,
  198. (newVal) => {
  199. reIdentifyNumber.value = Number(newVal);
  200. }
  201. );
  202. /**
  203. * 监听 schoolInfo 变化
  204. * 当学校信息变化时,更新本地的 schoolId 和 schoolName
  205. */
  206. watch(
  207. () => props.schoolInfo,
  208. (newVal) => {
  209. console.log("监听到学校信息的改变了", newVal);
  210. if (newVal) {
  211. // schoolId.value = newVal.schoolId || '';
  212. schoolName.value = newVal.schoolName ? newVal.schoolName + ':' : '';
  213. }
  214. },
  215. { deep: true, immediate: true }
  216. );
  217. // ==================== 生命周期 ====================
  218. /**
  219. * 组件挂载时执行
  220. * 初始化重新识别张数
  221. */
  222. onMounted(() => {
  223. console.log("打印学校id", props.schoolInfo);
  224. reIdentifyNumber.value = Number(props.reIdentifyTotal);
  225. });
  226. /**
  227. * 组件卸载前执行
  228. * 清理资源:关闭WebSocket连接、清除定时器
  229. */
  230. onBeforeUnmount(() => {
  231. // 清理 WebSocket 连接
  232. if (socket.value && socket.value.getSocketStatus() === WebSocket.OPEN) {
  233. socket.value.closeWebSocket();
  234. }
  235. // 清理定时器(当前未使用,保留以防后续扩展)
  236. // if (debounceTimer.value) {
  237. // clearTimeout(debounceTimer.value);
  238. // }
  239. });
  240. // ==================== 方法定义 ====================
  241. /**
  242. * 建立重新识别的 WebSocket 连接
  243. * @description 用于接收后端实时推送的识别进度消息
  244. */
  245. const ReIdentifyWebSocketLink = () => {
  246. // 如果已有连接且处于打开状态,先关闭
  247. if (socket.value && socket.value.getSocketStatus() === WebSocket.OPEN) {
  248. socket.value.closeWebSocket();
  249. }
  250. // 构建 WebSocket 用户标识:AGAIN_ + 科目ID
  251. let userCourseId = '';
  252. console.log("打印用户信息", userStore.userInfo);
  253. userCourseId = "AGAIN_" + examSubjectId.value;
  254. reIdentifyUserId.value = userCourseId;
  255. console.log("打印重新识别的userCourseId", userCourseId);
  256. console.log("import.meta.env.NODE_ENV", import.meta.env.NODE_ENV);
  257. // 根据环境设置 WebSocket URL
  258. if (import.meta.env.MODE === 'development') {
  259. // 开发环境使用固定地址
  260. websocketUrl.value = 'wss://dev3.k12100.net/teaching/api/v1/teaching/websocket/';
  261. } else if (import.meta.env.MODE === 'production') {
  262. // 生产环境使用环境变量
  263. websocketUrl.value = import.meta.env.VUE_APP_WEB_SOCKET_URL || '';
  264. }
  265. // 拼接完整的 WebSocket URL
  266. const url = websocketUrl.value + userCourseId;
  267. console.log("打印websocket链接地址url", url);
  268. // 创建并启动 WebSocket 连接
  269. socket.value = new WebSocketManager(url, MessageBack);
  270. socket.value.start();
  271. };
  272. /**
  273. * 开始重新识别
  274. * @description 调用后端接口触发重新识别,并建立 WebSocket 连接接收进度
  275. */
  276. const StartReIdentify = () => {
  277. // 如果已经识别完成,点击按钮则关闭弹窗并重置状态
  278. if (reIdentifyText.value === '识别完成') {
  279. showDialog.value = false;
  280. reIdentifyLoading.value = false;
  281. reIdentifyProgress.value = 0;
  282. reIdentifyText.value = '开始识别';
  283. return;
  284. }
  285. // 重置进度状态
  286. reIdentifyProgress.value = 0;
  287. reIdentifyLoading.value = true;
  288. reIdentifyText.value = '识别中';
  289. // 建立 WebSocket 连接
  290. ReIdentifyWebSocketLink();
  291. // 构建请求参数
  292. const param = {
  293. examSubjectId: examSubjectId.value, // 考试科目ID
  294. recognizeNumber: props.reIdentifyTotal, // 需要识别的总张数
  295. justObjective: props.justObjective, // 是否只识别客观题
  296. schoolId: schoolId.value // 学校ID
  297. };
  298. // 调用重新识别接口
  299. recognizeAgain(param).then(res => {
  300. console.log("重新识别结果", res);
  301. // reIdentifyLoading.value = false;
  302. if (res.code == 200) {
  303. // ElMessage.success("重新识别完成!");
  304. console.log("开始重新识别");
  305. }
  306. else
  307. {
  308. ElMessage.error("重新识别失败!" + res.msg);
  309. }
  310. }).catch(err => {
  311. console.error("重新识别请求失败", err);
  312. ElMessage.error("网络请求失败");
  313. });
  314. };
  315. /**
  316. * WebSocket 消息回调处理
  317. * @param data - WebSocket 返回的消息字符串(JSON格式)
  318. * @description 接收后端推送的识别进度,更新UI显示
  319. */
  320. const MessageBack = (data: string) => {
  321. const msg = JSON.parse(data);
  322. console.log("Websocket消息返回处理", msg);
  323. // code: 0-建立成功, 200-业务成功, 400-业务失败/异常
  324. if (msg.code === 200) {
  325. // 已识别张数 +1
  326. recognizedNumber.value++;
  327. // 待识别张数 -1
  328. reIdentifyNumber.value--;
  329. // 计算进度百分比
  330. const process = Math.round((Number(recognizedNumber.value) / Number(props.reIdentifyTotal)) * 100);
  331. reIdentifyProgress.value = Math.min(process, 100); // 确保不超过100%
  332. // 如果进度达到100%,表示识别完成
  333. if (reIdentifyProgress.value === 100) {
  334. ElMessage.success("重新识别完成!");
  335. // 重置所有状态
  336. reIdentifyNumber.value = 0;
  337. recognizedNumber.value = 0;
  338. reIdentifyProgress.value = 0;
  339. reIdentifyText.value = '识别完成';
  340. reIdentifyLoading.value = false;
  341. // 关闭弹窗
  342. showDialog.value = false;
  343. // 通知父组件识别完成
  344. emit('ReIdentifyComplete');
  345. }
  346. }
  347. };
  348. /**
  349. * 格式化文件名
  350. * @param fileName - 原始文件名
  351. * @returns 格式化后的文件名(超过17位时显示前5位+...+后12位)
  352. * @description 当前组件未使用,保留以备后续扩展
  353. */
  354. // const FormatFileName = (fileName: string): string => {
  355. // if (fileName.length <= 17) {
  356. // return fileName;
  357. // }
  358. // const prefix = fileName.substring(0, 5);
  359. // const suffix = fileName.substring(fileName.length - 12);
  360. // return prefix + '...' + suffix;
  361. // };
  362. /**
  363. * 检查文件是否为图片
  364. * @param fileName - 文件名
  365. * @returns 是否为图片文件
  366. * @description 当前组件未使用,保留以备后续扩展
  367. */
  368. // const isImageFile = (fileName: string): boolean => {
  369. // const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
  370. // const lowerFileName = fileName.toLowerCase();
  371. // return imageExtensions.some(ext => lowerFileName.endsWith(ext));
  372. // };
  373. /**
  374. * 获取文件预览URL
  375. * @param file - 文件对象
  376. * @returns 预览URL
  377. * @description 当前组件未使用,保留以备后续扩展
  378. */
  379. // const getFilePreviewUrl = (file: File): string => {
  380. // if (window.URL && file) {
  381. // return window.URL.createObjectURL(file);
  382. // }
  383. // return '';
  384. // };
  385. /**
  386. * 关闭弹窗
  387. * @description 重置状态并通知父组件
  388. */
  389. const handleClose = () => {
  390. showDialog.value = false;
  391. // selectedFiles.value = []; // 当前未使用
  392. emit('close');
  393. };
  394. /**
  395. * 取消上传
  396. * @description 当前组件未使用,保留以备后续扩展
  397. */
  398. // const CancelUpload = () => {
  399. // selectedFiles.value = [];
  400. // emit('CancelUpload', batchNumber.value);
  401. // showDialog.value = false;
  402. // };
  403. // ==================== 暴露方法 ====================
  404. defineExpose({
  405. handleClose
  406. // CancelUpload // 当前未使用
  407. });
  408. </script>
  409. <style lang="scss" scoped>
  410. // 样式保持不变
  411. </style>