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

习惯了将解析写在代码注释,这里就直接上代码啦,里面用到的bxm-ui3组件库是博主基于element-Plus做的,可以通过npm i bxm-ui3自行安装使用

javascript 复制代码
// 识别方法:
// 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,对里面内容进行简单处理了之后就可以使用了

javascript 复制代码
<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>

组件使用示例

html 复制代码
<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>
javascript 复制代码
// 识别
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()
}

自己做的试题编辑的组件

javascript 复制代码
<!--根据最新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>

以下是效果图

单选:

多选:

填空:

判断:

简答/论述:

相关推荐
王哲晓4 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
理想不理想v9 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云19 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
aPurpleBerry1 小时前
JS常用数组方法 reduce filter find forEach
javascript
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x2 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢2 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落3 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt