scanList.vue 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123
  1. <template>
  2. <div class="page_list">
  3. <div class="search_content">
  4. <div class="content_left">
  5. <span class="scan_state_title">客户端状态:</span>
  6. <span class="scan_state_open" v-if="scanClientStates">
  7. <i class="iconfont icon_open"></i>已打开</span>
  8. <span class="scan_state_close" v-else>
  9. <i class="iconfont icon_close"></i>未打开</span>
  10. <span v-if="scanClientStates" class="scan_state_title" style="color: #999;">(当前客户端版本:{{scanClientVersion}})</span>
  11. <!-- <el-select v-model="params.batchNo" placeholder="选择批次" @change="GoSearch()" class="select_width" >
  12. <el-option label="全部批次" value=""></el-option>
  13. <el-option v-for="item in batchList"
  14. :key="item.value"
  15. :label="item.label" :value="item.value"></el-option>
  16. </el-select>
  17. <el-select v-model="params.batchNo" placeholder="选择状态" @change="GoSearch()" class="select_width" >
  18. <el-option label="全部状态" value=""></el-option>
  19. <el-option v-for="item in batchList"
  20. :key="item.value"
  21. :label="item.label" :value="item.value"></el-option>
  22. </el-select>
  23. <el-input placeholder="考试名称,编号" v-model="params.keyWord" @input="GoSearch" @change="GoSearch()" class="input_width" >
  24. <el-button @click="GoSearch()" slot="append" icon="el-icon-search"></el-button>
  25. </el-input> -->
  26. </div>
  27. <div class="content_right">
  28. <el-button @click="Refresh" >
  29. <i class="iconfont icon_shuaxin"></i>刷新
  30. </el-button>
  31. <!-- <el-button class="delete_item" type="text" @click="OpenDeleteAllDialog" v-if="tableData.length>0">删除所有</el-button> -->
  32. <el-button @click="OpenReIdentify" >重新识别</el-button>
  33. <el-button @click="OpenEditExamList()" type="primary" v-if="isImportStudent">编辑考场名单</el-button>
  34. <el-button @click="OpenImportStudent()" type="primary" v-else>导入考场名单</el-button>
  35. </div>
  36. </div>
  37. <div class="page_jg_20"></div>
  38. <div class="page_content" >
  39. <div class="content_table">
  40. <div class="page_table">
  41. <el-table :data="tableData" style="width: 100%" :height="tableHeight">
  42. <!-- <el-table-column prop="questionName" label="序号" width="100" align="center" >
  43. </el-table-column> -->
  44. <el-table-column prop="batchNo" label="批次" width="120" align="center">
  45. </el-table-column>
  46. <el-table-column prop="scannedPaperNum" label="扫描张数" align="center">
  47. </el-table-column>
  48. <el-table-column prop="uploadNum" label="待上传张数" align="center" >
  49. <template v-slot="scope">
  50. <div class="table_row_answer" v-if="GetNoUploadNumber(scope.row)>0" @click="OpenUpload(scope.row)">
  51. <span style="color:red;text-decoration:underline;"> {{GetNoUploadNumber(scope.row)}}</span>
  52. <!-- {{scope.row.failedNumber || 0}} -->
  53. </div>
  54. <div v-else>0</div>
  55. </template>
  56. </el-table-column>
  57. <el-table-column prop="uploadNum" label="已上传张数" align="center" >
  58. <template v-slot="scope">
  59. </template>
  60. </el-table-column>
  61. <el-table-column prop="scanUserName" label="扫描人" align="center" >
  62. <template v-slot="scope">
  63. </template>
  64. </el-table-column>
  65. <el-table-column prop="scanUserName" label="扫描时间" align="center">
  66. <template v-slot="scope">
  67. {{formatTimestamp(scope.row.scannedTime) }}
  68. </template>
  69. </el-table-column>
  70. <el-table-column prop="name" label="操作" width="250" align="center">
  71. <template v-slot="scope">
  72. <div class="ele_button table_row_button" v-if="scope.row.uploadStatus==0" style="color:#2E64FA;">
  73. <i class="el-icon-loading"></i>{{loadingText}}……
  74. </div>
  75. <div class="ele_button table_row_button" v-else>
  76. <span class="btn_editor" @click="GotoBatchDetail(scope.row.batchNo)">详情</span>
  77. <span class="btn_delete" v-if="scope.row.scanUserName==currentUserName " @click="OptionDelete(scope.row)">删除</span>
  78. <span class="editor_disable" v-else>删除</span>
  79. </div>
  80. </template>
  81. </el-table-column>
  82. </el-table>
  83. </div>
  84. </div>
  85. <div class="content_right">
  86. <div class="right_header">
  87. <div class="scan_set"> 扫描设置</div>
  88. <div class="scan_set_scan_code">
  89. <el-select style="width: 100px;margin-right: 6px;" v-model="useDriveUI" placeholder="是否显示驱动弹窗" @change="ChangeDriveUI">
  90. <el-option v-for="item in scanTypeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
  91. </el-select>
  92. <!-- 识别号: -->
  93. <el-select style="width: 100px;margin-right: 6px;" v-model="scanIdentifyCode" placeholder="选择识别号" @change="ChangeCode" >
  94. <el-option v-for="item in scanIdentifyList" :key="item.value" :label="item.label" :value="item.value"></el-option>
  95. </el-select>
  96. </div>
  97. </div>
  98. <div class="right_center">
  99. <div class="scan_buttons">
  100. <ScanButton :process="scanProcess" :quekao="scanQuekao" :yichang="scanYichang" @click="OpenScan()"></ScanButton>
  101. </div>
  102. <div class="scan_list">
  103. <div class="list_item no_scan" >
  104. <div class="list_item_info" @click="GotoDetail(0)">
  105. <div class="item_info_title">
  106. 未扫描
  107. </div>
  108. <div class="item_info_number">
  109. <span class="number_no_scan">{{scanDataInfo.unScanned}}人</span>
  110. <!-- <span class="number_no_icon"><img src="../../assets/icon/no_scan_icon.png"></span> -->
  111. </div>
  112. </div>
  113. </div>
  114. <div class="list_item no_exam" >
  115. <div class="list_item_info " @click="GotoDetail(2)">
  116. <div class="item_info_title">
  117. 缺考
  118. </div>
  119. <div class="item_info_number">
  120. <span class="number_no_exam">{{scanDataInfo.examMissNum}}人</span>
  121. <!-- <span class="number_no_icon"><img src="../../assets/icon/miss_exam.png"></span> -->
  122. </div>
  123. </div>
  124. </div>
  125. <div class="list_item annormal_icon" >
  126. <div class="list_item_info " @click="GotoDetail(3)">
  127. <div class="item_info_title">
  128. 异常
  129. </div>
  130. <div class="item_info_number">
  131. <span class="number_abnormal">{{scanDataInfo.abnormalNum}}份</span>
  132. <!-- <span class="number_no_icon"><img src="../../assets/icon/abnormal_icon.png"></span> -->
  133. </div>
  134. </div>
  135. </div>
  136. <div class="list_item sucess_upload" >
  137. <div class="list_item_info " @click="GotoDetail(1)">
  138. <div class="item_info_title">
  139. 已上传
  140. </div>
  141. <div class="item_info_number">
  142. <span class="number_uploaded">{{scanDataInfo.scannedNum}}人</span>
  143. <!-- <span class="number_no_icon"><img src="../../assets/icon/sucess_upload.png"></span> -->
  144. </div>
  145. </div>
  146. </div>
  147. </div>
  148. </div>
  149. <div class="right_button">
  150. <el-button type="primary" @click="FinishScanned()" :loading="finishLoading" style="width:calc(100% - 40px);">扫描完成</el-button>
  151. </div>
  152. </div>
  153. </div>
  154. <SelectStudent v-model="showSelectStudent" @success="StudentSuccess"></SelectStudent>
  155. <AbnormalDetail v-model="showAbnormalDialog" :scanState="scanState"></AbnormalDetail>
  156. </div>
  157. </template>
  158. <script lang="ts" setup>
  159. import { useExamStore } from '@/store/exam'
  160. import { useUserStore } from '@/store/user'
  161. import { useRouter } from 'vue-router'
  162. import { onMounted ,ref,computed,onUnmounted,nextTick } from 'vue';
  163. import ScanButton from './components/scanButton.vue'
  164. import SelectStudent from './components/selectStudent.vue'
  165. import { finishScanned,getExamScoreSummary,hasImportStudent,getBatchList,getCurrentBatchNo,deleteBatch,updateScanCount,setExamScanSetting,saveChooseCode} from '@/api/exam'
  166. import scanCommon from '@/utils/scanCommon';
  167. import { ElMessageBox, ElMessage } from 'element-plus'
  168. import { formatTimestamp } from '@/utils/common';
  169. import AbnormalDetail from './abnormalDetail.vue';
  170. import WebSocketManager from '@/utils/webSocket'//后端websocket链接js
  171. // 实例化 Store
  172. const examStore = useExamStore()
  173. const userStore = useUserStore()
  174. const router = useRouter()
  175. // 考试科目 ID
  176. const examSubjectId = computed(() => {
  177. return examStore.currentExam?.id
  178. })//计算属性
  179. const userInfo=computed(() => {
  180. return userStore.userInfo;
  181. })//用户信息
  182. const schoolType=computed(() => {
  183. return ''
  184. })
  185. //考试科目code
  186. const examSubjectCode=computed(() => {
  187. return examStore.currentExam?.examSubjectCode
  188. })
  189. //当前登录人姓名
  190. const currentUserName=computed(() => {
  191. return userStore.userName;
  192. })
  193. //查询是否导入了学生名单
  194. const HasImportStudent = async () => {
  195. const params = {
  196. examSubjectId: examSubjectId.value,
  197. schoolId: 0,//单校 0
  198. };
  199. const res = await hasImportStudent(params);
  200. console.log("打印是否导入了学生名单",res);
  201. if(res.code==200)
  202. {
  203. isImportStudent.value=res.data;
  204. }
  205. else{
  206. isImportStudent.value=false;
  207. }
  208. if(!isImportStudent.value)
  209. {
  210. console.log("未导入学生名单,跳转到选择学生名单界面");
  211. //跳转导入学生名单界面
  212. router.push({
  213. name: 'selectExamination',
  214. });
  215. }
  216. }
  217. // 组件创建时立即执行
  218. HasImportStudent();//查询是否导入了学生名单();
  219. const selectSchoolId=ref(0);//学校ID
  220. const showAbnormalDialog=ref(false);//异常弹窗
  221. const scanState = ref(0);//扫描状态//默认未扫描
  222. const params=ref({
  223. batchNo:'',
  224. keyWord:''
  225. })
  226. const scanClientStates=ref(false);//客户端状态
  227. const scanClientVersion=ref('');//客户端版本
  228. const isScanning=ref(false);//是否正在扫描 扫描状态
  229. const hasMakeTemplate=ref(false);//模版是否制作完成
  230. const currentBatchNo=ref('');//当前批次号
  231. const currentBatchNoId=ref('');//当前批次号id
  232. const loadingText=ref('');//加载文本
  233. const baseUrl=import.meta.env.VITE_API_BASE_URL;
  234. const uploadUrl=`https://dev3.k12100.net/teaching/api/v1/ai_exam_scan/upload_multi_img`;//图片上传地址
  235. const updateCountUrl=`https://dev3.k12100.net/teaching/api/v1/ai_exam_scan/record_batch_scan_number`;
  236. const webSocket=ref<InstanceType<typeof WebSocketManager> | null>(null);//websocket
  237. let websocketUrl=import.meta.env.VITE_WEB_SOCKET_URL;//websocket链接地址
  238. // 定义数据类型接口
  239. interface BatchItem {
  240. id?: string | number;
  241. batchNo: string;
  242. batchTypeName?: string;
  243. scanUserName?: string;
  244. scannedPaperNum?: number;//扫描张数
  245. scannedTime?: number | string;
  246. uploadNum?: number;//上传张数
  247. uploadStatus?: number; // 确保包含此属性
  248. failedNumber?: number;
  249. [key: string]: any; // 允许其他动态属性
  250. }
  251. const tableData=ref<BatchItem[]>([]); //批次列表
  252. const tableHeight=ref(500);
  253. const scanIdentifyList=[
  254. {
  255. label:'考号',
  256. value:'2',
  257. },
  258. {
  259. label:'学号',
  260. value:'1',
  261. }
  262. ];//识别号列表
  263. const scanTypeList=[
  264. {
  265. label:'不弹窗',
  266. value:0,
  267. },
  268. {
  269. label:'弹窗',
  270. value:1,
  271. }
  272. ];
  273. const scanIdentifyCode=ref('1');////识别号 1-学号 2-考号
  274. const useDriveUI=ref(0);//1是会弹框,其他值或不写则不弹
  275. const isImportStudent=ref(false);//是否导入了学生名单
  276. const showSelectStudent=ref(false);//是否显示选择学生名单弹窗
  277. const finishLoading=ref(false);//扫描完成加载中
  278. const scanDataInfo=ref({
  279. abnormalNum:0,//异常数量
  280. examMissNum:0,//缺考数量
  281. unScanned:0,//未扫描数量
  282. scannedNum:0,//已上传数量
  283. examTotal:0,//考试总人数
  284. });//扫描返回的数据信息
  285. //扫描进度
  286. const scanProcess=computed(() => {
  287. return Math.floor((scanDataInfo.value.scannedNum+scanDataInfo.value.examMissNum)/scanDataInfo.value.examTotal)*100;
  288. });
  289. //缺考进度
  290. const scanQuekao=computed(() => {
  291. return Math.floor((scanDataInfo.value.examMissNum)/scanDataInfo.value.examTotal)*100;
  292. });
  293. //异常进度
  294. const scanYichang=computed(() => {
  295. return Math.floor((scanDataInfo.value.abnormalNum)/scanDataInfo.value.examTotal)*100;
  296. });
  297. //刷新
  298. const Refresh = () => {
  299. tableData.value=[];
  300. GetScanBatchList();
  301. }
  302. //扫描完成
  303. const FinishScanned= async () =>{
  304. console.log("打印完成")
  305. ElMessageBox.confirm('确认完成吗?', '提示', {
  306. confirmButtonText: '确定',
  307. cancelButtonText: '取消',
  308. type: 'warning',
  309. customClass:'page_dialog'
  310. }).then(async () => {
  311. let param={
  312. id:examSubjectId.value
  313. };
  314. finishLoading.value=true;
  315. const res = await finishScanned(param)
  316. if(res.code == 200)
  317. {
  318. // ElMessage.success("完成成功!")
  319. let idList=res.data.split(",");
  320. let analysisParam = {
  321. examId:idList[0],
  322. examPaperIds:[idList[1]],//多科传0 单科传科目ID
  323. }
  324. //扫描完成 调用汇总接口
  325. const analysisRes = await getExamScoreSummary(analysisParam);
  326. if(analysisRes.code == 200)
  327. {
  328. ElMessage.success("完成成功!")
  329. finishLoading.value=false;
  330. }
  331. else
  332. {
  333. ElMessage.error(analysisRes.msg);
  334. finishLoading.value=false;
  335. }
  336. }
  337. else
  338. {
  339. ElMessage.error(res.msg);
  340. finishLoading.value=false;
  341. }
  342. })
  343. }
  344. // 根据环境配置请求地址
  345. const ConfigRequestUrlByEnv= () =>{
  346. console.log("打印环境", import.meta.env.MODE);
  347. if (import.meta.env.MODE === 'development')
  348. {
  349. //开发环境
  350. console.log("打印import.meta.env.VITE_API_BASE_URL", import.meta.env.VITE_API_BASE_URL);
  351. websocketUrl = 'wss://dev3.k12100.net/teaching/api/v1/teaching/websocket/'
  352. console.log("设置webSocket地址",websocketUrl);
  353. } else if (import.meta.env.MODE === 'production')
  354. {
  355. websocketUrl = import.meta.env.VITE_WEB_SOCKET_URL || '';
  356. console.log("打印websocket地址", websocketUrl);
  357. console.log("打印上传图片地址", uploadUrl);
  358. }
  359. }
  360. //切换弹窗模式
  361. const ChangeDriveUI=()=>{
  362. console.log("打印弹窗模式", useDriveUI.value);
  363. let params={
  364. alterWindow:useDriveUI.value
  365. };
  366. setExamScanSetting(params).then(res => {
  367. if(res.code == 200)
  368. {
  369. ElMessage.success("设置成功!")
  370. }
  371. else
  372. {
  373. ElMessage.error(res.msg);
  374. }
  375. })
  376. }
  377. //改变识别码
  378. const ChangeCode=()=>{
  379. let param={
  380. examSubjectId:examSubjectId.value,
  381. chooseCode:scanIdentifyCode.value//识别号 1-学号 2-平台号 3-学籍号 4-考号
  382. };
  383. saveChooseCode(param).then(res=>{
  384. console.log("打印识别码设置结果",res);
  385. if(res.code==200)
  386. {
  387. ElMessage.success("设置成功!")
  388. }
  389. else
  390. {
  391. ElMessage.error(res.msg);
  392. }
  393. })
  394. }
  395. //获取未上传图片的数量
  396. const GetNoUploadNumber=(item:BatchItem)=>
  397. {
  398. const scannedPaperNum = item.scannedPaperNum ?? 0;
  399. const uploadNum = item.uploadNum ?? 0;
  400. const result = scannedPaperNum - uploadNum;
  401. return result > 0 ? result : 0;
  402. }
  403. // 跟后端websocket连接 1.建立socket连接 用户ID_考试科目ID_学校ID_批次号
  404. const WebSocketLink = () => {
  405. if(webSocket.value && webSocket.value.getSocketStatus() === WebSocket.OPEN)
  406. {
  407. webSocket.value.closeWebSocket();
  408. }
  409. let userCourseId='';
  410. let schoolIdValue = userInfo.value.schoolId; // 这里逻辑是 单校都取登录用户的学校ID 如果是联校账号 给单校扫描 传单校的学校id
  411. // if(schoolType.value == 1) {
  412. // //联校 盟主学校
  413. // schoolIdValue = selectSchoolId.value;
  414. // } else {
  415. // schoolIdValue = userInfo.value.schoolId;
  416. // }
  417. console.log("打印用户信息", userInfo.value);
  418. userCourseId = `${userInfo.value.userId}_${examSubjectId.value}_${schoolIdValue}_${currentBatchNo.value}`; //用户 科目 批次号
  419. console.log("websocketUrl",websocketUrl);
  420. const url = `${websocketUrl}${userCourseId}`;
  421. console.log("打印websocket链接地址url", url);
  422. webSocket.value = new WebSocketManager(url, MessageBack);
  423. webSocket.value.start();
  424. }
  425. //websocket消息返回
  426. const MessageBack = (data:any) => {
  427. // console.log("打印websocket返回数据", data);
  428. const msg=JSON.parse(data);
  429. console.log("Websocket消息返回处理",msg);
  430. //code 0 建立成功 200 业务成功 400 业务失败 异常 需要提示信息
  431. if(msg.code==0)
  432. {
  433. //建立成功
  434. console.log("和后端的websocket建立成功");
  435. }
  436. else if(msg.code==200)
  437. {
  438. //业务成功
  439. // console.log("打印当前tableData的数据",this.tableData);
  440. //正常扫描
  441. let currentData=msg.data[0];
  442. let targetItem = tableData.value.find(item => item.batchNo == currentData.batchNo);
  443. console.log("识别成功+1",targetItem);
  444. if(msg.abnormalNum==-1)
  445. {
  446. scanDataInfo.value.unScanned = msg.unScanned;
  447. scanDataInfo.value.examMissNum = msg.examMissNum;
  448. scanDataInfo.value.abnormalNum = msg.abnormalNum;
  449. scanDataInfo.value.scannedNum = msg.scannedNum;
  450. if (targetItem) {
  451. if(currentData.uploadNum==-1)
  452. {
  453. targetItem.uploadNum++;
  454. }
  455. }
  456. }
  457. else
  458. {
  459. if (targetItem) {
  460. targetItem.uploadNum = msg.data[0].uploadNum;
  461. }
  462. scanDataInfo.value.unScanned = msg.unScanned;
  463. scanDataInfo.value.examMissNum = msg.examMissNum;
  464. scanDataInfo.value.abnormalNum = msg.abnormalNum;
  465. scanDataInfo.value.scannedNum = msg.scannedNum;
  466. let process = (Number(scanDataInfo.value.scannedNum) + Number(scanDataInfo.value.examMissNum)) / Number(scanDataInfo.value.examTotal) * 100
  467. if (isNaN(process)) {
  468. process = 0
  469. }
  470. }
  471. if(targetItem)
  472. {
  473. if(Number(targetItem.scannedPaperNum) === Number(targetItem.uploadNum))
  474. {
  475. targetItem.uploadStatus=1;
  476. // loadingText.value='';
  477. isScanning.value=false;
  478. // removeBeforeUnloadListener();// 移除页面离开阻止监听器
  479. }
  480. else
  481. {
  482. }
  483. }
  484. }
  485. else if(msg.code==400)
  486. {
  487. //业务失败
  488. GetScanBatchList();
  489. ElMessage.error(msg.message);
  490. }
  491. }
  492. //重新识别弹窗
  493. const OpenReIdentify=() => {
  494. }
  495. //开始扫描
  496. const OpenScan = () => {
  497. if(isScanning.value)
  498. {
  499. //正在扫描
  500. ElMessage.warning('正在扫描中,请勿重复点击哦!');
  501. return;
  502. }
  503. //判断客户端是否已经连接
  504. if(scanClientStates.value)
  505. {
  506. //一打开客户端
  507. console.log('客户端已打开,开始扫描');
  508. // isScanning.value=true;//扫描状态
  509. const params={
  510. examSubjectId:examSubjectId.value,
  511. schoolId:selectSchoolId.value,
  512. };
  513. getCurrentBatchNo(params).then((res:any)=>{
  514. console.log("当前获取批次结果",res);
  515. if(res.code==200)
  516. {
  517. currentBatchNo.value=res.data.batchNo;
  518. currentBatchNoId.value=res.data.id;
  519. let newData={
  520. batchNo:currentBatchNo.value,
  521. id:currentBatchNoId.value,//批次号
  522. batchTypeName:'扫描',
  523. scanUserName:currentUserName.value,
  524. scannedPaperNum:0,//扫描张数
  525. scannedTime:Date.now(),//扫描时间
  526. uploadNum:0,//上传张数
  527. uploadStatus:0,//扫描状态
  528. };
  529. tableData.value.push(newData);
  530. //开始扫描
  531. let jsonParam={
  532. examSubjectId:examSubjectId.value,
  533. batchNumber:currentBatchNo.value,
  534. // sensitive: 35,//灵敏度参数 固定35
  535. filter:0,//是否过滤参数 1-是 0-否
  536. // heightRatio:this.heightRatio,//占打分框高度比例
  537. schoolId:selectSchoolId.value//学校id 联校单校都需要学校id
  538. };
  539. let json = {
  540. "action":"startScan",//交互指令参数
  541. "batchNumber":GetBatchStr(res.data.id,currentBatchNo.value),//批次号 这里传给客户端的批次号需要进行处理 截取批次id后5位拼接batchNumber
  542. "subjectCode":examSubjectCode.value,//科目编号
  543. "paperSchema":1,//this.paperSchema,// 单双面//页面类型 1 单面 2 双面
  544. "token":localStorage.getItem('token'),//token信息
  545. "uploadUrl":uploadUrl, // 图片上传地址
  546. "updateCountUrl":updateCountUrl,//更新扫描纸张数量接口 接口参数 批次id post 请求 id
  547. "batchId":currentBatchNoId.value,//批次号id
  548. "dpi":150,//dpi参数 设置图片清晰度质量的
  549. "isColor":1,//是否彩色图片 手阅卡扫描彩色图片 0 黑白 1 彩色
  550. "useDriveUI":0,//startScan和scanTemplate增加useDriveUI参数,=1时会弹框,其他值或不写则不弹
  551. "jsonParam":JSON.stringify(jsonParam),// 后端使用的参数 json字符串 后端接口固定三个参数 1:jsonParam 2:seqNumber(这个批次的图片序号:从1开始) 3:file 图片文件
  552. };//正式版参数
  553. console.log("打印发送的数据",JSON.stringify(json));
  554. setTimeout(() => {
  555. scanCommon.send(JSON.stringify(json))
  556. },100)
  557. // 建立与后端的websocket连接
  558. WebSocketLink();
  559. }
  560. else
  561. {
  562. ElMessage.error(res.msg);
  563. }
  564. });
  565. }
  566. else
  567. {
  568. //提示客户端未打开
  569. ElMessage.warning('请先打开客户端');
  570. }
  571. }
  572. //获取批次字符串
  573. const GetBatchStr=(batchId:any,batChNo:any)=>{
  574. const batchNoValue = batchId.substring(batchId.length - 5);
  575. const batchNo=String(batChNo).padStart(3, '0');
  576. const batchNumber=batchNoValue+batchNo;//传给客户端的图片批次号 id后五位数加上00批次号
  577. return batchNumber;
  578. }
  579. //根据处理后的批次号转换成处理前的批次号
  580. const GetBatchNumber=(batchNumber:any)=>{
  581. if (!batchNumber) {
  582. console.warn('批次号为空或未定义');
  583. return currentBatchNo.value;//返回当前的批次号
  584. }
  585. const batchStr = batchNumber.toString();
  586. if (batchStr.length < 3) {
  587. console.warn('批次号长度小于3位:', batchStr);
  588. return currentBatchNo.value;//返回当前的批次号
  589. }
  590. const lastThree = batchStr.slice(-3);
  591. const parsedNumber = parseInt(lastThree, 10);
  592. if (isNaN(parsedNumber)) {
  593. console.error('无法解析批次号:', lastThree);
  594. return currentBatchNo.value;//返回当前的批次号
  595. }
  596. console.log("打印最后的批次号",parsedNumber);
  597. return parsedNumber.toString();
  598. }
  599. //删除批次
  600. const OptionDelete=(row: any) => {
  601. console.log('删除批次', row);
  602. if(row.scanUserName==currentUserName.value)
  603. {
  604. ElMessageBox.confirm('确定要删除该批次吗?', '提示', {
  605. confirmButtonText: '确定',
  606. cancelButtonText: '取消',
  607. customClass:'page_dialog',
  608. type: 'warning'
  609. }).then(() => {
  610. // 删除操作
  611. console.log('删除', row);
  612. const params={
  613. examSubjectId:examSubjectId.value,
  614. batchNo:row.batchNo,
  615. schoolId:selectSchoolId.value,
  616. };
  617. deleteBatch(params).then((res:any)=>{
  618. if(res.code==200)
  619. {
  620. ElMessage.success("删除成功!")
  621. GetScanBatchList();
  622. }
  623. else
  624. {
  625. ElMessage.error(res.msg);
  626. }
  627. })
  628. }).catch(() => {
  629. // 取消操作
  630. console.log('取消删除');
  631. });
  632. }
  633. else
  634. {
  635. ElMessage.warning('只能删除自己上传的记录');
  636. }
  637. }
  638. //跳转到异常详情页
  639. const GotoDetail = (type: number) => {
  640. console.log('跳转到异常详情页', type);
  641. scanState.value=type;
  642. // 根据类型跳转到不同的详情页
  643. showAbnormalDialog.value=true;
  644. }
  645. //跳转到批次详情页
  646. const GotoBatchDetail=(batchNo:any)=>{
  647. router.push({
  648. path: '/exam/scanDetail',
  649. query: {
  650. examSubjectId: examSubjectId.value,
  651. batchNo: batchNo,
  652. },
  653. })
  654. }
  655. // 打开导入学生名单
  656. const OpenImportStudent = () => {
  657. showSelectStudent.value=true;
  658. }
  659. // 打开编辑考试名单
  660. const OpenEditExamList = () => {
  661. router.push({
  662. path: '/exam/examList',
  663. query: {
  664. examSubjectId: examSubjectId.value,
  665. },
  666. });
  667. }
  668. //学生名单导入成功
  669. const StudentSuccess = () => {
  670. HasImportStudent();
  671. }
  672. //获取扫描批次列表
  673. const GetScanBatchList=async()=>{
  674. const params = {
  675. examSubjectId: examSubjectId.value,
  676. schoolId: 0,//单校 0
  677. };
  678. const res = await getBatchList(params);
  679. if(res.code==200)
  680. {
  681. selectSchoolId.value=res.data.schoolId;//获取学校id
  682. if(res.data.scannedWebSocketVO)
  683. {
  684. tableData.value=res.data.scannedWebSocketVO?.data || [];
  685. scanDataInfo.value.abnormalNum=res.data.scannedWebSocketVO.abnormalNum;//异常数量
  686. scanDataInfo.value.examMissNum=res.data.scannedWebSocketVO.examMissNum;//缺考数量
  687. scanDataInfo.value.scannedNum=res.data.scannedWebSocketVO.scannedNum;//已上传数量
  688. scanDataInfo.value.unScanned=res.data.scannedWebSocketVO.unScanned;//未扫描数量
  689. scanDataInfo.value.examTotal=res.data.scannedWebSocketVO.examTotal;//总人数
  690. let examInfoCount={
  691. unScanned:res.data.scannedWebSocketVO.unScanned,
  692. examMissNum:res.data.scannedWebSocketVO.examMissNum,
  693. scannedNum:res.data.scannedWebSocketVO.scannedNum,
  694. abnormalNum:res.data.scannedWebSocketVO.abnormalNum,
  695. examTotal:res.data.scannedWebSocketVO.examTotal,
  696. };
  697. examStore.setExamInfoCount(examInfoCount);//设置扫描数据信息
  698. }
  699. }
  700. }
  701. //计算高度的函数
  702. const CalculateTableHeight = () => {
  703. // nextTick 确保 DOM 更新后再获取尺寸
  704. nextTick(() => {
  705. // window.innerHeight 是浏览器可视区域高度
  706. // 简单算法:视窗高度 - 固定占用高度
  707. let computedHeight = window.innerHeight - 136;
  708. // 限制最小高度,防止太矮
  709. if (computedHeight < 200) {
  710. computedHeight = 200;
  711. }
  712. tableHeight.value = computedHeight;
  713. });
  714. };
  715. //更新扫描张数
  716. const UpdateBatchScanNumber = (batchId: any, scanNumber: Number) => {
  717. const params={
  718. id:batchId,
  719. recordNumber: scanNumber,
  720. };
  721. updateScanCount(params).then((res:any)=>{
  722. if(res.code==200)
  723. {
  724. console.log("第"+batchId+"批次更新扫描张数"+scanNumber+"成功", res);
  725. }
  726. })
  727. };
  728. // 处理扫描结果
  729. const HandleScanResult = (res: any) => {
  730. console.log('收到扫描数据', res);
  731. // 业务逻辑...
  732. if (res.action == 'uploading')
  733. {
  734. let batchNumber: any = '';
  735. if (res.batchNumber)
  736. {
  737. batchNumber = GetBatchNumber(res.batchNumber || res.data?.batchNumber);
  738. }
  739. const targetItem = tableData.value.find((item: any) => item.batchNo == batchNumber);
  740. if (typeof loadingText !== 'undefined') loadingText.value = "启动扫描仪中";
  741. ElMessage.success("正在启动扫描仪,请稍后…");
  742. if (targetItem)
  743. {
  744. targetItem.uploadStatus = 0; //更新上传状态 0 开始上传
  745. }
  746. }
  747. // 开始扫描指令
  748. if (res.action == 'startScan')
  749. {
  750. let batchNumber = GetBatchNumber(res?.batchNumber || res.data?.batchNumber);
  751. let currentItem = tableData.value.find((item: any) => item.batchNo == batchNumber);
  752. console.log("打印currentItem", currentItem);
  753. //开始扫描指令
  754. if (res.code == 200)
  755. {
  756. ElMessage.success("扫描仪启动成功,开始扫描…");
  757. if(typeof loadingText !== 'undefined') loadingText.value = "正在扫描中";
  758. }
  759. if (res.code == 502)
  760. {
  761. console.log("开始扫描指令502错误 未检测到纸张或者卡纸", res);
  762. console.log("打印currentItem", currentItem);
  763. if (currentItem) {
  764. currentItem.uploadStatus = 1; //更新上传状态
  765. }
  766. if (typeof loadingText !== 'undefined') loadingText.value = ""; //清空上传提示
  767. isScanning.value = false; //重置扫描状态
  768. ElMessage.error(res.msg);
  769. }
  770. if (res.code == 510)
  771. {
  772. console.log("启动扫描失败,上传正在进行中", res);
  773. isScanning.value = true; //重置扫描状态
  774. ElMessage.warning(res.msg + '请勿重复点击');
  775. }
  776. if (res.code == 509) {
  777. console.log("扫描仪正在使用中", res);
  778. if (res.msg == '未找到指定的扫描仪') {
  779. isScanning.value = false; //重置扫描状态
  780. }
  781. ElMessage.warning(res.msg + ',请稍后再试!');
  782. if (currentItem) {
  783. currentItem.uploadStatus = 1; //更新上传状态
  784. }
  785. }
  786. }
  787. // 扫描完成指令
  788. if (res.action == 'uploadFinish') {
  789. console.log("上传完成uploadFinish 更新上传动画状态", res);
  790. // 修复:直接调用 GetBatchNumber
  791. let batchNumber = GetBatchNumber(res?.batchNumber || res.data?.batchNumber);
  792. let targetItem = tableData.value.find((item: any) => item.batchNo == batchNumber);
  793. if (typeof loadingText !== 'undefined') loadingText.value = '正在上传中';
  794. if (targetItem) {
  795. targetItem.scannedPaperNum = res.scanNumber; //更新扫描张数
  796. targetItem.failedNumber = res.failedNumber; //更新失败张数
  797. if (typeof UpdateBatchScanNumber === 'function') {
  798. UpdateBatchScanNumber(targetItem.id, Number(targetItem.scannedPaperNum));
  799. }
  800. }
  801. isScanning.value = false; //重置扫描状态
  802. }
  803. if (res.action == 'uploadNumber') {
  804. console.log("上传完成uploadNumber 更新上传张数", res);
  805. // 逻辑已注释,保持原样
  806. }
  807. if (res.action == 'scanNumber') {
  808. console.log("扫描张数scanNumber 更新扫描张数");
  809. // 修复:直接调用 GetBatchNumber
  810. let batchNumber = GetBatchNumber(res.batchNumber || res.data?.batchNumber);
  811. let targetItem = tableData.value.find((item: any) => item.batchNo == batchNumber);
  812. console.log("打印targetItem", targetItem);
  813. if (targetItem) {
  814. targetItem.scannedPaperNum = res.number; //更新上传张数
  815. targetItem.uploadStatus = 0; //更新上传状态 0 开始上传
  816. }
  817. // 修复:确保 UpdateBatchScanNumber 已定义
  818. if (targetItem && typeof UpdateBatchScanNumber === 'function') {
  819. UpdateBatchScanNumber(targetItem.id, Number(targetItem.scannedPaperNum));
  820. }
  821. }
  822. if (res.action == 'loadImage') {
  823. console.log("加载图片loadImage 获取图片数据", res);
  824. // 修复:reImageList 必须是 ref
  825. if (typeof reImageList !== 'undefined') {
  826. reImageList.value = res.data || [];
  827. reImageList.value.forEach((item: any) => {
  828. item.uploadStatus = -2;
  829. item.uploadProgress = 0;
  830. });
  831. }
  832. let batchNumber = GetBatchNumber(res.batchNumber);
  833. let targetItem = tableData.value.find((item: any) => item.batchNo == batchNumber);
  834. console.log("打印重新上传图片列表", reImageList.value);
  835. console.log("打印重新上传的批次Item", targetItem);
  836. if (targetItem) {
  837. targetItem.uploadStatus = 0;
  838. }
  839. if (typeof loadingText !== 'undefined') loadingText.value = '正在上传中';
  840. if (typeof uploadDialogTitle !== 'undefined') uploadDialogTitle.value = '未上传的图片';
  841. if (typeof showUploadDialog !== 'undefined') showUploadDialog.value = true;
  842. }
  843. if (res.action == 'finish') {
  844. console.log("图片重传finish指令", res);
  845. let batchNumber = GetBatchNumber(res.batchNumber);
  846. let targetItem = tableData.value.find((item: any) => item.batchNo == batchNumber);
  847. let fileIndex = res.fileIndex.split(",");
  848. console.log("打印fileIndex", fileIndex);
  849. fileIndex.forEach((index: string) => {
  850. // 修复:reImageList.value
  851. let reImageItem = reImageList.value.find((item: any) => item.fileIndex == index);
  852. if (reImageItem) {
  853. let progress = reImageItem.uploadProgress;
  854. const interval = setInterval(() => {
  855. progress += 5;
  856. if (progress >= 100) {
  857. progress = 100;
  858. clearInterval(interval);
  859. // 修复:Vue3 不需要 $set,直接赋值即可触发响应式
  860. reImageItem.uploadStatus = res.uploadState;
  861. }
  862. reImageItem.uploadProgress = progress;
  863. }, 100);
  864. }
  865. });
  866. // 修复:reImageCount 可能是 ref 或计算属性
  867. let currentReImageCount = typeof reImageCount !== 'undefined' ? reImageCount.value : reImageList.value.length;
  868. if (currentReImageCount === 0) {
  869. if (typeof showUploadDialog !== 'undefined') showUploadDialog.value = false;
  870. if (targetItem) {
  871. targetItem.uploadStatus = 1;
  872. isScanning.value = false;
  873. // 修复:确保 GetBatchList 已定义
  874. if (typeof GetBatchList === 'function') GetBatchList();
  875. }
  876. }
  877. }
  878. if (res.action == 'getFailedImage') {
  879. console.log("获取失败图片getFailedImage", res);
  880. let failedList = res.data || [];
  881. failedList.forEach((item: any) => {
  882. let batchNumber = GetBatchNumber(item.batchNumber);
  883. let currentItem = tableData.value.find((item1: any) => item1.batchNo == batchNumber);
  884. if (currentItem) {
  885. if (item.failedNumber == 0) {
  886. if (currentItem.scannedPaperNum != currentItem.uploadNum) {
  887. currentItem.scannedPaperNum = currentItem.uploadNum;
  888. if (typeof UpdateBatchScanNumber === 'function') {
  889. UpdateBatchScanNumber(currentItem.id, currentItem.uploadNum);
  890. }
  891. }
  892. } else {
  893. let number = currentItem.uploadNum + item.failedNumber;
  894. currentItem.scannedPaperNum = number;
  895. if (typeof UpdateBatchScanNumber === 'function') {
  896. UpdateBatchScanNumber(currentItem.id, number);
  897. }
  898. }
  899. }
  900. });
  901. }
  902. if (res.action == 'scanFinishBatch')
  903. {
  904. if (typeof loadingText !== 'undefined') loadingText.value = '本次扫描结束';
  905. console.log("本批次扫描完成scanFinishBatch 更新扫描张数", res);
  906. }
  907. };
  908. onMounted(() => {
  909. // 初始化连接
  910. scanCommon.init(HandleScanResult);
  911. // 监听连接状态
  912. scanCommon.watchConnection((isOnline,clientVersion) => {
  913. scanClientStates.value=isOnline;
  914. scanClientVersion.value=clientVersion; //客户端版本号
  915. // console.log("客户端版本",clientVersion);
  916. });
  917. if (!examStore.currentExam) {
  918. console.warn('当前没有选中的考试信息')
  919. // 可选:如果没有数据,可以重定向回列表页或提示用户
  920. }
  921. GetScanBatchList();//获取扫描批次列表
  922. CalculateTableHeight();//初始化计算表格高度
  923. console.log("打印当前的配置",import.meta.env.VUE_APP_BASE);
  924. //环境判断
  925. if (import.meta.env.VUE_APP_BASE === 'https://www.k12100.com')
  926. {
  927. }
  928. else
  929. {
  930. ConfigRequestUrlByEnv();///连接本地时候使用
  931. }
  932. // 监听窗口大小变化
  933. window.addEventListener('resize', CalculateTableHeight);
  934. });
  935. // 卸载时移除监听,防止内存泄漏
  936. onUnmounted(() => {
  937. window.removeEventListener('resize', CalculateTableHeight);
  938. // 组件销毁时务必停止,防止内存泄漏和后台重连
  939. scanCommon.stop();
  940. });
  941. </script>
  942. <style lang="scss" scoped>
  943. .page_list
  944. {
  945. width: 100%;
  946. height: 100%;
  947. padding: 20px;
  948. background-color: #fff;
  949. border-radius: 4px;
  950. box-sizing: border-box;
  951. }
  952. .scan_state_title
  953. {
  954. font-size: 14px;
  955. color:#333;
  956. font-weight: 400;
  957. }
  958. .scan_state_open
  959. { margin-left: 5px;
  960. font-size: 14px;
  961. font-weight: 400;
  962. color: #2BC644;
  963. i
  964. {
  965. margin-right: 5px;
  966. }
  967. }
  968. .scan_state_close
  969. {
  970. font-size: 14px;
  971. font-weight: 400;
  972. margin-left: 5px;
  973. color:#F56C6C;
  974. i
  975. {
  976. margin-right: 5px;
  977. }
  978. }
  979. </style>