feat:初始化 -融骅
This commit is contained in:
83
front/src/api/assessment-evaluation/exam.js
Normal file
83
front/src/api/assessment-evaluation/exam.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】获取所有考试分类
|
||||
* @returns [{}]
|
||||
*/
|
||||
export const findAllExamClassifyApi = () =>
|
||||
fetch({ url: '/assessmentEvaluation/ExamClassify', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【增】新增考试分类
|
||||
*/
|
||||
export const createExamClassifyApi = (data) =>
|
||||
fetch({ url: '/assessmentEvaluation/ExamClassify', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【删】删除考试分类
|
||||
*/
|
||||
export const deleteExamClassifyApi = (id) =>
|
||||
fetch({ url: '/assessmentEvaluation/ExamClassify/' + id, method: 'delete' })
|
||||
/**
|
||||
* 【改】删除考试分类
|
||||
*/
|
||||
export const editExamClassifyApi = ({ id, ...data }) =>
|
||||
fetch({ url: '/assessmentEvaluation/ExamClassify/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】分页查询考试列表
|
||||
* @param {Object} params 分页条件
|
||||
* @returns
|
||||
*/
|
||||
export const pagingExamListApi = (params) => fetch({ url: '/assessmentEvaluation/exam/paging', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 【删】批量删除考试
|
||||
* @param {array} data 考试id列表
|
||||
* @returns
|
||||
*/
|
||||
export const batchDeleteExamApi = (data) => fetch({ url: '/assessmentEvaluation/exam', method: 'delete', data })
|
||||
/**
|
||||
* 【增】新增考试
|
||||
* @param {array} data 考试信息
|
||||
* @returns
|
||||
*/
|
||||
export const createExamApi = (data) => fetch({ url: '/assessmentEvaluation/exam', method: 'post', data })
|
||||
/**
|
||||
* 【查】根据ID查询考试详情
|
||||
* @param {array} id
|
||||
* @returns
|
||||
*/
|
||||
export const getExamDetailsByIdApi = (id) => fetch({ url: '/assessmentEvaluation/exam/item/' + id, method: 'get' })
|
||||
/**
|
||||
* 【改】根据ID修改考试详情
|
||||
* @param {array} data
|
||||
* @returns
|
||||
*/
|
||||
export const patchExamDetailsByIdApi = ({ id, ...data }) => fetch({ url: '/assessmentEvaluation/exam/item/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】查询所有老师
|
||||
* @returns
|
||||
*/
|
||||
export const getAllTeacher = () => fetch({ url: '/system/user/findAllTeacher', method: 'get' })
|
||||
/**
|
||||
* 【查】根据用户查用户组织架构
|
||||
* @returns
|
||||
*/
|
||||
export const getMyOrg = () => fetch({ url: '/system/org/findOrgByUserId', method: 'get' })
|
||||
/**
|
||||
* 【查】所有组织架构
|
||||
* @returns
|
||||
*/
|
||||
export const getAllOrg = () => fetch({ url: '/system/org', method: 'get' })
|
||||
/**
|
||||
* 【查】根据组织架构查学生列表
|
||||
* @returns
|
||||
*/
|
||||
export const getStudentByOrgId = (orgId) => fetch({ url: '/system/user/findStudentByOrgId', method: 'get', params: { orgId } })
|
||||
/**
|
||||
* 【查】根据试卷ID查所有试题 用于预览
|
||||
* @returns
|
||||
*/
|
||||
export const getQuestionsByPaperApi = (paperId) => fetch({ url: '/assessmentEvaluation/exampaper/questionsByPaper/' + paperId, method: 'get' })
|
||||
30
front/src/api/assessment-evaluation/humanEval.js
Normal file
30
front/src/api/assessment-evaluation/humanEval.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】分页查询人工判卷考卷及信息
|
||||
* {currentPage,pageSize,classifyId,isPractive,title}
|
||||
*/
|
||||
export const pagingGradePaperApi = (params) =>
|
||||
fetch({ url: '/assessmentEvaluation/StudentOnlineExam/pagingGrade', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 修改是否为匿名评卷
|
||||
* @param {object} param0 试卷id+是否匿名
|
||||
* @returns
|
||||
*/
|
||||
export const patchIsAnonymousApi = ({ id, ...data }) =>
|
||||
fetch({ url: '/assessmentEvaluation/exam/updateIsAnonymous/' + id, method: 'patch', data })
|
||||
/**
|
||||
* 人工判卷考试的学员考试查询 分页
|
||||
* @param {object} param0 试卷id+是否匿名
|
||||
* @returns
|
||||
*/
|
||||
export const pagingGradeDetailsApi = ({ id, ...params }) =>
|
||||
fetch({ url: '/assessmentEvaluation/StudentOnlineExam/pagingGradeDetails/' + id, method: 'get', params })
|
||||
/**
|
||||
* 传入修改数组,修改题目result相关字段
|
||||
* @param {Array} data
|
||||
* @returns
|
||||
*/
|
||||
export const patchSomeExamResult = (histroyId, data) =>
|
||||
fetch({ url: '/assessmentEvaluation/OnlineExamHistory/result/' + histroyId, method: 'patch', data })
|
||||
27
front/src/api/assessment-evaluation/mistake.js
Normal file
27
front/src/api/assessment-evaluation/mistake.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】分页查询错题巩固列表
|
||||
* {currentPage,pageSize,title}
|
||||
*/
|
||||
export const pagingMistakeListApi = (params) =>
|
||||
fetch({ url: '/assessmentEvaluation/mistake/paging', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 【查】根据试题分类查询所有错退
|
||||
* {currentPage,pageSize,title}
|
||||
*/
|
||||
export const getMistakesByclassifyId = (classifyId, showUserAnswer = 1) =>
|
||||
fetch({ url: '/assessmentEvaluation/mistake/questions/' + classifyId, method: 'get', params: { showUserAnswer } })
|
||||
/**
|
||||
* 【改】上传错题答题模式的答案
|
||||
* {currentPage,pageSize,title}
|
||||
*/
|
||||
export const patchMistakes = (classifyId, mistakes) =>
|
||||
fetch({ url: '/assessmentEvaluation/mistake/submitMistakes/' + classifyId, method: 'patch', data: mistakes })
|
||||
/**
|
||||
* 【查】根据试题分类查最后一次考试ID
|
||||
* {currentPage,pageSize,title}
|
||||
*/
|
||||
export const getLastExamHistoryId = (classifyId) =>
|
||||
fetch({ url: '/assessmentEvaluation/mistake/lastExamHistory/' + classifyId, method: 'get' })
|
||||
49
front/src/api/assessment-evaluation/onlineTest.js
Normal file
49
front/src/api/assessment-evaluation/onlineTest.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】分页查询分配给本学员的在线考试
|
||||
* @param {object} params 分页参数
|
||||
* @returns
|
||||
*/
|
||||
export const pagingOnlineExamListApi = (params) => fetch({ url: '/assessmentEvaluation/StudentOnlineExam/paging', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 【查】根据在线考试ID获取本次考试所有信息
|
||||
* @param {number} id 考试ID
|
||||
* @returns
|
||||
*/
|
||||
export const getOnlineExamAllDataApi = (id) => fetch({ url: '/assessmentEvaluation/OnlineExamHistory/questions/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【增】根据在线考试ID新增考试历史记录
|
||||
* @param {{onlineExamId?: number,isPracticeExam?: number}} id 考试ID
|
||||
* @returns
|
||||
*/
|
||||
export const createExamHistoryApi = (data) => fetch({ url: '/assessmentEvaluation/OnlineExamHistory', method: 'post', data })
|
||||
/**
|
||||
* 【增】根据在线考试ID新增考试历史记录
|
||||
* @param {{onlineExamId?: number,isPracticeExam?: number}} id 考试ID
|
||||
* @returns
|
||||
*/
|
||||
export const createSimExamHistoryApi = (data) => fetch({ url: '/assessmentEvaluation/OnlineExamHistory/simtest', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【增】考试完成,提交试卷
|
||||
* @param {number} id 考试ID
|
||||
* @param {array} answers 考试答案列表
|
||||
* @returns
|
||||
*/
|
||||
export const submitExamApi = ({ id, answers }) => fetch({ url: '/assessmentEvaluation/OnlineExamHistory/submitExam/' + id, method: 'post', data: answers })
|
||||
/**
|
||||
* 【查】查询最后一次考试的历史记录
|
||||
* @param {number} id 考试ID
|
||||
* @returns
|
||||
*/
|
||||
export const getLastedHistoryApi = (id) => fetch({ url: '/assessmentEvaluation/StudentOnlineExam/lastExamHistory/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】根据在线考试ID获取考试答题结果
|
||||
* @param {number} id 考试ID
|
||||
* @returns
|
||||
*/
|
||||
export const getOnlineExamResultApi = (id) => fetch({ url: '/assessmentEvaluation/OnlineExamHistory/examQuestionResult/' + id, method: 'get' })
|
||||
99
front/src/api/assessment-evaluation/paper.js
Normal file
99
front/src/api/assessment-evaluation/paper.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】获取所有试卷分类
|
||||
* @returns [{}]
|
||||
*/
|
||||
export const findAllPaperClassifyApi = () =>
|
||||
fetch({ url: '/assessmentEvaluation/ExamPaperClassify', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【增】新增试卷分类
|
||||
*/
|
||||
export const createPaperClassifyApi = (data) =>
|
||||
fetch({ url: '/assessmentEvaluation/ExamPaperClassify', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【删】删除试卷分类
|
||||
*/
|
||||
export const deletePaperClassifyApi = (id) =>
|
||||
fetch({ url: '/assessmentEvaluation/ExamPaperClassify/' + id, method: 'delete' })
|
||||
/**
|
||||
* 【改】修改试卷分类
|
||||
*/
|
||||
export const editPaperClassifyApi = ({ id, ...data }) =>
|
||||
fetch({ url: '/assessmentEvaluation/ExamPaperClassify/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】分页查询试卷
|
||||
* {currentPage,pageSize,classifyId,isPractive,title}
|
||||
*/
|
||||
export const pagingFindPaperApi = (params) =>
|
||||
fetch({ url: '/assessmentEvaluation/exampaper/paging', method: 'get', params })
|
||||
/**
|
||||
* 【删】批量删除试卷
|
||||
*/
|
||||
export const deleteSomePapersApi = (data) =>
|
||||
fetch({ url: '/assessmentEvaluation/exampaper', method: 'delete', data })
|
||||
/**
|
||||
* 【删】修改试卷信息
|
||||
*/
|
||||
export const patchPaperApi = ({ id, ...data }) =>
|
||||
fetch({ url: '/assessmentEvaluation/exampaper/item/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【增】新增试卷
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createPaperApi = (data) =>
|
||||
fetch({ url: '/assessmentEvaluation/exampaper', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【查】根据试卷ID查询试卷详情
|
||||
* @param {number|string} id
|
||||
* @returns
|
||||
*/
|
||||
export const getPaperInfoByIdApi = (id) =>
|
||||
fetch({ url: '/assessmentEvaluation/exampaper/item/' + id, method: 'get' })
|
||||
/**
|
||||
* 【改】根据试卷ID修改试卷
|
||||
* @param {object} param0 信息
|
||||
* @returns
|
||||
*/
|
||||
export const patchPaperInfoByIdApi = ({ id, ...data }) =>
|
||||
fetch({ url: '/assessmentEvaluation/exampaper/item/' + id, method: 'patch', data })
|
||||
/**
|
||||
* 【增】根据试卷ID复制试卷
|
||||
* @param {object} param0 信息
|
||||
* @returns
|
||||
*/
|
||||
export const copyPaperInfoByIdApi = ({ id, ...data }) =>
|
||||
fetch({ url: '/assessmentEvaluation/exampaper/copy/' + id, method: 'post', data })
|
||||
|
||||
/**
|
||||
*【改】批量移动试卷到分类
|
||||
* @param {Object{ids:[],classifyId:number}} data
|
||||
* @returns
|
||||
*/
|
||||
export const batchMovePaperClassify = (data) => fetch({ url: '/assessmentEvaluation/exampaper/putchAllClassifyId', method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【增】新增模拟试卷
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createSimTestApi = (data) =>
|
||||
fetch({ url: '/assessmentEvaluation/simtest', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【查】分页查询试卷
|
||||
* {currentPage,pageSize,classifyId,isPractive,title}
|
||||
*/
|
||||
export const pagingFindSimPaperApi = (params) =>
|
||||
fetch({ url: '/assessmentEvaluation/simtest/paging', method: 'get', params })
|
||||
/**
|
||||
* 【查】试卷被哪些考试引用
|
||||
*/
|
||||
export const checkQuoteApi = (id) =>
|
||||
fetch({ url: '/assessmentEvaluation/exampaper/checkQuote/' + id, method: 'get' })
|
||||
111
front/src/api/assessment-evaluation/questions.js
Normal file
111
front/src/api/assessment-evaluation/questions.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】获取所有试题分类
|
||||
* @returns [{}]
|
||||
*/
|
||||
export const findAllQuestionsClassifyApi = () =>
|
||||
fetch({ url: '/assessmentEvaluation/questionClassify', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【增】新增试题分类
|
||||
*/
|
||||
export const createQuestionsClassifyApi = (data) =>
|
||||
fetch({ url: '/assessmentEvaluation/questionClassify', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【删】删除试题分类
|
||||
*/
|
||||
export const deleteQuestionsClassifyApi = (id) =>
|
||||
fetch({ url: '/assessmentEvaluation/questionClassify/' + id, method: 'delete' })
|
||||
/**
|
||||
* 【改】修改试题分类
|
||||
*/
|
||||
export const editQuestionsClassifyApi = ({ id, ...data }) =>
|
||||
fetch({ url: '/assessmentEvaluation/questionClassify/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】获取所有试题列表
|
||||
*/
|
||||
export const pagingFindQuestionsApi = (params) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions/paging', method: 'get', params })
|
||||
/**
|
||||
* 【查】获取所有筛选条件选项(包含试题类型和难易程度)
|
||||
*/
|
||||
export const getAllQueryParamsOptionsApi = () =>
|
||||
fetch({ url: '/assessmentEvaluation/questions/queryParams', method: 'get' })
|
||||
/**
|
||||
* 【查】只获取试题类型
|
||||
*/
|
||||
export const getQuestionTypeOptionsApi = () =>
|
||||
fetch({ url: '/assessmentEvaluation/questionType', method: 'get' })
|
||||
/**
|
||||
* 【查】获取所有试题难度选项
|
||||
* @returns [{}]
|
||||
*/
|
||||
export const getQuestionDifficultyLevelOptionsApi = () =>
|
||||
fetch({ url: '/assessmentEvaluation/questionDifficultyLevel', method: 'get' })
|
||||
/**
|
||||
* 【刪】多选删除试题
|
||||
* @param string[] data
|
||||
*/
|
||||
export const deleteSomeQuestionsApi = (data) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions', method: 'delete', data })
|
||||
/**
|
||||
* 【查】获取所有知识点
|
||||
*/
|
||||
export const getAllknowledgePointApi = () =>
|
||||
fetch({ url: '/assessmentEvaluation/knowledgePoint', method: 'get' })
|
||||
/**
|
||||
* 【增】新增知识点
|
||||
* @param string name
|
||||
*/
|
||||
export const createKnowledgePointApi = (name) =>
|
||||
fetch({ url: '/assessmentEvaluation/knowledgePoint', method: 'post', data: { name } })
|
||||
/**
|
||||
* 【删】删除知识点
|
||||
*/
|
||||
export const deleteKnowledgePointApi = (id) =>
|
||||
fetch({ url: '/assessmentEvaluation/knowledgePoint/' + id, method: 'delete' })
|
||||
/**
|
||||
* 【增】删除知识点
|
||||
*/
|
||||
export const createQuestionApi = (data) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions', method: 'post', data })
|
||||
/**
|
||||
* 【查】根据ID查试题内容
|
||||
*/
|
||||
export const getQuestionByIdApi = (id) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions/item/' + id, method: 'get' })
|
||||
/**
|
||||
* 【改】修改试题
|
||||
*/
|
||||
export const patchQuestionByIdApi = ({ id, ...data }) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions/item/' + id, method: 'patch', data })
|
||||
/**
|
||||
* 【改】修改试题
|
||||
*/
|
||||
export const studentCanUseApi = ({ id, ...data }) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions/item/studentCanUse/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】根据类型查试题总数
|
||||
* @param {number} type 类型ID
|
||||
* @returns 总数
|
||||
*/
|
||||
export const getQuestionCountByTypeIdApi = (params) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions/count', method: 'get', params })
|
||||
/**
|
||||
* 【查】随机抽提
|
||||
*/
|
||||
export const getRandQuestionApi = (params) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions/random', method: 'get', params })
|
||||
|
||||
export const importQuestionsApi = (classifyId, data) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions/import/' + classifyId, method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【查】导出试题
|
||||
*/
|
||||
export const exportQuestionsApi = (classifyId) =>
|
||||
fetch({ url: '/assessmentEvaluation/questions/export/' + classifyId, method: 'get' })
|
||||
88
front/src/api/evaluation/index copy.js
Normal file
88
front/src/api/evaluation/index copy.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【增】 - 评价
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {string} data.name 评价名
|
||||
* @param {string} data.appModule 应用模块
|
||||
* @returns
|
||||
*/
|
||||
export const createEvaluationApi = (data) =>
|
||||
fetch({ url: 'evaluation', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 评价
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteEvaluationApi = (id) =>
|
||||
fetch({ url: 'evaluation/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* 【改】 - 评价
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateEvaluationApi = ({ id, name }) =>
|
||||
fetch({ url: 'evaluation/' + id, method: 'patch', data: { name } })
|
||||
|
||||
/**
|
||||
* 【查】 - 评价
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findAllEvaluationApi = () =>
|
||||
fetch({ url: 'evaluation', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【增】 - 评价指标
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createEvaluationIndicatorApi = ({ name, evaluationId }) =>
|
||||
fetch({ url: 'evaluation/indicator', method: 'post', data: { name, evaluationId } })
|
||||
|
||||
/**
|
||||
* 【改】 - 评价指标
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateEvaluationIndicatorApi = ({ id, name, evaluationId }) =>
|
||||
fetch({ url: 'evaluation/indicator/' + id, method: 'patch', data: { name, evaluationId } })
|
||||
|
||||
/**
|
||||
* 【删】 - 评价指标
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteEvaluationIndicatorApi = (id) =>
|
||||
fetch({ url: 'evaluation/indicator/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* 【查】 - 学生评分
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findAllStudentEvaluationApi = (orgId) =>
|
||||
fetch({ url: 'evaluation/indicator/findAllStudentEvaluation', method: 'get', params: { orgId } })
|
||||
|
||||
/**
|
||||
* 【增】 - 学生评分
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createStudentEvaluationScoreApi = (data) =>
|
||||
fetch({ url: 'evaluation/indicator/createStudentEvaluationScore', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 学生评分
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateStudentEvaluationScoreApi = ({ id, ...data }) =>
|
||||
fetch({ url: 'evaluation/indicator/updateStudentEvaluationScore/' + id, method: 'patch', data })
|
||||
41
front/src/api/evaluation/index.js
Normal file
41
front/src/api/evaluation/index.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【增】 - 评价指标
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createEvaluationIndicatorApi = (data) =>
|
||||
fetch({ url: 'evaluation/indicator', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【查】 -获取评价指标信息
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const getssEvaluationIndicatorApi = () =>
|
||||
fetch({ url: 'evaluation/indicator', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 -获取老师所管辖的学生评价信息
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const getAllTakeChargeStudentApi = (data) =>
|
||||
fetch({ url: 'evaluation', method: 'get', data })
|
||||
|
||||
/**
|
||||
* 【改】 -评分
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const evaluationApi = (data) =>
|
||||
fetch({ url: 'evaluation', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【查】 -学员查询自己的评分
|
||||
* @returns
|
||||
*/
|
||||
export const getSleftEvaluationApi = () =>
|
||||
fetch({ url: 'evaluation/my', method: 'get' })
|
||||
50
front/src/api/online-course/course-notes.js
Normal file
50
front/src/api/online-course/course-notes.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【增】 - 添加笔记
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const addCourseNoteAPI = (data) => {
|
||||
return fetch({ url: 'course-notes', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】 - 获取笔记列表
|
||||
* @param {number} page 页码
|
||||
* @param {number} pageSize 页面大小
|
||||
* @returns
|
||||
*/
|
||||
export const getCourseNotesListAPI = (data) => {
|
||||
return fetch({ url: `course-notes?page=${data.page}&pageSize=${data.pageSize}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】 - 获取笔记详情
|
||||
* @param {number} id 要查询的ID
|
||||
* @returns
|
||||
*/
|
||||
export const getOneCourseNotesListAPI = (id) => {
|
||||
return fetch({ url: 'course-notes/' + id, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【删】 - 删除笔记
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteNoteAPI = (id) => {
|
||||
return fetch({ url: 'course-notes/' + id, method: 'delete' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑笔记
|
||||
* @param {number} id
|
||||
* @param {string} explain
|
||||
* @param {string} content
|
||||
* @param {string} title
|
||||
* @returns
|
||||
*/
|
||||
export const editorNoteAPI = (data) => {
|
||||
return fetch({ url: 'course-notes/' + data.id, method: 'patch', data })
|
||||
}
|
||||
97
front/src/api/online-course/my-course.js
Normal file
97
front/src/api/online-course/my-course.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】获课程评价
|
||||
*/
|
||||
export const getCourseEvaluate = ({ courseId, ...data }) => {
|
||||
return fetch({ url: 'online-course/course-evaluate/' + courseId, method: 'get', params: data })
|
||||
}
|
||||
/**
|
||||
* 获取所有课程列表
|
||||
* @param page
|
||||
* @param pageSize
|
||||
*/
|
||||
export const getMyCourseListAllAPI = (data) => {
|
||||
return fetch({ url: 'my-course', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】获取某学习记录下,课程的习题信息
|
||||
* @param {number} recordId
|
||||
*/
|
||||
export const getOneRecordExercisesAPI = (recordId) => {
|
||||
return fetch({ url: 'my-course/exercise/' + recordId, method: 'get' })
|
||||
}
|
||||
/**
|
||||
* 【查】获取某学习记录下,课程的习题信息
|
||||
* @param {number} recordId
|
||||
*/
|
||||
export const getCommentByCourseIdApi = (courseId) => {
|
||||
return fetch({ url: 'my-course/comment/' + courseId, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类课程列表
|
||||
* @param {number} page
|
||||
* @param {number} pageSize
|
||||
* @param {number} type
|
||||
* @returns
|
||||
*/
|
||||
export const getMyCourseListTypeAPI = (data) => {
|
||||
return fetch({ url: 'my-course/type', method: 'post', data })
|
||||
}
|
||||
/**
|
||||
* 获取某个课程的详细信息
|
||||
*/
|
||||
export const getCourseInfoAPI = (id) => {
|
||||
return fetch({ url: `my-course/${id}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新或保存课程的评价信息
|
||||
*/
|
||||
|
||||
export const updateEvealuateAPI = (data) => {
|
||||
return fetch({ url: 'my-course/eveal', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新课件学习信息
|
||||
*/
|
||||
|
||||
export const updateCourewareAPI = (data) => {
|
||||
return fetch({ url: 'my-course/ware', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【改】更新课件学习信息
|
||||
* @param {Array} answer
|
||||
*/
|
||||
export const submitAnswerAPI = (data) => {
|
||||
return fetch({ url: '/my-course/exercise', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【增】【改】更新讨论学习信息
|
||||
* @param {number} recordId
|
||||
* @param {Anumber|null} replyId
|
||||
* @param {Anumber|null} replyUserId
|
||||
* @param {string} content
|
||||
* @param {Anumber|null} replyUserInfo
|
||||
*/
|
||||
|
||||
export const submitDisAPI = (data) => {
|
||||
return fetch({ url: 'my-course/discussion', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【增】【改】提交讨论学习信息
|
||||
* @param {number|null} id
|
||||
* @param {number|null} recordId
|
||||
* @param {number|null} disId
|
||||
* @param {number} type
|
||||
*/
|
||||
|
||||
export const submitDisKudosAPI = (data) => {
|
||||
return fetch({ url: 'my-course/discussion/kudos', method: 'post', data })
|
||||
}
|
||||
72
front/src/api/online-course/online-FAQ.js
Normal file
72
front/src/api/online-course/online-FAQ.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】-获取我的提问列表
|
||||
* @param {number} page
|
||||
* @param {number} pageSize
|
||||
* @returns
|
||||
*/
|
||||
export const getMyIssuesAPI = (data) => {
|
||||
return fetch({ url: `online-issues?page=${data.page}&pageSize=${data.pageSize}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】-获取我的回答
|
||||
* @param {number} page
|
||||
* @param {number} pageSize
|
||||
* @returns
|
||||
*/
|
||||
export const getMyAnswerAPI = (data) => {
|
||||
return fetch({ url: `online-issues/my-answer?page=${data.page}&pageSize=${data.pageSize}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【增】-新建问题
|
||||
* @param {string} title
|
||||
* @param {string} content
|
||||
* @param {Array} answerPersonIds
|
||||
* @returns
|
||||
*/
|
||||
export const createIssuesAPI = (data) => {
|
||||
return fetch({ url: 'online-issues', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【改】-编辑问题
|
||||
* @param {string} title
|
||||
* @param {string} content
|
||||
* 当前解答人的id
|
||||
* @param {Array} answerPersonIds
|
||||
* 保存过要删除的id
|
||||
* @param {Array} delAnswerPersonIds
|
||||
* @returns
|
||||
*/
|
||||
export const editorIssuesAPI = (data) => {
|
||||
return fetch({ url: 'online-issues/update-issues', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【改】- 回复问题
|
||||
* @param {string} id
|
||||
* @param {string} content
|
||||
* @returns
|
||||
*/
|
||||
export const answerAPI = (data) => {
|
||||
return fetch({ url: 'online-issues/answer', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】-
|
||||
* @param {string} id
|
||||
* @returns看。l
|
||||
*/
|
||||
export const getIssuesInfo = (id) => {
|
||||
return fetch({ url: `online-issues/${id}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
*【查】-当前用户老师管理的学生
|
||||
*/
|
||||
export const getMyTeacherStudentAPI = () => {
|
||||
return fetch({ url: 'online-issues/myteacher', method: 'get' })
|
||||
}
|
||||
72
front/src/api/online-course/online-course.js
Normal file
72
front/src/api/online-course/online-course.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
// 分类查询
|
||||
export const getCourseClassfiyApi = () => {
|
||||
return fetch({ url: 'online-course/classify', method: 'get' })
|
||||
}
|
||||
/**
|
||||
* 添加分类
|
||||
*
|
||||
*/
|
||||
export const addCourseClassfiyApi = (data) => {
|
||||
return fetch({ url: 'online-course/classify', method: 'post', data })
|
||||
}
|
||||
/**
|
||||
* 【改】 - 编辑分类
|
||||
* @param {number} id
|
||||
* @param {string} name
|
||||
* @returns
|
||||
*/
|
||||
export const editorCourseClassfiyApi = (data) => {
|
||||
return fetch({ url: 'online-course/classify/' + data.id, method: 'patch', data })
|
||||
}
|
||||
|
||||
export const deleteCourseClassfiyApi = (data) => {
|
||||
return fetch({ url: 'online-course/classify/delete', method: 'post', data })
|
||||
}
|
||||
/**
|
||||
* 获取课程列表
|
||||
*/
|
||||
export const getCourseListApi = (data) => {
|
||||
return fetch({ url: `online-course/course?page=${data.page}&pageSize=${data.pageSize}&classify=${data.classify}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改课程状态
|
||||
*/
|
||||
export const setCourseStatusApi = (data) => {
|
||||
return fetch({ url: `online-course/course_status/${data.id}?status=${data.status}`, method: 'get' })
|
||||
}
|
||||
/**
|
||||
* 查询课程列表
|
||||
*/
|
||||
export const searchCourseListApi = (data) => {
|
||||
return fetch({ url: `online-course/course_search/?page=${data.page}&pageSize=${data.pageSize}&value=${data.value}&classify=${data.classify}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除课程——可批量
|
||||
*/
|
||||
export const deleteCourseApi = (data) => {
|
||||
return fetch({ url: 'online-course/batch_delete', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加课程
|
||||
*/
|
||||
export const addCourseApi = (data) => {
|
||||
return fetch({ url: 'online-course', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取课程详情
|
||||
*/
|
||||
export const getCourseInfoApi = (id) => {
|
||||
return fetch({ url: `online-course/course/${id}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加课程
|
||||
*/
|
||||
export const editorCourseApi = (id, data) => {
|
||||
return fetch({ url: `online-course/course/${id}`, method: 'patch', data })
|
||||
}
|
||||
77
front/src/api/online-teaching/live-manage.js
Normal file
77
front/src/api/online-teaching/live-manage.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【增】 - 新建直播
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const addLiveAPI = (data) => {
|
||||
return fetch({ url: 'live-teaching', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】 - 获取直播列表
|
||||
* @param {number} page 页码
|
||||
* @param {number} pageSize 页面大小
|
||||
* @returns
|
||||
*/
|
||||
export const getLiveListAPI = (params) => {
|
||||
return fetch({ url: 'live-teaching', method: 'get', params })
|
||||
}
|
||||
/**
|
||||
* 【查】 - 获取某直播详情
|
||||
* @param {number} id 页码
|
||||
* @returns
|
||||
*/
|
||||
export const getLiveInfoAPI = (id) => {
|
||||
return fetch({ url: 'live-teaching/' + id, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】 - 直播时获取某直播详情
|
||||
* @param {number} id 页码
|
||||
* @returns
|
||||
*/
|
||||
export const getLivePublishInfoAPI = (id) => {
|
||||
return fetch({ url: 'live-teaching/live-publish/' + id, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】 - 搜索直播列表
|
||||
* @param {number} page 页码
|
||||
* @param {number} pageSize 页面大小
|
||||
* @param {string} value 搜索内容
|
||||
* @returns
|
||||
*/
|
||||
export const searchLiveListAPI = (data) => {
|
||||
return fetch({ url: 'live-teaching/search', method: 'post', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【删】 - 删除直播
|
||||
* @param {[number]} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteLiveAPI = (id) => {
|
||||
return fetch({ url: 'live-teaching/' + id.join(','), method: 'delete' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑直播信息
|
||||
* @param {object} data
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const editorLiveAPI = (data) => {
|
||||
return fetch({ url: 'live-teaching', method: 'patch', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【改】 - 更新直播部分信息
|
||||
* @param {object} data
|
||||
* @param {number} data.id
|
||||
* @returns
|
||||
*/
|
||||
export const updateLiveInfoAPI = (data) => {
|
||||
return fetch({ url: 'live-teaching/update', method: 'post', data })
|
||||
}
|
||||
57
front/src/api/online-teaching/live-student.js
Normal file
57
front/src/api/online-teaching/live-student.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】 - 获取我的直播列表
|
||||
* @param {number} page 页码
|
||||
* @param {number} pageSize 页面大小
|
||||
* @returns
|
||||
*/
|
||||
export const getMyLiveListAPI = (data) => {
|
||||
return fetch({ url: `live-class?page=${data.page}&pageSize=${data.pageSize}`, method: 'get' })
|
||||
}
|
||||
/**
|
||||
* 【查】 - 获取我的直播列表
|
||||
* @param {number} page 页码
|
||||
* @param {number} pageSize 页面大小
|
||||
* @param {string} value 页面大小
|
||||
* @returns
|
||||
*/
|
||||
export const searchMyLiveListAPI = (data) => {
|
||||
return fetch({ url: `live-class/search?page=${data.page}&pageSize=${data.pageSize}&value=${data.value}`, method: 'get' })
|
||||
}
|
||||
/**
|
||||
* 【查】 - 获取直播间学院列表
|
||||
* @param {number} id 直播间ID
|
||||
* @returns
|
||||
*/
|
||||
export const getLiveStudentList = (id) => {
|
||||
return fetch({ url: `live-class/liveStudent/${id}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】 - 参加直播时获取直播详情
|
||||
* @param {number} id 直播ID
|
||||
* @returns
|
||||
*/
|
||||
export const getLiveWatchInfoAPI = (id) => {
|
||||
return fetch({ url: `live-class/${id}`, method: 'get' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 【查】 - 查询直播下的所有试题
|
||||
* @param {number} id 直播ID
|
||||
* @returns
|
||||
*/
|
||||
export const getLiveQuestionAPI = (id) => {
|
||||
return fetch({ url: `live-class/question/${id}`, method: 'get' })
|
||||
}
|
||||
/**
|
||||
* 【增】 - 回答练习题
|
||||
* @param {object} data 答案信息
|
||||
* @param {Array} data.answerInfo 答案信息
|
||||
* @param {number} data.liveId 直播id
|
||||
* @returns
|
||||
*/
|
||||
export const liveAnsweringQuestionAPI = (data) => {
|
||||
return fetch({ url: 'live-class/answering', method: 'post', data })
|
||||
}
|
||||
38
front/src/api/statistic/index.js
Normal file
38
front/src/api/statistic/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
const CACHES = {}
|
||||
|
||||
/**
|
||||
* 【查】 - 获取统计数据
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findStatisticDataApi = async (fun, { noCatch, ...params } = {}) => {
|
||||
const module = window.__CHART_MODULE__
|
||||
const cacheKey = noCatch ? undefined : `${module}_${fun}_${JSON.stringify(params)}`
|
||||
|
||||
if (cacheKey && CACHES[cacheKey]) return CACHES[cacheKey]
|
||||
|
||||
const { data } = await fetch({ url: 'statistic', params: { module, fun, ...params } })
|
||||
|
||||
if (cacheKey) CACHES[cacheKey] = data
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* 上报数据
|
||||
*
|
||||
* @param {Object} data 存储数据
|
||||
* @param {string} data.type 数据类型
|
||||
* @param {string} data.page 上报页面
|
||||
*
|
||||
* @param {number?} data.id 上报记录ID,为空时创建,不为空时则修改
|
||||
* @param {string?} data.field01 备用字段
|
||||
* @param {string?} data.field02 备用字段
|
||||
* @param {string?} data.field03 备用字段
|
||||
* @param {string?} data.remarks 备注说明
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const reportDataApi = (data) =>
|
||||
fetch({ url: 'statistic/reportData', method: 'post', data: { ...data, page: location.href.split(location.host)[1] } })
|
||||
49
front/src/api/system/index.js
Normal file
49
front/src/api/system/index.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 登陆
|
||||
*
|
||||
* @param {string} username 用户名
|
||||
* @param {string} password 密码
|
||||
* @returns
|
||||
*/
|
||||
export const loginApi = (username, password) =>
|
||||
fetch({ url: '/system/login', method: 'post', data: { username, password } })
|
||||
|
||||
/**
|
||||
* 校验管理员密码
|
||||
*
|
||||
* @param {string} password
|
||||
* @returns
|
||||
*/
|
||||
export const checkAdminPasswordApi = (password) =>
|
||||
fetch({ url: '/system/checkAdminPassword', params: { password } })
|
||||
|
||||
/**
|
||||
* 单文件上传
|
||||
*
|
||||
* @param {File} file 文件
|
||||
* @param {Object} options 参数
|
||||
* @param {Object} options.data 携带Body请求参数
|
||||
* @param {string} options.data.path 文件存储路径
|
||||
* @param {number} options.data.classifyId 文件分类
|
||||
* @param {()=>void} options.onUploadProgress 上传进度回调
|
||||
* @param {any} options.signal 用于取消请求
|
||||
* @returns
|
||||
*/
|
||||
export const uploadApi = (file, options) =>
|
||||
fetch({
|
||||
url: '/resource/upload',
|
||||
method: 'post',
|
||||
timeout: 0,
|
||||
...options,
|
||||
formData: { ...options?.data, file }
|
||||
})
|
||||
|
||||
/**
|
||||
* 【查】 - 角色功能
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findRoleFeatureApi = () =>
|
||||
fetch({ url: 'system/feature', method: 'get' })
|
||||
43
front/src/api/system/org.js
Normal file
43
front/src/api/system/org.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【增】 - 组织机构
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createOrgApi = (data) =>
|
||||
fetch({ url: 'system/org', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 组织机构
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOrgApi = (id) =>
|
||||
fetch({ url: 'system/org/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* 【改】 - 组织机构
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateOrgApi = ({ id, ...data }) =>
|
||||
fetch({ url: 'system/org/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】 - 组织机构
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findAllOrgApi = () =>
|
||||
fetch({ url: 'system/org', method: 'get' })
|
||||
|
||||
/**
|
||||
* 通过用户ID查询组织
|
||||
*
|
||||
* @param {string} userId 用户ID,可为空,默认当前用户ID
|
||||
* @returns
|
||||
*/
|
||||
export const findOrgByUserIdApi = (userId) =>
|
||||
fetch({ url: 'system/org/findOrgByUserId', method: 'get', params: { userId } })
|
||||
150
front/src/api/system/resource.js
Normal file
150
front/src/api/system/resource.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【增】 - 资源类型
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createResourceClassifyApi = (data) =>
|
||||
fetch({ url: 'resource/classify', method: 'post', data })
|
||||
/**
|
||||
* 【查】 - 共享资源评论区
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const pagingDis = (params) =>
|
||||
fetch({ url: 'resource/pagingDis', method: 'get', params })
|
||||
/**
|
||||
* 【增】 - 共享资源评论区评论
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const submitDisAPI = (data) =>
|
||||
fetch({ url: 'resource/discussion', method: 'post', data })
|
||||
/**
|
||||
* 【删】 - 删除共享资源评论区
|
||||
*
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteDisAPI = (id) =>
|
||||
fetch({ url: 'resource/discussion/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* 【增】 - 资源文件
|
||||
*
|
||||
* @param {Array} data 文件夹名
|
||||
* @returns
|
||||
*/
|
||||
export const createResourceApi = (data) =>
|
||||
fetch({ url: 'resource/createResource', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【增】 - 资源文件夹
|
||||
*
|
||||
* @param {string} name 文件夹名
|
||||
* @returns
|
||||
*/
|
||||
export const createFolderApi = (data) =>
|
||||
fetch({ url: 'resource/createFolder', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 资源类型
|
||||
*
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteResourceClassifyApi = (id) =>
|
||||
fetch({ url: 'resource/classify/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* 【改】 - 资源类型
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateResourceClassifyApi = ({ id, ...data }) =>
|
||||
fetch({ url: 'resource/classify/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】 - 资源类型
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findAllResourceClassifyApi = () =>
|
||||
fetch({ url: 'resource/classify', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 所有资源
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findAllResourceApi = (params) =>
|
||||
fetch({ url: 'resource', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 【查】 - 资源信息
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findResourceApi = (id) =>
|
||||
fetch({ url: 'resource/findOne/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 资源文件夹
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findAllFolderApi = () =>
|
||||
fetch({ url: 'resource/findAllFolder', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【改】 - 资源名称
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateResourceNameApi = (id, name) =>
|
||||
fetch({ url: 'resource/rename/' + id, method: 'patch', data: { name } })
|
||||
|
||||
/**
|
||||
* 【改】 - 资源-资源类型
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateResourceClassifyIdsApi = (ids, classifyIds) =>
|
||||
fetch({
|
||||
url: 'resource/updateClassify',
|
||||
method: 'patch',
|
||||
data: { ids, classifyIds: classifyIds.toString(), classifyId: classifyIds[classifyIds.length - 1] }
|
||||
})
|
||||
|
||||
/**
|
||||
* 【改】 - 资源路径
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateResourcePathApi = (ids, path) =>
|
||||
fetch({ url: 'resource/updatePath', method: 'patch', data: { ids, path } })
|
||||
|
||||
/**
|
||||
* 【删】 - 资源
|
||||
*
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteResourceApi = (id) =>
|
||||
fetch({ url: 'resource/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* 【改】 - 文件转换
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const fileConverApi = (id) =>
|
||||
fetch({ url: 'resource/fileConver/' + id, timeout: 0, method: 'patch' })
|
||||
34
front/src/api/system/role.js
Normal file
34
front/src/api/system/role.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【增】 - 角色
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createRoleApi = (data) =>
|
||||
fetch({ url: 'system/role', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 角色
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteRoleApi = (id) =>
|
||||
fetch({ url: 'system/role/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* 【改】 - 角色
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateRoleApi = ({ id, ...data }) =>
|
||||
fetch({ url: 'system/role/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】 - 角色
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findAllRoleApi = () =>
|
||||
fetch({ url: 'system/role', method: 'get' })
|
||||
91
front/src/api/system/user.js
Normal file
91
front/src/api/system/user.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【增】 - 用户
|
||||
*
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createUserApi = (data) =>
|
||||
fetch({ url: 'system/user', method: 'post', data })
|
||||
export const registerApi = (data) =>
|
||||
fetch({ url: 'system/user/register', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 删除多个用户
|
||||
* @param {string[]} ids
|
||||
* @returns
|
||||
*/
|
||||
export const deleteUsersApi = (ids) =>
|
||||
fetch({ url: 'system/user?ids=' + ids, method: 'delete' })
|
||||
|
||||
/**
|
||||
* 【改】 - 用户
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateUserApi = ({ id, ...data }) =>
|
||||
fetch({ url: 'system/user/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】 - 角色
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findAllUserApi = (params) =>
|
||||
fetch({ url: 'system/user', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 【查】 - 查询所有教员
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findAllTeacherApi = () =>
|
||||
fetch({ url: 'system/user/findAllTeacher', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 根据角色查询学员
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findUserByRoleApi = (baseRoleId) =>
|
||||
fetch({ url: '/system/user/role/' + baseRoleId, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 根据组织ID查询学员
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export const findStudentByOrgIdApi = (orgId) =>
|
||||
fetch({ url: 'system/user/findStudentByOrgId', method: 'get', params: { orgId } })
|
||||
|
||||
/**
|
||||
* 【查】 - 查询老师所管辖的所有学生
|
||||
*
|
||||
* @param {string} teacherId 老师ID,默认当前登陆人
|
||||
* @returns
|
||||
*/
|
||||
export const findStudentByTeacherApi = (teacherId) =>
|
||||
fetch({ url: 'system/user/findStudentByTeacher', method: 'get', params: { teacherId } })
|
||||
|
||||
/**
|
||||
* 【改】 - 修改用户密码
|
||||
* @param {Object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updatePasswordApi = (data) =>
|
||||
fetch({ url: 'system/user/updatePassword', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 重置用户密码
|
||||
* @returns
|
||||
*/
|
||||
export const resetPasswordApi = (id) =>
|
||||
fetch({ url: 'system/user/resetPassword/' + id, method: 'patch' })
|
||||
|
||||
/**
|
||||
* 【新增】 - 批量添加
|
||||
* @returns
|
||||
*/
|
||||
export const batchCreateUserApi = (data) =>
|
||||
fetch({ url: 'system/user/batchCreate', method: 'post', data })
|
||||
96
front/src/api/training/device.js
Normal file
96
front/src/api/training/device.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* -----------------------设备分类-----------------------
|
||||
* 【查】 - 获取所有设备分类
|
||||
*/
|
||||
export const getDeviceClassifyApi = () =>
|
||||
fetch({ url: '/train/device/classify', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 新增设备分类
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const createDeviceClassifyApi = (data) =>
|
||||
fetch({ url: '/train/device/classify', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑设备分类
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const editorDeviceClassifyApi = (id, data) =>
|
||||
fetch({ url: '/train/device/classify/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 删除设备分类
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const deleteDeviceClassifyApi = (id) =>
|
||||
fetch({ url: '/train/device/classify/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* -----------------------设备-----------------------
|
||||
* 【查】 - 分页查询设备
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const paginggetDeviceApi = (params) =>
|
||||
fetch({ url: '/train/device/device/paging', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 【查】 - 查询所有设备
|
||||
*/
|
||||
|
||||
export const getDeviceApi = () =>
|
||||
fetch({ url: '/train/device/device', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 查询设备详情
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const getDeviceDetailsApi = (id) =>
|
||||
fetch({ url: '/train/device/device/details/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【增】 - 增加设备
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const createDevicesApi = (data) =>
|
||||
fetch({ url: '/train/device/device', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 增加设备
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const editorDevicesApi = (id, data) =>
|
||||
fetch({ url: '/train/device/device/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】 - 查询设备拥有的部件及对应的部件状态
|
||||
* @param {Array<Number>} data
|
||||
*/
|
||||
|
||||
export const getDevicesInfoApi = (data) =>
|
||||
fetch({ url: '/train/device/device/findOperationContent', method: 'post', data: { devices: data } })
|
||||
|
||||
/**
|
||||
* 【查】 - 删除设备
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const deleteDevicesApi = (data) =>
|
||||
fetch({ url: '/train/device/device/all', method: 'delete', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 删除设备
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const testConnectDeviceApi = (params) =>
|
||||
fetch({ url: '/train/device/device/ping', method: 'get', params })
|
||||
19
front/src/api/training/mqtt.js
Normal file
19
front/src/api/training/mqtt.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* -----------------------设备分类-----------------------
|
||||
* 【查】 - 获取所有设备分类
|
||||
*/
|
||||
export const getMqttOnlineClient = async () => {
|
||||
return fetch({
|
||||
url: '/train/trainModule/proxy',
|
||||
method: 'post',
|
||||
data: {
|
||||
url: MqttApiAddres + 'clients',
|
||||
auth: {
|
||||
...MqttApiKey
|
||||
},
|
||||
method: 'GET'
|
||||
}
|
||||
})
|
||||
}
|
||||
41
front/src/api/training/operation.js
Normal file
41
front/src/api/training/operation.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】 - 获取所有部件状态
|
||||
*/
|
||||
export const getOperationApi = () =>
|
||||
fetch({ url: '/train/device/operation', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 分页获取所有部件状态
|
||||
* @param {object} paging
|
||||
* @returns
|
||||
*/
|
||||
export const getOperationPageApi = (params) =>
|
||||
fetch({ url: '/train/device/operation/paging', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 【增】 - 新增部件状态
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createOperationApi = (data) =>
|
||||
fetch({ url: '/train/device/operation', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑部件状态
|
||||
* @param {id} number
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const editorOperationApi = (id, data) =>
|
||||
fetch({ url: '/train/device/operation/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑部件状态
|
||||
* @param {Array[number]} ids
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const deleteOperationApi = (data) =>
|
||||
fetch({ url: '/train/device/operation/all', method: 'delete', data })
|
||||
41
front/src/api/training/post.js
Normal file
41
front/src/api/training/post.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】 - 获取所有号位
|
||||
*/
|
||||
export const getPostApi = () =>
|
||||
fetch({ url: '/train/device/position', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 分页获取所有号位
|
||||
* @param {object} paging
|
||||
* @returns
|
||||
*/
|
||||
export const getPostPageApi = (paging) =>
|
||||
fetch({ url: `/train/device/position/paging/?currentPage=${paging.currentPage}&pageSize=${paging.pageSize}`, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【增】 - 新增号位
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createPostApi = (data) =>
|
||||
fetch({ url: '/train/device/position', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑号位
|
||||
* @param {id} number
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const editorPostApi = (id, data) =>
|
||||
fetch({ url: '/train/device/position/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑号位
|
||||
* @param {Array[number]} ids
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const deletePostApi = (data) =>
|
||||
fetch({ url: '/train/device/position/all', method: 'delete', data })
|
||||
123
front/src/api/training/subject.js
Normal file
123
front/src/api/training/subject.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* -----------------------科目分类-----------------------
|
||||
* 【查】 - 获取所有科目分类
|
||||
*/
|
||||
export const getSubjectClassifyApi = (params) =>
|
||||
fetch({ url: '/train/subject/classify', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 【查】 - 新增科目分类
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const createSubjectClassifyApi = (data) =>
|
||||
fetch({ url: '/train/subject/classify', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑科目分类
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const editorSubjectClassifyApi = (id, data) =>
|
||||
fetch({ url: '/train/subject/classify/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 删除科目分类
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const deleteSubjectClassifyApi = (id) =>
|
||||
fetch({ url: '/train/subject/classify/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* -----------------------科目类型-----------------------
|
||||
* 【查】 - 查询科目所所有类型
|
||||
* @param {object} data
|
||||
*/
|
||||
export const getSubjectTypeApi = () =>
|
||||
fetch({ url: '/train/subject/traintype', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 新增科目分类类型
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const createSubjectTypefyApi = (data) =>
|
||||
fetch({ url: '/train/subject/traintype', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑科目分类类型
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const editorSubjectTypeApi = (id, data) =>
|
||||
fetch({ url: '/train/subject/traintype/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 删除科目分类类型
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const deleteSubjectTypeApi = (id) =>
|
||||
fetch({ url: '/train/subject/traintype/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* -----------------------科目-----------------------
|
||||
* 【查】 - 分页查询科目
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const pagingGetSubjectApi = (params) =>
|
||||
fetch({ url: '/train/subject/subject/paging', method: 'get', params })
|
||||
|
||||
/**
|
||||
* 【改】 - 修改科目状态
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const changeSubjectStatusApi = (data) =>
|
||||
fetch({ url: '/train/subject/subject/status', method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【查】 - 查询科目详情
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const getSubjectDetailsApi = (id) =>
|
||||
fetch({ url: '/train/subject/subject/details/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【增】 - 增加科目
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const createSubjectApi = (data) =>
|
||||
fetch({ url: '/train/subject/subject', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【复制】 - 增加科目
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const copySubjectApi = (data) =>
|
||||
fetch({ url: '/train/subject/subject/copySubject', method: 'post', data })
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑科目
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const editorSubjectApi = (id, data) =>
|
||||
fetch({ url: '/train/subject/subject/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 删除科目
|
||||
* @param {object} data
|
||||
*/
|
||||
|
||||
export const deleteSubjectApi = (data) =>
|
||||
fetch({ url: '/train/subject/subject/all', method: 'delete', data })
|
||||
72
front/src/api/training/train-evealuation.js
Normal file
72
front/src/api/training/train-evealuation.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】 - 获取训练分数详情
|
||||
*/
|
||||
export const getScoreApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/score/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取训练步骤详情
|
||||
*/
|
||||
export const getStepApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/step/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取用户训练综合信息
|
||||
*/
|
||||
export const getUserTrainInfoApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/user/basic/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取用户号位选择信息
|
||||
*/
|
||||
export const getUserPositionfoApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/user/position/' + id, method: 'get' })
|
||||
/**
|
||||
* 【查】 - 获取用户训练用时信息
|
||||
*/
|
||||
export const getUserTrainDurationApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/user/duration/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取用户科目成绩信息
|
||||
*/
|
||||
export const getUserSubjectGradeApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/user/scoreLine/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取用户号位成绩信息
|
||||
*/
|
||||
export const getUserPositionGradeApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/user/positionScore/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取所有训练场次科目成绩信息
|
||||
*/
|
||||
export const getUserTrainSubjectGradeApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/user/subjectScore/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取用户所操作设备的正确错误占比
|
||||
*/
|
||||
export const getUserOperationResultApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/user/deviceOper/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取各班组各号位平均成绩统计
|
||||
*/
|
||||
export const getTeamGroupAgeScoresApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/subject/posAvgScore/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 各号位的各班组操作步骤统计
|
||||
*/
|
||||
export const positionGroupOpeartionApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/subject/posAvgOperation/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 各号位的各班组操作步骤统计
|
||||
*/
|
||||
export const getTeamGroupDurationApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/analysis/subject/posAvgDuration/' + id, method: 'get' })
|
||||
99
front/src/api/training/train.js
Normal file
99
front/src/api/training/train.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import fetch from '@/utils/fetch'
|
||||
|
||||
/**
|
||||
* 【查】 - 获取所有训练
|
||||
*/
|
||||
export const getPostApi = () =>
|
||||
fetch({ url: '/train/device/position', method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 分页获取所有号位
|
||||
* @param {object} paging
|
||||
* @returns
|
||||
*/
|
||||
export const getPostPageApi = (paging) =>
|
||||
fetch({ url: `/train/device/position/paging/?currentPage=${paging.currentPage}&pageSize=${paging.pageSize}`, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【增】 - 新增训练
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const createTrainApi = (data) =>
|
||||
fetch({ url: '/train/trainModule', method: 'post', data })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取所有训练信息
|
||||
* @returns
|
||||
*/
|
||||
export const getTrainListApi = (status) =>
|
||||
fetch({ url: '/train/trainModule?', method: 'get', params: { status } })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取所有训练步骤
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const getTrainStepApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/step/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【查】 - 获取训练详细信息
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const getTrainDetailsApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/details/' + id, method: 'get' })
|
||||
|
||||
/**
|
||||
* 【改】 - 获取所有训练信息
|
||||
* @param {number} id
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const patchStepInfoApi = ({ id, data }) =>
|
||||
fetch({ url: '/train/trainModule/log/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【改】 - 编辑训练
|
||||
* @param {id} number
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const editorTrainApi = (id, data) =>
|
||||
fetch({ url: '/train/trainModule/status/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【删】 - 删除训练
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
export const deleteTrainApi = (id) =>
|
||||
fetch({ url: '/train/trainModule/' + id, method: 'delete' })
|
||||
|
||||
/**
|
||||
* 【删】 - 清空训练
|
||||
* @param {Array} ids
|
||||
* @returns
|
||||
*/
|
||||
export const deleteAllTrainApi = () =>
|
||||
fetch({ url: '/train/trainModule/all', method: 'delete' })
|
||||
|
||||
|
||||
/**
|
||||
* 【改】 - 更新训练的步骤信息
|
||||
* @param {number} ids
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const updateStepApi = (id, data) =>
|
||||
fetch({ url: '/train/trainModule/log/' + id, method: 'patch', data })
|
||||
|
||||
/**
|
||||
* 【增】 - 训练结果保存
|
||||
* @param {number} ids
|
||||
* @param {object} data
|
||||
* @returns
|
||||
*/
|
||||
export const saveTrainScoreApi = (id, data) =>
|
||||
fetch({ url: '/train/trainModule/score/' + id, method: 'post', data })
|
||||
386
front/src/components/business/Preview.vue
Normal file
386
front/src/components/business/Preview.vue
Normal file
@@ -0,0 +1,386 @@
|
||||
<script lang="jsx">
|
||||
import { getAcceptType } from '@/utils'
|
||||
import { fileConverApi } from '@/api/system/resource'
|
||||
|
||||
import { dataReportMixin } from '@/utils/data-report'
|
||||
|
||||
function image (h) {
|
||||
return (
|
||||
<div class="r_t_image">
|
||||
<img
|
||||
src={this.src}
|
||||
draggable="false"
|
||||
style={`transform:translate(${this.image.x}px, ${this.image.y}px) scale(${this.image.s}) rotate(${this.image.r}deg)`}
|
||||
onmousedown={this.onMove}
|
||||
/>
|
||||
<div class="actions">
|
||||
<i class="el-icon-zoom-in" onclick={() => this.imageScale(1)} />
|
||||
<i class="el-icon-refresh-right" onclick={() => this.imageRotate(1)} />
|
||||
<i class="el-icon-full-screen" onclick={() => this.onFullScreen()} />
|
||||
<i class="el-icon-refresh-left" onclick={() => this.imageRotate(-1)} />
|
||||
<i class="el-icon-zoom-out" onclick={() => this.imageScale(-1)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function video (h) {
|
||||
return (
|
||||
<div class="r_t_mp4">
|
||||
<video
|
||||
src={this.src}
|
||||
controls
|
||||
disablePictureInPicture
|
||||
controlsList="nodownload noremoteplayback noplaybackrate"
|
||||
class={{ r_t_mp4: 1, timeline: this.timeline }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function audio (h) {
|
||||
return (
|
||||
<div class="r_t_mp3">
|
||||
<div class="left">
|
||||
<i onclick={this.playAudio} class={'icon i-' + ['bofang', 'zanting'][this.mp3.state]} />
|
||||
</div>
|
||||
<div class="right">
|
||||
<p data-label="文件名称">{this.resource.name}</p>
|
||||
<p data-label="文件大小">{this.resource.size.formatFileSize()}</p>
|
||||
<p data-label="上传时间">{new Date(this.resource.createTime).format('yyyy-MM-dd')}</p>
|
||||
<div class="progress" onclick={this.onClickProgress} style={{ '--progress': this.mp3.current_time / this.mp3.duration }} />
|
||||
<div class="times" data-current-time={this.formatAudioTime(this.mp3.current_time)} data-duration-time={this.formatAudioTime(this.mp3.duration)}>
|
||||
</div>
|
||||
<audio ref="audioRef" src={this.src} oncanplay={this.onAudioCanplay} onended={() => (this.mp3.state = 0)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function pdf (h) {
|
||||
return (
|
||||
<div class="r_t_pdf">
|
||||
<iframe src={this.src} />
|
||||
|
||||
<div class="actions">
|
||||
<i class="el-icon-full-screen" onclick={() => this.onFullScreen()} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const components = { image, audio, video, pdf }
|
||||
|
||||
export default {
|
||||
props: {
|
||||
timeline: { type: Boolean },
|
||||
resource: { type: Object }
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
src: null,
|
||||
isFullScreen: false,
|
||||
mp3: { state: 0, current_time: 0, duration: 0 },
|
||||
image: { x: 0, y: 0, s: 1, r: 0 },
|
||||
errorMsg: null
|
||||
}),
|
||||
|
||||
created () {
|
||||
const type = getAcceptType(this.resource.mimetype)
|
||||
if (!type) return
|
||||
|
||||
this.viewType = components[type] ? type : 'pdf'
|
||||
this.src = this.resource.preview?.fileLinkTransfer()
|
||||
|
||||
if (!this.src) this.toConver(this.resource)
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
this.isFullScreen = !!document.fullscreenElement
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
async toConver (resource) {
|
||||
try {
|
||||
const { data } = await fileConverApi(resource.id)
|
||||
resource.preview = data
|
||||
if (this.resource === resource) this.src = data.fileLinkTransfer()
|
||||
} catch (err) {
|
||||
this.errorMsg = '文件转换失败'
|
||||
}
|
||||
},
|
||||
|
||||
imageScale (s) {
|
||||
const _s = this.image.s + s * 0.2
|
||||
if (_s >= 0.6 && _s <= 2) this.image.s = _s
|
||||
},
|
||||
|
||||
imageRotate (r) {
|
||||
this.image.r = this.image.r + r * 90
|
||||
},
|
||||
|
||||
onFullScreen () {
|
||||
if (this.isFullScreen) {
|
||||
document.exitFullscreen()
|
||||
} else {
|
||||
this.$el.requestFullscreen()
|
||||
}
|
||||
},
|
||||
|
||||
onMove (el) {
|
||||
const ox = this.image.x
|
||||
const oy = this.image.y
|
||||
const sx = el.pageX
|
||||
const sy = el.pageY
|
||||
document.body.onmouseup = () => {
|
||||
el.target.onmouseup = null
|
||||
el.target.onmousemove = null
|
||||
}
|
||||
el.target.onmousemove = (e) => {
|
||||
this.image.x = ox + e.pageX - sx
|
||||
this.image.y = oy + e.pageY - sy
|
||||
}
|
||||
},
|
||||
|
||||
onClickProgress (e) {
|
||||
this.$refs.audioRef.currentTime = this.mp3.duration * e.offsetX / 300
|
||||
},
|
||||
|
||||
formatAudioTime (t) {
|
||||
return `${Math.floor(t / 60).toString().padStart(2, 0)}:${Math.ceil(t % 60).toString().padStart(2, 0)}`
|
||||
},
|
||||
|
||||
onAudioCanplay (e) {
|
||||
const handleTimes = ({ target }) => {
|
||||
this.mp3.current_time = target.currentTime
|
||||
this.mp3.duration = target.duration
|
||||
}
|
||||
e.target.addEventListener('timeupdate', handleTimes)
|
||||
handleTimes(e)
|
||||
},
|
||||
|
||||
playAudio () {
|
||||
if (this.$refs.audioRef.paused) {
|
||||
this.$refs.audioRef.play()
|
||||
this.mp3.state = 1
|
||||
} else {
|
||||
this.$refs.audioRef.pause()
|
||||
this.mp3.state = 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
if (!this.viewType) return <div class="no_resource_preview">该文件暂不支持预览</div>
|
||||
|
||||
if (this.errorMsg) return <div class="load_file_error">{this.errorMsg}</div>
|
||||
|
||||
if (!this.src) return <div class="load_file">文件加载中,请稍后...</div>
|
||||
|
||||
return (
|
||||
<div class={{ resource_layout: 1, 'is-full': this.isFullScreen }}>{
|
||||
components[this.viewType]?.call(this, h)
|
||||
}</div>
|
||||
)
|
||||
},
|
||||
|
||||
mixins: [
|
||||
dataReportMixin('RESOURCE_VIEW', {
|
||||
isCancelReport () { return !this.viewType },
|
||||
handleReportData () { return { field01: this.resource.id } }
|
||||
}, true)
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.no_resource_preview,
|
||||
.load_file_error,
|
||||
.load_file {
|
||||
text-align: center;
|
||||
line-height: 180px;
|
||||
color: $--color-primary;
|
||||
background-color: #fafafa;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.load_file_error {
|
||||
color: $--color-danger;
|
||||
}
|
||||
|
||||
.resource_layout {
|
||||
display: flex;
|
||||
min-width: 580px;
|
||||
min-height: 150px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.actions {
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, .6);
|
||||
border-radius: 88px;
|
||||
|
||||
i {
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.r_t_image {
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.r_t_pdf {
|
||||
width: 100%;
|
||||
height: 70vh;
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-full .r_t_pdf {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.r_t_mp3 {
|
||||
margin: 30px 0;
|
||||
display: flex;
|
||||
|
||||
.left {
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
border: 2px solid rgba($color: $--color-primary, $alpha: 0.6);
|
||||
box-shadow: 0 0 6px 1px rgba($color: $--color-primary, $alpha: 0.3);
|
||||
border-radius: 6px;
|
||||
background-color: rgba($color: $--color-primary, $alpha: 0.1);
|
||||
|
||||
.icon {
|
||||
margin-top: -25px;
|
||||
margin-left: -25px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
font-size: 50px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
box-shadow: inherit;
|
||||
border-radius: inherit;
|
||||
background-color: #208ac6;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
margin-left: 24px;
|
||||
|
||||
> p {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
width: 300px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
|
||||
&::before {
|
||||
content: attr(data-label) ':';
|
||||
color: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 6px;
|
||||
height: 8px;
|
||||
border-radius: 30px;
|
||||
background-color: #f5f5f5;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: calc(100% - 100% * var(--progress));
|
||||
background-color: #208ac6;
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.times {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #aaa;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: attr(data-current-time);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: attr(data-duration-time);
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-left: -16px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
background-color: #87D2D9;
|
||||
background-image: linear-gradient(260deg,#40b8c3,#78d5de);
|
||||
box-shadow: 3px 4px 8px 0 #c0c4cc;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.r_t_mp4 {
|
||||
&,
|
||||
video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 66vh;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
video:not(.timeline)::-webkit-media-controls-timeline {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
47
front/src/components/layout/ActionBar.vue
Normal file
47
front/src/components/layout/ActionBar.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div :class="['action_bar', { 't-center': center, 't-right': right }]">
|
||||
<el-button v-if="!noCencel" round @click="handleClick('onCancel')">
|
||||
{{ cencelTxt }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!noConfirm"
|
||||
type="primary"
|
||||
:loading="confirmLoading"
|
||||
round
|
||||
@click="handleClick('onConfirm')"
|
||||
>
|
||||
{{ confirmTxt }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
noConfirm: { type: Boolean },
|
||||
noCencel: { type: Boolean },
|
||||
center: { type: Boolean },
|
||||
right: { type: Boolean },
|
||||
cencelTxt: { type: String, default: '取消' },
|
||||
confirmTxt: { type: String, default: '确定' },
|
||||
confirmLoading: { type: Boolean }
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleClick (name) {
|
||||
this.$emit(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action_bar {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.el-button + .el-button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
112
front/src/components/layout/DialogLayout.vue
Normal file
112
front/src/components/layout/DialogLayout.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="dialog_layout" v-if="visible">
|
||||
<div class="_ctx" :style="{ width }">
|
||||
<h2 class="_title">
|
||||
{{ title }}<i class="_close el-icon-close" @click="onCancel" v-if="showClose" />
|
||||
</h2>
|
||||
<div class="_body" :style="{ height: bodyHeight }"><slot /></div>
|
||||
<ActionBar
|
||||
:class="{ shadow_bar: shadowBar }"
|
||||
center
|
||||
v-bind="actionBarOption"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActionBar from './ActionBar.vue'
|
||||
|
||||
export default {
|
||||
components: { ActionBar },
|
||||
|
||||
props: {
|
||||
visible: { type: Boolean },
|
||||
shadowBar: { type: Boolean, default: true },
|
||||
title: { type: String },
|
||||
showClose: { type: Boolean, default: true },
|
||||
width: { type: String },
|
||||
bodyHeight: { type: String },
|
||||
actionBarOption: { type: Object }
|
||||
},
|
||||
|
||||
watch: {
|
||||
visible: {
|
||||
immediate: true,
|
||||
handler (v) {
|
||||
if (!v) return
|
||||
|
||||
this.$nextTick(() => document.body.appendChild(this.$el))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
this.$emit('update:visible', false)
|
||||
this.$el.parentNode?.removeChild(this.$el)
|
||||
},
|
||||
|
||||
methods: {
|
||||
onCancel () {
|
||||
this.$emit('onCancel')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.dialog_layout {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 99;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
> ._ctx {
|
||||
min-width: 300px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 46%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 6px;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
> ._title {
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
background-color: #87d2d9;
|
||||
position: relative;
|
||||
|
||||
> ._close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
> ._body {
|
||||
padding: 12px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
> .action_bar {
|
||||
padding: 0 12px 12px;
|
||||
|
||||
&.shadow_bar {
|
||||
padding: 8px 12px;
|
||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
front/src/components/layout/FormLayout.vue
Normal file
57
front/src/components/layout/FormLayout.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
|
||||
methods: {
|
||||
async validate () {
|
||||
try {
|
||||
await this.$refs.form_layout.validate()
|
||||
} catch (err) {
|
||||
const msg = Object.values(err)[0][0].message
|
||||
this.$message.error(msg)
|
||||
throw msg
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const { items, ...props } = this.$attrs
|
||||
const childs = []
|
||||
for (let i = 0, len = items.length; i < len; i++) {
|
||||
const { model, style, ...itemProps } = items[i]
|
||||
childs.push(h(
|
||||
'el-form-item',
|
||||
{
|
||||
style: { width: props.inline === '' ? '36%' : null, ...style },
|
||||
props: itemProps,
|
||||
scopedSlots: {
|
||||
default: () => {
|
||||
if (itemProps.prop && this.$scopedSlots[itemProps.prop]) return this.$scopedSlots[itemProps.prop](items[i])
|
||||
|
||||
const { tag, on, attrs = {}, ...modelProps } = model
|
||||
attrs.maxlength ??= 99
|
||||
|
||||
return h(tag, {
|
||||
props: { ...modelProps, value: props.model[itemProps.prop] },
|
||||
attrs: { placeholder: tag === 'el-input' ? '请输入' : modelProps.placeholder || '请选择', ...attrs },
|
||||
on: { input: (val) => (props.model[itemProps.prop] = val), ...on }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
return h('el-form', { ref: 'form_layout', props, class: 'form_layout' }, childs)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.form_layout {
|
||||
.el-cascader,
|
||||
.el-select,
|
||||
.el-date-editor.el-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
133
front/src/components/layout/SearchTreeMenu.vue
Normal file
133
front/src/components/layout/SearchTreeMenu.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="search_tree_menu v-menu">
|
||||
<h2 class="v-title">{{ title }}</h2>
|
||||
|
||||
<SearchInput v-model="search" />
|
||||
|
||||
<div class="mt-12 t-right">
|
||||
<el-button v-if="$listeners.onCopy" type="warning" circle plain icon="el-icon-copy-document" size="mini" title="复制" @click="handleCopy" />
|
||||
<el-button v-if="$listeners.onEdit" type="primary" circle plain icon="el-icon-edit" size="mini" title="编辑" @click="handleEdit" />
|
||||
<el-button v-if="$listeners.onCreate" type="success" circle plain icon="el-icon-plus" size="mini" title="创建" @click="handleCreate" />
|
||||
<el-button v-if="$listeners.onDelete" type="danger" circle plain icon="el-icon-delete" size="mini" title="删除" @click="handleDelete" />
|
||||
</div>
|
||||
|
||||
<div class="tree_layout">
|
||||
<ElTree
|
||||
ref="treeRef"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
highlight-current
|
||||
:props="{ label: 'name' }"
|
||||
v-bind="$attrs"
|
||||
:data="treeData"
|
||||
:node-key="treeNodeKey"
|
||||
:filter-node-method="filterNode"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { SearchInput } from '../widget'
|
||||
|
||||
export default {
|
||||
components: { SearchInput },
|
||||
|
||||
props: {
|
||||
title: { type: String },
|
||||
treeNodeKey: { type: String, default: 'id' },
|
||||
treeData: { type: Array, default: () => [] },
|
||||
treeProps: { type: Object, default: () => ({}) }
|
||||
},
|
||||
|
||||
data: () => ({ search: '' }),
|
||||
|
||||
watch: {
|
||||
search (v) {
|
||||
this.$refs.treeRef.filter(v)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
filterNode (value, data) {
|
||||
if (!value) return true
|
||||
return data.name.includes(value)
|
||||
},
|
||||
|
||||
/**
|
||||
* (key) 待被选节点的 key,若为 null 则取消当前高亮的节点
|
||||
*/
|
||||
setCurrentKey (key) {
|
||||
return this.$refs.treeRef.setCurrentKey(key)
|
||||
},
|
||||
|
||||
getCurrentNode () {
|
||||
return this.$refs.treeRef.getCurrentNode()
|
||||
},
|
||||
|
||||
handleCreate () {
|
||||
this.$emit('onCreate', this.getCurrentNode())
|
||||
},
|
||||
|
||||
handleEdit () {
|
||||
const checkedTreeNode = this.getCurrentNode()
|
||||
if (!checkedTreeNode) return this.$message.error('请选择要修改的数据')
|
||||
this.$emit('onEdit', checkedTreeNode)
|
||||
},
|
||||
|
||||
handleCopy () {
|
||||
const checkedTreeNode = this.getCurrentNode()
|
||||
if (!checkedTreeNode) return this.$message.error('请选择要复制的数据')
|
||||
this.$emit('onCopy', checkedTreeNode)
|
||||
},
|
||||
|
||||
handleDelete () {
|
||||
const checkedTreeNode = this.getCurrentNode()
|
||||
if (!checkedTreeNode) return this.$message.error('请选择要删除的数据')
|
||||
this.$emit('onDelete', checkedTreeNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.search_tree_menu .tree_layout {
|
||||
|
||||
.el-tree-node__label {
|
||||
color: $--color-text-regular;
|
||||
}
|
||||
|
||||
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content .el-tree-node__label {
|
||||
color: $--color-primary;
|
||||
}
|
||||
|
||||
.el-tree-node:focus > .el-tree-node__content,
|
||||
.el-tree-node__content:hover {
|
||||
background-color: #ecf8f9;
|
||||
}
|
||||
|
||||
.el-tree__empty-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search_tree_menu.v-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.v-title {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.tree_layout {
|
||||
flex: 1;
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #d8d8d8;
|
||||
overflow: overlay;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
front/src/components/layout/TableLayout.vue
Normal file
129
front/src/components/layout/TableLayout.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<script>
|
||||
import Pagination from '../widget/Pagination.vue'
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
|
||||
props: {
|
||||
selection: { type: Boolean },
|
||||
pageInfo: { type: Object }
|
||||
},
|
||||
|
||||
created () {
|
||||
this.handleColumn = {
|
||||
actions: (actions, props) => {
|
||||
return actions.map((v) => this.$createElement(
|
||||
'el-button',
|
||||
{
|
||||
props: { type: 'text', icon: 'el-icon-' + v.type },
|
||||
class: '_action_ ' + v.type,
|
||||
on: { click: () => v.click && v.click(v.type, props, actions) }
|
||||
},
|
||||
v.label
|
||||
))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleRowSelections (rows, selected) {
|
||||
for (const row of rows) {
|
||||
this.$refs.tableRef.toggleRowSelection(row, selected)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const { column, data, calcMaxHeight, ...tableProps } = this.$attrs
|
||||
const cols = []
|
||||
|
||||
if (this.selection) cols.push(h('el-table-column', { props: { type: 'selection', width: '44' } }))
|
||||
|
||||
for (let i = 0, len = column.length; i < len; i++) {
|
||||
const { on, actions, ...props } = column[i]
|
||||
cols.push(h('el-table-column', {
|
||||
props,
|
||||
on,
|
||||
scopedSlots: props.type === 'index'
|
||||
? undefined
|
||||
: {
|
||||
default: (properties) => {
|
||||
if (props.prop && this.$scopedSlots[props.prop]) return this.$scopedSlots[props.prop](properties)
|
||||
return properties.row[properties.column.property]
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
return h('div', { class: 'table_layout' }, [
|
||||
h('el-table', {
|
||||
ref: 'tableRef',
|
||||
style: { '--table-max-height': calcMaxHeight },
|
||||
props: { data, stripe: true, border: true, ...tableProps },
|
||||
on: { ...this.$listeners, 'current-change': this.$listeners['table-current-change'] }
|
||||
}, cols),
|
||||
|
||||
this.pageInfo && h(Pagination, { props: { pageInfo: this.pageInfo }, on: this.$listeners })
|
||||
])
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.table_layout {
|
||||
|
||||
.el-table {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 2px 10px 0px rgba(16,166,180,0.2);
|
||||
border: 1px solid #208ac6;
|
||||
|
||||
.cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th.el-table__cell {
|
||||
padding: 6px 0;
|
||||
background-color: $--color-primary;
|
||||
|
||||
.cell {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
._action_.edit {
|
||||
color: $--color-primary;
|
||||
}
|
||||
|
||||
._action_.view {
|
||||
color: $--color-primary;
|
||||
}
|
||||
|
||||
._action_.delete {
|
||||
color: $--color-danger;
|
||||
}
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(100vh - var(--table-max-height));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.el-table__header .el-table__cell {
|
||||
border-right-color: rgba($color: #fff, $alpha: 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.el-table::before, .el-table--group::after, .el-table--border::after {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell{
|
||||
background-color: #f1fafa;
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
front/src/components/layout/index.js
Normal file
13
front/src/components/layout/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import ActionBar from './ActionBar.vue'
|
||||
import FormLayout from './FormLayout.vue'
|
||||
import TableLayout from './TableLayout.vue'
|
||||
import DialogLayout from './DialogLayout.vue'
|
||||
import SearchTreeMenu from './SearchTreeMenu.vue'
|
||||
|
||||
export {
|
||||
ActionBar,
|
||||
FormLayout,
|
||||
TableLayout,
|
||||
DialogLayout,
|
||||
SearchTreeMenu
|
||||
}
|
||||
33
front/src/components/widget/Pagination.vue
Normal file
33
front/src/components/widget/Pagination.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
pageInfo: { type: Object, default: () => ({}) }
|
||||
},
|
||||
|
||||
render (h) {
|
||||
const onPageInfoChange = this.$listeners['page-info-change'] && {
|
||||
'size-change': (size) => {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.pageInfo.size = size
|
||||
this.$listeners?.['page-info-change']()
|
||||
},
|
||||
'current-change': (current) => {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.pageInfo.current = current
|
||||
this.$listeners?.['page-info-change']()
|
||||
}
|
||||
}
|
||||
return h('el-pagination', {
|
||||
props: {
|
||||
background: true,
|
||||
layout: 'total, sizes, prev, pager, next, jumper',
|
||||
currentPage: this.pageInfo.current,
|
||||
pageSize: this.pageInfo.size,
|
||||
pageSizes: [10, 30, 50, 100],
|
||||
...this.pageInfo
|
||||
},
|
||||
on: { ...this.$listeners, ...onPageInfoChange }
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
61
front/src/components/widget/QueryInput.vue
Normal file
61
front/src/components/widget/QueryInput.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="query_input el-input el-input--small">
|
||||
<input
|
||||
v-model="value"
|
||||
type="text"
|
||||
placeholder="请输入查询内容"
|
||||
class="el-input__inner"
|
||||
@keyup.enter="query"
|
||||
/>
|
||||
<div class="suf" @click="query">查询</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({ value: '' }),
|
||||
|
||||
watch: {
|
||||
'$attrs.value': {
|
||||
immediate: true,
|
||||
handler (v) {
|
||||
this.value = v
|
||||
}
|
||||
},
|
||||
|
||||
value (v) {
|
||||
this.$emit('input', v)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
query () {
|
||||
this.$emit('query')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.query_input {
|
||||
width: 188px;
|
||||
position: relative;
|
||||
|
||||
.el-input__inner {
|
||||
padding-right: 51px;
|
||||
}
|
||||
|
||||
.suf {
|
||||
padding: 0 10px 0 6px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 12px;
|
||||
color: $--color-text-secondary;
|
||||
border-left: 1px solid $--color-text-secondary;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
55
front/src/components/widget/SearchInput.vue
Normal file
55
front/src/components/widget/SearchInput.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="search_input el-input el-input--small">
|
||||
<input
|
||||
v-model="value"
|
||||
type="text"
|
||||
placeholder="请输入搜索内容"
|
||||
class="el-input__inner"
|
||||
/>
|
||||
<i class="el-icon-search" title="搜索" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({ value: '' }),
|
||||
|
||||
watch: {
|
||||
'$attrs.value': {
|
||||
immediate: true,
|
||||
handler (v) {
|
||||
this.value = v
|
||||
}
|
||||
},
|
||||
|
||||
value (v) {
|
||||
this.$emit('input', v)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.search_input {
|
||||
.el-input__inner {
|
||||
padding-right: 51px;
|
||||
}
|
||||
|
||||
.el-icon-search {
|
||||
width: 36px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
background-color: $--color-primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
9
front/src/components/widget/index.js
Normal file
9
front/src/components/widget/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Pagination from './Pagination.vue'
|
||||
import QueryInput from './QueryInput.vue'
|
||||
import SearchInput from './SearchInput.vue'
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
QueryInput,
|
||||
SearchInput
|
||||
}
|
||||
26
front/src/main.js
Normal file
26
front/src/main.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import './utils/prototype'
|
||||
import './utils/capture'
|
||||
import './utils/data-report'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
import 'element-ui/packages/theme-chalk/src/index.scss'
|
||||
import './styles/index.scss'
|
||||
import './styles/element-ui.scss'
|
||||
import './utils/iframe-message'
|
||||
/**
|
||||
* 挂载全局 $ELEMENT 属性
|
||||
*/
|
||||
window.Vue.prototype.$ELEMENT = { size: 'small', zIndex: 99 }
|
||||
|
||||
/**
|
||||
* 挂载全局 $store 属性
|
||||
*/
|
||||
window.Vue.prototype.$store = store
|
||||
/**
|
||||
* 创建 Vue 实例
|
||||
*/
|
||||
new window.Vue({
|
||||
router,
|
||||
render: (h) => h('RouterView')
|
||||
}).$mount('#app')
|
||||
223
front/src/router/feature.js
Normal file
223
front/src/router/feature.js
Normal file
@@ -0,0 +1,223 @@
|
||||
export const KeepAliveLayout = {
|
||||
template: '<KeepAlive><RouterView /></KeepAlive>'
|
||||
}
|
||||
|
||||
export const features = [
|
||||
{
|
||||
path: 'electronic-textbook',
|
||||
meta: { title: '电子化教材', icon: 'i-xitongguanli' },
|
||||
component: () => import('@/views/electronic-textbook')
|
||||
},
|
||||
// {
|
||||
// path: 'app1',
|
||||
// meta: { title: '测试', icon: 'i-xitongguanli', type: 'qiankun' }
|
||||
// component: () => import('@/views/test')
|
||||
// },
|
||||
{
|
||||
path: 'system',
|
||||
meta: { title: '系统管理', icon: 'i-xitongguanli' },
|
||||
component: KeepAliveLayout,
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
meta: { title: '用户管理' },
|
||||
component: () => import('@/views/system/user')
|
||||
},
|
||||
{
|
||||
path: 'org',
|
||||
meta: { title: '组织机构管理' },
|
||||
component: () => import('@/views/system/org')
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
meta: { title: '角色管理' },
|
||||
component: () => import('@/views/system/role')
|
||||
},
|
||||
{
|
||||
path: 'electronic-textbook-manage',
|
||||
meta: { title: '手册管理', icon: 'i-xitongguanli' },
|
||||
component: () => import('@/views/textbook-manage')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'train',
|
||||
meta: { title: '训练管理', icon: 'i-zaixiankecheng' },
|
||||
component: KeepAliveLayout,
|
||||
children: [
|
||||
{
|
||||
path: 'subject',
|
||||
meta: { title: '科目设置' },
|
||||
component: () => import('@/views/training/subject')
|
||||
},
|
||||
{
|
||||
path: 'training',
|
||||
meta: { title: '组织训练' },
|
||||
component: () => import('@/views/training/training')
|
||||
},
|
||||
{
|
||||
path: 'device',
|
||||
meta: { title: '设备管理' },
|
||||
component: () => import('@/views/training/device')
|
||||
},
|
||||
{
|
||||
path: 'evealuation',
|
||||
meta: { title: '统计分析' },
|
||||
component: () => import('@/views/training/evealuation')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'resource',
|
||||
meta: { title: '资源管理', icon: 'i-zaixiankecheng' },
|
||||
component: KeepAliveLayout,
|
||||
children: [
|
||||
{
|
||||
path: 'resource-manage',
|
||||
meta: { title: '资源管理' },
|
||||
component: () => import('@/views/system/resource')
|
||||
},
|
||||
{
|
||||
path: 'resource-share',
|
||||
meta: { title: '资源分享' },
|
||||
component: () => import('@/views/system/resource/share')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'online-course',
|
||||
meta: { title: '在线授课', icon: 'i-zaixianjiaoxue' },
|
||||
component: KeepAliveLayout,
|
||||
children: [
|
||||
{
|
||||
path: 'course-manage',
|
||||
meta: { title: '课程管理' },
|
||||
component: () => import('@/views/online-course/course-manage')
|
||||
},
|
||||
{
|
||||
path: 'my-course',
|
||||
meta: { title: '我的课程' },
|
||||
component: () => import('@/views/online-course/my-course')
|
||||
},
|
||||
{
|
||||
path: 'class-notes',
|
||||
meta: { title: '课堂笔记' },
|
||||
component: () => import('@/views/online-course/class-notes')
|
||||
},
|
||||
{
|
||||
path: 'online-FAQ',
|
||||
meta: { title: '在线答疑' },
|
||||
component: () => import('@/views/online-course/online-FAQ')
|
||||
},
|
||||
{
|
||||
path: 'live-lectures',
|
||||
meta: { title: '直播授课' },
|
||||
component: () => import('@/views/online-teaching/live-lectures')
|
||||
},
|
||||
{
|
||||
path: 'live-in-class',
|
||||
meta: { title: '直播上课' },
|
||||
component: () => import('@/views/online-teaching/live-in-class')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// {
|
||||
// path: 'online-teaching',
|
||||
// meta: { title: '在线教学', icon: 'i-zaixianjiaoxue' },
|
||||
// component: KeepAliveLayout,
|
||||
// children: [
|
||||
|
||||
// ]
|
||||
// },
|
||||
|
||||
{
|
||||
path: 'assessment-evaluation',
|
||||
meta: { title: '课程考核', icon: 'i-kaoheceping' },
|
||||
component: KeepAliveLayout,
|
||||
children: [
|
||||
{
|
||||
path: 'question-bank-manage',
|
||||
meta: { title: '题库管理' },
|
||||
component: () =>
|
||||
import('@/views/assessment-evaluation/question-bank-manage')
|
||||
},
|
||||
{
|
||||
path: 'examination-paper-manage',
|
||||
meta: { title: '试卷管理' },
|
||||
component: () =>
|
||||
import('@/views/assessment-evaluation/examination-paper-manage')
|
||||
},
|
||||
{
|
||||
path: 'exam-arrangement',
|
||||
meta: { title: '考试安排' },
|
||||
component: () =>
|
||||
import('@/views/assessment-evaluation/exam-arrangement')
|
||||
},
|
||||
{
|
||||
path: 'human-evaluation',
|
||||
meta: { title: '人工评卷' },
|
||||
component: () =>
|
||||
import('@/views/assessment-evaluation/human-evaluation')
|
||||
},
|
||||
{
|
||||
path: 'simulation-test',
|
||||
meta: { title: '模拟考试' },
|
||||
component: () =>
|
||||
import('@/views/assessment-evaluation/simulation-test')
|
||||
},
|
||||
{
|
||||
path: 'online-test',
|
||||
meta: { title: '在线考试' },
|
||||
component: () => import('@/views/assessment-evaluation/online-test')
|
||||
},
|
||||
{
|
||||
path: 'wrong-topic-consolidate',
|
||||
meta: { title: '错题巩固' },
|
||||
component: () =>
|
||||
import('@/views/assessment-evaluation/wrong-topic-consolidate')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'evaluation',
|
||||
meta: { title: '综合评价', icon: 'i-pingjia' },
|
||||
component: KeepAliveLayout,
|
||||
children: [
|
||||
{
|
||||
path: 'student-evaluation',
|
||||
meta: { title: '学员评价' },
|
||||
component: () => import('@/views/evaluation/student-evaluation')
|
||||
},
|
||||
{
|
||||
path: 'evaluation-setting',
|
||||
meta: { title: '评价设置' },
|
||||
component: () => import('@/views/evaluation/evaluation-setting')
|
||||
},
|
||||
{
|
||||
path: 'my-evaluation',
|
||||
meta: { title: '我的评价' },
|
||||
component: () => import('@/views/evaluation/my-evaluation')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export function findAllFeature () {
|
||||
const fn = (list) => {
|
||||
const ls = []
|
||||
for (const v of list) {
|
||||
const item = { name: v.path, label: v.meta.title }
|
||||
if (v.children && v.children.length) {
|
||||
item.children = fn(v.children)
|
||||
} else {
|
||||
item.children = []
|
||||
}
|
||||
|
||||
ls.push(item)
|
||||
}
|
||||
return ls
|
||||
}
|
||||
return fn(features)
|
||||
}
|
||||
77
front/src/router/index.js
Normal file
77
front/src/router/index.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { lst } from '../utils'
|
||||
import { features, KeepAliveLayout } from './feature'
|
||||
import { apps } from '@/utils/qiankun-init'
|
||||
import routes from './routes'
|
||||
const router = new window.VueRouter({
|
||||
mode: 'history',
|
||||
base: '',
|
||||
routes: [
|
||||
{ path: '/login', meta: { noToken: true }, component: () => import('@/views/login') },
|
||||
{ path: '/register', meta: { noToken: true }, component: () => import('@/views/register') },
|
||||
{ path: '/404', meta: { noToken: true }, component: () => import('@/views/404') },
|
||||
{ path: '/entry', meta: { noToken: true }, component: () => import('@/views/entry') }
|
||||
]
|
||||
})
|
||||
|
||||
const HomePage = {
|
||||
path: '/',
|
||||
meta: { title: '训练管理' },
|
||||
component: () => import('@/views/home'),
|
||||
children: [{ path: '/', meta: { title: '数据统计' }, component: () => import('@/views/statistic') },
|
||||
{ path: '/print-paper/:paperId?', meta: { noToken: false, key: 'printPaper' }, component: () => import('@/views/assessment-evaluation/examination-paper-manage/components/PrintPaper.vue') }
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
export function initRoutes () {
|
||||
const moduleAuth = lst.get('module')
|
||||
if (!moduleAuth) return
|
||||
|
||||
const fn = (list) => {
|
||||
const routeList = []
|
||||
const menuList = []
|
||||
for (const v of list) {
|
||||
if (moduleAuth.includes(v.path)) {
|
||||
const menu = { name: v.path, label: v.meta.title, icon: v.meta.icon }
|
||||
if (v.children && v.children.length) {
|
||||
const child = fn(v.children)
|
||||
routeList.push({ ...v, children: child.routeList })
|
||||
menu.children = child.menuList
|
||||
} else {
|
||||
routeList.push(v)
|
||||
|
||||
if (routes[v.path]) {
|
||||
routeList.push({ path: v.path, meta: v.meta, component: KeepAliveLayout, children: routes[v.path] })
|
||||
}
|
||||
}
|
||||
menuList.push(menu)
|
||||
}
|
||||
}
|
||||
return { routeList, menuList }
|
||||
}
|
||||
apps && features.push(...apps.map(i => {
|
||||
return {
|
||||
path: i.activeRule,
|
||||
meta: { title: i.name }
|
||||
}
|
||||
}))
|
||||
const { routeList, menuList } = fn(features)
|
||||
console.log(menuList)
|
||||
lst.save('menu', menuList)
|
||||
HomePage.children.push(...routeList)
|
||||
// router.addRoutes([HomePage, { path: '*', redirect: '/404' }])
|
||||
router.addRoutes([HomePage])
|
||||
}
|
||||
|
||||
initRoutes()
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
console.log(to.meta.noToken || lst.get('token', '').startsWith('Bearer '))
|
||||
if (to.meta.noToken || lst.get('token', '').startsWith('Bearer ')) {
|
||||
next()
|
||||
} else {
|
||||
next('/login')
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
62
front/src/router/routes.js
Normal file
62
front/src/router/routes.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**********************************************************************************************************************
|
||||
格式: [从属页面的 path 属性值]: [ { 路由对象 }, { 路由对象 } ]
|
||||
|
||||
例子如下:
|
||||
'question-bank-manage': [
|
||||
{ path: 'add-question', meta: { title: '添加试题' }, component: () => import('@/views/assessment-evaluation/add-question') }
|
||||
]
|
||||
**********************************************************************************************************************/
|
||||
export default {
|
||||
'course-manage': [
|
||||
{ path: 'course-evaluate/:courseId', meta: { title: '课程评价' }, component: () => import('@/views/online-course/course-manage/course-evaluate') },
|
||||
{ path: 'course-comment/:courseId', meta: { title: '课程评论' }, component: () => import('@/views/online-course/course-manage/course-comment') },
|
||||
{ path: 'add-coures', meta: { title: '添加课程' }, component: () => import('@/views/online-course/course-manage/add-coures') },
|
||||
{ path: 'edit-coures/:courseId', meta: { title: '编辑课程' }, component: () => import('@/views/online-course/course-manage/add-coures') }
|
||||
],
|
||||
'my-course': [
|
||||
{ path: 'course-study/:courseId', meta: { title: '课程学习' }, component: () => import('@/views/online-course/course-study') },
|
||||
{ path: 'course-exercies', meta: { title: '课后习题' }, component: () => import('@/views/online-course/course-exercises/course-exercises.vue') }
|
||||
],
|
||||
'question-bank-manage': [
|
||||
{ path: 'add-modify-question/:questionId?', meta: { title: '添加试题' }, component: () => import('@/views/assessment-evaluation/question-bank-manage/add-modify-question') }
|
||||
],
|
||||
'examination-paper-manage': [
|
||||
{ path: 'add-modify-paper', meta: { title: '添加试卷' }, component: () => import('@/views/assessment-evaluation/examination-paper-manage/add-modify-paper') },
|
||||
{ path: 'add-modify-paper/:paperId?', meta: { title: '编辑试卷' }, component: () => import('@/views/assessment-evaluation/examination-paper-manage/add-modify-paper') }
|
||||
],
|
||||
'exam-arrangement': [
|
||||
{ path: 'add-modify-exam', meta: { title: '发布考试' }, component: () => import('@/views/assessment-evaluation/exam-arrangement/add-modify-exam') },
|
||||
{ path: 'add-modify-exam/:examId?', meta: { title: '编辑考试' }, component: () => import('@/views/assessment-evaluation/exam-arrangement/add-modify-exam') },
|
||||
{ path: 'add-modify-exam/:examId?/:type?', meta: { title: '复制考试' }, component: () => import('@/views/assessment-evaluation/exam-arrangement/add-modify-exam') }
|
||||
],
|
||||
'online-test': [
|
||||
{ path: 'begin-online-exam/:studentExamId', meta: { title: '在线考试' }, component: () => import('@/views/assessment-evaluation/online-test/begin-online-exam') }
|
||||
],
|
||||
'simulation-test': [
|
||||
{ path: 'add-sim-test/:paperId?', meta: { title: '新建考试' }, component: () => import('@/views/assessment-evaluation/simulation-test/add-modify-sim-test') }
|
||||
],
|
||||
'human-evaluation': [
|
||||
{ path: 'eval-details/:examId?', meta: { title: '人员评卷' }, component: () => import('@/views/assessment-evaluation/human-evaluation/eval-details') },
|
||||
{ path: 'begin-eval/:studentExamId?', meta: { title: '开始判卷' }, component: () => import('@/views/assessment-evaluation/online-test/begin-online-exam') }
|
||||
],
|
||||
'wrong-topic-consolidate': [
|
||||
{ path: 'mistake-again/:classifyId/:mode?', meta: { title: '错题巩固' }, component: () => import('@/views/assessment-evaluation/wrong-topic-consolidate/mistakes-again') },
|
||||
{ path: 'history-exam-priview/:studentExamId', meta: { title: '试卷预览' }, component: () => import('@/views/assessment-evaluation/online-test/begin-online-exam') }
|
||||
],
|
||||
'live-lectures': [
|
||||
{ path: 'live-editor', meta: { title: '新建直播' }, component: () => import('@/views/online-teaching/add-live') },
|
||||
{ path: 'live-play', meta: { title: '进行直播' }, component: () => import('@/views/online-teaching/live-play') }
|
||||
// { path: 'live-playback', meta: { title: '直播回放' }, component: () => import('@/views/online-teaching/live-playback') },
|
||||
],
|
||||
'live-in-class': [
|
||||
{ path: 'live-watch', meta: { title: '观看直播' }, component: () => import('@/views/online-teaching/live-watch') },
|
||||
{ path: 'live-watch/answering', meta: { title: '直播练习题' }, component: () => import('@/views/online-teaching/live-answering-question') }
|
||||
],
|
||||
device: [
|
||||
{ path: 'create-device', meta: { title: '设备编辑' }, component: () => import('@/views/training/device/device-create/index.vue') }
|
||||
],
|
||||
user: [
|
||||
{ path: 'batch-add', meta: { title: '批量添加' }, component: () => import('@/views/system/user/batch-add-user') }],
|
||||
subject: [{ path: 'subject-editor', meta: { title: '编辑科目' }, component: () => import('@/views/training/subject/editor-subject') }]
|
||||
// training: [{ path: 'create-train', meta: { title: '添加训练' }, component: () => import('@/views/training/training/create-train') }]
|
||||
}
|
||||
144
front/src/store/index.js
Normal file
144
front/src/store/index.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import { checkAdminPasswordApi, uploadApi } from '@/api/system/index'
|
||||
import { findResourceApi } from '@/api/system/resource'
|
||||
import { lst, getDistFile, getAcceptType } from '@/utils/index'
|
||||
|
||||
const store = {
|
||||
user: undefined,
|
||||
|
||||
isOpenWin: location.hash.includes('win=1'),
|
||||
|
||||
statistic_tab_active: 0,
|
||||
|
||||
upload_dialog: { visible: false, form_data: undefined, tasks: [] },
|
||||
|
||||
update_password_dialog: { visible: false },
|
||||
|
||||
resource_preview_dialog: { visible: false, resource: null },
|
||||
|
||||
class_notes_dialog: { visible: false },
|
||||
online_FAQ_Dialog: { visible: false },
|
||||
isFullscreen: false
|
||||
}
|
||||
|
||||
const actions = {
|
||||
setUser (user) {
|
||||
if (!user) return
|
||||
lst.save('user', this.user = user)
|
||||
},
|
||||
setFullscreen (status) {
|
||||
this.$set(this, 'isFullscreen', status)
|
||||
this.isFullscreen = status
|
||||
},
|
||||
setToken (token) {
|
||||
if (!token) return
|
||||
lst.save('token', this.token = 'Bearer ' + token)
|
||||
},
|
||||
|
||||
async logout (isShowTip) {
|
||||
if (isShowTip) await this.$confirm('将要退出登录,是否继续?', '退出提示', { type: 'warning' })
|
||||
lst.clear()
|
||||
window.location.reload()
|
||||
},
|
||||
|
||||
showUpdatePasswordDialog () {
|
||||
this.update_password_dialog.visible = true
|
||||
},
|
||||
|
||||
showUploadDialog (formData) {
|
||||
this.upload_dialog.visible = true
|
||||
this.upload_dialog.form_data = formData
|
||||
},
|
||||
|
||||
showClassNotesDialog () {
|
||||
this.class_notes_dialog.visible = true
|
||||
},
|
||||
|
||||
showOnlineFaqDialog () {
|
||||
this.online_FAQ_Dialog.visible = true
|
||||
},
|
||||
|
||||
async showResourcePreviewDialog (resource) {
|
||||
if (!resource) return
|
||||
|
||||
let res = resource.id ? resource : null
|
||||
if (!res) {
|
||||
const { data } = await findResourceApi(resource)
|
||||
res = data
|
||||
}
|
||||
this.resource_preview_dialog.resource = res
|
||||
this.resource_preview_dialog.visible = true
|
||||
},
|
||||
|
||||
async upload () {
|
||||
const fs = await getDistFile()
|
||||
const tasks = []
|
||||
|
||||
for (const file of fs) {
|
||||
const name = file.name
|
||||
if (file.size > 1024 * 1024 * 500) {
|
||||
return window.ELEMENT.Message.error(`文件【${file.name}】体积过大,上传失败`)
|
||||
}
|
||||
const type = getAcceptType(file.type)
|
||||
if (!type) {
|
||||
return window.ELEMENT.Message.error(`文件【${file.name}】的格式暂不支持`)
|
||||
}
|
||||
tasks.push({ name, file, size: file.size, status: 0, progress: 0, type })
|
||||
}
|
||||
|
||||
Promise.allSettled(tasks.map(async (task, index) => {
|
||||
task.onUploadProgress = ({ loaded, total }) => {
|
||||
task.progress = loaded / total * 100 | 0
|
||||
}
|
||||
|
||||
try {
|
||||
task.id = new Date().getTime() + Math.random().toString().substring(3, 8)
|
||||
this.upload_dialog.tasks.push(task)
|
||||
task.status = 1
|
||||
task.abortCtrl = new AbortController()
|
||||
await uploadApi(task.file, { data: this.upload_dialog.form_data, signal: task.abortCtrl.signal, onUploadProgress: task.onUploadProgress })
|
||||
task.status = 2
|
||||
this.$emit('onUploadSuccess', { task, index, tasks })
|
||||
} catch (err) {
|
||||
task.status = 3
|
||||
task.error = err
|
||||
this.$emit('onUploadError', task)
|
||||
throw err
|
||||
}
|
||||
})).then(() => {
|
||||
this.$emit('onUploadEnd', tasks)
|
||||
})
|
||||
},
|
||||
|
||||
async inputCheckAdminPassword (title = '重要提示') {
|
||||
const { value } = await this.$prompt('请输入超级管理员密码', title, {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'password',
|
||||
inputPattern: /^(?![0-9]+$)(?![a-zA-Z]+$)[a-zA-Z0-9]{6,12}$/,
|
||||
inputPlaceholder: '请输入',
|
||||
inputErrorMessage: '密码输入不合法,若是初始密码请修改后再试'
|
||||
})
|
||||
|
||||
return checkAdminPasswordApi(value)
|
||||
}
|
||||
}
|
||||
|
||||
export default new window.Vue({
|
||||
data: store,
|
||||
methods: actions,
|
||||
created () {
|
||||
this.setUser(lst.get('user'))
|
||||
},
|
||||
|
||||
computed: {
|
||||
ws () {
|
||||
if (!this.__WS__) {
|
||||
this.__WS__ = window.io(
|
||||
import.meta.env.VITE_APP_WS_URL,
|
||||
{ transports: ['websocket'], auth: { token: lst.get('token') } }
|
||||
)
|
||||
}
|
||||
return this.__WS__
|
||||
}
|
||||
}
|
||||
})
|
||||
103
front/src/styles/element-ui.scss
Normal file
103
front/src/styles/element-ui.scss
Normal file
@@ -0,0 +1,103 @@
|
||||
.el-message-box {
|
||||
border: none;
|
||||
|
||||
&__header {
|
||||
background-color: #87D2D9;
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
&__title span {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&__headerbtn &__close {
|
||||
color: #333;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 20px 16px 16px;
|
||||
}
|
||||
|
||||
&__btns {
|
||||
text-align: center;
|
||||
|
||||
.el-button {
|
||||
border-radius: 50px;
|
||||
|
||||
+ .el-button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
&__item {
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.el-table {
|
||||
.el-checkbox__input {
|
||||
.el-checkbox__inner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid $--color-primary;
|
||||
background: rgba($--color-primary, 0.1);
|
||||
border-radius: 20%;
|
||||
|
||||
&::after {
|
||||
background: rgba($--color-primary, 1);
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
border-radius: 30%;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
border: none;
|
||||
transition: transform 0.05s ease-in 0.05s;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.el-checkbox__inner {
|
||||
&::after {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.middle {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__header .el-table-column--selection {
|
||||
.el-checkbox__input {
|
||||
.el-checkbox__inner {
|
||||
border: 1px solid #fff;
|
||||
|
||||
&::after {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color: $--color-text-primary;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
|
||||
&::after {
|
||||
content: ':';
|
||||
}
|
||||
}
|
||||
}
|
||||
238
front/src/styles/index.scss
Normal file
238
front/src/styles/index.scss
Normal file
@@ -0,0 +1,238 @@
|
||||
/* stylelint-disable font-family-no-missing-generic-family-keyword */
|
||||
[class^="i-"],
|
||||
[class*=" i-"] {
|
||||
font-family: iconfont !important;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: #f3f8ff;
|
||||
font-family: arial, Helvetica, sans-serif;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
color: $--color-text-primary;
|
||||
}
|
||||
|
||||
.copyright::after {
|
||||
content: "Copyright@2023北京灏博云天科技有限公司";
|
||||
display: block;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
bottom: 6px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
letter-spacing: 2px;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #eee;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.flex-h {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-v {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flex-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex-middle-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.p-12 {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.mb-12 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mr-12 {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.ml-12 {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.mt-12 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.color-primary {
|
||||
color: $--color-primary;
|
||||
}
|
||||
|
||||
.color-white {
|
||||
color: #fff;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: rgba($color: #fff, $alpha: 80%);
|
||||
}
|
||||
}
|
||||
|
||||
.color-warning {
|
||||
color: $--color-warning;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: rgba($color: $--color-warning, $alpha: 80%);
|
||||
}
|
||||
}
|
||||
|
||||
.el-cascader-node {
|
||||
height: fit-content !important;
|
||||
}
|
||||
|
||||
.color-danger {
|
||||
color: $--color-danger;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: rgba($color: $--color-danger, $alpha: 80%);
|
||||
}
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
/* stylelint-disable-next-line value-no-vendor-prefix */
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.ellipsis-1 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ellipsis-2 {
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.ellipsis-3 {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
||||
.t-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.t-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.t-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.v-empty {
|
||||
margin: 8vh auto 0;
|
||||
color: #ccc;
|
||||
user-select: none;
|
||||
|
||||
p {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.v-card {
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 8px 0 rgb(16 166 180 / 20%);
|
||||
}
|
||||
|
||||
.v-title {
|
||||
padding-left: 6px;
|
||||
font-size: 15px;
|
||||
border-left: 6px solid $--color-primary;
|
||||
}
|
||||
|
||||
.v-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.v-menu {
|
||||
margin-right: 18px;
|
||||
padding-right: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 226px;
|
||||
height: inherit;
|
||||
border-right: 1px solid #e9e8ee;
|
||||
}
|
||||
|
||||
.v-ctx {
|
||||
flex: 1;
|
||||
height: inherit;
|
||||
overflow: hidden overlay;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
input {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.el-input__suffix {
|
||||
background-color: white;
|
||||
height: calc(100% - 2px);
|
||||
right: 3px;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.self-button {
|
||||
height: 20px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
12
front/src/styles/variables.scss
Normal file
12
front/src/styles/variables.scss
Normal file
@@ -0,0 +1,12 @@
|
||||
$--color-primary: #208ac6;
|
||||
$--color-success: #02c761;
|
||||
$--color-danger: #f04343;
|
||||
$--color-warning: #ffae00;
|
||||
$--color-text-primary: #333;
|
||||
$--color-text-regular: #666;
|
||||
$--color-text-secondary: #999;
|
||||
$--background-color-base: #f1fafa;
|
||||
|
||||
$--border-color-lighter: #d8d8d8;
|
||||
|
||||
$--font-path: "element-ui/lib/theme-chalk/fonts";
|
||||
19
front/src/utils/capture.js
Normal file
19
front/src/utils/capture.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 异常捕获
|
||||
*/
|
||||
const ErrorCapture = {
|
||||
|
||||
/**
|
||||
* 处理IMG异常
|
||||
* @param {Event} event
|
||||
*/
|
||||
IMG ({ target }) {
|
||||
target.src = target.className.includes('avatar') ? '/logo.svg' : '/default.svg'
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获全局error事件
|
||||
*/
|
||||
document.addEventListener('error', (e) => ErrorCapture[e.target.tagName]?.(e), true)
|
||||
57
front/src/utils/data-report.js
Normal file
57
front/src/utils/data-report.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { reportDataApi } from '@/api/statistic/index'
|
||||
|
||||
export const REPORT_TYPES = {
|
||||
ONLINE_COURSE: '在线课程',
|
||||
LIVE_TEACHING: '教学直播',
|
||||
MOCK_EXAMINATION: '模拟考试',
|
||||
ERROR_CONSOLIDATION: '错题巩固',
|
||||
CLASS_NOTE: '课堂笔记',
|
||||
ONLINE_FAQ: '在线答疑',
|
||||
RESOURCE_VIEW: '资源查看',
|
||||
SYSTEM_USE: '系统使用'
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据上报 mixin
|
||||
*
|
||||
* @param {[keyof REPORT_TYPES]} type 上报类型
|
||||
*
|
||||
* @param {Object} options 方法
|
||||
* @param {boolean} options.isListenerUnload 是否弹出关闭页面提示框
|
||||
* @param {()=>boolean} options.isCancelReport 是否取消上报
|
||||
* @param {()=>{ id?:number, field01?:string, field02?:string, field03?:string, remarks?:string }} options.handleReportData 处理上报数据
|
||||
* field01 备用字段
|
||||
* field02 备用字段
|
||||
* field03 备用字段
|
||||
* remarks 备注说明
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export function dataReportMixin (type, { isListenerUnload, ...methods } = {}) {
|
||||
if (!type) throw Error('上报类型错误')
|
||||
|
||||
return {
|
||||
mounted () {
|
||||
if (this.isCancelReport?.()) return console.warn('上报已中断,上报数据:', type)
|
||||
|
||||
this.reportData()
|
||||
if (isListenerUnload) window.addEventListener('beforeunload', this.reportData)
|
||||
},
|
||||
|
||||
beforeDestroy () {
|
||||
if (isListenerUnload) window.removeEventListener('beforeunload', this.reportData)
|
||||
this.reportData()
|
||||
},
|
||||
|
||||
methods: {
|
||||
...methods,
|
||||
|
||||
async reportData (e) {
|
||||
e && (e.returnValue = '将要退出,是否继续?')
|
||||
|
||||
reportDataApi({ ...this.handleReportData?.(), id: this.__report_data_id__, type })
|
||||
.then(({ data }) => (this.__report_data_id__ = data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
front/src/utils/fetch.js
Normal file
63
front/src/utils/fetch.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { lst } from './index'
|
||||
|
||||
const _instance = window.axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_URL,
|
||||
timeout: 100000
|
||||
})
|
||||
|
||||
_instance.interceptors.request.use(
|
||||
(config) => {
|
||||
const _token = lst.get('token')
|
||||
_token && (config.headers.Authorization = _token)
|
||||
|
||||
if (config.formData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data;'
|
||||
config.data = new FormData()
|
||||
for (const k in config.formData) config.data.append(k, config.formData[k])
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(e) => {
|
||||
throw e
|
||||
}
|
||||
)
|
||||
|
||||
_instance.interceptors.response.use(
|
||||
({ data, status, config }) => {
|
||||
if ([201, 200].includes(status) && (data.code === 0 || config.noCatch)) return data
|
||||
throw new Error(data.msg)
|
||||
},
|
||||
(e) => {
|
||||
throw e
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {Object} option 选项
|
||||
* @param {string} option.url 请求地址
|
||||
* @param {string} option.headers 请求头
|
||||
* @param {{}} option.params URL 参数
|
||||
* @param {{}} option.data body 参数
|
||||
* @param {{}} option.formData FormData 对象参数
|
||||
* @param {'get'|'post'|'patch'|'delete'} option.method
|
||||
*/
|
||||
export default async (option) => {
|
||||
try {
|
||||
return await _instance(option)
|
||||
} catch (err) {
|
||||
let _msg
|
||||
if (err.response) {
|
||||
if (err.response.data.code === 401) {
|
||||
lst.clear()
|
||||
location.href = '/'
|
||||
}
|
||||
_msg = err.response.data.msg
|
||||
}
|
||||
|
||||
if (!option.noNotify) {
|
||||
window.ELEMENT.Message.error(_msg || err.message || '系统繁忙,请稍后再试!')
|
||||
}
|
||||
throw new Error(_msg || err)
|
||||
}
|
||||
}
|
||||
18
front/src/utils/iframe-message.js
Normal file
18
front/src/utils/iframe-message.js
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
const vue = new Vue()
|
||||
window.addEventListener('message', async (e) => {
|
||||
if (e.data.type === 'confim') {
|
||||
// console.log($confirm)
|
||||
await confirm(e)
|
||||
}
|
||||
})
|
||||
async function confirm (e) {
|
||||
vue.$confirm(e.data.title, '操作提示', { type: 'warning' })
|
||||
.then(() => {
|
||||
console.log(e)
|
||||
e.source.postMessage({ status: true, id: e.data.id }, '*')
|
||||
})
|
||||
.catch(() => {
|
||||
e.source.postMessage({ status: false, id: e.data.id }, '*')
|
||||
})
|
||||
}
|
||||
112
front/src/utils/index.js
Normal file
112
front/src/utils/index.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import storage from './storage'
|
||||
|
||||
export function mapToTree (map) {
|
||||
const list = []
|
||||
for (const k in map) {
|
||||
if (map[k].pid === 0) {
|
||||
list.push(map[k])
|
||||
} else {
|
||||
map[map[k].pid].children ??= []
|
||||
map[map[k].pid].children.push(map[k])
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件时 accept 的枚举类
|
||||
*/
|
||||
export const ACCEPT_MAP = {
|
||||
image: { icon: 'i-tupiao', color: '#ff6a4d', name: '图片文件', accept: 'image/*' },
|
||||
audio: { icon: 'i-yinyue', color: '#9660f5', name: '音频文件', accept: 'audio/*' },
|
||||
video: { icon: 'i-shipin', color: '#55d1e0', name: '视频文件', accept: 'video/*' },
|
||||
ppt: { icon: 'i-ppt', color: '#ff9357', name: '演示文稿', accept: 'application/vnd.ms-powerpoint' },
|
||||
pptx: { icon: 'i-pptx', color: '#ff9357', name: '演示文稿', accept: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
|
||||
xls: { icon: 'i-xls', color: '#28c17a', name: '表格文件', accept: 'application/vnd.ms-excel' },
|
||||
xlsx: { icon: 'i-xlsx', color: '#28c17a', name: '表格文件', accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
|
||||
pdf: { icon: 'i-pdf', color: '#ff738e', name: '文本文件', accept: 'application/pdf' },
|
||||
doc: { icon: 'i-doc', color: '#39afff', name: '文本文件', accept: 'application/msword' },
|
||||
docx: { icon: 'i-docx', color: '#39afff', name: '文本文件', accept: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
|
||||
folder: { icon: 'i-wenjianjia', color: '#ffd94a', name: '文件夹', accept: '' }
|
||||
}
|
||||
|
||||
export function getAcceptType (accept) {
|
||||
if (!accept) return
|
||||
|
||||
for (const type in ACCEPT_MAP) {
|
||||
if (accept.startsWith(ACCEPT_MAP[type].accept.replace(/\*$/, ''))) return type
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 颜色生成器
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns
|
||||
*/
|
||||
export function colorGenerator (value) {
|
||||
if (!value) return
|
||||
return Number(value.split('').reduce((n, s) => n + s.charCodeAt(), '')).toString(16).replace(/.*(.{6})$/, '#$1')
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件工具类
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {[keyof ACCEPT_MAP]} options.accepts 指定可选的文件类型
|
||||
* @param {boolean} options.multiple 是否允许用户选择多个文件
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getDistFile ({ accepts = [], multiple = false } = {}) {
|
||||
if (!getDistFile.el) { // 如果没有 Input Dom对象,则创建
|
||||
getDistFile.el = document.createElement('input')
|
||||
getDistFile.el.type = 'file'
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
getDistFile.el.setAttribute(
|
||||
'accept',
|
||||
(accepts?.length ? accepts : Object.keys(ACCEPT_MAP))
|
||||
.map(k => ACCEPT_MAP[k].accept).join(' , ')
|
||||
)
|
||||
getDistFile.el.setAttribute('multiple', multiple)
|
||||
|
||||
getDistFile.el.onchange = () => {
|
||||
resolve([...getDistFile.el.files])
|
||||
getDistFile.el.value = null
|
||||
}
|
||||
getDistFile.el.click()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载
|
||||
*
|
||||
* @param {string} src 文件地址
|
||||
* @param {string} name 文件下载名
|
||||
* @returns
|
||||
*/
|
||||
export async function download (src, name) {
|
||||
if (!src) return
|
||||
const href = await fetch(src).then(res => {
|
||||
if (res.status === 200) return res.arrayBuffer()
|
||||
throw Error('文件下载出错')
|
||||
})
|
||||
|
||||
if (!download.el) {
|
||||
download.el = document.createElement('a')
|
||||
}
|
||||
|
||||
download.el.download = name || ''
|
||||
download.el.href = URL.createObjectURL(new Blob([href]))
|
||||
download.el.click()
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地存储 localStorage
|
||||
*/
|
||||
export const lst = storage(localStorage)
|
||||
|
||||
/**
|
||||
* 本地存储 sessionStorage
|
||||
*/
|
||||
export const sst = storage(sessionStorage)
|
||||
435
front/src/utils/prototype.js
vendored
Normal file
435
front/src/utils/prototype.js
vendored
Normal file
@@ -0,0 +1,435 @@
|
||||
import jsPDF from 'jspdf'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { font } from './simhei-normal'
|
||||
/* eslint-disable no-extend-native */
|
||||
String.prototype.fetchValue = function (object) {
|
||||
return this.split('.').reduce((o, k) => (o === undefined || o === null ? undefined : o[k]), object || {})
|
||||
}
|
||||
|
||||
String.prototype.resolveObject = function (value) {
|
||||
const [first, ...os] = this.split('.').reverse()
|
||||
return os.reduce((o, k) => ({ [k]: o }), { [first]: value })
|
||||
}
|
||||
|
||||
String.prototype.fileLinkTransfer = function () {
|
||||
if (this) return import.meta.env.VITE_APP_BASE_URL + '/file-bucket/' + this
|
||||
}
|
||||
|
||||
File.prototype.fileLinkTransfer = function () {
|
||||
if (this) return URL.createObjectURL(this)
|
||||
}
|
||||
|
||||
Number.prototype.formatFileSize = function (fractionDigits = 2, type = 'string') {
|
||||
if (isNaN(this)) return this
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
let size = this
|
||||
let index = 0
|
||||
while (size > 1024 && index < units.length - 1) {
|
||||
size = size / 1024
|
||||
index++
|
||||
}
|
||||
if (type === 'object') return { value: size.toFixed(fractionDigits), unit: units[index] }
|
||||
return size.toFixed(fractionDigits) + units[index]
|
||||
}
|
||||
|
||||
Date.prototype.format = function (fmt) {
|
||||
const o = {
|
||||
'M+': this.getMonth() + 1, // 月份
|
||||
'd+': this.getDate(), // 日
|
||||
'h+': this.getHours(), // 小时
|
||||
'm+': this.getMinutes(), // 分
|
||||
's+': this.getSeconds(), // 秒
|
||||
'q+': Math.floor((this.getMonth() + 3) / 3), // 季度
|
||||
S: this.getMilliseconds() // 毫秒
|
||||
}
|
||||
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length))
|
||||
for (const k in o) {
|
||||
if (new RegExp('(' + k + ')').test(fmt)) { fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)) }
|
||||
}
|
||||
return fmt
|
||||
}
|
||||
|
||||
Date.prototype.toTypeStr = function (timestamp, fmt) {
|
||||
if (typeof timestamp === 'string') {
|
||||
fmt = timestamp
|
||||
timestamp = undefined
|
||||
}
|
||||
const time = (timestamp || new Date().getTime()) - this.getTime()
|
||||
return time < 60 * 1000
|
||||
? '刚刚'
|
||||
: time < 60 * 60 * 1000
|
||||
? Math.floor(time / (60 * 1000)) + '分钟前'
|
||||
: time < 24 * 60 * 60 * 1000
|
||||
? Math.floor(time / (60 * 60 * 1000)) + '小时前'
|
||||
: time < 48 * 60 * 60 * 1000
|
||||
? '昨天'
|
||||
: this.format(fmt || 'yyyy-MM-dd hh:mm')
|
||||
}
|
||||
|
||||
Array.generate = function (length = 0, fn) {
|
||||
const _list = []
|
||||
if (fn) {
|
||||
let i = 0
|
||||
while (i < length) _list.push(fn(i++))
|
||||
}
|
||||
return _list
|
||||
}
|
||||
|
||||
Array.prototype.toTree = function ({ key = 'id', parentKey = 'pid', childKey = 'children' } = {}) {
|
||||
const list = this // JSON.parse(JSON.stringify(this))
|
||||
const map = {}
|
||||
let index = this.length
|
||||
while (index--) {
|
||||
map[list[index][key]] = list[index]
|
||||
}
|
||||
|
||||
const tree = []
|
||||
const len = list.length
|
||||
while (++index < len) {
|
||||
const item = list[index]
|
||||
const pid = item[parentKey]
|
||||
if (map[pid]) {
|
||||
map[pid][childKey] ??= []
|
||||
map[pid][childKey].push(item)
|
||||
} else {
|
||||
tree.push(item)
|
||||
}
|
||||
}
|
||||
return { map, tree }
|
||||
}
|
||||
|
||||
// window.Vue.prototype.$html2Pdf = async function (id, isPrint) {
|
||||
// const ele = document.getElementById(id)
|
||||
// console.log(ele)
|
||||
// html2canvas(ele, { useCORS: true }).then(canvas => {
|
||||
// // 未生成pdf的html页面高度
|
||||
// // 未生成pdf的html页面高度
|
||||
// let leftHeight = canvas.height
|
||||
|
||||
// const a4Width = 595.28
|
||||
// const a4Height = 841.89 // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
|
||||
// // 一页pdf显示html页面生成的canvas高度;
|
||||
// const a4HeightRef = Math.floor((canvas.width / a4Width) * a4Height)
|
||||
|
||||
// // pdf页面偏移
|
||||
// let position = 0
|
||||
|
||||
// const pageData = canvas.toDataURL('image/jpeg', 1.0)
|
||||
|
||||
// const pdf = new jsPDF('p', 'pt', 'a4') // A4纸,纵向
|
||||
// let index = 1
|
||||
// const canvas1 = document.createElement('canvas')
|
||||
// let height
|
||||
// pdf.setDisplayMode('fullwidth', 'continuous', 'FullScreen')
|
||||
|
||||
// const pdfName = 'title'
|
||||
// function createImpl (canvas) {
|
||||
// console.log(leftHeight, a4HeightRef)
|
||||
// if (leftHeight > 0) {
|
||||
// index++
|
||||
|
||||
// let checkCount = 0
|
||||
// if (leftHeight > a4HeightRef) {
|
||||
// let i = position + a4HeightRef
|
||||
// for (i = position + a4HeightRef; i >= position; i--) {
|
||||
// let isWrite = true
|
||||
// for (let j = 0; j < canvas.width; j++) {
|
||||
// const c = canvas.getContext('2d').getImageData(j, i, 1, 1).data
|
||||
|
||||
// if (c[0] !== 0xff || c[1] !== 0xff || c[2] !== 0xff) {
|
||||
// isWrite = false
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if (isWrite) {
|
||||
// checkCount++
|
||||
// if (checkCount >= 10) {
|
||||
// break
|
||||
// }
|
||||
// } else {
|
||||
// checkCount = 0
|
||||
// }
|
||||
// }
|
||||
// height = Math.round(i - position) || Math.min(leftHeight, a4HeightRef)
|
||||
// if (height <= 0) {
|
||||
// height = a4HeightRef
|
||||
// }
|
||||
// } else {
|
||||
// height = leftHeight
|
||||
// }
|
||||
|
||||
// canvas1.width = canvas.width
|
||||
// canvas1.height = height
|
||||
|
||||
// console.log(index, 'height:', height, 'pos', position)
|
||||
|
||||
// const ctx = canvas1.getContext('2d')
|
||||
// ctx.drawImage(
|
||||
// canvas,
|
||||
// 0,
|
||||
// position,
|
||||
// canvas.width,
|
||||
// height,
|
||||
// 0,
|
||||
// 0,
|
||||
// canvas.width,
|
||||
// height
|
||||
// )
|
||||
|
||||
// const pageHeight = Math.round((a4Width / canvas.width) * height)
|
||||
// // pdf.setPageSize(null, pageHeight)
|
||||
// if (position !== 0) {
|
||||
// pdf.addPage()
|
||||
// }
|
||||
// pdf.addImage(
|
||||
// canvas1.toDataURL('image/jpeg', 1.0),
|
||||
// 'JPEG',
|
||||
// 20,
|
||||
// 20,
|
||||
// a4Width,
|
||||
// (a4Width / canvas1.width) * height
|
||||
// )
|
||||
// leftHeight -= height
|
||||
// position += height
|
||||
// if (leftHeight > 0) {
|
||||
// setTimeout(createImpl, 500, canvas)
|
||||
// } else {
|
||||
// console.log(pdf)
|
||||
// pdf.autoPrint()
|
||||
// window.open(pdf.output('bloburl'), '_blank')
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 当内容未超过pdf一页显示的范围,无需分页
|
||||
// if (leftHeight < a4HeightRef) {
|
||||
// pdf.addImage(
|
||||
// pageData,
|
||||
// 'JPEG',
|
||||
// 20,
|
||||
// 20,
|
||||
// a4Width,
|
||||
// (a4Width / canvas.width) * leftHeight
|
||||
// )
|
||||
// console.log(pdf)
|
||||
// pdf.autoPrint()
|
||||
// window.open(pdf.output('bloburl'), '_blank')
|
||||
// } else {
|
||||
// try {
|
||||
// pdf.deletePage(0)
|
||||
// setTimeout(createImpl, 500, canvas)
|
||||
// } catch (err) {
|
||||
// // console.log(err);
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// window.Vue.prototype.$html2Pdf = async function (id, isPrint) {
|
||||
// const ele = document.getElementById(id)
|
||||
// const pageWidth = 595.28 - 40 // A4纸的宽高 减去左右边距
|
||||
// const pageHeight = 841.89 - 40
|
||||
// // a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
|
||||
|
||||
// html2canvas(ele, {
|
||||
// // allowTaint: true,
|
||||
// // taintTest: false,
|
||||
// useCORS: true,
|
||||
// backgroundColor: '#FFF',
|
||||
// width: ele.offsetWidth - 15, // 因为多出的需要剪裁掉,
|
||||
// height: ele.offsetHeight
|
||||
// // async: true,
|
||||
// // scale: '1', // 放大倍数
|
||||
// // dpi: '192', // 精度
|
||||
// // scrollY: ele.top, // 关键代码
|
||||
// // height: ele.height + 50 // 加高度,避免截取不全
|
||||
// }).then(canvas => {
|
||||
// const contentWidth = canvas.width
|
||||
// const contentHeight = canvas.height
|
||||
// const pageData = canvas.toDataURL('image/jpeg/png', 1)
|
||||
// const PDF = new jsPDF('p', 'mm', 'a4') // , true
|
||||
// let pageCount = 1
|
||||
// // canvas图片的高
|
||||
// const imgHeight = pageWidth / contentWidth * contentHeight // canvas的宽与PDF的宽比列一样的时候,canvas的高缩放后的值
|
||||
|
||||
// let leftHeight = imgHeight
|
||||
// let position = 0
|
||||
// // 如果图片的高小于A4纸的高,不分页
|
||||
// if (leftHeight < pageHeight) {
|
||||
// PDF.addImage(pageData, 'JPEG', 20, 0, pageWidth, imgHeight)
|
||||
// } else {
|
||||
// // 分页
|
||||
// // let index = 0
|
||||
// while (leftHeight > 0) {
|
||||
// pageCount++
|
||||
|
||||
// PDF.addImage(pageData, 'JPEG', 20, position, pageWidth, imgHeight)
|
||||
// leftHeight = leftHeight - pageHeight
|
||||
// position -= pageHeight
|
||||
// if (leftHeight > 0) {
|
||||
// PDF.addPage()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// PDF.addFileToVFS('blobs', font)
|
||||
|
||||
// PDF.addFont('blobs', 'font_blobs', 'normal')
|
||||
// // 通过字体名使用字体
|
||||
// PDF.setFont('font_blobs')
|
||||
|
||||
// for (let i = 1; i < pageCount; i++) {
|
||||
// console.log(i)
|
||||
// PDF.setPage(i)
|
||||
// PDF.setFontSize(8)
|
||||
// PDF.text('当前第 ' + String(i) + '页 共 ' + String(pageCount) + '页', PDF.internal.pageSize.width / 2, pageHeight + 35, {
|
||||
// align: 'center'
|
||||
// })
|
||||
// }
|
||||
// PDF.autoPrint()
|
||||
// window.open(PDF.output('bloburl'), '_blank')
|
||||
// })
|
||||
// }
|
||||
const isSplit = (nodes, index, pageHeight) => {
|
||||
// 计算当前这块dom是否跨越了a4大小,以此分割
|
||||
if (nodes[index].offsetTop + nodes[index].offsetHeight < pageHeight && nodes[index + 1] && nodes[index + 1].offsetTop + nodes[index + 1].offsetHeight > pageHeight) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const formatPdf = (ele) => {
|
||||
const ST = document.documentElement.scrollTop || document.body.scrollTop
|
||||
const SL = document.documentElement.scrollLeft || document.body.scrollLeft
|
||||
document.documentElement.scrollTop = 0
|
||||
document.documentElement.scrollLeft = 0
|
||||
document.body.scrollTop = 0
|
||||
document.body.scrollLeft = 0
|
||||
// const ST = document.documentElement.scrollTop || document.body.scrollTop
|
||||
// const SL = document.documentElement.scrollLeft || document.body.scrollLeft
|
||||
document.documentElement.scrollTop = 0
|
||||
document.documentElement.scrollLeft = 0
|
||||
document.body.scrollTop = 0
|
||||
document.body.scrollLeft = 0
|
||||
// 获取滚动条的位置并赋值为0,因为是el-dialog弹框,并且内容较多出现了纵向的滚动条,截图出来的效果只能截取到视图窗口显示的部分,超出窗口部分则无法生成。所以先将滚动条置顶
|
||||
const A4_WIDTH = 592.28
|
||||
const A4_HEIGHT = 841.89
|
||||
|
||||
const imageWrapper = ele // 获取DOM
|
||||
const pageHeight = imageWrapper.scrollWidth / A4_WIDTH * A4_HEIGHT + 80
|
||||
// const lableListID = imageWrapper.querySelectorAll('.main-form-area')
|
||||
const lableListID = imageWrapper.querySelectorAll('.answer-item')
|
||||
// 进行分割操作,当dom内容已超出a4的高度,则将该dom前插入一个空dom,把他挤下去,分割
|
||||
for (let i = 0; i < lableListID.length; i++) {
|
||||
// console.log(lableListID[i])
|
||||
const multiple = Math.ceil((lableListID[i].offsetTop + lableListID[i].offsetHeight) / pageHeight)
|
||||
if (isSplit(lableListID, i, multiple * pageHeight)) {
|
||||
// console.log(lableListID[i])
|
||||
const divParent = lableListID[i].parentNode // 获取该div的父节点
|
||||
const newNode = document.createElement('div')
|
||||
newNode.className = 'emptyDiv'
|
||||
newNode.style.background = '#ffffff'
|
||||
const _H = multiple * pageHeight - (lableListID[i].offsetTop + lableListID[i].offsetHeight)
|
||||
// 留白
|
||||
newNode.style.height = _H + 'px'
|
||||
newNode.style.width = '100%'
|
||||
const next = lableListID[i].nextSibling // 获取div的下一个兄弟节点
|
||||
// 判断兄弟节点是否存在
|
||||
if (next) {
|
||||
// 存在则将新节点插入到div的下一个兄弟节点之前,即div之后
|
||||
divParent.insertBefore(newNode, next)
|
||||
} else {
|
||||
// 不存在则直接添加到最后,appendChild默认添加到divParent的最后
|
||||
divParent.appendChild(newNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
window.Vue.prototype.$html2Pdf = async function (id, exportInfo) {
|
||||
const ele = document.getElementById(id)
|
||||
formatPdf(ele)
|
||||
// a4纸的尺寸[595.28,841.89],html页面生成的canvas在pdf中图片的宽高
|
||||
|
||||
html2canvas(ele, {
|
||||
// allowTaint: true,
|
||||
// // taintTest: false,
|
||||
// x: ele.getBoundingClientRect().left + 13, // 绘制的dom元素相对于视口的位置
|
||||
// y: ele.getBoundingClientRect().top,
|
||||
useCORS: true,
|
||||
backgroundColor: '#FFF',
|
||||
width: ele.offsetWidth - 15, // 因为多出的需要剪裁掉,
|
||||
height: ele.offsetHeight,
|
||||
// async: true,
|
||||
scale: 3, // 放大倍数
|
||||
dpi: 350 // 精度
|
||||
// scrollY: ele.top, // 关键代码
|
||||
// height: ele.height + 50 // 加高度,避免截取不全
|
||||
}).then(canvas => {
|
||||
const pdf = new jsPDF('p', 'mm', 'a4') // A4纸,纵向
|
||||
const ctx = canvas.getContext('2d')
|
||||
const a4w = 190; const a4h = 277 // A4大小,210mm x 297mm,四边各保留10mm的边距,显示区域190x277
|
||||
const imgHeight = Math.floor(a4h * canvas.width / a4w) // 按A4显示比例换算一页图像的像素高度
|
||||
let renderedHeight = 0
|
||||
|
||||
while (renderedHeight < canvas.height) {
|
||||
const page = document.createElement('canvas')
|
||||
page.width = canvas.width
|
||||
page.height = Math.min(imgHeight, canvas.height - renderedHeight)// 可能内容不足一页
|
||||
// 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
|
||||
if (renderedHeight === 0) {
|
||||
page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, (Math.min(imgHeight, canvas.height - renderedHeight) + 20)), 0, 0)
|
||||
pdf.addImage(page.toDataURL('image/jpeg', 0.2), 'JPEG', 10, 0, a4w, Math.min(a4h, a4w * page.height / page.width)) // 添加图像到页面,保留10mm边距
|
||||
} else {
|
||||
page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0)
|
||||
pdf.addImage(page.toDataURL('image/jpeg', 0.2), 'JPEG', 10, 10, a4w, Math.min(a4h, a4w * page.height / page.width)) // 添加图像到页面,保留10mm边距
|
||||
}
|
||||
renderedHeight += imgHeight
|
||||
if (renderedHeight < canvas.height) { pdf.addPage() }// 如果后面还有内容,添加一个空页
|
||||
}
|
||||
const pageCount = pdf.internal.getNumberOfPages()
|
||||
pdf.addFileToVFS('blobs', font)
|
||||
|
||||
pdf.addFont('blobs', 'font_blobs', 'normal')
|
||||
// 通过字体名使用字体
|
||||
pdf.setFont('font_blobs')
|
||||
pdf.setFontSize(8)
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
pdf.setPage(i)
|
||||
pdf.text('第 ' + String(i) + ' 页 共 ' + String(pageCount) + ' 页', pdf.internal.pageSize.width / 2, 287 + 5, {
|
||||
align: 'center'
|
||||
})
|
||||
}
|
||||
console.log(exportInfo, 11111111111111)
|
||||
if (exportInfo) {
|
||||
pdf.save(exportInfo.title + '.pdf')
|
||||
} else {
|
||||
pdf.autoPrint()
|
||||
window.open(pdf.output('bloburl'), '_blank')
|
||||
const emptyDivs = ele.querySelectorAll('.emptyDiv')
|
||||
emptyDivs.forEach(item => {
|
||||
item.parentNode.removeChild(item)
|
||||
console.log(item)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.Vue.prototype.$elementFullscreen = async function (ele, callback, todo) {
|
||||
console.log(typeof ele)
|
||||
if (!(ele instanceof HTMLElement)) {
|
||||
ele = ele.$el
|
||||
}
|
||||
const div = document.createElement('div')
|
||||
await ele.requestFullscreen()
|
||||
window.Vue.prototype.$store.isFullscreen = true
|
||||
console.log(todo)
|
||||
todo && todo()
|
||||
|
||||
setTimeout(() => {
|
||||
const fun = () => {
|
||||
ele.removeEventListener('fullscreenchange', fun)
|
||||
window.Vue.prototype.$store.isFullscreen = false
|
||||
callback()
|
||||
}
|
||||
ele.addEventListener('fullscreenchange', fun)
|
||||
}, 100)
|
||||
console.log(ele)
|
||||
}
|
||||
51
front/src/utils/qiankun-init.js
Normal file
51
front/src/utils/qiankun-init.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { registerMicroApps, start } from 'qiankun'
|
||||
import store from '../store'
|
||||
// 子应用配置
|
||||
export const apps = [
|
||||
{
|
||||
name: '子应用1',
|
||||
entry: '//localhost:9099',
|
||||
id: 'app1',
|
||||
container: '#app1',
|
||||
activeRule: '/app1',
|
||||
// 测试传参,并将store传递至子应用进行消息通信!
|
||||
props: { test: 1111, store }
|
||||
},
|
||||
{
|
||||
name: '子应用2',
|
||||
entry: '//localhost:8888',
|
||||
id: 'app2',
|
||||
type: 'qiankun',
|
||||
container: '#app2',
|
||||
activeRule: '/app2',
|
||||
// 测试传参,并将store传递至子应用进行消息通信!
|
||||
props: { test: 1111, store }
|
||||
}
|
||||
]
|
||||
// 乾坤初始化方法
|
||||
export default function qianKunInit () {
|
||||
window.__POWERED_BY_QIANKUN_PARENT__ = true
|
||||
registerMicroApps(apps, {
|
||||
beforeLoad: [
|
||||
(app) => {
|
||||
console.log(app, '111111111111111111')
|
||||
}],
|
||||
destoryed: [
|
||||
(app) => {
|
||||
}
|
||||
]
|
||||
})
|
||||
const mainEle = document.getElementById('qiankun-main')
|
||||
apps.forEach(item => {
|
||||
const child = document.createElement('div')
|
||||
child.id = item.id
|
||||
mainEle.appendChild(child)
|
||||
item.container = child
|
||||
console.log(child)
|
||||
})
|
||||
start({
|
||||
prefetch: false, // 可选,是否开启预加载,默认为 true。 不开启
|
||||
sandbox: true, // 可选,是否开启沙箱,默认为 true。// 从而确保微应用的样式不会对全局造成影响。
|
||||
singular: true // 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true。
|
||||
})
|
||||
}
|
||||
7
front/src/utils/simhei-normal.js
Normal file
7
front/src/utils/simhei-normal.js
Normal file
File diff suppressed because one or more lines are too long
52
front/src/utils/storage.js
Normal file
52
front/src/utils/storage.js
Normal file
@@ -0,0 +1,52 @@
|
||||
export default (storage) => {
|
||||
if (!storage) return
|
||||
|
||||
const skey = (key) => '' + key
|
||||
|
||||
return {
|
||||
/**
|
||||
* 获取
|
||||
* @param {string} key key
|
||||
* @param {any} defaultValue defaultValue
|
||||
*/
|
||||
get (key, defaultValue) {
|
||||
try {
|
||||
return JSON.parse(storage.getItem(skey(key))) || defaultValue
|
||||
} catch (error) {
|
||||
return defaultValue
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 保存
|
||||
* @param {string} key key
|
||||
* @param {any} val 值
|
||||
*/
|
||||
save (key, val) {
|
||||
if (val === undefined || val === null) return this.remove(key)
|
||||
storage.setItem(skey(key), JSON.stringify(val))
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取 所有key
|
||||
*/
|
||||
keys () {
|
||||
return Object.keys(storage)
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param {string} key key
|
||||
*/
|
||||
remove (key) {
|
||||
storage.removeItem(skey(key))
|
||||
},
|
||||
|
||||
/**
|
||||
* 清空
|
||||
*/
|
||||
clear () {
|
||||
storage.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
39
front/src/views/404/index.vue
Normal file
39
front/src/views/404/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="_404">
|
||||
<img src="/svg/404.svg" style="height:56vh" />
|
||||
<p class="tip">页面没有找到</p>
|
||||
<router-link class="to_home" to="/">返回首页</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
._404 {
|
||||
padding-top: 16vh;
|
||||
text-align: center;
|
||||
|
||||
.tip {
|
||||
margin: 30px 0 50px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: $--color-primary;
|
||||
}
|
||||
|
||||
.to_home {
|
||||
width: 150px;
|
||||
line-height: 36px;
|
||||
display: inline-block;
|
||||
border: 1px solid $--color-primary;
|
||||
color: $--color-primary;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
border-radius: 30px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $--color-primary;
|
||||
color: #fff;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<el-cascader
|
||||
:options="question_classify_list"
|
||||
:props="{
|
||||
checkStrictly: true,
|
||||
value: 'id',
|
||||
label: 'name',
|
||||
children,
|
||||
emitPath: false,
|
||||
}"
|
||||
clearable
|
||||
v-model="classify"
|
||||
>
|
||||
</el-cascader>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { findAllExamClassifyApi } from '@/api/assessment-evaluation/exam'
|
||||
export default {
|
||||
data: (_) => ({
|
||||
classify: null,
|
||||
question_classify_list: []
|
||||
}),
|
||||
mounted () {
|
||||
if (this.mountedLoad) {
|
||||
this.findAllQuestionsClassify()
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.findAllQuestionsClassify()
|
||||
},
|
||||
props: {
|
||||
/*
|
||||
* 是否在mounted生命周期中加载
|
||||
*/
|
||||
mountedLoad: {
|
||||
default: false
|
||||
},
|
||||
/*
|
||||
* 绑定v-model传入类型
|
||||
*/
|
||||
value: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/*
|
||||
* 监听类型被修改
|
||||
*/
|
||||
value: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.classify = nv
|
||||
}
|
||||
},
|
||||
/*
|
||||
* 监听类型被选择
|
||||
*/
|
||||
classify: {
|
||||
immediate: false,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.$emit('input', nv)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
* 请求所有试题类型
|
||||
*/
|
||||
findAllQuestionsClassify () {
|
||||
findAllExamClassifyApi()
|
||||
.then(({ data }) => {
|
||||
let questionClassifyMap = {}
|
||||
questionClassifyMap = data.toTree()
|
||||
const list = questionClassifyMap.tree
|
||||
this.question_classify_list = list
|
||||
})
|
||||
.catch((err) => {
|
||||
console.info(err)
|
||||
})
|
||||
.finally((_) => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<DialogLayout :visible="show" @onCancel="onCancel" width="600px" title="自定义评语设置" @onConfirm="onConfirm">
|
||||
<div class="gy-form" style="--fix:130px;">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle">及格提示信息:</div>
|
||||
<el-input placeholder="请输入评语" v-model="form.successTips"></el-input>
|
||||
</div>
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle">不及格提示信息:</div>
|
||||
<el-input placeholder="请输入评语" v-model="form.failedTips"></el-input>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle">等待人工评卷信息:</div>
|
||||
<el-input placeholder="请输入评语" v-model="form.waitingTips"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</DialogLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import DialogLayout from '@/components/layout/DialogLayout.vue'
|
||||
export default {
|
||||
components: { DialogLayout },
|
||||
data: _ => ({
|
||||
show: false,
|
||||
initForm: null,
|
||||
form: {
|
||||
successTips: '',
|
||||
failedTips: '',
|
||||
waitingTips: ''
|
||||
}
|
||||
}),
|
||||
props: {
|
||||
value: {
|
||||
default: false
|
||||
},
|
||||
successTips: {
|
||||
},
|
||||
failedTips: {},
|
||||
waitingTips: {}
|
||||
},
|
||||
mounted () {
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 确认按钮按下回调
|
||||
*/
|
||||
onConfirm () {
|
||||
this.$emit('update:successTips', this.form.successTips)
|
||||
this.$emit('update:failedTips', this.form.failedTips)
|
||||
this.$emit('update:waitingTips', this.form.waitingTips)
|
||||
this.show = false
|
||||
},
|
||||
/**
|
||||
* 取消按钮按下回调
|
||||
*/
|
||||
onCancel () {
|
||||
this.show = false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* 编辑考试时传入数据
|
||||
*/
|
||||
value: {
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.show = nv
|
||||
if (nv) {
|
||||
this.form.successTips = this.successTips
|
||||
this.form.failedTips = this.failedTips
|
||||
this.form.waitingTips = this.waitingTips
|
||||
}
|
||||
}
|
||||
},
|
||||
show: {
|
||||
handler (nv) { this.$emit('input', nv) }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<DialogLayout :visible="true" title="试卷模式预览" :shadowBar="false" :actionBarOption="{noCencel:true,noConfirm:true}" @onCancel="$emit('onCancel')">
|
||||
<img :src="`/imgs/${['preview-full-volume','preview-question-by-question'][mode]}.png`">
|
||||
</DialogLayout>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import DialogLayout from '@/components/layout/DialogLayout.vue'
|
||||
export default {
|
||||
components: { DialogLayout },
|
||||
data: () => ({
|
||||
}),
|
||||
props: {
|
||||
/**
|
||||
* 传入类型,0 整卷模式, 1 逐题模式
|
||||
*/
|
||||
mode: {
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="gy-form inline" style="--fix:70px;">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle right">班级选择:</div>
|
||||
<el-cascader :options="org_list"
|
||||
:props="{ checkStrictly: true, value: 'id', label: 'name', children, emitPath: false }" placeholder="请选择"
|
||||
clearable v-model="current_org" @change="getStudents(current_org)" :filter-method="filterMethod">
|
||||
</el-cascader>
|
||||
</div>
|
||||
<div class="gy-form-item mb">
|
||||
<QueryInput v-model="search" @query="findStudentList"></QueryInput>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-transfer gy-form">
|
||||
<div class="left-list list gy-check">
|
||||
<div class="header">
|
||||
<el-checkbox v-model="left_check_all" @change="checkAll($event,'left')"></el-checkbox>
|
||||
<div>
|
||||
待选学员
|
||||
</div>
|
||||
</div>
|
||||
<el-checkbox-group v-model="left_checked" class="check-list">
|
||||
<el-checkbox v-for="(item) in left_list" :key="item.id" :label="item.id">{{item.name}}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<div class="feature-group">
|
||||
<el-button type="primary" @click="toRight">
|
||||
添加学员<i class="el-icon-arrow-right el-icon--right"></i>
|
||||
</el-button>
|
||||
<el-button type="primary" @click="toLeft">
|
||||
<i class="el-icon-arrow-left "></i>删除学员
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="right-list list gy-check">
|
||||
<div class="header ">
|
||||
<el-checkbox v-model="right_check_all" @change="checkAll($event,'right')"></el-checkbox>
|
||||
<div>
|
||||
已选学员
|
||||
</div>
|
||||
</div>
|
||||
<el-checkbox-group v-model="right_checked" class="check-list">
|
||||
<el-checkbox v-for="(item) in right_list" :key="item.id" :label="item.id">{{item.name}}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 学员选择器
|
||||
*/
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import {
|
||||
// getMyOrg,
|
||||
getAllOrg, getStudentByOrgId
|
||||
} from '@/api/assessment-evaluation/exam'
|
||||
import { QueryInput } from '@/components/widget'
|
||||
export default {
|
||||
components: { QueryInput },
|
||||
data: () => ({
|
||||
/**
|
||||
* 机构数据列表
|
||||
*/
|
||||
org_list: [],
|
||||
/**
|
||||
* 右侧列表中的数据
|
||||
*/
|
||||
right_list: [],
|
||||
/**
|
||||
* 左侧剩余未选中的数据
|
||||
*/
|
||||
left_list: [],
|
||||
/**
|
||||
* 左侧被选中的数据
|
||||
*/
|
||||
left_checked: [],
|
||||
/**
|
||||
* 右侧被选中的数据
|
||||
*/
|
||||
right_checked: [],
|
||||
/**
|
||||
* 左侧列表是否全选
|
||||
*/
|
||||
left_check_all: false,
|
||||
/**
|
||||
* 右侧列表是否全选
|
||||
*/
|
||||
right_check_all: false,
|
||||
/**
|
||||
* 当前筛选的机构
|
||||
*/
|
||||
current_org: null,
|
||||
/**
|
||||
* 请求到的所有学员数据
|
||||
*/
|
||||
all_student: [],
|
||||
/**
|
||||
* 搜索框内容绑定
|
||||
*/
|
||||
search: '',
|
||||
/**
|
||||
* 已选择的学员信息
|
||||
*/
|
||||
selected_students: []
|
||||
}),
|
||||
props: {
|
||||
value: {
|
||||
default: _ => []
|
||||
},
|
||||
transOptions: {
|
||||
default: _ => []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// value: {
|
||||
// deep: true,
|
||||
// immediate: true,
|
||||
// handler (nv) {
|
||||
// this.right_list = nv
|
||||
// }
|
||||
// },
|
||||
/**
|
||||
* 右侧列表被修改,抛出所有学员id
|
||||
*/
|
||||
right_list: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.$emit('input', nv?.map(item => item.id))
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 初始化时从父组件传入的数据放入右侧列表中
|
||||
*/
|
||||
transOptions: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.right_list = nv
|
||||
}
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.initAllList()
|
||||
this.initAllChecked()
|
||||
this.current_org = null
|
||||
this.getAllOrgs()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 全选操作
|
||||
*/
|
||||
checkAll (e, type) {
|
||||
if (type === 'left') {
|
||||
this.left_checked = e ? this.left_list.map(item => item.id) : []
|
||||
} else if (type === 'right') {
|
||||
this.right_checked = e ? this.right_list.map(item => item.id) : []
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 移动到右侧操作回调
|
||||
*/
|
||||
toRight () {
|
||||
for (let index = this.left_list.length - 1; index >= 0; index--) {
|
||||
const element = this.left_list[index]
|
||||
const leftItemIsChecked = this.left_checked.findIndex(ioi => ioi === element.id)
|
||||
if (leftItemIsChecked !== -1) {
|
||||
this.right_list.unshift(element)
|
||||
this.left_list.splice(index, 1)
|
||||
}
|
||||
}
|
||||
this.initAllChecked()
|
||||
// this.left_checked
|
||||
},
|
||||
/**
|
||||
* 移动到左侧列表按钮回调
|
||||
*/
|
||||
toLeft () {
|
||||
const length = this.right_list.length - 1
|
||||
for (let index = length; index >= 0; index--) {
|
||||
const element = this.right_list[index]
|
||||
const rightItemIsChecked = this.right_checked.findIndex(ioi => ioi === element.id)
|
||||
|
||||
if (rightItemIsChecked !== -1) {
|
||||
if (+this.current_org === +element.orgId) {
|
||||
this.left_list.unshift(element)
|
||||
}
|
||||
this.right_list.splice(index, 1)
|
||||
}
|
||||
}
|
||||
this.initAllChecked()
|
||||
},
|
||||
/**
|
||||
* 请求所有机构
|
||||
*/
|
||||
async getAllOrgs () {
|
||||
const { data } = await getAllOrg()
|
||||
const { tree } = data.toTree()
|
||||
this.org_list = Object.freeze(tree)
|
||||
},
|
||||
/**
|
||||
* 初始化左右列表
|
||||
*/
|
||||
initAllList () {
|
||||
this.left_list = []
|
||||
this.right_list = []
|
||||
this.search = ''
|
||||
},
|
||||
/**
|
||||
* 清空所有选中
|
||||
*/
|
||||
initAllChecked () {
|
||||
this.left_checked = []
|
||||
this.right_checked = []
|
||||
this.left_check_all = false
|
||||
this.right_check_all = false
|
||||
},
|
||||
/**
|
||||
* 根据机构获取其中的学员
|
||||
*/
|
||||
async getStudents (orgId) {
|
||||
if (!orgId) {
|
||||
this.left_list = []
|
||||
return
|
||||
}
|
||||
const { data } = await getStudentByOrgId(orgId)
|
||||
data.forEach(item => {
|
||||
item.orgId = orgId
|
||||
})
|
||||
this.initAllChecked()
|
||||
// 过滤右表已存在的老学员
|
||||
this.right_list.forEach(item => {
|
||||
const hasThisStudent = data.findIndex(oi => oi.id === item.id)
|
||||
if (hasThisStudent !== -1) {
|
||||
data.splice(hasThisStudent, 1)
|
||||
}
|
||||
})
|
||||
this.left_list = data
|
||||
},
|
||||
/**
|
||||
* 获取所有学员
|
||||
*/
|
||||
async findStudentList () {
|
||||
if (!this.current_org) return this.$message.error('请先选择一个班级')
|
||||
this.left_list = this.left_list.sort((a, b) => {
|
||||
if (a.name.indexOf(this.search) !== -1) {
|
||||
return -1
|
||||
} else if (b.name.indexOf(this.search) !== -1) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gy-transfer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 370px;
|
||||
.check-list{
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
flex:1;
|
||||
overflow-y: scroll;
|
||||
flex-direction: column;
|
||||
.el-checkbox{
|
||||
padding: 5px 0 ;
|
||||
}
|
||||
}
|
||||
.list {
|
||||
width: 150px;
|
||||
// border: 1px solid $--color-primary;
|
||||
box-shadow: 0 0 0 1px $--color-primary , 1px 1px 5px 1px rgba(66,66,66,0.3);
|
||||
height: 200px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.header {
|
||||
height: 30px;
|
||||
background: $--color-primary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
&>div {
|
||||
color:#fff;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feature-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
justify-content: center;
|
||||
width: 120px;
|
||||
padding: 0 10px;
|
||||
::v-deep {
|
||||
.el-button+.el-button{
|
||||
margin-top: 30px;
|
||||
}
|
||||
.el-button {
|
||||
margin: 0;
|
||||
height: 30px;
|
||||
padding: 0;
|
||||
border-radius: 15px;
|
||||
&+&{
|
||||
}
|
||||
.el-icon-arrow-left,
|
||||
.el-icon-arrow-right {
|
||||
padding: 2px;
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
color: $--color-primary;
|
||||
display: inline-block;
|
||||
}
|
||||
.el-icon-arrow-left{
|
||||
margin-right: 3px;
|
||||
}
|
||||
.el-icon-arrow-right{
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<DialogLayout :visible="show" @onCancel="show=false" width="350px" title="选择老师" @onConfirm="$emit('onChange',current_teacher)">
|
||||
<div class="gy-form" style="--fix:70px;">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle">老师:</div>
|
||||
<el-select
|
||||
v-model="current_teacher"
|
||||
placeholder="请选择" value-key="id">
|
||||
<el-option
|
||||
v-for="v in form_manage_teacher"
|
||||
:key="v.id"
|
||||
:label="v.name"
|
||||
:value="v"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</DialogLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import DialogLayout from '@/components/layout/DialogLayout.vue'
|
||||
import { getAllTeacher } from '@/api/assessment-evaluation/exam'
|
||||
export default {
|
||||
components: { DialogLayout },
|
||||
data: _ => ({
|
||||
show: false,
|
||||
form_manage_teacher: [],
|
||||
current_teacher: {}
|
||||
}),
|
||||
mounted () {
|
||||
this.getAllTeacher()
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
default: false
|
||||
},
|
||||
teacher: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler (nv) { this.show = nv }
|
||||
},
|
||||
show: {
|
||||
handler (nv) { this.$emit('input', nv) }
|
||||
},
|
||||
teacher: {
|
||||
handler (nv) {
|
||||
if (nv) {
|
||||
this.current_teacher = nv
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 获取系统中所有教员
|
||||
*/
|
||||
getAllTeacher () {
|
||||
getAllTeacher().then(res => {
|
||||
this.form_manage_teacher = res.data
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,560 @@
|
||||
<template>
|
||||
<div v-loading="page_is_loading">
|
||||
<div class="gy-area-title">考试属性</div>
|
||||
<div class="gy-form inline" style="--fix: 77px">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle require">考试名称:</div>
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
placeholder="请输入名称"
|
||||
style="width: 350px"
|
||||
></el-input>
|
||||
</div>
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle require">考试分类:</div>
|
||||
<ExamClassifySelector v-model="form.classifyId"></ExamClassifySelector>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form mb" style="--fix: 77px">
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label">考试描述:</div>
|
||||
<el-input
|
||||
type="textarea"
|
||||
placeholder="请输入考试描述信息"
|
||||
v-model="form.examDesc"
|
||||
style="width: 100%"
|
||||
></el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-area-title">考试控制</div>
|
||||
<div class="gy-form inline" style="--fix: 77px">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle require">考试时间:</div>
|
||||
<!-- <el-date-picker v-model="form.examStartTime" type="datetime" placeholder="请选择开始时间">
|
||||
</el-date-picker>
|
||||
<div class="gy-label middle" style="min-width: 30px;">-</div>
|
||||
<el-date-picker v-model="form.examEndTime" type="datetime" placeholder="请选择结束时间">
|
||||
</el-date-picker> -->
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
value-format="yyyy-MM-ddTHH:mm:ss.000Z"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
>
|
||||
</el-date-picker>
|
||||
</div>
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle require">考试时长:</div>
|
||||
<div>
|
||||
<el-input
|
||||
v-model="form.examDuration"
|
||||
placeholder="60"
|
||||
:min="1"
|
||||
type="number"
|
||||
style="width: 90px"
|
||||
>
|
||||
</el-input>
|
||||
分钟
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle require">考试机会:</div>
|
||||
<div>
|
||||
<el-input
|
||||
v-model="form.examTimes"
|
||||
placeholder="3"
|
||||
:min="1"
|
||||
:step="1"
|
||||
type="number"
|
||||
style="width: 90px"
|
||||
>
|
||||
</el-input>
|
||||
次
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-area-title">试卷模式</div>
|
||||
<el-radio-group v-model="form.examMode">
|
||||
<div class="gy-form inline" style="--fix: 77px; padding-left: 10px">
|
||||
<div class="gy-form-item">
|
||||
<div>
|
||||
<el-radio :label="0"
|
||||
>整卷模式
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-fbks-yulan"
|
||||
@click="
|
||||
mode_preview_dialog_mode = 0;
|
||||
mode_preview_dialog_show = true;
|
||||
"
|
||||
>预览</el-button
|
||||
>
|
||||
</el-radio>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div>
|
||||
<el-radio :label="1"
|
||||
>逐题模式
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-fbks-yulan"
|
||||
@click="
|
||||
mode_preview_dialog_mode = 1;
|
||||
mode_preview_dialog_show = true;
|
||||
"
|
||||
>预览</el-button
|
||||
>
|
||||
</el-radio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
<ModePriviewDialog
|
||||
v-if="mode_preview_dialog_show"
|
||||
:mode="mode_preview_dialog_mode"
|
||||
@onCancel="mode_preview_dialog_show = false"
|
||||
></ModePriviewDialog>
|
||||
<div class="gy-area-title">选择试卷</div>
|
||||
<div class="gy-form inline" style="--fix: 77px">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle require">试卷:</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
class="addbtn-restyle"
|
||||
@click="select_paper_dialog_show = true"
|
||||
v-if="current_selected_paper == null"
|
||||
>选取试卷</el-button
|
||||
>
|
||||
<el-button
|
||||
type="text"
|
||||
v-else
|
||||
@click="select_paper_dialog_show = true"
|
||||
>{{ current_selected_paper?.title }}</el-button
|
||||
>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-fbks-yulan"
|
||||
@click="previewPaper"
|
||||
v-show="current_selected_paper != null"
|
||||
>预览</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle">总分:</div>
|
||||
<div style="height: 32px; line-height: 32px">
|
||||
{{ current_selected_paper ? current_selected_paper.totalScore : 0 }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle">及格分:</div>
|
||||
<div style="margin-right: 20px">
|
||||
<el-input
|
||||
v-model="form.passPercent"
|
||||
placeholder="60"
|
||||
min="0"
|
||||
max="100"
|
||||
step="5"
|
||||
type="number"
|
||||
style="width: 90px"
|
||||
></el-input>
|
||||
<span> % </span>
|
||||
</div>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-ksap-bianji2"
|
||||
@click="edit_tips_dialog_show = true"
|
||||
>自定义评语设置</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-area-title">阅卷方式</div>
|
||||
<el-radio-group v-model="form.gradePaperMode">
|
||||
<div
|
||||
class="gy-form inline middle"
|
||||
style="--fix: 77px; padding-left: 10px; height: 32px"
|
||||
>
|
||||
<div class="gy-form-item middle" style="margin-right: 65px">
|
||||
<el-radio :label="0">机器阅卷 </el-radio>
|
||||
</div>
|
||||
<div class="gy-form-item middle">
|
||||
<el-radio :label="1">人工阅卷</el-radio>
|
||||
</div>
|
||||
<div class="gy-form-item middle" v-show="form.gradePaperMode === 1">
|
||||
<div style="font-size: 14px; margin-right: 10px">
|
||||
{{ examiner_info?.name }}
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
class="addbtn-restyle"
|
||||
plain
|
||||
icon="el-icon-plus"
|
||||
@click="teacher_dialog_show = true"
|
||||
>选取阅卷人</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
<div class="gy-area-title">选取学员</div>
|
||||
<StudentsSelector
|
||||
v-model="current_selected_students"
|
||||
:transOptions="students_selector_options"
|
||||
></StudentsSelector>
|
||||
|
||||
<div style="width: 100%; margin-top: 20px; text-align: center">
|
||||
<el-button type="primary" round @click="onSave">保存</el-button>
|
||||
<el-button type="primary" round @click="onCancel" plain>取消</el-button>
|
||||
</div>
|
||||
<DialogLayout
|
||||
:visible="select_paper_dialog_show"
|
||||
title="选取试卷"
|
||||
:actionBarOption="{ noConfirm: true, noCencel: true }"
|
||||
@onCancel="select_paper_dialog_show = false"
|
||||
>
|
||||
<ExamPaperSelector
|
||||
:mountedLoad="true"
|
||||
@onSelect="paperSelected"
|
||||
></ExamPaperSelector>
|
||||
</DialogLayout>
|
||||
<ExamTipsForm
|
||||
v-model="edit_tips_dialog_show"
|
||||
:successTips.sync="form.successTips"
|
||||
:failedTips.sync="form.failedTips"
|
||||
:waitingTips.sync="form.waitingTips"
|
||||
></ExamTipsForm>
|
||||
<TeacherSelector
|
||||
:teacher.sync="examiner_info"
|
||||
v-model="teacher_dialog_show"
|
||||
@onChange="examninerChanged"
|
||||
></TeacherSelector>
|
||||
<QuestionListPreview
|
||||
:questionList="preview_current_questions_list"
|
||||
v-if="preview_paper_dialog_show"
|
||||
@onCancel="preview_paper_dialog_show = false"
|
||||
></QuestionListPreview>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ExamTipsForm from './components/ExamTipsForm.vue'
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import ExamClassifySelector from './components/ExamClassifySelector.vue'
|
||||
import ExamPaperSelector from '@/views/assessment-evaluation/examination-paper-manage/components/ExamPaperSelector.vue'
|
||||
import { DialogLayout } from '@/components/layout'
|
||||
import {
|
||||
createExamApi,
|
||||
getExamDetailsByIdApi,
|
||||
patchExamDetailsByIdApi,
|
||||
getQuestionsByPaperApi
|
||||
} from '@/api/assessment-evaluation/exam'
|
||||
|
||||
import TeacherSelector from './components/TeacherSelector.vue'
|
||||
import StudentsSelector from './components/StudentsSelector.vue'
|
||||
import ModePriviewDialog from './components/ModePriviewDialog.vue'
|
||||
import QuestionListPreview from '../../examination-paper-manage/add-modify-paper/components/QuestionListPreview.vue'
|
||||
/**
|
||||
* 初始化页面时用到的模板数据
|
||||
*/
|
||||
const formInitData = {
|
||||
title: '',
|
||||
classifyId: null,
|
||||
examDesc: '',
|
||||
examStartTime: null,
|
||||
examEndTime: null,
|
||||
examDuration: 60,
|
||||
examTimes: 1,
|
||||
successTips: '恭喜您,成功通过本次考试!',
|
||||
failedTips: '很遗憾,您没有通过本次考试!',
|
||||
waitingTips: '请等待老师人工评分!',
|
||||
// 试卷模式
|
||||
examMode: 0,
|
||||
// 阅卷方式
|
||||
gradePaperMode: 0,
|
||||
// 试卷ID
|
||||
examPaperId: null,
|
||||
// 阅卷人
|
||||
examinerId: '',
|
||||
totalScore: 0,
|
||||
passPercent: 60
|
||||
}
|
||||
export default {
|
||||
components: {
|
||||
ExamClassifySelector,
|
||||
ExamPaperSelector,
|
||||
StudentsSelector,
|
||||
DialogLayout,
|
||||
ExamTipsForm,
|
||||
TeacherSelector,
|
||||
ModePriviewDialog,
|
||||
QuestionListPreview
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
/**
|
||||
* 所选日期时间范围
|
||||
*/
|
||||
dateRange: [],
|
||||
form: { ...formInitData },
|
||||
page_is_loading: false,
|
||||
examiner_info: null,
|
||||
select_paper_dialog_show: false,
|
||||
edit_tips_dialog_show: false,
|
||||
current_selected_paper: null,
|
||||
teacher_dialog_show: false,
|
||||
current_selected_students: [],
|
||||
students_selector_options: null,
|
||||
mode_preview_dialog_show: false,
|
||||
mode_preview_dialog_mode: 0,
|
||||
preview_paper_dialog_show: false,
|
||||
preview_current_questions_list: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
examiner_info: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
nv && (this.form.examinerId = nv.id)
|
||||
}
|
||||
},
|
||||
dateRange: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
if (nv.length) {
|
||||
this.form.examStartTime = nv[0]
|
||||
this.form.examEndTime = nv[1]
|
||||
}
|
||||
}
|
||||
},
|
||||
current_selected_paper: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
if (nv) {
|
||||
this.form.examPaperId = nv.id
|
||||
this.form.totalScore = nv.totalScore
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isModify () {
|
||||
return this.$route.params.examId != null
|
||||
},
|
||||
isCopy () {
|
||||
return this.$route.params.type === 'copy'
|
||||
},
|
||||
isAgain () {
|
||||
return this.$route.params.type === 'again'
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.initFrom()
|
||||
this.examiner_info = this.$store.user
|
||||
|
||||
if (Object.keys(this.$route.query).includes('paperId')) {
|
||||
this.current_selected_paper = {
|
||||
id: this.$route.query.paperId,
|
||||
title: this.$route.query.title,
|
||||
totalScore: this.$route.query.totalScore
|
||||
}
|
||||
}
|
||||
if (this.isModify) {
|
||||
this.getExamDetails()
|
||||
} else {
|
||||
this.form.examinerId = this.$store.user.id
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 预览试卷
|
||||
*/
|
||||
previewPaper () {
|
||||
this.page_is_loading = true
|
||||
getQuestionsByPaperApi(this.current_selected_paper.id)
|
||||
.then((res) => {
|
||||
this.preview_current_questions_list = res.data
|
||||
this.preview_paper_dialog_show = true
|
||||
})
|
||||
.finally((_) => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 判卷人被修改回调
|
||||
*/
|
||||
examninerChanged (examniner) {
|
||||
if (!Object.keys(examniner).includes('id')) {
|
||||
this.examiner_info = this.$store.user
|
||||
} else {
|
||||
this.examiner_info = examniner
|
||||
}
|
||||
this.teacher_dialog_show = false
|
||||
},
|
||||
/**
|
||||
* 获取考试详细数据
|
||||
*/
|
||||
getExamDetails () {
|
||||
this.page_is_loading = true
|
||||
getExamDetailsByIdApi(this.$route.params.examId)
|
||||
.then((res) => {
|
||||
const tempForm = {}
|
||||
const resExamInfo = res.data.exam.examInfo
|
||||
const resExamInfoKeys = Object.keys(resExamInfo)
|
||||
const initKeys = Object.keys(formInitData)
|
||||
resExamInfoKeys.forEach((item) => {
|
||||
if (initKeys.includes(item)) {
|
||||
tempForm[item] = resExamInfo[item]
|
||||
}
|
||||
})
|
||||
tempForm.id = resExamInfo.id
|
||||
this.form = tempForm
|
||||
this.dateRange = [
|
||||
new Date(tempForm.examStartTime),
|
||||
new Date(tempForm.examEndTime)
|
||||
]
|
||||
|
||||
this.form.examStartTime = new Date(this.form.examStartTime)
|
||||
this.form.examEndTime = new Date(this.form.examEndTime)
|
||||
if (this.isAgain) {
|
||||
if (this.form.title.indexOf('【复制再考】') === -1) {
|
||||
this.form.title = this.form.title + '【复制再考】'
|
||||
}
|
||||
this.form.examStartTime = null
|
||||
this.form.examEndTime = null
|
||||
}
|
||||
if (this.isAgain || this.isCopy) {
|
||||
delete this.form.id
|
||||
}
|
||||
this.current_selected_paper = res.data.exam.paperInExam
|
||||
this.current_selected_students = res.data.students.map((_) => _.id)
|
||||
this.students_selector_options = res.data.students
|
||||
this.examiner_info = res.data.exam.examInfo.userinfo
|
||||
})
|
||||
.catch((err) => {
|
||||
console.info(err)
|
||||
})
|
||||
.finally((_) => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 考试结束提示时的提示信息被修改回调
|
||||
*/
|
||||
tipsChanged (tips) {
|
||||
this.form.successTips = tips.successTips
|
||||
this.form.failedTips = tips.failedTips
|
||||
this.form.waitingTips = tips.waitingTips
|
||||
// 隐藏弹窗
|
||||
this.edit_tips_dialog_show = false
|
||||
},
|
||||
/**
|
||||
* 试卷被选取
|
||||
*/
|
||||
paperSelected (row) {
|
||||
this.current_selected_paper = row
|
||||
|
||||
this.select_paper_dialog_show = false
|
||||
},
|
||||
/**
|
||||
* 初始化form表单
|
||||
*/
|
||||
initFrom () {
|
||||
this.form = { ...formInitData }
|
||||
this.current_selected_students = []
|
||||
this.students_selector_options = []
|
||||
this.current_selected_paper = null
|
||||
this.dateRange = []
|
||||
},
|
||||
onCancel () {
|
||||
this.$router.replace({ path: '/assessment-evaluation/exam-arrangement' })
|
||||
this.initFrom()
|
||||
},
|
||||
// 保存
|
||||
onSave () {
|
||||
const form = this.form
|
||||
console.log(form)
|
||||
this.$message.closeAll()
|
||||
const date = new Date()
|
||||
if (!form.title) return this.$message.error('请输入考试名称')
|
||||
if (!form.classifyId) return this.$message.error('请选择考试分类')
|
||||
if (!form.examStartTime) return this.$message.error('请选择考试开始时间')
|
||||
if (!form.examEndTime) return this.$message.error('请选择考试结束时间')
|
||||
if (date.getTime(form.examEndTime) < date.getTime(form.examStartTime)) {
|
||||
return this.$message.error('考试结束时间应晚于考试开始时间')
|
||||
}
|
||||
if (!form.examDuration) return this.$message.error('请填写考试时长')
|
||||
if (!form.examTimes) return this.$message.error('请填写考试机会')
|
||||
if (form.examMode == null) return this.$message.error('请选择试卷模式')
|
||||
if (!form.examPaperId) return this.$message.error('请选择试卷')
|
||||
if (!form.passPercent) return this.$message.error('请填写及格分占比')
|
||||
if (form.gradePaperMode == null) {
|
||||
return this.$message.error('请选择阅卷方式')
|
||||
}
|
||||
if (form.gradePaperMode === 1 && !form.examinerId) {
|
||||
return this.$message.error('人工阅卷需要确认阅卷人')
|
||||
}
|
||||
if (form.passPercent < 0 || form.passPercent > 100) {
|
||||
return this.$message.error('及格分占比应在0-100之间')
|
||||
}
|
||||
if (form.examDuration < 1) {
|
||||
return this.$message.error('请设置正确的考试时长')
|
||||
}
|
||||
if (form.examTimes < 1) return this.$message.error('考试次数最少1次')
|
||||
if (this.current_selected_students.length === 0) {
|
||||
return this.$message.error('请选择考试学员')
|
||||
}
|
||||
if (this.isModify && !this.isAgain && !this.isCopy) {
|
||||
this.onModify()
|
||||
return
|
||||
}
|
||||
this.page_is_loading = true
|
||||
createExamApi({
|
||||
exam: this.form,
|
||||
students: this.current_selected_students
|
||||
})
|
||||
.then((res) => {
|
||||
this.$message.success('发布成功')
|
||||
// TODO: 暂时保存后跳转到列表页,之后需要留在当前页,可发布考试(已完成)
|
||||
this.$router.replace({
|
||||
path: '/assessment-evaluation/exam-arrangement'
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.info(err)
|
||||
})
|
||||
.finally(() => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
onModify () {
|
||||
this.page_is_loading = true
|
||||
patchExamDetailsByIdApi({
|
||||
id: this.form.id,
|
||||
exam: this.form,
|
||||
students: this.current_selected_students
|
||||
})
|
||||
.then((res) => {
|
||||
this.$message.success('编辑成功')
|
||||
this.$router.replace({
|
||||
path: '/assessment-evaluation/exam-arrangement'
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.info(err)
|
||||
})
|
||||
.finally(() => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
311
front/src/views/assessment-evaluation/exam-arrangement/index.vue
Normal file
311
front/src/views/assessment-evaluation/exam-arrangement/index.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
|
||||
<template>
|
||||
<div class="v-page classify_page">
|
||||
<SearchTreeMenu ref="treeMenuRef" title="考试分类列表" :tree-data="paper_classify_list" @current-change="change" @onEdit="toEditClassify"
|
||||
@onCreate="toCreateClassify" @onDelete="toDeleteclassify" v-loading="classify_loading" />
|
||||
<div class="v-ctx" v-loading="paper_list_loading">
|
||||
<FormLayout
|
||||
ref="formLayoutRef"
|
||||
:items="formItems"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="70px"
|
||||
label-position="right"
|
||||
:inline="true"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<!-- <el-input placeholder="请输入查询内容" v-model="form.title" class="input-with-select" clearable>
|
||||
<el-button slot="append" @click="pagingFindList">查询</el-button>
|
||||
</el-input> -->
|
||||
<QueryInput v-model="form.title" @query="pagingFindList"></QueryInput>
|
||||
</template>
|
||||
<template v-slot:btns>
|
||||
<el-button type="danger" @click="deletePapers" round >删除选中</el-button>
|
||||
<el-button type="primary" @click="addPaperHandler" round >发布考试</el-button>
|
||||
<!-- icon="el-icon-s-promotion" -->
|
||||
</template>
|
||||
</FormLayout>
|
||||
<!-- class="gy-el-table" -->
|
||||
<TableLayout :column="column" :data="table_data" :pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })" @size-change="(e) => pagingChange({ pageSize: e })"
|
||||
selection @selection-change="handleSelectionChange">
|
||||
<template v-slot:examStartTime="props">
|
||||
<div>
|
||||
<p>开始时间:{{props.row.examStartTime}}</p>
|
||||
<p>结束时间:{{props.row.examEndTime}}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:passPercent="props">
|
||||
<p>{{Math.round(props.row.passPercent * props.row.totalScore /100)}}</p>
|
||||
</template>
|
||||
<template v-slot:status="props">
|
||||
<div :style="{'background-color':['#208ac6','#01c883','#999999'][props.row.status]}" class="exam-status">
|
||||
{{['待开考','考试中','已结束'][props.row.status]}}
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:action="props">
|
||||
<!-- <el-button type="text" icon="el-icon-edit">{{ props.row.type }}</el-button> -->
|
||||
<el-button type="text" icon="i-j-ksap-bianji2" :disabled="props.row.status==1" @click="editExam(props.row)" v-if="props.row.status==0">编辑</el-button>
|
||||
<el-button type="text" icon="i-j-ksap-fuzhi" :disabled="props.row.status==1" @click="editExam(props.row,'copy')" v-if="props.row.status==0">复制</el-button>
|
||||
<el-button type="text" icon="i-j-ksap-fuzhi" :disabled="props.row.status==1" @click="editExam(props.row,'again')" v-if="props.row.status==2">复制再考</el-button>
|
||||
<!-- <el-button type="text" icon="el-icon-download" >下载</el-button> -->
|
||||
<el-button type="text" icon="i-j-ksap-shanchu" @click="deletePapers($event,props.row)" style="color:#f04343 !important">删除</el-button>
|
||||
</template>
|
||||
</TableLayout>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { SearchTreeMenu, TableLayout, FormLayout } from '@/components/layout'
|
||||
|
||||
import { findAllExamClassifyApi, createExamClassifyApi, deleteExamClassifyApi, editExamClassifyApi, pagingExamListApi, batchDeleteExamApi } from '@/api/assessment-evaluation/exam'
|
||||
import QueryInput from '@/components/widget/QueryInput.vue'
|
||||
export default {
|
||||
components: {
|
||||
SearchTreeMenu, TableLayout, FormLayout, QueryInput
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
paper_classify_list: [],
|
||||
table_data: [],
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
current_classify_id: null,
|
||||
// 试题分类树是否加载中
|
||||
classify_loading: false,
|
||||
paper_list_loading: false,
|
||||
form: { title: '' },
|
||||
// 表格当前选中项
|
||||
tableSelectionsList: []
|
||||
|
||||
}),
|
||||
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'title' },
|
||||
{ prop: 'btns', model: { class: 'gy-btns' } }
|
||||
]
|
||||
this.rules = {
|
||||
// name: { required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
}
|
||||
this.column = [
|
||||
{ prop: 'title', label: '考试名称', 'min-width': 130, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'classify', label: '考试分类', 'min-width': 130, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'totalScore', label: '总分', width: 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'passPercent', label: '及格分', width: 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'creator', label: '创建人', align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'examStartTime', label: '考试时间', width: 250, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'status', label: '状态', width: 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{
|
||||
prop: 'action',
|
||||
label: '操作',
|
||||
// width: 350,
|
||||
width: 250,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
}
|
||||
]
|
||||
|
||||
this.findAllQuestionsClassify()
|
||||
},
|
||||
activated () {
|
||||
this.pagingFindList()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 修改考试分类点击回调
|
||||
*/
|
||||
toEditClassify (item) {
|
||||
if (item.id === 'system') return this.$message.error('根分类不能修改')
|
||||
this.$prompt('请输入新分类名:', '编辑', {
|
||||
inputPattern: /.{1,}/,
|
||||
inputErrorMessage: '请输入分类名'
|
||||
}).then(({ value }) => {
|
||||
this.classify_loading = true
|
||||
editExamClassifyApi({ ...item, name: value }).then(res => { this.$message.success('修改成功'); this.findAllQuestionsClassify() }).finally(_ => {
|
||||
this.classify_loading = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
/**
|
||||
* 编辑考试
|
||||
*/
|
||||
editExam (row, type) {
|
||||
let path = '/assessment-evaluation/exam-arrangement/add-modify-exam/' + row.id
|
||||
if (type) {
|
||||
path += '/' + type
|
||||
}
|
||||
this.$router.push({ path })
|
||||
},
|
||||
/**
|
||||
* 新建考试
|
||||
*/
|
||||
addPaperHandler () {
|
||||
this.$router.push({ path: '/assessment-evaluation/exam-arrangement/add-modify-exam' })
|
||||
},
|
||||
/**
|
||||
* 批量选中项
|
||||
*/
|
||||
handleSelectionChange (e) {
|
||||
this.tableSelectionsList = e
|
||||
},
|
||||
/**
|
||||
* 分页修改
|
||||
*/
|
||||
pagingChange (event) {
|
||||
if ((typeof event.currentPage !== 'number') && (typeof event.pageSize !== 'number')) return
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
/**
|
||||
* 初始化分页数据
|
||||
*/
|
||||
initPageInfo () {
|
||||
this.page_info = { currentPage: 1, pageSize: this.page_info.pageSize, total: 0 }
|
||||
},
|
||||
/**
|
||||
* 分页请求数据
|
||||
*/
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
let classifyId = this.current_classify_id
|
||||
if (classifyId === 'system') classifyId = null
|
||||
this.paper_list_loading = true
|
||||
pagingExamListApi({ ...this.page_info, classifyId, ...this.form }).then(res => {
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
}).finally(_ => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 获取所有考试分类
|
||||
*/
|
||||
async findAllQuestionsClassify () {
|
||||
const { data } = await findAllExamClassifyApi()
|
||||
const list = [
|
||||
{ id: 'system', name: '考试分类', disabled: true, children: [] }
|
||||
]
|
||||
this.questionClassifyMap = {}
|
||||
this.questionClassifyMap = data.toTree()
|
||||
|
||||
list[0].children = this.questionClassifyMap.tree
|
||||
this.paper_classify_list = list
|
||||
},
|
||||
/**
|
||||
* 分类被选中
|
||||
*/
|
||||
change (classify) {
|
||||
this.current_classify_id = classify.id
|
||||
this.pagingFindList()
|
||||
},
|
||||
/**
|
||||
* 创建考试分类
|
||||
*/
|
||||
toCreateClassify (node) {
|
||||
node?.id === 'system' && (node = null)
|
||||
this.$prompt('请输入分类名', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
}).then(async ({ value }) => {
|
||||
const data = {
|
||||
name: value,
|
||||
pid: node == null ? 0 : node.id,
|
||||
level: node == null ? 1 : node.level + 1
|
||||
}
|
||||
await createExamClassifyApi(data)
|
||||
this.findAllQuestionsClassify()
|
||||
}).catch(_ => { })
|
||||
// this.$refs.treeMenuRef.setCurrentKey(node)
|
||||
// this.active_classify = { name: '', desc: '', base: null, features: [] }
|
||||
},
|
||||
/**
|
||||
* 批量删除、单个删除
|
||||
*/
|
||||
deletePapers (e, questionItem) {
|
||||
if (this.tableSelectionsList.length === 0 && (questionItem == null)) {
|
||||
this.$message.error('请先选择要删除的项目')
|
||||
} else {
|
||||
let delList = this.tableSelectionsList
|
||||
if (questionItem) {
|
||||
delList = [questionItem]
|
||||
}
|
||||
const hasStarted = delList.filter(item => +item.status !== 1)
|
||||
const tipsText = hasStarted.length !== delList.length ? '选中项中包含已开考的考试,确认删除吗?' : '确认删除吗?'
|
||||
this.$confirm(tipsText, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
}).then(res => {
|
||||
// if (hasStarted.length !== delList.length) {
|
||||
// delList = hasStarted
|
||||
// }
|
||||
this.paper_list_loading = true
|
||||
batchDeleteExamApi(delList.map(_ => _.id)).then(res => {
|
||||
this.pagingFindList()
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
}).catch(_ => {})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 删除分类与其下子分类
|
||||
*/
|
||||
toDeleteclassify (classify) {
|
||||
if (classify.id === 'system') return
|
||||
new Promise((resolve, reject) => {
|
||||
this.$confirm('此操作将永久删除此分类,及其子级分类,请确认后删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
}).then(res => resolve()).catch(_ => reject())
|
||||
}).then(_ => {
|
||||
this.classify_loading = true
|
||||
deleteExamClassifyApi(classify.id).finally(() => {
|
||||
this.findAllQuestionsClassify()
|
||||
this.$refs.treeMenuRef.setCurrentKey()
|
||||
this.classify_loading = false
|
||||
})
|
||||
}).catch(_ => { })
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" >
|
||||
.classify_page .form_layout {
|
||||
margin-top: 16px;
|
||||
|
||||
.suf {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.el-form-item:last-child {
|
||||
float: right;
|
||||
}
|
||||
}</style>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.exam-status {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,277 @@
|
||||
<template>
|
||||
<div>
|
||||
<DialogLayout
|
||||
title="固定选题"
|
||||
width="680px"
|
||||
:visible="true"
|
||||
@onCancel="$emit('onCancel')"
|
||||
@onConfirm="onFixedConfirm"
|
||||
class="gy-dialog-layout"
|
||||
>
|
||||
<div>
|
||||
<div class="gy-form inline" style="--fix: 77px">
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle">试题分类:</div>
|
||||
<QuestionClassifySelector
|
||||
:mountedLoad="true"
|
||||
v-model="form.classifyId"
|
||||
></QuestionClassifySelector>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle">选题限制:</div>
|
||||
<QuestionDifficultyLevelSelector
|
||||
:mountedLoad="true"
|
||||
v-model="form.difficultyLevel"
|
||||
></QuestionDifficultyLevelSelector>
|
||||
</div>
|
||||
<div class="gy-form-item mb" style="margin: 10px 0">
|
||||
<div class="gy-label middle">过滤:</div>
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
placeholder="请输入关键字过滤"
|
||||
style="width: 350px"
|
||||
></el-input>
|
||||
<el-button
|
||||
style="margin-left: 10px"
|
||||
round
|
||||
type="primary"
|
||||
@click="pagingFindList"
|
||||
>查询</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form" style="--fix: 80px; margin: 10px 0">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle">题目总数:</div>
|
||||
<div>{{ page_info.total }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<TableLayout
|
||||
ref="tableRef"
|
||||
:column="column"
|
||||
:data="table_data"
|
||||
:pageInfo="page_info"
|
||||
class="gy-el-table"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })"
|
||||
@size-change="(e) => pagingChange({ pageSize: e })"
|
||||
selection
|
||||
@select="userSelectItem"
|
||||
@select-all="allSelectHandler"
|
||||
>
|
||||
</TableLayout>
|
||||
<p style="padding-left: 10px">
|
||||
已选:{{ table_selections_list.length }} 题
|
||||
<el-button
|
||||
type="text"
|
||||
@click="preview_dialog_show = true"
|
||||
icon="i-j-fbks-yulan"
|
||||
>预览</el-button
|
||||
>
|
||||
</p>
|
||||
<!-- @selection-change="handleSelectionChange" -->
|
||||
</div>
|
||||
</DialogLayout>
|
||||
<QuestionListPreview
|
||||
:questionList="table_selections_list"
|
||||
v-if="preview_dialog_show"
|
||||
@onCancel="preview_dialog_show = false"
|
||||
></QuestionListPreview>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QuestionClassifySelector from '@/views/assessment-evaluation/question-bank-manage/add-modify-question/components/QuestionClassifySelector.vue'
|
||||
import QuestionDifficultyLevelSelector from '@/views/assessment-evaluation/question-bank-manage/add-modify-question/components/QuestionDifficultyLevelSelector.vue'
|
||||
import { TableLayout, DialogLayout } from '@/components/layout'
|
||||
import { pagingFindQuestionsApi } from '@/api/assessment-evaluation/questions'
|
||||
import QuestionListPreview from './QuestionListPreview.vue'
|
||||
export default {
|
||||
components: {
|
||||
TableLayout,
|
||||
QuestionClassifySelector,
|
||||
DialogLayout,
|
||||
QuestionListPreview,
|
||||
QuestionDifficultyLevelSelector
|
||||
},
|
||||
data: () => ({
|
||||
table_data: [],
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
form: { title: '', type: null, difficultyLevel: null },
|
||||
table_selections_list: [],
|
||||
old_selections_list: [],
|
||||
preview_dialog_show: false
|
||||
}),
|
||||
props: {
|
||||
questionType: {
|
||||
default: null
|
||||
},
|
||||
oldSelection: {
|
||||
default: []
|
||||
},
|
||||
excludeIds: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
table_selections_list: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (nv) {}
|
||||
},
|
||||
oldSelection: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.old_selections_list = nv
|
||||
this.initTableSelection()
|
||||
}
|
||||
},
|
||||
questionType: {
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.form.type = nv
|
||||
this.pagingFindList()
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.column = [{ prop: 'title', label: '题目', width: 150, align: 'center', 'show-overflow-tooltip': true }, { prop: 'type', label: '试题类型', align: 'center', 'show-overflow-tooltip': true }, { prop: 'classify', label: '试题分类', align: 'center', 'show-overflow-tooltip': true }, { prop: 'difficultyLevel', label: '难度', width: 60, align: 'center', 'show-overflow-tooltip': true }, { prop: 'knowledgePoint', label: '知识点', align: 'center', 'show-overflow-tooltip': true }, { prop: 'creator', label: '创建人', align: 'center', 'show-overflow-tooltip': true }]
|
||||
this.pagingFindList()
|
||||
this.table_selections_list = this.old_selections_list
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 确认选题回调
|
||||
*/
|
||||
onFixedConfirm () {
|
||||
this.$emit('current-change', this.table_selections_list)
|
||||
},
|
||||
/*
|
||||
* 更新表数据时调用的方法。它用于选择已经选择的行。
|
||||
*/
|
||||
initTableSelection () {
|
||||
/*
|
||||
*
|
||||
*/
|
||||
this.table_data.forEach((row) => {
|
||||
const id = this.old_selections_list.findIndex(
|
||||
(kitem) => kitem.id === row.id
|
||||
)
|
||||
if (id !== -1) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.tableRef.$refs.tableRef.toggleRowSelection(row, true)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
/*
|
||||
* 当用户选择表中的所有行时调用的方法。
|
||||
*/
|
||||
allSelectHandler (a) {
|
||||
if (a.length > 0) {
|
||||
a.forEach((item) => {
|
||||
const ind = this.table_selections_list.findIndex(
|
||||
(i) => i.id === item.id
|
||||
)
|
||||
if (ind === -1) {
|
||||
this.table_selections_list.push(item)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.table_data.forEach((item) => {
|
||||
const ind = this.table_selections_list.findIndex(
|
||||
(i) => i.id === item.id
|
||||
)
|
||||
if (ind !== -1) {
|
||||
this.table_selections_list.splice(ind, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
/*
|
||||
* 当用户选择表中的行时调用的方法。
|
||||
*/
|
||||
userSelectItem (a, b) {
|
||||
const checked = a.includes(b)
|
||||
if (checked) {
|
||||
this.table_selections_list.push(b)
|
||||
} else {
|
||||
const ind = this.table_selections_list.findIndex((i) => i.id === b.id)
|
||||
this.table_selections_list.splice(ind, 1)
|
||||
}
|
||||
/*
|
||||
*
|
||||
*/
|
||||
},
|
||||
/*
|
||||
* 用户更改页面时调用的方法。
|
||||
*/
|
||||
pagingChange (event) {
|
||||
if (
|
||||
typeof event.currentPage !== 'number' &&
|
||||
typeof event.pageSize !== 'number'
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
/*
|
||||
* handleSelectionChange (e) {
|
||||
* this.table_selections_list = e
|
||||
* },
|
||||
*/
|
||||
/*
|
||||
* 将 page_info 设置为默认值。
|
||||
*/
|
||||
initPageInfo () {
|
||||
this.page_info = {
|
||||
currentPage: 1,
|
||||
pageSize: this.page_info.pageSize,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
/*
|
||||
* 当分页时调用的方法。
|
||||
*/
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
let classifyId = this.current_classify_id
|
||||
if (classifyId === 'system') classifyId = null
|
||||
this.questions_list_loading = true
|
||||
if (this.excludeIds) {
|
||||
this.form.excludeIds = this.excludeIds
|
||||
}
|
||||
if (parseInt(this.$store.user.baseRole) === 3) {
|
||||
this.form.student = 1
|
||||
}
|
||||
pagingFindQuestionsApi({ ...this.page_info, classifyId, ...this.form })
|
||||
.then((res) => {
|
||||
/*
|
||||
*
|
||||
*/
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
this.$nextTick(() => {
|
||||
this.initTableSelection()
|
||||
})
|
||||
})
|
||||
.finally((_) => {
|
||||
this.questions_list_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<el-cascader :options="question_classify_list"
|
||||
:props="{ checkStrictly: true, value: 'id', label: 'name', children, emitPath: false }" clearable
|
||||
v-model="classify">
|
||||
</el-cascader>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { findAllPaperClassifyApi } from '@/api/assessment-evaluation/paper'
|
||||
export default {
|
||||
data: _ => ({
|
||||
classify: null,
|
||||
question_classify_list: []
|
||||
}),
|
||||
mounted () {
|
||||
if (this.mountedLoad) {
|
||||
this.findAllPaperClassify()
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.findAllPaperClassify()
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
default: null
|
||||
},
|
||||
mountedLoad: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.classify = nv
|
||||
}
|
||||
},
|
||||
classify: {
|
||||
immediate: false,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.$emit('input', nv)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
* 用于获取级联器数据的 API 调用。
|
||||
*/
|
||||
findAllPaperClassify () {
|
||||
findAllPaperClassifyApi().then(({ data }) => {
|
||||
let questionClassifyMap = {}
|
||||
questionClassifyMap = data.toTree()
|
||||
const list = questionClassifyMap.tree
|
||||
this.question_classify_list = list
|
||||
}).catch(err => {
|
||||
console.info(err)
|
||||
}).finally(_ => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div v-loading="is_loading" class="selector">
|
||||
<!-- <TableLayout :column="column" :data="table_data">
|
||||
<template v-slot:action="props">
|
||||
<p v-if="props.row.action != null">{{ props.row.action }}</p>
|
||||
<div v-else>
|
||||
<el-button type="text" @click="current_question_type = props.row.typeId;fixed_questions_show = true">固定选题</el-button>
|
||||
<el-button type="text" @click="currentQuestion = props.row; previewQuestionShow = true">随机选题</el-button>
|
||||
<el-button type="text" icon="el-icon-delete" @click="deleteQuestions($event, props.row)"
|
||||
style="color:#f04343 !important">清除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</TableLayout> -->
|
||||
<div class="gy-table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="blod">
|
||||
<td>组成</td> <td colspan="2">固定题</td> <td colspan="2">随机题</td> <td colspan="5"></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr class="blod">
|
||||
<td>题型</td> <td>选择试题</td> <td>试题数</td> <td>选择试题</td> <td>试题数</td> <td>每题分值</td> <td>总题数</td> <td>分值</td> <td rowspan="4">操作</td>
|
||||
</tr>
|
||||
<tbody>
|
||||
<tr v-for="item in table_data" :key="item.type">
|
||||
<td>{{ item.type }}</td>
|
||||
<td>
|
||||
<el-button
|
||||
type="text"
|
||||
@click="
|
||||
current_question_type = item.typeId;
|
||||
fixed_questions_show = true;
|
||||
"
|
||||
>固定选题
|
||||
</el-button>
|
||||
</td>
|
||||
<td>{{ item.fixdQuestions.length }}</td>
|
||||
<td>
|
||||
<el-button
|
||||
type="text"
|
||||
@click="
|
||||
current_question_type = item.typeId;
|
||||
rand_questions_show = true;
|
||||
"
|
||||
>随机选题
|
||||
</el-button>
|
||||
</td>
|
||||
<td>{{ item.randQuestions.length }}</td>
|
||||
<td>
|
||||
<!-- <el-input type="number" v-model="item.itemScore" style="width:80px" min="0"> -->
|
||||
<!-- </el-input> -->
|
||||
<el-input-number
|
||||
@change="
|
||||
(nv, ov) => {
|
||||
if (typeof nv != 'number')
|
||||
$nextTick(() => {
|
||||
item.itemScore = ov;
|
||||
});
|
||||
}
|
||||
"
|
||||
v-model="item.itemScore"
|
||||
size="mini"
|
||||
style="width: 120px"
|
||||
:min="1"
|
||||
label="描述文字"
|
||||
>
|
||||
</el-input-number>
|
||||
</td>
|
||||
<td>{{ item.fixdQuestions.length + item.randQuestions.length }}</td>
|
||||
<td>
|
||||
{{
|
||||
(item.fixdQuestions.length + item.randQuestions.length) *
|
||||
item.itemScore
|
||||
}}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<el-button
|
||||
type="text"
|
||||
@click="deleteThisType(item)"
|
||||
style="color: red"
|
||||
>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="blod">
|
||||
<td>总分</td>
|
||||
<td colspan="6">{{ totalScore }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<FixedQuestionSelectTable
|
||||
v-if="fixed_questions_show"
|
||||
@onCancel="fixed_questions_show = false"
|
||||
:questionType="current_question_type"
|
||||
:oldSelection="getOldSelection('fixed')"
|
||||
@current-change="fixedChange"
|
||||
:excludeIds="getExclude('fixed')"
|
||||
>
|
||||
</FixedQuestionSelectTable>
|
||||
<RandQuestionSelecter
|
||||
v-if="rand_questions_show"
|
||||
@onCancel="rand_questions_show = false"
|
||||
:questionType="current_question_type"
|
||||
@current-change="randChange"
|
||||
:oldSelection="getOldSelection('rand')"
|
||||
:excludeIds="getExclude('rand')"
|
||||
>
|
||||
</RandQuestionSelecter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import { getQuestionTypeOptionsApi } from '@/api/assessment-evaluation/questions'
|
||||
import FixedQuestionSelectTable from './FixedQuestionSelectTable.vue'
|
||||
import RandQuestionSelecter from './RandQuestionSelecter.vue'
|
||||
export default {
|
||||
components: {
|
||||
/*
|
||||
* TableLayout,
|
||||
*/
|
||||
FixedQuestionSelectTable,
|
||||
RandQuestionSelecter
|
||||
},
|
||||
data: () => ({
|
||||
column: [],
|
||||
table_data: [],
|
||||
is_loading: false,
|
||||
fixed_questions_show: false,
|
||||
rand_questions_show: false,
|
||||
question_type_options: [],
|
||||
current_question_type: null
|
||||
}),
|
||||
props: {
|
||||
questionData: {
|
||||
require: false,
|
||||
default: null
|
||||
},
|
||||
isMounted: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
table_data: {
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
const formData = this.dataFormat()
|
||||
this.$emit('current-change', formData)
|
||||
}
|
||||
},
|
||||
questionData: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.questionDataFormat(nv)
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 总分数 - 计算属性
|
||||
*/
|
||||
totalScore () {
|
||||
if (this.table_data.length > 0) {
|
||||
return this.table_data.reduce((prev, item, arr) => {
|
||||
/*
|
||||
*
|
||||
*/
|
||||
return (
|
||||
prev +
|
||||
(item.fixdQuestions.length + item.randQuestions.length) *
|
||||
item.itemScore
|
||||
)
|
||||
}, 0)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.isMounted && this.getQuestionTypeOptions()
|
||||
},
|
||||
activated () {
|
||||
this.getQuestionTypeOptions()
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
* 从表中删除问题。
|
||||
*/
|
||||
deleteThisType (type) {
|
||||
type.fixdQuestions = []
|
||||
type.randQuestions = []
|
||||
},
|
||||
/*
|
||||
* 用于格式化从父组件传入的数据的方法。
|
||||
*/
|
||||
questionDataFormat (nv) {
|
||||
/*
|
||||
*
|
||||
*/
|
||||
if (nv != null && Object.keys(nv).includes('fixdQuestions')) {
|
||||
/*
|
||||
*
|
||||
*/
|
||||
this.table_data = this.question_type_options.map((item) => {
|
||||
const thisTypeFixdQuestions = nv.fixdQuestions.filter(
|
||||
(qus) => qus.typeId === item.id
|
||||
)
|
||||
const thisTypeRandQuestions = nv.randQuestions.filter(
|
||||
(qus) => qus.typeId === item.id
|
||||
)
|
||||
return {
|
||||
type: item.name,
|
||||
typeId: item.id,
|
||||
fixdQuestions: thisTypeFixdQuestions,
|
||||
randQuestions: thisTypeRandQuestions,
|
||||
itemScore:
|
||||
thisTypeFixdQuestions.length > 0
|
||||
? thisTypeFixdQuestions[0].itemScore
|
||||
: thisTypeRandQuestions.length > 0
|
||||
? thisTypeRandQuestions[0].itemScore
|
||||
: 1
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 数据格式化
|
||||
*/
|
||||
dataFormat () {
|
||||
const questions = this.table_data.reduce(
|
||||
(prev, item, arr) => {
|
||||
let fixd = item.fixdQuestions
|
||||
let rand = item.randQuestions
|
||||
fixd = fixd.map((question) => ({
|
||||
...question,
|
||||
itemScore: item.itemScore
|
||||
}))
|
||||
rand = rand.map((question) => ({
|
||||
...question,
|
||||
itemScore: item.itemScore
|
||||
}))
|
||||
prev.fixdQuestions = prev.fixdQuestions.concat(fixd)
|
||||
prev.randQuestions = prev.randQuestions.concat(rand)
|
||||
prev.totalScore += (fixd.length + rand.length) * item.itemScore
|
||||
prev.questionCount += fixd.length + rand.length
|
||||
return prev
|
||||
},
|
||||
{
|
||||
fixdQuestions: [],
|
||||
randQuestions: [],
|
||||
totalScore: 0,
|
||||
questionCount: 0
|
||||
}
|
||||
)
|
||||
/*
|
||||
*
|
||||
*/
|
||||
return questions
|
||||
},
|
||||
/**
|
||||
* 固定选题被改变
|
||||
*/
|
||||
fixedChange (questionList) {
|
||||
const currentRow = this.table_data.find(
|
||||
(item) => item.typeId === this.current_question_type
|
||||
)
|
||||
currentRow.fixdQuestions = JSON.parse(JSON.stringify(questionList))
|
||||
this.fixed_questions_show = false
|
||||
},
|
||||
/**
|
||||
* 随机选题被改变
|
||||
*/
|
||||
randChange (questionList) {
|
||||
const currentRow = this.table_data.find(
|
||||
(item) => item.typeId === this.current_question_type
|
||||
)
|
||||
currentRow.randQuestions = JSON.parse(JSON.stringify(questionList))
|
||||
this.rand_questions_show = false
|
||||
},
|
||||
/**
|
||||
* 获取已选中项
|
||||
*/
|
||||
getOldSelection (type = 'fixed') {
|
||||
const currentRow = this.table_data.find(
|
||||
(item) => item.typeId === this.current_question_type
|
||||
)
|
||||
return type === 'fixed'
|
||||
? currentRow.fixdQuestions
|
||||
: currentRow.randQuestions
|
||||
},
|
||||
getExclude (type = 'fixed') {
|
||||
if (!this.current_question_type) {
|
||||
return null
|
||||
}
|
||||
const currentRow = this.table_data.find(
|
||||
(item) => item.typeId === this.current_question_type
|
||||
)
|
||||
if (type === 'fixed') {
|
||||
return currentRow.randQuestions.map((item) => item.id)
|
||||
} else {
|
||||
return currentRow.fixdQuestions.map((item) => item.id)
|
||||
}
|
||||
},
|
||||
// 获取问题类型的 API 调用。
|
||||
getQuestionTypeOptions () {
|
||||
getQuestionTypeOptionsApi()
|
||||
.then(({ data }) => {
|
||||
this.question_type_options = data
|
||||
if (Object.keys(this.questionData).includes('fixdQuestions')) {
|
||||
this.questionDataFormat(this.questionData)
|
||||
return
|
||||
}
|
||||
this.table_data = this.question_type_options.map((item) => ({
|
||||
type: item.name,
|
||||
typeId: item.id,
|
||||
fixdQuestions: [],
|
||||
randQuestions: [],
|
||||
itemScore: 1
|
||||
}))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.info(err)
|
||||
})
|
||||
.finally((_) => {
|
||||
this.is_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table_layout ::v-deep .el-table--small .el-table__cell {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.table_layout ::v-deep .el-table .el-table__row:first-child .cell {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<DialogLayout title="预览" width="680px" :visible="true" @onConfirm="$emit('onCancel')" @onCancel="$emit('onCancel')"
|
||||
:actionBarOption="{noCencel:true}">
|
||||
<div>
|
||||
<div v-for="(item,ind) in questionList" :key="item.id">
|
||||
<QuestionItem :questionInfo="item" :serial="ind+1"></QuestionItem>
|
||||
</div>
|
||||
</div>
|
||||
</DialogLayout>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DialogLayout } from '@/components/layout'
|
||||
import QuestionItem from '@/views/assessment-evaluation/question-bank-manage/components/QuestionItem.vue'
|
||||
export default {
|
||||
components: { DialogLayout, QuestionItem },
|
||||
data: () => ({
|
||||
mode: 0,
|
||||
format_questions_list: []
|
||||
}),
|
||||
props: {
|
||||
questionList: {
|
||||
default: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
created () {
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div>
|
||||
<DialogLayout
|
||||
title="随机选题"
|
||||
width="710px"
|
||||
:visible="true"
|
||||
@onCancel="$emit('onCancel')"
|
||||
@onConfirm="onRandConfirm"
|
||||
>
|
||||
<div>
|
||||
<div class="gy-form inline" style="--fix: 80px">
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle">试题分类:</div>
|
||||
<QuestionClassifySelector
|
||||
:mountedLoad="true"
|
||||
v-model="form.classifyId"
|
||||
></QuestionClassifySelector>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle">难易程度:</div>
|
||||
<QuestionDifficultyLevelSelector
|
||||
:mountedLoad="true"
|
||||
v-model="form.difficultyLevel"
|
||||
>
|
||||
</QuestionDifficultyLevelSelector>
|
||||
</div>
|
||||
<div class="gy-form-item" style="margin: 5px 0; height: 24px">
|
||||
<div class="gy-label middle">题目总数:</div>
|
||||
<div class="gy-label middle" style="justify-content:flex-start">{{ question_total }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form">
|
||||
<div
|
||||
class="gy-form-item"
|
||||
style="margin: 10px 0; align-items: center; --fix: 80px"
|
||||
>
|
||||
<div class="gy-label middle">数量:</div>
|
||||
<!-- <el-input v-model="ranCount" type="number" style="width:80px " :max="question_total"></el-input> -->
|
||||
<el-input-number
|
||||
v-model="ranCount"
|
||||
size="mini"
|
||||
style="width: 120px"
|
||||
:min="0"
|
||||
:max="+question_total"
|
||||
>
|
||||
</el-input-number>
|
||||
/ {{ question_total }}
|
||||
<el-button
|
||||
style="margin-left: 10px"
|
||||
round
|
||||
type="primary"
|
||||
@click="getRandQuestions"
|
||||
>抽题</el-button
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="gy-form-item"
|
||||
style="
|
||||
margin: 5px 0;
|
||||
height: 30px;
|
||||
align-items: center;
|
||||
--fix: 80px;
|
||||
"
|
||||
>
|
||||
<div class="gy-label middle">已选:</div>
|
||||
<div>
|
||||
{{ table_selections_list.length }}题
|
||||
<el-button
|
||||
type="text"
|
||||
@click="preview_dialog_show = true"
|
||||
style="padding: 0 5px"
|
||||
icon="i-j-fbks-yulan"
|
||||
>预览</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogLayout>
|
||||
<QuestionListPreview
|
||||
:questionList="table_selections_list"
|
||||
v-if="preview_dialog_show"
|
||||
@onCancel="preview_dialog_show = false"
|
||||
></QuestionListPreview>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DialogLayout } from '@/components/layout'
|
||||
import QuestionClassifySelector from '@/views/assessment-evaluation/question-bank-manage/add-modify-question/components/QuestionClassifySelector.vue'
|
||||
import QuestionDifficultyLevelSelector from '@/views/assessment-evaluation/question-bank-manage/add-modify-question/components/QuestionDifficultyLevelSelector.vue'
|
||||
import {
|
||||
getQuestionCountByTypeIdApi,
|
||||
getRandQuestionApi
|
||||
} from '@/api/assessment-evaluation/questions'
|
||||
import QuestionListPreview from './QuestionListPreview.vue'
|
||||
export default {
|
||||
components: {
|
||||
QuestionClassifySelector,
|
||||
QuestionDifficultyLevelSelector,
|
||||
DialogLayout,
|
||||
QuestionListPreview
|
||||
},
|
||||
data: () => ({
|
||||
form: { type: null, difficultyLevel: null, classifyId: null },
|
||||
table_selections_list: [],
|
||||
question_total: 0,
|
||||
ranCount: 0,
|
||||
preview_dialog_show: false
|
||||
}),
|
||||
props: {
|
||||
questionType: {
|
||||
default: null,
|
||||
require: true
|
||||
},
|
||||
oldSelection: {
|
||||
default: []
|
||||
},
|
||||
excludeIds: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
form: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.getCountByType(nv)
|
||||
}
|
||||
},
|
||||
|
||||
questionType: {
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.form.type = nv
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.getCountByType()
|
||||
this.table_selections_list = this.oldSelection
|
||||
},
|
||||
methods: {
|
||||
// 当用户单击确认按钮时调用的方法。
|
||||
onRandConfirm () {
|
||||
this.$emit('current-change', this.table_selections_list)
|
||||
},
|
||||
// 当用户单击“抽题”按钮时调用的方法。
|
||||
getRandQuestions () {
|
||||
if (this.ranCount > this.question_total) { return this.$message.error('抽题数不能比总提数大') }
|
||||
const queryForm = {
|
||||
count: this.ranCount,
|
||||
...this.form
|
||||
}
|
||||
if (this.excludeIds) {
|
||||
queryForm.excludeIds = this.excludeIds
|
||||
}
|
||||
getRandQuestionApi(queryForm).then((res) => {
|
||||
this.table_selections_list = res.data
|
||||
})
|
||||
},
|
||||
// 获取问题总数的方法。
|
||||
getCountByType () {
|
||||
if (!this.form.type) return
|
||||
if (this.excludeIds) {
|
||||
this.form.excludeIds = this.excludeIds
|
||||
}
|
||||
if (parseInt(this.$store.user.baseRole) === 3) {
|
||||
this.form.student = 1
|
||||
}
|
||||
getQuestionCountByTypeIdApi(this.form).then((res) => {
|
||||
this.question_total = res.data
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.gy-label{
|
||||
padding: 0;
|
||||
justify-content: flex-end;
|
||||
padding-right: 10px !important;
|
||||
}
|
||||
.gy-dialog-layout ::v-deep {
|
||||
._ctx {
|
||||
overflow: visible;
|
||||
._title {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div v-loading="page_is_loading">
|
||||
<div class="gy-area-title">试卷属性</div>
|
||||
<div class="gy-form inline" style="--fix:77px;">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle require">试卷名称:</div>
|
||||
<el-input v-model="form.title" placeholder="请输入名称" style="width:350px "></el-input>
|
||||
</div>
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle require">试卷分类:</div>
|
||||
<PaperClassifySelector v-model="form.classifyId"></PaperClassifySelector>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form" style="--fix:77px;">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label">试卷描述:</div>
|
||||
<el-input type="textarea" placeholder="请输入试卷描述信息" v-model="form.paperDesc" style="width:100%"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form" style="--fix:77px;" v-if="!isSimTest">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle ">共享试卷:</div>
|
||||
<div>
|
||||
<el-switch
|
||||
v-model="form.share"
|
||||
:active-value="1"
|
||||
:inactive-value="0">
|
||||
</el-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-area-title">试卷设置</div>
|
||||
<div class="gy-form inline" style="--fix:77px;">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle require">答题时长:</div>
|
||||
<el-input v-model="form.paperDuration" placeholder="时长" type="number" style="width:90px "></el-input>
|
||||
</div>
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle ">及格分:</div>
|
||||
<div>
|
||||
<el-input v-model="form.passPercent" placeholder="60" min="0" max="100" step="5" type="number"
|
||||
style="width:90px "></el-input> %
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle " style="height:32px">
|
||||
计:<span style="color:red;padding:0 5px">{{Math.ceil(form.totalScore * form.passPercent /100) }}</span>分,总分 <span style="color:red;padding:0 5px">{{ form.totalScore }}</span> 分
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form inline" style="--fix:100px;">
|
||||
<div class="gy-form-item mb" style="margin-right: 45px;">
|
||||
<div class="gy-label middle ">试题排序乱序:</div>
|
||||
<div>
|
||||
<el-switch
|
||||
v-model="form.questionsIsRandomSort"
|
||||
:active-value="1"
|
||||
:inactive-value="0">
|
||||
</el-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle ">试题选项乱序:</div>
|
||||
<div>
|
||||
<el-switch
|
||||
v-model="form.optionsIsRandomSort"
|
||||
:active-value="1"
|
||||
:inactive-value="0">
|
||||
</el-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gy-area-title">设置题型</div>
|
||||
<div>
|
||||
<PaperSelectQuestionTable @current-change="changeQuestionsInfo" :questionData="questionInfo" @totalScore="totalScoreChange">
|
||||
</PaperSelectQuestionTable>
|
||||
</div>
|
||||
<div style="width:100%;margin-top:20px;text-align: center;">
|
||||
<el-button type="primary" round @click="onToPublishExam" v-if="isSimTest">立即考试</el-button>
|
||||
<el-button type="error" plain round @click="$router.go(-1)" v-if="!isSimTest">返回</el-button>
|
||||
<el-button type="primary" round @click="onPreview">预览</el-button>
|
||||
<el-button type="primary" round @click="onSave(false)" v-if="!isSimTest">保存</el-button>
|
||||
<el-button type="primary" round @click="onToPublishExam" v-if="!isSimTest">发布考试</el-button>
|
||||
</div>
|
||||
<QuestionListPreview :questionList="current_questions_list" v-if="preview_dialog_show"
|
||||
@onCancel="preview_dialog_show = false;"></QuestionListPreview>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import QuestionListPreview from './components/QuestionListPreview.vue'
|
||||
import PaperClassifySelector from './components/PaperClassifySelector.vue'
|
||||
import PaperSelectQuestionTable from './components/PaperSelectQuestionTable.vue'
|
||||
import { createPaperApi, getPaperInfoByIdApi, patchPaperInfoByIdApi, createSimTestApi } from '@/api/assessment-evaluation/paper'
|
||||
import { createSimExamHistoryApi } from '@/api/assessment-evaluation/onlineTest'
|
||||
|
||||
export default {
|
||||
components: { PaperClassifySelector, PaperSelectQuestionTable, QuestionListPreview },
|
||||
data () {
|
||||
return {
|
||||
form: {
|
||||
title: '',
|
||||
classifyId: null,
|
||||
paperDesc: '',
|
||||
totalScore: 0,
|
||||
questionCount: 0,
|
||||
passPercent: 60,
|
||||
paperDuration: 120,
|
||||
optionsIsRandomSort: 0,
|
||||
questionsIsRandomSort: 0,
|
||||
share: 0
|
||||
},
|
||||
paperInfo: {},
|
||||
questionInfo: {},
|
||||
page_is_loading: false,
|
||||
current_questions_list: [],
|
||||
preview_dialog_show: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
isSimTest: {
|
||||
default: false
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isModify () {
|
||||
return this.$route.params.paperId != null
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.initFrom()
|
||||
if (this.isModify) {
|
||||
// return false
|
||||
this.getPaperInfo()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
totalScoreChange (total) {
|
||||
this.totalScore = total
|
||||
},
|
||||
initFrom () {
|
||||
this.form = {
|
||||
title: '',
|
||||
classifyId: null,
|
||||
paperDesc: '',
|
||||
totalScore: 0,
|
||||
questionCount: 0,
|
||||
passPercent: 60,
|
||||
paperDuration: 60,
|
||||
optionsIsRandomSort: 0,
|
||||
questionsIsRandomSort: 0,
|
||||
share: 0
|
||||
}
|
||||
if (this.isSimTest) this.form.isPracticeExam = 1
|
||||
|
||||
this.paperInfo = {}
|
||||
this.questionInfo = {}
|
||||
},
|
||||
// 预览
|
||||
onPreview () {
|
||||
// this.$message('开发中')
|
||||
|
||||
if (this.questionInfo?.fixdQuestions || this.questionInfo?.randQuestions) {
|
||||
this.current_questions_list = this.questionInfo?.fixdQuestions?.concat(this.questionInfo?.randQuestions)
|
||||
this.preview_dialog_show = true
|
||||
} else if (this.form.questionInfo?.fixdQuestions || this.form.questionInfo?.randQuestions) {
|
||||
this.current_questions_list = this.form.questionInfo?.fixdQuestions?.concat(this.form.questionInfo?.randQuestions)
|
||||
this.preview_dialog_show = true
|
||||
}
|
||||
},
|
||||
// 保存
|
||||
onSave (isToPublishExam = false) {
|
||||
const form = this.form
|
||||
if (!form.title) return this.$message.error('请输入试卷名称')
|
||||
if (!form.classifyId) return this.$message.error('请选择试卷分类')
|
||||
if (!form.paperDuration) return this.$message.error('请填写答题时长')
|
||||
if (!form.passPercent) return this.$message.error('请填写及格分占比')
|
||||
if (form.passPercent < 0 || form.passPercent > 100) return this.$message.error('及格分占比应在0-100之间')
|
||||
if (form.questionCount === 0) return this.$message.error('请选择一些试题')
|
||||
if (form.totalScore === 0) return this.$message.error('请确认题型的分值')
|
||||
if (isToPublishExam) {
|
||||
form.status = 1
|
||||
}
|
||||
if (this.isSimTest) {
|
||||
this.toCreateSimTest()
|
||||
return
|
||||
}
|
||||
if (this.isModify) {
|
||||
this.onModify(isToPublishExam)
|
||||
return
|
||||
}
|
||||
this.page_is_loading = true
|
||||
createPaperApi(this.form).then(res => {
|
||||
this.$message.success('保存成功')
|
||||
this.paperInfo = res.data.newPaper
|
||||
// TODO: 暂时保存后跳转到列表页,之后需要留在当前页,可发布考试
|
||||
if (isToPublishExam) {
|
||||
this.backOrToPublish(res.data.newPaper.id, res.data.newPaper.title, res.data.newPaper.totalScore)
|
||||
} else {
|
||||
this.$router.replace({ path: '/assessment-evaluation/examination-paper-manage' })
|
||||
}
|
||||
}).catch(err => {
|
||||
console.info(err)
|
||||
}).finally(() => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
toCreateSimTest () {
|
||||
this.page_is_loading = true
|
||||
createSimTestApi(this.form).then(res => {
|
||||
this.paperInfo = res.data.newPaper
|
||||
const { studentOnlineExam } = res.data
|
||||
createSimExamHistoryApi({ onlineExamId: studentOnlineExam.id }).then(res => {
|
||||
if (res.data) {
|
||||
this.$message.success('开始考试')
|
||||
this.$router.push({ path: '/assessment-evaluation/online-test/begin-online-exam/' + res.data.id })
|
||||
}
|
||||
})
|
||||
// TODO: 暂时保存后跳转到列表页,之后需要留在当前页,可发布考试
|
||||
}).catch(err => {
|
||||
console.info(err)
|
||||
}).finally(() => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
backOrToPublish (paperId = null, title = null, totalScore = null) {
|
||||
let url = '/assessment-evaluation/examination-paper-manage'
|
||||
const query = {}
|
||||
if (paperId && title) {
|
||||
url = '/assessment-evaluation/exam-arrangement/add-modify-exam'
|
||||
query.paperId = paperId
|
||||
query.title = title
|
||||
query.totalScore = totalScore
|
||||
}
|
||||
this.$router.replace({ path: url, query })
|
||||
},
|
||||
onModify (isToPublishExam = false) {
|
||||
this.page_is_loading = true
|
||||
patchPaperInfoByIdApi(this.form).then(res => {
|
||||
this.$message.success('编辑成功')
|
||||
if (isToPublishExam) {
|
||||
this.backOrToPublish(res.data.newPaper.id, res.data.newPaper.title, res.data.newPaper.totalScore)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.info(err)
|
||||
}).finally(() => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
// 去发布考试
|
||||
onToPublishExam () {
|
||||
this.onSave(true)
|
||||
},
|
||||
changeQuestionsInfo (questionInfo) {
|
||||
this.questionInfo = {}
|
||||
this.form.questionInfo = questionInfo
|
||||
this.form.questionCount = questionInfo.questionCount
|
||||
this.form.totalScore = questionInfo.totalScore
|
||||
},
|
||||
getPaperInfo () {
|
||||
const id = this.$route.params.paperId
|
||||
if (!id) return
|
||||
this.page_is_loading = true
|
||||
getPaperInfoByIdApi(id).then(res => {
|
||||
this.pageInfo = res.data
|
||||
this.form.id = this.pageInfo.id
|
||||
this.form.title = this.pageInfo.title
|
||||
this.form.classifyId = this.pageInfo.classifyId
|
||||
this.form.paperDesc = this.pageInfo.paperDesc
|
||||
this.form.passPercent = this.pageInfo.passPercent
|
||||
this.form.paperDuration = this.pageInfo.paperDuration
|
||||
this.form.questionsIsRandomSort = this.pageInfo.questionsIsRandomSort
|
||||
this.form.optionsIsRandomSort = this.pageInfo.optionsIsRandomSort
|
||||
this.form.share = this.pageInfo.share
|
||||
this.questionInfo = this.pageInfo.questionInfo
|
||||
}).finally(() => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
|
||||
<template>
|
||||
<div class="v-page classify_page">
|
||||
<div class="v-ctx" v-loading="paper_list_loading">
|
||||
<FormLayout
|
||||
ref="formLayoutRef"
|
||||
:items="formItems"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="70px"
|
||||
label-position="right"
|
||||
:inline="true"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<!-- <el-input placeholder="请输入查询内容" v-model="form.title" class="input-with-select" clearable>
|
||||
<el-button slot="append" @click="pagingFindList">查询</el-button>
|
||||
</el-input> -->
|
||||
<QueryInput v-model="form.title" @query="pagingFindList"></QueryInput>
|
||||
</template>
|
||||
<template v-slot:btns>
|
||||
</template>
|
||||
</FormLayout>
|
||||
<TableLayout :column="column" :data="table_data" :pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })" @size-change="(e) => pagingChange({ pageSize: e })">
|
||||
<template v-slot:action="props">
|
||||
<el-button type="text" icon="el-icon-check" @click="onSelect($event,props.row)" >选择</el-button>
|
||||
</template>
|
||||
</TableLayout>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TableLayout, FormLayout } from '@/components/layout'
|
||||
|
||||
import { pagingFindPaperApi } from '@/api/assessment-evaluation/paper'
|
||||
import QueryInput from '@/components/widget/QueryInput.vue'
|
||||
export default {
|
||||
components: {
|
||||
TableLayout, FormLayout, QueryInput
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
table_data: [],
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
current_classify_id: null,
|
||||
// 试题分类树是否加载中
|
||||
paper_list_loading: false,
|
||||
form: { title: '', status: 1 }
|
||||
}),
|
||||
props: {
|
||||
mountedLoad: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'title' },
|
||||
{ prop: 'btns', model: { class: 'gy-btns' } }
|
||||
]
|
||||
this.rules = {
|
||||
// name: { required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
}
|
||||
this.column = [
|
||||
{ prop: 'title', label: '试卷名', align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'classify', label: '试卷分类', align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'totalScore', label: '总分', align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'questionCount', label: '试题总数', align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'creator', label: '创建人', align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'createTime', label: '创建时间', width: 150, align: 'center', 'show-overflow-tooltip': true },
|
||||
{
|
||||
prop: 'action',
|
||||
label: '操作',
|
||||
// width: 350,
|
||||
width: 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
}
|
||||
]
|
||||
},
|
||||
mounted () {
|
||||
if (this.mountedLoad) this.pagingFindList()
|
||||
},
|
||||
activated () {
|
||||
this.pagingFindList()
|
||||
},
|
||||
methods: {
|
||||
// 组件发出的自定义事件。
|
||||
onSelect (_, row) {
|
||||
this.$emit('onSelect', row)
|
||||
},
|
||||
// 页面更改时调用的方法。
|
||||
pagingChange (event) {
|
||||
if ((typeof event.currentPage !== 'number') && (typeof event.pageSize !== 'number')) return
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
// 将 page_info 设置为默认值。
|
||||
initPageInfo () {
|
||||
this.page_info = { currentPage: 1, pageSize: this.page_info.pageSize, total: 0 }
|
||||
},
|
||||
// 当用户单击搜索按钮时调用的方法。
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
let classifyId = this.current_classify_id
|
||||
if (classifyId === 'system') classifyId = null
|
||||
this.paper_list_loading = true
|
||||
pagingFindPaperApi({ ...this.page_info, classifyId, ...this.form }).then(res => {
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
}).finally(_ => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .form_layout{
|
||||
margin-top: 16px;
|
||||
.el-form-item:last-child{
|
||||
float:right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,654 @@
|
||||
<template>
|
||||
<div v-loading="!paperInfo.id" class="v-page print-paper">
|
||||
<div style="max-width: 210mm; min-width: 210mm">
|
||||
<div id="paperView">
|
||||
<div class="seal">
|
||||
<div class="examinee-info">
|
||||
<p v-for="(item, index) in examineeInfo" :key="index">
|
||||
<span style="font-size: 15px; font-weight: bold">{{
|
||||
item.value
|
||||
}}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="seal-title">
|
||||
<div>线</div>
|
||||
<div>封</div>
|
||||
<div>密</div>
|
||||
</div>
|
||||
</div>
|
||||
<ExercisePerviewPaper
|
||||
class="paper-perview"
|
||||
:questions="questions"
|
||||
:title="paperInfo?.title"
|
||||
:mode="mode"
|
||||
ref="printRef"
|
||||
>
|
||||
<template v-slot:title>{{ paperInfo?.title }}</template>
|
||||
<template v-slot:otherInfo>
|
||||
<div class="exam-base-explain">
|
||||
全卷满分:{{ paperInfo.totalScore }}分,考试时间:{{
|
||||
paperInfo.paperDuration
|
||||
}}分钟
|
||||
</div>
|
||||
<div class="exam-base-notice" v-if="checkTypeInfo.info?.考生须知">
|
||||
<div class="notice-title">考生须知</div>
|
||||
<div class="notice-content">
|
||||
<p
|
||||
v-for="(item, index) in notice"
|
||||
:key="index"
|
||||
style="word-break: break-all"
|
||||
>
|
||||
{{ index + 1 }}:{{ item.value }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="exam-question-type-score"
|
||||
v-if="checkTypeInfo.info?.总评分"
|
||||
>
|
||||
<table border="1">
|
||||
<thead>
|
||||
<th
|
||||
class="type-score-item"
|
||||
v-for="(item, index) in questionsClssify.classify"
|
||||
:key="index"
|
||||
>
|
||||
{{ item.title }}
|
||||
</th>
|
||||
<th class="type-score-item">总分</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<td
|
||||
class="type-score-item"
|
||||
v-for="(item, index) in questionsClssify.classify"
|
||||
:key="index"
|
||||
>
|
||||
{{ item.userScore }}
|
||||
<br />
|
||||
<!-- {{ item.score }} -->
|
||||
</td>
|
||||
<td class="type-score-item">
|
||||
{{
|
||||
questionsClssify.classify?.reduce((prev, item) => {
|
||||
if (typeof item.userScore === "undefined") {
|
||||
return "";
|
||||
}
|
||||
return (prev += item.userScore);
|
||||
}, 0)
|
||||
}}
|
||||
<br />
|
||||
<!-- {{
|
||||
questionsClssify.classify?.reduce((pre, item) => {
|
||||
return item.score + pre;
|
||||
}, 0)
|
||||
}} -->
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</ExercisePerviewPaper>
|
||||
</div>
|
||||
</div>
|
||||
<div class="print-toolbar">
|
||||
<div>
|
||||
<div class="form-item flex-center">
|
||||
<div class="form-item-label">试卷类型:</div>
|
||||
<div class="form-item-content">
|
||||
<el-select
|
||||
v-model="printType"
|
||||
@change="changeType"
|
||||
size="mini"
|
||||
placeholder="请选择打印类型"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in printData"
|
||||
:key="item.id"
|
||||
:label="item.type"
|
||||
:value="item.id"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item-label">试卷信息:</div>
|
||||
<div class="form-item-content">
|
||||
<p
|
||||
v-for="(item, key) in checkTypeInfo.info"
|
||||
:key="item"
|
||||
style="margin-bottom: 10px; white-space: nowrap"
|
||||
>
|
||||
<el-radio value="1" label="1"
|
||||
><span
|
||||
style="
|
||||
color: #555555;
|
||||
margin-right: 10px;
|
||||
width: 90px;
|
||||
display: inline-block;
|
||||
"
|
||||
>{{ key }}</span
|
||||
>
|
||||
<el-button
|
||||
@click="toEditorInfo(key)"
|
||||
style="font-size: 14px; padding: 0"
|
||||
type="text"
|
||||
size="mini"
|
||||
v-if="['考生信息填写', '考生须知'].includes(key)"
|
||||
>编辑</el-button
|
||||
>
|
||||
</el-radio>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<div class="form-item-label">试卷排版:</div>
|
||||
<div class="form-item-content">
|
||||
<el-radio-group v-model="composing">
|
||||
<el-radio :label="0" style="margin-bottom: 10px">单页版</el-radio>
|
||||
<el-radio :label="1">双页版</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" style="width: 100%" @click="toPrint(false)"
|
||||
>打印</el-button
|
||||
>
|
||||
<el-button type="primary" style="width: 100%;margin:10px 0 0 0;" @click="toPrint(paperInfo)"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
<DialogLayout
|
||||
:title="editorInfo.name"
|
||||
:actionBarOption="{
|
||||
noCencel: true,
|
||||
noConfirm: true,
|
||||
}"
|
||||
:visible="editorInfo.name.length"
|
||||
@onCancel="editorInfo = { name: '', key: '' }"
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<p
|
||||
class="editorInfo-item"
|
||||
v-for="(item, index) in editorInfo.data"
|
||||
:key="index"
|
||||
>
|
||||
<el-input v-model="item.value" size="mini"></el-input>
|
||||
<el-button
|
||||
:disabled="index === 0"
|
||||
type="text"
|
||||
:style="{
|
||||
'margin-left': '10px',
|
||||
color: index === 0 ? '#ccc' : '#f56c6c',
|
||||
}"
|
||||
@click="delEditorInfo(index)"
|
||||
>删除</el-button
|
||||
>
|
||||
</p>
|
||||
<el-button type="primary" @click="addEditorInfo">添加</el-button>
|
||||
</div>
|
||||
</DialogLayout>
|
||||
<DialogLayout
|
||||
:visible="isCheckExam"
|
||||
title="选择试卷"
|
||||
@onCancel="isCheckExam = false"
|
||||
@onConfirm="selectExam"
|
||||
>
|
||||
<SelectExamPaper v-if="$store.user.baseRole === 3"></SelectExamPaper>
|
||||
<HumanEvaluation :column="paperColumn" v-else></HumanEvaluation>
|
||||
</DialogLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ExercisePerviewPaper from '@/views/statistic/components/ExercisePerviewPaper.vue'
|
||||
import { getPaperInfoByIdApi } from '@/api/assessment-evaluation/paper'
|
||||
import { DialogLayout } from '@/components/layout'
|
||||
import SelectExamPaper from './SelectExamPaper.vue'
|
||||
import HumanEvaluation from '@/views/assessment-evaluation/human-evaluation/index.vue'
|
||||
import { getOnlineExamResultApi } from '@/api/assessment-evaluation/onlineTest'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ExercisePerviewPaper,
|
||||
DialogLayout,
|
||||
SelectExamPaper,
|
||||
HumanEvaluation
|
||||
},
|
||||
data: () => ({
|
||||
// 试卷ID
|
||||
paperId: null,
|
||||
// 试卷信息
|
||||
paperInfo: {},
|
||||
// 试题列表
|
||||
questions: [],
|
||||
// 隐藏或展示选取试卷的弹窗
|
||||
isCheckExam: false,
|
||||
// 打印类型
|
||||
printType: null,
|
||||
// 编辑考生信息和考生信息切换
|
||||
editorInfo: { name: '', key: '', data: [], max: 0 },
|
||||
// 单双页
|
||||
composing: 0,
|
||||
// 试题分类后的数据
|
||||
questionsClssify: {},
|
||||
// 选中的打印模式详细信息
|
||||
checkTypeInfo: {},
|
||||
// 打印模式对应的组件状态
|
||||
mode: 2,
|
||||
// 路径参数
|
||||
query: {},
|
||||
// 几种打印模式
|
||||
printData: [
|
||||
{
|
||||
type: '正式试卷',
|
||||
id: 1,
|
||||
mode: 2,
|
||||
info: {
|
||||
考生信息填写: true,
|
||||
考生须知: true,
|
||||
考试时间及总分: true,
|
||||
试卷分类: true,
|
||||
总评分: true,
|
||||
题型评分: true,
|
||||
分割线: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '练习试卷',
|
||||
id: 2,
|
||||
mode: 2,
|
||||
info: {
|
||||
考生信息填写: true,
|
||||
考试时间及总分: true,
|
||||
试卷分类: true,
|
||||
总评分: true,
|
||||
题型评分: true,
|
||||
分割线: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '背题试卷',
|
||||
id: 3,
|
||||
mode: 4,
|
||||
info: {
|
||||
考生信息填写: true,
|
||||
考试时间及总分: true,
|
||||
试卷分类: true,
|
||||
// 总评分: true,
|
||||
// 题型评分: true,
|
||||
分割线: true,
|
||||
答案: true,
|
||||
解析: true
|
||||
}
|
||||
},
|
||||
{
|
||||
type: '考试试卷',
|
||||
id: 4,
|
||||
mode: 1,
|
||||
info: {
|
||||
考生信息填写: true,
|
||||
考试时间及总分: true,
|
||||
试卷分类: true,
|
||||
总评分: true,
|
||||
题型评分: true,
|
||||
分割线: true,
|
||||
答案: true,
|
||||
解析: true,
|
||||
评分: true,
|
||||
评语: true
|
||||
}
|
||||
}
|
||||
],
|
||||
// 基础的考生信息
|
||||
examineeInfo: [
|
||||
{ value: '姓名__________' },
|
||||
{ value: '班级__________' },
|
||||
{ value: '证件号__________' }
|
||||
],
|
||||
// 考生须知
|
||||
notice: [
|
||||
{
|
||||
value:
|
||||
'考前三十分钟,考生需持符合报考规定的并与准考证显示信息一致的有效证件,进入规定的考场。'
|
||||
},
|
||||
{ value: '考生入场后,按号入座,将本人《准考证》放在课桌上,以便核验。' },
|
||||
{
|
||||
value:
|
||||
'笔试开考10分钟内,允许迟到考生进入考场参加考试,考试结束时间按照统一规定结束。开考10分钟后,禁止迟到考生入场考试。提前离场考生离场后不得进场续考。考试最后十分钟内考生不得离场。'
|
||||
},
|
||||
{
|
||||
value:
|
||||
'考试期间考生可携带物品:签字笔、铅笔、橡皮、二十四色彩笔(包含黑色、蓝色、棕色、绿色、灰色、橙色、粉色、紫色、红色、黄色)铅笔盒、塑料瓶装水、药品、纸巾和准考证。'
|
||||
}
|
||||
],
|
||||
// 选取试卷时的表头
|
||||
paperColumn: [
|
||||
{
|
||||
prop: 'title',
|
||||
label: '考试名称',
|
||||
'min-width': 125,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'examStartTime',
|
||||
label: '开始时间',
|
||||
'min-width': 130,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'examEndTime',
|
||||
label: '结束时间',
|
||||
'min-width': 130,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'totalScore',
|
||||
label: '考试总分',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'passScore',
|
||||
label: '及格分',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'gradedCount',
|
||||
label: '已评分人数',
|
||||
'min-width': 90,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'ungradedCount',
|
||||
label: '未评分人数',
|
||||
'min-width': 90,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'action',
|
||||
label: '操作',
|
||||
// width: 350,
|
||||
width: 120,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
}
|
||||
]
|
||||
}),
|
||||
created () {
|
||||
const query = this.$route.query
|
||||
this.query = query
|
||||
this.paperId = +this.$route.params.paperId
|
||||
// 判断是否为考试试卷模式
|
||||
if (query.examIdPaperId) {
|
||||
this.printType = 4
|
||||
this.changeType(4)
|
||||
return
|
||||
}
|
||||
// 如果没有试卷信息则跳转选择
|
||||
if (!this.paperId) {
|
||||
this.$router.push('/assessment-evaluation/examination-paper-manage')
|
||||
}
|
||||
this.printType = 1
|
||||
this.changeType(1)
|
||||
this.getPaperInfo(this.paperId)
|
||||
},
|
||||
mounted () {},
|
||||
methods: {
|
||||
/**
|
||||
* 获取试卷信息
|
||||
* @param {number} id
|
||||
* @returns
|
||||
*/
|
||||
async getPaperInfo (id) {
|
||||
const { data } = await getPaperInfoByIdApi(id)
|
||||
this.questions = data.questionInfo.fixdQuestions.concat(
|
||||
data.questionInfo.randQuestions
|
||||
)
|
||||
this.paperInfo = data
|
||||
this.questionsClssify = this.formatQuestions(this.questions)
|
||||
console.log(this.questions, data, this.questionsClssify, '1123123123')
|
||||
},
|
||||
/**
|
||||
* 编辑考生信息或考生须知
|
||||
* @returns
|
||||
*/
|
||||
toEditorInfo (key) {
|
||||
this.editorInfo = {
|
||||
name: key,
|
||||
key: key === '考生信息填写' ? 'examineeInfo' : 'notice',
|
||||
max: key === '考生信息填写' ? 5 : 5
|
||||
}
|
||||
this.$set(this.editorInfo, 'data', this[this.editorInfo.key])
|
||||
console.log(this.editorInfo, '111111')
|
||||
},
|
||||
/**
|
||||
* 调用打印接口
|
||||
* @returns
|
||||
*/
|
||||
toPrint (exportInfo) {
|
||||
this.$html2Pdf('paperView', exportInfo)
|
||||
},
|
||||
/**
|
||||
* 切换打印模式
|
||||
* @param {number} e
|
||||
* @returns
|
||||
*/
|
||||
changeType (e) {
|
||||
// 如果当前为第四打印模式 需要判断是否已经选中了考生答过的试卷
|
||||
if (e === 4 && !this.query.examIdPaperId) {
|
||||
this.isCheckExam = true
|
||||
return
|
||||
} else if (e === 4 && this.query.examIdPaperId) {
|
||||
getOnlineExamResultApi(this.query.examIdPaperId)
|
||||
.then((res) => {
|
||||
this.paperInfo = res.data.exam
|
||||
this.questions = res.data.questions
|
||||
this.questionsClssify = this.formatQuestions(this.questions)
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
// 如果非第四打印模式并且未选中试卷 则去试卷页面选择
|
||||
} else if (!this.paperId) {
|
||||
this.$router.push('/assessment-evaluation/examination-paper-manage')
|
||||
} else if (this.paperId) {
|
||||
this.getPaperInfo(this.paperId)
|
||||
}
|
||||
this.isCheckExam = false
|
||||
this.checkTypeInfo = this.printData.find((item) => {
|
||||
return item.id === e
|
||||
})
|
||||
this.mode = this.checkTypeInfo.mode
|
||||
},
|
||||
// 处理试题信息
|
||||
formatQuestions (questionList) {
|
||||
const resQ = questionList.reduce(
|
||||
(prev, item) => {
|
||||
const index = prev.classify.findIndex((j) => j.title === item.type)
|
||||
if (index === -1) {
|
||||
prev.classify.push({
|
||||
title: item.type,
|
||||
score: item.itemScore || item.score || 0,
|
||||
userScore:
|
||||
typeof item.userScore !== 'undefined'
|
||||
? item.userScore
|
||||
: undefined
|
||||
})
|
||||
} else {
|
||||
prev.classify[index].score += item.itemScore || item.score || 0
|
||||
if (typeof item.userScore !== 'undefined') {
|
||||
prev.classify[index].userScore += item.userScore
|
||||
}
|
||||
// console.log(item.userScore)
|
||||
}
|
||||
if (Object.keys(prev.questions).includes(item.type)) {
|
||||
prev.questions[item.type].push(item)
|
||||
} else prev.questions[item.type] = [item]
|
||||
return prev
|
||||
},
|
||||
{ classify: [], questions: {} }
|
||||
)
|
||||
return resQ
|
||||
},
|
||||
// 删除编辑的信息
|
||||
delEditorInfo (index) {
|
||||
this.editorInfo.data.splice(index, 1)
|
||||
},
|
||||
addEditorInfo () {
|
||||
const { max, data } = this.editorInfo
|
||||
|
||||
if (max && max <= data.length) {
|
||||
this.$message.error('最多添加五条信息')
|
||||
return
|
||||
}
|
||||
|
||||
data.push({ value: '' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.print-paper {
|
||||
display: flex;
|
||||
::v-deep .question-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
> div {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.print-toolbar {
|
||||
max-width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#paperView {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding-right: 10mm;
|
||||
overflow: hidden;
|
||||
}
|
||||
.paper-perview {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.seal {
|
||||
height: 297mm;
|
||||
// width: 80px;
|
||||
max-width: 50px;
|
||||
min-width: 50px;
|
||||
position: relative;
|
||||
border-right: 1px dashed #464646;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.seal-title {
|
||||
position: absolute;
|
||||
right: -7.5px;
|
||||
height: 100%;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
> div {
|
||||
transform: rotate(-90deg);
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
.examinee-info {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
padding-top: 20px;
|
||||
justify-content: space-around;
|
||||
> p {
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: left top;
|
||||
}
|
||||
}
|
||||
.flex-center {
|
||||
align-items: center;
|
||||
}
|
||||
.form-item {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
// align-items: center;
|
||||
font-size: 15px;
|
||||
.form-item-label {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
// width: 110px;
|
||||
}
|
||||
.form-item-content {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.exam-base-explain {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.exam-base-notice {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
.notice-content,
|
||||
.notice-title {
|
||||
border: 1px solid #aeaeae;
|
||||
}
|
||||
.notice-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100px;
|
||||
font-size: 25px;
|
||||
padding: 14px 24px;
|
||||
border-right: none;
|
||||
}
|
||||
.notice-content {
|
||||
padding: 5px;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
color: black;
|
||||
p {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.exam-question-type-score {
|
||||
display: flex;
|
||||
// width: 100%;
|
||||
justify-content: center;
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
.type-score-item {
|
||||
width: 100px;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.editorInfo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<TableLayout
|
||||
:column="column"
|
||||
:data="table_data"
|
||||
:pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })"
|
||||
@size-change="(e) => pagingChange({ pageSize: e })"
|
||||
>
|
||||
<template v-slot:examStartTime="props">
|
||||
<div>
|
||||
<p>开始时间:{{ props.row.examStartTime }}</p>
|
||||
<p>结束时间:{{ props.row.examEndTime }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:passPercent="props">
|
||||
<p>
|
||||
{{ Math.round((props.row.passPercent * props.row.totalScore) / 100) }}
|
||||
</p>
|
||||
</template>
|
||||
<template v-slot:myDuration="props">
|
||||
<p>{{ formatTime(props.row.myDuration) }}</p>
|
||||
</template>
|
||||
<template v-slot:status="props">
|
||||
<div
|
||||
:style="{
|
||||
'background-color':
|
||||
props.row.examTimes - props.row.myTimes <= 0
|
||||
? '#999999'
|
||||
: ['#10a6b4', '#01c883', '#999999'][props.row.status],
|
||||
}"
|
||||
class="exam-status"
|
||||
>
|
||||
<span v-if="props.row.examTimes - props.row.myTimes <= 0">
|
||||
已结束
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ ["待开考", "已开始", "已结束"][props.row.status] }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:action="props">
|
||||
<!-- <el-button type="text" icon="el-icon-edit">{{ props.row.type }}</el-button> -->
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-fbks-yulan"
|
||||
@click="toPrint(props.row)"
|
||||
v-if="
|
||||
props.row.status == 2 || props.row.examTimes - props.row.myTimes <= 0
|
||||
"
|
||||
>打印
|
||||
</el-button>
|
||||
</template>
|
||||
</TableLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
pagingOnlineExamListApi,
|
||||
getLastedHistoryApi
|
||||
} from '@/api/assessment-evaluation/onlineTest'
|
||||
import { TableLayout } from '@/components/layout'
|
||||
export default {
|
||||
components: { TableLayout },
|
||||
data () {
|
||||
return {
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
column: []
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.pagingFindList()
|
||||
},
|
||||
created () {
|
||||
this.pagingFindList()
|
||||
this.column = [
|
||||
{
|
||||
prop: 'title',
|
||||
label: '考试名称',
|
||||
'min-width': 130,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'classify',
|
||||
label: '考试分类',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'totalScore',
|
||||
label: '总分',
|
||||
'min-width': 60,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'passPercent',
|
||||
label: '及格分',
|
||||
'min-width': 60,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'myScore',
|
||||
label: '成绩',
|
||||
'min-width': 50,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'myDuration',
|
||||
label: '考试用时',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'mistakes',
|
||||
label: '错题',
|
||||
'min-width': 50,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'examStartTime',
|
||||
label: '考试时间',
|
||||
'min-width': 220,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'action',
|
||||
label: '操作',
|
||||
// width: 350,
|
||||
width: 200,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
},
|
||||
methods: {
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
this.paper_list_loading = true
|
||||
pagingOnlineExamListApi({ ...this.page_info, ...this.form })
|
||||
.then((res) => {
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
pagingChange (event) {
|
||||
if (
|
||||
typeof event.currentPage !== 'number' &&
|
||||
typeof event.pageSize !== 'number'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
toExamHandler (type = 2, row) {
|
||||
if (type === 2) {
|
||||
this.is_show_info = true
|
||||
} else if (type === 1) {
|
||||
this.is_show_info = false
|
||||
}
|
||||
this.current_exam = row
|
||||
this.exam_info_dialog_show = true
|
||||
},
|
||||
/*
|
||||
初始化分页数据
|
||||
*/
|
||||
initPageInfo () {
|
||||
this.page_info = {
|
||||
currentPage: 1,
|
||||
pageSize: this.page_info.pageSize,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
formatTime (msTime) {
|
||||
const time = msTime / 1000
|
||||
let hour = Math.floor(time / 60 / 60)
|
||||
hour = hour.toString().padStart(2, '0')
|
||||
let minute = Math.floor(time / 60) % 60
|
||||
minute = minute.toString().padStart(2, '0')
|
||||
let second = Math.floor(time) % 60
|
||||
second = second.toString().padStart(2, '0')
|
||||
return `${hour}:${minute}:${second}`
|
||||
},
|
||||
/*
|
||||
获取最后一次考试的考试记录
|
||||
*/
|
||||
lookLastExam (row) {
|
||||
this.paper_list_loading = true
|
||||
getLastedHistoryApi(row.id)
|
||||
.then((res) => {
|
||||
if (res.data == null) {
|
||||
return this.$message.error('该场考试没有查询到您的考试记录')
|
||||
}
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/online-test/begin-online-exam/' +
|
||||
res.data.id,
|
||||
query: { preview: 1 }
|
||||
})
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
toPrint (row) {
|
||||
console.log(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.exam-status {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div class="SelectExamPaper">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -0,0 +1,575 @@
|
||||
<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
|
||||
<template>
|
||||
<div class="v-page classify_page">
|
||||
<SearchTreeMenu
|
||||
ref="treeMenuRef"
|
||||
title="试卷分类列表"
|
||||
:tree-data="paper_classify_list"
|
||||
@current-change="change"
|
||||
@onCreate="toCreateClassify"
|
||||
@onDelete="toDeleteclassify"
|
||||
v-loading="classify_loading"
|
||||
@onEdit="toEditClassify"
|
||||
/>
|
||||
<div class="v-ctx" v-loading="paper_list_loading">
|
||||
<FormLayout
|
||||
ref="formLayoutRef"
|
||||
:items="formItems"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="70px"
|
||||
label-position="right"
|
||||
:inline="true"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<!-- <el-input placeholder="请输入查询内容" v-model="form.title" class="input-with-select" clearable>
|
||||
<el-button slot="append" @click="pagingFindList">查询</el-button>
|
||||
</el-input> -->
|
||||
<QueryInput v-model="form.title" @query="pagingFindList"></QueryInput>
|
||||
</template>
|
||||
<template v-slot:btns>
|
||||
<el-button type="danger" @click="deletePapers" round
|
||||
>删除选中</el-button
|
||||
>
|
||||
<!-- <el-button type="primary" round @click="$message('开发中')">批量下载</el-button> -->
|
||||
<el-button type="primary" round plain @click="batchMoveToClassify"
|
||||
>移动到分类</el-button
|
||||
>
|
||||
<el-button type="primary" @click="addPaperHandler" round
|
||||
>添加试卷</el-button
|
||||
>
|
||||
<!-- icon="el-icon-plus" -->
|
||||
</template>
|
||||
</FormLayout>
|
||||
<TableLayout
|
||||
:column="column"
|
||||
:data="table_data"
|
||||
:pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })"
|
||||
@size-change="(e) => pagingChange({ pageSize: e })"
|
||||
selection
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template v-slot:action="props">
|
||||
<!-- 用户是管理员或用户是创建人才可以编辑,删除试卷 -->
|
||||
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-ksap-bianji2"
|
||||
@click="editPaper(props.row)"
|
||||
v-if="
|
||||
props.row.share === 1 ||
|
||||
$store.user.baseRole === 1 ||
|
||||
$store.user.id === props.row.creatorId
|
||||
"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-ksap-fuzhi"
|
||||
@click="copyPaper(props.row)"
|
||||
>复制</el-button
|
||||
>
|
||||
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-fbks-yulan"
|
||||
@click="previewPaper(props.row)"
|
||||
>预览</el-button
|
||||
>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-printer"
|
||||
class="print-icon"
|
||||
@click="
|
||||
$router.push(
|
||||
'/print-paper/' + props.row.id
|
||||
)
|
||||
"
|
||||
>打印</el-button
|
||||
>
|
||||
<!-- <el-button type="text" icon="el-icon-download" >下载</el-button> -->
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-ksap-shanchu"
|
||||
@click="deletePapers($event, props.row)"
|
||||
style="color: #f04343 !important"
|
||||
v-if="
|
||||
props.row.share === 1 ||
|
||||
$store.user.baseRole === 1 ||
|
||||
$store.user.id === props.row.creatorId
|
||||
"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
<template v-slot:isOpen="props">
|
||||
<el-switch
|
||||
v-model="props.row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
style="margin-left: 10px"
|
||||
@change="changeItemStatus($event, props.row)"
|
||||
>
|
||||
</el-switch>
|
||||
</template>
|
||||
</TableLayout>
|
||||
</div>
|
||||
<DialogLayout
|
||||
title="选择分类"
|
||||
width="280px"
|
||||
:visible="select_classify_dialog_is_show"
|
||||
@onCancel="
|
||||
(_) => {
|
||||
current_selected_classify_id = null;
|
||||
select_classify_dialog_is_show = false;
|
||||
}
|
||||
"
|
||||
@onConfirm="onMoveConfirm"
|
||||
>
|
||||
<PaperClassifySelector
|
||||
v-model="current_selected_classify_id"
|
||||
:mountedLoad="true"
|
||||
></PaperClassifySelector>
|
||||
</DialogLayout>
|
||||
<DialogLayout
|
||||
title="预览"
|
||||
width="600px"
|
||||
:visible="preview_paper_dialog_show"
|
||||
@onCancel="preview_paper_dialog_show = false"
|
||||
:actionBarOption="{ confirmTxt: '打印' }"
|
||||
@onConfirm="printx"
|
||||
>
|
||||
<ExercisePerviewPaper
|
||||
:questions="preview_current_questions_list"
|
||||
:title="current_select_paper?.title"
|
||||
:mode="2"
|
||||
ref="printRef"
|
||||
></ExercisePerviewPaper>
|
||||
</DialogLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
SearchTreeMenu,
|
||||
TableLayout,
|
||||
FormLayout,
|
||||
DialogLayout
|
||||
} from '@/components/layout'
|
||||
|
||||
import {
|
||||
findAllPaperClassifyApi,
|
||||
createPaperClassifyApi,
|
||||
deletePaperClassifyApi,
|
||||
pagingFindPaperApi,
|
||||
deleteSomePapersApi,
|
||||
patchPaperApi,
|
||||
copyPaperInfoByIdApi,
|
||||
batchMovePaperClassify,
|
||||
editPaperClassifyApi,
|
||||
checkQuoteApi
|
||||
} from '@/api/assessment-evaluation/paper'
|
||||
import { getQuestionsByPaperApi } from '@/api/assessment-evaluation/exam'
|
||||
import QueryInput from '@/components/widget/QueryInput.vue'
|
||||
import PaperClassifySelector from './add-modify-paper/components/PaperClassifySelector.vue'
|
||||
// import QuestionListPreview from '@/views/assessment-evaluation/examination-paper-manage/add-modify-paper/components/QuestionListPreview.vue'
|
||||
import ExercisePerviewPaper from '@/views/statistic/components/ExercisePerviewPaper.vue'
|
||||
export default {
|
||||
components: {
|
||||
SearchTreeMenu,
|
||||
TableLayout,
|
||||
FormLayout,
|
||||
QueryInput,
|
||||
DialogLayout,
|
||||
PaperClassifySelector,
|
||||
ExercisePerviewPaper
|
||||
},
|
||||
data: () => ({
|
||||
paper_classify_list: [],
|
||||
table_data: [],
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
current_classify_id: null,
|
||||
// 试题分类树是否加载中
|
||||
classify_loading: false,
|
||||
paper_list_loading: false,
|
||||
form: { title: '' },
|
||||
// 表格当前选中项
|
||||
table_selections_list: [],
|
||||
current_selected_classify_id: null,
|
||||
select_classify_dialog_is_show: false,
|
||||
preview_current_questions_list: [],
|
||||
// 预览弹窗是否显示
|
||||
preview_paper_dialog_show: false,
|
||||
// 当前选中的试卷
|
||||
current_select_paper: null
|
||||
}),
|
||||
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'title' },
|
||||
{ prop: 'btns', model: { class: 'gy-btns' } }
|
||||
]
|
||||
this.rules = {
|
||||
// name: { required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
}
|
||||
this.column = [
|
||||
{
|
||||
prop: 'title',
|
||||
label: '试卷名',
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'classify',
|
||||
label: '试卷分类',
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'totalScore',
|
||||
label: '总分',
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'questionCount',
|
||||
label: '试题总数',
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'creator',
|
||||
label: '创建人',
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建时间',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{ prop: 'action', label: '操作', width: 290, align: 'center' },
|
||||
{
|
||||
prop: 'isOpen',
|
||||
label: '是否开启',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
}
|
||||
]
|
||||
|
||||
this.findAllQuestionsClassify()
|
||||
},
|
||||
activated () {
|
||||
this.pagingFindList()
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 调用打印,可导出pdf
|
||||
*/
|
||||
printx () {
|
||||
if (window.printElement) {
|
||||
window.printElement.remove()
|
||||
}
|
||||
window.printElement = document.createElement('iframe')
|
||||
window.printElement.style = 'display:none;'
|
||||
window.printElement.onload = () => {
|
||||
const printBody = window.printElement.contentWindow.document.body
|
||||
printBody.appendChild(document.head.cloneNode(1))
|
||||
printBody.style = '-webkit-print-color-adjust:exact;background:#fff;'
|
||||
printBody.appendChild(this.$refs.printRef.$el.cloneNode(1))
|
||||
window.printElement.contentWindow.print()
|
||||
}
|
||||
document.body.appendChild(window.printElement)
|
||||
},
|
||||
/**
|
||||
* 预览试卷,获取试题和试卷相关信息
|
||||
*/
|
||||
previewPaper (row) {
|
||||
this.current_select_paper = row
|
||||
this.page_is_loading = true
|
||||
getQuestionsByPaperApi(row.id)
|
||||
.then((res) => {
|
||||
this.preview_current_questions_list = res.data
|
||||
this.preview_paper_dialog_show = true
|
||||
})
|
||||
.finally((_) => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 修改分类
|
||||
*/
|
||||
toEditClassify (item) {
|
||||
if (item.id === 'system') return this.$message.error('根分类不能修改')
|
||||
this.$prompt('请输入新分类名:', '编辑', {
|
||||
inputPattern: /.{1,}/,
|
||||
inputErrorMessage: '请输入分类名'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
this.classify_loading = true
|
||||
editPaperClassifyApi({ ...item, name: value })
|
||||
.then((res) => {
|
||||
this.$message.success('修改成功')
|
||||
this.findAllQuestionsClassify()
|
||||
})
|
||||
.finally((_) => {
|
||||
this.classify_loading = false
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
},
|
||||
/**
|
||||
* 确定移动到分类
|
||||
*/
|
||||
onMoveConfirm () {
|
||||
const ids = this.table_selections_list.map((item) => item.id)
|
||||
const classifyId = this.current_selected_classify_id
|
||||
this.paper_list_loading = true
|
||||
batchMovePaperClassify({ ids, classifyId })
|
||||
.then((res) => {
|
||||
this.pagingFindList()
|
||||
this.select_classify_dialog_is_show = false
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 批量移动到分类
|
||||
*/
|
||||
batchMoveToClassify () {
|
||||
if (this.table_selections_list.length === 0) {
|
||||
return this.$message.error('请先选择要移动的试卷')
|
||||
}
|
||||
this.select_classify_dialog_is_show = true
|
||||
},
|
||||
/**
|
||||
* 复制试卷
|
||||
*/
|
||||
copyPaper (row) {
|
||||
this.$prompt(`复制试卷【${row.title}】并重命名为:`, '重命名', {
|
||||
inputPattern: /.{1,}/,
|
||||
inputErrorMessage: '请填写新试卷名'
|
||||
})
|
||||
.then(({ value }) => {
|
||||
this.paper_list_loading = true
|
||||
copyPaperInfoByIdApi({ id: row.id, title: value })
|
||||
.then((res) => {
|
||||
this.pagingFindList()
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
})
|
||||
.catch((_) => {})
|
||||
},
|
||||
/**
|
||||
* 修改试卷是否开启
|
||||
*/
|
||||
changeItemStatus (status, paperItem) {
|
||||
const form = {
|
||||
id: paperItem.id,
|
||||
status
|
||||
}
|
||||
patchPaperApi(form).then((res) => {
|
||||
if (res.code !== 0) {
|
||||
paperItem.status = status === 1 ? 0 : 1
|
||||
this.$message.error('修改失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 编辑试卷
|
||||
*/
|
||||
async editPaper (row) {
|
||||
const { data } = await checkQuoteApi(row.id)
|
||||
if (data.length > 0) {
|
||||
const text = data.map((item) => item.title)
|
||||
this.$confirm(
|
||||
`当前试卷有考试(${text.join(',')})已经引用, 您确定要修改内容吗?`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
.then((_) => {
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/examination-paper-manage/add-modify-paper/' +
|
||||
row.id
|
||||
})
|
||||
})
|
||||
.catch((_) => {})
|
||||
} else {
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/examination-paper-manage/add-modify-paper/' +
|
||||
row.id
|
||||
})
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 点击添加试卷的回调
|
||||
*/
|
||||
addPaperHandler () {
|
||||
this.$router.push({
|
||||
path: '/assessment-evaluation/examination-paper-manage/add-modify-paper'
|
||||
})
|
||||
},
|
||||
handleSelectionChange (e) {
|
||||
this.table_selections_list = e
|
||||
},
|
||||
/**
|
||||
* 分页切换
|
||||
*/
|
||||
pagingChange (event) {
|
||||
if (
|
||||
typeof event.currentPage !== 'number' &&
|
||||
typeof event.pageSize !== 'number'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
initPageInfo () {
|
||||
this.page_info = {
|
||||
currentPage: 1,
|
||||
pageSize: this.page_info.pageSize,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 分页获取试卷信息
|
||||
*/
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
let classifyId = this.current_classify_id
|
||||
if (classifyId === 'system') classifyId = null
|
||||
this.paper_list_loading = true
|
||||
pagingFindPaperApi({ ...this.page_info, classifyId, ...this.form })
|
||||
.then((res) => {
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
async findAllQuestionsClassify () {
|
||||
const { data } = await findAllPaperClassifyApi()
|
||||
const list = [
|
||||
{ id: 'system', name: '试卷分类', disabled: true, children: [] }
|
||||
]
|
||||
this.questionClassifyMap = {}
|
||||
this.questionClassifyMap = data.toTree()
|
||||
|
||||
list[0].children = this.questionClassifyMap.tree
|
||||
this.paper_classify_list = list
|
||||
},
|
||||
|
||||
change (classify) {
|
||||
this.current_classify_id = classify.id
|
||||
this.pagingFindList()
|
||||
},
|
||||
|
||||
toCreateClassify (node) {
|
||||
node?.id === 'system' && (node = null)
|
||||
this.$prompt('请输入分类名', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
.then(async ({ value }) => {
|
||||
const data = {
|
||||
name: value,
|
||||
pid: node == null ? 0 : node.id,
|
||||
level: node == null ? 1 : node.level + 1
|
||||
}
|
||||
await createPaperClassifyApi(data)
|
||||
this.findAllQuestionsClassify()
|
||||
})
|
||||
.catch((_) => {})
|
||||
// this.$refs.treeMenuRef.setCurrentKey(node)
|
||||
// this.active_classify = { name: '', desc: '', base: null, features: [] }
|
||||
},
|
||||
// 批量删除、单个删除
|
||||
deletePapers (e, questionItem) {
|
||||
if (this.table_selections_list.length === 0 && questionItem == null) {
|
||||
this.$message.error('请先选择要删除的项目')
|
||||
} else {
|
||||
let delList = this.table_selections_list
|
||||
if (questionItem) {
|
||||
delList = [questionItem]
|
||||
}
|
||||
this.$confirm('确认删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
})
|
||||
.then((res) => {
|
||||
this.paper_list_loading = true
|
||||
deleteSomePapersApi(delList.map((_) => _.id)).then((res) => {
|
||||
this.pagingFindList()
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
})
|
||||
.catch((_) => {})
|
||||
}
|
||||
},
|
||||
toDeleteclassify (classify) {
|
||||
if (classify.id === 'system') return
|
||||
new Promise((resolve, reject) => {
|
||||
this.$confirm(
|
||||
'此操作将永久删除此分类,及其子级分类,请确认后删除?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
}
|
||||
)
|
||||
.then((res) => resolve())
|
||||
.catch((_) => reject(_))
|
||||
})
|
||||
.then((_) => {
|
||||
this.classify_loading = true
|
||||
deletePaperClassifyApi(classify.id).finally(() => {
|
||||
this.findAllQuestionsClassify()
|
||||
this.$refs.treeMenuRef.setCurrentKey()
|
||||
this.classify_loading = false
|
||||
})
|
||||
})
|
||||
.catch((_) => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" >
|
||||
.print-icon span {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.classify_page .form_layout {
|
||||
margin-top: 16px;
|
||||
|
||||
.suf {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.el-form-item:last-child {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,338 @@
|
||||
<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
|
||||
<template>
|
||||
<div class="v-page classify_page">
|
||||
<div class="v-ctx" v-loading="paper_list_loading">
|
||||
<FormLayout
|
||||
ref="formLayoutRef"
|
||||
:items="formItems"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="70px"
|
||||
label-position="right"
|
||||
:inline="true"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<QueryInput v-model="form.title" @query="pagingFindList"></QueryInput>
|
||||
</template>
|
||||
<template v-slot:btns> </template>
|
||||
</FormLayout>
|
||||
<TableLayout
|
||||
:column="column"
|
||||
:data="table_data"
|
||||
:pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })"
|
||||
@size-change="(e) => pagingChange({ pageSize: e })"
|
||||
>
|
||||
<template v-slot:action="props">
|
||||
<el-button
|
||||
type="text"
|
||||
class="print-icon"
|
||||
:icon="
|
||||
props.row.isGradePaper == 1
|
||||
? 'el-icon-refresh'
|
||||
: 'el-icon-finished'
|
||||
"
|
||||
@click="toGradePaper(props.row)"
|
||||
>{{ props.row.isGradePaper == 1 ? "重新" : "开始" }}判分
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
class="print-icon"
|
||||
icon="el-icon-download"
|
||||
v-if="props.row.isGradePaper == 1"
|
||||
@click="previewPaper(props.row)"
|
||||
>下载
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-printer"
|
||||
class="print-icon"
|
||||
v-if="props.row.isGradePaper == 1"
|
||||
@click="printPaper(props.row)"
|
||||
>打印
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-slot:realname>
|
||||
<p>保密</p>
|
||||
</template>
|
||||
<template v-slot:myDuration="props">
|
||||
<p>{{ formatTime(props.row.myDuration) }}</p>
|
||||
</template>
|
||||
<template v-slot:isGradePaper="props">
|
||||
<p>{{ ["未判卷", "已判卷"][props.row.isGradePaper] }}</p>
|
||||
</template>
|
||||
<template v-slot:passScore="props">
|
||||
<p>{{ +props.row.score >= +props.row.passScore ? "是" : "否" }}</p>
|
||||
</template>
|
||||
</TableLayout>
|
||||
<DialogLayout
|
||||
title="预览"
|
||||
width="600px"
|
||||
:visible="preview_paper_dialog_show"
|
||||
@onCancel="preview_paper_dialog_show = false"
|
||||
:actionBarOption="{ confirmTxt: '打印' }"
|
||||
@onConfirm="printx"
|
||||
>
|
||||
<ExercisePerviewPaper
|
||||
:questions="preview_current_questions_list"
|
||||
:title="current_select_paper?.title"
|
||||
:mode="1"
|
||||
ref="printRef"
|
||||
></ExercisePerviewPaper>
|
||||
</DialogLayout>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TableLayout, FormLayout, DialogLayout } from '@/components/layout'
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import { pagingGradeDetailsApi } from '@/api/assessment-evaluation/humanEval.js'
|
||||
import QueryInput from '@/components/widget/QueryInput.vue'
|
||||
import ExercisePerviewPaper from '@/views/statistic/components/ExercisePerviewPaper.vue'
|
||||
import { getOnlineExamResultApi } from '@/api/assessment-evaluation/onlineTest'
|
||||
export default {
|
||||
components: {
|
||||
TableLayout,
|
||||
FormLayout,
|
||||
QueryInput,
|
||||
ExercisePerviewPaper,
|
||||
DialogLayout
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
table_data: [],
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
// 试题分类树是否加载中
|
||||
paper_list_loading: false,
|
||||
form: { title: '', isPractive: 1 },
|
||||
is_show_info: false,
|
||||
exam_info_dialog_show: false,
|
||||
current_exam: null,
|
||||
examId: null,
|
||||
preview_paper_dialog_show: false,
|
||||
current_select_paper: null,
|
||||
preview_current_questions_list: []
|
||||
}),
|
||||
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'title' },
|
||||
{ prop: 'btns', model: { class: 'gy-btns' } }
|
||||
]
|
||||
this.column = [
|
||||
{
|
||||
prop: 'username',
|
||||
label: '账号',
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'realname',
|
||||
label: '用户名',
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '开始时间',
|
||||
'min-width': 150,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'reportTime',
|
||||
label: '交卷时间',
|
||||
'min-width': 150,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'myDuration',
|
||||
label: '答题时长',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'score',
|
||||
label: '分数',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
// { prop: 'shouldCount', label: '应交试卷', 'min-width': 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{
|
||||
prop: 'passScore',
|
||||
label: '及格',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'isGradePaper',
|
||||
label: '判卷记录',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'action',
|
||||
label: '操作',
|
||||
// width: 350,
|
||||
width: 180,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
},
|
||||
activated () {
|
||||
this.examId = this.$route.params.examId
|
||||
this.pagingFindList()
|
||||
},
|
||||
methods: {
|
||||
// 当用户点击“下载”按钮时调用的方法。
|
||||
previewPaper (row) {
|
||||
this.paper_list_loading = true
|
||||
getOnlineExamResultApi(row.historyId)
|
||||
.then((res) => {
|
||||
this.current_select_paper = res.data.exam
|
||||
this.preview_current_questions_list = res.data.questions
|
||||
this.preview_paper_dialog_show = true
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
// 当用户单击“打印”按钮时调用的方法。
|
||||
printx () {
|
||||
if (window.printElement) {
|
||||
window.printElement.remove()
|
||||
}
|
||||
window.printElement = document.createElement('iframe')
|
||||
window.printElement.style = 'display:none;'
|
||||
window.printElement.onload = () => {
|
||||
const printBody = window.printElement.contentWindow.document.body
|
||||
printBody.appendChild(document.head.cloneNode(1))
|
||||
printBody.style = '-webkit-print-color-adjust:exact;background:#fff;'
|
||||
printBody.appendChild(this.$refs.printRef.$el.cloneNode(1))
|
||||
window.printElement.contentWindow.print()
|
||||
}
|
||||
document.body.appendChild(window.printElement)
|
||||
},
|
||||
printPaper (row) {
|
||||
this.$router.push({
|
||||
path: '/print-paper/' + row.paperId,
|
||||
query: { examIdPaperId: row.historyId }
|
||||
})
|
||||
},
|
||||
// 当用户点击“开始判分”按钮时调用的方法。
|
||||
toGradePaper (row) {
|
||||
if (+row.isOver === 0) {
|
||||
this.$confirm(
|
||||
'本场考试还没有结束,学员可重新考试,是否要进行判卷?',
|
||||
'提示',
|
||||
{ type: 'info' }
|
||||
)
|
||||
.then((res) => {
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/human-evaluation/begin-eval/' +
|
||||
row.historyId,
|
||||
query: { preview: 3 }
|
||||
})
|
||||
})
|
||||
.catch((_) => {})
|
||||
} else {
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/human-evaluation/begin-eval/' +
|
||||
row.historyId,
|
||||
query: { preview: 3 }
|
||||
})
|
||||
}
|
||||
},
|
||||
// 将毫秒数转换为“HH:MM:SS”形式的字符串。
|
||||
formatTime (msTime) {
|
||||
const time = msTime / 1000
|
||||
let hour = Math.floor(time / 60 / 60)
|
||||
hour = hour.toString().padStart(2, '0')
|
||||
let minute = Math.floor(time / 60) % 60
|
||||
minute = minute.toString().padStart(2, '0')
|
||||
let second = Math.floor(time) % 60
|
||||
second = second.toString().padStart(2, '0')
|
||||
return `${hour}:${minute}:${second}`
|
||||
},
|
||||
// 当用户单击页码或页面大小时调用的方法。
|
||||
pagingChange (event) {
|
||||
if (
|
||||
typeof event.currentPage !== 'number' &&
|
||||
typeof event.pageSize !== 'number'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
// 将 `page_info` 数据属性设置为新对象。
|
||||
initPageInfo () {
|
||||
this.page_info = {
|
||||
currentPage: 1,
|
||||
pageSize: this.page_info.pageSize,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
// 当用户单击页码或页面大小时调用的方法。
|
||||
pagingFindList (e) {
|
||||
if (!this.examId) return
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
this.paper_list_loading = true
|
||||
pagingGradeDetailsApi({
|
||||
...this.page_info,
|
||||
...this.form,
|
||||
id: this.examId
|
||||
})
|
||||
.then((res) => {
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.classify_page .form_layout {
|
||||
margin-top: 16px;
|
||||
|
||||
.suf {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.el-form-item:last-child {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.exam-status {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
287
front/src/views/assessment-evaluation/human-evaluation/index.vue
Normal file
287
front/src/views/assessment-evaluation/human-evaluation/index.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
|
||||
<template>
|
||||
<div class="v-page classify_page">
|
||||
<div class="v-ctx" v-loading="paper_list_loading">
|
||||
<FormLayout
|
||||
ref="formLayoutRef"
|
||||
:items="formItems"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="70px"
|
||||
label-position="right"
|
||||
:inline="true"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<QueryInput v-model="form.title" @query="pagingFindList"></QueryInput>
|
||||
</template>
|
||||
<template v-slot:year>
|
||||
<el-date-picker
|
||||
v-model="current_query_year"
|
||||
type="year"
|
||||
placeholder="选择年份"
|
||||
@change="pagingFindList"
|
||||
>
|
||||
</el-date-picker>
|
||||
</template>
|
||||
<template v-slot:btns> </template>
|
||||
</FormLayout>
|
||||
<TableLayout
|
||||
:column="column"
|
||||
:data="table_data"
|
||||
:pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })"
|
||||
@size-change="(e) => pagingChange({ pageSize: e })"
|
||||
>
|
||||
<template v-slot:status="props">
|
||||
<el-switch
|
||||
v-model="props.row.isAnonymous"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
style="margin-left: 10px"
|
||||
@change="changeItemIsAnonymous($event, props.row)"
|
||||
>
|
||||
</el-switch>
|
||||
</template>
|
||||
<template v-slot:gradeMode="props">
|
||||
<el-tag :type="props.row.gradeMode === 1 ? 'success' : 'info'">{{
|
||||
props.row.gradeMode === 1 ? "人工" : "机器"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
<template v-slot:action="props">
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-rgpj-xiangqing"
|
||||
@click="toDetails(props.row)"
|
||||
>查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</TableLayout>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TableLayout, FormLayout } from '@/components/layout'
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import {
|
||||
pagingGradePaperApi,
|
||||
patchIsAnonymousApi
|
||||
} from '@/api/assessment-evaluation/humanEval.js'
|
||||
|
||||
import QueryInput from '@/components/widget/QueryInput.vue'
|
||||
export default {
|
||||
components: {
|
||||
TableLayout,
|
||||
FormLayout,
|
||||
QueryInput
|
||||
},
|
||||
props: {
|
||||
column: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{
|
||||
prop: 'title',
|
||||
label: '考试名称',
|
||||
'min-width': 125,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'examStartTime',
|
||||
label: '开始时间',
|
||||
'min-width': 130,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'examEndTime',
|
||||
label: '结束时间',
|
||||
'min-width': 130,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'totalScore',
|
||||
label: '考试总分',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'passScore',
|
||||
label: '及格分',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
// { prop: 'shouldCount', label: '应交试卷', 'min-width': 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{
|
||||
prop: 'gradeMode',
|
||||
label: '判卷方式',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'gradedCount',
|
||||
label: '已评分人数',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'ungradedCount',
|
||||
label: '未评分人数',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '匿名判卷',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'action',
|
||||
label: '操作',
|
||||
// width: 350,
|
||||
width: 120,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
table_data: [],
|
||||
// 存储当前页码和页数的变量。
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
// 试题分类树是否加载中
|
||||
paper_list_loading: false,
|
||||
// 用于存储表单数据的变量。
|
||||
form: { title: '', isPractive: 1 },
|
||||
// 不曾用过。
|
||||
is_show_info: false,
|
||||
// 什么都不做。
|
||||
exam_info_dialog_show: false,
|
||||
// 什么都不做。
|
||||
current_exam: null,
|
||||
// 存储当前年份的变量。
|
||||
current_query_year: null
|
||||
}),
|
||||
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'title' },
|
||||
{ prop: 'year' },
|
||||
{ prop: 'btns', model: { class: 'gy-btns' } }
|
||||
]
|
||||
this.pagingFindList()
|
||||
},
|
||||
activated () {
|
||||
this.pagingFindList()
|
||||
},
|
||||
methods: {
|
||||
toDetails (row) {
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/human-evaluation/eval-details/' + row.examId
|
||||
})
|
||||
},
|
||||
// 修改是否为匿名评卷
|
||||
changeItemIsAnonymous (isAnonymous, row) {
|
||||
const form = {
|
||||
id: row.examId,
|
||||
isAnonymous
|
||||
}
|
||||
patchIsAnonymousApi(form).then((res) => {
|
||||
if (res.code !== 0) {
|
||||
paperItem.isAnonymous = isAnonymous === 1 ? 0 : 1
|
||||
this.$message.error('修改失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
// 它将以毫秒为单位的时间格式化为 `hh:mm:ss` 格式的字符串。
|
||||
formatTime (msTime) {
|
||||
const time = msTime / 1000
|
||||
let hour = Math.floor(time / 60 / 60)
|
||||
hour = hour.toString().padStart(2, '0')
|
||||
let minute = Math.floor(time / 60) % 60
|
||||
minute = minute.toString().padStart(2, '0')
|
||||
let second = Math.floor(time) % 60
|
||||
second = second.toString().padStart(2, '0')
|
||||
return `${hour}:${minute}:${second}`
|
||||
},
|
||||
// 页面更改时调用的方法。
|
||||
pagingChange (event) {
|
||||
if (
|
||||
typeof event.currentPage !== 'number' &&
|
||||
typeof event.pageSize !== 'number'
|
||||
) { return }
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
// 将 page_info 设置为默认值。
|
||||
initPageInfo () {
|
||||
this.page_info = {
|
||||
currentPage: 1,
|
||||
pageSize: this.page_info.pageSize,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
// 加载页面时调用的方法。
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
if (this.current_query_year) {
|
||||
this.form.year = this.current_query_year.format('yyyy-MM-dd hh:mm:ss')
|
||||
} else {
|
||||
this.form.year = null
|
||||
}
|
||||
this.paper_list_loading = true
|
||||
pagingGradePaperApi({ ...this.page_info, ...this.form })
|
||||
.then((res) => {
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.classify_page .form_layout {
|
||||
margin-top: 16px;
|
||||
.suf {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
.el-form-item:last-child {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.exam-status {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div class="aq-card-inner">
|
||||
<div class="gy-area-title">答题卡</div>
|
||||
|
||||
<div class="tips-card" v-if="![1,3].includes(mode)">
|
||||
<div class="tips-item"> 已作答 <div class="color-card success"></div>
|
||||
</div>
|
||||
<div class="tips-item" v-if="mode !== 5"> 标记 <div class="color-card flag"></div>
|
||||
</div>
|
||||
<div class="tips-item"> 未作答 <div class="color-card unwrite"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tips-card" v-else>
|
||||
<div class="tips-item"> 正确 <div class="color-card success"></div>
|
||||
</div>
|
||||
<div class="tips-item"> 错误 <div class="color-card flag"></div>
|
||||
</div>
|
||||
<!-- <div class="tips-item"> 未作答 <div class="color-card unwrite"></div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div v-if="(mode === 1)" style="display: flex;justify-content:space-between;margin: 10px 0;font-size:12px" >
|
||||
<div class="gy-label">试卷总分:{{ examScore }} 分</div>
|
||||
<div class="gy-label">您的得分:{{ userTotalScore }} 分</div>
|
||||
</div>
|
||||
<div style="overflow: auto;">
|
||||
<div v-for="(item, ind) in questionList?.classify" :key="ind">
|
||||
<p style="padding:10px 0;font-weight: bold;font-size: 14px;">
|
||||
{{ item.title }}
|
||||
<span style="font-size: 12px;font-weight: normal;display: inline-block;margin-left: 10px;color:#999">
|
||||
(共{{ questionList.questions[item.title].length }}题)
|
||||
</span>
|
||||
</p>
|
||||
<div class="serial-list" v-if="![1,3].includes(mode)">
|
||||
<div
|
||||
:class="['serial-item', { 'flag': flag.includes(questionItem.id), 'success': localAnswers.includes(questionItem.id) }]"
|
||||
v-for="(questionItem, ind) in questionList.questions[item.title]" @click="itemOnClick(questionItem)"
|
||||
:key="questionItem.id" :questionInfo="questionItem">{{ ind + 1 }}</div>
|
||||
</div>
|
||||
<div class="serial-list" v-else>
|
||||
<div
|
||||
:class="['serial-item', { 'error': questionItem.score > questionItem.userScore, 'success': questionItem.score <= questionItem.userScore }]"
|
||||
v-for="(questionItem, ind) in questionList.questions[item.title]" @click="itemOnClick(questionItem)"
|
||||
:key="questionItem.id" :questionInfo="questionItem">{{ ind + 1 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mode === 5">
|
||||
<div class="action-btn-group">
|
||||
<el-button round type="primary" @click="handler('submit')">提交答题</el-button>
|
||||
<el-button plain round type="danger" @click="handler('cancel')">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="(mode === 1)">
|
||||
<div class="action-btn-group">
|
||||
<el-button plain round type="danger" @click="handler('cancel')">退出预览</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
export default {
|
||||
props: {
|
||||
questions: {},
|
||||
flag: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
answers: {
|
||||
type: Object,
|
||||
default: _ => ({})
|
||||
},
|
||||
mode: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data: _ => ({
|
||||
// 已回答的问题 ID 数组。
|
||||
localAnswers: [],
|
||||
// 计算属性。
|
||||
questionList: null,
|
||||
// 用于计算考试的总分。
|
||||
originQuestionList: null
|
||||
}),
|
||||
computed: {
|
||||
// 用于计算考试总分的计算属性。
|
||||
examScore () {
|
||||
if (this.originQuestionList && this.originQuestionList.length > 0) {
|
||||
return this.originQuestionList.reduce((prev, item) => {
|
||||
return prev + item.score
|
||||
}, 0)
|
||||
} else {
|
||||
return '无'
|
||||
}
|
||||
},
|
||||
// 用于计算考试总分的计算属性。
|
||||
userTotalScore () {
|
||||
if (this.originQuestionList && this.originQuestionList.length > 0) {
|
||||
return this.originQuestionList.reduce((prev, item) => {
|
||||
return prev + item.userScore
|
||||
}, 0)
|
||||
} else {
|
||||
return '无'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 包含问题答案的对象。
|
||||
answers: {
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.localAnswers = nv.map(item => item.questionId)
|
||||
}
|
||||
},
|
||||
// 从父组件传入的 prop。
|
||||
questions: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
// const questionArr = Object.keys(this.questionList.questions).reduce((prev, item) => {
|
||||
// return prev.concat(this.questionList.questions[item])
|
||||
// }, [])
|
||||
if (nv) {
|
||||
this.questionList = this.formatQuestions(nv)
|
||||
this.originQuestionList = nv
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handler (emit) {
|
||||
this.$emit(emit)
|
||||
},
|
||||
// 当用户单击问题时调用的方法。
|
||||
itemOnClick (question) {
|
||||
this.$emit('smooth', question)
|
||||
},
|
||||
// 获取一组问题并返回一个具有两个属性的对象:`classify` 和 `questions`。
|
||||
formatQuestions (questionList) {
|
||||
// const questionsClassify = {}
|
||||
const resQ = questionList.reduce((prev, item) => {
|
||||
if (prev.classify.findIndex(j => j.title === item.type) === -1) prev.classify.push({ title: item.type, score: item.score })
|
||||
if (Object.keys(prev.questions).includes(item.type)) prev.questions[item.type].push(item)
|
||||
else prev.questions[item.type] = [item]
|
||||
return prev
|
||||
}, { classify: [], questions: {} })
|
||||
return resQ
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.serial-list {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.serial-item {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
background-color: #999999;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&.success {
|
||||
background-color: #00c781;
|
||||
}
|
||||
|
||||
&.flag,
|
||||
&.error {
|
||||
background-color: #ee0000;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.aq-card-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tips-card {
|
||||
width: 100%;
|
||||
padding: 10px 5px;
|
||||
border-radius: 4px;
|
||||
background-color: #f1f5fe;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-size: 12px;
|
||||
|
||||
.tips-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.color-card {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 0 5px;
|
||||
|
||||
&.success {
|
||||
background-color: #00c781;
|
||||
}
|
||||
|
||||
&.flag,
|
||||
&.error {
|
||||
background-color: #ee0000;
|
||||
}
|
||||
|
||||
&.unwrite {
|
||||
background-color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 整卷 -->
|
||||
<div v-if="paperStyle == 0">
|
||||
<div v-for="(item, ind) in questionList?.classify" :key="ind">
|
||||
<p
|
||||
class="ele-item answer-item"
|
||||
style="padding: 10px 0 20px 0; font-weight: bold; font-size: 14px"
|
||||
>
|
||||
第{{
|
||||
["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"][ind]
|
||||
}}部分:
|
||||
{{ item.title }}
|
||||
<span
|
||||
style="
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
"
|
||||
>
|
||||
(共{{ questionList.questions[item.title].length }}题<span
|
||||
v-if="(typeof item.score !== 'undefined')"
|
||||
>,每题{{ item.score }}分</span>
|
||||
)
|
||||
</span>
|
||||
<span
|
||||
v-if="isPrint && mode !== 4"
|
||||
style="
|
||||
margin-left: 10px;
|
||||
height: 25px;
|
||||
display: inline-block;
|
||||
border: 1px solid black;
|
||||
text-align: center;
|
||||
line-height: 23px;
|
||||
"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
border-right: 1px solid black;
|
||||
"
|
||||
>得分</span
|
||||
>
|
||||
<span style="width: 60px; display: inline-block">
|
||||
{{ item.userScore }}
|
||||
</span>
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
:style="{ 'margin-bottom': '10px' }"
|
||||
class="ele-item"
|
||||
v-for="(questionItem, ind) in questionList.questions[item.title]"
|
||||
:key="questionItem.id"
|
||||
>
|
||||
<QuestionItem
|
||||
:random="random"
|
||||
:questionInfo="questionItem"
|
||||
:serial="ind + 1"
|
||||
@answerChanged="userAnswerOnChange($event, questionItem)"
|
||||
@flag="toggleFlagList"
|
||||
:isFlag="flagList.includes(questionItem.id)"
|
||||
:mode="mode"
|
||||
@eval="questionItemHasEval"
|
||||
></QuestionItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 逐题模式 -->
|
||||
<div v-if="paperStyle == 1">
|
||||
<div>
|
||||
<p style="padding: 10px 0 20px 0; font-weight: bold; font-size: 14px">
|
||||
{{ questions[currentQuestionInd].type }}
|
||||
<span
|
||||
style="
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
"
|
||||
>
|
||||
(共{{
|
||||
questionList.questions[questions[currentQuestionInd].type].length
|
||||
}}题,<span v-if="questions[currentQuestionInd].score"
|
||||
>每题{{ questions[currentQuestionInd].score }}分</span
|
||||
>)
|
||||
</span>
|
||||
</p>
|
||||
<QuestionItem
|
||||
:questionInfo="questions[currentQuestionInd]"
|
||||
:key="questions[currentQuestionInd].id"
|
||||
:serial="currentQuestionInd + 1"
|
||||
@answerChanged="
|
||||
userAnswerOnChange($event, questions[currentQuestionInd])
|
||||
"
|
||||
@flag="toggleFlagList"
|
||||
:isFlag="flagList.includes(questions[currentQuestionInd].id)"
|
||||
:mode="mode"
|
||||
:answer="getAnswer(questions[currentQuestionInd].id)"
|
||||
></QuestionItem>
|
||||
</div>
|
||||
<div class="changer-btns">
|
||||
<div>
|
||||
<el-button
|
||||
size="medium"
|
||||
icon="el-icon-arrow-left"
|
||||
type="primary"
|
||||
circle
|
||||
:disabled="currentQuestionInd == 0"
|
||||
@click="changeQuestion(-1)"
|
||||
></el-button>
|
||||
<p>上一题</p>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
size="medium"
|
||||
:icon="
|
||||
!flagList.includes(questions[currentQuestionInd].id)
|
||||
? 'el-icon-star-off'
|
||||
: 'el-icon-star-on'
|
||||
"
|
||||
:style="{
|
||||
color: flagList.includes(questions[currentQuestionInd].id)
|
||||
? '#ee0000'
|
||||
: '#333333',
|
||||
}"
|
||||
circle
|
||||
@click="
|
||||
toggleFlagList({
|
||||
isFlag: !flagList.includes(questions[currentQuestionInd].id),
|
||||
question: questions[currentQuestionInd],
|
||||
})
|
||||
"
|
||||
></el-button>
|
||||
<p>标记</p>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
size="medium"
|
||||
icon="el-icon-arrow-right"
|
||||
type="primary"
|
||||
circle
|
||||
:disabled="currentQuestionInd == questions.length - 1"
|
||||
@click="changeQuestion(1)"
|
||||
></el-button>
|
||||
<p>下一题</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QuestionItem from '@/views/assessment-evaluation/question-bank-manage/components/QuestionItem.vue'
|
||||
export default {
|
||||
components: { QuestionItem },
|
||||
props: {
|
||||
questions: {},
|
||||
// 模式:0答题 1预览 2模拟考试 3判卷 4错题巩固-背题模式 5-错题巩固-答题模式
|
||||
mode: { default: 0, type: Number },
|
||||
// 试卷模式,0整卷,1逐题
|
||||
paperStyle: {
|
||||
default: 0,
|
||||
type: Number
|
||||
},
|
||||
random: {
|
||||
default: () => [0, 0],
|
||||
type: Array
|
||||
}
|
||||
|
||||
// onlyMistake: {
|
||||
// default: false
|
||||
// },
|
||||
// onlyTextarea: {
|
||||
// default: false
|
||||
// }
|
||||
},
|
||||
data: () => ({
|
||||
flagList: [],
|
||||
answerList: [],
|
||||
questionList: null,
|
||||
currentQuestionInd: 0,
|
||||
evalList: [],
|
||||
isPrint: false
|
||||
}),
|
||||
watch: {
|
||||
questions: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
if (nv) {
|
||||
this.questionList = this.formatQuestions(nv)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.flagList = []
|
||||
this.answerList = []
|
||||
this.evalList = []
|
||||
this.currentQuestionInd = 0
|
||||
if (this.$route.meta?.key === 'printPaper') {
|
||||
this.isPrint = true
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (this.$route.meta?.key === 'printPaper') {
|
||||
this.isPrint = true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 当子组件发出事件时调用的方法。
|
||||
questionItemHasEval (nv) {
|
||||
const oldInd = this.evalList.findIndex((item) => item.id === nv.id)
|
||||
if (oldInd !== -1) this.evalList.splice(oldInd, 1)
|
||||
if (Object.keys(nv).includes('score')) {
|
||||
this.evalList.push(nv)
|
||||
}
|
||||
this.$emit('evalChanged', this.evalList)
|
||||
},
|
||||
// 使用questionID切换当前试题ind
|
||||
changeIndById (id) {
|
||||
const ind = this.questions.findIndex((item) => item.id === id)
|
||||
this.currentQuestionInd = ind
|
||||
},
|
||||
// 一种更改当前问题索引的方法。
|
||||
changeQuestion (offset) {
|
||||
this.currentQuestionInd += offset
|
||||
},
|
||||
getAnswer (questionId) {
|
||||
const ind = this.answerList.findIndex(
|
||||
(item) => item.questionId === questionId
|
||||
)
|
||||
if (ind === -1) return null
|
||||
|
||||
return this.answerList[ind].answer
|
||||
},
|
||||
// 当子组件发出事件时调用的方法。
|
||||
userAnswerOnChange (answer, question) {
|
||||
const ind = this.answerList.findIndex(
|
||||
(item) => item.questionId === question.id
|
||||
)
|
||||
if (ind !== -1) {
|
||||
this.answerList.splice(ind, 1)
|
||||
}
|
||||
if (answer.length > 0) {
|
||||
const item = { questionId: question.id, answer }
|
||||
if (this.mode === 5) {
|
||||
item.resultId = question.resultId
|
||||
}
|
||||
this.answerList.push(item)
|
||||
|
||||
this.$emit('answers', this.answerList)
|
||||
}
|
||||
},
|
||||
// 当用户单击星形图标时调用的方法。
|
||||
toggleFlagList ({ isFlag, question }) {
|
||||
if (isFlag === true) {
|
||||
this.flagList.push(question.id)
|
||||
} else {
|
||||
const itemInd = this.flagList.indexOf(question.id)
|
||||
this.flagList.splice(itemInd, 1)
|
||||
}
|
||||
this.$emit('flag', this.flagList)
|
||||
},
|
||||
// 获取一组问题并返回一个具有两个属性的对象:`classify` 和 `questions`。
|
||||
formatQuestions (questionList) {
|
||||
// const questionsClassify = {}
|
||||
const resQ = questionList.reduce(
|
||||
(prev, item) => {
|
||||
const index = prev.classify.findIndex((j) => j.title === item.type)
|
||||
if (index === -1) {
|
||||
prev.classify.push({
|
||||
title: item.type,
|
||||
score: item.itemScore || item.score || 0,
|
||||
userScore:
|
||||
typeof item.userScore !== 'undefined'
|
||||
? item.userScore
|
||||
: undefined
|
||||
})
|
||||
} else {
|
||||
if (typeof item.userScore !== 'undefined') {
|
||||
prev.classify[index].userScore += item.userScore
|
||||
}
|
||||
}
|
||||
if (Object.keys(prev.questions).includes(item.type)) {
|
||||
prev.questions[item.type].push(item)
|
||||
} else prev.questions[item.type] = [item]
|
||||
return prev
|
||||
},
|
||||
{ classify: [], questions: {} }
|
||||
)
|
||||
return resQ
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.changer-btns {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: 1px solid #efefef;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
& > p {
|
||||
padding: 5px 0;
|
||||
color: $--color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<div class="gy-area-title">答题提示</div>
|
||||
<div class="time-remaining">
|
||||
剩余时间:<span>{{ formatTime(timeRemaining) }}</span>
|
||||
</div>
|
||||
<div class="write-progress">
|
||||
<div class="percent">
|
||||
<div>答题进度:</div>
|
||||
<div class="ratio">{{ answers?.length }}/{{ questions?.length }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-btn-group">
|
||||
<el-button round type="primary" @click="handler('submit')"
|
||||
>提交试卷</el-button
|
||||
>
|
||||
<el-button plain round type="danger" @click="handler('cancel')"
|
||||
>退出考试</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
export default {
|
||||
props: {
|
||||
answers: {},
|
||||
historyInfo: {},
|
||||
questions: {},
|
||||
mode: {},
|
||||
now: { default: 0 }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
reverseTimer: null,
|
||||
timeRemaining: 0,
|
||||
serviceTime: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
now: {
|
||||
handler (nv) {
|
||||
if (nv <= 0) {
|
||||
nv = new Date().getTime()
|
||||
}
|
||||
this.serviceTime = nv
|
||||
}
|
||||
},
|
||||
historyInfo: {
|
||||
deep: true,
|
||||
handler (nv) {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.initReverseTimer()
|
||||
},
|
||||
activated () {
|
||||
this.initReverseTimer()
|
||||
},
|
||||
deactivated () {
|
||||
this.cancelTimer()
|
||||
},
|
||||
// eslint-disable-next-line vue/no-deprecated-destroyed-lifecycle
|
||||
beforeDestroy () {
|
||||
this.cancelTimer()
|
||||
},
|
||||
|
||||
methods: {
|
||||
/*
|
||||
传入事件,抛出事件
|
||||
*/
|
||||
handler (emit) {
|
||||
this.$emit(emit)
|
||||
},
|
||||
/*
|
||||
取消计时器
|
||||
*/
|
||||
cancelTimer () {
|
||||
if (this.reverseTimer != null) {
|
||||
clearInterval(this.reverseTimer)
|
||||
this.reverseTimer = null
|
||||
}
|
||||
},
|
||||
/*
|
||||
初始化倒计时定时器
|
||||
*/
|
||||
initReverseTimer () {
|
||||
this.cancelTimer()
|
||||
this.reverseTimer = setInterval(() => {
|
||||
if (this.historyInfo) {
|
||||
const curTime = this.serviceTime
|
||||
const endTime = this.historyInfo.endTimestamp
|
||||
this.timeRemaining = endTime - curTime
|
||||
if (this.timeRemaining <= 0) {
|
||||
this.timeRemaining = 0
|
||||
this.handler('forceSubmit')
|
||||
clearInterval(this.reverseTimer)
|
||||
}
|
||||
} else {
|
||||
this.cancelTimer()
|
||||
}
|
||||
this.serviceTime += 1000
|
||||
}, 1000)
|
||||
},
|
||||
/*
|
||||
格式化毫秒时间戳
|
||||
*/
|
||||
formatTime (msTime) {
|
||||
const time = msTime / 1000
|
||||
let hour = Math.floor(time / 60 / 60)
|
||||
hour = hour.toString().padStart(2, '0')
|
||||
let minute = Math.floor(time / 60) % 60
|
||||
minute = minute.toString().padStart(2, '0')
|
||||
let second = Math.floor(time) % 60
|
||||
second = second.toString().padStart(2, '0')
|
||||
return `${hour}:${minute}:${second}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-btn-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.write-progress {
|
||||
background: #f1f5fe;
|
||||
border-radius: 16px;
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
.percent {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
font-weight: bold;
|
||||
.ratio {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
.time-remaining {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
padding: 0 10px;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 25px;
|
||||
background: #f1f5fe;
|
||||
|
||||
span {
|
||||
font-size: 18px;
|
||||
color: #ee0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div style="display:flex;height:100%;flex-direction: column;justify-content: space-between;">
|
||||
<div>
|
||||
<div class="gy-area-title">评卷设置</div>
|
||||
<div class="write-progress">
|
||||
<p>仅看错题:<el-switch v-model="isOnlyMistake" ></el-switch></p>
|
||||
<p style="margin-top: 10px;">仅看简答题:<el-switch v-model="isOnlyTextArea"></el-switch></p>
|
||||
</div>
|
||||
<div class="gy-area-title">信息统计</div>
|
||||
<div class="write-progress form">
|
||||
<p>学员姓名:<span>{{currentStudentInfo?.realname}}</span></p>
|
||||
<p>考试成绩:<span>{{currentStudentInfo?.score}}</span></p>
|
||||
<p>考试结果:<span :style="{color:isPass?'#00c782':'#ec0000'}">{{isPass?'及格':'不及格'}}</span></p>
|
||||
<p>总题数:<span>{{questionCount}}道</span></p>
|
||||
<p>自动评分:<span>{{questionCount - humanEvalCount}}道</span></p>
|
||||
<p>人工评分:<span>{{humanEvalCount}}道</span></p>
|
||||
</div>
|
||||
<div class="action-btn-group" v-if="logs.length>0" style="margin-top:10px;">
|
||||
<el-button round type="primary" @click="prevStudent">上一人</el-button>
|
||||
<el-button round type="danger" @click="nextStudent">下一人</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-btn-group">
|
||||
<el-button round type="primary" @click="handler('submit')">提交评分</el-button>
|
||||
<el-button plain round type="danger" @click="handler('cancel')">退出评分</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
export default {
|
||||
props: {
|
||||
logs: {
|
||||
default: []
|
||||
},
|
||||
questions: {
|
||||
default: []
|
||||
},
|
||||
onlyMistake: {},
|
||||
onlyTextarea: {},
|
||||
evalList: {
|
||||
default: []
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentStudentInfo: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isOnlyMistake: {
|
||||
get () {
|
||||
return this.onlyMistake
|
||||
},
|
||||
set (v) {
|
||||
this.$emit('update:onlyMistake', v)
|
||||
}
|
||||
},
|
||||
isOnlyTextArea: {
|
||||
get () {
|
||||
return this.onlyTextarea
|
||||
},
|
||||
set (v) {
|
||||
this.$emit('update:onlyTextarea', v)
|
||||
}
|
||||
},
|
||||
questionCount () {
|
||||
return this.questions.length
|
||||
},
|
||||
/*
|
||||
人工判卷数量
|
||||
*/
|
||||
humanEvalCount () {
|
||||
return this.questions.reduce((prev, item) => {
|
||||
if (['input', 'textarea'].includes(item.element)) {
|
||||
return prev + 1
|
||||
}
|
||||
return prev
|
||||
}, 0)
|
||||
},
|
||||
isPass () {
|
||||
return (this.currentStudentInfo?.score) >= (this.currentStudentInfo?.passSocre)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
historyInfo: {
|
||||
deep: true,
|
||||
handler (nv) { }
|
||||
},
|
||||
|
||||
'$route.params.studentExamId': {
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
if (this.logs.length > 0) {
|
||||
this.currentStudentInfo = this.logs.find(item => item.historyId === +nv)
|
||||
} else {
|
||||
this.currentStudentInfo = null
|
||||
}
|
||||
}
|
||||
},
|
||||
logs: {
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.isOnlyMistake = false
|
||||
this.isOnlyTextArea = false
|
||||
|
||||
if (this.currentStudentInfo == null) { this.currentStudentInfo = nv.find(item => item.historyId === +this.$route.params.studentExamId) }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
},
|
||||
activated () {
|
||||
|
||||
},
|
||||
deactivated () {
|
||||
},
|
||||
// eslint-disable-next-line vue/no-deprecated-destroyed-lifecycle
|
||||
beforeDestroy () {
|
||||
},
|
||||
|
||||
methods: {
|
||||
/*
|
||||
判断是否提交了评分,并弹窗提示
|
||||
*/
|
||||
checkEvalList () {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.evalList.length > 0) {
|
||||
this.$confirm('您的判分还未提交,确定要执行吗?', '提示', { type: 'info' }).then(res => {
|
||||
resolve()
|
||||
}).catch(() => {
|
||||
reject(new Error())
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
},
|
||||
/*
|
||||
上一位学员的答卷
|
||||
*/
|
||||
prevStudent () {
|
||||
if (this.logs.length === 0) return
|
||||
const currentInd = this.logs.findIndex(item => item.historyId === this.currentStudentInfo.historyId)
|
||||
if (currentInd > 0) {
|
||||
const prev = this.logs[currentInd - 1]
|
||||
this.checkEvalList().then(res => {
|
||||
this.$router.replace({ path: '/assessment-evaluation/human-evaluation/begin-eval/' + prev.historyId, query: { preview: 3 } })
|
||||
}).catch(_ => {})
|
||||
} else {
|
||||
this.$message.closeAll()
|
||||
this.$message.error('已经是第一位学员了')
|
||||
}
|
||||
},
|
||||
/*
|
||||
下一位学员的答卷
|
||||
*/
|
||||
nextStudent () {
|
||||
if (this.logs.length === 0) return
|
||||
const currentInd = this.logs.findIndex(item => item.historyId === this.currentStudentInfo.historyId)
|
||||
if (currentInd < this.logs.length - 1) {
|
||||
const prev = this.logs[currentInd + 1]
|
||||
this.checkEvalList().then(res => {
|
||||
this.$router.replace({ path: '/assessment-evaluation/human-evaluation/begin-eval/' + prev.historyId, query: { preview: 3 } })
|
||||
}).catch(_ => {})
|
||||
} else {
|
||||
this.$message.closeAll()
|
||||
this.$message.error('已经是最后一位学员了')
|
||||
}
|
||||
},
|
||||
handler (emit) {
|
||||
if (emit === 'cancel') {
|
||||
this.checkEvalList().then(res => {
|
||||
this.$emit(emit)
|
||||
})
|
||||
return
|
||||
}
|
||||
this.$emit(emit)
|
||||
},
|
||||
/*
|
||||
格式化毫秒到hh:mm:ss
|
||||
*/
|
||||
formatTime (msTime) {
|
||||
const time = msTime / 1000
|
||||
let hour = Math.floor(time / 60 / 60)
|
||||
hour = hour.toString().padStart(2, '0')
|
||||
let minute = Math.floor(time / 60) % 60
|
||||
minute = minute.toString().padStart(2, '0')
|
||||
let second = Math.floor(time) % 60
|
||||
second = second.toString().padStart(2, '0')
|
||||
return `${hour}:${minute}:${second}`
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-btn-group{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.write-progress{
|
||||
&.form{
|
||||
line-height: 1.5;
|
||||
span{
|
||||
color:$--color-primary;
|
||||
}
|
||||
}
|
||||
background: #f1f5fe;
|
||||
border-radius: 4px;
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
.percent{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
font-weight: bold;
|
||||
.ratio{
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color:#666;
|
||||
}
|
||||
}
|
||||
}
|
||||
.time-remaining {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
padding:0 10px;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 25px;
|
||||
background: #f1f5fe;
|
||||
|
||||
span {
|
||||
font-size: 18px;
|
||||
color: #ee0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,428 @@
|
||||
<template>
|
||||
<div v-loading="page_is_loading" class="exam-paper">
|
||||
<div
|
||||
class="aq-card"
|
||||
ref="AQCard"
|
||||
v-if="mode === 0 || mode === 3 || mode === 1"
|
||||
>
|
||||
<AQCard
|
||||
:questions="questions"
|
||||
:flag="flagList"
|
||||
:answers="answers"
|
||||
@smooth="aqItemOnClick"
|
||||
:mode="mode"
|
||||
@cancel="onPreviewCancel"
|
||||
></AQCard>
|
||||
</div>
|
||||
<div class="questions-area">
|
||||
<h1 style="text-align: center; padding: 10px 0; font-size: 18px">
|
||||
{{ exam?.title }}
|
||||
</h1>
|
||||
<QuestionsList
|
||||
:questions="questions"
|
||||
@flag="flagListChanged"
|
||||
@answers="userAnswerChanged"
|
||||
:mode="mode"
|
||||
:paperStyle="mode == 0 ? exam?.examMode : 0"
|
||||
ref="questionList"
|
||||
@evalChanged="evalListChanged"
|
||||
:random="random"
|
||||
></QuestionsList>
|
||||
</div>
|
||||
<div class="tips-card" v-if="mode === 0">
|
||||
<StudentTips
|
||||
:answers="answers"
|
||||
:historyInfo="onlineExamInfo"
|
||||
:questions="questions"
|
||||
:now="now"
|
||||
@submit="submitThisExamBtnHandler"
|
||||
@forceSubmit="forceSubmitHandler"
|
||||
@cancel="cancelThisExamBtnHandler"
|
||||
:mode="mode"
|
||||
>
|
||||
</StudentTips>
|
||||
</div>
|
||||
<div class="tips-card" v-if="mode === 3">
|
||||
<TeacherTips
|
||||
@cancel="cancelThisExamBtnHandler"
|
||||
:onlyMistake.sync="onlyMistake"
|
||||
:evalList="evalList"
|
||||
:onlyTextarea.sync="onlyTextarea"
|
||||
:logs="allStudentExamLogs"
|
||||
:questions="questions"
|
||||
@submit="submitEval"
|
||||
></TeacherTips>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getOnlineExamAllDataApi,
|
||||
submitExamApi,
|
||||
getOnlineExamResultApi
|
||||
} from '@/api/assessment-evaluation/onlineTest'
|
||||
import {
|
||||
pagingGradeDetailsApi,
|
||||
patchSomeExamResult
|
||||
} from '@/api/assessment-evaluation/humanEval'
|
||||
import AQCard from './components/AQCard.vue'
|
||||
import QuestionsList from './components/QuestionsList.vue'
|
||||
import StudentTips from './components/StudentTips.vue'
|
||||
import TeacherTips from './components/TeacherDatas.vue'
|
||||
export default {
|
||||
components: { AQCard, QuestionsList, StudentTips, TeacherTips },
|
||||
data: () => ({
|
||||
page_is_loading: false,
|
||||
exam: null,
|
||||
paper: null,
|
||||
onlineExamInfo: null,
|
||||
questions: [],
|
||||
questionsBak: [],
|
||||
classifyQuestions: {},
|
||||
flagList: [],
|
||||
random: [0, 0],
|
||||
answers: [],
|
||||
mode: -1,
|
||||
allStudentExamLogs: [],
|
||||
onlyMistake: false,
|
||||
onlyTextarea: false,
|
||||
evalList: [],
|
||||
now: 0
|
||||
}),
|
||||
mounted () {
|
||||
this.initMode()
|
||||
},
|
||||
activated () {
|
||||
this.initPage()
|
||||
},
|
||||
watch: {
|
||||
onlyMistake: {
|
||||
handler (nv) {
|
||||
this.filterQuestion()
|
||||
}
|
||||
},
|
||||
onlyTextarea: {
|
||||
handler (nv) {
|
||||
this.filterQuestion()
|
||||
}
|
||||
},
|
||||
'$route.params.studentExamId': {
|
||||
handler (nv) {
|
||||
this.initPage()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 根据错题和简答题过滤试题
|
||||
filterQuestion () {
|
||||
let question = this.questionsBak
|
||||
if (this.mode === 3) {
|
||||
if (this.onlyMistake) {
|
||||
question = question.filter((item) => item.userScore < item.score)
|
||||
}
|
||||
if (this.onlyTextarea) {
|
||||
question = question.filter((item) =>
|
||||
[
|
||||
'textarea'
|
||||
// , 'input'
|
||||
].includes(item.element)
|
||||
)
|
||||
}
|
||||
this.questions = question
|
||||
}
|
||||
},
|
||||
/*
|
||||
取消预览,返回上一页
|
||||
*/
|
||||
onPreviewCancel () {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
// getPdf,
|
||||
submitEval () {
|
||||
this.page_is_loading = true
|
||||
patchSomeExamResult(this.onlineExamInfo.id, this.evalList)
|
||||
.then((res) => {
|
||||
this.$message.success('提交成功')
|
||||
this.evalList = []
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally((_) => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
evalListChanged (nv) {
|
||||
this.evalList = nv
|
||||
},
|
||||
onlyMistakeHandler (v) {},
|
||||
/*
|
||||
初始化页面的handler
|
||||
*/
|
||||
initPage () {
|
||||
this.initAlldata()
|
||||
this.initMode()
|
||||
if (this.mode === 0) {
|
||||
this.getAllData()
|
||||
} else if (this.mode === 3) {
|
||||
this.getResultData()
|
||||
} else {
|
||||
this.getResultData()
|
||||
}
|
||||
},
|
||||
/*
|
||||
获取所有学员考试记录
|
||||
*/
|
||||
getAllStudentExamLogs (examId) {
|
||||
pagingGradeDetailsApi({ id: examId }).then((res) => {
|
||||
this.allStudentExamLogs = res.data.data
|
||||
})
|
||||
},
|
||||
// 设置模式
|
||||
initMode () {
|
||||
if (this.$route.query.preview === '1') {
|
||||
this.mode = 1
|
||||
} else if (this.$route.query.preview === '3') {
|
||||
this.mode = 3
|
||||
} else {
|
||||
this.mode = 0
|
||||
}
|
||||
},
|
||||
/*
|
||||
初始化所有数据
|
||||
*/
|
||||
initAlldata () {
|
||||
this.questions = []
|
||||
this.questionsBak = []
|
||||
this.classifyQuestions = {}
|
||||
this.flagList = []
|
||||
this.answers = []
|
||||
this.evalList = []
|
||||
this.random = [0, 0]
|
||||
this.exam = null
|
||||
this.paper = null
|
||||
this.onlineExamInfo = null
|
||||
this.page_is_loading = false
|
||||
},
|
||||
/*
|
||||
考试时间结束,强制交卷
|
||||
*/
|
||||
forceSubmitHandler () {
|
||||
this.$message.warning('考试结束时间到,将会强制提交答卷!')
|
||||
this.submitExam()
|
||||
},
|
||||
/*
|
||||
点击交卷按钮后的cb
|
||||
*/
|
||||
submitThisExamBtnHandler () {
|
||||
if (this.answers.length < this.questions.length) {
|
||||
return this.$confirm('还有题目未完成,是否强制交卷', '警告', {
|
||||
type: 'warning'
|
||||
})
|
||||
.then((res) => {
|
||||
this.submitExam()
|
||||
})
|
||||
.catch((_) => {})
|
||||
}
|
||||
if (this.flagList.length > 0) {
|
||||
return this.$confirm('有标记题目未取消,是否提交试卷', '警告', {
|
||||
type: 'warning'
|
||||
})
|
||||
.then((res) => {
|
||||
this.submitExam()
|
||||
})
|
||||
.catch((_) => {})
|
||||
}
|
||||
this.submitExam()
|
||||
},
|
||||
/*
|
||||
点击退出考试按钮后的cb
|
||||
*/
|
||||
cancelThisExamBtnHandler () {
|
||||
if (this.mode === 3) {
|
||||
return this.$router.go(-1)
|
||||
}
|
||||
return this.$confirm('未完成试卷,是否退出考试!', '警告', {
|
||||
type: 'error'
|
||||
})
|
||||
.then((res) => {
|
||||
if (this.exam.classifyId === -1) {
|
||||
return this.$router.replace({
|
||||
path: '/assessment-evaluation/simulation-test'
|
||||
})
|
||||
}
|
||||
this.$router.replace({ path: '/assessment-evaluation/online-test' })
|
||||
})
|
||||
.catch((_) => {})
|
||||
},
|
||||
/*
|
||||
提交试卷api
|
||||
*/
|
||||
submitExam () {
|
||||
if (!this.$route.params.studentExamId) return
|
||||
submitExamApi({
|
||||
id: this.$route.params.studentExamId,
|
||||
answers: this.answers
|
||||
}).then((res) => {
|
||||
const tipText = ` 试卷总分:${
|
||||
res.data.paperScore
|
||||
}分,及格分:${Math.ceil(
|
||||
(res.data.paperScore * res.data.passPercent) / 100
|
||||
)}分,您的成绩:${res.data.score}分`
|
||||
if (res.data.gradePaperMode === 0) {
|
||||
this.$confirm(
|
||||
res.data.isPass
|
||||
? this.exam.successTips + tipText
|
||||
: this.exam.failedTips + tipText,
|
||||
'提示',
|
||||
{ type: res.data.isPass ? 'success' : 'error' }
|
||||
).finally((_) => {
|
||||
if (this.exam.classifyId === -1) {
|
||||
return this.$router.replace({
|
||||
path: '/assessment-evaluation/simulation-test'
|
||||
})
|
||||
}
|
||||
this.$router.replace({
|
||||
path: '/assessment-evaluation/online-test'
|
||||
})
|
||||
})
|
||||
} else if (res.data.gradePaperMode === 1) {
|
||||
this.$confirm(this.exam.waitingTips, '提示', {
|
||||
type: 'info'
|
||||
}).finally((_) => {
|
||||
if (this.exam.classifyId === -1) {
|
||||
return this.$router.replace({
|
||||
path: '/assessment-evaluation/simulation-test'
|
||||
})
|
||||
}
|
||||
this.$router.replace({
|
||||
path: '/assessment-evaluation/online-test'
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
/*
|
||||
左侧栏目试题被点击,试卷滚动或切换到试题,并闪烁题目,
|
||||
*/
|
||||
aqItemOnClick (question) {
|
||||
if (+this.exam.examMode === 0) {
|
||||
const item = document.querySelector(`[tag="${question.id}"]`)
|
||||
item.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth'
|
||||
})
|
||||
item.classList.add('shine')
|
||||
setTimeout(() => {
|
||||
item.classList.remove('shine')
|
||||
}, 2000)
|
||||
} else if (+this.exam.examMode === 1) {
|
||||
this.$refs.questionList.changeIndById(question.id)
|
||||
}
|
||||
},
|
||||
/*
|
||||
用户答案修改后回调
|
||||
*/
|
||||
userAnswerChanged (nv) {
|
||||
this.answers = nv
|
||||
},
|
||||
/*
|
||||
用户标记试题列表被修改的回调
|
||||
*/
|
||||
flagListChanged (nv) {
|
||||
this.flagList = nv
|
||||
},
|
||||
/*
|
||||
开始考试
|
||||
*/
|
||||
getAllData () {
|
||||
if (!this.$route.params.studentExamId) return
|
||||
this.page_is_loading = true
|
||||
getOnlineExamAllDataApi(this.$route.params.studentExamId)
|
||||
.then((res) => {
|
||||
const { exam, onlineExamInfo, paper, questions, now } = res.data
|
||||
this.exam = exam
|
||||
this.onlineExamInfo = onlineExamInfo
|
||||
this.questions = questions
|
||||
this.questionsBak = questions
|
||||
this.now = now
|
||||
this.random = [
|
||||
paper.questionsIsRandomSort,
|
||||
paper.optionsIsRandomSort
|
||||
]
|
||||
if (this.random[0] === 1) {
|
||||
this.questions.sort(() => Math.random() - 0.5)
|
||||
this.questions.sort((a, b) => a.typeId - b.typeId)
|
||||
}
|
||||
})
|
||||
.catch((_) => {
|
||||
// 后端校验是否可以考试,不可以考试则退出到列表
|
||||
if (this.exam.classifyId === -1) {
|
||||
return this.$router.replace({
|
||||
path: '/assessment-evaluation/simulation-test'
|
||||
})
|
||||
}
|
||||
this.$router.replace({ path: '/assessment-evaluation/online-test' })
|
||||
})
|
||||
.finally((_) => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
/*
|
||||
查看答卷
|
||||
*/
|
||||
getResultData () {
|
||||
if (!this.$route.params.studentExamId) return
|
||||
this.page_is_loading = true
|
||||
getOnlineExamResultApi(this.$route.params.studentExamId)
|
||||
.then((res) => {
|
||||
const { exam, onlineExamInfo, questions } = res.data
|
||||
this.exam = exam
|
||||
this.getAllStudentExamLogs(exam.id)
|
||||
this.onlineExamInfo = onlineExamInfo
|
||||
this.questions = questions
|
||||
this.questionsBak = questions
|
||||
this.random = [0, 0]
|
||||
})
|
||||
.catch((_) => {
|
||||
this.$router.go(-1)
|
||||
})
|
||||
.finally((_) => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.exam-paper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.aq-card {
|
||||
flex-basis: 240px;
|
||||
flex-shrink: 0;
|
||||
width: 240px;
|
||||
padding-right: 15px;
|
||||
border-right: 1px solid #efefef;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.tips-card {
|
||||
flex-basis: 200px;
|
||||
width: 200px;
|
||||
padding-left: 15px;
|
||||
border-left: 1px solid #efefef;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.questions-area {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
469
front/src/views/assessment-evaluation/online-test/index.vue
Normal file
469
front/src/views/assessment-evaluation/online-test/index.vue
Normal file
@@ -0,0 +1,469 @@
|
||||
<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
|
||||
<template>
|
||||
<div class="v-page classify_page">
|
||||
<div class="v-ctx" v-loading="paper_list_loading">
|
||||
<FormLayout
|
||||
ref="formLayoutRef"
|
||||
:items="formItems"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="70px"
|
||||
label-position="right"
|
||||
:inline="true"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<!-- <el-input placeholder="请输入查询内容" v-model="form.title" class="input-with-select" clearable>
|
||||
<el-button slot="append" @click="pagingFindList">查询</el-button>
|
||||
</el-input> -->
|
||||
<QueryInput v-model="form.title" @query="pagingFindList"></QueryInput>
|
||||
</template>
|
||||
<template v-slot:btns> </template>
|
||||
</FormLayout>
|
||||
<TableLayout
|
||||
:column="column"
|
||||
:data="table_data"
|
||||
:pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })"
|
||||
@size-change="(e) => pagingChange({ pageSize: e })"
|
||||
>
|
||||
<template v-slot:examStartTime="props">
|
||||
<div>
|
||||
<p>开始时间:{{ props.row.examStartTime }}</p>
|
||||
<p>结束时间:{{ props.row.examEndTime }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:passPercent="props">
|
||||
<p>
|
||||
{{
|
||||
Math.round((props.row.passPercent * props.row.totalScore) / 100)
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
<template v-slot:myDuration="props">
|
||||
<p>{{ formatTime(props.row.myDuration) }}</p>
|
||||
</template>
|
||||
<template v-slot:status="props">
|
||||
<div
|
||||
:style="{
|
||||
'background-color':
|
||||
props.row.examTimes - props.row.myTimes <= 0
|
||||
? '#999999'
|
||||
: ['#208ac6', '#01c883', '#999999'][props.row.status],
|
||||
}"
|
||||
class="exam-status"
|
||||
>
|
||||
<span v-if="props.row.examTimes - props.row.myTimes <= 0">
|
||||
已结束
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ ["待开考", "已开始", "已结束"][props.row.status] }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:action="props">
|
||||
<!-- <el-button type="text" icon="el-icon-edit">{{ props.row.type }}</el-button> -->
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-x-zxks-canjiakaoshi"
|
||||
style="color: #02c761 !important"
|
||||
v-if="
|
||||
props.row.status == 1 &&
|
||||
props.row.examTimes - props.row.myTimes > 0
|
||||
"
|
||||
@click="toExamHandler(1, props.row)"
|
||||
>参加考试</el-button
|
||||
>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-rgpj-xiangqing"
|
||||
@click="toExamHandler(2, props.row)"
|
||||
v-if="props.row.status < 1"
|
||||
>
|
||||
查看详情
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-fbks-yulan"
|
||||
@click="lookLastExam(props.row)"
|
||||
v-if="
|
||||
props.row.status == 2 ||
|
||||
props.row.examTimes - props.row.myTimes <= 0
|
||||
"
|
||||
>查看答卷
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="el-icon-printer"
|
||||
class="print-icon"
|
||||
@click="toPrint(props.row)"
|
||||
v-if="
|
||||
props.row.status == 2 ||
|
||||
props.row.examTimes - props.row.myTimes <= 0
|
||||
"
|
||||
>打印
|
||||
</el-button>
|
||||
<!-- <el-button type="text" icon="el-icon-edit" @click="downLoadThisPdf" v-if="props.row.status == 2">下载
|
||||
</el-button> -->
|
||||
|
||||
<!-- <el-dropdown v-if="props.row.status == 2" style="margin-left: 10px;" @command="downLoadThisPdf" trigger="click">
|
||||
<span class="el-dropdown-link">
|
||||
<el-button type="text" icon="el-icon-arrow-down">下载
|
||||
</el-button>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="a">下载答卷</el-dropdown-item>
|
||||
<el-dropdown-item command="b">下载考卷</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown> -->
|
||||
</template>
|
||||
</TableLayout>
|
||||
</div>
|
||||
<DialogLayout
|
||||
:title="is_show_info ? `考试详情` : '确认考试'"
|
||||
:visible="exam_info_dialog_show"
|
||||
@onCancel="
|
||||
exam_info_dialog_show = false;
|
||||
current_exam = null;
|
||||
"
|
||||
width="600px"
|
||||
@onConfirm="toExamPaper"
|
||||
:actionBarOption="{
|
||||
noCencel: current_exam?.status == 0,
|
||||
noConfirm: current_exam?.status == 0,
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<div class="gy-form mb">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label">考试开始时间:</div>
|
||||
<div>{{ current_exam?.examStartTime }}</div>
|
||||
</div>
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label">考试结束时间:</div>
|
||||
<div>{{ current_exam?.examEndTime }}</div>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label">考试时长:</div>
|
||||
<div>
|
||||
<span class="red"> {{ current_exam?.examDuration }}</span
|
||||
>分钟
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form inline mb">
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label">考试总分:</div>
|
||||
<div class="red">{{ current_exam?.totalScore }}</div>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label">及格分:</div>
|
||||
<div class="red">
|
||||
{{
|
||||
Math.round(
|
||||
(current_exam?.totalScore * current_exam?.passPercent) / 100
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form inline mb">
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label">考试次数:</div>
|
||||
<div class="red">{{ current_exam?.examTimes }}次</div>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label">剩余次数:</div>
|
||||
<div class="red">
|
||||
{{ current_exam?.examTimes - current_exam?.myTimes }}次
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form mb">
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label">考试描述:</div>
|
||||
<div class="gy-in">{{ current_exam?.examDesc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TableLayout, FormLayout, DialogLayout } from '@/components/layout'
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import {
|
||||
pagingOnlineExamListApi,
|
||||
createExamHistoryApi,
|
||||
getLastedHistoryApi
|
||||
} from '@/api/assessment-evaluation/onlineTest'
|
||||
import QueryInput from '@/components/widget/QueryInput.vue'
|
||||
export default {
|
||||
components: {
|
||||
TableLayout,
|
||||
FormLayout,
|
||||
QueryInput,
|
||||
DialogLayout
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
table_data: [],
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
// 试题分类树是否加载中
|
||||
paper_list_loading: false,
|
||||
form: { title: '' },
|
||||
is_show_info: false,
|
||||
exam_info_dialog_show: false,
|
||||
current_exam: null
|
||||
}),
|
||||
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'title' },
|
||||
{ prop: 'btns', model: { class: 'gy-btns' } }
|
||||
]
|
||||
this.rules = {
|
||||
// name: { required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
}
|
||||
this.column = [
|
||||
{
|
||||
prop: 'title',
|
||||
label: '考试名称',
|
||||
'min-width': 130,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'classify',
|
||||
label: '考试分类',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'totalScore',
|
||||
label: '总分',
|
||||
'min-width': 60,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'passPercent',
|
||||
label: '及格分',
|
||||
'min-width': 60,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'myScore',
|
||||
label: '成绩',
|
||||
'min-width': 50,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'myDuration',
|
||||
label: '考试用时',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'mistakes',
|
||||
label: '错题',
|
||||
'min-width': 50,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'examStartTime',
|
||||
label: '考试时间',
|
||||
'min-width': 220,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '状态',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'action',
|
||||
label: '操作',
|
||||
// width: 350,
|
||||
width: 200,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
},
|
||||
activated () {
|
||||
this.pagingFindList()
|
||||
},
|
||||
methods: {
|
||||
/* 测试用, */
|
||||
downLoadThisPdf (type) {
|
||||
this.$message.info(type)
|
||||
// getPdf('测试', 'jj9999')
|
||||
},
|
||||
/*
|
||||
获取最后一次考试的考试记录
|
||||
*/
|
||||
lookLastExam (row) {
|
||||
this.paper_list_loading = true
|
||||
getLastedHistoryApi(row.id)
|
||||
.then((res) => {
|
||||
if (res.data == null) {
|
||||
return this.$message.error('该场考试没有查询到您的考试记录')
|
||||
}
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/online-test/begin-online-exam/' +
|
||||
res.data.id,
|
||||
query: { preview: 1 }
|
||||
})
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
formatTime (msTime) {
|
||||
const time = msTime / 1000
|
||||
let hour = Math.floor(time / 60 / 60)
|
||||
hour = hour.toString().padStart(2, '0')
|
||||
let minute = Math.floor(time / 60) % 60
|
||||
minute = minute.toString().padStart(2, '0')
|
||||
let second = Math.floor(time) % 60
|
||||
second = second.toString().padStart(2, '0')
|
||||
return `${hour}:${minute}:${second}`
|
||||
},
|
||||
/*
|
||||
去考试,会检查是否有机会
|
||||
*/
|
||||
toExamPaper () {
|
||||
if (this.is_show_info) {
|
||||
this.exam_info_dialog_show = false
|
||||
} else if (+this.current_exam.status === 1) {
|
||||
if (this.current_exam?.examTimes - this.current_exam?.myTimes <= 0) {
|
||||
return this.$message.error('您已没有该场考试的考试机会')
|
||||
}
|
||||
createExamHistoryApi({ onlineExamId: this.current_exam.id }).then(
|
||||
(res) => {
|
||||
if (res.data) {
|
||||
this.$message('开始考试,本次考试将计入考试次数')
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/online-test/begin-online-exam/' +
|
||||
res.data.id
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
this.exam_info_dialog_show = false
|
||||
} else if (+this.current_exam.status === 0) {
|
||||
return this.$message.error('考试暂未开始')
|
||||
} else if (+this.current_exam.status === 2) {
|
||||
return this.$message.error('考试已结束')
|
||||
}
|
||||
},
|
||||
toExamHandler (type = 2, row) {
|
||||
if (type === 2) {
|
||||
this.is_show_info = true
|
||||
} else if (type === 1) {
|
||||
this.is_show_info = false
|
||||
}
|
||||
this.current_exam = row
|
||||
this.exam_info_dialog_show = true
|
||||
},
|
||||
/*
|
||||
分页变化
|
||||
*/
|
||||
pagingChange (event) {
|
||||
if (
|
||||
typeof event.currentPage !== 'number' &&
|
||||
typeof event.pageSize !== 'number'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
/*
|
||||
初始化分页数据
|
||||
*/
|
||||
initPageInfo () {
|
||||
this.page_info = {
|
||||
currentPage: 1,
|
||||
pageSize: this.page_info.pageSize,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
toPrint (row) {
|
||||
this.paper_list_loading = true
|
||||
getLastedHistoryApi(row.id)
|
||||
.then((res) => {
|
||||
if (res.data == null) {
|
||||
return this.$message.error('该场考试没有查询到您的考试记录')
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/print-paper/' + row.paperId,
|
||||
query: { examIdPaperId: res.data.id }
|
||||
})
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
/*
|
||||
分页获取数据api
|
||||
*/
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
this.paper_list_loading = true
|
||||
pagingOnlineExamListApi({ ...this.page_info, ...this.form })
|
||||
.then((res) => {
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.classify_page .form_layout {
|
||||
margin-top: 16px;
|
||||
|
||||
.suf {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
// .el-form-item:last-child {
|
||||
// float: right;
|
||||
// }
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.exam-status {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<div class="gy-import-questions" v-loading="is_uploading">
|
||||
<div style="display: flex; justify-content: space-between; flex-wrap: wrap">
|
||||
<div class="gy-form inline" style="--fix: 80px">
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label middle">试题分类:</div>
|
||||
<QuestionClassifySelector
|
||||
v-model="classifyId"
|
||||
:mountedLoad="true"
|
||||
></QuestionClassifySelector>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<el-button
|
||||
round
|
||||
type="text"
|
||||
@click="downloadModelFile"
|
||||
style="padding: 0"
|
||||
>下载模板</el-button
|
||||
>
|
||||
<div
|
||||
class="gy-label middle"
|
||||
style="height: 32px; white-space: nowrap; color: #ed0000"
|
||||
>
|
||||
(下载模板后按要求录入内容)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button round type="primary" @click="uploadFile">导入</el-button>
|
||||
<el-button
|
||||
round
|
||||
type="primary"
|
||||
@click="filterMistakeAndUpload"
|
||||
v-if="passList.length > 0 && errorList.length > 0"
|
||||
>过滤错误并上传</el-button
|
||||
>
|
||||
<el-button round type="primary" plain @click="errorList = passList = []"
|
||||
>清理</el-button
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="fileUploader"
|
||||
id="fileUploader"
|
||||
ref="fileUploader"
|
||||
accept=".xlsx"
|
||||
@change="getFileContent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="errorList.length === 0">
|
||||
<el-empty description="请先导入试题"></el-empty>
|
||||
</div>
|
||||
<ul class="error-info-list" v-else>
|
||||
<li class="error-item header">
|
||||
<div class="column">表名</div>
|
||||
<div class="column">行号</div>
|
||||
<div class="column">题目</div>
|
||||
<div class="column">错误信息</div>
|
||||
</li>
|
||||
<div style="overflow-y: auto; max-height: 50vh">
|
||||
<li v-for="(item, ind) in errorList" :key="ind" class="error-item">
|
||||
<div class="column">{{ item.question.sheetName }}</div>
|
||||
<div class="column">{{ item.question.rowNum }}</div>
|
||||
<div class="column">{{ item.question.title }}</div>
|
||||
<ul class="column">
|
||||
<li v-for="(err, ind) in item.error" :key="ind">
|
||||
{{ err }}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as XLSX from 'xlsx'
|
||||
import QuestionClassifySelector from './QuestionClassifySelector.vue'
|
||||
import { importQuestionsApi } from '@/api/assessment-evaluation/questions'
|
||||
export default {
|
||||
components: { QuestionClassifySelector },
|
||||
data: () => ({
|
||||
errorList: [],
|
||||
passList: [],
|
||||
classifyId: null,
|
||||
is_uploading: false
|
||||
}),
|
||||
methods: {
|
||||
uploadFile () {
|
||||
if (this.classifyId == null) return this.$message.error('请先选择科目')
|
||||
this.$refs.fileUploader.click()
|
||||
},
|
||||
uploadToServer () {
|
||||
const data = this.passList
|
||||
const classifyId = this.classifyId
|
||||
this.is_uploading = true
|
||||
importQuestionsApi(classifyId, data)
|
||||
.then((res) => {
|
||||
this.$message.success('导入成功')
|
||||
this.passList = []
|
||||
this.errorList = []
|
||||
this.$router.go(-1)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.info(err)
|
||||
})
|
||||
.finally((_) => {
|
||||
this.is_uploading = false
|
||||
})
|
||||
},
|
||||
filterMistakeAndUpload () {
|
||||
this.$confirm(
|
||||
`文件中共包含 ${
|
||||
this.errorList.length + this.passList.length
|
||||
} 道题,其中${this.errorList.length}道题不符合规则,本次将导入 ${
|
||||
this.passList.length
|
||||
} 道题,确认导入吗?`,
|
||||
'提示',
|
||||
{ type: 'info' }
|
||||
)
|
||||
.then((res) => {
|
||||
this.uploadToServer()
|
||||
})
|
||||
.catch((_) => {})
|
||||
},
|
||||
getFileContent (e) {
|
||||
this._toJSON(e.target.files[0])
|
||||
.then((res) => {
|
||||
const { pass, errors } = this.checkFormat(res)
|
||||
this.passList = pass
|
||||
this.errorList = errors
|
||||
if (errors.length === 0) {
|
||||
this.$confirm(
|
||||
`本次将导入 ${pass.length} 道题,确认导入吗?`,
|
||||
'提示',
|
||||
{ type: 'info' }
|
||||
)
|
||||
.then((res) => {
|
||||
this.uploadToServer()
|
||||
})
|
||||
.catch((_) => {})
|
||||
}
|
||||
e.target.value = ''
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$message.error(err.message)
|
||||
})
|
||||
},
|
||||
checkFormat (questionList) {
|
||||
const pass = []
|
||||
const errors = []
|
||||
const types = ['单选题', '多选题', '判断题', '填空题', '简答题']
|
||||
const difficultys = ['易', '较易', '中等', '较难', '难']
|
||||
questionList.forEach((item) => {
|
||||
const errorInfos = []
|
||||
let tempOptions
|
||||
let tempAnswer
|
||||
|
||||
if (item.title.length > 255) {
|
||||
errorInfos.push('题目长度不能超过255个字符')
|
||||
}
|
||||
if (!item.title) {
|
||||
errorInfos.push('题目不能为空')
|
||||
}
|
||||
if (!item.type) {
|
||||
errorInfos.push('试题类型不能为空')
|
||||
} else {
|
||||
if (!types.includes(item.type)) {
|
||||
errorInfos.push(
|
||||
`【${item.type}】不是标准的试题类型(${types.join(',')})`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!item.difficulty) {
|
||||
errorInfos.push('难易程度不能为空')
|
||||
} else {
|
||||
if (!difficultys.includes(item.difficulty)) {
|
||||
errorInfos.push(
|
||||
`【${item.difficulty}】不是标准的难易程度(${difficultys.join(
|
||||
','
|
||||
)})`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!this.isJSON(item.answer) || !item.answer) {
|
||||
errorInfos.push('答案格式不符或为空')
|
||||
} else {
|
||||
tempAnswer = JSON.parse(item.answer)
|
||||
if (!Array.isArray(tempAnswer)) {
|
||||
errorInfos.push('答案应是一个数组')
|
||||
} else {
|
||||
if (tempAnswer.length === 0 && item.type !== '填空题') {
|
||||
errorInfos.push('没有答案')
|
||||
}
|
||||
}
|
||||
if (['单选题', '判断题', '简答题'].includes(item.type)) {
|
||||
if (tempAnswer.length > 1) {
|
||||
errorInfos.push(`题型【${item.type}】只能有一个答案`)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.isJSON(item.options) || !item.options) {
|
||||
errorInfos.push('选项格式不符或为空')
|
||||
} else {
|
||||
tempOptions = JSON.parse(item.options)
|
||||
if (!Array.isArray(tempOptions)) {
|
||||
errorInfos.push('选项应是一个数组')
|
||||
} else {
|
||||
if (!tempOptions.length === 0) {
|
||||
errorInfos.push('没有选项')
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
['单选题', '判断题', '多选题'].includes(item.type) &&
|
||||
tempAnswer &&
|
||||
tempOptions
|
||||
) {
|
||||
if (tempOptions.length < tempAnswer.length) {
|
||||
errorInfos.push('试题答案数量不能多于试题选项数量')
|
||||
}
|
||||
}
|
||||
if (
|
||||
['判断题'].includes(item.type) &&
|
||||
tempAnswer.length > 0 &&
|
||||
![0, 1].includes(tempAnswer[0])
|
||||
) {
|
||||
errorInfos.push('判断题答案只能为0-错误、1-正确')
|
||||
}
|
||||
if (errorInfos.length > 0) {
|
||||
errors.push({
|
||||
question: item,
|
||||
error: errorInfos
|
||||
})
|
||||
} else {
|
||||
pass.push(item)
|
||||
}
|
||||
})
|
||||
return { pass, errors }
|
||||
},
|
||||
_toJSON (file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!/^.+\.xlsx$/.test(file.name)) {
|
||||
reject(new Error('不是Excel表格文件'))
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const data = event.target.result
|
||||
const workbook = XLSX.read(data, {
|
||||
type: 'binary'
|
||||
})
|
||||
let resultRes = []
|
||||
workbook.SheetNames.forEach(function (sheetName) {
|
||||
let sheetData = XLSX.utils.sheet_to_json(
|
||||
workbook.Sheets[sheetName],
|
||||
{
|
||||
header: [
|
||||
'title',
|
||||
'difficulty',
|
||||
'knowledge',
|
||||
'options',
|
||||
'answer',
|
||||
'analysis'
|
||||
],
|
||||
// 表格为空时默认值为空字符串
|
||||
defval: ''
|
||||
}
|
||||
)
|
||||
|
||||
sheetData.forEach((item, ind) => {
|
||||
item.sheetName = sheetName
|
||||
item.rowNum = ind + 1
|
||||
item.type = sheetName
|
||||
})
|
||||
|
||||
if (sheetData.length > 0) {
|
||||
const rowValues = Object.values(sheetData[0])
|
||||
if (rowValues.includes('题目') && rowValues.includes('知识点')) {
|
||||
sheetData = sheetData.slice(1)
|
||||
}
|
||||
}
|
||||
resultRes = resultRes.concat(sheetData)
|
||||
})
|
||||
resolve(resultRes)
|
||||
}
|
||||
reader.readAsBinaryString(file)
|
||||
})
|
||||
},
|
||||
isJSON (str) {
|
||||
try {
|
||||
const obj = JSON.parse(str)
|
||||
if (typeof obj === 'object' && obj) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
// 根据试题分类导出模板
|
||||
downloadModelFile () {
|
||||
const dataSheets = [
|
||||
{ sheetName: '单选题', data: [['题目', '难易程度(易、较易、中等、较难、难)', '知识点', '选项', '答案(选择题:大写A-Z对应选项)', '答案解析'], ['单选题例子,导入前请先删除', '较易', '语文', '["选项1","选项2","选项3"]', '["A"]', '答案解析XXX']] },
|
||||
{ sheetName: '多选题', data: [['题目', '难易程度(易、较易、中等、较难、难)', '知识点', '选项', '答案(选择题:大写A-Z对应选项)', '答案解析'], ['多选题例子,导入前请先删除', '易', '语文', '["选项1","选项2","选项3"]', '["A","B","C"]', '答案解析XXX']] },
|
||||
{ sheetName: '判断题', data: [['题目', '难易程度(易、较易、中等、较难、难)', '知识点', '选项', '答案(判断题:1正确0错误)', '答案解析'], ['判断题例子,导入前请先删除', '中等', '数学', '["正确","错误"]', '[1]', '答案解析XXX']] },
|
||||
{ sheetName: '填空题', data: [['题目', '难易程度(易、较易、中等、较难、难)', '知识点', '选项', '答案(保留[],填空题无需填写答案,选项即是答案)', '答案解析'], ['填空题例子,导入前请先删除', '较难', '英语', '["空1答案","空2答案","空3答案"]', '[]', '答案解析XXX']] },
|
||||
{
|
||||
sheetName: '简答题',
|
||||
data: [
|
||||
[
|
||||
'题目',
|
||||
'难易程度(易、较易、中等、较难、难)',
|
||||
'知识点',
|
||||
// '选项(选项即是关键词)',
|
||||
'选项(保留[],无需填写选项)',
|
||||
'答案',
|
||||
'答案解析'
|
||||
],
|
||||
[
|
||||
'简答题例子,导入前请先删除',
|
||||
'难',
|
||||
'数学',
|
||||
// '["关键词1","关键词2","关键词3"]'
|
||||
'[]',
|
||||
'["简答题标准答案只有纯文本"]',
|
||||
'答案解析XXX'
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
const sheets = []
|
||||
dataSheets.forEach((sheetItem) => {
|
||||
const colWidths = []
|
||||
const data = sheetItem.data
|
||||
data.forEach((row, index) => {
|
||||
// 遍历列
|
||||
row.forEach((col, ind) => {
|
||||
if (colWidths[ind] == null) colWidths[ind] = []
|
||||
colWidths[ind].push(this.getCellWidth(col))
|
||||
})
|
||||
})
|
||||
const ws = XLSX.utils.aoa_to_sheet(data)
|
||||
ws['!cols'] = []
|
||||
colWidths.forEach((widths, index) => {
|
||||
ws['!cols'].push({ wch: Math.max(...widths) })
|
||||
})
|
||||
sheets.push({
|
||||
name: sheetItem.sheetName,
|
||||
ws
|
||||
})
|
||||
})
|
||||
|
||||
// const ws = XLSX.utils.json_to_sheet(data)
|
||||
|
||||
const wb = XLSX.utils.book_new()
|
||||
sheets.forEach((wsItem) => {
|
||||
XLSX.utils.book_append_sheet(wb, wsItem.ws, wsItem.name)
|
||||
})
|
||||
XLSX.writeFile(
|
||||
wb,
|
||||
`试题导入模板${new Date().format('yyyy-MM-dd hh-mm-ss')}.xlsx`
|
||||
)
|
||||
},
|
||||
getCellWidth (value) {
|
||||
// 判断是否为null或undefined
|
||||
if (value == null) {
|
||||
return 10
|
||||
} else if (/.*[\u4e00-\u9fa5]+.*$/.test(value)) {
|
||||
// 中文的长度
|
||||
const chineseLength = value.match(/[\u4e00-\u9fa5]/g).length
|
||||
// 其他不是中文的长度
|
||||
const otherLength = value.length - chineseLength
|
||||
return chineseLength * 2.1 + otherLength * 1.1
|
||||
} else {
|
||||
return value.toString().length * 1.1
|
||||
/* 另一种方案
|
||||
value = value.toString()
|
||||
return value.replace(/[\u0391-\uFFE5]/g, 'aa').length
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.error-info-list {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
border: 2px solid $--color-primary;
|
||||
border-radius: 4px;
|
||||
|
||||
&,
|
||||
ul {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.error-item {
|
||||
display: flex;
|
||||
border-top: 1px solid #eee;
|
||||
|
||||
&.header {
|
||||
color: #fff;
|
||||
border-top: none;
|
||||
background: $--color-primary;
|
||||
|
||||
.column {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
padding: 5px;
|
||||
border-right: 1px solid #eee;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
line-height: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:nth-child(1),
|
||||
&:nth-child(2) {
|
||||
width: 60px;
|
||||
flex-basis: 60px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
// &:nth-child(4)
|
||||
// {
|
||||
// width: 100px;
|
||||
// flex-basis: 100px;
|
||||
// flex-grow: 0;
|
||||
// }
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#fileUploader {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<el-select filterable allow-create default-first-option clearable v-model="knowledgeId" placeholder="选择或输入添加知识点"
|
||||
@change="createKnowledges" :loading="loading">
|
||||
<!-- value-key="id" -->
|
||||
<el-option v-for="item in knowledgesOptions" :key="item.id" :label="item.name" :value="item.id">
|
||||
<span style="float: left">{{ item.name }}</span>
|
||||
<span
|
||||
style="float: right; color: #8492a6; font-size: 13px;height:100%;display:flex;align-items:center;justify-content:center"
|
||||
class="el-icon-delete" @click.stop="deleteKnowledge(item)"></span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAllknowledgePointApi, createKnowledgePointApi, deleteKnowledgePointApi } from '@/api/assessment-evaluation/questions'
|
||||
export default {
|
||||
data: _ => ({
|
||||
knowledgeId: null,
|
||||
knowledgesOptions: [],
|
||||
loading: false
|
||||
}),
|
||||
mounted () {
|
||||
this.findAllKnowledges()
|
||||
},
|
||||
activated () {
|
||||
this.findAllKnowledges()
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.knowledgeId = nv
|
||||
}
|
||||
},
|
||||
knowledgeId: {
|
||||
immediate: false,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.$emit('input', nv)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
请求api获取所有知识点
|
||||
*/
|
||||
findAllKnowledges () {
|
||||
// 获取知识点
|
||||
this.loading = true
|
||||
getAllknowledgePointApi().then(({ data: knowledges }) => {
|
||||
this.knowledgesOptions = knowledges
|
||||
}).finally(_ => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
/*
|
||||
删除某个知识点
|
||||
*/
|
||||
deleteKnowledge (item) {
|
||||
this.loading = true
|
||||
|
||||
deleteKnowledgePointApi(item.id).finally(_ => {
|
||||
this.loading = false
|
||||
this.findAllKnowledges()
|
||||
if (this.knowledgeId === item.id) {
|
||||
this.knowledgeId = null
|
||||
}
|
||||
})
|
||||
},
|
||||
/*
|
||||
创建知识点回调
|
||||
*/
|
||||
createKnowledges (e) {
|
||||
if (typeof e === 'string' && e !== '') {
|
||||
this.loading = true
|
||||
createKnowledgePointApi(e).then(res => {
|
||||
this.knowledgesOptions.push(res.data)
|
||||
this.$nextTick(() => {
|
||||
this.knowledgeId = res.data.id
|
||||
})
|
||||
}).finally(_ => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<el-cascader :options="question_classify_list"
|
||||
:props="{ checkStrictly: true, value: 'id', label: 'name', children, emitPath: false }" clearable
|
||||
v-model="classify">
|
||||
</el-cascader>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { findAllQuestionsClassifyApi } from '@/api/assessment-evaluation/questions'
|
||||
export default {
|
||||
data: _ => ({
|
||||
classify: null,
|
||||
question_classify_list: []
|
||||
}),
|
||||
mounted () {
|
||||
if (this.mountedLoad) {
|
||||
this.findAllQuestionsClassify()
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.findAllQuestionsClassify()
|
||||
},
|
||||
props: {
|
||||
mountedLoad: {
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.classify = nv
|
||||
}
|
||||
},
|
||||
classify: {
|
||||
immediate: false,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.$emit('input', nv)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
获取所有试题分类
|
||||
*/
|
||||
findAllQuestionsClassify () {
|
||||
findAllQuestionsClassifyApi().then(({ data }) => {
|
||||
let questionClassifyMap = {}
|
||||
questionClassifyMap = data.toTree()
|
||||
const list = questionClassifyMap.tree
|
||||
this.question_classify_list = list
|
||||
}).catch(err => {
|
||||
console.info(err)
|
||||
}).finally(_ => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<el-select v-model="difficultyLevel" placeholder="请选择" style="width:100px" clearable :loading="loading">
|
||||
<el-option v-for="item in question_difficulty_level" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getQuestionDifficultyLevelOptionsApi } from '@/api/assessment-evaluation/questions'
|
||||
export default {
|
||||
data: _ => ({
|
||||
difficultyLevel: null,
|
||||
question_difficulty_level: [],
|
||||
loading: false
|
||||
}),
|
||||
mounted () {
|
||||
if (this.mountedLoad) {
|
||||
this.findAllQuestionDifficultyLevel()
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.findAllQuestionDifficultyLevel()
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
default: null
|
||||
},
|
||||
mountedLoad: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.difficultyLevel = nv
|
||||
}
|
||||
},
|
||||
difficultyLevel: {
|
||||
immediate: false,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.$emit('input', nv)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
请求所有试题难易程度选项
|
||||
*/
|
||||
findAllQuestionDifficultyLevel () {
|
||||
this.loading = true
|
||||
getQuestionDifficultyLevelOptionsApi().then(({ data }) => {
|
||||
this.question_difficulty_level = data
|
||||
}).finally(_ => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,106 @@
|
||||
|
||||
<template>
|
||||
<el-tabs v-model="activeTagId" type="card" v-loading="loading">
|
||||
<el-tab-pane
|
||||
v-for="item in options"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:name="item.name"
|
||||
>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { getQuestionTypeOptionsApi } from '@/api/assessment-evaluation/questions'
|
||||
|
||||
export default {
|
||||
data: (_) => ({
|
||||
difficultyLevel: null,
|
||||
activeTagId: null,
|
||||
loading: false
|
||||
}),
|
||||
mounted () {
|
||||
this.initActive()
|
||||
},
|
||||
activated () {
|
||||
this.initActive()
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
default: null
|
||||
},
|
||||
options: {
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
if (nv && Object.keys(nv).includes('id')) {
|
||||
this.activeTagId = nv.name
|
||||
}
|
||||
}
|
||||
},
|
||||
activeTagId: {
|
||||
immediate: false,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
let nObj = null
|
||||
if (this.options.length > 0) {
|
||||
nObj = this.getTypeById(nv)
|
||||
}
|
||||
this.$emit('input', nObj)
|
||||
this.$emit('change', nObj)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
初始化tabbar当前选中项
|
||||
*/
|
||||
initActive () {
|
||||
this.$nextTick(() => {
|
||||
this.activeTagId =
|
||||
this.activeTagId != null ? this.activeTagId : this.options[0].name
|
||||
})
|
||||
},
|
||||
/*
|
||||
根据ID获取选中项
|
||||
*/
|
||||
getTypeById (name) {
|
||||
return this.options.find((item) => item.name === name)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-tabs--card > .el-tabs__header {
|
||||
border: none;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
::v-deep .el-tabs__nav-scroll {
|
||||
border: none;
|
||||
|
||||
.el-tabs__nav {
|
||||
border: unset;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
|
||||
.el-tabs__item {
|
||||
padding: 0 30px !important;
|
||||
border-radius: 4px 4px 0 0 !important;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
color: $--color-primary;
|
||||
|
||||
&.is-active {
|
||||
background: $--color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tag
|
||||
:key="tag"
|
||||
v-for="tag in dynamicTags"
|
||||
size="medium"
|
||||
closable
|
||||
:disable-transitions="false"
|
||||
@close="handleClose(tag)"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
class="input-new-tag"
|
||||
v-if="inputVisible"
|
||||
v-model="inputValue"
|
||||
ref="saveTagInput"
|
||||
size="small"
|
||||
@keyup.enter.native="handleInputConfirm"
|
||||
@blur="handleInputConfirm"
|
||||
>
|
||||
</el-input>
|
||||
<el-button
|
||||
v-else
|
||||
class="button-new-tag"
|
||||
size="mini"
|
||||
@click="showInput"
|
||||
icon="i-x-ktbj-tianjia"
|
||||
>
|
||||
增加关键词</el-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
dynamicTags: [],
|
||||
inputVisible: false,
|
||||
inputValue: ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
require: false,
|
||||
default: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.dynamicTags = nv
|
||||
}
|
||||
},
|
||||
dynamicTags: {
|
||||
immediate: false,
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
this.$emit('input', nv)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
标签关闭按钮被点击
|
||||
*/
|
||||
handleClose (tag) {
|
||||
this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1)
|
||||
},
|
||||
/*
|
||||
显示输入框新增标签
|
||||
*/
|
||||
showInput () {
|
||||
this.inputVisible = true
|
||||
this.$nextTick((_) => {
|
||||
this.$refs.saveTagInput.$refs.input.focus()
|
||||
})
|
||||
},
|
||||
/*
|
||||
输入框失去焦点或者回车键被按下后,调用
|
||||
*/
|
||||
handleInputConfirm () {
|
||||
const inputValue = this.inputValue
|
||||
if (inputValue) {
|
||||
this.dynamicTags.push(inputValue)
|
||||
}
|
||||
this.inputVisible = false
|
||||
this.inputValue = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-tag {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.input-new-tag {
|
||||
width: 100px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
margin-left: 10px;
|
||||
vertical-align: bottom;
|
||||
::v-deep .el-input__inner {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,433 @@
|
||||
<template>
|
||||
<div class="v-page">
|
||||
<div class="v-ctx" v-loading="page_is_loading">
|
||||
<QuestionTypeTabBar :options="questionTypeOptions" v-model="activeName">
|
||||
</QuestionTypeTabBar>
|
||||
<div class="gy-form inline" style="--fix:80px;margin-bottom: 20px;" v-if="activeName && activeName.id!==-1">
|
||||
<div class="gy-form-item mb">
|
||||
<div class="gy-label middle">试题分类:</div>
|
||||
<QuestionClassifySelector v-model="form.classifyId" :mountedLoad="true"></QuestionClassifySelector>
|
||||
</div>
|
||||
<div class="gy-form-item mb" >
|
||||
<div class="gy-label middle">知识点:</div>
|
||||
<KnowledgeSelector v-model="form.knowledgeId" :mountedLoad="true"></KnowledgeSelector>
|
||||
</div>
|
||||
<div class="gy-form-item" >
|
||||
<div class="gy-label middle">难易程度:</div>
|
||||
<QuestionDifficultyLevelSelector v-model="form.difficultyLevel" :mountedLoad="true"></QuestionDifficultyLevelSelector>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-question-editer" v-if="activeName && activeName.id!==-1">
|
||||
<div class="gy-form" style="--fix:80px;">
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label ">题目:</div>
|
||||
<el-input type="textarea" v-model="form.title"></el-input>
|
||||
</div>
|
||||
<div class="gy-form-item " style="margin-top:16px;">
|
||||
<div class="gy-label " v-if="!['textarea'].includes(currentOptionElement)">选项:</div>
|
||||
</div>
|
||||
<!-- 多选项,多答案的题型选项或答案的编辑 -->
|
||||
<div class="gy-unform-content" style="margin-top:16px;">
|
||||
<div class="options-list">
|
||||
<div :class="{'radio-option':true,'horizontal-option':['radio*2'].includes(currentOptionElement)}"
|
||||
v-if="['radio', 'checkbox','radio*2','input'].includes(currentOptionElement)">
|
||||
<div v-for="(item, ind) in form.options" :key="ind" class="option-item">
|
||||
<div style="display:flex;align-items:center;"
|
||||
v-if="['radio', 'checkbox','radio*2'].includes(currentOptionElement)">
|
||||
<!-- 单选题、多选题、填空题的左侧文字ABC -->
|
||||
<div style="width:30px;font-size:14px;padding:9px 0"
|
||||
v-if="['radio', 'checkbox','input'].includes(currentOptionElement)">{{ optionsWordList[ind] }}:</div>
|
||||
<!-- 单选题、多选题、判断题的checkbox -->
|
||||
<el-checkbox-group v-model="form.answer">
|
||||
<el-checkbox :label="optionsWordList[ind]" @change="selectedAnswer(optionsWordList[ind])">{{['radio',
|
||||
'checkbox'].includes(currentOptionElement)?'答案':form.options[ind]}}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<!-- 右侧删除按钮 -->
|
||||
<el-button type="text" icon="i-j-ksap-shanchu" style="color:#ec0000;font-size:14px;margin-left:20px"
|
||||
@click="deleteOption(ind)" v-if="['radio', 'checkbox','input'].includes(currentOptionElement)">删除
|
||||
</el-button>
|
||||
</div>
|
||||
<el-input v-model="form.options[ind]" placeholder="请输入选项内容"
|
||||
v-if="['radio', 'checkbox'].includes(currentOptionElement)"></el-input>
|
||||
<div style="display:flex;align-items:center;" v-if="['input'].includes(currentOptionElement)">
|
||||
<!-- 单选题、多选题、填空题的左侧文字ABC -->
|
||||
<div style="min-width:60px;font-size:14px;padding:15px 10px 15px 0">填空{{ ind+1 }}:</div>
|
||||
<!-- 右侧删除按钮 -->
|
||||
<el-input v-model="form.options[ind]" placeholder="请输入填空内容" style="flex-basis:0;flex-grow:1;"></el-input>
|
||||
<el-button type="text" icon="i-j-ksap-shanchu" style="color:#ec0000;font-size:14px;padding:9px 10px"
|
||||
@click="deleteOption(ind)">删除</el-button>
|
||||
</div>
|
||||
<!-- 选项输入框 -->
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="pramiry" v-if="['radio', 'checkbox','input'].includes(currentOptionElement)" plain
|
||||
icon="el-icon-plus" class="addbtn-restyle mt" @click="addOption">增加选项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 简答题解答和关键字的编辑 -->
|
||||
<div class="gy-form-item" v-if="['textarea'].includes(currentOptionElement)">
|
||||
<div class="gy-label ">解答:</div>
|
||||
<el-input type="textarea" v-model="form.answer[0]"></el-input>
|
||||
</div>
|
||||
<!-- <div class="gy-form-item" style="padding:16px 0;" v-if="['textarea'].includes(currentOptionElement)">
|
||||
<div class="gy-label ">关键词:</div>
|
||||
<TagEditer v-model="form.options"></TagEditer>
|
||||
</div> -->
|
||||
<div class="gy-unform-content">
|
||||
<div class="gy-form-item">
|
||||
<div class="gy-label" style="padding:10px 0">答案解析:</div>
|
||||
</div>
|
||||
<div class="gy-form-item">
|
||||
<el-input type="textarea" v-model="form.analysis" placeholder="可依据需求填写答案解析内容"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 试题导入开始 -->
|
||||
<ImportQuestions v-if="activeName && activeName.id===-1"></ImportQuestions>
|
||||
<!-- 试题导入结束 -->
|
||||
<div style="width:100%;margin-top:30px;" v-if="activeName && activeName.id!==-1">
|
||||
<ActionBar center @onConfirm="confirmQuestion" @onCancel="cancelQuestion"></ActionBar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import { getQuestionTypeOptionsApi, createQuestionApi, getQuestionByIdApi, patchQuestionByIdApi } from '@/api/assessment-evaluation/questions'
|
||||
import { ActionBar } from '@/components/layout'
|
||||
// import TagEditer from './components/TagEditer.vue'
|
||||
import QuestionClassifySelector from './components/QuestionClassifySelector.vue'
|
||||
import KnowledgeSelector from './components/KnowledgeSelector.vue'
|
||||
import QuestionDifficultyLevelSelector from './components/QuestionDifficultyLevelSelector.vue'
|
||||
import QuestionTypeTabBar from './components/QuestionTypeTabBar.vue'
|
||||
import { formatJsonPropertyToMap, formatMapPropertyToJson } from '@/views/assessment-evaluation/utils/questionInfoMapJsonTrans'
|
||||
import ImportQuestions from './components/ImportQuestions.vue'
|
||||
const globalWords = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
|
||||
export default {
|
||||
components: { ActionBar, /* TagEditer, */ QuestionClassifySelector, KnowledgeSelector, QuestionDifficultyLevelSelector, QuestionTypeTabBar, ImportQuestions },
|
||||
data: () => ({
|
||||
page_is_loading: false,
|
||||
activeName: null,
|
||||
questionTypeOptions: [],
|
||||
optionsWordList: globalWords,
|
||||
modifyBackupForm: {},
|
||||
form: {
|
||||
type: null,
|
||||
difficultyLevel: null,
|
||||
title: '',
|
||||
options: [],
|
||||
answer: [],
|
||||
classifyId: null,
|
||||
knowledgeId: null,
|
||||
analysis: ''
|
||||
}
|
||||
// rules: {
|
||||
// classifyId: { required: true, message: '请选择分类', trigger: 'blur' }
|
||||
// }
|
||||
}),
|
||||
props: {
|
||||
questionId: {
|
||||
require: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isModify () {
|
||||
return this.$route.params.questionId != null
|
||||
},
|
||||
currentOptionElement () {
|
||||
return this.activeName ? this.activeName.element : null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
activeName: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.typeOnChangeHandler(nv)
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'classifyId', label: '科目' }
|
||||
// { prop: 'knowledgeId', label: '知识点' },
|
||||
// { prop: 'difficultyLevel', label: '难度' }
|
||||
]
|
||||
},
|
||||
activated () {
|
||||
this.findQuestionTypeOptions().then(() => {
|
||||
this.getRouteParamsAndRequest()
|
||||
})
|
||||
},
|
||||
deactivated () {
|
||||
this.initFormData()
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
获取路由传参
|
||||
*/
|
||||
getRouteParamsAndRequest () {
|
||||
const { questionId } = this.$route.params
|
||||
const { activeTag } = this.$route.query
|
||||
|
||||
if (questionId) {
|
||||
this.getQuestionDetails(questionId)
|
||||
}
|
||||
if (!isNaN(parseInt(activeTag))) {
|
||||
this.changeQuestionTypeTag(activeTag)
|
||||
}
|
||||
},
|
||||
/*
|
||||
表单信息
|
||||
*/
|
||||
changeFormInitData () {
|
||||
if (!this.isModify) {
|
||||
this.activeName != null && (this.form = {
|
||||
type: this.activeName.id,
|
||||
difficultyLevel: null,
|
||||
title: '',
|
||||
options: [],
|
||||
answer: [],
|
||||
classifyId: null,
|
||||
knowledgeId: null,
|
||||
analysis: ''
|
||||
})
|
||||
} else {
|
||||
this.form = JSON.parse(JSON.stringify(this.modifyBackupForm))
|
||||
this.form.type = this.activeName.id
|
||||
}
|
||||
if (this.modifyBackupForm && this.activeName.id === this.modifyBackupForm.type) {
|
||||
this.form.answer = [...this.modifyBackupForm.answer]
|
||||
this.form.options = [...this.modifyBackupForm.options]
|
||||
}
|
||||
// this.form = JSON.parse(JSON.stringify(this.modifyBackupForm))
|
||||
if (this.form.answer && ['radio', 'checkbox'].includes(this.currentOptionElement)) {
|
||||
if (this.form.answer.every(item => typeof item === 'number')) {
|
||||
this.form.answer = []
|
||||
}
|
||||
this.optionsWordList = globalWords
|
||||
}
|
||||
if (['radio*2'].includes(this.currentOptionElement)) {
|
||||
this.form.answer = (this.modifyBackupForm?.element === 'radio*2' && this.modifyBackupForm?.answer) ? this.modifyBackupForm.answer : [1]
|
||||
this.form.options = ['正确', '错误']
|
||||
this.optionsWordList = [1, 0]
|
||||
}
|
||||
if (['radio', 'x'].includes(this.currentOptionElement)) {
|
||||
if (this.form.answer && this.form.answer.length > 1) {
|
||||
this.form.answer.splice(1)
|
||||
}
|
||||
}
|
||||
},
|
||||
/*
|
||||
初始化表单数据
|
||||
*/
|
||||
initFormData () {
|
||||
if (this.isModify) {
|
||||
this.modifyBackupForm = null
|
||||
// this.cancelQuestion();
|
||||
}
|
||||
if (this.activeName != null) {
|
||||
(this.form = {
|
||||
type: this.activeName.id,
|
||||
difficultyLevel: null,
|
||||
title: '',
|
||||
options: [],
|
||||
answer: [],
|
||||
classifyId: null,
|
||||
knowledgeId: null,
|
||||
analysis: ''
|
||||
})
|
||||
if (['radio*2'].includes(this.currentOptionElement)) {
|
||||
this.form.answer = [1]
|
||||
this.form.options = ['正确', '错误']
|
||||
this.optionsWordList = [1, 0]
|
||||
}
|
||||
}
|
||||
},
|
||||
/*
|
||||
取消添加
|
||||
*/
|
||||
cancelQuestion () {
|
||||
this.initFormData()
|
||||
this.$router.replace({ path: '/assessment-evaluation/question-bank-manage' })
|
||||
},
|
||||
/*
|
||||
删除选项
|
||||
*/
|
||||
deleteOption (ind) {
|
||||
this.form.options.splice(ind, 1)
|
||||
},
|
||||
/*
|
||||
新增选项
|
||||
*/
|
||||
addOption () {
|
||||
const optionsList = this.form.options
|
||||
if (['radio', 'checkbox'].includes(this.currentOptionElement)) {
|
||||
if (optionsList.length >= this.optionsWordList.length) return this.$message.error('最多26个选项')
|
||||
}
|
||||
optionsList.push('')
|
||||
},
|
||||
handleClick (e) {
|
||||
|
||||
},
|
||||
/*
|
||||
试题类型改变的回调【watch】
|
||||
*/
|
||||
typeOnChangeHandler (nv) {
|
||||
if (nv) {
|
||||
this.form.type = nv.id
|
||||
if (!this.isModify) {
|
||||
this.initFormData()
|
||||
}
|
||||
this.changeFormInitData()
|
||||
}
|
||||
},
|
||||
/*
|
||||
根据ID获取试题详情
|
||||
*/
|
||||
getQuestionDetails (id) {
|
||||
this.page_is_loading = true
|
||||
getQuestionByIdApi(id).then(res => {
|
||||
const { data } = res
|
||||
const newData = formatJsonPropertyToMap(data)
|
||||
this.changeQuestionTypeTag(newData.type).then(_ => {
|
||||
this.modifyBackupForm = newData
|
||||
this.form = JSON.parse(JSON.stringify(newData))
|
||||
})
|
||||
}).finally(_ => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
/*
|
||||
试题类型的tab bar 被切换
|
||||
*/
|
||||
changeQuestionTypeTag (xid) {
|
||||
const selfComponent = this
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let count = 20
|
||||
function changex (idx, self) {
|
||||
if (self.questionTypeOptions.length > 0) {
|
||||
const item = self.questionTypeOptions.find(item => parseInt(item.id) === parseInt(idx))
|
||||
self.$nextTick(_ => {
|
||||
self.activeName = item
|
||||
})
|
||||
resolve()
|
||||
} else {
|
||||
count--
|
||||
if (count > 0) {
|
||||
setTimeout(() => {
|
||||
changex(idx, self)
|
||||
}, 100)
|
||||
} else {
|
||||
reject(new Error('超时请求试题类型'))
|
||||
}
|
||||
}
|
||||
}
|
||||
changex(xid, selfComponent)
|
||||
})
|
||||
},
|
||||
/*
|
||||
答案被选择的回调
|
||||
*/
|
||||
selectedAnswer (e) {
|
||||
// 单选题keyi
|
||||
if (this.activeName.element === 'radio' || this.activeName.element === 'radio*2') {
|
||||
this.form.answer = [e]
|
||||
}
|
||||
},
|
||||
/*
|
||||
提交试题后的回调
|
||||
*/
|
||||
confirmQuestion () {
|
||||
const form = this.form
|
||||
if (form.classifyId == null) return this.$message.error('请选择科目')
|
||||
// if (form.knowledgeId == null) return this.$message.info('请选择知识点')
|
||||
if (form.difficultyLevel == null) return this.$message.error('请选择难度')
|
||||
if (form.title === '') return this.$message.error('请填写题目')
|
||||
// if (form.options.length === 0) return this.$message.error('请添加选项或关键词') //修改为重复率
|
||||
if (!['textarea'].includes(this.currentOptionElement) && form.options.length === 0) return this.$message.error('请添加选项')
|
||||
if (['radio', 'checkbox', 'radio*2'].includes(this.currentOptionElement) && form.answer.length === 0) return this.$message.error('请确定正确答案或选项')
|
||||
if ((this.activeName.element === 'radio' || this.activeName.element === 'radio*2') && form.answer.length > 1) return this.$message.error('题型只能选择一个正确答案')
|
||||
if (form.options.filter(item => item === '' || item === null).length > 0) return this.$message.error('选项不能为空')
|
||||
const newForm = formatMapPropertyToJson(this.form)
|
||||
if (newForm.knowledgeId === 'null' || newForm.knowledgeId === '') newForm.knowledgeId = null
|
||||
|
||||
this.page_is_loading = true
|
||||
/*
|
||||
是否为修改试题
|
||||
*/
|
||||
if (this.isModify) {
|
||||
patchQuestionByIdApi(newForm).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success('修改成功')
|
||||
this.cancelQuestion()
|
||||
// this.initFormData()
|
||||
}
|
||||
}).catch(err => {
|
||||
console.info(err)
|
||||
}).finally(_ => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
} else {
|
||||
createQuestionApi(newForm).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success('添加成功')
|
||||
this.initFormData()
|
||||
}
|
||||
}).catch(err => {
|
||||
console.info(err)
|
||||
}).finally(_ => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
/*
|
||||
获取所有试题类型选项 用于渲染tabbar
|
||||
*/
|
||||
findQuestionTypeOptions () {
|
||||
// 获取试题分类
|
||||
return new Promise((resolve, reject) => {
|
||||
this.page_is_loading = true
|
||||
getQuestionTypeOptionsApi().then(({ data }) => {
|
||||
this.questionTypeOptions = data
|
||||
if (!this.isModify) {
|
||||
this.questionTypeOptions.push({ id: -1, name: '导入' })
|
||||
}
|
||||
this.activeName = this.activeName != null ? this.activeName : this.questionTypeOptions[0]
|
||||
}).catch(err => {
|
||||
console.info(err)
|
||||
}).finally(_ => {
|
||||
this.page_is_loading = false
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.el-form.el-form--label-left {
|
||||
.el-form-item__label {
|
||||
width: 60px !important;
|
||||
padding: 0 0 0 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
// .option-item+.option-item{
|
||||
// margin-top: 16px;
|
||||
// }
|
||||
.radio-option.horizontal-option{
|
||||
display:flex;
|
||||
.option-item{
|
||||
margin-right:20px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,594 @@
|
||||
<template>
|
||||
<div
|
||||
class="gy-question-item gy-form"
|
||||
:tag="formatQuestion.id"
|
||||
v-if="formatQuestion != null"
|
||||
>
|
||||
<!-- 左侧显示分类 -->
|
||||
<div
|
||||
class="question-type-box"
|
||||
v-if="!$route.fullPath.includes('print-paper')"
|
||||
>
|
||||
<div class="question-type">{{ formatQuestion.type }}</div>
|
||||
</div>
|
||||
<!-- 左侧显示分类结束 -->
|
||||
|
||||
<!-- 右侧显示试题相关 -->
|
||||
<div class="question-area">
|
||||
<!-- 试题及选项区域 -->
|
||||
|
||||
<div class="question-answer-area">
|
||||
<div class="question-title answer-item" style="margin-bottom: 10px">
|
||||
<div class="title-text">{{ serial }}. {{ formatQuestion.title }}</div>
|
||||
<div
|
||||
class="title-flag"
|
||||
v-if="[0, 2].includes(mode)"
|
||||
@click="addToFlagList"
|
||||
:style="{ color: isFlag ? '#ec0000' : '#989898' }"
|
||||
>
|
||||
<i class="i-x-kskk-biaoji"></i>
|
||||
</div>
|
||||
<div v-if="[1, 3].includes(mode)" style="margin-left: 10px">
|
||||
<i
|
||||
class="el-icon-check"
|
||||
style="font-size: 20px; color: #07c885"
|
||||
v-if="formatQuestion.userScore >= formatQuestion.score"
|
||||
></i>
|
||||
<i
|
||||
class="el-icon-close"
|
||||
style="font-size: 20px; color: #ee0000"
|
||||
v-if="formatQuestion.userScore < formatQuestion.score"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <p>typeid:{{ formatQuestion.typeId }} element:{{ formatQuestion.element }} id:{{ formatQuestion.id }} </p> -->
|
||||
<!-- <p>answer:{{ formatQuestion.answer }}</p> -->
|
||||
<!-- <p>options:{{ formatQuestion.options }}</p> -->
|
||||
<div class="main-form-area" v-if="[0, 2, 5, 6].includes(mode)">
|
||||
<div
|
||||
class="radio-options-list"
|
||||
v-if="
|
||||
['radio', 'checkbox', 'radio*2'].includes(formatQuestion.element)
|
||||
"
|
||||
>
|
||||
<el-checkbox-group
|
||||
v-model="locolAnswers"
|
||||
v-if="Object.keys(formatQuestion.options[0]).includes('value')"
|
||||
:class="{
|
||||
vertical: ['radio', 'checkbox'].includes(
|
||||
formatQuestion.element
|
||||
),
|
||||
}"
|
||||
>
|
||||
<el-checkbox
|
||||
class="answer-item"
|
||||
v-for="(item, ind) in formatQuestion.options"
|
||||
:key="ind"
|
||||
:label="
|
||||
['radio', 'checkbox'].includes(formatQuestion.element)
|
||||
? item.value
|
||||
: radioWords[ind]
|
||||
"
|
||||
>
|
||||
{{
|
||||
["radio", "checkbox"].includes(formatQuestion.element)
|
||||
? radioWords[ind] + "、" + item.title
|
||||
: item
|
||||
}}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<el-checkbox-group
|
||||
v-model="locolAnswers"
|
||||
v-else
|
||||
:class="{
|
||||
vertical: ['radio', 'checkbox'].includes(
|
||||
formatQuestion.element
|
||||
),
|
||||
}"
|
||||
>
|
||||
<el-checkbox
|
||||
class="answer-item"
|
||||
v-for="(item, ind) in formatQuestion.options"
|
||||
:key="ind"
|
||||
:label="radioWords[ind]"
|
||||
>
|
||||
{{
|
||||
formatQuestion.element === "radio*2"
|
||||
? item
|
||||
: radioWords[ind] + "、" + item
|
||||
}}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<!-- 填空题 -->
|
||||
<div
|
||||
class="input-options-list"
|
||||
v-if="['input'].includes(formatQuestion.element)"
|
||||
>
|
||||
<div class="gy-form">
|
||||
<div
|
||||
class="gy-form-item input answer-item"
|
||||
v-for="(item, ind) in formatQuestion.options"
|
||||
:key="ind"
|
||||
>
|
||||
<div class="gy-label answer-item">填空{{ ind + 1 }}:</div>
|
||||
<el-input
|
||||
class="answer-item"
|
||||
type="text"
|
||||
v-model="locolAnswers[ind]"
|
||||
placeholder="请输入答案"
|
||||
></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 简答题 -->
|
||||
<div
|
||||
class="textarea-options-list"
|
||||
v-if="['textarea'].includes(formatQuestion.element)"
|
||||
>
|
||||
<div class="gy-form">
|
||||
<div class="gy-form-item answer-item">
|
||||
<div class="gy-label">答案:</div>
|
||||
<el-input
|
||||
class="answer-item"
|
||||
type="textarea"
|
||||
v-model="locolAnswers[0]"
|
||||
placeholder="请输入答案"
|
||||
></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 预览模式 -->
|
||||
<div v-if="[1, 3, 4].includes(mode)">
|
||||
<div
|
||||
class="radio-options-list"
|
||||
v-if="['radio', 'checkbox'].includes(formatQuestion.element)"
|
||||
>
|
||||
<el-checkbox-group
|
||||
v-model="locolAnswers"
|
||||
:class="{
|
||||
vertical: ['radio', 'checkbox'].includes(
|
||||
formatQuestion.element
|
||||
),
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="(item, ind) in formatQuestion.options"
|
||||
:key="ind"
|
||||
class="gy-option answer-item"
|
||||
>
|
||||
{{
|
||||
["radio", "checkbox"].includes(formatQuestion.element)
|
||||
? radioWords[ind] + "、"
|
||||
: ""
|
||||
}}{{ item }}
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<div class="gy-form">
|
||||
<div
|
||||
class="gy-form-item mb"
|
||||
style="color: #ff943d"
|
||||
v-if="[1, 3].includes(mode)"
|
||||
>
|
||||
<div class="gy-label answer-item" style="color: #ff943d">
|
||||
学员答案:
|
||||
</div>
|
||||
<div
|
||||
v-if="['radio', 'checkbox'].includes(formatQuestion.element)"
|
||||
>
|
||||
{{ formatQuestion.userAnswer?.join("、") }}
|
||||
</div>
|
||||
<div
|
||||
class="answer-item"
|
||||
v-if="
|
||||
['radio*2'].includes(formatQuestion.element) &&
|
||||
formatQuestion.userAnswer?.length > 0
|
||||
"
|
||||
>
|
||||
{{ ["错误", "正确"][formatQuestion.userAnswer[0]] }}
|
||||
</div>
|
||||
<div
|
||||
class="answer-item"
|
||||
v-if="['input'].includes(formatQuestion.element)"
|
||||
>
|
||||
{{ formatQuestion.userAnswer?.join("、") }}
|
||||
</div>
|
||||
<div
|
||||
class="answer-item"
|
||||
v-if="
|
||||
['textarea'].includes(formatQuestion.element) &&
|
||||
formatQuestion?.userAnswer?.length > 0
|
||||
"
|
||||
>
|
||||
{{ formatQuestion?.userAnswer[0] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form-item mb answer-item" style="color: #07c885">
|
||||
<div class="gy-label answer-item" style="color: #07c885">
|
||||
正确答案:
|
||||
</div>
|
||||
<div
|
||||
class="answer-item"
|
||||
v-if="['radio', 'checkbox'].includes(formatQuestion.element)"
|
||||
>
|
||||
{{ formatQuestion.answer?.join("、") }}
|
||||
</div>
|
||||
<div
|
||||
class="answer-item"
|
||||
v-if="
|
||||
['radio*2'].includes(formatQuestion.element) &&
|
||||
formatQuestion.answer?.length > 0
|
||||
"
|
||||
>
|
||||
{{ ["错误", "正确"][formatQuestion.answer[0]] }}
|
||||
</div>
|
||||
<div
|
||||
class="answer-item"
|
||||
v-if="['input'].includes(formatQuestion.element)"
|
||||
>
|
||||
{{ formatQuestion.options?.join("、") }}
|
||||
</div>
|
||||
<div
|
||||
class="answer-item"
|
||||
v-if="
|
||||
['textarea'].includes(formatQuestion.element) &&
|
||||
formatQuestion?.answer?.length > 0
|
||||
"
|
||||
style="line-height: 1.3"
|
||||
>
|
||||
{{ formatQuestion?.answer[0] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gy-form-item mb" style="color: #ee0000">
|
||||
<div class="gy-label answer-item" style="color: #ee0000">
|
||||
答案解析:
|
||||
</div>
|
||||
<div class="answer-item" style="line-height: 1.3">
|
||||
{{ formatQuestion.analysis }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gy-form-item mb"
|
||||
v-if="
|
||||
mode === 3 &&
|
||||
['input', 'textarea'].includes(formatQuestion.element)
|
||||
"
|
||||
style="color: #ee0000"
|
||||
>
|
||||
<div class="gy-label middle answer-item">本题得分:</div>
|
||||
<div v-if="mode === 1" class="answer-item">
|
||||
{{ formatQuestion.teacherComment }}
|
||||
</div>
|
||||
<div v-if="mode === 3" style="width: 80px" class="answer-item">
|
||||
<el-input
|
||||
v-model="theQuestionEvalInfo.score"
|
||||
type="number"
|
||||
min="0"
|
||||
></el-input>
|
||||
</div>
|
||||
<div
|
||||
class="gy-label middle answer-item"
|
||||
style="margin-left: 5px; font-weight: normal"
|
||||
>
|
||||
分,本题分数{{ formatQuestion.score }}分
|
||||
<!-- <el-button type="text" icon="el-icon-check" style="margin-left: 10px;">确认</el-button>
|
||||
<el-button type="text" icon="el-icon-delete">取消</el-button> -->
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gy-form-item answer-item"
|
||||
v-if="
|
||||
['input', 'textarea'].includes(formatQuestion.element) &&
|
||||
mode != 4
|
||||
"
|
||||
>
|
||||
<div class="gy-label middle answer-item">老师评语:</div>
|
||||
<div v-if="mode === 1" class="answer-item">
|
||||
{{ formatQuestion.teacherComment }}
|
||||
</div>
|
||||
<div v-if="mode === 3" class="answer-item" style="flex-grow: 1">
|
||||
<el-input
|
||||
v-model="theQuestionEvalInfo.comment"
|
||||
placeholder="如需输入评语,请输入"
|
||||
></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 题目结束 -->
|
||||
<div class="question-answer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧试题结束 -->
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import { formatJsonPropertyToMap } from '@/views/assessment-evaluation/utils/questionInfoMapJsonTrans'
|
||||
const globalWords = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z'
|
||||
]
|
||||
export default {
|
||||
props: {
|
||||
questionInfo: { type: Object, default: () => ({}) },
|
||||
isFlag: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
type: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
serial: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
random: {
|
||||
default: () => [0, 0],
|
||||
type: Array
|
||||
},
|
||||
// 同questionsList组件mode属性
|
||||
// 模式:0答题 1预览 2模拟考试 3判卷 4错题巩固-背题模式 5-错题巩固-答题模式 6-预览自动选中答案
|
||||
mode: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
answer: {}
|
||||
},
|
||||
data: () => ({
|
||||
formatQuestion: null,
|
||||
// checkedOptions: [],
|
||||
locolAnswers: [],
|
||||
radioWords: globalWords,
|
||||
theQuestionEvalInfo: {
|
||||
score: 0,
|
||||
comment: ''
|
||||
}
|
||||
}),
|
||||
watch: {
|
||||
theQuestionEvalInfo: {
|
||||
deep: true,
|
||||
immediate: false,
|
||||
handler (nv) {
|
||||
if (['input', 'textarea'].includes(this.formatQuestion.element)) {
|
||||
if (
|
||||
+nv.score === +this.formatQuestion.userScore &&
|
||||
(nv.comment === '' ||
|
||||
nv.comment == null ||
|
||||
nv.comment === this.formatQuestion.teacherComment)
|
||||
) {
|
||||
this.$emit('eval', { id: this.formatQuestion.resultId })
|
||||
} else {
|
||||
this.$emit('eval', {
|
||||
id: this.formatQuestion.resultId,
|
||||
score: +nv.score,
|
||||
comment: nv.comment
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
answer: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.$nextTick(() => {
|
||||
if (nv) {
|
||||
this.locolAnswers = nv
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
/*
|
||||
试题信息被修改
|
||||
*/
|
||||
questionInfo: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler (nv) {
|
||||
this.locolAnswers = []
|
||||
this.formatQuestion = nv != null ? formatJsonPropertyToMap(nv) : null
|
||||
/*
|
||||
如果是判断题
|
||||
*/
|
||||
if (nv.element === 'radio*2') {
|
||||
this.radioWords = [1, 0]
|
||||
} else {
|
||||
this.radioWords = globalWords
|
||||
}
|
||||
if (this.mode === 3) {
|
||||
this.theQuestionEvalInfo = {
|
||||
score: this.formatQuestion.userScore,
|
||||
comment: this.formatQuestion.teacherComment
|
||||
}
|
||||
}
|
||||
if (this.mode === 6) {
|
||||
if (nv.element === 'input') {
|
||||
this.locolAnswers = this.formatQuestion.options
|
||||
} else {
|
||||
this.locolAnswers = this.formatQuestion.answer
|
||||
}
|
||||
}
|
||||
if (Object.keys(this.formatQuestion).includes('userAnswer')) {
|
||||
this.locolAnswers = this.formatQuestion.userAnswer
|
||||
}
|
||||
if (this.random[1] === 1) {
|
||||
if (['radio', 'checkbox'].includes(this.formatQuestion.element)) {
|
||||
this.formatQuestion.options = this.formatQuestion.options.map(
|
||||
(item, ind) => {
|
||||
return {
|
||||
value: this.radioWords[ind],
|
||||
title: item
|
||||
}
|
||||
}
|
||||
)
|
||||
this.formatQuestion.options.sort(() => Math.random() - 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
locolAnswers: {
|
||||
deep: true,
|
||||
handler (nv) {
|
||||
if (['radio*2', 'radio'].includes(this.formatQuestion.element)) {
|
||||
if (nv?.length > 1) {
|
||||
this.locolAnswers = nv.splice(1)
|
||||
}
|
||||
}
|
||||
nv = nv != null ? nv : []
|
||||
this.$emit('answerChanged', nv)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {},
|
||||
methods: {
|
||||
addToFlagList () {
|
||||
this.$emit('flag', {
|
||||
isFlag: !this.isFlag,
|
||||
question: this.questionInfo
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.main-form-area {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.gy-form-item {
|
||||
.gy-label {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.gy-form-item.input {
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
.el-input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-options-list {
|
||||
margin-bottom: 15px;
|
||||
::v-deep .el-checkbox-group {
|
||||
&.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.el-checkbox {
|
||||
margin-right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.el-checkbox__label {
|
||||
white-space: pre-wrap;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.el-checkbox__input {
|
||||
padding-top: 3px;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.el-checkbox + .el-checkbox {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
::v-deep .el-checkbox__label {
|
||||
// white-space: pre-wrap;
|
||||
color: black;
|
||||
font-size: 13px;
|
||||
}
|
||||
.question-title {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-weight: bold;
|
||||
.title-text {
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.title-flag {
|
||||
font-size: 20px;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.gy-question-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@keyframes hard-tip {
|
||||
from {
|
||||
color: #000;
|
||||
}
|
||||
to {
|
||||
color: #f00;
|
||||
// font-weight: bold;
|
||||
}
|
||||
}
|
||||
&.shine {
|
||||
.title-text {
|
||||
animation: hard-tip 0.3s infinite alternate-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.question-type-box {
|
||||
flex-basis: 70px;
|
||||
min-width: 70px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.question-area {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.question-type {
|
||||
display: flex;
|
||||
padding: 3px 8px;
|
||||
background: $--color-primary;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,382 @@
|
||||
<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
|
||||
<template>
|
||||
<div class="v-page classify_page">
|
||||
<SearchTreeMenu ref="treeMenuRef" title="试题分类列表" :tree-data="question_classify_list" @current-change="change"
|
||||
@onCreate="toCreateClassify" @onDelete="toDeleteclassify" @onEdit="toEditClassify" v-loading="classify_loading" />
|
||||
<div class="v-ctx" v-loading="questions_list_loading">
|
||||
<FormLayout
|
||||
ref="formLayoutRef"
|
||||
:items="formItems"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="70px"
|
||||
label-position="right"
|
||||
:inline="true"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<!-- <el-input placeholder="请输入查询内容" v-model="form.title" class="input-with-select" clearable>
|
||||
<el-button slot="append" @click="pagingFindList">查询</el-button>
|
||||
</el-input> -->
|
||||
<QueryInput v-model="form.title" @query="pagingFindList"></QueryInput>
|
||||
</template>
|
||||
<template v-slot:type>
|
||||
<el-select v-model="form.type" placeholder="题型" style="width:100px" clearable @change="pagingFindList">
|
||||
<el-option v-for="item in paramsOptions.questionType" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-slot:difficultyLevel>
|
||||
<el-select v-model="form.difficultyLevel" placeholder="难度" style="width:100px" clearable @change="pagingFindList">
|
||||
<el-option v-for="item in paramsOptions.difficultyLevel" :key="item.id" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-slot:btns>
|
||||
<el-button type="danger" @click="deleteQuestions" round >删除选中</el-button>
|
||||
<el-button round @click="importExcel">导入</el-button>
|
||||
<el-button round @click="exportAllQuestions">导出</el-button>
|
||||
<el-button type="primary" @click="addQuestionHandler" round>添加试题</el-button>
|
||||
<!-- icon="el-icon-plus" -->
|
||||
</template>
|
||||
|
||||
</FormLayout>
|
||||
<TableLayout :column="column" :data="table_data" :pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })" @size-change="(e) => pagingChange({ pageSize: e })"
|
||||
selection @selection-change="handleSelectionChange">
|
||||
<template v-slot:action="props">
|
||||
<!-- <el-button type="text" icon="el-icon-edit">{{ props.row.type }}</el-button> -->
|
||||
<el-button type="text" icon="i-j-ksap-bianji2" @click="editQuestion(props.row)">编辑</el-button>
|
||||
<el-button type="text" icon="i-j-fbks-yulan" @click="currentQuestion = props.row;previewQuestionShow=true">预览</el-button>
|
||||
<el-button type="text" icon="i-j-ksap-shanchu" @click="deleteQuestions($event,props.row)" style="color:#f04343 !important">删除</el-button>
|
||||
</template>
|
||||
<template v-slot:student="props">
|
||||
<!-- icon="el-icon-plus" -->
|
||||
<el-switch
|
||||
v-if="props.row.creatorId === $store.user.id"
|
||||
v-model="props.row.student"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="changeItemStatus($event,props.row)"
|
||||
>
|
||||
</el-switch>
|
||||
</template>
|
||||
</TableLayout>
|
||||
</div>
|
||||
<DialogLayout :visible="previewQuestionShow" title="预览" :actionBarOption="{noCencel:true,noConfirm:true}" :shadowBar="false" @onCancel="previewQuestionShow=false" width="500px">
|
||||
<div>
|
||||
<QuestionItem :questionInfo="currentQuestion" :mode="6"></QuestionItem>
|
||||
</div>
|
||||
</DialogLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { SearchTreeMenu, TableLayout, FormLayout, DialogLayout } from '@/components/layout'
|
||||
import QuestionItem from './components/QuestionItem.vue'
|
||||
import QueryInput from '@/components/widget/QueryInput.vue'
|
||||
import { findAllQuestionsClassifyApi, createQuestionsClassifyApi, deleteQuestionsClassifyApi, pagingFindQuestionsApi, getAllQueryParamsOptionsApi, deleteSomeQuestionsApi, editQuestionsClassifyApi, exportQuestionsApi, studentCanUseApi } from '@/api/assessment-evaluation/questions'
|
||||
import * as XLSX from 'xlsx'
|
||||
export default {
|
||||
components: {
|
||||
SearchTreeMenu, TableLayout, FormLayout, QuestionItem, QueryInput, DialogLayout
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
previewQuestionShow: false,
|
||||
currentQuestion: null,
|
||||
question_classify_list: [],
|
||||
classify_default: [],
|
||||
table_data: [],
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
current_classify_id: null,
|
||||
// 试题分类树是否加载中
|
||||
classify_loading: false,
|
||||
questions_list_loading: false,
|
||||
form: { title: '', type: null, difficultyLevel: null },
|
||||
rules: [],
|
||||
formItems: [],
|
||||
paramsOptions: {},
|
||||
// 表格当前选中项
|
||||
tableSelectionsList: []
|
||||
}),
|
||||
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'title' },
|
||||
{ prop: 'type' },
|
||||
{ prop: 'difficultyLevel' },
|
||||
{ prop: 'btns', model: { class: 'gy-btns' } }
|
||||
// {
|
||||
// prop: 'desc',
|
||||
// label: '角色描述',
|
||||
// model: { tag: 'el-input', type: 'textarea' }
|
||||
// }
|
||||
]
|
||||
this.rules = {
|
||||
// name: { required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
}
|
||||
this.column = [
|
||||
{ prop: 'title', label: '题目', width: 200, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'classify', label: '试题分类', align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'type', label: '试题类型', width: 100, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'difficultyLevel', label: '试题难度', width: 100, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'createTime', label: '创建时间', width: 100, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'creator', label: '创建人', align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'action', label: '操作', width: 200, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: 'student', label: '学员可选', width: 80, align: 'center', 'show-overflow-tooltip': true }
|
||||
]
|
||||
|
||||
this.findAllQuestionsClassify()
|
||||
},
|
||||
activated () {
|
||||
this.pagingFindList()
|
||||
this.findAllQueryParams()
|
||||
},
|
||||
methods: {
|
||||
changeItemStatus (e, row) {
|
||||
this.changeQuestionStudentUse(row, e)
|
||||
},
|
||||
changeQuestionStudentUse (row, studentCanUse) {
|
||||
const form = {
|
||||
id: row.id,
|
||||
student: studentCanUse
|
||||
}
|
||||
studentCanUseApi(form).then((res) => {
|
||||
if (res.code !== 0) {
|
||||
paperItem.student = studentCanUse === 1 ? 0 : 1
|
||||
this.$message.error('修改失败')
|
||||
}
|
||||
})
|
||||
},
|
||||
// 导出所有试题
|
||||
exportAllQuestions () {
|
||||
let classifyId = this.current_classify_id
|
||||
if (this.current_classify_id === 'system' || !this.current_classify_id) {
|
||||
classifyId = ''
|
||||
}
|
||||
const classifyItem = this.classify_default.find(item => item.id === classifyId)
|
||||
exportQuestionsApi(classifyId).then(res => {
|
||||
const { data } = res
|
||||
const sheetDatas = data.reduce((prev, item, ind) => {
|
||||
let hasSheet = prev.find(p => p.sheetName === item['类型'])
|
||||
if (hasSheet) {
|
||||
delete item['类型']
|
||||
hasSheet.data.push(item)
|
||||
} else {
|
||||
hasSheet = { sheetName: item['类型'], data: [] }
|
||||
delete item['类型']
|
||||
hasSheet.data.push(item)
|
||||
prev.push(hasSheet)
|
||||
}
|
||||
return prev
|
||||
}, [])
|
||||
|
||||
const sheets = []
|
||||
sheetDatas.forEach((sheetItem) => {
|
||||
const data = sheetItem.data
|
||||
const ws = XLSX.utils.json_to_sheet(data)
|
||||
ws['!cols'] = [{ wch: 60 }, { wch: 10 }, { wch: 30 }, { wch: 40 }, { wch: 40 }, { wch: 40 }]
|
||||
sheets.push({
|
||||
name: sheetItem.sheetName,
|
||||
ws
|
||||
})
|
||||
})
|
||||
const wb = XLSX.utils.book_new()
|
||||
sheets.forEach(wsItem => {
|
||||
XLSX.utils.book_append_sheet(wb, wsItem.ws, wsItem.name)
|
||||
})
|
||||
// XLSX.writeFile(wb, `试题导入模板${(new Date()).format('yyyy-MM-dd hh-mm-ss')}.xlsx`)
|
||||
// const ws = XLSX.utils.json_to_sheet(data)
|
||||
// ws['!cols'] = [{ wch: 60 }, { wch: 10 }, { wch: 10 }, { wch: 30 }, { wch: 40 }, { wch: 40 }, { wch: 40 }]
|
||||
// const wb = XLSX.utils.book_new()
|
||||
// XLSX.utils.book_append_sheet(wb, ws, 'Sheet1')
|
||||
let fileName = ''
|
||||
if (classifyItem) {
|
||||
fileName += classifyItem.name + ' - '
|
||||
this.$message(`您当前选中的【${classifyItem.name}】分类,已导出该分类下的试题`)
|
||||
}
|
||||
fileName += (new Date()).format('yyyy-MM-dd hh-mm-ss')
|
||||
XLSX.writeFile(wb, fileName + '.xlsx')
|
||||
})
|
||||
},
|
||||
// 编辑分类
|
||||
toEditClassify (item) {
|
||||
if (item.id === 'system') return this.$message('根分类不能修改')
|
||||
this.$prompt('请输入新分类名:', '编辑', {
|
||||
inputPattern: /.{1,}/,
|
||||
inputErrorMessage: '请输入分类名'
|
||||
}).then(({ value }) => {
|
||||
this.classify_loading = true
|
||||
editQuestionsClassifyApi({ ...item, name: value }).then(res => { this.$message.success('修改成功'); this.findAllQuestionsClassify() }).finally(_ => {
|
||||
this.classify_loading = false
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
// 导入按钮handler
|
||||
importExcel () {
|
||||
this.$router.push({
|
||||
path: '/assessment-evaluation/question-bank-manage/add-modify-question',
|
||||
query: {
|
||||
activeTag: -1
|
||||
}
|
||||
})
|
||||
},
|
||||
// 跳转到编辑
|
||||
editQuestion (row) {
|
||||
this.$router.push({ path: '/assessment-evaluation/question-bank-manage/add-modify-question/' + row.id })
|
||||
},
|
||||
addQuestionHandler () {
|
||||
this.$router.push({ path: '/assessment-evaluation/question-bank-manage/add-modify-question' })
|
||||
// this.$router.push({ path: '/assessment-evaluation/online-test' })
|
||||
},
|
||||
// 列表选中
|
||||
handleSelectionChange (e) {
|
||||
this.tableSelectionsList = e
|
||||
},
|
||||
/*
|
||||
分页变化
|
||||
*/
|
||||
pagingChange (event) {
|
||||
if ((typeof event.currentPage !== 'number') && (typeof event.pageSize !== 'number')) return
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
|
||||
this.pagingFindList()
|
||||
},
|
||||
/*
|
||||
初始化分页信息
|
||||
*/
|
||||
initPageInfo () {
|
||||
this.page_info = { currentPage: 1, pageSize: this.page_info.pageSize, total: 0 }
|
||||
},
|
||||
/*
|
||||
分页获取列表
|
||||
*/
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
let classifyId = this.current_classify_id
|
||||
if (classifyId === 'system') classifyId = null
|
||||
this.questions_list_loading = true
|
||||
pagingFindQuestionsApi({ ...this.page_info, classifyId, ...this.form }).then(res => {
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
}).finally(_ => {
|
||||
this.questions_list_loading = false
|
||||
})
|
||||
},
|
||||
/*
|
||||
获取所有筛选条件选项(包含试题类型和难易程度)
|
||||
*/
|
||||
async findAllQueryParams () {
|
||||
const { data } = await getAllQueryParamsOptionsApi()
|
||||
|
||||
this.paramsOptions = data
|
||||
},
|
||||
/*
|
||||
获取所有试题分类
|
||||
*/
|
||||
async findAllQuestionsClassify () {
|
||||
const { data } = await findAllQuestionsClassifyApi()
|
||||
const list = [
|
||||
{ id: 'system', name: '试题分类', disabled: true, children: [] }
|
||||
]
|
||||
this.classify_default = data
|
||||
this.questionClassifyMap = {}
|
||||
this.questionClassifyMap = data.toTree()
|
||||
|
||||
list[0].children = this.questionClassifyMap.tree
|
||||
this.question_classify_list = list
|
||||
},
|
||||
|
||||
async change (classify) {
|
||||
this.current_classify_id = classify.id
|
||||
this.pagingFindList()
|
||||
},
|
||||
/*
|
||||
创建分类
|
||||
*/
|
||||
toCreateClassify (node) {
|
||||
node?.id === 'system' && (node = null)
|
||||
this.$prompt('请输入分类名', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
}).then(async ({ value }) => {
|
||||
const data = {
|
||||
name: value,
|
||||
pid: node == null ? 0 : node.id,
|
||||
level: node == null ? 1 : node.level + 1
|
||||
}
|
||||
await createQuestionsClassifyApi(data)
|
||||
this.findAllQuestionsClassify()
|
||||
}).catch(_ => { })
|
||||
// this.$refs.treeMenuRef.setCurrentKey(node)
|
||||
// this.active_classify = { name: '', desc: '', base: null, features: [] }
|
||||
},
|
||||
// 批量删除、单个删除
|
||||
deleteQuestions (e, questionItem) {
|
||||
if (this.tableSelectionsList.length === 0 && (questionItem == null)) {
|
||||
this.$message.error('请先选择要删除的项目')
|
||||
} else {
|
||||
let delList = this.tableSelectionsList
|
||||
if (questionItem) {
|
||||
delList = [questionItem]
|
||||
}
|
||||
this.$confirm('确认删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
}).then(res => {
|
||||
this.questions_list_loading = true
|
||||
deleteSomeQuestionsApi(delList.map(_ => _.id)).then(res => {
|
||||
this.pagingFindList()
|
||||
this.questions_list_loading = false
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
/*
|
||||
删除分类
|
||||
*/
|
||||
toDeleteclassify (classify) {
|
||||
if (classify.id === 'system') return
|
||||
new Promise((resolve, reject) => {
|
||||
this.$confirm('此操作将永久删除此分类,及其子级分类,请确认后删除?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
}).then(res => resolve()).catch(_ => reject())
|
||||
}).then(_ => {
|
||||
this.classify_loading = true
|
||||
deleteQuestionsClassifyApi(classify.id).finally(() => {
|
||||
this.findAllQuestionsClassify()
|
||||
this.$refs.treeMenuRef.setCurrentKey()
|
||||
this.classify_loading = false
|
||||
})
|
||||
}).catch(_ => { })
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" >
|
||||
.classify_page .form_layout {
|
||||
margin-top: 16px;
|
||||
|
||||
.suf {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.el-form-item:last-child {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<addModifyPaper :isSimTest="true"></addModifyPaper>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import addModifyPaper from '@/views/assessment-evaluation/examination-paper-manage/add-modify-paper'
|
||||
export default {
|
||||
components: { addModifyPaper },
|
||||
activated () {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
403
front/src/views/assessment-evaluation/simulation-test/index.vue
Normal file
403
front/src/views/assessment-evaluation/simulation-test/index.vue
Normal file
@@ -0,0 +1,403 @@
|
||||
<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
|
||||
<template>
|
||||
<div class="v-page classify_page">
|
||||
<div class="v-ctx" v-loading="paper_list_loading">
|
||||
<FormLayout
|
||||
ref="formLayoutRef"
|
||||
:items="formItems"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="70px"
|
||||
label-position="right"
|
||||
:inline="true"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<QueryInput v-model="form.title" @query="pagingFindList"></QueryInput>
|
||||
</template>
|
||||
<template v-slot:btns>
|
||||
<el-button round type="primary" @click="createSimuTest"
|
||||
>新建考试</el-button
|
||||
>
|
||||
</template>
|
||||
</FormLayout>
|
||||
<TableLayout
|
||||
:column="column"
|
||||
:data="table_data"
|
||||
:pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })"
|
||||
@size-change="(e) => pagingChange({ pageSize: e })"
|
||||
>
|
||||
<template v-slot:examStartTime="props">
|
||||
<div>
|
||||
<p>开始时间:{{ props.row.examStartTime }}</p>
|
||||
<p>结束时间:{{ props.row.examEndTime }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:passPercent="props">
|
||||
<p>
|
||||
{{
|
||||
Math.round((props.row.passPercent * props.row.totalScore) / 100)
|
||||
}}
|
||||
</p>
|
||||
</template>
|
||||
<template v-slot:myDuration="props">
|
||||
<p>{{ formatTime(props.row.myDuration) }}</p>
|
||||
</template>
|
||||
<template v-slot:status="props">
|
||||
<div
|
||||
:style="{
|
||||
'background-color': ['#208ac6', '#01c883', '#999999'][
|
||||
props.row.status
|
||||
],
|
||||
}"
|
||||
class="exam-status"
|
||||
>
|
||||
{{ ["待开考", "已开始", "已结束"][props.row.status] }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:action="props">
|
||||
<!-- <el-button type="text" icon="el-icon-edit">{{ props.row.type }}</el-button> -->
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-x-zxks-canjiakaoshi"
|
||||
style="color: #02c761 !important"
|
||||
v-if="props.row.status == 1"
|
||||
@click="toExamHandler(1, props.row)"
|
||||
>参加考试</el-button
|
||||
>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-fbks-yulan"
|
||||
@click="lookLastExam(props.row)"
|
||||
>查看答卷
|
||||
</el-button>
|
||||
<el-button
|
||||
class="print-icon"
|
||||
type="text"
|
||||
icon="el-icon-printer"
|
||||
@click="toPrint(props.row)"
|
||||
>打印
|
||||
</el-button>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="i-j-ksap-shanchu"
|
||||
@click="deletePapers($event, props.row)"
|
||||
style="color: #f04343 !important"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</TableLayout>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TableLayout, FormLayout } from '@/components/layout'
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import {
|
||||
createExamHistoryApi,
|
||||
getLastedHistoryApi,
|
||||
createSimExamHistoryApi
|
||||
} from '@/api/assessment-evaluation/onlineTest'
|
||||
import {
|
||||
pagingFindSimPaperApi,
|
||||
deleteSomePapersApi
|
||||
} from '@/api/assessment-evaluation/paper'
|
||||
import QueryInput from '@/components/widget/QueryInput.vue'
|
||||
import { dataReportMixin } from '@/utils/data-report'
|
||||
export default {
|
||||
components: {
|
||||
TableLayout,
|
||||
FormLayout,
|
||||
QueryInput
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
table_data: [],
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
// 试题分类树是否加载中
|
||||
paper_list_loading: false,
|
||||
form: { title: '', isPractive: 1 },
|
||||
is_show_info: false,
|
||||
exam_info_dialog_show: false,
|
||||
current_exam: null
|
||||
}),
|
||||
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'title' },
|
||||
{ prop: 'btns', model: { class: 'gy-btns' } }
|
||||
]
|
||||
this.rules = {
|
||||
// name: { required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
}
|
||||
this.column = [
|
||||
{
|
||||
prop: 'title',
|
||||
label: '试卷名',
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
// { prop: 'classify', label: '试卷分类', align: 'center', 'show-overflow-tooltip': true },
|
||||
{
|
||||
prop: 'totalScore',
|
||||
label: '总分',
|
||||
'min-width': 50,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'passPercent',
|
||||
label: '及格分',
|
||||
'min-width': 50,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
// { prop: 'questionCount', label: '试题总数', align: 'center', 'show-overflow-tooltip': true },
|
||||
// { prop: 'creator', label: '创建人', align: 'center', 'show-overflow-tooltip': true },
|
||||
{
|
||||
prop: 'myScore',
|
||||
label: '成绩',
|
||||
'min-width': 50,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'myDuration',
|
||||
label: '考试用时',
|
||||
'min-width': 80,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'mistakes',
|
||||
label: '错题',
|
||||
'min-width': 50,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建时间',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
},
|
||||
{
|
||||
prop: 'action',
|
||||
label: '操作',
|
||||
// width: 350,
|
||||
width: 300,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
}
|
||||
]
|
||||
},
|
||||
activated () {
|
||||
this.pagingFindList()
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
删除试卷回调
|
||||
*/
|
||||
deletePapers (e, examItem) {
|
||||
let delList = this.tableSelectionsList
|
||||
if (examItem) {
|
||||
delList = [examItem]
|
||||
}
|
||||
const tipsText = '确认删除吗?'
|
||||
this.$confirm(tipsText, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
})
|
||||
.then((res) => {
|
||||
this.paper_list_loading = true
|
||||
deleteSomePapersApi(delList.map((_) => _.id)).then((res) => {
|
||||
this.pagingFindList()
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
})
|
||||
.catch((_) => {})
|
||||
},
|
||||
/*
|
||||
创建新的模拟测试页面
|
||||
*/
|
||||
createSimuTest () {
|
||||
this.$router.push({
|
||||
path: '/assessment-evaluation/simulation-test/add-sim-test'
|
||||
})
|
||||
},
|
||||
/*
|
||||
最后一次考试记录信息
|
||||
*/
|
||||
lookLastExam (row) {
|
||||
this.paper_list_loading = true
|
||||
getLastedHistoryApi(row.studentOnlineExamId)
|
||||
.then((res) => {
|
||||
if (res.data == null) {
|
||||
return this.$message.error('该场考试没有查询到您的考试记录')
|
||||
}
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/online-test/begin-online-exam/' +
|
||||
res.data.id,
|
||||
query: { preview: 1 }
|
||||
})
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
formatTime (msTime) {
|
||||
const time = msTime / 1000
|
||||
let hour = Math.floor(time / 60 / 60)
|
||||
hour = hour.toString().padStart(2, '0')
|
||||
let minute = Math.floor(time / 60) % 60
|
||||
minute = minute.toString().padStart(2, '0')
|
||||
let second = Math.floor(time) % 60
|
||||
second = second.toString().padStart(2, '0')
|
||||
return `${hour}:${minute}:${second}`
|
||||
},
|
||||
/*
|
||||
去考试检测
|
||||
*/
|
||||
toExamPaper () {
|
||||
if (this.is_show_info) {
|
||||
this.exam_info_dialog_show = false
|
||||
} else if (+this.current_exam.status === 1) {
|
||||
if (this.current_exam?.examTimes - this.current_exam?.myTimes <= 0) { return this.$message.error('您已没有该场考试的考试机会') }
|
||||
createExamHistoryApi({ onlineExamId: this.current_exam.id }).then(
|
||||
(res) => {
|
||||
if (res.data) {
|
||||
this.$message('开始考试,本次考试将计入考试次数')
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/online-test/begin-online-exam/' +
|
||||
res.data.id
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
this.exam_info_dialog_show = false
|
||||
} else if (+this.current_exam.status === 0) {
|
||||
return this.$message.error('考试暂未开始')
|
||||
} else if (+this.current_exam.status === 2) {
|
||||
return this.$message.error('考试已结束')
|
||||
}
|
||||
},
|
||||
/*
|
||||
去考试参加考试回调
|
||||
*/
|
||||
toExamHandler (type = 2, row) {
|
||||
this.current_exam = row
|
||||
createSimExamHistoryApi({
|
||||
onlineExamId: this.current_exam.studentOnlineExamId
|
||||
}).then((res) => {
|
||||
if (res.data) {
|
||||
this.$message.success('开始考试,本次考试将计入考试次数')
|
||||
this.$router.push({
|
||||
path:
|
||||
'/assessment-evaluation/online-test/begin-online-exam/' +
|
||||
res.data.id
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
/*
|
||||
分页变化
|
||||
*/
|
||||
pagingChange (event) {
|
||||
if (
|
||||
typeof event.currentPage !== 'number' &&
|
||||
typeof event.pageSize !== 'number'
|
||||
) { return }
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
/*
|
||||
初始化分页信息
|
||||
*/
|
||||
initPageInfo () {
|
||||
this.page_info = {
|
||||
currentPage: 1,
|
||||
pageSize: this.page_info.pageSize,
|
||||
total: 0
|
||||
}
|
||||
},
|
||||
/*
|
||||
分页查询
|
||||
*/
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
this.paper_list_loading = true
|
||||
pagingFindSimPaperApi({ ...this.page_info, ...this.form })
|
||||
.then((res) => {
|
||||
const { currentPage, pageSize, total } = res.data
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
},
|
||||
toPrint (row) {
|
||||
this.paper_list_loading = true
|
||||
getLastedHistoryApi(row.studentOnlineExamId)
|
||||
.then((res) => {
|
||||
if (res.data == null) {
|
||||
return this.$message.error('该场考试没有查询到您的考试记录')
|
||||
}
|
||||
this.$router.push({
|
||||
path: '/print-paper/' + row.id,
|
||||
query: { examIdPaperId: res.data.id }
|
||||
})
|
||||
})
|
||||
.finally((_) => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
mixins: [
|
||||
/**
|
||||
* 数据上报
|
||||
*/
|
||||
dataReportMixin('MOCK_EXAMINATION')
|
||||
]
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.classify_page .form_layout {
|
||||
margin-top: 16px;
|
||||
|
||||
.suf {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
.el-form-item:last-child {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.exam-status {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
260
front/src/views/assessment-evaluation/utils/gy.srs.sdk.js
Normal file
260
front/src/views/assessment-evaluation/utils/gy.srs.sdk.js
Normal file
@@ -0,0 +1,260 @@
|
||||
|
||||
//
|
||||
// Copyright (c) 2013-2021 Winlin
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// https://192.168.2.7:8080/rtc/v1/publish/
|
||||
'use strict'
|
||||
const baseUrl = window.GyConfig.baseUrl
|
||||
export function SrsError (name, message) {
|
||||
this.name = name
|
||||
this.message = message
|
||||
this.stack = (new Error()).stack
|
||||
}
|
||||
SrsError.prototype = Object.create(Error.prototype)
|
||||
SrsError.prototype.constructor = SrsError
|
||||
function _getTid () {
|
||||
return Number(parseInt(new Date().getTime() * Math.random() * 100)).toString(16).slice(0, 7)
|
||||
}
|
||||
// Depends on adapter-7.4.0.min.js from https://github.com/webrtc/adapter
|
||||
// Async-awat-prmise based SRS RTC Publisher.
|
||||
export function SrsRtcPublisherAsync () {
|
||||
const self = {
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||
self.constraints = {
|
||||
audio: true,
|
||||
video: {
|
||||
width: { ideal: 1920, max: 1920 }
|
||||
}
|
||||
}
|
||||
self.audioContext = new AudioContext()
|
||||
self.mixedOutput = self.audioContext.createMediaStreamDestination()
|
||||
self.muted = function (mute) {
|
||||
const audioTracks = self.mixedOutput.stream.getAudioTracks()
|
||||
|
||||
audioTracks.forEach(track => {
|
||||
track.enabled = mute
|
||||
})
|
||||
}
|
||||
/**
|
||||
*开始推流
|
||||
* @param {String} url 推流地址
|
||||
* @param {{micro:Boolean}} param1 {micro:false}默认是否静音
|
||||
* @returns
|
||||
*/
|
||||
self.publish = async function (url, {
|
||||
micro
|
||||
// screen
|
||||
}) {
|
||||
self.pc.addTransceiver('video', { direction: 'sendonly' })
|
||||
self.pc.addTransceiver('audio', { direction: 'sendonly' })
|
||||
if (!navigator.mediaDevices && window.location.protocol === 'http:' && window.location.hostname !== 'localhost') {
|
||||
throw new SrsError('HttpsRequiredError', 'Please use HTTPS or localhost to publish, read https://github.com/ossrs/srs/issues/2762#issuecomment-983147576')
|
||||
}
|
||||
// const stream = await navigator.mediaDevices.getUserMedia(self.constraints)
|
||||
let stream = null
|
||||
let stream2 = null
|
||||
stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
audio: true,
|
||||
video: {
|
||||
width: { ideal: 1920, max: 1920 }
|
||||
}
|
||||
})
|
||||
try {
|
||||
stream2 = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: false
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException) {
|
||||
if (error.name === 'NotFoundError') {
|
||||
console.info(error)
|
||||
}
|
||||
}
|
||||
stream2 = null
|
||||
}
|
||||
|
||||
// 多轨音频融合
|
||||
self.audioContext = new AudioContext()
|
||||
self.mixedOutput = self.audioContext.createMediaStreamDestination()
|
||||
if (stream.getAudioTracks().length > 0) {
|
||||
const localMicrophoneStreamNode = self.audioContext.createMediaStreamSource(stream)
|
||||
localMicrophoneStreamNode.connect(self.mixedOutput)
|
||||
}
|
||||
if (stream2 && stream2.getAudioTracks().length > 0) {
|
||||
const localMicrophoneStreamNode2 = self.audioContext.createMediaStreamSource(stream2)
|
||||
localMicrophoneStreamNode2.connect(self.mixedOutput)
|
||||
}
|
||||
self.muted(micro)
|
||||
// stream2.getTracks().forEach(function (track) {
|
||||
// // stream.addTrack(track)
|
||||
// self.pc.addTrack(track)
|
||||
// self.ontrack && self.ontrack({ track })
|
||||
// })
|
||||
const screenStream = new MediaStream()
|
||||
const videoTrack = stream.getVideoTracks()
|
||||
const audioTrack = self.mixedOutput.stream.getTracks()
|
||||
if (videoTrack.length > 0) { screenStream.addTrack(videoTrack[0]) }
|
||||
if (audioTrack.length > 0) {
|
||||
screenStream.addTrack(audioTrack[0])
|
||||
}
|
||||
|
||||
screenStream.getTracks().forEach(function (track) {
|
||||
self.pc.addTrack(track)
|
||||
// Notify about local track when stream is ok.
|
||||
self.ontrack && self.ontrack({ track })
|
||||
})
|
||||
|
||||
const offer = await self.pc.createOffer()
|
||||
|
||||
await self.pc.setLocalDescription(offer)
|
||||
const session = await new Promise(function (resolve, reject) {
|
||||
// @see https://github.com/rtcdn/rtcdn-draft
|
||||
const data = {
|
||||
api: baseUrl + '/rtc/v1/publish/',
|
||||
tid: _getTid(),
|
||||
streamurl: url,
|
||||
clientip: null,
|
||||
sdp: offer.sdp
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.onload = function () {
|
||||
if (xhr.readyState !== xhr.DONE) return
|
||||
if (xhr.status !== 200) return reject(xhr)
|
||||
const data = JSON.parse(xhr.responseText)
|
||||
|
||||
return data.code ? reject(xhr) : resolve(data)
|
||||
}
|
||||
xhr.open('POST', data.api, true)
|
||||
xhr.setRequestHeader('Content-type', 'application/json')
|
||||
xhr.send(JSON.stringify(data))
|
||||
})
|
||||
|
||||
await self.pc.setRemoteDescription(
|
||||
new RTCSessionDescription({ type: 'answer', sdp: session.sdp })
|
||||
)
|
||||
|
||||
// session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/'
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
// Close the publisher.
|
||||
self.close = function () {
|
||||
self.pc && self.pc.close()
|
||||
self.pc = null
|
||||
}
|
||||
|
||||
// The callback when got local stream.
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/addStream#Migrating_to_addTrack
|
||||
self.ontrack = function (event) {
|
||||
// Add track to stream of SDK.
|
||||
self.stream.addTrack(event.track)
|
||||
}
|
||||
|
||||
// Internal APIs.
|
||||
|
||||
self.pc = new RTCPeerConnection(null)
|
||||
self.stream = new MediaStream()
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
export function SrsRtcPlayerAsync () {
|
||||
const self = {}
|
||||
self.play = async function (url) {
|
||||
self.pc.addTransceiver('audio', { direction: 'recvonly' })
|
||||
self.pc.addTransceiver('video', { direction: 'recvonly' })
|
||||
|
||||
const offer = await self.pc.createOffer()
|
||||
await self.pc.setLocalDescription(offer)
|
||||
const session = await new Promise(function (resolve, reject) {
|
||||
// @see https://github.com/rtcdn/rtcdn-draft
|
||||
const data = {
|
||||
api: baseUrl + '/rtc/v1/play/',
|
||||
tid: _getTid(),
|
||||
streamurl: url,
|
||||
clientip: null,
|
||||
sdp: offer.sdp
|
||||
}
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.onload = function () {
|
||||
if (xhr.readyState !== xhr.DONE) return
|
||||
if (xhr.status !== 200) return reject(xhr)
|
||||
const data = JSON.parse(xhr.responseText)
|
||||
|
||||
return data.code ? reject(xhr) : resolve(data)
|
||||
}
|
||||
xhr.open('POST', data.api, true)
|
||||
xhr.setRequestHeader('Content-type', 'application/json')
|
||||
xhr.send(JSON.stringify(data))
|
||||
})
|
||||
await self.pc.setRemoteDescription(
|
||||
new RTCSessionDescription({ type: 'answer', sdp: session.sdp })
|
||||
)
|
||||
// session.simulator = conf.schema + '//' + conf.urlObject.server + ':' + conf.port + '/rtc/v1/nack/'
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
// Close the player.
|
||||
self.close = function () {
|
||||
self.pc && self.pc.close()
|
||||
self.pc = null
|
||||
}
|
||||
self.ontrack = function (event) {
|
||||
self.stream.addTrack(event.track)
|
||||
}
|
||||
self.pc = new RTCPeerConnection(null)
|
||||
|
||||
// Create a stream to add track to the stream, @see https://webrtc.org/getting-started/remote-streams
|
||||
self.stream = new MediaStream()
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack
|
||||
self.pc.ontrack = function (event) {
|
||||
if (self.ontrack) {
|
||||
self.ontrack(event)
|
||||
}
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
export function PIPCameraPlayer () {
|
||||
const self = {
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||
self.constraints = {
|
||||
audio: true,
|
||||
video: {
|
||||
width: { ideal: 1920, max: 1920 }
|
||||
}
|
||||
}
|
||||
self.play = async function () {
|
||||
let stream2 = null
|
||||
try {
|
||||
stream2 = await navigator.mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: true
|
||||
})
|
||||
} catch (error) {
|
||||
console.info(error)
|
||||
throw error
|
||||
}
|
||||
stream2 && stream2.getTracks().forEach(function (track) {
|
||||
self.ontrack && self.ontrack({ track })
|
||||
})
|
||||
}
|
||||
self.ontrack = function (event) {
|
||||
// Add track to stream of SDK.
|
||||
self.stream.addTrack(event.track)
|
||||
}
|
||||
self.stream = new MediaStream()
|
||||
|
||||
return self
|
||||
}
|
||||
271
front/src/views/assessment-evaluation/utils/gyStyle.scss
Normal file
271
front/src/views/assessment-evaluation/utils/gyStyle.scss
Normal file
@@ -0,0 +1,271 @@
|
||||
/* stylelint-disable font-family-no-missing-generic-family-keyword */
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
$--area-title-font-size: 16px;
|
||||
|
||||
.gy-area-title {
|
||||
font-size: $--area-title-font-size;
|
||||
font-weight: bold;
|
||||
padding-left: 12px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
& ~ & {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: $--area-title-font-size;
|
||||
background: $--color-primary;
|
||||
display: block;
|
||||
content: "";
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.gy-option {
|
||||
font-size: 13px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.gy-form {
|
||||
--fix: 60px;
|
||||
|
||||
.addbtn-restyle {
|
||||
border: 1px dashed $--color-primary;
|
||||
color: $--color-primary;
|
||||
background: transparent;
|
||||
font-weight: bold;
|
||||
|
||||
&.mt {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-radio__input {
|
||||
.el-radio__inner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid $--color-primary;
|
||||
background: rgba($--color-primary, 0.1);
|
||||
border-radius: 20%;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.el-radio__inner {
|
||||
&::after {
|
||||
background: rgba($--color-primary, 1);
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
border-radius: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.middle {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.gy-unform-content {
|
||||
padding: 0 0 0 12px;
|
||||
}
|
||||
|
||||
font-family:
|
||||
"Source Han Sans CN-Bold",
|
||||
"Source Han Sans CN";
|
||||
|
||||
&.mb,
|
||||
.mb {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.gy-form-item {
|
||||
display: flex;
|
||||
|
||||
&.middle {
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-textarea {
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.gy-label {
|
||||
flex-basis: auto;
|
||||
min-width: var(--fix);
|
||||
padding: 0 0 0 12px;
|
||||
height: 100%;
|
||||
vertical-align: top;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.require {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
display: block;
|
||||
content: "*";
|
||||
color: #e00;
|
||||
left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&.middle {
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.right {
|
||||
padding: 0 12px 0 0;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.gy-in {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.gy-label + div {
|
||||
.red,
|
||||
&.red {
|
||||
color: #eb5b5c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* form 行排 */
|
||||
&.inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
|
||||
.gy-form-item {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gy-table {
|
||||
box-shadow: 0 0 0 1px $--color-primary;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
td {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border: 1px solid #efefef;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: $--color-primary;
|
||||
border: 1px solid $--color-primary;
|
||||
color: #fff;
|
||||
|
||||
td {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
tr.blod {
|
||||
td {
|
||||
font-weight: bold;
|
||||
padding: 8px 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gy-el-table {
|
||||
.el-table {
|
||||
border-radius: 4px;
|
||||
border: 1px solid $--color-primary;
|
||||
}
|
||||
|
||||
.el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
|
||||
background: rgba($--color-primary, 0.05);
|
||||
}
|
||||
|
||||
.el-table .el-table__cell.el-table-column--selection .cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.gy-form,
|
||||
.gy-el-table {
|
||||
.el-checkbox__input {
|
||||
.el-checkbox__inner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid $--color-primary;
|
||||
background: rgba($--color-primary, 0.1);
|
||||
border-radius: 20%;
|
||||
|
||||
&::after {
|
||||
background: rgba($--color-primary, 1);
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
border-radius: 30%;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
border: none;
|
||||
transition: transform 0.05s ease-in 0.05s;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.el-checkbox__inner {
|
||||
&::after {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.middle {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.gy-check .header,
|
||||
.el-table__header .el-table-column--selection {
|
||||
.el-checkbox__input {
|
||||
.el-checkbox__inner {
|
||||
border: 1px solid #fff;
|
||||
|
||||
&::after {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 将omap中可以转为js类型的属性都转换
|
||||
* @param {Object} omap
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function formatJsonPropertyToMap (omap) {
|
||||
const map = { ...omap }
|
||||
const keys = Object.keys(map)
|
||||
keys.forEach(item => {
|
||||
try {
|
||||
map[item] = JSON.parse(map[item])
|
||||
} catch (error) {
|
||||
}
|
||||
})
|
||||
return map
|
||||
}
|
||||
/**
|
||||
* 将omap中的非原始类型转为json字符串
|
||||
* @param {Object} omap
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function formatMapPropertyToJson (omap) {
|
||||
const map = { ...omap }
|
||||
const keys = Object.keys(map)
|
||||
keys.forEach(item => {
|
||||
if (typeof map[item] === 'object') {
|
||||
map[item] = JSON.stringify(map[item])
|
||||
}
|
||||
})
|
||||
return map
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<!-- eslint-disable vue/no-deprecated-v-bind-sync -->
|
||||
<template>
|
||||
<div class="v-page classify_page">
|
||||
<div class="v-ctx" v-loading="paper_list_loading">
|
||||
<FormLayout ref="formLayoutRef" :items="formItems" :model="form" :rules="rules" label-width="70px"
|
||||
label-position="right" :inline="true">
|
||||
<template v-slot:title>
|
||||
<QueryInput v-model="form.title" @query="pagingFindList"></QueryInput>
|
||||
</template>
|
||||
<template v-slot:btns>
|
||||
</template>
|
||||
</FormLayout>
|
||||
<TableLayout :column="column" :data="table_data" :pageInfo="page_info"
|
||||
@current-change="(e) => pagingChange({ currentPage: e })" @size-change="(e) => pagingChange({ pageSize: e })">
|
||||
<template v-slot:action="props">
|
||||
<!-- <el-button type="text" icon="i-j-jxzb-datika" @click="getLastExamHistory(props.row.classifyId)">查看答卷
|
||||
</el-button> -->
|
||||
<el-button type="text" icon="i-j-jxzb-datika" @click="toMistakeModeLinkAndRow(3,props.row)">查看答卷
|
||||
</el-button>
|
||||
<el-button type="text" icon="i-j-ksap-bianji2" @click="toMistakeAgain(props.row)">巩固练习
|
||||
</el-button>
|
||||
</template>
|
||||
</TableLayout>
|
||||
</div>
|
||||
<DialogLayout :shadowBar="false" :visible="mode_selector_dialog_show" title="练习方式" :actionBarOption="{noCencel:true,noConfirm:true}" @onCancel="mode_selector_dialog_show=false;current_classify=null">
|
||||
<div class="links">
|
||||
<div class="link-item" @click="toMistakeModeLink(2)">
|
||||
<i class="el-icon-edit"></i>
|
||||
答题模式
|
||||
</div>
|
||||
<div class="link-item" @click="toMistakeModeLink(1)">
|
||||
<i class="el-icon-document"></i>
|
||||
背题模式
|
||||
</div>
|
||||
</div>
|
||||
</DialogLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TableLayout, FormLayout, DialogLayout } from '@/components/layout'
|
||||
import '@/views/assessment-evaluation/utils/gyStyle.scss'
|
||||
import { pagingMistakeListApi, getLastExamHistoryId } from '@/api/assessment-evaluation/mistake'
|
||||
import QueryInput from '@/components/widget/QueryInput.vue'
|
||||
import { dataReportMixin } from '@/utils/data-report'
|
||||
export default {
|
||||
components: {
|
||||
TableLayout, FormLayout, QueryInput, DialogLayout
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
table_data: [],
|
||||
page_info: { currentPage: 1, pageSize: 10, total: 0 },
|
||||
// 试题分类树是否加载中
|
||||
paper_list_loading: false,
|
||||
form: { title: '', isPractive: 1 },
|
||||
mode_selector_dialog_show: false,
|
||||
current_classify: null
|
||||
}),
|
||||
props: {
|
||||
isMountedLoad: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.formItems = [
|
||||
{ prop: 'title' },
|
||||
{ prop: 'btns', model: { class: 'gy-btns' } }
|
||||
]
|
||||
this.column = [
|
||||
{ prop: 'classify', label: '试题分类', 'min-width': 150, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: '单选题', label: '单选题', 'min-width': 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: '多选题', label: '多选题', 'min-width': 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: '判断题', label: '判断题', 'min-width': 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: '填空题', label: '填空题', 'min-width': 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
// { prop: 'shouldCount', label: '应交试卷', 'min-width': 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{ prop: '简答题', label: '简答题', 'min-width': 80, align: 'center', 'show-overflow-tooltip': true },
|
||||
{
|
||||
prop: 'action',
|
||||
label: '操作',
|
||||
// width: 350,
|
||||
width: 200,
|
||||
align: 'center',
|
||||
'show-overflow-tooltip': true
|
||||
}
|
||||
]
|
||||
},
|
||||
mounted () {
|
||||
if (this.isMountedLoad) {
|
||||
this.pagingFindList()
|
||||
}
|
||||
},
|
||||
activated () {
|
||||
this.pagingFindList()
|
||||
},
|
||||
methods: {
|
||||
// 当用户单击“查看答案卷”按钮时调用的方法。
|
||||
getLastExamHistory (classifyId) {
|
||||
getLastExamHistoryId(classifyId).then(res => {
|
||||
if (res.data.length > 0) {
|
||||
this.$router.push({ path: '/assessment-evaluation/wrong-topic-consolidate/history-exam-priview/' + res.data[0].id, query: { preview: 1 } })
|
||||
} else {
|
||||
this.$message.error('未找到考试记录')
|
||||
}
|
||||
})
|
||||
},
|
||||
// 按钮调用的方法。
|
||||
toMistakeModeLinkAndRow (mode, row) {
|
||||
this.current_classify = row
|
||||
this.$router.push({ path: '/assessment-evaluation/wrong-topic-consolidate/mistake-again/' + this.current_classify.classifyId + '/' + mode })
|
||||
},
|
||||
// 当用户单击“巩固练习”按钮时调用的函数。
|
||||
toMistakeModeLink (mode) {
|
||||
this.$router.push({ path: '/assessment-evaluation/wrong-topic-consolidate/mistake-again/' + this.current_classify.classifyId + '/' + mode })
|
||||
this.mode_selector_dialog_show = false
|
||||
this.current_classify = null
|
||||
},
|
||||
// 当用户单击“巩固练习”按钮时调用的函数。
|
||||
toMistakeAgain (row) {
|
||||
this.current_classify = row
|
||||
this.mode_selector_dialog_show = true
|
||||
// this.$router.push({ path: '/assessment-evaluation/wrong-topic-consolidate/mistake-again/' + row.classifyId + '/' + 1 })
|
||||
},
|
||||
// 修改是否为匿名评卷
|
||||
formatTime (msTime) {
|
||||
const time = msTime / 1000
|
||||
let hour = Math.floor(time / 60 / 60)
|
||||
hour = hour.toString().padStart(2, '0')
|
||||
let minute = Math.floor(time / 60) % 60
|
||||
minute = minute.toString().padStart(2, '0')
|
||||
let second = Math.floor(time) % 60
|
||||
second = second.toString().padStart(2, '0')
|
||||
return `${hour}:${minute}:${second}`
|
||||
},
|
||||
// 当用户单击页码时调用的函数。
|
||||
pagingChange (event) {
|
||||
if ((typeof event.currentPage !== 'number') && (typeof event.pageSize !== 'number')) return
|
||||
|
||||
this.page_info = { ...this.page_info, ...event }
|
||||
this.pagingFindList()
|
||||
},
|
||||
// 将 page_info 设置为默认值。
|
||||
initPageInfo () {
|
||||
this.page_info = { currentPage: 1, pageSize: this.page_info.pageSize, total: 0 }
|
||||
},
|
||||
// 当用户单击“查看答题纸”按钮时调用的函数。
|
||||
pagingFindList (e) {
|
||||
if (e instanceof PointerEvent) {
|
||||
this.initPageInfo()
|
||||
}
|
||||
this.paper_list_loading = true
|
||||
pagingMistakeListApi({ ...this.page_info, ...this.form }).then(res => {
|
||||
const { currentPage, pageSize, total, types } = res.data
|
||||
const tableData = res.data.data
|
||||
tableData.forEach((item) => {
|
||||
const itemTimes = item.times.split(',')
|
||||
const itemTimeArr = itemTimes.map(typeTimes => {
|
||||
return typeTimes.split(':')
|
||||
})
|
||||
|
||||
types.forEach((type) => {
|
||||
const itemCount = itemTimeArr.find(arrItem => +arrItem[0] === type.id)
|
||||
if (itemCount) {
|
||||
item[type.name] = itemCount[1]
|
||||
} else {
|
||||
item[type.name] = 0
|
||||
}
|
||||
})
|
||||
})
|
||||
this.table_data = res.data.data
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (this.table_data.length === 0 && this.page_info.currentPage != 1) {
|
||||
this.initPageInfo()
|
||||
this.pagingFindList()
|
||||
}
|
||||
|
||||
this.page_info = { currentPage, pageSize, total }
|
||||
}).finally(_ => {
|
||||
this.paper_list_loading = false
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
mixins: [
|
||||
/**
|
||||
* 数据上报
|
||||
*/
|
||||
dataReportMixin('ERROR_CONSOLIDATION')
|
||||
]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.links{
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-around;
|
||||
.link-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
[class^="el-icon"]{
|
||||
font-size: 35px;
|
||||
color:$--color-primary;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
color:#666;
|
||||
}
|
||||
}
|
||||
::v-deep .form_layout {
|
||||
margin-top: 16px;
|
||||
.suf {
|
||||
line-height: 1 !important;
|
||||
}
|
||||
.el-form-item:last-child {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.exam-status {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: #ccc;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="exam-paper">
|
||||
<div class="paper-main" v-loading="page_is_loading">
|
||||
<div class="aq-card" v-if="mode === 5">
|
||||
<AQCard :questions="questions" :answers="answers" @smooth="aqItemOnClick" :mode="mode" @submit="submitMistakes"
|
||||
@cancel="cancelMistakeExam"></AQCard>
|
||||
</div>
|
||||
<div class="questions-area">
|
||||
<QuestionsList :questions="questions" @answers="userAnswerChanged" :mode="mode" :paperStyle="0"
|
||||
ref="questionList">
|
||||
</QuestionsList>
|
||||
</div>
|
||||
</div>
|
||||
<ActionBar v-if="[4,1].includes(mode)" noCencel center confirmTxt="退出" style="margin-bottom:10px;" @onConfirm="$router.go(-1)"></ActionBar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QuestionsList from '../../online-test/begin-online-exam/components/QuestionsList.vue'
|
||||
import { getMistakesByclassifyId, patchMistakes } from '@/api/assessment-evaluation/mistake'
|
||||
import AQCard from '../../online-test/begin-online-exam/components/AQCard.vue'
|
||||
import ActionBar from '@/components/layout/ActionBar.vue'
|
||||
export default {
|
||||
components: {
|
||||
QuestionsList,
|
||||
AQCard,
|
||||
ActionBar
|
||||
},
|
||||
data: () => ({
|
||||
questions: [],
|
||||
answers: [],
|
||||
mode: 1,
|
||||
page_is_loading: false
|
||||
}),
|
||||
|
||||
activated () {
|
||||
this.questions = []
|
||||
this.answers = []
|
||||
if (this.$route.params.mode === '1') {
|
||||
this.getMistakesList(1)
|
||||
this.mode = 4
|
||||
} else if (this.$route.params.mode === '2') {
|
||||
this.getMistakesList(0)
|
||||
this.mode = 5
|
||||
} else if (this.$route.params.mode === '3') {
|
||||
this.getMistakesList(1)
|
||||
this.mode = 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 当用户单击提交按钮时调用的方法。
|
||||
submitMistakes () {
|
||||
this.page_is_loading = true
|
||||
patchMistakes(this.$route.params.classifyId, this.answers).then(res => {
|
||||
const { data } = res
|
||||
const { pass, faild } = data
|
||||
this.$alert(`本次答题正确${pass}道,错误${faild}道。`, '提示', {
|
||||
callback: () => {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
})
|
||||
}).finally(_ => {
|
||||
this.page_is_loading = false
|
||||
})
|
||||
},
|
||||
// 当用户单击取消按钮时调用的方法。
|
||||
cancelMistakeExam () {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
// 当用户单击 AQCard 组件中的问题时调用的方法。
|
||||
aqItemOnClick (question) {
|
||||
const item = document.querySelector(`[tag="${question.id}"]`)
|
||||
item.scrollIntoView({
|
||||
block: 'start',
|
||||
behavior: 'smooth'
|
||||
})
|
||||
item.classList.add('shine')
|
||||
setTimeout(() => {
|
||||
item.classList.remove('shine')
|
||||
}, 2000)
|
||||
},
|
||||
// 当用户更改答案时调用的方法。
|
||||
userAnswerChanged (nv) {
|
||||
this.answers = nv
|
||||
},
|
||||
// 激活组件时调用的方法。它用于从服务器获取问题。
|
||||
getMistakesList (showUserAnswer = 1) {
|
||||
this.page_is_loading = true
|
||||
getMistakesByclassifyId(this.$route.params.classifyId, showUserAnswer).then(res => {
|
||||
this.questions = res.data
|
||||
}).finally(_ => { this.page_is_loading = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.exam-paper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
.paper-main{
|
||||
flex-grow:1;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.aq-card {
|
||||
flex-basis: 240px;
|
||||
flex-shrink: 0;
|
||||
width: 240px;
|
||||
padding-right: 15px;
|
||||
border-right: 1px solid #efefef;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.tips-card {
|
||||
flex-basis: 200px;
|
||||
width: 200px;
|
||||
padding-left: 15px;
|
||||
border-left: 1px solid #efefef;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.questions-area {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
overflow-y: scroll;
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
front/src/views/electronic-textbook/index.vue
Normal file
36
front/src/views/electronic-textbook/index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div id="editor-box" style="width: 100%; height: 100%">
|
||||
<iframe
|
||||
class="editor-iframe"
|
||||
:src="`${url}/#/manualsPerview?token=${token}&baseRole=${$store.user.baseRole}`"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
token: JSON.parse(localStorage.getItem('token')),
|
||||
url: import.meta.env.VITE_APP_IFRAME_URL
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
console.log(this.$store.user, '111')
|
||||
// 处理当前iframe的父级为0
|
||||
// const editorBox = document.getElementById('editor-box')
|
||||
// editorBox.parentElement.style.padding = 0
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.editor-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
::v-deep .v-card {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
78
front/src/views/entry/components/Vcard.vue
Normal file
78
front/src/views/entry/components/Vcard.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div
|
||||
@click="activeCard"
|
||||
@mouseenter="changStatus(true)"
|
||||
@mouseleave="changStatus(false)"
|
||||
class="entry-card"
|
||||
:style="{
|
||||
'--icon': options.icon,
|
||||
'--img': `url(/imgs/entry/${index + 1}.png)`,
|
||||
}"
|
||||
v-show="!options.hide"
|
||||
>
|
||||
<img class="icon" :src="`/imgs/entry/${options.icon}.png`" alt="" />
|
||||
<div class="title">{{ options.title }}</div>
|
||||
<div class="i-dao-hang" :style="{ opacity: mousemove ? 1 : 0.5 }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: { options: { type: Object }, index: { type: Number } },
|
||||
data () {
|
||||
return {
|
||||
mousemove: false
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.options.actived) this.activeCard()
|
||||
},
|
||||
|
||||
methods: {
|
||||
activeCard () {
|
||||
this.$router.push(this.options.link)
|
||||
},
|
||||
changStatus (status) {
|
||||
this.mousemove = status
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.entry-card {
|
||||
cursor: pointer;
|
||||
-webkit-user-drag: none;
|
||||
min-width: 250px;
|
||||
min-height: 300px;
|
||||
background: var(--img) no-repeat;
|
||||
background-size: 100% 100%;
|
||||
height: 60%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
padding-top: 100px;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.icon {
|
||||
width: 35%;
|
||||
height: 20%;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
.title {
|
||||
font-size: 25px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
.i-dao-hang {
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
position: absolute;
|
||||
bottom: 30%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user