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

习惯了将解析写在代码注释,这里就直接上代码啦,里面用到的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)">
                            &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" 
                                    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>

以下是效果图

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java设计模式 _行为型模式_观察者模式

一、观察者模式 1、观察者模式 观察者模式 ( Observer Pattern )是一种行为型模式。 常用于对象间存在一对多关系时&#xff0c;比如&#xff0c;当一个对象被修改时&#xff0c;需要自动通知它的依赖对象。 2、实现思路 &#xff08;1&#xff09;、定义被观察者的行为&…

Kafka原生API使用Java代码-生产者-发送消息

文章目录 1、生产者发送消息1.1、使用EFAK创建主题my_topic31.2、根据kafka官网文档写代码1.3、pom.xml1.4、KafkaProducer1.java1.5、使用EFAK查看主题1.6、再次运行KafkaProducer1.java1.7、再次使用EFAK查看主题 1、生产者发送消息 1.1、使用EFAK创建主题my_topic3 1.2、根…

c++(六)

c&#xff08;六&#xff09; 多态概念在c中是如何实现多态静态多态&#xff08;绑定&#xff09;动态多态&#xff08;绑定&#xff09; 动态多态的实现原理动态内存分配中遇到的问题 重载、重定义、重写的区别抽象类接口类---抽象类空类对象的内存大小explicitfinal修饰类修饰…

稳定性大升级!EMCS全球服务网络携手NineData实现数据实时同步

易客满&#xff08;ECMS Express&#xff09;专注于提供全球化的国际物流解决方案和经济快递服务&#xff0c;服务网络覆盖全球主要贸易市场的国际物流公司。ECMS拥有国际快递、国际货代、仓储供应链全球覆盖服务能力。 1. 易客满&#xff08;ECMS&#xff09;数据复制的技术挑…

SOLIDWORKS 2024:零件亮点的升级与突破

随着科技的不断发展&#xff0c;工程设计软件也在持续进步&#xff0c;以更好地满足工程师和设计师的需求。SOLIDWORKS&#xff0c;作为一款广泛使用的三维CAD软件&#xff0c;一直在不断地推出新版本&#xff0c;以提供更强大、更便捷的功能。今天&#xff0c;我们将深入探讨S…

高边坡监测常用的主要仪器设备

随着人类的发展&#xff0c;近几年由于人类生活生产的破坏&#xff0c;地球环境不断恶化。鉴于这一现象&#xff0c;监测行业也随之应运而生。常见的监测类型有&#xff1a;边坡监测&#xff0c;地灾监测&#xff0c;水库监测&#xff0c;大坝监测&#xff0c;矿山监测&#xf…

geotrust通配符证书600元且赠送一个月

GeoTrust作为国际知名的数字证书颁发机构&#xff0c;旗下有RapidSSL、QuickSSL等子品牌经营着各种类型的SSL数字证书&#xff0c;其中RapidSSL旗下的SSL数字证书都是入门级的&#xff0c;性价比高。审核速度也比较快&#xff0c;证书的适用范围也比较广泛。今天就随SSL盾小编了…

HarmonyOS-9(stage模式)

配置文件 {"module": {"requestPermissions": [ //权限{"name": "ohos.permission.EXECUTE_INSIGHT_INTENT"}],"name": "entry", //模块的名称"type": "entry", //模块类型 :ability类型和…

【Python Cookbook】S01E02 从任意长度的可迭代对象中分解元素

目录 问题解决方案讨论 问题 从某个不确定长度的迭代对象中分解出 N N N 个元素。 解决方案 *分解操作和各种函数式语言中的列表处理功能有着一定的相似性。例如&#xff0c;如果有一个列表&#xff0c;可以像下面这样轻松将其分解为头部和尾部。 scores [99, 97, 91, 89…

CHIMA专访美创高级总监丁斐:为医疗数据安全构筑体系化防御新机制

5月17-19日&#xff0c;中国医院信息网络大会&#xff08;CHIMA 2024&#xff09;在南京隆重召开。作为结识多年的老友&#xff0c;美创科技再携以数据为中心的全系列安全业务、新一代数字化安全平台、医疗行业解决方案精彩亮相。 会议期间&#xff0c;CHIMA专访美创科技&…

金融反欺诈指南:车险欺诈为何如此猖獗?

目录 车险欺诈猖獗的原因 车险欺诈的识别难点 多重合作打击车险欺诈 保险企业需要提升反欺诈能力 监管部门需要加强协同合作 青岛市人民检察院在其官方微信公众号上发布的梁某保险诈骗案显示&#xff0c;2020 年以来&#xff0c;某汽修厂负责人梁某、某汽车服务公司负责人孙某&…

精通推荐算法7:多任务学习 -- 总体架构

1 多任务学习的总体架构 目前的互联网主流推荐场景在大多数情况下需要优化多个业务目标。例如在淘宝商品推荐中&#xff0c;需要兼顾点击率和转化率。在抖音短视频推荐中&#xff0c;需要考虑完播率、播放时长、点赞率、评论率、关注率等目标。为了提升各项业务目标&#xff0…

光栅幅值细分原理与实现

本文介绍光栅幅值细分原理与实现。 光栅是工业测量领域中常见的传感器&#xff0c;如下图。主要厂家有雷尼绍&#xff0c;海德汉&#xff0c;配套的光栅读数头有模拟信号的&#xff0c;也有直接细分输出数字脉冲的&#xff0c;本文的细分针对模拟信号&#xff0c;即有正弦信号…

数字化业务流程升级再造,解困基本半导体的CRM应用5年之痒

在新能源汽车、工业互联、5G通信、消费电子等需求的强力拉动下&#xff0c;以碳化硅为代表的第三代半导体产业迎来爆发式增长。 深圳基本半导体有限公司&#xff08;以下简称“基本半导体”&#xff09;是中国第三代半导体创新企业&#xff0c;专业从事碳化硅功率器件的研发与…

NineData 联合创始人周振兴将参加开源数据库技术沙龙,并和 PolarDB 开源社区一起去娃哈哈带来主题分享!

5月31日&#xff08;周五&#xff09;&#xff0c;PolarDB 开源社区将联合娃哈哈集团共同举办开源数据库技术沙龙&#xff01;NineData 联合创始人周振兴受邀参加&#xff0c;并将分享《NineData&#xff0c;Any to Any 数据复制之路》的技术分享。 本次活动汇聚了 PolarDB 产品…

用于水利工程系统方面的传感器M-A542VR10

近几年快速发展的IC技术和计算机技术&#xff0c;为传感器的发展提供了良好与可靠的科学技术基础。使传感器的发展日新月益&#xff0c;且数字化、多功能与智能化是现代传感器发展的重要特征爱普生也在不断发展自己的传感器型号。随着水利工程技术的不断进步&#xff0c;传感器…

面向对象编程:坦克飞机大战游戏的重构之旅

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、面向对象编程思想入门 坦克对象的定义 属性与行为方法的实现 二、面向过程与面向对象…

子网划分案例

4.2子网划分 “有类编址”的地址划分过于死板&#xff0c;划分的颗粒度太大&#xff0c;会有大量的主机号不能被充分利用&#xff0c;从而造成了大量的IP地址资源浪费。因此可以利用子网划分来减少地址浪费&#xff0c;即VLSM (Variable Length Subnet Mask)&#xff0c;可变长…

python清洗苹果产量数据:从字符串到整型的转化

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言 二、使用普通方法清洗数据 1. 创建字典并遍历 2. 示例代码 3. 结果展示 三、使…

WAMP无法启动mysql

一种原因是原来安装过mysql,mysql默认是自启动的&#xff0c;而WAMP内置mysql会发生冲突&#xff0c;所以 解决方法&#xff1a; winR 输入 services.msc 将mysql关闭&#xff0c;并设为手动模式