记录使用自定义编辑器做试题识别功能

习惯了将解析写在代码注释,这里就直接上代码啦,里面用到的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][.、.]|$)/gsulet matchOption = nullwhile((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 = questionContentdata.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 = newInputTextdata.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) % 26if(imn === 0 || (imn === 1 && remainder === 0)) {// A~Zreturn String.fromCharCode(65 + index)}else if((imn > 1 || (imn === 1 && remainder > 0)) && imn <= 26){// AA、AB...BA...CA~ZZreturn (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)// 处理下一个的unserLineStartif (index < underLineList.length - 1) {handleCheckUnderStart(index, underLineList)}})questionContent = newStr} else { // 这是下划线上没有内容,至少三个连续的_才识别成填空题,避免部分单词识别错误,例如COMMENT_NODE// 找到下划线let underRegx = /(_{3})+/g// let underRegx = /[\_]+/glet understrArr = newStr.match(underRegx) || []for (let i = 0; i < understrArr.length; i++) {// 将_替换成'___'let start = newStr.indexOf(understrArr[i])let end = start + understrArr[i].lengthnewStr = newStr.substring(0, start) + '___' + newStr.substring(end)}questionContent = newStrlet index = 0while(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]\.?[.、.])/slet regx2 = /^(.*?)(?=[A-Za-z](?:(?:\s*\.\s*)|(?:\s*,\s*)|$))/slet 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*[\))]/glet 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) { // 条件1resultContent += `${item}`} else {let isInner = truefor(let i = 0; i < newArr.length; i++) {newArr[i] = newArr[i].toLocaleUpperCase()if (!answers.includes(newArr[i])) { // 条件2resultContent += `${item}`isInner = falsebreak // 退出循环}}// 条件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.innerTextemits('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.childNodeschildNodes.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.innerTextemits('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 = colorelement.style.fontSize = fontSizeelement.style.backgroundColor = bgcelement.style.padding = 0element.style.margin = 0element.style.lineHeight = 20 + 'px'}} else if (element.nodeType === Node.TEXT_NODE) {// 如果是文本节点,查找其父元素并设置颜色if (element.parentElement.style) {element.parentElement.style.color = colorelement.parentElement.style.fontSize = fontSizeelement.parentElement.style.backgroundColor = bgcelement.parentElement.style.padding = 0element.parentElement.style.margin = 0element.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-buttonsoplain:disabled="btnDisabled || !inputText"@click="handleClear"><i class="bxm-icon-fail btn-icon"></i>清 空</bxm-button><bxm-buttontype="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.questionContentformDataText.value.bxmQuestionDetail.questionType = data.questionTypeformDataText.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-uploadref="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-inputv-model.trim="item.answerContent"clearableplaceholder="请输入选项内容"maxlength="50"show-word-limitstyle="width: 50%; margin-right: 10px;"></el-input><!-- 单选 --><template v-if="['00'].includes(formData.bxmQuestionDetail.questionType)"><el-radiov-model="item.answerRight":label="index"@change="changeAnswerRight($event, index)">&nbsp;</el-radio></template><!-- 多选 --><template v-else><el-checkbox v-model="item.answerRight" true-label="0" false-label="1":disabled="disabled">&nbsp;</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" linktype="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" linktype="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" linktype="primary"@click="delAnswer(index)"></bxm-button></el-tooltip></div></el-form-item></template><!-- 填空 --><template v-else-if="['02'].includes(formData.bxmQuestionDetail.questionType)"><el-form-itemv-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-tagv-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-inputv-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-buttonicon="Plus" type="primary"linkstyle="margin-left: 10px"@click="showInput(index)"></bxm-button></el-tooltip></div><el-tooltip content="删除" placement="top"><bxm-button type="primary" icon="delete" linkstyle="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"linkicon="Plus"class="radio-add-btn"@click="addAnswer(formData.bxmAnswerList.length - 1)">添加选项</bxm-button><bxm-button v-if="['02'].includes(formData.bxmQuestionDetail.questionType)"type="primary"linkicon="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].answerContentif (!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].answerMoreSelectlet 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 = falseval.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 = valBxmMessageBox.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]] : selAnswerOrdsitem.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 = valsetAnswerData()}).catch(() => {})}} else {formData.value.bxmQuestionDetail.questionType = valsetAnswerData()}}// 设置答案数据
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) + 1if (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) + 1if (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) + 1val.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].inputValueif (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 = falseformData.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 = falseif (['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 = truelet 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 = nullreturn 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 = nulllet file = nullformData.value.fileList.map((item, itemIndex) => {if (item.isOnline ? item.fileName === uploadFile.fileName : item.fileName === uploadFile.name) {file = itemindex = itemIndex}})let resultFile = nullfile !== 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 = falsecurrentIndex.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>

以下是效果图

单选:
在这里插入图片描述

多选:
在这里插入图片描述
填空:
在这里插入图片描述
判断:
在这里插入图片描述
简答/论述:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/20924.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Qt | Qt 资源简介(rcc、qmake)

1、资源系统是一种独立于平台的机制,用于在应用程序的可执行文件中存储二进制文件(前面所讨论的数据都存储在外部设备中)。若应用程序始终需要一组特定的文件(比如图标),则非常有用。 2、资源系统基于 qmake,rcc(Qt 的资源编译器,用于把资源转换为 C++代码)和 QFile …

java—MyBatis框架

简介 什么是 MyBatis&#xff1f; MyBatis 是一款优秀的持久层框架&#xff0c;它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO&…

软件公司为什么必须要使用低代码系统?

在当今软件行业全国比较内卷的大环境下&#xff0c;软件公司面临着前所未有的挑战。为了在这个竞争激烈的市场中生存并脱颖而出&#xff0c;驰骋低代码设计者认为&#xff0c;软件公司必须要使用低代码系统。以下是几个关键的原因&#xff1a; 时代发展的必然选择 低代码系统是…

领导力中的说服艺术

本文主要介绍了亚里士多德修辞三角理论&#xff0c;即演讲者在说服听众时如何运用品格&#xff08;Ethos&#xff09;、情感&#xff08;Pathos&#xff09;和逻辑&#xff08;Logos&#xff09;三种基本的修辞手法。原文: The Art of Persuasion in Leadership 亚里士多德的说…

【自动驾驶技术】自动驾驶汽车AI芯片汇总——地平线篇

0. 前言 按照国际惯例&#xff0c;首先声明&#xff1a;本文只是我自己学习的理解&#xff0c;虽然参考了他人的宝贵见解及成果&#xff0c;但是内容可能存在不准确的地方。如果发现文中错误&#xff0c;希望批评指正&#xff0c;共同进步。 本篇文章是这个系列的第二篇&#x…

50个常用的Docker命令及如何使用

这里整理了50个常用的Docker命令以及每个命令的使用方法。 docker version:显示Docker版本信息。 示例:docker version docker info:显示Docker系统信息。 示例:docker info docker pull <image>:从Docker Hub下载镜像。 示例:docker pull ubuntu docker run <i…

CMake编译安装、生成可执行程序、生成静态动态库以及静态动态库的链接

1 CMake介绍 CMake是一个开源的、跨平台的构建系统&#xff0c;用于管理软件从源代码到可执行文件的整个构建过程。它最初由Kitware公司为ITK&#xff08;Insight Segmentation and Registration Toolkit&#xff09;和VTK&#xff08;Visualization Toolkit&#xff09;等开源…

在Linux kali下载、安装Perl环境

目录 Perl介绍 下载安装 官网下载 在Windows安装 在Linux和Mac OS安装 Perl介绍 Perl一种功能丰富的计算机程序语言&#xff0c;运行在超过100种计算机平台上&#xff0c;适用广泛&#xff0c;从最初是为文本处理而开发的&#xff0c;现在用于各种任务&#xff0c;包括系统…

面试官:Spring中都应用了哪些设计模式?

设计模式是我们项目中经常会涉及到的项目进行重构、解构时的一种方法。 比如我们常见的单例模式、工厂模式、策略模式、装饰器模式等都是比较常用的&#xff1b;关于 23 种设计模式&#xff0c;大家可以找本书专门去学习一下&#xff0c;在 Java 框架的源码中也不例外&#xf…

如何用python做一个用户登录界面——浔川python社

1 需解决的问题&#xff1a; 1.1如何用python做一个用户登录界面&#xff1f; 1.2需要用到哪些库、模块&#xff1f; 2 问题解决&#xff1a; 2.1 回答 1.1 &#xff1a;合理即可&#xff0c;无标准回答。 2.2 回答 1.2 &#xff1a;tk库&#xff08;缩写&#xff09;、GUL界面…

C++20实战之channel

C20实战之channel 继前面两节的直播&#xff0c;讲解了thread、jthread、stop_token、stop_source、stop_callback、cv、cv_any等的用法与底层实现&#xff0c;那么如何基于这些知识实现一个小项目呢&#xff1f; 于是引出了这篇&#xff0c;写一个channel出来。 注&#xff1a…

【AI】设计师人人必备的Ai课程,AIGC实战教学

课程介绍 专为设计师定制的AI绘画视觉课程&#xff0c;包含排版、插画、海报和动漫等。共43节课程&#xff0c;2.06G视频&#xff0c;教授AI应用技巧&#xff0c;提高设计效率和质量。内容涵盖词生图方法、AI风格设计等&#xff0c;帮助学员在设计领域取得成就。 1_01-ai课程…

jrt落地deepin

经过昨天一晚上的努力&#xff0c;把deepin和win10的双系统安装好了。同时把jrt开发需要的svn&#xff0c;jdk,idea安装好里&#xff0c;代码也checkout里。 首先安装系统碰到安装deepin后启动时候无法选择win10,在宏伟兄帮助下找到资料执行sudo update-grub解决了。 然后程…

糖果促销【百度之星】/思维

糖果促销 思维 大佬的解法&#xff1a; #include<bits/stdc.h> using namespace std; typedef long long ll; int main() {ll t;cin>>t;for(int i0;i<t;i){ll p,k;cin>>p>>k;if(k0) cout<<0<<endl;else{k-(k-1)/p;cout<<k<…

v-for中key的作用

v-for中key的作用 例如我们用v-for渲染一个列表[1, 2, 4 ,5]&#xff0c;然后在中间插入一个3变成[1,2,3,4,5]。v-for写了key和没有写key&#xff0c;Vue是怎么处理的呢&#xff1f; Vue对有key的会调用patchKeyedChildren方法&#xff1b;没有key的调用patchUnkeyedChildren方…

Vue3 -Computed计算属性

前言&#xff1a; Computed属性属于Vue3中的响应式核心(与之共同说明的还有ref&#xff0c;reactive&#xff0c;watch...) 接受一个 getter 函数&#xff0c;返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set…

AI搜索,围攻百度

图片&#xff5c;电影《双子杀手》截图 ©自象限原创 作者丨程心 国内的大模型厂商落地C端&#xff0c;都盯上了AI搜索。 随着5月30号&#xff0c;腾讯宣布推出基于混元大模型的APP“腾讯元宝”&#xff0c;并基于搜狗搜索引擎&#xff0c;上线AI搜索功能。几乎当下所有…

【Qt】Qt Style Sheets (QSS) 指南:打造个性化用户界面

文章目录 前言&#xff1a;1. QSS 选择器2. 子控件选择器&#xff08;Sub-Controls&#xff09;2.1. 示例&#xff1a;给 QComboBox 给下拉按钮加上图标2.2. 示例&#xff1a;修改进度条颜色 3. 伪类选择器3.1. 代码示例: 设置按钮的伪类样式.3.2. 代码示例: 使用事件方式实现同…

数模混合芯片设计中的修调技术是什么?

一、修调目的 数模混合芯片需要修调技术主要是因为以下几个原因&#xff1a; 工艺偏差&#xff08;Process Variations&#xff09;&#xff1a; 半导体制造过程中存在不可避免的工艺偏差&#xff0c;如晶体管尺寸、阈值电压、电阻和电容值等&#xff0c;这些参数的实际值与…

【学习Day5】操作系统

✍&#x1f3fb;记录学习过程中的输出&#xff0c;坚持每天学习一点点~ ❤️希望能给大家提供帮助~欢迎点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;指点&#x1f64f; 学习编辑文章的时间不太够用&#xff0c;先放思维导图&#xff0c;后续复习完善细节。