feat:初始化 -融骅

This commit is contained in:
2023-10-17 09:15:30 +08:00
parent c9ff84e6a2
commit 405e152b38
1190 changed files with 138344 additions and 455 deletions

View 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' })

View 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 })

View 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' })

View 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' })

View 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' })

View 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' })

View 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 })

View 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' })

View 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 })
}

View 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 })
}

View 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' })
}

View 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 })
}

View 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 })
}

View 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 })
}

View 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] } })

View 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' })

View 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 } })

View 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' })

View 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' })

View 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 })

View 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 })

View 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'
}
})
}

View 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 })

View 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 })

View 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 })

View 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' })

View 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 })

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
}

View 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>

View 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>

View 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>

View 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
View 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
View 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
View 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

View 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
View 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__
}
}
})

View 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
View 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;
}

View 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";

View 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)

View 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
View 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)
}
}

View 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
View 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
View 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)
}

View 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。
})
}

File diff suppressed because one or more lines are too long

View 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()
}
}
}

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
&nbsp;/ {{ 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,12 @@
<template>
<div class="SelectExamPaper">
</div>
</template>
<script>
export default {}
</script>
<style>
</style>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View 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
}

View 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;
}
}
}
}
}

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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>

View 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>

View 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