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

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

以下是效果图

单选:

多选:

填空:

判断:

简答/论述:

相关推荐
webmote1 小时前
Fabric.js 入门教程:扩展自定义对象的完整实践(V6)
运维·javascript·canvas·fabric·绘图
冴羽1 小时前
Solid.js 最新官方文档翻译(12)—— 派生信号与 Memos
前端·javascript·react.js
新中地GIS开发老师2 小时前
25考研希望渺茫,工作 VS 二战,怎么选?
javascript·学习·考研·arcgis·地理信息科学·地信
萧大侠jdeps2 小时前
Vue 3 与 Tauri 集成开发跨端APP
前端·javascript·vue.js·tauri
JYeontu3 小时前
实现一个动态脱敏指令,输入时候显示真实数据,展示的时候进行脱敏
前端·javascript·vue.js
发呆的薇薇°3 小时前
react里使用Day.js显示时间
前端·javascript·react.js
GISer_Jing3 小时前
前端面试题合集(一)——HTML/CSS/Javascript/ES6
前端·javascript·html
清岚_lxn3 小时前
es6 字符串每隔几个中间插入一个逗号
前端·javascript·算法
刺客-Andy3 小时前
React 第十九节 useLayoutEffect 用途使用技巧注意事项详解
前端·javascript·react.js·typescript·前端框架
谢道韫6664 小时前
今日总结 2024-12-27
开发语言·前端·javascript