This commit is contained in:
“zouyf” 2024-11-20 09:55:42 +08:00
commit 4bc5e3411f
3 changed files with 894 additions and 5 deletions

View File

@ -79,6 +79,7 @@
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"number-precision": "^1.6.0", "number-precision": "^1.6.0",
"vue-cropper": "1.0.3", "vue-cropper": "1.0.3",
"qs": "^6.12.0",
"pptxgenjs": "^3.12.0", "pptxgenjs": "^3.12.0",
"pptxtojson": "^1.0.3", "pptxtojson": "^1.0.3",
"prosemirror-commands": "^1.6.0", "prosemirror-commands": "^1.6.0",

View File

@ -3,7 +3,7 @@
<div class="page-left"> <div class="page-left">
<el-tabs v-model="activeAptTab" style="height: 100%;"> <el-tabs v-model="activeAptTab" style="height: 100%;">
<el-tab-pane label="人工录入" name="人工录入" class="prepare-center-zglr"> <el-tab-pane label="人工录入" name="人工录入" class="prepare-center-zglr">
<div> <div class="row-import-manual">
<vue-cropper <vue-cropper
class="import-manual-cropper" class="import-manual-cropper"
ref="cropper" ref="cropper"
@ -41,6 +41,14 @@
<el-button type="primary" size="small" class="manual-crop-menu-browse" @click="getClipboardImg">获取剪贴板图片</el-button> <el-button type="primary" size="small" class="manual-crop-menu-browse" @click="getClipboardImg">获取剪贴板图片</el-button>
<el-button type="primary" size="small" class="manual-crop-menu-whole" @click="identifyOverallImg">整题识别</el-button> <el-button type="primary" size="small" class="manual-crop-menu-whole" @click="identifyOverallImg">整题识别</el-button>
</div> </div>
<!-- orc 使用说明 -->
<div class="import-manual-explain">
<p>orc 使用说明</p>
<p>1本地浏览 </p>
<p>2获取剪贴板图片 </p>
<p>3整题识别 </p>
<p> </p>
</div>
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
@ -52,9 +60,9 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref,watch, reactive, getCurrentInstance,nextTick } from 'vue'
import "vue-cropper/dist/index.css"; import "vue-cropper/dist/index.css";
import { VueCropper } from "vue-cropper"; import { VueCropper } from "vue-cropper";
import { onMounted, ref,watch, reactive, getCurrentInstance,nextTick } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
@ -65,6 +73,8 @@ import { useGetHomework } from '@/hooks/useGetHomework'
import { sessionStore } from '@/utils/store' import { sessionStore } from '@/utils/store'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import { ocrImg2ExamByManualUpl, ocrImg2ItemByManualUpl } from "@/views/classTask/newClassTaskAssign/questionUpload/ocrImg2ExamQues";
const userStore = useUserStore().user const userStore = useUserStore().user
const route = useRoute(); const route = useRoute();
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
@ -81,10 +91,10 @@ const courseObj = reactive({
node: null, // node: null, //
// //
}) })
const activeAptTab = ref("自主搜题"); const activeAptTab = ref("人工录入");
// const taskList = ref([]); //
// const tasklist_loading = ref(false); //
// false - api true - json
const OCR_WORK_TEST = false;
// [] // []
const cropOption = reactive({ const cropOption = reactive({
img: '', // url , base64, blob img: '', // url , base64, blob
@ -137,6 +147,73 @@ const initHomeWork = async()=> {
// tasklist_loading.value = false; // tasklist_loading.value = false;
} }
/**
* @desc: 上传本地图片
* @return: {*}
* @param {*} uploadFile 上传的文件
*/
const handleImportImg = (uploadFile) => {
if (!/\.(jpg|jpeg|png|JPG|PNG)$/.test(uploadFile.name)) {
ElMessage({
message: '图片类型要求: jpeg、jpg、png',
type: 'error',
});
return;
}
console.log('uploadFile', uploadFile);
//
// cropOption.img = window.URL.createObjectURL(uploadFile.raw);
cropOption.img = window.URL.createObjectURL(new Blob([uploadFile.raw]));
console.log(cropOption.img);
ElMessage.success('上传成功');
};
/**
* @desc: 获取剪贴板图片
* @return: {*}
*/
const getClipboardImg = async() => {
try {
const clipboardItems = await navigator.clipboard.read();
for (const item of clipboardItems) {
for (const type of item.types) {
if (type.includes('image/')) {
const blob = await item.getType(type);
// blob Blob
cropOption.img = URL.createObjectURL(blob);
ElMessage.success('获取剪贴板图片成功');
}
}
}
} catch (error) {
console.error('Failed to read clipboard contents:', error);
}
};
/**
* @desc: 根据截取图片做[整题]识别格式化
* @return: {*}
*/
const identifyOverallImg =()=>{
if (cropOption.img == null || cropOption.img == '') {
ElMessage({
message: '识别区域中无图片, 禁止识别',
type: 'error',
});
return;
}
proxy.$refs.cropper.getCropData(async (data) => {
const examQues = await ocrImg2ExamByManualUpl(OCR_WORK_TEST, data);
nextTick( () => {
// index: submitType: 1- 2- 3-
const submitType = 2;
examQues.status = '1';
proxy.$refs.refquesItem.updateForm(examQues, 0, submitType);
})
});
};
</script> </script>
@ -165,7 +242,42 @@ const initHomeWork = async()=> {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.row-import-manual{
height: 100%;
display: flex;
flex-direction: column;
.import-manual-cropper{
width: 100%;
// height: 560px;
min-height: 400px;
border: 1px solid #000;
} }
.import-manual-crop-menu{
display: flex;
background-color: #e8e9eb;
.manual-crop-menu-browse{
margin-right: 10px;
}
.manual-crop-menu-whole{
margin-left: auto;
}
}
.import-manual-explain {
text-align: left;
}
}
}
} }
.page-right { .page-right {
width: 100%; width: 100%;

View File

@ -0,0 +1,776 @@
import { ElMessageBox, ElMessage } from "element-plus";
import qs from "qs";
import { pyOCRAPI } from "@/api/education/entpcoursework";
const EXAM_JUDGED_DICTIONARY = ["正确", "对", "√", "T", "错误", "错", "×", "F"];
const baidubceConfig = {
// Header
'Content-Type': "application/x-www-form-urlencoded",
// 格式
'Accept' : 'application/json',
// id(临时测试)
'client_id': "U0DrGBE6X92IXgV6cJMNON8F",
// 密钥(临时测试)
'client_secret': 'oWb0M0YWMmZPMQIhIUkJX99ddr7h61qf',
};
/**
* @desc: [人工录入]中识别[单项]内容
* @return: {*}
* @param {boolean} isLocalTest 本地测试
* @param {string} imgBase64 识别图片的base64
* @param {string} examType 需识别的试题类型
* @param {string} curItem 需识别的单项类型
* [curItem] 参数说明: title-题目 workdesc-单选题选项 workdesc:single:multi:blanks:judge:QAA(questions and answers)-复合题选项 workanswer-答案
* method-答案分析 analyse-答案解答 discuss-答案点评
*/
export const ocrImg2ItemByManualUpl = async (isLocalTest = false, imgBase64 = '', examType = '', curItem = '') => {
let examItem = null;
let ocrJson = null;
let regex = null;
// 识别内容拼接
let ocrTxt = ''
if(isLocalTest) {
// 临时本地测试json格式跟百度ocr一致
const response = await fetch('/cropImgTest/single.json');
const resOcr = await response.json();
ocrJson = resOcr.results;
// 识别内容拼接
ocrJson.forEach(ele => {
ocrTxt += `${ele.words.word}<br />`;
});
//--------------------------------------------------------------
// 备用ocr识别服务 python的一个识别服务
// const response = await ocrImgPyJson(imgBase64);
// if(!response?.data) {
// return examQues;
// }
// ocrJson = response.data;
// // 识别内容拼接
// ocrJson.forEach(ele => {
// ocrTxt += `${ele}<br />`;
// });
}
else {
const tmp = await ocrImg2Json(imgBase64);
if(!tmp?.data) {
return examItem;
}
ocrJson = tmp.data.results;
// 识别内容拼接
ocrJson.forEach(ele => {
ocrTxt += `${ele.words.word}<br />`;
});
}
if(ocrJson == '') {
ElMessage.error('[人工录入-单项]识别的图片为空, 识别失败, 请检查重试!');
return examItem;
}
if(ocrTxt == '') {
ElMessage.error('[人工录入-单项]识别内容拼接失败, 请检查重试!');
return examItem;
}
ocrTxt = ocrTxt.trim();
// 根据[单项类型]转换对应的识别内容
if (curItem === 'title' || curItem === 'method' || curItem === 'analyse' || curItem === 'discuss') {
regex = /^(\d*[..。])?(\(.*?\)|.*?\)/g
// 去掉开头的序号和题源(针对题目) + 去掉自定义的<br />标签
ocrTxt = ocrTxt.replace(regex, '').replace(/<br \/>/g, '');
examItem = ocrTxt;
}
else if (curItem === 'workdesc') {
// 该类型下无需[判断题]和[主观题]处理
if (examType.includes('复合题')) {
// 因[题目+选项]分离正则匹配需要, 故需开头手动拼一个<br />
let mutiParams = processExamMulti(`<br />${ocrTxt}`, '');
examItem = {
worktype: '单选题',
params: [],
}
mutiParams.arrWorkDesc.forEach( item => {
const obj = {
title: item.title,
workanswer: '',
checkAnswer: [],
type: item.type,
options: item.options.map(element => {return {text: element.replace(/<br \/>/g, '')}}),
}
examItem.params.push(obj);
});
return examItem;
}
else if (examType.includes('单选题') || examType.includes('多选题')) {
/** 单选题/多选题 - 选项 */
// 先判断是否存在选项标识, 且存在2个及以上(A.---1.---(1)---1)
regex = /\s*[A-H][..。]/g;
const matches = ocrTxt.match(regex);
if (matches==null || matches.length < 2){
ElMessage.error('[人工录入-单项]识别[选项]失败, 请检查重试!');
return examItem;
}
regex = /\s*[A-H][..。]/g;
examItem = ocrTxt.split(regex);
examItem.splice(0, 1); //将分隔出来的第一组空字符去掉(后续是否有空字符不管)
examItem = examItem.map(item => {
const obj = {
text: item.replace(/<br \/>/g, ''),
}
return obj;
})
return examItem;
}
else if (examType.includes('填空题')) {
// 填空题 - 选项
const obj = {
text: ocrTxt.replace(/<br \/>/g, ' ')
}
examItem = [];
examItem.push(obj);
return examItem;
}
}
else if (curItem === 'workanswer') {
// 该类型下只做[主观题]和[复合题]的处理
if (examType.includes('主观题')) {
ocrTxt = ocrTxt.replace(/<br \/>/g, '');
examItem = ocrTxt;
}
}
// 返回转换格式后的识别内容
return examItem;
}
/**
* @desc: [人工录入]中识别[整题]试题
* @return: {*}
* @param {*} isLocalTest 本地测试
* @param {*} imgBase64 识别图片的base64
*/
export const ocrImg2ExamByManualUpl = async (isLocalTest = false, imgBase64 = '') => {
let examQues = {};
let ocrJson = '';
// 识别内容拼接
let ocrTxt = '';
if(isLocalTest) {
// 临时本地测试json格式跟百度ocr一致
const response = await fetch('/cropImgTest/single.json');
const resOcr = await response.json();
ocrJson = resOcr.results;
// 识别内容拼接
ocrJson.forEach(ele => {
ocrTxt += `${ele.words.word}<br />`;
});
//--------------------------------------------------------------
// 备用ocr识别服务 python的一个识别服务
// const response = await ocrImgPyJson(imgBase64);
// if(!response?.data) {
// return examQues;
// }
// ocrJson = response.data;
// ocrJson.forEach(ele => {
// ocrTxt += `${ele}<br />`;
// });
} else {
const tmp = await ocrImg2Json(imgBase64);
if(!tmp?.data) {
return examQues;
}
ocrJson = tmp.data.results;
ocrJson.forEach(ele => {
ocrTxt += `${ele.words.word}<br />`;
});
}
if(ocrJson == '') {
ElMessage.error('[人工录入-整题]图片识别内容为空, 识别失败, 请重试!');
return examQues;
}
if(ocrTxt == '') {
ElMessage.error('[人工录入-整题]识别内容拼接失败, 请重试!');
return examQues;
}
// 识别内容转为试题结构
examQues = assembleExam(ocrTxt);
if(examQues.err != '') {
ElMessage.error(`[人工录入-整题]${examQues.err}, 请重试!`);
examQues = {};
}
return examQues;
}
/**
* @desc: 百度云api识别图片转json
* @return: {*}
* @param {*} eachSub 图片的base64
*/
const ocrImg2Json = async (urlBase64) => {
//判断是否存在截取图片
if (!urlBase64 || urlBase64 == '') {
ElMessage.error("未检测到截图图片, 请截取图片后再识别");
return null;
}
const resToken = await bdyAPI_getToken();
if (resToken.status !== 200) {
ElMessage.error("百度智能云用户标识有误");
return null;
}
const token = resToken.data?.access_token;
let base64Code = urlBase64.split(",")[1];
const query = {
image: base64Code, //图片地址(base64)
line_probability: false, //是否返回每行识别结果的置信度。默认为false
disp_line_poly: false, //是否返回每行的四角点坐标。默认为false
words_type: 'handprint_mix', //文字类型。 默认:印刷文字识别 = handwring_only手写文字识别 = handprint_mix 手写印刷混排识别
layout_analysis: false, //是否分析文档版面包括layout图、表、标题、段落、目录attribute栏、页眉、页脚、页码、脚注的分析输出
recg_long_division: false, //是否检测并识别手写竖式
recg_formula: true, //控制是否检测并识别公式默认为false
}
const resOcr = await bdyAPI_getOcrContent(token, base64Code, query);
if (resOcr.status !== 200) {
ElMessage.error("百度智能云图片识别错误");
return null;
}
return resOcr;
}
/**
* @desc: python_ocr备用方案识别图片转json
* @return: {*}
* @param {*} eachSub 图片的base64
*/
const ocrImgPyJson = async (urlBase64) => {
//判断是否存在截取图片
if (!urlBase64 || urlBase64 == '') {
ElMessage.error("未检测到截图图片, 请截取图片后再识别");
return null;
}
const resOcr = await pyOCRAPI(urlBase64);
if (resOcr.status !== 200) {
ElMessage.error("图片识别错误");
return null;
}
return resOcr;
}
/** [百度智能云]获取token */
const bdyAPI_getToken = async function () {
return axios({
headers: {
'Content-Type': `${baidubceConfig['Content-Type']}`,
},
method: 'POST',
url: `/baidubce/oauth/2.0/token?grant_type=client_credentials&client_id=${baidubceConfig['client_id']}&client_secret=${baidubceConfig['client_secret']}`,
// data: {
// grant_type: 'client_credentials',
// client_id: `${baidubceConfig['client_id']}`,
// client_secret: `${baidubceConfig['client_secret']}`,
// },
})
}
/** [百度智能云]ocr图片识别 */
const bdyAPI_getOcrContent = async function (token, imgUrl, params) {
return axios({
headers: {
'Content-Type': `${baidubceConfig['Content-Type']}`,
'Accept': `${baidubceConfig['Accept']}`,
},
method: 'POST',
url: `/baidubce/rest/2.0/ocr/v1/doc_analysis?access_token=${token}`,
data: qs.stringify(params),
// data: {
// image: imgUrl, //图片地址(base64)
// line_probability: false, //是否返回每行识别结果的置信度。默认为false
// disp_line_poly: false, //是否返回每行的四角点坐标。默认为false
// words_type: 'handprint_mix', //文字类型。 默认:印刷文字识别 = handwring_only手写文字识别 = handprint_mix 手写印刷混排识别
// layout_analysis: false, //是否分析文档版面包括layout图、表、标题、段落、目录attribute栏、页眉、页脚、页码、脚注的分析输出
// recg_long_division: false, //是否检测并识别手写竖式
// recg_formula: true, //控制是否检测并识别公式默认为false
// },
})
}
/**
* @desc: 根据识别内容组装试题结构
* @return: {*}
* @param {*} eachSub 识别拼接完成后的整体内容
*/
const assembleExam = (eachSub) => {
let subObj = {
id: 0,
worktype: '单选题', // 题的类型 存的中文 单选题 多选题
workgroup: '0', // 1:真题 0非真题
examdate: '', // 题的生成时间(2024-04-16T00:00:00)
title: '', // 题目内容
workdesc: '', // 题目选项 #&使用这个分割开 A 0 B 1 C 2 D 3
workanswer: '', // 答案
workanalysis: '', // 解析3合1
worktag: '', // 题源信息( 2023•河北 中文括号+4位年份+ • +地区 )
difficulty: 0, // 试题难度(暂定为0-100)
timelength: 60, // 推荐用时(s)
status: '0', // 扫描上传时需将状态先置位为0(试题审核后改为1)
score: 4, // 试题基础分值
// 试题解析错误信息
err: '',
// 界面展示格式化
titleFormat: '',
workdescFormat: '',
workanswerFormat: '',
method: [], //分析
analyse: [], //解答
discuss: [], //点评
};
let regex = null;
let titleAndWorkDesc = '',
answer = '';
// 获取[题源] - 格式化
regex = /^(\d*[..。])?(\(.*?\)|.*?\)/g
let workTag = eachSub.match(regex);
if (workTag) {
subObj.worktag = workTag[0].replace(/^\d*[..。]/g, '');
subObj.worktag = subObj.worktag.replace('(', '').replace(')', '');
}
// 去掉开头的序号和题源
eachSub = eachSub.replace(regex, '');
// 先判断是否存在答案
regex = /[\[【]答案.*?[\]】]/g;
let hasAnswer = eachSub.match(regex);
if (!hasAnswer) {
// 不存在答案, 仅处理[题干+选项]
titleAndWorkDesc = eachSub;
}else {
// 存在答案, 需处理[题干+选项]和[答案+解析]
regex = /(<br \/>?\s*[【\[].*?[】\]])/g;
let tmpList = eachSub.split(regex);
if (tmpList.length < 2) {
subObj.err = '试题匹配答案失败, 请检查识别格式'
return subObj;
}
// 第一部分[题干-选项] 处理
titleAndWorkDesc = tmpList[0];
// 将4个以上连续的下划线统一替换为5个
titleAndWorkDesc = titleAndWorkDesc.replace(/_{4,}/g, '_____');
// 第二部分[分析-答案] 处理
let answerAndAnswer = {};
// 将第二部分的内容做key-value绑定 - 键为【分析】、【讨论】、【方法】等. 值为随之分隔的内容
for (let i=1; i<tmpList.length-1; i=i+2){
let key = tmpList[i];
key = key.replace(/<br \/>|【|】|\[|\]/g, '');
let value = tmpList[i+1];
value = value.replace(/^<br \/>+|<br \/>+$/g, '');
answerAndAnswer[key] = value;
}
// [试题解析] 处理
let method = '', analyse = '', discuss = '';
if (answerAndAnswer['试题立意']) {
discuss += `${answerAndAnswer['试题立意']}<br /><br />`;
}
if (answerAndAnswer['评分参考']) {
discuss += `${answerAndAnswer['评分参考']}<br /><br />`;
}
discuss = discuss.replace(/<br \/>+$/, "");
if (answerAndAnswer['能力素养']) {
method += `${answerAndAnswer['能力素养']}<br /><br />`;
}
if (answerAndAnswer['能力解读']) {
method += `${answerAndAnswer['能力解读']}<br /><br />`;
}
method = method.replace(/<br \/>+$/, "");
if (answerAndAnswer['误项排除']) {
analyse += `${answerAndAnswer['误项排除']}<br /><br />`;
}
if (answerAndAnswer['失分剖析']) {
analyse += `失分剖析: ${answerAndAnswer['失分剖析']}<br /><br />`;
}
analyse = analyse.replace(/<br \/>+$/, "");
// [试题解析] - 格式化
const jjj = { analyse: [analyse], discuss: [discuss], method: [method] };
subObj.workanalysis = JSON.stringify(jjj);
// [试题解析] - 界面展示格式化
subObj.method.push(method);
subObj.analyse.push(analyse);
subObj.discuss.push(discuss);
// [答案] - 初步初始化 --- 根据答案判断试题大分类: 复合题(实际为大题) 或 其他基础题型(单选,多选,填空,判断)
answer = answerAndAnswer['答案'].trim();
if(!answer) {
answer = answerAndAnswer['答案及评分参考'].trim();
answer = answer.replace(/^\d+[\u4e00-\u9fa5][..。]\s*<br \/>/, ''); // 去掉 - 有些开头会有[xx分。]
}
// 将多余的空格替换为固定的4个空格
answer = answer.replaceAll("\\s{3,}"," ");
if (answer == null | answer == '') {
subObj.err = '题目缺少[答案]';
return subObj;
}
}
let tmpExam = null;
if (answer === '') {
/**
* 基础题型 - [单选题] [多选题] [填空题] [判断题] [主观题]
*/
tmpExam = processExamSingle(titleAndWorkDesc, answer);
}
else {
// 匹配是否存在 1. 1 (1)的存在, 题目与答案都存在则说明题型为复合题(嵌套题)
regex = /^(\d+[..。]|\(\d+\)|\d+)/;
let answerFind = regex.test(answer);
regex = /(\d+[..。]|\(\d+\)|\d+)/;
let titleFind = regex.test(titleAndWorkDesc);
if(titleFind && answerFind){
/**
* [复合题] - 处理逻辑
*/
tmpExam = processExamMulti(titleAndWorkDesc, answer);
}
else {
/**
* 基础题型 - [单选题] [多选题] [填空题] [判断题] [主观题]
*/
tmpExam = processExamSingle(titleAndWorkDesc, answer);
}
}
if (tmpExam) {
// 错误信息
if(tmpExam.errMsg !== '') {
subObj.err = tmpExam.err;
return subObj;
}
subObj.worktype = tmpExam.workType;
subObj.title = tmpExam.title;
if (tmpExam.arrWorkDesc.length > 0) {
subObj.workdesc = JSON.stringify(tmpExam.arrWorkDesc);
}
if (tmpExam.arrWorkAnswer.length > 0) {
subObj.workanswer = JSON.stringify(tmpExam.arrWorkAnswer);
}
}
return subObj;
}
/**
* @desc: 单题(基础题) 处理逻辑
* @return: {*}
* @param {*} titleAndWorkDesc [题干]+[选项]
* @param {*} answer [答案]
*/
const processExamSingle = function (titleAndWorkDesc, answer) {
let examSingle = {
workType: '单选题',
title: '',
arrWorkDesc: [],
arrWorkAnswer: [],
errMsg: '', //以此判断当前是否处理成功
}
let tmpSplit = [];
let regex = null;
let matcher = null;
/** [判断题]的处理逻辑, resp: -1-未找到 0-*为对应匹配的index */
let judgedStatus = answer!=='' ? containsExactMatch(answer) : -1;
/** 其他基础题型(单选,多选,填空,判断)的处理逻辑 */
// 先去掉开头的试题序号
regex = /^\d+[..。]\s*/;
titleAndWorkDesc = titleAndWorkDesc.replace(regex, '').trim();
// 题型判断
regex = /<br \/>\s*[A-H][..。]/
if (regex.test(titleAndWorkDesc)) {
/**
* [单选题][多选题]
*/
answer = answer.replace("<br />", "").trim();
// [题型] - 格式化 - 根据答案字符个数区分[单选]或[多选]
examSingle.workType = answer==='' ? '单选题' : answer.length == 1 ? "单选题" : "多选题";
// 切分题干+选项
regex = /<br \/>*\s*[A-H][..。]/g;
tmpSplit = titleAndWorkDesc.split(regex);
// [题干]-格式化 --- 正常数据
examSingle.title = tmpSplit[0].trim();
// [选项]-处理 --- ['ABC123','ABC123']
for (let i = 1; i < tmpSplit.length; i++) {
let option = tmpSplit[i].replace("<br />", "").trim();
//option = option.replace("_", "");
// [选项] - 格式化
examSingle.arrWorkDesc.push(option);
}
// [题目答案] --- ['0'] | ['0','1']
if (answer !== '') {
// 答案为空时, 置空后直接返回
let ans2num = ''
for (let i = 0; i < answer.length; i++) {
ans2num += (answer.charCodeAt(i) - 65).toString();
}
// [题目答案] - 格式化
examSingle.arrWorkAnswer = ans2num.split('').sort((a, b) => a - b);
}
}
else if (titleAndWorkDesc.indexOf("_____") != -1) {
/**
* 填空题
*/
// [题型] - 格式化
examSingle.workType = "填空题";
// [题干]-格式化
examSingle.title = titleAndWorkDesc;
// [选项] - 格式化 --- 填空题无选项
//examSingle.arrWorkDesc = [];
// [题目答案] - 处理(已将3个连续以上的空格已转为4个空格, 故可直接替换) --- ['填空1','填空2']
if (answer !== '') {
examSingle.arrWorkAnswer = answer.split(" ");
}
}
else if( judgedStatus != -1 ) {
/**
* 判断题
*/
// [题型] - 格式化
examSingle.workType = "判断题";
// [题干] - 格式化
examSingle.title = titleAndWorkDesc;
// [选项] - 格式化 --- 判断题无选项
//examSingle.arrWorkDesc = [];
// [题目答案] - 处理(字典前一半为正确, 后一半为错误, 如返回值小于长度的一半则为正常, 反之为错误) --- ['0'/'1']
let resp = judgedStatus - JUDGED_DICTIONARY.length / 2 < 0 ? "1" : "0";
// [题目答案] - 格式化
examSingle.arrWorkAnswer.push(resp);
}
else {
/**
* 主观题
*/
// [题型] - 格式化
examSingle.workType = "主观题";
// [题干]-格式化
examSingle.title = titleAndWorkDesc;
// [选项] - 格式化 --- 主观题无选项
//examSingle.arrWorkDesc = [];
// [题目答案] - 处理 --- ['qweasd123']
if (answer !== '') {
examSingle.arrWorkAnswer.push(answer);
}
}
return examSingle;
}
/**
* @desc: 复合题 处理逻辑
* @return: {*}
* @param {*} titleAndWorkDesc [题干]+[选项]
* @param {*} answer [答案]
*/
const processExamMulti = function (titleAndWorkDesc, answer) {
let examMulti = {
workType: '复合题',
title: '',
arrWorkDesc: [],
arrWorkAnswer: [],
errMsg: '', //以此判断当前是否处理成功
}
let tmpSplit = [];
let regex = null;
let matcher = null;
// [题型] - 格式化
examMulti.workType = "复合题";
// 先确定当前是以什么形式的小题序号来切分 --- 需要全部独立判断, 避免出现复合题中, 每小题内还包含小题的情况--- 1.回答以下问题 (1)***** (2)******
let cliceSucc = false;
let arrAnswer = []
if(!cliceSucc){
regex = /<br \/>\s*\d+[..。]\s*/;
if (regex.test(titleAndWorkDesc)) {
// 再次以答案中的序号同步匹配一次
regex = /^\s*\d+[..。]\s*/;
if(answer === '' || regex.test(answer)){
regex = /<br \/>\s*\d+[..。]\s*/g;
tmpSplit = titleAndWorkDesc.split(regex);
if (answer !== '') {
// 存在答案时, 再校验
regex = /^\s*\d+[..。]\s*|<br \/>\s*\d+[..。]\s*|\s+\d+[..。]\s*/g;
arrAnswer = answer.split(regex);
}
cliceSucc = true;
}
}
}
if (!cliceSucc){
regex = /<br \/>\s*\d+\s*/;
if (regex.test(titleAndWorkDesc)) {
// 再次以答案中的序号同步匹配一次
regex = /\s*\d+\s*/;
if(answer === '' || regex.test(answer)){
regex = /<br \/>\s*\d+\s*/g;
tmpSplit = titleAndWorkDesc.split(regex);
if (answer !== '') {
// 存在答案时, 再校验
regex = /^\s*\d+\s*|<br \/>\s*\d+\s*|\s+\d+\s*/g;
arrAnswer = answer.split(regex);
}
cliceSucc = true;
}
}
}
if (!cliceSucc){
regex = /<br \/>\s*\(\d+\)\s*/;
if (regex.test(titleAndWorkDesc)) {
// 再次以答案中的序号同步匹配一次
regex = /^\s*\(\d+\)\s*/;
if(answer === '' || regex.test(answer)){
regex = /<br \/>\s*\(\d+\)\s*/g;
tmpSplit = titleAndWorkDesc.split(regex);
if (answer !== '') {
// 存在答案时, 再校验
regex = /^\s*\(\d+\)\s*|<br \/>\s*\(\d+\)\s*|\s+\(\d+\)\s*/g;
arrAnswer = answer.split(regex);
}
cliceSucc = true;
}
}
}
if (!cliceSucc){
examMulti.errMsg = '[复合题]小题与答案序号[不匹配]';
return examMulti;
}
if (tmpSplit.length < 2){
examMulti.errMsg = '[复合题]题干与小题[切分失败]';
return examMulti;
}
if (answer !== '' && arrAnswer.length < 2){
examMulti.errMsg = '[复合题]答案切分小题失败';
return examMulti;
}
if (answer !== '' && tmpSplit.length != arrAnswer.length){
examMulti.errMsg = '[复合题]小题个数与答案个数[不一致]';
return examMulti;
}
// [题干]-格式化 --- 正常数据
examMulti.title = tmpSplit[0].trim();
// [选项]+[答案] - 逻辑处理
for (let i=1; i<tmpSplit.length; i++){
const tmp = tmpSplit[i].trim();
// 因arrAnswer[0]对应为分隔出来的首位空数组, 故这里也可直接使用i=1作为下标获取答案
const tmpAnswer = answer === '' ? '' : arrAnswer[i].trim();
// 单题处理
const tmpExam = processExamSingle(tmp, tmpAnswer);
if(tmpExam.errMsg !== ''){
examMulti.errMsg = '[复合题]小题解析失败';
return examMulti;
}
/**
* 处理[选项显示] - 特殊结构
* [
* {type: '单选题', title: '题目1', options: ['ABC123','ABC123']},
* {type: '多选题', title: '题目1', options: ['ABC123','ABC123']},
* {type: '填空题', title: '题目1', options: []},
* {type: '判断题', title: '题目1', options: []},
* ]
*/
const subWorkDesc = {
type: tmpExam.workType,
title: tmpExam.title,
options: tmpExam.arrWorkDesc,
}
examMulti.arrWorkDesc.push(subWorkDesc);
/**
* 处理[答案显示] - 特殊结构
* [
* {type: '单选题', answer: ['0']},
* {type: '多选题', answer: ['0','1']},
* {type: '填空题', answer: ['填空1','填空2']},
* {type: '判断题', answer: ['0'/'1']},
* ]
*/
const subWorkAnswer = {
type: tmpExam.workType,
answer: tmpExam.arrWorkAnswer,
}
examMulti.arrWorkAnswer.push(subWorkAnswer);
}
return examMulti;
}
/**
* @desc: [判断题] 处理逻辑, 字典前一半为正确, 后一半为错误, 如返回值小于长度的一半则为正常, 反之为错误
* @return: {*} index 返回的索引
* @param {*} key 当前需查询的答案
*/
const containsExactMatch = function (answer) {
answer = answer.toUpperCase().trim();
answer = answer.replace("_____", "");
let index = 0;
for (let item of EXAM_JUDGED_DICTIONARY) {
if (answer === item) {
return index;
}
index++;
}
return -1;
}