赞
踩
习惯了将解析写在代码注释,这里就直接上代码啦,里面用到的bxm-ui3组件库是博主基于element-Plus做的,可以通过npm i bxm-ui3自行安装使用
// 识别方法: // dom 当前识别数据所在区域, questionType 当前点击编辑选择的题目类型(论述题、简答题要用) export const recognitionMethod = (inputText, dom, questionType) => { // 存一份 let newInputText = inputText.trim() let data = { questionContent: '', questionType: '', questionAnalysis: '', answerList: [] } // 解析答案 let { result, newText } = recognitionResult(newInputText) data.questionAnalysis = result || '' // 单选多选题匹配 const regx1 = /(?:^\d+、)?(.*?)\s*[\((]\s*([A-Za-z]*)\s*[\))]\s*([\s\S]+)/ // 填空题匹配 若下划线上无答案,则三个下划线为一个空 const regx2 = /(?:^\d+、)?(.*?)[\_]+\s*/g // const regx2 = /(?:^\d+、)?(.*?)(_{3})+\s*/g // 判断题匹配 含有(√|×|对|错|正确|错误) const regx3 = /(?:^\d+、)?(.*?)\(([√×对错正确错误])\)\s*/ let match = newText.match(regx1) let match2 = newText.match(regx2) let match3 = newText.match(regx3) // 填空题:去根据dom获取出来有下划线的部分即为答案 let underLineList = getUnderlineList(dom, newText) if (match) { // 基本的单选多选 let answer = match[2] || '' let optionsStr = match[3] // 没有答案或者只有一个答案识别为单选,多个答案为多选 if (answer.length === 1 || !answer.length) { data.questionType = '00' } else { data.questionType = '01' } // 单选/多选,有选项 if (optionsStr) { let options = [] let regexOption = /[A-Za-z][.、.]\s*(?:.*?)(\([^)]*\))?(?=[A-Za-z][.、.]|$)/gsu let matchOption = null while((matchOption = regexOption.exec(optionsStr)) !== null) { options.push(matchOption[0].replace(/[A-Za-z][\.、.]\s*/, '') + (matchOption[1] ? matchOption[1] : '')); } if (!options.length) { // 选项 let optionRegx1 = /[A-Za-z](\.|、)/ options = optionsStr.split(optionRegx1).filter(option => { return !['', '.', '、', '.'].includes(option) }) } if (options.length) { options.map((item, index) => { let obj = { answerContent: item, answerOrd: `${index + 1}`, answerRight: false, answerTitle: checkIndex(index) } // 单选 if (data.questionType === '00') { obj.answerRight = (checkIndex(index) === answer || checkIndex(index).toLocaleLowerCase() === answer) ? '0' : false } else { // 多选 let answers = answer.split('') obj.answerRight = (answers.includes(checkIndex(index)) || answers.includes(checkIndex(index).toLocaleLowerCase())) ? '0' : '1' } data.answerList.push(obj) }) } } handleQuestionContent(match[1], newText, data) } else if (match3) { // 判断题 data.questionType = '03' data.questionContent = match3[1] + '()' let answer = match3[2] for(let i = 0; i < 2; i++) { let obj = { answerOrd: `${i + 1}`, answerRight: i === 0 ? ['对', '正确', '√'].includes(answer) ? i : false : ['错', '错误', '×'].includes(answer) ? i : false, answerTitle: i === 0 ? '正确' : '错误' } data.answerList.push(obj) } } else if (underLineList.length || match2) { // 填空题 data.questionType = '02' let { questionContent, answerList } = recognitionPack(newText, underLineList) data.questionContent = questionContent data.answerList = answerList } else { // 简答题/论述题 没有匹配其余的直接处理为论述题或简答题 // 当前点击编辑选择的题目类型如果不是论述题或简答题,就默认设置为简答题 data.questionType = ['04', '06'].includes(questionType) ? questionType : '04' let newStr = '' // 去掉数字、开头 if (/^\d+、/.test(newInputText)) { newStr = newInputText.replace(/^\d+、/, '') } else { newStr = newInputText } // 一共6种可以解读为答案的内容 let resultRegx = /(答:)|(答案:)|(解析:)|(分析:)|(解答:)|(回答:)]/g // 给了解析 if (resultRegx.test(newInputText)) { // ['题干', '第一种', '第二种'.....'最后一个是根据前面某一种分割出来的答案']如果有解析就是正常的8个项 let arr = newStr.split(resultRegx) if (arr.length >= 8) { data.questionContent = arr[0].trim() data.questionAnalysis = arr[7].trim() } else { data.questionContent = newInputText data.questionAnalysis = '' } } else { data.questionAnalysis = '' data.questionContent = newStr.trim() } } return data } // 序号A~Z-----AA~AZ export const checkIndex = (index) => { let imn = Math.floor((index + 1)/26) let remainder = (index + 1) % 26 if(imn === 0 || (imn === 1 && remainder === 0)) { // A~Z return String.fromCharCode(65 + index) }else if((imn > 1 || (imn === 1 && remainder > 0)) && imn <= 26){ // AA、AB...BA...CA~ZZ return (String.fromCharCode(65 + (remainder ? (imn - 1) : (imn - 2))) + String.fromCharCode(65 + (remainder ? (remainder - 1) : 25))) } } // 解析答案 export const recognitionResult = (inputText) => { let result = '' let newText = inputText // 一共6种可以解读为答案的内容 let resultRegx = /(答:)|(答案:)|(解析:)|(分析:)|(解答:)|(回答:)]/g // 给了解析 if (resultRegx.test(inputText)) { // ['题干', '第一种', '第二种'.....'最后一个是根据前面某一种分割出来的答案']如果有解析就是正常的8个项 let arr = inputText.split(resultRegx) newText = arr[0].trim() if (arr.length >= 8) { result = arr[7] } else { result = '' } } return { result, newText } } // 以下为填空题识别相关方法 // 填空题识别 export const recognitionPack = (inputText, underLineList) => { let questionContent = '' let answerList = [] let newStr = /^\d+、/.test(inputText) ? inputText.replace(/^\d+、/, '') : inputText // 这是下划线上有内容 if (underLineList.length) { underLineList.map((item, index) => { let obj = { answerOrd: index + 1, answerMoreSelect: item.answerMoreSelect, answerTitle: `第${index + 1}空答案`, inputVisible: false, inputValue: '', } answerList.push(obj) // 将答案替换成'___' let end = item.underLineStart + item.answerLength // 这里加了三个_,underLineList中剩余的项的unserLineStart都要处理,否则会错位 newStr = newStr.substring(0, item.underLineStart) + '___' + newStr.substring(end) // 处理下一个的unserLineStart if (index < underLineList.length - 1) { handleCheckUnderStart(index, underLineList) } }) questionContent = newStr } else { // 这是下划线上没有内容,至少三个连续的_才识别成填空题,避免部分单词识别错误,例如COMMENT_NODE // 找到下划线 let underRegx = /(_{3})+/g // let underRegx = /[\_]+/g let understrArr = newStr.match(underRegx) || [] for (let i = 0; i < understrArr.length; i++) { // 将_替换成'___' let start = newStr.indexOf(understrArr[i]) let end = start + understrArr[i].length newStr = newStr.substring(0, start) + '___' + newStr.substring(end) } questionContent = newStr let index = 0 while(index < understrArr.length) { answerList.push({ answerOrd: `${index + 1}`, answerMoreSelect: [], answerTitle: `第${index + 1}空答案`, inputVisible: false, inputValue: '' }) index++ } } return { questionContent, answerList } } // 判断节点是否有下划线样式 function isLeafWithUnderline(node) { if (node.nodeType === Node.TEXT_NODE) { return false } let style = window.getComputedStyle(node) // textDecoration含有underline的一定有下划线 return style.textDecoration && style.textDecoration.includes('underline') } // 递归获取到最深层叶子节点,遇到有下划线的节点直接视为叶子节点 function findDeepestNodes(node, deepestNodes = []) { // 注释节点 if (node.nodeType === Node.COMMENT_NODE) { return deepestNodes } // 如果当前节点是文本节点或者具有下划线样式,认为是叶子节点 if (node.nodeType === Node.TEXT_NODE || isLeafWithUnderline(node)) { deepestNodes.push(node) return deepestNodes // 返回当前节点,不再深入遍历其子节点 } // 遍历当前节点的所有子节点 for (let child of node.childNodes) { findDeepestNodes(child, deepestNodes) } return deepestNodes } // 获取下划线列表 export const getUnderlineList = (dom, newText) => { let allTextNodes = findDeepestNodes(dom) let list = [] let fullText = '' // 找到下划线标签进行数据处理 for(let index = 0; index < allTextNodes.length; index++) { let node = allTextNodes[index] // 文本节点获取内容和样式是不一样的 let style = node.nodeType === Node.TEXT_NODE ? {} : window.getComputedStyle(node) fullText += !node?.innerText ? node.textContent : node.innerText // 去掉数字开头 fullText = /^\d+、/.test(fullText) ? fullText.replace(/^\d+、/, '') : fullText // 有下划线的把下划线内容记录下来,下划线位置记录下来 if (style?.textDecoration && style?.textDecoration.includes('underline') && node.innerText !== '') { let obj = { answerMoreSelect: node.innerText, answerTitle: `第${index + 1}空答案`, answerLength: node.innerText.length, // 答案长度 underLineStart: fullText.length - node.innerText.length } list.push(obj) } } // 处理下划线连在一起但是为u标签时,要合并成一个空 if (list.length) { for(let i = 0; i < list.length; i++) { // 连续的下划线: if (i > 0 && list[i].underLineStart === list[i - 1].underLineStart + list[i - 1].answerLength) { list[i - 1] = { answerMoreSelect: list[i - 1].answerMoreSelect + list[i].answerMoreSelect, // 上一个的文本与当前文本组合 answerTitle: `第${i}空答案`, // 只留前一个,所以下标是前一个的 answerLength: list[i - 1].answerLength + list[i].answerLength, // 上一个的文本长度与当前文本长度之和 underLineStart: list[i - 1].underLineStart // 上一个文本的起始位置就是最终的起始位置 } list.splice(i, 1) i-- } } } return list } // 获取增加或减少了多少长度 export const getChangeLen = (curUnderIndex, underList) => { let addLen = 0 // 遍历当前以及之前的 for(let i = 0; i <= curUnderIndex; i++) { // 当前下划线文本超出了下划线3个字符的长度,替换成3个下划线之后会少了 answerLength-3 的长度,后面的都需要往前移动answerLength-3个位置 // 当前下划线文本少于下划线3个字符的长度,替换成3个下划线之后会多了 3-answerLength 的长度,后面的都需要往后移动3-answerLength个位置 if (underList[i].answerLength !== 3) { addLen += 3 - underList[i].answerLength // 变化的量可能正可能负 } } return addLen } // 处理下划线起始位置 export const handleCheckUnderStart = (curUnderIndex, underList) => { if (curUnderIndex >= underList.length - 1) return // 获取需要变动的数量 let changeLen = getChangeLen(curUnderIndex, underList) // 处理当前的后一个即可 underList[curUnderIndex + 1].underLineStart += changeLen } // 处理选择题的题干,获取到答案并更新选项(题干中有多处为答案或者由多处括号,括号里是字母但不一定是答案的情况) export const handleQuestionContent = (content, allText, data) => { if (!content || !allText) return '' let successContent = '' // 去掉数字开头 let newTextAll = allText.replace(/^\d+[.、.]\s*/, '') // 找到传入的题干在所有字符串中的位置 let contentIndex = newTextAll.indexOf(content) // 截取选项之前的内容比对 let regx1 = /^(.*?)(?=\s*[A-Za-z]\.?[.、.])/s let regx2 = /^(.*?)(?=[A-Za-z](?:(?:\s*\.\s*)|(?:\s*,\s*)|$))/s let matchArr1 = newTextAll.match(regx1) let matchArr2 = newTextAll.match(regx2) let matchArr = [] if (matchArr1 && matchArr2) { // 两个都匹配比较谁匹配更接近 matchArr = matchArr1[0].length > matchArr2[0].length ? matchArr1 : matchArr2 } else if (matchArr1 || matchArr2) { // 有一个不能匹配直接获取能匹配那个 matchArr = matchArr1 ? matchArr1 : matchArr2 } else { matchArr = null } // 已有的题干和真正的不同,需要对已有信息进行修改 if (matchArr && matchArr.length > 0 && matchArr[0] !== content) { // 选项之前的内容 successContent = matchArr[0] // 去掉空行 successContent = successContent.replace(/(\r?\n\s*)+/g, '\n') let answers = data.answerList.map(item => { return item.answerTitle }) // 从括号中找到真正的答案 let answerKeyRegex = /[\((]\s*([A-Z]+)\s*[\))]/g let contentArr = successContent.split(answerKeyRegex) let resultContent = '' let successAnswerArr = [] contentArr.map(item => { let regxAnswer = /^[A-Za-z]+$/g // 仅为大小写字母 if (regxAnswer.test(item)) { // 只有一个字母,并且字母在已生成的选项中,说明是其中的一个答案 if (item.length === 1 && answers.includes(item.toLocaleUpperCase())) { // 替换成括号 resultContent += '()' // 记录出真正的答案,在最后去编辑选项设置选中 !successAnswerArr.includes(item.toLocaleUpperCase()) && successAnswerArr.push(item.toLocaleUpperCase()) } else if (item.length > 1) { /** * 多个字母需要判断: * 1.字母有重复说明不是答案,直接还原 * 2.字母不重复但是有字母不在已生成的选项中,直接还原 * 3.字母不重复并且都在选项中为答案,同时将data中的试题类型修改为多选,选项默认选中项需要更改 */ let itemArr = item.split('').filter(val => { return val !== '' }) let newArr = [...new Set(JSON.parse(JSON.stringify(itemArr)))] if (itemArr.length !== newArr.length) { // 条件1 resultContent += `(${item})` } else { let isInner = true for(let i = 0; i < newArr.length; i++) { newArr[i] = newArr[i].toLocaleUpperCase() if (!answers.includes(newArr[i])) { // 条件2 resultContent += `(${item})` isInner = false break // 退出循环 } } // 条件3,记录正确选项 if (isInner) { // 替换成括号 resultContent += '()' // 记录不重复的答案 successAnswerArr = [...new Set(successAnswerArr.concat(itemArr))] } } } else { // 还原 resultContent += `(${item})` } } else { resultContent += item } }) // 更新题干 data.questionContent = resultContent // 更新试题类型 if (successAnswerArr.length > 1) { data.questionType = '01' } else { data.questionType = '00' } // 处理选项 data.answerList.map((item, index) => { // 当前项为答案要默认选中 if (successAnswerArr.includes(item.answerTitle)) { item.answerRight = data.questionType === '00' ? index : '0' } else { item.answerRight = data.questionType === '00' ? false : '1' } }) } else { data.questionContent = content + '()' } }
这是我简单自定义的一个编辑器,其实是一个contenteditable的div,对里面内容进行简单处理了之后就可以使用了
<template> <div class="custom-editor" :style="{ height: height + 'px' }"> <div class="custom-editor-placeholder" :style="{ display: content ? 'none' : 'block' }">{{ placeholder }}</div> <div class="custom-editor-content" id="cusEditor" :contenteditable="!disabled"> </div> </div> </template> <script setup> import { onMounted, ref } from 'vue' const props = defineProps({ height: { type: Number, default: 300 }, disabled: { type: Boolean, default: false }, placeholder: { type: String, default: '' } }) let content = ref('') let customEditor = ref(null) const emits = defineEmits(['change']) onMounted(() => { customEditor.value = document.getElementById('cusEditor') customEditor.value.addEventListener('input', (e) => { content.value = e.target.innerText emits('change', customEditor.value.innerText) }) // 自定义粘贴,去掉图片,更改文字颜色(匹配系统颜色) customEditor.value.addEventListener('paste', async (e) => { e.preventDefault() let htmlContent = '' // 尝试从现代API获取HTML内容 if (e.clipboardData && e.clipboardData.types.includes('text/html')) { htmlContent = e.clipboardData.getData('text/html') } else if (e.originalEvent && e.originalEvent.clipboardData && e.originalEvent.clipboardData.getData) { htmlContent = e.originalEvent.clipboardData.getData('text/html') } else { htmlContent = (e.clipboardData || window.clipboardData).getData('text') } // 获取粘贴的纯文本,便于后面比较,避免粘贴内容不全 let pasteText = (e?.clipboardData || window?.clipboardData)?.getData('text') // 保存当前的选区 const selection = window.getSelection() const range = selection.getRangeAt(0) // 使用DOMParser解析粘贴的HTML内容 const parser = new DOMParser() const doc = parser.parseFromString(htmlContent, 'text/html') /** 重要 * 处理文本节点,一定要替换掉font节点, * 因为font节点获取内容会包括了css样式(比如字体、颜色、大小等等)转换成字符串的结果 * 无论是innerText还是textContent都是一样的结果,严重影响填空题识别 */ walkTree(doc.body) // ********重要********* // 直接创建一个div存放,现在无法找到又能在同一行又能保留原先样式粘贴进去, // 要在原有文字后面直接挨着来需要清除文字样式,会导致选择题无法识别 let div = document.createElement('div') let childNodes = doc.body.childNodes childNodes.forEach(node => { if (![Node.ATTRIBUTE_NODE, Node.COMMENT_NODE, Node.DOCUMENT_TYPE_NODE, Node.DOCUMENT_FRAGMENT_NODE].includes(node.nodeType)) { div.appendChild(node) } }) // 移除所有的img标签 const imgs = div.querySelectorAll('img') imgs.forEach(img => img.remove()) // 更改文字样式,匹配系统颜色 setBodyTextStyle(div, 'var(--el-text-color)', '12px', 'transparent') // 粘贴内容不全时进行修正 if (pasteText && div.innerText !== pasteText) { div.innerText = pasteText } // 在原有位置插入处理过的内容 range.deleteContents() // 如果要替换选中内容,则先删除 range.insertNode(div) // 插入编辑器 range.collapse(true) selection.removeAllRanges() selection.addRange(range) content.value = customEditor.value.innerText emits('change', customEditor.value.innerText) }) }) // 设置文字颜色以及文字大小,匹配系统颜色 const setBodyTextStyle = (body, color, fontSize, bgc) => { // 创建一个递归函数来遍历并设置颜色 function setColorRecursively(element) { if (element.nodeType === Node.ELEMENT_NODE) { // 如果是元素节点 for (let i = 0; i < element.childNodes.length; i++) { setColorRecursively(element.childNodes[i]) } // 设置当前元素的文本颜色 if (element.style) { element.style.color = color element.style.fontSize = fontSize element.style.backgroundColor = bgc element.style.padding = 0 element.style.margin = 0 element.style.lineHeight = 20 + 'px' } } else if (element.nodeType === Node.TEXT_NODE) { // 如果是文本节点,查找其父元素并设置颜色 if (element.parentElement.style) { element.parentElement.style.color = color element.parentElement.style.fontSize = fontSize element.parentElement.style.backgroundColor = bgc element.parentElement.style.padding = 0 element.parentElement.style.margin = 0 element.parentElement.style.lineHeight = 20 + 'px' } } } // 从body开始遍历 setColorRecursively(body) } // 清理文本节点,并转换所有非span元素的文本节点为span,比如是font const walkTree = (node) => { if (node.nodeType === Node.TEXT_NODE && node.tagName === 'FONT') { var span = document.createElement('span') while (node.firstChild) { span.appendChild(node.firstChild) } node.parentNode.replaceChild(span, node) } else if (node.nodeType === Node.ELEMENT_NODE) { for (var i = 0; i < node.childNodes.length; i++) { walkTree(node.childNodes[i]) } } } const clear = () => { content.value = '' customEditor.value.innerText = '' emits('change', customEditor.value.innerText) } defineExpose({ customEditor, clear }) </script> <style lang="scss" scoped> .custom-editor { position: relative; width: 100%; padding: 16px; z-index: 10000; .custom-editor-placeholder { position: absolute; top: 16px; left: 16px; color: var(--el-text-color-placeholder); opacity: .5; font-size: 13px; font-size: SourceHanSansCN Regular; z-index: 10001; line-height: 23px; } .custom-editor-content { position: relative; width: 100%; height: 100%; overflow-y: auto; outline: none; border: none; box-shadow: none; z-index: 10002; line-height: 23px; } } span { font-size: 12px; font-family: SourceHanSansCN Regular; } </style>
组件使用示例
<div class="text-title"> <span>输入区</span> <div> <bxm-button soplain :disabled="btnDisabled || !inputText" @click="handleClear"> <i class="bxm-icon-fail btn-icon"></i> 清 空 </bxm-button> <bxm-button type="primary" plain :disabled="btnDisabled || !inputText" @click="handleRecognition"> <i class="bxm-icon-switch btn-icon"></i> 识 别 </bxm-button> </div> </div> <CustomEditor :data="inputText" ref="editor" :disabled="btnDisabled" :height="600" style="margin-top: 16px" placeholder="请将试题粘贴在此处,点击识别,系统将自动解析题干及选项。" @change="(val) => { inputText = val }"> </CustomEditor>
// 识别
const handleRecognition = () => {
let data = recognitionMethod(inputText.value, editor.value.customEditor, props.questionType)
formDataText.value.bxmAnswerList = JSON.parse(JSON.stringify(data.answerList || []))
formDataText.value.bxmQuestionDetail.questionContent = data.questionContent
formDataText.value.bxmQuestionDetail.questionType = data.questionType
formDataText.value.bxmQuestionDetail.questionAnalysis = data.questionAnalysis
}
const handleClear = () => {
editor.value && editor.value.clear()
}
自己做的试题编辑的组件
<!--根据最新ui设计写的试题编辑--> <template> <el-form class="edit-question-box" :model="formData" :disabled="disabled || importLoading" ref="ruleForm" label-width="100px" @submit.native.prevent> <div class="tips one-line" v-if="['02'].includes(formData.bxmQuestionDetail.questionType)"> <i class="bxm-icon-info tip-icon"></i> 提示:填空用连续三个下划线"_"表示,1个填空题最多设置5个空,若一个空有多个参考答案,匹配任意一个都算正确。 </div> <el-form-item prop="bxmQuestionDetail.questionContent" :key="getUniqueCode()" :rules="[{ required: true, message: '请填写题干', trigger: 'blur' }]"> <template #label> <div v-if="canChangeType && !qustionId" class="questionContent-custom-label" style="width: 100%"> <el-dropdown trigger="click" size="mini" :disabled="disabled || importLoading" @command="handlequestionTypeChange($event, '00')"> <bxm-tag type="primary" plain style="cursor: pointer"> 【{{ title }}】 </bxm-tag> <template #dropdown> <el-dropdown-menu> <el-dropdown-item v-for="item in questionTypeList" :key="item.key" :command="item.value"> {{ item.key }}题 </el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </div> <template v-else>【{{ title }}】</template> </template> <el-input v-model="formData.bxmQuestionDetail.questionContent" type="textarea" :rows="3" placeholder="请输入题干"> </el-input> </el-form-item> <el-form-item label="【图片】" prop="fileList"> <div class="uplod-box"> <el-upload ref="upload" v-model:file-list:="formData.fileList" action="action" :multiple="true" :auto-upload="false" list-type="picture" :show-file-list="false" accept=".jpeg,.jpg,.png" :disabled="disabled || importLoading" :on-change="handleImageChange" :on-preview="handlePictureCardPreview" :on-remove="handleRemove"> <bxm-button type="primary" :loading="importLoading" :disabled="disabled || importLoading" icon="Upload"> 选择文件 </bxm-button> <template #tip> <div class="el-upload__tip"> 支持上传多个jpeg、jpg、png文件,单个文件不超过10M。 </div> </template> </el-upload> <!-- upload无法回显 自己画一个回显 --> <ul class="img-box"> <li v-for="(file, index) in formData.fileList" :key="index + 'fileList'" class="img-item"> <img :src="file.url" alt=""> <div class="item-name" @click="handlePictureCardPreview(file)"> <el-icon class="item-name-icon"> <Document /> </el-icon> <span class="item-name-label">{{ file.fileName }}</span> </div> <el-icon v-if="!(disabled || importLoading)" class="item-close" @click="handleRemove(file)"> <Close /> </el-icon> </li> </ul> </div> </el-form-item> <div class="edit-question-content"> <!-- 单选/多选 --> <template v-if="['00', '01'].includes(formData.bxmQuestionDetail.questionType)"> <el-form-item v-for="(item, index) in formData.bxmAnswerList" :key="index + getUniqueCode()" :prop="`formData.bxmAnswerList.${index}.answerContent`" :rules="[{ required: false, validate: (rule, value, callback) => handleValidContent(callback, index), trigger: 'blur' }]"> <template #label> <div class="question-custom-label"> <svg-icon icon-class="sort" class="label-icon"></svg-icon> <span class="label-title">{{ item.answerTitle }}.</span> </div> </template> <el-input v-model.trim="item.answerContent" clearable placeholder="请输入选项内容" maxlength="50" show-word-limit style="width: 50%; margin-right: 10px;"> </el-input> <!-- 单选 --> <template v-if="['00'].includes(formData.bxmQuestionDetail.questionType)"> <el-radio v-model="item.answerRight" :label="index" @change="changeAnswerRight($event, index)"> </el-radio> </template> <!-- 多选 --> <template v-else> <el-checkbox v-model="item.answerRight" true-label="0" false-label="1" :disabled="disabled"> </el-checkbox> </template> <div class="set-answer"> <span class="set-answer-title" v-if="showResult(item, index)">设为答案</span> </div> <!-- 操作按钮 --> <div class="answer-btn-box"> <template v-if="index > 0 && formData.bxmAnswerList.length > 1"> <el-tooltip content="上移" placement="top"> <bxm-button icon="Top" link type="primary" @click="upAnswer(index)"> </bxm-button> </el-tooltip> <el-divider direction="vertical" style="margin-left: 2px;"></el-divider> </template> <template v-if="index < formData.bxmAnswerList.length - 1 && formData.bxmAnswerList.length > 1"> <el-tooltip content="下移" placement="top"> <bxm-button icon="Bottom" link type="primary" @click="downAnswer(index)"> </bxm-button> </el-tooltip> <el-divider direction="vertical" style="margin-left: 2px;"></el-divider> </template> <el-tooltip content="删除" placement="top"> <bxm-button icon="Delete" link type="primary" @click="delAnswer(index)"> </bxm-button> </el-tooltip> </div> </el-form-item> </template> <!-- 填空 --> <template v-else-if="['02'].includes(formData.bxmQuestionDetail.questionType)"> <el-form-item v-for="(item, index) in formData.bxmAnswerList" :key="index + getUniqueCode()" :prop="`formData.bxmAnswerList.${index}.answerContent`" :rules="[{ required: false, validate: (rule, value, callback) => handleValidContent(callback, index), trigger: 'change' }]"> <template #label> <div class="question-custom-label"> <svg-icon icon-class="sort" class="label-icon"></svg-icon> <span class="label-title">{{ index + 1 }}.</span> </div> </template> <div class="pack-input-box"> <el-tag v-for="(tag, tagIndex) in item.answerMoreSelect" :key="tag" type="info" :closable="!disabled" :disable-transitions="false" style="margin: 2px 4px;" @close="handleCloseTag(tag, index, tagIndex)"> <el-tooltip v-if="tag.length > 10" :content="tag" placement="top"> {{ tag.slice(0, 10) }}... </el-tooltip> <template v-else>{{ tag }}</template> </el-tag> <el-input v-if="item.inputVisible" v-model.trim="item.inputValue" :ref="`saveTagInput${index}`" class="input-new-tag" style="height: 25px" @keyup.enter.native="handleInputConfirm(index)" @blur="handleInputConfirm(index)"> </el-input> <el-tooltip v-else content="新增" placement="top"> <bxm-button icon="Plus" type="primary" link style="margin-left: 10px" @click="showInput(index)"> </bxm-button> </el-tooltip> </div> <el-tooltip content="删除" placement="top"> <bxm-button type="primary" icon="delete" link style="margin-left: 10px" @click="delAnswer02(index)"> </bxm-button> </el-tooltip> </el-form-item> </template> <!-- 判断 --> <template v-else-if="['03'].includes(formData.bxmQuestionDetail.questionType)"> <el-form-item> <el-radio v-model="item.answerRight" v-for="(item, index) in formData.bxmAnswerList" :key="index" :label="index" style="margin-left: 16px" @change="changeAnswerRight($event, index)"> {{ item.answerTitle }} <el-icon style="margin-left: 5px"> <Check v-if="item.answerTitle === '正确'" /> <Close v-else /> </el-icon> </el-radio> </el-form-item> </template> </div> <!-- 添加按钮 --> <bxm-button v-if="['00', '01'].includes(formData.bxmQuestionDetail.questionType)" type="primary" link icon="Plus" class="radio-add-btn" @click="addAnswer(formData.bxmAnswerList.length - 1)"> 添加选项 </bxm-button> <bxm-button v-if="['02'].includes(formData.bxmQuestionDetail.questionType)" type="primary" link icon="Plus" class="radio-add-btn" @click="addAnswer02"> 添加答案 </bxm-button> <div v-if="!['04', '06'].includes(formData.bxmQuestionDetail.questionType)" class="dash-line"></div> <div class="edit-question-bottom"> <el-form-item v-if="!['04', '06'].includes(formData.bxmQuestionDetail.questionType)" label="答案:" style="margin-bottom: 8px"> <template v-if="['00', '01', '03'].includes(formData.bxmQuestionDetail.questionType)"> {{ selectedAnswer }} <el-icon style="margin-left: 5px" v-if="formData.bxmQuestionDetail.questionType === '03'"> <Check v-if="selectedAnswer === '正确'" /> <Close v-else-if="selectedAnswer === '错误'" /> </el-icon> </template> <template v-else> <span v-for="(item, index) in formData.bxmAnswerList" :key="index + getUniqueCode()"> <span class="p-lr-5">{{ index + 1 }}.</span> <span v-for="(val, valIndex) in item.answerMoreSelect" :key="valIndex + 'span'"> <span class="answer-span p-lr-5"> {{ val }} </span> <span v-if="valIndex !== item.answerMoreSelect.length - 1" class="p-lr-5"> / </span> </span> </span> </template> </el-form-item> <el-form-item label="解析:" props="questionAnalysis" :key="getUniqueCode()"> <el-input v-model="formData.bxmQuestionDetail.questionAnalysis" type="textarea" :rows="8" class="question-content-input" placeholder="请输入解析"> </el-input> </el-form-item> </div> </el-form> </template> <script setup> import { ref, reactive, onMounted, watch, nextTick, computed, onBeforeMount } from 'vue' import { BxmMessage, BxmMessageBox } from 'bxm-ui3' // 下面几个方法就自己写写吧 import { validateIsNull } from 'utils/validate' import { findItemByValue } from '../../consts/index' import { checkIndex } from '../consts/index' const props = defineProps({ questionType: { type: String, default: '00' }, disabled: { type: Boolean, default: false }, data: { type: Object, default: () => { return {} } }, qustionId: { type: [String, Number], default: '' }, // 是否能够更改试题类型 canChangeType: { type: Boolean, default: false } }) let formData = ref({ fileList: [], bxmQuestionDetail: { questBankId: '', questionAnalysis: '', questionContent: '', questionType: '', }, bxmAnswerList: [ { answerContent: '', answerOrd: '1', answerRight: false, answerTitle: 'A', questDetailId: '' } ] }) let questionTypeList = reactive([ { value: '00', key: '单选', disabled: false }, { value: '01', key: '多选', disabled: false }, { value: '02', key: '填空', disabled: false }, { value: '03', key: '判断', disabled: false }, { value: '04', key: '简答', disabled: false }, { value: '06', key: '论述', disabled: false } ]) const ruleForm = ref(null) let resultFileList = reactive([]) let importLoading = ref(false) let dialogImage = ref(false) let currentIndex = ref(0) let upload = ref(null) const emits = defineEmits(['change', 'importChange']) const showResult = computed(() => { return (data, index) => { // 单选时 if (props.questionType === '00') { return data.answerRight === index } else { return data.answerRight === '0' || formData.value.bxmAnswerList[index].answerRight === '0' } } }) const selectedAnswer = computed(() => { let result = '' if (props.questionType === '03') { formData.value.bxmAnswerList.map(item => { if (item.answerRight !== false) { result = item.answerTitle } }) } else if (['00', '01'].includes(props.questionType)) { let filterList = [] if (props.questionType === '01') { filterList = formData.value.bxmAnswerList.filter(item => { return item.answerRight && item.answerRight !== '1' }) || [] } else { filterList = formData.value.bxmAnswerList.filter((item, index) => { return item.answerRight === index }) || [] } result = filterList.map(item => { return item.answerTitle }).join('、') } return result }) const title = computed(() => { return findItemByValue(questionTypeList, formData.value.bxmQuestionDetail.questionType).key + '题' }) const handleValidContent = (callback, index) => { if (['00', '01'].includes(props.questionType)) { let curValue = formData.value.bxmAnswerList[index].answerContent if (!curValue) { return callback('请填写选项内容') } if (curValue.length > 50) { return callback(`选项${checkIndex(index)}内容长度超出50,请修改`) } let list = formData.value.bxmAnswerList.filter(item => { return item.answerContent === curValue }) if (list.length > 1) { return callback('选项不可重复') } } else if (['02'].includes(props.questionType)) { let curAnswer = formData.value.bxmAnswerList[index].answerMoreSelect let list = Array.isArray(curAnswer) && curAnswer.length ? curAnswer : curAnswer.split(',') let newList = list.filter(item => { return item === formData.value.bxmAnswerList[index].inputValue }) if (newList > 0) { return callback('同一空答案不可重复') } } return callback() } // 处理数据 const handleFormData = (data) => { nextTick(() => { formData.value.bxmQuestionDetail = Object.assign({}, data.bxmQuestionDetail) let bxmAnswers = JSON.parse(JSON.stringify(data.bxmAnswerList ? data.bxmAnswerList : data.bxmAnswers)) for (const val of bxmAnswers) { val.answerOrd = parseInt(val.answerOrd) if (['00', '03'].includes(formData.value.bxmQuestionDetail.questionType)) { if (val.answerRight === '0' || val.answerRight === val.answerOrd - 1) { // 为答案 val.answerRight !== val.answerOrd - 1 && (val.answerRight = val.answerOrd - 1) } else { val.answerRight = false } } else if (formData.value.bxmQuestionDetail.questionType === '02') { val.answerMoreSelect = Array.isArray(val.answerMoreSelect) ? val.answerMoreSelect : val.answerMoreSelect.split(',') val.inputVisible = false val.inputValue = '' // 此处用map更新没有用for实时 for(let i = 0; i < val.answerMoreSelect.length; i++) { val.answerMoreSelect[i] = val.answerMoreSelect[i].trim() } } // 去除选项、填空答案前后空格 if (val.answerContent) { val.answerContent = val.answerContent.trim() } } // 判断题如果没有答案加上默认的 if (!bxmAnswers.length && formData.value.bxmQuestionDetail.questionType === '03') { bxmAnswers = [ { answerOrd: '1', answerRight: false, answerTitle: '正确' }, { answerOrd: '2', answerRight: false, answerTitle: '错误' } ] } formData.value.bxmAnswerList = JSON.parse(JSON.stringify(bxmAnswers)) // 文件列表处理 formData.value.fileList = [] resultFileList = [] if (Array.isArray(data.fileList) && data.fileList.length) { data.fileList.map(item => { item.url = window.location.origin + '/' + item.filePath; // isOnline: 是否是编辑时后端直接返回的图片 formData.value.fileList.push({ ...item, isOnline: true }) // 存储数据 resultFileList.push({ isDelete: false, fileName: item.fileName, filePath: item.filePath, isOnline: true }) }) } }) } // 类型变化 const handlequestionTypeChange = (val, type) => { if (val === formData.value.bxmQuestionDetail.questionType) { return false } if (type === '00') { // 单选/多选相互切换时,加是否保留选项提示 if (['00', '01'].includes(formData.value.bxmQuestionDetail.questionType) && ['00', '01'].includes(val)) { BxmMessageBox.confirm('确认更改试题类型?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { formData.value.bxmQuestionDetail.questionType = val BxmMessageBox.confirm('是否保留选项信息,保留时若为多选切换为单选将只保留第一个选中项为答案,若不保留将清空选项信息', '提示', { confirmButtonText: '保留选项', cancelButtonText: '清空选项', type: 'warning' }).then(() => { let selAnswer = formData.value.bxmAnswerList.filter((item, index) => { return val === '00' ? item.answerRight === '0' : item.answerRight === index }) let selAnswerOrds = selAnswer.map(item => { return item.answerOrd }) formData.value.bxmAnswerList.map((item, index) => { // 多选切换为单选 if (val === '00') { selAnswerOrds = selAnswerOrds.length > 1 ? [selAnswerOrds[0]] : selAnswerOrds item.answerRight = selAnswerOrds.includes(item.answerOrd) ? index : false } else { // 单选切换为多选 item.answerRight = selAnswerOrds.includes(item.answerOrd) ? '0' : '1' } }) }).catch(() => { setAnswerData() }) }).catch(() => { }) } else { BxmMessageBox.confirm('切换试题类型将只保留题干信息,是否继续?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { formData.value.bxmQuestionDetail.questionType = val setAnswerData() }).catch(() => { }) } } else { formData.value.bxmQuestionDetail.questionType = val setAnswerData() } } // 设置答案数据 const setAnswerData = () => { // 判断 if (formData.value.bxmQuestionDetail.questionType === '03') { formData.value.bxmAnswerList = [ { answerOrd: '1', answerRight: false, answerTitle: '正确' }, { answerOrd: '2', answerRight: false, answerTitle: '错误' } ] } else if (formData.value.bxmQuestionDetail.questionType === '02') { // 填空 formData.value.bxmAnswerList = [{ answerOrd: '1', answerMoreSelect: [], answerTitle: '第1空答案', inputVisible: false, inputValue: '' }] } else if (formData.value.bxmQuestionDetail.questionType === '01') { // 多选 formData.value.bxmAnswerList = [{ answerContent: '', answerOrd: '1', answerRight: '1', answerTitle: 'A', questDetailId: '' }] } else if (formData.value.bxmQuestionDetail.questionType === '00') { // 单选 formData.value.bxmAnswerList = [{ answerContent: '', answerOrd: '1', answerRight: false, answerTitle: 'A', questDetailId: '' }] } } watch(() => props.questionType, (val) => { handlequestionTypeChange(val) }, { immediate: true, deep: true }) watch(() => props.data, (obj) => { handleFormData(Object.assign({}, obj)) }, { immediate: true, deep: true }) watch(() => importLoading.value, (val) => { emits('importChange', val) }, { immediate: true, deep: true }) // 处理文件删除 const handleBatchDelFile = async (type) => { if (!resultFileList.length) { return } let list = [] if (type === '00') { // 点击的取消按钮 if (!props.qustionId) { // 新增 // 删除全部文件 list = resultFileList } else { // 编辑 // 删除不是后端返回的文件 list = resultFileList.filter(item => { return item.isOnline === false }) } } else { // 点的确定 // 删除用户点过删除的文件 list = resultFileList.filter(item => { return item.isDelete === true }) } if (list.length) { let params = { filePathList: list.map(item => { return item.filePath }) } await deleteFileList(params).catch(() => {}) } } // 当前项往下增加一项 const addAnswer = (index) => { formData.value.bxmAnswerList.splice(index + 1, 0, { answerContent: '', answerOrd: '', answerRight: formData.value.bxmQuestionDetail.questionType === '00' ? false : '1', answerTitle: '', questDetailId: '' }) for (const index in formData.value.bxmAnswerList) { const val = formData.value.bxmAnswerList[index] val.answerTitle = checkIndex(parseInt(index)) val.answerOrd = parseInt(index) + 1 } } // 将当前项往上提一个 const upAnswer = (index) => { if (index !== 0) { formData.value.bxmAnswerList[index] = formData.value.bxmAnswerList.splice(index - 1, 1, formData.value.bxmAnswerList[index])[0]; for (const index in formData.value.bxmAnswerList) { const val = formData.value.bxmAnswerList[index] val.answerTitle = checkIndex(parseInt(index)) val.answerOrd = parseInt(index) + 1 if (formData.value.bxmQuestionDetail.questionType === '00' && val.answerRight !== false) { val.answerRight = parseInt(index) } } } } // 删除当前项 const delAnswer = (index) => { if (formData.value.bxmAnswerList.length !== 1) { formData.value.bxmAnswerList.splice(index, 1) for (const index in formData.value.bxmAnswerList) { const val = formData.value.bxmAnswerList[index] val.answerTitle = checkIndex(parseInt(index)) val.answerOrd = parseInt(index) + 1 } } } // 将当前项往下降一个 const downAnswer = (index) => { if (index !== formData.value.bxmAnswerList.length - 1) { formData.value.bxmAnswerList[index] = formData.value.bxmAnswerList.splice(index + 1, 1, formData.value.bxmAnswerList[index])[0]; for (const index in formData.value.bxmAnswerList) { const val = formData.value.bxmAnswerList[index] val.answerTitle = checkIndex(parseInt(index)) val.answerOrd = parseInt(index) + 1 if (formData.value.bxmQuestionDetail.questionType === '00' && val.answerRight !== false) { val.answerRight = parseInt(index) } } } } // 修改答案值 const changeAnswerRight = (value, index) => { for (const i in formData.value.bxmAnswerList) { formData.value.bxmAnswerList[i].answerRight = false // 未选中的存为false,保存时改为0,选中的改为1 } formData.value.bxmAnswerList[index].answerRight = index } // 填空题增加一个空位 const addAnswer02 = () => { if (formData.value.bxmAnswerList.length < 5) { formData.value.bxmAnswerList.push({ answerMoreSelect: [], inputVisible: false, inputValue: '' }) reSort() } } // 填空题删除一个空位 const delAnswer02 = (index) => { formData.value.bxmAnswerList.splice(index, 1) reSort() } // 填空题增加或修改后答案重新排序 const reSort = () => { for (const index in formData.value.bxmAnswerList) { const val = formData.value.bxmAnswerList[index] val.answerOrd = parseInt(index) + 1 val.answerTitle = `第${parseInt(index) + 1}空答案` } } // 填空题删除tag const handleCloseTag = (tag, index, tagIndex) => { // 原先的有问题 // formData.value.bxmAnswerList[index].answerMoreSelect.splice(formData.value.bxmAnswerList.indexOf(tag), 1) // 新的 formData.value.bxmAnswerList[index].answerMoreSelect.splice(tagIndex, 1) } // 显示新增tag输入框 const showInput = (index) => { formData.value.bxmAnswerList[index].inputVisible = true } // 新增tag const handleInputConfirm = (index) => { const inputValue = formData.value.bxmAnswerList[index].inputValue if (inputValue) { if (formData.value.bxmAnswerList[index].answerMoreSelect.includes(inputValue)) { BxmMessage({ type: 'warning', message: '同一空答案中不能有重复项,请修改!' }) return } formData.value.bxmAnswerList[index].answerMoreSelect.push(inputValue) } formData.value.bxmAnswerList[index].inputVisible = false formData.value.bxmAnswerList[index].inputValue = '' } const resetTemp = () => { formData.value.bxmQuestionDetail = { questBankId: props.libraryId, questionAnalysis: '', questionContent: '', questionType: '' } formData.value.bxmAnswerList = [ { answerContent: '', answerOrd: '1', answerRight: false, answerTitle: 'A', questDetailId: '' } ] } // 校验问题 const validateForm = async () => { let flag = await ruleForm.value.validate() if (flag === true) { let bxmAnswerListNew = JSON.parse(JSON.stringify(formData.value.bxmAnswerList)) // 深拷贝一下,防止修改自身时填空题类型的tag报错 // 校验题干 if (!validateIsNull(formData.value.bxmQuestionDetail.questionContent)) { BxmMessage({ type: 'warning', message: '请填写题干!' }) return false } let answerRightValidate = false if (['00', '01', '03'].includes(formData.value.bxmQuestionDetail.questionType)) { // 单选/多选选项重复校验 if (['00', '01'].includes(formData.value.bxmQuestionDetail.questionType)) { // 选项校验 for (let i = 0; i < bxmAnswerListNew.length; i++) { let msg = handleValidContent((msg) => { return msg }, i) if (msg) { BxmMessage({ type: 'warning', message: msg }) return false } } let answerContent = [...new Set(bxmAnswerListNew.map(item => { return item.answerContent }))] if (answerContent.length < bxmAnswerListNew.length) { BxmMessage({ type: 'warning', message: '选项不可重复!' }) return false } } // 校验判断题答案是否选择了答案 if (formData.value.bxmQuestionDetail.questionType === '03') { let answerRights = [...new Set(bxmAnswerListNew.map(item => { return item.answerRight }))] if (answerRights.length < bxmAnswerListNew.length) { BxmMessage({ type: 'warning', message: '请选择一个答案!' }) return false } } for (const val of bxmAnswerListNew) { if (['00', '01'].includes(formData.value.bxmQuestionDetail.questionType) && !validateIsNull(val.answerContent)) { BxmMessage({ type: 'warning', message: '请先将选项内容填写完整!' }) return false } if (formData.value.bxmQuestionDetail.questionType === '00') { if (val.answerRight === false) { val.answerRight = '1' } else { val.answerRight = '0' answerRightValidate = true } } if (formData.value.bxmQuestionDetail.questionType === '03') { if (val.answerRight === false) { val.answerRight = '1' answerRightValidate = true } else { val.answerRight = '0' } } if (formData.value.bxmQuestionDetail.questionType === '01' && val.answerRight === '0') { answerRightValidate = true } } if (!answerRightValidate) { BxmMessage({ type: 'warning', message: '请至少选择一个答案!' }) return false } } else if (['02'].includes(formData.value.bxmQuestionDetail.questionType)) { if (bxmAnswerListNew.length === 0) { BxmMessage({ type: 'warning', message: '请填写答案!' }) return false } for (const val of bxmAnswerListNew) { if (val.answerMoreSelect.length === 0) { BxmMessage({ type: 'warning', message: '请将答案填写完整!' }) return false } else { let answers = [...new Set(val.answerMoreSelect)] if (answers.length < val.answerMoreSelect.length) { BxmMessage({ type: 'warning', message: '填空题同一空答案不能有重复,请检查!' }) return false } val.answerMoreSelect = val.answerMoreSelect.join(',') } } } else { bxmAnswerListNew = [] bxmAnswerListNew.push({ questionText: formData.value.bxmQuestionDetail.questionAnalysis }) // .replace(/<[^>]+>/g, '') } formData.value.bxmQuestionDetail.questionContent = formData.value.bxmQuestionDetail.questionContent.replace(/<p>/g, '').replace(/<\/p>/g, '') return { bxmQuestionDetail: formData.value.bxmQuestionDetail, bxmAnswerList: bxmAnswerListNew, fileList: formData.value.fileList } } return flag } const handleContentChange = (html, text) => { formData.value.bxmQuestionDetail.questionContent = text } // 有关图片上传 const handleImageChange = async (file, fileList) => { if (fileList.length) { importLoading.value = true let type = file.name.split('.').pop() if (!['jpeg', 'jpg', 'png', 'PNG', 'JPG', 'JPEG'].includes(type)) { BxmMessage({ type: 'warning', message: `${file.name}图片格式不支持,请重新选择!` }) useDebounce() // 当前图片不显示在页面 upload.value.handleRemove(file) return } let size = Math.ceil(file.size / 1024 / 1024); if (size > 10) { BxmMessage({ type: 'warning', message: `${file.name}图片超过10M,无法上传,请重新选择!` }) useDebounce() // 当前图片不显示在页面 upload.value.handleRemove(file) return } let fileNames = formData.value.fileList.map(item => { return item.fileName }); if (fileNames.includes(file.name)) { BxmMessage({ type: 'warning', message: `${file.name}图片已存在,请重新选择!` }) let index = fileList.findIndex(item => { return item.name === uploadFile.name }) fileList.splice(index, 1) useDebounce() return } // 多加一次设置loading,保证接口请求时要是禁用状态 !importLoading.value && (importLoading.value = true) const upFormData = new FormData() upFormData.append('file', file.raw) let { fileName, filePath } = await 接口(upFormData).catch(() => { // 当前图片不显示在页面 upload.value.handleRemove(file) useDebounce() }); formData.value.fileList.push({ fileName, filePath, url: window.location.origin + '/' + filePath, isOnline: false, // 表示刚上传的图片 }) // 存储数据 resultFileList.push({ fileName, filePath, isDelete: false, isOnline: false }) useDebounce() } } // 防抖 const debounce = function (func, delay) { let timer = null return function () { clearTimeout(timer) timer = setTimeout(() => { func() }, delay) } } const useDebounce = debounce(function () { importLoading.value = false }, 1000) // 图片预览,这就自己写写吧 const handlePictureCardPreview = (uploadFile, index) => { formData.value.fileList.map((item, idx) => { if (item.isOnline) { item.fileName === uploadFile.fileName && (currentIndex.value = index) } else { item.fileName === uploadFile.name && (currentIndex.value = idx) } }) dialogImage.value = true } const handleRemove = (uploadFile) => { let index = null let file = null formData.value.fileList.map((item, itemIndex) => { if (item.isOnline ? item.fileName === uploadFile.fileName : item.fileName === uploadFile.name) { file = item index = itemIndex } }) let resultFile = null file !== null && (resultFile = resultFileList.find(item => item.fileName === file.fileName)) resultFile && (resultFile.isDelete = true) // 删除文件 index !== null && (formData.value.fileList.splice(index, 1)) } const handleImageClose = () => { dialogImage.value = false currentIndex.value = 0 } // 清除图片,重置上传按钮 const clearImg = () => { upload.value.clearFiles() formData.value.fileList = [] resultFileList = [] } const getFormData = () => { return JSON.parse(JSON.stringify(formData.value)) } defineExpose({ resetTemp, validateForm, formData, handleBatchDelFile, clearImg, getFormData }) </script> <style lang="scss" scoped> $--color-primary: #6383ff; .p-lr-5 { padding: 0 5px; } .edit-question-box { .flex-center { display: flex; align-items: center; } .tips { height: 32px; line-height: 32px; background-color: var(--color-primary-light); color: #6383FF; font-size: 12px; padding: 0 16px; margin-bottom: 10px; .tip-icon { padding: 0 4px; font-size: 14px; } } .edit-question-content { max-height: 200px; overflow-y: auto; .pack-input-box { @extend .flex-center; width: 80%; min-height: 32px; max-height: 155px; border-radius: 4px; border: var(--border-base-3); overflow-x: auto; padding: 0 12px; .input-new-tag { width: 90px; margin-left: 8px; vertical-align: bottom; } :deep(.el-input___inner) { height: 25px } } } .questionContent-custom-label { @extend .flex-center; justify-content: flex-end; width: 100%; height: 32px; } .question-custom-label { @extend .flex-center; width: 100%; text-align: center; .label-icon { margin: 0 16px; font-size: 12px; } .label-title { width: 30px; } } .set-answer { width: 50px; text-align: center; .set-answer-title { font-family: SourceHanSansCN, SourceHanSansCN; font-weight: 400; font-size: 12px; color: var(--color-text-secondary); } } .answer-btn-box { margin-left: 8px; @extend .flex-center; } .radio-add-btn { margin: 0 0 15px 45px; } .dash-line { height: 1px; width: 100%; border-top: 1px dashed #E3E5ED; margin-bottom: 10px; } .edit-question-bottom { background: var(--descriptions-item-bordered-label-background); border-radius: 4px; padding: 15px 15px 15px 0; .answer-span { border-bottom: 1px solid var(--color-text-primary); } } :deep(.el-form-item__label) { font-size: 12px; padding: 0 9px 0 0 !important; color: var(--color-text-primary); } :deep(.el-form-item__label:before) { display: none !important; } :deep(.el-form-item__content) { @extend .flex-center; flex-wrap: nowrap; font-size: 12px; color: var(--color-text-primary); word-break: break-all; } :deep(.el-radio) { margin-right: 0; } :deep(.el-radio__label) { font-size: 12px; color: var(--color-text-regular); } :deep( .question-content-input .el-textarea__inner) { background-color: var(--descriptions-item-bordered-label-background); border: none; box-shadow: none; padding: 0; margin-top: 7.5px; } } .uplod-box { display: flex; flex-direction: column; } .img-box { display: flex; flex-direction: column; list-style: none; padding: 0; margin: 0; .img-item { display: flex; align-items: center; position: relative; border: var(--border-base-3); border-radius: 6px; margin-top: 10px; padding: 10px; overflow: hidden; &:hover { .item-close { display: block; } } img { display: inline-flex; justify-content: center; align-items: center; width: 70px; height: 70px; object-fit: contain; } .item-name { cursor: pointer; padding-left: 8px; display: flex; align-items: center; .item-name-icon { font-size: 14px; margin-right: 8px; color: var(--color-info); } .item-name-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; &:hover { color: $--color-primary; } } } .item-close { display: none; position: absolute; right: 5px; top: 5px; cursor: pointer; &:hover { color: $--color-primary; } } } } :deep(.el-upload-list__item-file-name) { cursor: pointer; &:hover { color: $--color-primary; } } :deep(.el-upload-list__item-file-name) { font-size: 12px; } :deep(.el-upload-list), :deep(.el-upload-list--picture .el-upload-list__item-thumbnail) { background-color: transparent; } .img-box { max-height: 214px; overflow-y: auto; } </style>
以下是效果图
单选:
多选:
填空:
判断:
简答/论述:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。