baigl #46
|
@ -79,6 +79,7 @@
|
|||
"nanoid": "^5.0.7",
|
||||
"number-precision": "^1.6.0",
|
||||
"vue-cropper": "1.0.3",
|
||||
"qs": "^6.12.0",
|
||||
"pptxgenjs": "^3.12.0",
|
||||
"pptxtojson": "^1.0.3",
|
||||
"prosemirror-commands": "^1.6.0",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="page-left">
|
||||
<el-tabs v-model="activeAptTab" style="height: 100%;">
|
||||
<el-tab-pane label="人工录入" name="人工录入" class="prepare-center-zglr">
|
||||
<div>
|
||||
<div class="row-import-manual">
|
||||
<vue-cropper
|
||||
class="import-manual-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-whole" @click="identifyOverallImg">整题识别</el-button>
|
||||
</div>
|
||||
<!-- orc 使用说明 -->
|
||||
<div class="import-manual-explain">
|
||||
<p>orc 使用说明</p>
|
||||
<p>1、本地浏览 </p>
|
||||
<p>2、获取剪贴板图片 </p>
|
||||
<p>3、整题识别 </p>
|
||||
<p> </p>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
@ -52,9 +60,9 @@
|
|||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref,watch, reactive, getCurrentInstance,nextTick } from 'vue'
|
||||
import "vue-cropper/dist/index.css";
|
||||
import { VueCropper } from "vue-cropper";
|
||||
import { onMounted, ref,watch, reactive, getCurrentInstance,nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
|
@ -65,6 +73,8 @@ import { useGetHomework } from '@/hooks/useGetHomework'
|
|||
import { sessionStore } from '@/utils/store'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
import { ocrImg2ExamByManualUpl, ocrImg2ItemByManualUpl } from "@/views/classTask/newClassTaskAssign/questionUpload/ocrImg2ExamQues";
|
||||
const userStore = useUserStore().user
|
||||
const route = useRoute();
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
@ -81,10 +91,10 @@ const courseObj = reactive({
|
|||
node: null, // 选择的课程节点
|
||||
//
|
||||
})
|
||||
const activeAptTab = ref("自主搜题");
|
||||
// const taskList = ref([]); // 作业列表
|
||||
// const tasklist_loading = ref(false); // 加载中
|
||||
const activeAptTab = ref("人工录入");
|
||||
|
||||
// false - 正式百度api true - 本地json测试
|
||||
const OCR_WORK_TEST = false;
|
||||
// [裁剪]参数
|
||||
const cropOption = reactive({
|
||||
img: '', // 裁剪图片的地址 url 地址, base64, blob
|
||||
|
@ -137,6 +147,73 @@ const initHomeWork = async()=> {
|
|||
// 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>
|
||||
|
@ -165,7 +242,42 @@ const initHomeWork = async()=> {
|
|||
height: 100%;
|
||||
display: flex;
|
||||
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 {
|
||||
width: 100%;
|
||||
|
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue