习惯了将解析写在代码注释,这里就直接上代码啦,里面用到的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>
以下是效果图
单选:
多选:
填空:
判断:
简答/论述: