当前位置:   article > 正文

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

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

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

// 识别方法:
// dom 当前识别数据所在区域, questionType 当前点击编辑选择的题目类型(论述题、简答题要用)
export const recognitionMethod = (inputText, dom, questionType) => {
    // 存一份
    let newInputText = inputText.trim()

    let data = {
        questionContent: '',
        questionType: '',
        questionAnalysis: '',
        answerList: []
    }
    // 解析答案
    let { result, newText } = recognitionResult(newInputText)
    data.questionAnalysis = result || ''

    // 单选多选题匹配
    const regx1 = /(?:^\d+、)?(.*?)\s*[\((]\s*([A-Za-z]*)\s*[\))]\s*([\s\S]+)/

    // 填空题匹配  若下划线上无答案,则三个下划线为一个空
    const regx2 = /(?:^\d+、)?(.*?)[\_]+\s*/g
    // const regx2 = /(?:^\d+、)?(.*?)(_{3})+\s*/g

    // 判断题匹配  含有(√|×|对|错|正确|错误)
    const regx3 = /(?:^\d+、)?(.*?)\(([√×对错正确错误])\)\s*/

    let match = newText.match(regx1)
    let match2 = newText.match(regx2)
    let match3 = newText.match(regx3)

    // 填空题:去根据dom获取出来有下划线的部分即为答案
    let underLineList = getUnderlineList(dom, newText)

    if (match) { // 基本的单选多选
        let answer = match[2] || ''
        let optionsStr = match[3]
        // 没有答案或者只有一个答案识别为单选,多个答案为多选
        if (answer.length === 1 || !answer.length) {
            data.questionType = '00'
        } else {
            data.questionType = '01'
        }
        // 单选/多选,有选项
        if (optionsStr) {
            let options = []
            let regexOption = /[A-Za-z][.、.]\s*(?:.*?)(\([^)]*\))?(?=[A-Za-z][.、.]|$)/gsu
            let matchOption = null
            while((matchOption = regexOption.exec(optionsStr)) !== null) {
                options.push(matchOption[0].replace(/[A-Za-z][\.、.]\s*/, '') + (matchOption[1] ? matchOption[1] : ''));
            }
            if (!options.length) {
                // 选项
                let optionRegx1 = /[A-Za-z](\.|、)/
                options = optionsStr.split(optionRegx1).filter(option => { return !['', '.', '、', '.'].includes(option) })
            }
            if (options.length) {
                options.map((item, index) => {
                    let obj = {
                        answerContent: item,
                        answerOrd: `${index + 1}`,
                        answerRight: false,
                        answerTitle: checkIndex(index)
                    }
                    // 单选
                    if (data.questionType === '00') {
                        obj.answerRight = (checkIndex(index) === answer || checkIndex(index).toLocaleLowerCase() === answer) ? '0' : false
                    } else { // 多选
                        let answers = answer.split('')
                        obj.answerRight = (answers.includes(checkIndex(index)) || answers.includes(checkIndex(index).toLocaleLowerCase())) ? '0' : '1'
                    }
                    data.answerList.push(obj)
                })
            }
        }

        handleQuestionContent(match[1], newText, data)
    } else if (match3) { // 判断题
        data.questionType = '03'
        data.questionContent = match3[1] + '()'
        let answer = match3[2]
        for(let i = 0; i < 2; i++) {
            let obj = {
                answerOrd: `${i + 1}`,
                answerRight: i === 0 ? 
                            ['对', '正确', '√'].includes(answer) ? i : false : 
                            ['错', '错误', '×'].includes(answer) ? i : false,
                answerTitle: i === 0 ? '正确' : '错误'
            }
            data.answerList.push(obj)
        }
    }  else if (underLineList.length || match2) { // 填空题
        data.questionType = '02'
        let { questionContent, answerList } = recognitionPack(newText, underLineList)
        data.questionContent = questionContent
        data.answerList = answerList
    } else { // 简答题/论述题   没有匹配其余的直接处理为论述题或简答题
        // 当前点击编辑选择的题目类型如果不是论述题或简答题,就默认设置为简答题
        data.questionType = ['04', '06'].includes(questionType) ? questionType : '04'
        let newStr = ''
        // 去掉数字、开头
        if (/^\d+、/.test(newInputText)) {
            newStr = newInputText.replace(/^\d+、/, '')
        } else {
            newStr = newInputText
        }
        // 一共6种可以解读为答案的内容
        let resultRegx = /(答:)|(答案:)|(解析:)|(分析:)|(解答:)|(回答:)]/g
        // 给了解析
        if (resultRegx.test(newInputText)) {
            // ['题干', '第一种', '第二种'.....'最后一个是根据前面某一种分割出来的答案']如果有解析就是正常的8个项
            let arr = newStr.split(resultRegx)
            if (arr.length >= 8) {
                data.questionContent = arr[0].trim()
                data.questionAnalysis = arr[7].trim()
            } else {
                data.questionContent = newInputText
                data.questionAnalysis = ''
            }
        } else {
            data.questionAnalysis = ''
            data.questionContent = newStr.trim()
        }
    }
    return data
}

// 序号A~Z-----AA~AZ
export const checkIndex = (index) => {
    let imn = Math.floor((index + 1)/26)
    let remainder = (index + 1) % 26
    if(imn === 0 || (imn === 1 && remainder === 0)) {
        // A~Z
        return String.fromCharCode(65 + index)
    }else if((imn > 1 || (imn === 1 && remainder > 0)) && imn <= 26){
        // AA、AB...BA...CA~ZZ
        return (String.fromCharCode(65 + (remainder ? (imn - 1) : (imn - 2))) + String.fromCharCode(65 + (remainder ? (remainder - 1) : 25)))
    }
}

// 解析答案
export const recognitionResult = (inputText) => {
    let result = ''
    let newText = inputText
    // 一共6种可以解读为答案的内容
    let resultRegx = /(答:)|(答案:)|(解析:)|(分析:)|(解答:)|(回答:)]/g
    // 给了解析
    if (resultRegx.test(inputText)) {
        // ['题干', '第一种', '第二种'.....'最后一个是根据前面某一种分割出来的答案']如果有解析就是正常的8个项
        let arr = inputText.split(resultRegx)
        newText = arr[0].trim()
        if (arr.length >= 8) {
            result = arr[7]
        } else {
            result = ''
        }
    }
    return { result, newText }
}

// 以下为填空题识别相关方法

// 填空题识别
export const recognitionPack = (inputText, underLineList) => {
    let questionContent = ''
    let answerList = []
    let newStr = /^\d+、/.test(inputText) ? inputText.replace(/^\d+、/, '') : inputText
    // 这是下划线上有内容
    if (underLineList.length) {
        underLineList.map((item, index) => {
            let obj = {
                answerOrd: index + 1,
                answerMoreSelect: item.answerMoreSelect,
                answerTitle: `${index + 1}空答案`,
                inputVisible: false,
                inputValue: '',
            }
            answerList.push(obj)
            // 将答案替换成'___'
            let end = item.underLineStart + item.answerLength
            // 这里加了三个_,underLineList中剩余的项的unserLineStart都要处理,否则会错位
            newStr = newStr.substring(0, item.underLineStart) + '___' + newStr.substring(end)
            // 处理下一个的unserLineStart
            if (index < underLineList.length - 1) {
                handleCheckUnderStart(index, underLineList)
            }
        })
        questionContent = newStr
    } else { // 这是下划线上没有内容,至少三个连续的_才识别成填空题,避免部分单词识别错误,例如COMMENT_NODE
        // 找到下划线
        let underRegx = /(_{3})+/g
        // let underRegx = /[\_]+/g
        let understrArr = newStr.match(underRegx) || []
        for (let i = 0; i < understrArr.length; i++) {
            // 将_替换成'___'
            let start = newStr.indexOf(understrArr[i])
            let end = start + understrArr[i].length
            newStr = newStr.substring(0, start) + '___' + newStr.substring(end)
        }
        questionContent = newStr
        let index = 0
        while(index < understrArr.length) {
            answerList.push({
                answerOrd: `${index + 1}`,
                answerMoreSelect: [],
                answerTitle: `${index + 1}空答案`,
                inputVisible: false,
                inputValue: ''
            })
            index++
        }
    }
    return {
        questionContent,
        answerList
    }
}

// 判断节点是否有下划线样式
function isLeafWithUnderline(node) {
    if (node.nodeType === Node.TEXT_NODE) {
        return false
    }
    let style = window.getComputedStyle(node)
    // textDecoration含有underline的一定有下划线
    return style.textDecoration && style.textDecoration.includes('underline')
}

// 递归获取到最深层叶子节点,遇到有下划线的节点直接视为叶子节点
function findDeepestNodes(node, deepestNodes = []) {
    // 注释节点
    if (node.nodeType === Node.COMMENT_NODE) { return deepestNodes }
    // 如果当前节点是文本节点或者具有下划线样式,认为是叶子节点
    if (node.nodeType === Node.TEXT_NODE || isLeafWithUnderline(node)) {
        deepestNodes.push(node)
        return deepestNodes // 返回当前节点,不再深入遍历其子节点
    }
    
    // 遍历当前节点的所有子节点
    for (let child of node.childNodes) {
        findDeepestNodes(child, deepestNodes)
    }
    
    return deepestNodes
}

// 获取下划线列表
export const getUnderlineList = (dom, newText) => {
    let allTextNodes = findDeepestNodes(dom)
    let list = []
    let fullText = ''

    // 找到下划线标签进行数据处理
    for(let index = 0; index < allTextNodes.length; index++) {
        let node = allTextNodes[index]
        // 文本节点获取内容和样式是不一样的
        let style = node.nodeType === Node.TEXT_NODE ? {} : window.getComputedStyle(node)
        fullText += !node?.innerText ? node.textContent : node.innerText
        // 去掉数字开头
        fullText = /^\d+、/.test(fullText) ? fullText.replace(/^\d+、/, '') : fullText
        // 有下划线的把下划线内容记录下来,下划线位置记录下来
        if (style?.textDecoration && style?.textDecoration.includes('underline') && node.innerText !== '') {
            let obj = {
                answerMoreSelect: node.innerText,
                answerTitle: `${index + 1}空答案`,
                answerLength: node.innerText.length, // 答案长度
                underLineStart: fullText.length - node.innerText.length 
            }
            list.push(obj)
        }
    }
    
    // 处理下划线连在一起但是为u标签时,要合并成一个空
    if (list.length) {
        for(let i = 0; i < list.length; i++) {
            // 连续的下划线:
            if (i > 0 && list[i].underLineStart === list[i - 1].underLineStart + list[i - 1].answerLength) {
                list[i - 1] = {
                    answerMoreSelect: list[i - 1].answerMoreSelect + list[i].answerMoreSelect, // 上一个的文本与当前文本组合
                    answerTitle: `${i}空答案`, // 只留前一个,所以下标是前一个的
                    answerLength: list[i - 1].answerLength + list[i].answerLength, // 上一个的文本长度与当前文本长度之和
                    underLineStart: list[i - 1].underLineStart // 上一个文本的起始位置就是最终的起始位置
                }
                list.splice(i, 1)
                i--
            }
        }
    }

    return list
}

// 获取增加或减少了多少长度
export const getChangeLen = (curUnderIndex, underList) => {
    let addLen = 0
    // 遍历当前以及之前的
    for(let i = 0; i <= curUnderIndex; i++) {
        // 当前下划线文本超出了下划线3个字符的长度,替换成3个下划线之后会少了 answerLength-3 的长度,后面的都需要往前移动answerLength-3个位置
        // 当前下划线文本少于下划线3个字符的长度,替换成3个下划线之后会多了 3-answerLength 的长度,后面的都需要往后移动3-answerLength个位置
        if (underList[i].answerLength !== 3) {
            addLen += 3 - underList[i].answerLength // 变化的量可能正可能负
        }
    }
    return addLen
}

// 处理下划线起始位置
export const handleCheckUnderStart = (curUnderIndex, underList) => {
    if (curUnderIndex >= underList.length - 1) return
    // 获取需要变动的数量
    let changeLen = getChangeLen(curUnderIndex, underList)
    // 处理当前的后一个即可
    underList[curUnderIndex + 1].underLineStart += changeLen
}

// 处理选择题的题干,获取到答案并更新选项(题干中有多处为答案或者由多处括号,括号里是字母但不一定是答案的情况)
export const handleQuestionContent = (content, allText, data) => {
    if (!content || !allText) return ''
    let successContent = ''
    // 去掉数字开头
    let newTextAll = allText.replace(/^\d+[.、.]\s*/, '')
    // 找到传入的题干在所有字符串中的位置
    let contentIndex = newTextAll.indexOf(content)
    // 截取选项之前的内容比对
    let regx1 = /^(.*?)(?=\s*[A-Za-z]\.?[.、.])/s
    let regx2 = /^(.*?)(?=[A-Za-z](?:(?:\s*\.\s*)|(?:\s*,\s*)|$))/s
    let matchArr1 = newTextAll.match(regx1)
    let matchArr2 = newTextAll.match(regx2)
    let matchArr = []
    if (matchArr1 && matchArr2) { // 两个都匹配比较谁匹配更接近
        matchArr = matchArr1[0].length > matchArr2[0].length ? matchArr1 : matchArr2
    } else if (matchArr1 || matchArr2) { // 有一个不能匹配直接获取能匹配那个
        matchArr = matchArr1 ? matchArr1 : matchArr2
    } else {
        matchArr = null
    }
    // 已有的题干和真正的不同,需要对已有信息进行修改
    if (matchArr && matchArr.length > 0 && matchArr[0] !== content) {
        // 选项之前的内容
        successContent = matchArr[0]
        // 去掉空行
        successContent = successContent.replace(/(\r?\n\s*)+/g, '\n')
        let answers = data.answerList.map(item => { return item.answerTitle })
        // 从括号中找到真正的答案
        let answerKeyRegex = /[\((]\s*([A-Z]+)\s*[\))]/g
        let contentArr = successContent.split(answerKeyRegex)
        let resultContent = ''
        let successAnswerArr = []
        contentArr.map(item => {
            let regxAnswer = /^[A-Za-z]+$/g
            // 仅为大小写字母
            if (regxAnswer.test(item)) {
                // 只有一个字母,并且字母在已生成的选项中,说明是其中的一个答案
                if (item.length === 1 && answers.includes(item.toLocaleUpperCase())) { 
                    // 替换成括号
                    resultContent += '()'
                    // 记录出真正的答案,在最后去编辑选项设置选中
                    !successAnswerArr.includes(item.toLocaleUpperCase()) && successAnswerArr.push(item.toLocaleUpperCase())
                } else if (item.length > 1) { 
                    /**
                     * 多个字母需要判断:
                     * 1.字母有重复说明不是答案,直接还原
                     * 2.字母不重复但是有字母不在已生成的选项中,直接还原
                     * 3.字母不重复并且都在选项中为答案,同时将data中的试题类型修改为多选,选项默认选中项需要更改
                     */
                    let itemArr = item.split('').filter(val => { return val !== '' })
                    let newArr = [...new Set(JSON.parse(JSON.stringify(itemArr)))]
                    if (itemArr.length !== newArr.length) { // 条件1
                        resultContent += `${item}`
                    } else {
                        let isInner = true
                        for(let i = 0; i < newArr.length; i++) {
                            newArr[i] = newArr[i].toLocaleUpperCase()
                            if (!answers.includes(newArr[i])) { // 条件2
                                resultContent += `${item}`
                                isInner = false
                                break // 退出循环
                            }
                        }
                        // 条件3,记录正确选项
                        if (isInner) {
                            // 替换成括号
                            resultContent += '()'
                            // 记录不重复的答案
                            successAnswerArr = [...new Set(successAnswerArr.concat(itemArr))]
                        }
                    }
                } else {
                    // 还原
                    resultContent += `${item}`
                }
            } else {
                resultContent += item
            }
        })
        // 更新题干
        data.questionContent =  resultContent
        // 更新试题类型
        if (successAnswerArr.length > 1) {
            data.questionType = '01'
        } else {
            data.questionType = '00'
        }
        // 处理选项
        data.answerList.map((item, index) => {
            // 当前项为答案要默认选中
            if (successAnswerArr.includes(item.answerTitle)) {
                item.answerRight = data.questionType === '00' ? index : '0'
            } else {
                item.answerRight = data.questionType === '00' ? false : '1'
            }
        })
    } else {
        data.questionContent = content + '()'
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415

这是我简单自定义的一个编辑器,其实是一个contenteditable的div,对里面内容进行简单处理了之后就可以使用了


<template>
    <div 
        class="custom-editor"
        :style="{
            height: height + 'px'
        }">
        <div class="custom-editor-placeholder" :style="{ display: content ? 'none' : 'block' }">{{ placeholder }}</div>
        <div 
            class="custom-editor-content" 
            id="cusEditor"
            :contenteditable="!disabled">
        </div>
    </div>
</template>

<script setup>
import { onMounted, ref } from 'vue'


const props = defineProps({
    height: {
        type: Number,
        default: 300
    },
    disabled: {
        type: Boolean,
        default: false
    },
    placeholder: {
        type: String,
        default: ''
    }
})

let content = ref('')
let customEditor = ref(null)

const emits = defineEmits(['change'])

onMounted(() => {
    customEditor.value = document.getElementById('cusEditor')
    customEditor.value.addEventListener('input', (e) => {
        content.value = e.target.innerText
        emits('change', customEditor.value.innerText)
    })
    // 自定义粘贴,去掉图片,更改文字颜色(匹配系统颜色)
    customEditor.value.addEventListener('paste', async (e) => {
        e.preventDefault()

        let htmlContent = ''

        // 尝试从现代API获取HTML内容
        if (e.clipboardData && e.clipboardData.types.includes('text/html')) {
            htmlContent = e.clipboardData.getData('text/html')
        } else if (e.originalEvent && e.originalEvent.clipboardData && e.originalEvent.clipboardData.getData) {
            htmlContent = e.originalEvent.clipboardData.getData('text/html')
        } else {
            htmlContent = (e.clipboardData || window.clipboardData).getData('text')
        }
        // 获取粘贴的纯文本,便于后面比较,避免粘贴内容不全
        let pasteText = (e?.clipboardData || window?.clipboardData)?.getData('text')
        // 保存当前的选区
        const selection = window.getSelection()
        const range = selection.getRangeAt(0)

        // 使用DOMParser解析粘贴的HTML内容
        const parser = new DOMParser()
        const doc = parser.parseFromString(htmlContent, 'text/html')
        /**  重要
         * 处理文本节点,一定要替换掉font节点,
         * 因为font节点获取内容会包括了css样式(比如字体、颜色、大小等等)转换成字符串的结果
         * 无论是innerText还是textContent都是一样的结果,严重影响填空题识别
         */
        walkTree(doc.body) 

        // ********重要*********
        // 直接创建一个div存放,现在无法找到又能在同一行又能保留原先样式粘贴进去,
        // 要在原有文字后面直接挨着来需要清除文字样式,会导致选择题无法识别
        let div = document.createElement('div')
        let childNodes = doc.body.childNodes
        childNodes.forEach(node => {
            if (![Node.ATTRIBUTE_NODE, Node.COMMENT_NODE, Node.DOCUMENT_TYPE_NODE, Node.DOCUMENT_FRAGMENT_NODE].includes(node.nodeType)) {
                div.appendChild(node)
            }
        })
        
        // 移除所有的img标签
        const imgs = div.querySelectorAll('img')
        imgs.forEach(img => img.remove())

        // 更改文字样式,匹配系统颜色
        setBodyTextStyle(div, 'var(--el-text-color)', '12px', 'transparent')

        // 粘贴内容不全时进行修正
        if (pasteText && div.innerText !== pasteText) {
            div.innerText = pasteText
        }

        // 在原有位置插入处理过的内容
        range.deleteContents() // 如果要替换选中内容,则先删除
        range.insertNode(div) // 插入编辑器
        range.collapse(true)
        selection.removeAllRanges()
        selection.addRange(range)

        content.value = customEditor.value.innerText
        emits('change', customEditor.value.innerText)
    })
})

// 设置文字颜色以及文字大小,匹配系统颜色
const setBodyTextStyle = (body, color, fontSize, bgc) => {
    // 创建一个递归函数来遍历并设置颜色
    function setColorRecursively(element) {
        if (element.nodeType === Node.ELEMENT_NODE) {
            // 如果是元素节点
            for (let i = 0; i < element.childNodes.length; i++) {
                setColorRecursively(element.childNodes[i])
            }
            // 设置当前元素的文本颜色
            if (element.style) {
                element.style.color = color
                element.style.fontSize = fontSize
                element.style.backgroundColor = bgc
                element.style.padding = 0
                element.style.margin = 0
                element.style.lineHeight = 20 + 'px'
            }
        } else if (element.nodeType === Node.TEXT_NODE) {
            // 如果是文本节点,查找其父元素并设置颜色
            if (element.parentElement.style) {
                element.parentElement.style.color = color
                element.parentElement.style.fontSize = fontSize
                element.parentElement.style.backgroundColor = bgc
                element.parentElement.style.padding = 0
                element.parentElement.style.margin = 0
                element.parentElement.style.lineHeight = 20 + 'px'
            }
        }
    }

    // 从body开始遍历
    setColorRecursively(body)
}


// 清理文本节点,并转换所有非span元素的文本节点为span,比如是font
const walkTree = (node) => {
    if (node.nodeType === Node.TEXT_NODE && node.tagName === 'FONT') {
        var span = document.createElement('span')
        while (node.firstChild) {
            span.appendChild(node.firstChild)
        }
        node.parentNode.replaceChild(span, node)
    } else if (node.nodeType === Node.ELEMENT_NODE) {
        for (var i = 0; i < node.childNodes.length; i++) {
            walkTree(node.childNodes[i])
        }
    }
}

const clear = () => {
    content.value = ''
    customEditor.value.innerText = ''
    emits('change', customEditor.value.innerText)
}

defineExpose({
    customEditor,
    clear
})
</script>

<style lang="scss" scoped>
.custom-editor {
    position: relative;
    width: 100%;
    padding: 16px;
    z-index: 10000;
    .custom-editor-placeholder {
        position: absolute;
        top: 16px;
        left: 16px;
        color: var(--el-text-color-placeholder);
        opacity: .5;
        font-size: 13px;
        font-size: SourceHanSansCN Regular;
        z-index: 10001;
        line-height: 23px;
    }
    .custom-editor-content {
        position: relative;
        width: 100%;
        height: 100%;
        overflow-y: auto;
        outline: none;
        border: none;
        box-shadow: none;
        z-index: 10002;
        line-height: 23px;
    }
}
span {
    font-size: 12px;
    font-family: SourceHanSansCN Regular;
}
</style>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208

组件使用示例

<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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
// 识别
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()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

自己做的试题编辑的组件

<!--根据最新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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473
  • 474
  • 475
  • 476
  • 477
  • 478
  • 479
  • 480
  • 481
  • 482
  • 483
  • 484
  • 485
  • 486
  • 487
  • 488
  • 489
  • 490
  • 491
  • 492
  • 493
  • 494
  • 495
  • 496
  • 497
  • 498
  • 499
  • 500
  • 501
  • 502
  • 503
  • 504
  • 505
  • 506
  • 507
  • 508
  • 509
  • 510
  • 511
  • 512
  • 513
  • 514
  • 515
  • 516
  • 517
  • 518
  • 519
  • 520
  • 521
  • 522
  • 523
  • 524
  • 525
  • 526
  • 527
  • 528
  • 529
  • 530
  • 531
  • 532
  • 533
  • 534
  • 535
  • 536
  • 537
  • 538
  • 539
  • 540
  • 541
  • 542
  • 543
  • 544
  • 545
  • 546
  • 547
  • 548
  • 549
  • 550
  • 551
  • 552
  • 553
  • 554
  • 555
  • 556
  • 557
  • 558
  • 559
  • 560
  • 561
  • 562
  • 563
  • 564
  • 565
  • 566
  • 567
  • 568
  • 569
  • 570
  • 571
  • 572
  • 573
  • 574
  • 575
  • 576
  • 577
  • 578
  • 579
  • 580
  • 581
  • 582
  • 583
  • 584
  • 585
  • 586
  • 587
  • 588
  • 589
  • 590
  • 591
  • 592
  • 593
  • 594
  • 595
  • 596
  • 597
  • 598
  • 599
  • 600
  • 601
  • 602
  • 603
  • 604
  • 605
  • 606
  • 607
  • 608
  • 609
  • 610
  • 611
  • 612
  • 613
  • 614
  • 615
  • 616
  • 617
  • 618
  • 619
  • 620
  • 621
  • 622
  • 623
  • 624
  • 625
  • 626
  • 627
  • 628
  • 629
  • 630
  • 631
  • 632
  • 633
  • 634
  • 635
  • 636
  • 637
  • 638
  • 639
  • 640
  • 641
  • 642
  • 643
  • 644
  • 645
  • 646
  • 647
  • 648
  • 649
  • 650
  • 651
  • 652
  • 653
  • 654
  • 655
  • 656
  • 657
  • 658
  • 659
  • 660
  • 661
  • 662
  • 663
  • 664
  • 665
  • 666
  • 667
  • 668
  • 669
  • 670
  • 671
  • 672
  • 673
  • 674
  • 675
  • 676
  • 677
  • 678
  • 679
  • 680
  • 681
  • 682
  • 683
  • 684
  • 685
  • 686
  • 687
  • 688
  • 689
  • 690
  • 691
  • 692
  • 693
  • 694
  • 695
  • 696
  • 697
  • 698
  • 699
  • 700
  • 701
  • 702
  • 703
  • 704
  • 705
  • 706
  • 707
  • 708
  • 709
  • 710
  • 711
  • 712
  • 713
  • 714
  • 715
  • 716
  • 717
  • 718
  • 719
  • 720
  • 721
  • 722
  • 723
  • 724
  • 725
  • 726
  • 727
  • 728
  • 729
  • 730
  • 731
  • 732
  • 733
  • 734
  • 735
  • 736
  • 737
  • 738
  • 739
  • 740
  • 741
  • 742
  • 743
  • 744
  • 745
  • 746
  • 747
  • 748
  • 749
  • 750
  • 751
  • 752
  • 753
  • 754
  • 755
  • 756
  • 757
  • 758
  • 759
  • 760
  • 761
  • 762
  • 763
  • 764
  • 765
  • 766
  • 767
  • 768
  • 769
  • 770
  • 771
  • 772
  • 773
  • 774
  • 775
  • 776
  • 777
  • 778
  • 779
  • 780
  • 781
  • 782
  • 783
  • 784
  • 785
  • 786
  • 787
  • 788
  • 789
  • 790
  • 791
  • 792
  • 793
  • 794
  • 795
  • 796
  • 797
  • 798
  • 799
  • 800
  • 801
  • 802
  • 803
  • 804
  • 805
  • 806
  • 807
  • 808
  • 809
  • 810
  • 811
  • 812
  • 813
  • 814
  • 815
  • 816
  • 817
  • 818
  • 819
  • 820
  • 821
  • 822
  • 823
  • 824
  • 825
  • 826
  • 827
  • 828
  • 829
  • 830
  • 831
  • 832
  • 833
  • 834
  • 835
  • 836
  • 837
  • 838
  • 839
  • 840
  • 841
  • 842
  • 843
  • 844
  • 845
  • 846
  • 847
  • 848
  • 849
  • 850
  • 851
  • 852
  • 853
  • 854
  • 855
  • 856
  • 857
  • 858
  • 859
  • 860
  • 861
  • 862
  • 863
  • 864
  • 865
  • 866
  • 867
  • 868
  • 869
  • 870
  • 871
  • 872
  • 873
  • 874
  • 875
  • 876
  • 877
  • 878
  • 879
  • 880
  • 881
  • 882
  • 883
  • 884
  • 885
  • 886
  • 887
  • 888
  • 889
  • 890
  • 891
  • 892
  • 893
  • 894
  • 895
  • 896
  • 897
  • 898
  • 899
  • 900
  • 901
  • 902
  • 903
  • 904
  • 905
  • 906
  • 907
  • 908
  • 909
  • 910
  • 911
  • 912
  • 913
  • 914
  • 915
  • 916
  • 917
  • 918
  • 919
  • 920
  • 921
  • 922
  • 923
  • 924
  • 925
  • 926
  • 927
  • 928
  • 929
  • 930
  • 931
  • 932
  • 933
  • 934
  • 935
  • 936
  • 937
  • 938
  • 939
  • 940
  • 941
  • 942
  • 943
  • 944
  • 945
  • 946
  • 947
  • 948
  • 949
  • 950
  • 951
  • 952
  • 953
  • 954
  • 955
  • 956
  • 957
  • 958
  • 959
  • 960
  • 961
  • 962
  • 963
  • 964
  • 965
  • 966
  • 967
  • 968
  • 969
  • 970
  • 971
  • 972
  • 973
  • 974
  • 975
  • 976
  • 977
  • 978
  • 979
  • 980
  • 981
  • 982
  • 983
  • 984
  • 985
  • 986
  • 987
  • 988
  • 989
  • 990
  • 991
  • 992
  • 993
  • 994
  • 995
  • 996
  • 997
  • 998
  • 999
  • 1000
  • 1001
  • 1002
  • 1003
  • 1004
  • 1005
  • 1006
  • 1007
  • 1008
  • 1009
  • 1010
  • 1011
  • 1012
  • 1013
  • 1014
  • 1015
  • 1016
  • 1017
  • 1018
  • 1019
  • 1020
  • 1021
  • 1022
  • 1023
  • 1024
  • 1025
  • 1026
  • 1027
  • 1028
  • 1029
  • 1030
  • 1031
  • 1032
  • 1033
  • 1034
  • 1035
  • 1036
  • 1037
  • 1038
  • 1039
  • 1040
  • 1041
  • 1042
  • 1043
  • 1044
  • 1045
  • 1046
  • 1047
  • 1048
  • 1049
  • 1050
  • 1051
  • 1052
  • 1053
  • 1054
  • 1055
  • 1056
  • 1057
  • 1058
  • 1059
  • 1060
  • 1061
  • 1062
  • 1063
  • 1064
  • 1065
  • 1066
  • 1067
  • 1068
  • 1069
  • 1070
  • 1071
  • 1072
  • 1073
  • 1074
  • 1075
  • 1076
  • 1077
  • 1078
  • 1079
  • 1080
  • 1081
  • 1082
  • 1083
  • 1084
  • 1085
  • 1086
  • 1087
  • 1088
  • 1089
  • 1090
  • 1091
  • 1092
  • 1093
  • 1094
  • 1095
  • 1096
  • 1097
  • 1098
  • 1099
  • 1100
  • 1101
  • 1102
  • 1103
  • 1104
  • 1105
  • 1106
  • 1107
  • 1108
  • 1109
  • 1110
  • 1111
  • 1112
  • 1113
  • 1114
  • 1115
  • 1116
  • 1117
  • 1118
  • 1119
  • 1120
  • 1121
  • 1122
  • 1123
  • 1124
  • 1125
  • 1126
  • 1127
  • 1128
  • 1129
  • 1130
  • 1131
  • 1132
  • 1133
  • 1134
  • 1135
  • 1136
  • 1137
  • 1138
  • 1139
  • 1140
  • 1141
  • 1142
  • 1143
  • 1144
  • 1145
  • 1146
  • 1147
  • 1148
  • 1149
  • 1150
  • 1151
  • 1152
  • 1153
  • 1154
  • 1155
  • 1156
  • 1157
  • 1158
  • 1159
  • 1160
  • 1161
  • 1162
  • 1163
  • 1164
  • 1165
  • 1166
  • 1167
  • 1168
  • 1169
  • 1170
  • 1171
  • 1172
  • 1173
  • 1174
  • 1175
  • 1176
  • 1177
  • 1178
  • 1179
  • 1180
  • 1181
  • 1182
  • 1183
  • 1184
  • 1185
  • 1186
  • 1187
  • 1188
  • 1189
  • 1190
  • 1191
  • 1192
  • 1193
  • 1194
  • 1195
  • 1196
  • 1197
  • 1198
  • 1199
  • 1200
  • 1201
  • 1202
  • 1203
  • 1204
  • 1205
  • 1206
  • 1207
  • 1208
  • 1209
  • 1210
  • 1211
  • 1212
  • 1213
  • 1214
  • 1215
  • 1216
  • 1217
  • 1218
  • 1219
  • 1220
  • 1221
  • 1222
  • 1223
  • 1224
  • 1225
  • 1226
  • 1227
  • 1228
  • 1229
  • 1230
  • 1231
  • 1232
  • 1233
  • 1234
  • 1235
  • 1236
  • 1237
  • 1238
  • 1239
  • 1240
  • 1241
  • 1242
  • 1243
  • 1244
  • 1245
  • 1246
  • 1247
  • 1248
  • 1249
  • 1250
  • 1251
  • 1252
  • 1253
  • 1254
  • 1255
  • 1256
  • 1257
  • 1258
  • 1259
  • 1260
  • 1261
  • 1262
  • 1263
  • 1264
  • 1265
  • 1266
  • 1267
  • 1268
  • 1269
  • 1270
  • 1271
  • 1272
  • 1273
  • 1274
  • 1275
  • 1276
  • 1277
  • 1278
  • 1279
  • 1280
  • 1281
  • 1282
  • 1283
  • 1284

以下是效果图

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/648645
推荐阅读
相关标签
  

闽ICP备14008679号