| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071 |
- <template>
- <div class="detail_main">
- <div class="question_header">
- <div class="item_title">
- <div class="item_step">1</div>
- 扫描标准答案:<el-button type="primary" @click="GotoScan()" :loading="scanLoading" style="width: 88px;">{{scanText}}</el-button>
- <span class="preview_btn" @click="OpenImage()">预览</span>
- <!-- <el-button style="margin-left: 10px;width: 116px;">引用试题结构</el-button> -->
- </div>
- <div class="choose_template">
- 当前模板:{{currentTemplateName}}
- <!-- <el-select v-model="queryParams.start" placeholder="请选择" class="select_width" >
- <el-option :label="number" :value="number" v-for="number in templateQuestionCount"></el-option>
- </el-select> -->
- <span class="choose_btn" @click="OpenChooseTemplate()">更换</span>
- </div>
- </div>
- <div class="page_jg_20"></div>
- <div class="page_module">
- <div class="item_title">
- <div class="item_step">2</div>
- 设置试题结构:
- <div class="item_search">
- <div class="search_name">题目从</div>
- <el-select v-model="queryParams.start" placeholder="请选择" class="select_width" >
- <el-option :label="number" :value="number" v-for="number in questionRange"></el-option>
- </el-select>
- <div class="search_name">至</div>
- <el-select v-model="queryParams.end" placeholder="请选择" class="select_width" >
- <el-option :label="number" :value="number" v-for="number in questionRange"></el-option>
- </el-select>
- </div>
- <div class="item_search">
- <div class="search_name">题目类型:</div>
- <el-select v-model="queryParams.questionType" placeholder="请选择" class="select_width" >
- <el-option label="单选题" :value="1"></el-option>
- <el-option label="多选题" :value="2"></el-option>
- </el-select>
- </div>
- <div class="item_search">
- <div class="search_name">每题满分:</div>
- <el-input v-model="queryParams.fullMark" placeholder="请输入" maxlength="3" @input="(val: any) => onScoreInput(queryParams, 'fullMark', val)" class="select_width" ></el-input>
- </div>
- <div class="item_search">
- <div class="search_name">分组:</div>
- <el-input v-model="queryParams.groupName" placeholder="请输入" class="select_width" ></el-input>
- </div>
- <div class="item_search" v-if="queryParams.questionType == 2" >
- (给分规则:少选得<el-input v-model="shaoxuan" style="width: 50px;" maxlength="4" @change="ScoreYanzheng()"></el-input>分
- )
- </div>
- <div class="item_search">
- <el-button type="primary" style="width: 68px;" @click="EnterSetQuestion()">确定</el-button>
- </div>
- <div class="delete_all">
- <div class="delete_btn" @click="DeleteAll()">全部删除</div>
- </div>
- </div>
- <div class="page_jg_16"></div>
- <div class="table_42">
- <el-table :data="tableData" v-loading="tableLoading" element-loading-text="加载中……" element-loading-spinner="el-icon-loading" element-loading-background="#ffffff" style="width: 100%" :height="tableHeight">
- <el-table-column prop="questionName" label="题目名称" width="100">
- </el-table-column>
- <!-- 题目类型 questionType 1 单校 2多选 -->
- <el-table-column prop="questionType" label="题目类型" width="120">
- <template v-slot="scope">
- <el-select v-model="scope.row.questionType" style="width: calc(100% - 32px);" @change="TypeChange(scope.row)">
- <el-option :value="1" label="单选题"></el-option>
- <el-option :value="2" label="多选题"></el-option>
- </el-select>
- </template>
- </el-table-column>
- <el-table-column prop="date" label="标注答案" width="242">
- <template v-slot="scope">
- <div class="answer_input">
- <!-- 单选题 -->
- <div class="single_selected" v-if="scope.row.questionType == 1">
- <el-button v-for="(item,index) in scope.row.optionsSize" :type="scope.row.standardAnswer === answerList[index] ? 'primary': ''" class="selected_button" @click="SingleClick(scope.row,answerList[index])">{{answerList[index]}}</el-button>
- </div>
- <!-- 多选 -->
- <div class="single_selected" v-if="scope.row.questionType == 2">
- <el-button v-for="(item,index) in scope.row.optionsSize" :type="scope.row.standardAnswer?.includes(answerList[index]) ? 'primary': ''" class="selected_button" @click="MultiClick(answerList[index],scope.row)">{{answerList[index]}}</el-button>
-
- </div>
- <!-- 判断题 -->
- <div class="single_selected" v-if="scope.row.questionType == 3">
- <el-button v-for="(item,index) in 2" :type="scope.row.standardAnswer === tflist[index] ? 'primary': ''" class="selected_button" @click="SingleClick(tflist[index],scope.row)">{{tflist[index]}}</el-button>
-
- </div>
- </div>
- </template>
- </el-table-column>
- <el-table-column prop="fullScore" label="满分" width="120">
- <template v-slot="scope">
- <div class="full_mark_input">
- <el-input v-model="scope.row.fullScore" maxlength="3" @input="(val: any) => onScoreInput(scope.row, 'fullScore', val)"></el-input>
- </div>
- </template>
- </el-table-column>
- <el-table-column prop="name" label="多选题给分分布">
- <template v-slot="scope">
- <div v-if="scope.row.questionType == '1' || scope.row.questionType == '3'">
- {{ '-' }}
- </div>
- <div v-if="scope.row.questionType == '2'">
- <div v-if="scope.row.multipleRule == '1'">
- 全对得满分
- <span>,少选得{{ scope.row.multipleRuleDesc || 0 }}分,选错不得分</span>
- </div>
- <div v-if="scope.row.multipleRule == '2'">
- 全对得满分,选对1个得{{ scope.row.multipleRuleDesc?.split(',')[0] }}分
- <span v-if="scope.row.multipleRuleDesc && scope.row.standardAnswer.split(',').length > 2">,选对2个得{{ scope.row.multipleRuleDesc?.split(',')[1] }}分</span>
- <span v-if="scope.row.multipleRuleDesc && scope.row.standardAnswer.split(',').length == 4">,选对3个得{{ scope.row.multipleRuleDesc?.split(',')[2] }}分</span>
- </div>
- <div v-if="scope.row.multipleRule == '3'">
- {{scope.row.multipleRuleDesc}}
- </div>
- </div>
- </template>
- </el-table-column>
- <el-table-column prop="groupName" label="分组">
- <template v-slot="scope">
- <div class="input_width">
- <el-input v-model="scope.row.groupName" maxlength="3" ></el-input>
- </div>
- </template>
- </el-table-column>
- <el-table-column prop="name" label="操作" width="150">
- <template v-slot="scope">
- <div class="ele_button table_row_button">
- <span class="btn_editor" @click="EditorSingle(scope.row)">编辑</span>
- <span class="btn_delete" @click="DeleteSingle(scope.row)">删除</span>
- </div>
- </template>
- </el-table-column>
- </el-table>
- <div class="page_info">
- 共{{tableData.length}}题 总分:{{totalScore}}分
- </div>
- </div>
- <div class="page_button">
- <el-button type="primary" style="width: 200px;" @click="FinishQuestion()">完成</el-button>
- </div>
- </div>
- <ChooseTemplate v-model="showChooseTemplate" :currentTemplateId="currentTemplateId" @success="onTemplateChanged"></ChooseTemplate>
- <EditorQuestion ref="enterStandardEdit" @AnswerEnter="EditorAnswerEnter"></EditorQuestion>
- <ImagePreview v-model="showPaperDialog" :imageList="previewImageList" ></ImagePreview>
- </div>
- </template>
- <script lang="ts" setup>
- import { useExamStore } from '@/store/exam'
- import { useRouter } from 'vue-router'
- import { onMounted,computed ,onUnmounted,ref,nextTick } from 'vue';
- import ChooseTemplate from './components/chooseTemplate.vue';
- import EditorQuestion from './components/EditorQuestion.vue';
- import ImagePreview from './components/ImagePreview.vue'
- import { ElMessageBox,ElMessage } from 'element-plus'
- import { getScanSetCertificate,getScanAnswerPreview,getScanAnswerStates,getSmartQuestionList,getTemplateInfo,setQuestion,saveQuestion,deleteAllQuestion,deleteSingleQuestion,getExamProcess} from '@/api/exam'
- import scanCommon from '@/utils/scanCommon.ts'
- import { useThrottleFn } from '@vueuse/core';
- import { useUserStore } from '@/store/user'
- // 实例化 Store
- const examStore = useExamStore()
- const router = useRouter()
- const userStore = useUserStore()
- // 修改 schoolId 计算属性,直接从 store 获取
- const selectSchoolId = computed(() => userStore.schoolId)
- // 定义题目数据的接口类型
- interface QuestionItem {
- id?: string;
- questionName?: string;
- questionType?: number; // 1:单选, 2:多选, 3:判断
- standardAnswer?: string;
- fullScore?: string | number;
- groupName?: string;
- optionsSize?: number;
- multipleRule?: string;
- multipleRuleDesc?: string;
- [key: string]: any; // 允许其他动态属性
- }
- const queryParams= ref({
- start:'',
- end:'',
- questionType:null,
- fullMark:'',
- groupName:'',//分组名称
- })
- //多选题「少选得分」(questionType==2 时使用),与模板 v-model="shaoxuan" 绑定
- const shaoxuan = ref('')//批量设置多选题给分分布的功能
- const scanText=ref('开始扫描');
- const templateQuestionCount=ref(0);//模板题目总数量
- const templateQuestionStart=ref(1);//模板题目起始编号
- const templateQuestionEnd=ref(0);//模板题目结束编号
- const currentTemplateId=ref(0);//当前模板ID
- const currentTemplateName=ref('');//当前模板名称
- const showChooseTemplate=ref(false);// 弹出选择模板的弹窗
- const tableData = ref<QuestionItem[]>([])
- const tableLoading = ref(false)//表格loading状态
- const enterStandardEdit=ref<HTMLElement>();//标准答案编辑组件实例
- const showPaperDialog=ref(false);//答案预览弹窗
- const previewImageList=ref<any[]>([]);//答案预览图片列表
- const previewAnswerData=ref<any>({});//答案预览数据
- const tableHeight = ref(400)// 表格高度
- const answerList=['A','B','C','D','E','F','G','H','I','J','K']//答案列表
- const tflist=['T','F']//判断题列表
- let uploadUrl=`https://www.k12100.com/teaching/api/v1/ai_exam_scan/upload_multi_img`;//图片上传地址
- let updateCountUrl=`https://www.k12100.com/teaching/api/v1/ai_exam_scan/record_batch_scan_number`;
- //扫描答案
- const scanLoading=ref(false);//开始扫描答案
- let skipCertificateNextScan = false;//未检测到纸张后,下次点击开始扫描时跳过后端证书接口,直接下发扫描仪指令
- const examSubjectCode=ref('');//扫描答案的科目编号
- const showScanAnswer=ref(false);//扫描答案预览
- const imageFilesList=ref<any[]>([]);//扫描答案的图片列表
- const scanAnswerLoading=ref(false);//确定扫描答案loading
- const scanClientConnected=ref(false);//扫描客户端的连接状态
- // 考试科目 ID
- const examSubjectId = computed(() => {
- return examStore.currentExam?.id
- })//计算属性
- const templateId=computed(() => {
- return examStore.currentExam?.templateId
- })//计算属性
- // 题目编号可选范围(从接口返回的 start 到 end)
- const questionRange=computed(() => {
- const start = Number(templateQuestionStart.value) || 1
- const end = Number(templateQuestionEnd.value) || 0
- if (end < start) return []
- const list: number[] = []
- for (let i = start; i <= end; i++) {
- list.push(i)
- }
- return list
- })
- const totalScore=computed(() => {
- if (!tableData.value || tableData.value.length === 0) {
- return 0;
- }
-
- // 累加所有题目的 fullScore,防止值为空或非数字时出错
- const sum = tableData.value.reduce((total, item) => {
- // 确保 fullScore 是有效数字,如果是空字符串或 null 则视为 0
- const score = Number(item.fullScore);
- return total + (isNaN(score) ? 0 : score);
- }, 0);
- return sum;
- })
- // 通用分数输入处理函数
- // target: 需要更新的目标对象 (如 scope.row 或 queryParams)
- // key: 目标对象中的属性名 (如 'fullScore' 或 'fullMark')
- // // val: 当前输入的值
- const onScoreInput = (target: any, key: string, val: any) => {
- // 1. 确保 val 是字符串
- let strVal = String(val || '');
-
- // 2. 正则替换:只保留数字和小数点
- strVal = strVal.replace(/[^\d.]/g, '');
-
- // 3. 逻辑:只允许一个小数点
- const parts = strVal.split('.');
- if (parts.length > 2) {
- strVal = parts[0] + '.' + parts.slice(1).join('');
- }
-
- // 4. 逻辑:限制总长度不超过3
- if (strVal.length > 3) {
- strVal = strVal.slice(0, 3);
- }
-
- // 5. 逻辑:如果不以数字开头且不为空,防止单独输入 '.'
- // 注意:如果允许 "0." 这种输入,则不需要此步,但通常 "0." 后面还得跟数字,这里暂时保留防止非法状态
- if (strVal === '.') {
- strVal = '';
- }
- // 6. 关键步骤:直接修改目标对象的属性
- if (target && key) {
- target[key] = strVal;
- }
- };
- // 组件顶层分数输入 ref 映射:当 obj 传 null 时,按 prop 名写入对应的 ref
- const scoreRefMap: Record<string, any> = { shaoxuan }
- // 少选得分校验(@change 触发):不能超过每题满分
- const ScoreYanzheng = () => {
- const fullMark = parseFloat(String(queryParams.value.fullMark ?? ''))
- const score = parseFloat(String(shaoxuan.value ?? ''))
- if (!isNaN(score) && !isNaN(fullMark) && score >= fullMark) {
- ElMessage.error('少选得分必须小于每题满分')
- shaoxuan.value = ''
- }
- }
- //打开选择模板弹窗
- const OpenChooseTemplate=()=>{
- showChooseTemplate.value=true;
- }
- //模板更换成功回调
- const onTemplateChanged=(templateData: any)=>{
- console.log('模板更换成功', templateData)
- currentTemplateId.value = templateData.id
- currentTemplateName.value = templateData.templateName
- // 更新 store 中的 templateId 并持久化到 localStorage
- examStore.setExamInfo({ ...examStore.currentExam, templateId: templateData.id })
- // 重新获取模板信息(含题目起止范围)并刷新试题列表
- GetTemplateInfo()
- GetSmartQuestionList()
- }
- //切换题目类型
- const TypeChange=(item:any)=>{
- console.log("打印题目类型",item)
-
- }
- //确定扫描答案
- const EnterSubmit=()=>{
- }
- //单选点击事件
- const SingleClick=(item:any,answer:any)=>{
- console.log("打印单选点击",item,answer)
- item.standardAnswer = answer;
- console.log(item);//打印当前标准客户题题列表
- }
- // 多选点击事件
- const MultiClick=(answer:any,item:any)=>{
- console.log("打印多选点击",answer,item)
- // console.log("打印当前questionTable",this.questionTable);
- if(item.standardAnswer==undefined)
- {
- item.standardAnswer='';
- }
- // let answerlist=item.standardAnswer==''?[]:item.standardAnswer?.split(",");
- let answerlist = item.standardAnswer === '' ? [] : (item.standardAnswer || '').replace(/\s+/g, '').split(',').flatMap(item => item === '' ? [] : item.split(''));
- if (!answerlist?.includes(answer))
- {
- answerlist?.push(answer)
- }
- else
- {
- answerlist?.forEach((i,index) => {
- if (i === answer) {
- answerlist?.splice(index,1)
- }
- })
- }
- item.standardAnswer = answerlist?.join(",")
- // // 给当前题选择的答案赋值
- // for(var i=0;i<this.questionTable.length;i++)
- // {
- // if(item.id==this.questionTable[i].id)
- // {
- // this.questionTable[i].standardAnswer=item.standardAnswer;
- // }
- // }
- // console.log("打印当前questionTable",this.questionTable);
- }
- //删除全部 需要二次弹窗确认提示
- const DeleteAll=()=>{
- if(tableData.value.length==0)
- {
- ElMessage.error("您还没有添加题目哦!")
- return
- }
- console.log("打印删除全部")
- ElMessageBox.confirm('确认删除所有题目吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- customClass:'page_dialog',
- }).then(async () => {
- console.log('删除考试')
- const param={
- id:examSubjectId.value,
- };
- const res = await deleteAllQuestion(param)
- if(res.code == 200)
- {
- ElMessage.success("删除成功!")
- GetExamStatus();//获取考试状态
- // GetSmartQuestionList();
- }
- else
- {
- ElMessage.error(res.msg)
- }
- })
- }
- //确定完成
- const FinishQuestion=async ()=>{
- console.log("打印完成")
- if(tableData.value.length==0)
- {
- ElMessage.error("请添加题目!")
- return
- }
- ElMessageBox.confirm('确认完成吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- customClass:'page_dialog',
- }).then(async () => {
- console.log('完成',tableData.value)
- const res = await saveQuestion(tableData.value)
- if(res.code == 200)
- {
- ElMessage.success("保存成功!")
-
- GetExamStatus();//获取考试状态
- }
- else
- {
- ElMessage.error(res.msg)
- }
- })
- }
- //获取考试状态
- const GetExamStatus=async ()=>{
- const param = {
- examSubjectId:examSubjectId.value
- }
- getExamProcess(param).then((res: any) => {
- console.log('打印考试流程状态返回', res)
- if (res.code === 200) {
- const newStateStr = res.data;
- console.log('打印考试流程状态', newStateStr)
- // 替换为 Store Commit/Action
- examStore.setExamState(newStateStr);//设置状态数据
- if(newStateStr.includes(1))
- {
- router.push('/exam/scanList')
- }
- else
- {
- //未完成 不跳转
- GetSmartQuestionList();
- }
- }
- })
- }
- //确定编辑
- const EditorAnswerEnter=async (item:any)=>{
- console.log("打印确定编辑",item)
- let params=[];
- params.push(item);
- const res = await saveQuestion(params)
- if(res.code == 200)
- {
- ElMessage.success("保存成功!")
- GetSmartQuestionList();
- }
- else
- {
- ElMessage.error(res.msg)
- }
- }
- //删除单个题目 需要二次弹窗确认提示
- const DeleteSingle=(item:any)=>{
- console.log("打印删除单个",item)
- ElMessageBox.confirm('确认删除该题目吗?', '提示', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- customClass:'page_dialog',
- type: 'warning'
- }).then(async () => {
- console.log('删除考试')
- const param={
- id:item.id,
- examSubjectId:examSubjectId.value,
- };
- const res = await deleteSingleQuestion(param)
- if(res.code == 200)
- {
- ElMessage.success("删除成功!")
- GetSmartQuestionList();
- }
- else
- {
- ElMessage.error(res.msg);
- }
- })
- }
- const ConfigRequestUrlByEnv= () =>{
- console.log("打印环境", import.meta.env.MODE);
- if (import.meta.env.MODE === 'development')
- {
- //本地开发环境:写死测试环境地址
-
- uploadUrl = `https://index.k12100.net/teaching/api/v1/ai_exam_scan/upload_multi_img`;//图片上传地址
- updateCountUrl = `https://index.k12100.net/teaching/api/v1/ai_exam_scan/record_batch_scan_number`;//更新扫描数量地址
- }
- else
- {
- //线上环境(测试/生产):读取配置的环境变量
-
- uploadUrl = `${import.meta.env.VITE_API_BASE_URL_API}/api/v1/ai_exam_scan/upload_multi_img`;//图片上传地址
- updateCountUrl = `${import.meta.env.VITE_API_BASE_URL_API}/api/v1/ai_exam_scan/record_batch_scan_number`;//更新扫描数量地址
- }
- console.log("打印上传图片地址", uploadUrl);
- console.log("打印更新扫描数量地址", updateCountUrl);
- }
- //编辑单个题目
- const EditorSingle=(item:any)=>{
- console.log("打印编辑单个题目",item)
- enterStandardEdit.value.InitData(item);
- }
- // 打开预览图片
- const OpenImage=()=>{
- let params={
- examSubjectId:examSubjectId.value,
- };
- getScanAnswerPreview(params).then((res:any)=>{
- console.log("打印预览图片返回结果",res);
- if(res.code==200)
- {
- previewImageList.value=[];//初始清空
- res.data.forEach((image:any)=>{
- console.log("image",image);
- let obj={
- url:image,
- pageNo:1,//页面页码
- };
- previewImageList.value.push(obj);
- })
- if(previewImageList.value.length>0)
- {
- showPaperDialog.value=true;
- }
- else
- {
- ElMessage.warning('您还未扫描答案哦!');
-
- }
-
- }
- })
- }
- // 打开扫描仪器
- const GotoScan = useThrottleFn(() => {
- // showSelectType.value = 'scan';
- console.log("打印打开扫描仪器",scanCommon.isClientConnected())
- if (!scanClientConnected.value) {
- ElMessage.warning('请先打开扫描客户端');
- return;
- }
-
- scanLoading.value = true;
- scanText.value='扫描中...';
- //未检测到纸张后的重试:跳过后端证书接口,直接下发扫描仪指令并恢复轮询
- if (skipCertificateNextScan) {
- skipCertificateNextScan = false;
- StartScan();
- ScheduleScanStatePolling();
- return;
- }
- // showScanAnswer.value=true;
- //先调用后端的接口
- const param={
- examSubjectId:examSubjectId.value,
- };
- getScanSetCertificate(param).then((res:any)=>{
- console.log("打印返回结果",res);
- if(res.code==200)
- {
- StartScan();
-
- }
- else
- {
- //后端证书接口失败:复位按钮,允许用户重新点击开始扫描
- scanLoading.value = false;
- scanText.value='开始扫描';
- ElMessage.error(res.msg);
- }
- })
-
- }, 5000);
- // 启动扫描
- const StartScan = () => {
- scanCommon.init(HandleScanResult); // 跟扫描仪插件 websocket 连接
- console.log("打印 VUE_APP_BASE_API", import.meta.env.VITE_APP_BASE_API); // Vue3/Vite 环境变量获取方式
- console.log("打印上传图片地址", uploadUrl);
- //开始扫描
- let jsonParam={
- examSubjectId:examSubjectId.value,
- batchNumber:'9999',//批次号 固定9999 代表扫描答案
- // sensitive: 35,//灵敏度参数 固定35
- settingFlag:1,//设置参数 (0-正常图片 1- 设置题目 以及答案)
- schoolId:selectSchoolId.value//学校id 联校单校都需要学校id
- };
- let json = {
- "action":"startScan",//交互指令参数
- "batchNumber":'9999',//批次号 这里传给客户端的批次号需要进行处理 截取批次id后5位拼接batchNumber
- "subjectCode":examSubjectCode.value,//科目编号
- "paperSchema":1,//this.paperSchema,// 单双面//页面类型 1 单面 2 双面
- "token":localStorage.getItem('token'),//token信息
- "uploadUrl":uploadUrl, // 图片上传地址
- "updateCountUrl":updateCountUrl,//更新扫描纸张数量接口 接口参数 批次id post 请求 id
- "batchId":'10000',//批次号id
- "dpi":150,//dpi参数 设置图片清晰度质量的
- "isColor":1,//是否彩色图片 手阅卡扫描彩色图片 0 黑白 1 彩色
- "useDriveUI":0,//startScan和scanTemplate增加useDriveUI参数,=1时会弹框,其他值或不写则不弹
- "jsonParam":JSON.stringify(jsonParam),// 后端使用的参数 json字符串 后端接口固定三个参数 1:jsonParam 2:seqNumber(这个批次的图片序号:从1开始) 3:file 图片文件
- };//正式版参数
- console.log("打印发送的数据",JSON.stringify(json));
- setTimeout(() => {
- scanCommon.send(JSON.stringify(json))
- },100)
- };
- //扫描状态轮询定时器
- let scanStateTimer: ReturnType<typeof setTimeout> | null = null;
- //停止扫描状态轮询
- const StopScanStatePolling=()=>{
- if(scanStateTimer){
- clearTimeout(scanStateTimer);
- scanStateTimer = null;
- }
- };
- //继续轮询:2秒后再次调用获取扫描答案状态
- const ScheduleScanStatePolling=()=>{
- StopScanStatePolling();//先清除已有定时器,避免重复轮询
- scanStateTimer = setTimeout(()=>{
- GetScanAnswerStates();
- },2000);
- };
- //获取扫描答案状态
- // 轮询查询识别是否完成,每隔2S调用一次,0-表示识别完成,1-上传中 2-识别中 3-失败,其中1和2继续保持loading 0和3可以加载题目
- const GetScanAnswerStates=()=>{
- const param={
- examSubjectId:examSubjectId.value,
- };
- getScanAnswerStates(param).then((res:any)=>{
- console.log("打印获取扫描答案状态",res);
- if(res.code==200)
- {
- //0-表示识别完成,1-上传中 2-识别中 3-失败,其中1和2继续保持loading 0和3可以加载题目
- if(res.data==0)
- {
- //识别完成:结束轮询,加载试题列表
- StopScanStatePolling();
- scanLoading.value = false;
- scanText.value='开始扫描';
-
- GetSmartQuestionList();//获取试题列表
- // showScanAnswer.value = true;
- }
- else if(res.data==1)
- {
- //上传中:保持loading,2秒后继续轮询
- scanLoading.value = true;
- scanText.value='上传中...';
- ScheduleScanStatePolling();
- }
- else if(res.data==2)
- {
- //识别中:保持loading,2秒后继续轮询
- scanLoading.value = true;
- scanText.value='识别中...';
- ScheduleScanStatePolling();
- }
- else if(res.data==3)
- {
- //扫描失败:结束轮询,加载试题列表
- StopScanStatePolling();
- scanLoading.value = false;
- scanText.value='扫描失败';
- ElMessage.error('扫描失败');
- GetSmartQuestionList();//获取试题列表
- }
- }
- else
- {
- //接口异常:结束轮询,避免无限请求
- StopScanStatePolling();
- scanLoading.value = false;
- ElMessage.error(res.msg);
- }
- })
- .catch(()=>{
- //请求异常:结束轮询,避免无限请求
- StopScanStatePolling();
- scanLoading.value = false;
- })
- };
- //关闭扫描答案预览
- const CloseScanAnswer=()=>{
- showScanAnswer.value = false;
- }
- // 4. 计算高度的函数
- const CalculateTableHeight = () => {
- // 获取父容器的高度
- const containerHeight = window.innerHeight
-
- // 减去其他固定元素的高度(需要根据实际布局调整这些数值)
- // 例如:header(64px) + 间距(20px) + 标题区域(约50px) + 底部按钮区域(约60px) + 分页信息(56px)
- // 建议通过浏览器控制台检查实际占用高度
- const offset = 320 // 这是一个预估值,请根据实际 CSS 调整
-
- tableHeight.value = containerHeight - offset
- }
- //获取模板信息
- const GetTemplateInfo=()=>{
- const params={
- templateId:currentTemplateId.value,
- };
- getTemplateInfo(params).then((res) => {
- console.log("打印获取模板信息结果",res)
- if(res.code==200)
- {
- templateQuestionCount.value=res.data.questionEnd;//题目数量
- templateQuestionStart.value=res.data.questionStart;//题目起始编号
- templateQuestionEnd.value=res.data.questionEnd;//题目结束编号
- queryParams.value.end=res.data.questionEnd;//题目结束
- queryParams.value.start=res.data.questionStart;//题目开始
- currentTemplateName.value=res.data.templateName;//当前模板名称
- }
- })
- }
- //获取试题列表
- const GetSmartQuestionList=()=>{
- const params={
- examSubjectId: examSubjectId.value,
- };
- tableLoading.value = true;//开启表格loading
- getSmartQuestionList(params).then((res) => {
- console.log("打印获取试题结构列表结果",res)
-
- if(res.code==200)
- {
- // tableData.value=res.data;
- // 增加空值保护,确保 tableData 始终是数组
- tableData.value = Array.isArray(res.data) ? res.data : [];
- let result={};
- for (const item of tableData.value) {
- // name转数字作为key
- const key = (item.questionName);
- result[key] = item.standardAnswer;
- }
- previewAnswerData.value=result;
- console.log("打印转换后的数据",result)
- }
- }).finally(()=>{
- tableLoading.value = false;//关闭表格loading
- })
- }
- //确定设置题目
- const EnterSetQuestion=()=>{
- console.log("打印提交参数",queryParams.value)
- // 添加判断:end 不能小于 start
- if (queryParams.value.end < queryParams.value.start) {
- ElMessage.error('结束题号不能小于起始题号');
- return;
- }
- const params={
- examSubjectId: examSubjectId.value,//考试科目id
- questionMin: queryParams.value.start,//起始题号
- questionMax: queryParams.value.end,//结束题号
- questionType: queryParams.value.questionType,
- fullScore: queryParams.value.fullMark,
- groupName: queryParams.value.groupName,
- multipleRule: queryParams.value.questionType==2?'1':null,
- multipleRuleDesc: shaoxuan.value,
- };
- setQuestion(params).then((res) => {
- console.log("打印设置题目结果",res)
- if(res.code==200)
- {
- //设置成功 更新列表
- GetSmartQuestionList();
- }
- })
- }
- //扫描结果回调处理函数
- const HandleScanResult=(res:any)=>{
- console.log("打印扫描返回的res", res);
- //未检测到纸张或卡纸(code 502):停止轮询,复位按钮,允许用户重新点击开始扫描(下次直接下发扫描仪指令,不再调后端接口)
- if (res.code === 502) {
- StopScanStatePolling();
- scanLoading.value = false;
- scanText.value='开始扫描';
- skipCertificateNextScan = true;
- ElMessage.error(res.msg || '未检测到纸张或卡纸');
- return;
- }
- if (res.action === 'uploadFinish')
- {
- //扫描仪返回上传结束 开始启动轮询调用状态接口
- scanText.value='上传中...';
- ScheduleScanStatePolling();
-
- }
- }
- onMounted(() => {
-
- if (!examStore.currentExam) {
- console.warn('当前没有选中的考试信息')
- // 可选:如果没有数据,可以重定向回列表页或提示用户
- }
- scanCommon.init(HandleScanResult);
- scanCommon.watchConnection((isOnline, clientVersion) => {
- scanClientConnected.value = isOnline;
- // console.log('扫描客户端连接状态:', isOnline, '版本号:', clientVersion);
- });
- currentTemplateId.value=examStore.currentExam.templateId;
- ConfigRequestUrlByEnv();//配置请求地址
- GetScanAnswerStates();//获取扫描答案状态
- // GetSmartQuestionList();
- GetTemplateInfo();
- // 初始化计算
- nextTick(() => {
- CalculateTableHeight()
- })
- window.addEventListener('resize', CalculateTableHeight)
- })
- onUnmounted(() => {
- // 移除监听,防止内存泄漏
- window.removeEventListener('resize', CalculateTableHeight)
- // 停止扫描状态轮询,防止组件销毁后继续请求
- StopScanStatePolling();
- })
- </script>
-
- <style lang="scss" scoped>
- .detail_main
- {
- height: 100vh; /* 或 100% */
- display: flex;
- flex-direction: column;
- }
- .question_header
- {
- width: 100%;
- height: 64px;
- line-height: 64px;
- background: #FFFFFF;
- border-radius: 10px 10px 10px 10px;
- padding: 0 20px;
- box-sizing: border-box;
- display: flex;
- justify-content: space-between;
- .choose_template
- {
- font-weight: 500;
- font-size: 16px;
- color: #333333;
- .choose_btn
- {
- font-weight: 500;
- font-size: 16px;
- color: #2E64FA;
- margin-left: 10px;
- cursor: pointer;
- }
- }
- }
- .item_step
- {
- width: 20px;
- height: 20px;
- background: #2E64FA;
- border-radius: 50%;
- font-weight: 500;
- font-size: 14px;
- color: #FFFFFF;
- text-align: center;
- line-height: 20px;
- }
- .item_search
- {
- display: flex;
- align-items: center;
- margin-right: 10px;
- .search_name
- {
- width: auto;
- font-weight: 400;
- font-size: 16px;
- color: #666666;
- }
-
- .select_width
- {
- margin-left: 5px;
- margin-right: 5px;
- width: 90px;
- }
- }
- .item_title
- {
- font-weight: 500;
- font-size: 16px;
- color: #333333;
- display: flex;
- justify-content: flex-start;
- align-items: center;
- gap: 4px;
- position: relative;
- .preview_btn
- {
- font-weight: 500;
- font-size: 16px;
- color: #2E64FA;
- cursor: pointer;
- margin-left: 10px;
- }
- .delete_all
- {
- position: absolute;
- right: 0;
- top: 0;
- width: 100px;
- height: 36px;
- display: flex;
- justify-content: flex-end;
- align-items: center;
- .delete_btn
- {
- width: 88px;
- height: 34px;
- line-height: 34px;
- text-align: center;
- border-radius: 4px 4px 4px 4px;
- border: 1px solid #F56C6C;
- font-weight: 500;
- font-size: 14px;
- color: #F56C6C;
- cursor: pointer;
- }
- }
- }
- .page_info
- {
- font-weight: 500;
- font-size: 16px;
- color: #333333;
- text-align: right;
- height: 56px;
- line-height: 56px;
- }
- .page_button
- {
- text-align: center;
- }
- //标准答案录入
- .answer_input {
- width: 100%;
- padding-top: 8px;
- padding-bottom: 8px;
- margin: auto;
- // background-color: red;
- display: flex;
- justify-content: flex-start;
- flex-wrap: wrap;
- height: auto;
- .single_selected {
- width: auto;
- margin: auto;
- display: flex;
- justify-content: flex-start;
- flex-wrap: wrap;
- gap: 8px;
- .selected_button {
- width: 32px;
- height: 32px;
- display: flex;
- justify-content: center;
- align-items: center;
- margin-bottom: 5px;
- padding: 0 !important;
- border-radius: 50%;
- margin: 0;
- }
- }
- }
- </style>
|