fix conflict
This commit is contained in:
commit
7ab775d1cb
|
|
@ -3,6 +3,7 @@
|
||||||
.vs/
|
.vs/
|
||||||
venv/
|
venv/
|
||||||
media/
|
media/
|
||||||
|
temp/
|
||||||
*.log
|
*.log
|
||||||
static/
|
static/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ ENV = 'development'
|
||||||
|
|
||||||
# base api
|
# base api
|
||||||
#VUE_APP_BASE_API = 'http://10.0.11.127:8000/api'
|
#VUE_APP_BASE_API = 'http://10.0.11.127:8000/api'
|
||||||
#VUE_APP_BASE_API = 'http://127.0.0.1:8000/api'
|
#VUE_APP_BASE_API = 'http://127.0.0.1:2222/api'
|
||||||
VUE_APP_BASE_API = 'https://testsearch.ctc.ac.cn/api'
|
VUE_APP_BASE_API = 'https://testsearch.ctc.ac.cn/api'
|
||||||
|
|
||||||
#VUE_APP_BASE_API = 'http://47.95.0.242:9101/api'
|
#VUE_APP_BASE_API = 'http://47.95.0.242:9101/api'
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@riophae/vue-treeselect": "^0.4.0",
|
"@riophae/vue-treeselect": "^0.4.0",
|
||||||
"axios": "0.18.1",
|
"axios": "0.18.1",
|
||||||
|
"echarts": "^5.4.0",
|
||||||
"element-china-area-data": "^5.0.2",
|
"element-china-area-data": "^5.0.2",
|
||||||
"element-ui": "2.13.0",
|
"element-ui": "2.13.0",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
|
|
@ -48,9 +49,9 @@
|
||||||
"eslint-plugin-vue": "5.2.2",
|
"eslint-plugin-vue": "5.2.2",
|
||||||
"html-webpack-plugin": "3.2.0",
|
"html-webpack-plugin": "3.2.0",
|
||||||
"mockjs": "1.0.1-beta3",
|
"mockjs": "1.0.1-beta3",
|
||||||
"node-sass": "^4.13.1",
|
"node-sass": "^6.0.1",
|
||||||
"runjs": "^4.3.2",
|
"runjs": "^4.3.2",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^10.0.1",
|
||||||
"script-ext-html-webpack-plugin": "2.1.3",
|
"script-ext-html-webpack-plugin": "2.1.3",
|
||||||
"script-loader": "0.7.2",
|
"script-loader": "0.7.2",
|
||||||
"serve-static": "^1.13.2",
|
"serve-static": "^1.13.2",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
//获取题目类型
|
||||||
|
export function getQuestioncatList(query) {
|
||||||
|
return request({
|
||||||
|
url: '/exam/questioncat/',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//新增题目类型
|
||||||
|
export function createQuestioncat(data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/questioncat/`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//编辑题目类型
|
||||||
|
export function updateQuestioncat(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/questioncat/${id}/`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//删除题目类型
|
||||||
|
export function deleteQuestioncat(id) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/questioncat/${id}/`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//题目列表
|
||||||
|
export function getQuestionList(query) {
|
||||||
|
return request({
|
||||||
|
url: '/exam/question/',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//题目详情
|
||||||
|
export function getQuestionDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/question/${id}/`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//新增题目
|
||||||
|
export function createQuestion(data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/question/`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//编辑题目
|
||||||
|
export function updateQuestion(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/question/${id}/`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//删除题目
|
||||||
|
export function deleteQuestion(id) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/question/${id}/`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//导入题目
|
||||||
|
export function importQuestion(data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/question/import/`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//导出题目
|
||||||
|
export function exportQuestion(data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/question/export/`,
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//启用题目
|
||||||
|
export function enableQuestions(data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/question/enable/`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//试卷增删改查
|
||||||
|
//获取试卷
|
||||||
|
export function getPaperList(query) {
|
||||||
|
return request({
|
||||||
|
url: '/exam/paper/',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//试卷详情
|
||||||
|
export function getPaperDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/paper/${id}/`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//新增试卷
|
||||||
|
export function createPaper(data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/paper/`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//编辑试卷
|
||||||
|
export function updatePaper(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/paper/${id}/`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//删除试卷
|
||||||
|
export function deletePaper(id) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/paper/${id}/`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//考试增删改查
|
||||||
|
//获取考试列表
|
||||||
|
export function getExamList(query) {
|
||||||
|
return request({
|
||||||
|
url: '/exam/exam/',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//考试详情
|
||||||
|
export function getExamDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/exam/${id}/`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//新增考试
|
||||||
|
export function createExam(data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/exam/`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//编辑考试
|
||||||
|
export function updateExam(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/exam/${id}/`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//删除考试
|
||||||
|
export function deleteExam(id) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/exam/${id}/`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//考试记录增删改查
|
||||||
|
//考试记录列表和详情
|
||||||
|
export function getExamRecordList(query) {
|
||||||
|
return request({
|
||||||
|
url: '/exam/examrecord/',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//考试记录
|
||||||
|
export function getExamRecordDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/examrecord/${id}/`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//新增考试记录
|
||||||
|
export function createExamRecord(data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/examrecord/`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//编辑考试记录
|
||||||
|
export function updateExamRecord(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/examrecord/${id}/`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//删除考试记录
|
||||||
|
export function deleteExamRecord(id) {
|
||||||
|
return request({
|
||||||
|
url: `/exam/examrecord/${id}/`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -49,11 +49,108 @@ export function getVideoPlayCode(id) {
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
//已弃用
|
||||||
export function getMyView(id, data) {
|
export function getMyView(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/vod/video/${id}/myview/`,
|
||||||
|
method: 'get',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//已弃用
|
||||||
|
export function refreshMyView(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/vod/video/${id}/myview/`,
|
url: `/vod/video/${id}/myview/`,
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//开始播放
|
||||||
|
export function videoStart(id) {
|
||||||
|
return request({
|
||||||
|
url: `/vod/video/${id}/start/`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//观看统计
|
||||||
|
export function videoView2(data) {
|
||||||
|
return request({
|
||||||
|
url: `/vod/view2/`,
|
||||||
|
method: 'get',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//我的观看统计
|
||||||
|
export function myVideoView2(data) {
|
||||||
|
return request({
|
||||||
|
url: `/vod/view2/my/`,
|
||||||
|
method: 'get',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//观看记录
|
||||||
|
export function viewItem(data) {
|
||||||
|
return request({
|
||||||
|
url: `/vod/viewitem/`,
|
||||||
|
method: 'get',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//我的观看记录
|
||||||
|
export function myViewItem(data) {
|
||||||
|
return request({
|
||||||
|
url: `/vod/viewitem/my/`,
|
||||||
|
method: 'get',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//更新观看记录
|
||||||
|
export function refreshViewItem(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/vod/viewitem/${id}/`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//本视频的我的观看统计
|
||||||
|
export function myView(id) {
|
||||||
|
return request({
|
||||||
|
url: `/vod/video/${id}/my/`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//播放完成
|
||||||
|
export function viewItemComplete(id) {
|
||||||
|
return request({
|
||||||
|
url: `/vod/viewitem/${id}/complete/`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//单位观看量统计
|
||||||
|
export function groupByOrgView(data) {
|
||||||
|
return request({
|
||||||
|
url: '/vod/analyse/group_by_org_view/',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//个人观看量统计
|
||||||
|
export function groupByUserView(data) {
|
||||||
|
return request({
|
||||||
|
url: '/vod/analyse/group_by_user_view/',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//视频大类播放量统计
|
||||||
|
export function groupByCategoryView(data) {
|
||||||
|
return request({
|
||||||
|
url: '/vod/analyse/group_by_video_category_big/',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -300,12 +300,19 @@ export const asyncRoutes = [
|
||||||
meta: { title: '上传视频', perms: ['video_create'] }
|
meta: { title: '上传视频', perms: ['video_create'] }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'index/:id',
|
path: 'index',
|
||||||
name: 'Index',
|
name: 'index',
|
||||||
component: () => import('@/views/testvideo/index.vue'),
|
component: () => import('@/views/testvideo/index.vue'),
|
||||||
meta: { title: '视频播放', perms: ['video_view'] },
|
meta: { title: '视频播放', perms: ['video_view'] },
|
||||||
hidden: true
|
hidden: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'videoStatistics',
|
||||||
|
name: 'videoStatistics',
|
||||||
|
component: () => import('@/views/testvideo/videoStatistics.vue'),
|
||||||
|
meta: { title: '视频播放统计', perms: ['video_view'] },
|
||||||
|
// hidden: true
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -330,6 +337,74 @@ export const asyncRoutes = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/exam',
|
||||||
|
component: Layout,
|
||||||
|
redirect: '/exam/questions',
|
||||||
|
name: 'exam',
|
||||||
|
meta: { title: '考试', icon: 'PT', perms: ['pt_view'] },
|
||||||
|
alwaysShow: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'classify',
|
||||||
|
name: '题目分类',
|
||||||
|
component: () => import('@/views/exam/classify.vue'),
|
||||||
|
meta: { title: '题目分类', perms: ['pt_view'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'questions',
|
||||||
|
name: '题目列表',
|
||||||
|
component: () => import('@/views/exam/questions.vue'),
|
||||||
|
meta: { title: '题目列表', perms: ['pt_view'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'questionCreate',
|
||||||
|
name: '新增题目',
|
||||||
|
component: () => import('@/views/exam/questioncreate.vue'),
|
||||||
|
meta: { title: '新增题目'},
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'questionUpdate',
|
||||||
|
name: '编辑题目',
|
||||||
|
component: () => import('@/views/exam/questionupdate.vue'),
|
||||||
|
meta: { title: '编辑题目'},
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'testPaper',
|
||||||
|
name: '考试试卷',
|
||||||
|
component: () => import('@/views/exam/testPaper.vue'),
|
||||||
|
meta: { title: '考试试卷', perms: ['pt_view'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'paperCreate',
|
||||||
|
name: '新建试卷',
|
||||||
|
component: () => import('@/views/exam/testPaperCreate.vue'),
|
||||||
|
meta: { title: '新建试卷'},
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'paperUpdate',
|
||||||
|
name: '编辑试卷',
|
||||||
|
component: () => import('@/views/exam/testPaperUpdate.vue'),
|
||||||
|
meta: { title: '编辑试卷'},
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
name: '考试',
|
||||||
|
component: () => import('@/views/exam/index.vue'),
|
||||||
|
meta: { title: '考试', perms: ['pt_view'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'record',
|
||||||
|
name: '考试记录',
|
||||||
|
component: () => import('@/views/exam/examRecord.vue'),
|
||||||
|
meta: { title: '考试记录', perms: ['pt_view'] }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/system',
|
path: '/system',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ const getDefaultState = () => {
|
||||||
token: getToken(),
|
token: getToken(),
|
||||||
name: '',
|
name: '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
perms: []
|
perms: [],
|
||||||
|
dept:''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -249,24 +249,24 @@
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="对象数量">
|
<el-form-item label="对象数量">
|
||||||
<el-input-number v-model="abilityForm.num" :min="1" label="新增对象数量"></el-input-number>
|
<el-input-number v-model="abilityForm.num" :min="0" label="新增对象数量"></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="方法标准数量">
|
<el-form-item label="方法标准数量">
|
||||||
<el-input-number v-model="abilityForm.num3" :min="1" label="新增方法标准数量"></el-input-number>
|
<el-input-number v-model="abilityForm.num3" :min="0" label="新增方法标准数量"></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="参数数量">
|
<el-form-item label="参数数量">
|
||||||
<el-input-number v-model="abilityForm.num2" :min="1" label="新增参数数量"></el-input-number>
|
<el-input-number v-model="abilityForm.num2" :min="0" label="新增参数数量"></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="产品标准数量">
|
<el-form-item label="产品标准数量">
|
||||||
<el-input-number v-model="abilityForm.num4" :min="1" label="新增产品标准数量"></el-input-number>
|
<el-input-number v-model="abilityForm.num4" :min="0" label="新增产品标准数量"></el-input-number>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
@ -682,8 +682,13 @@
|
||||||
style="padding: 10px 20px;position: relative"
|
style="padding: 10px 20px;position: relative"
|
||||||
>
|
>
|
||||||
<p style="font-size: 20px;font-weight: bold;">新增能力</p>
|
<p style="font-size: 20px;font-weight: bold;">新增能力</p>
|
||||||
<p><span style="display: inline-block;width: 100px;font-weight: bold;">能力类型 :</span>{{showData.data.atype_name}}</p>
|
<p><span class="drawerInfoTitle">能力类型 :</span>{{showData.data.atype_name}}</p>
|
||||||
<p><span style="display: inline-block;width: 100px;font-weight: bold;">能力领域:</span>{{showData.data.afield_name}}</p>
|
<p><span class="drawerInfoTitle">能力领域:</span>{{showData.data.afield_name}}</p>
|
||||||
|
|
||||||
|
<p><span class="drawerInfoTitle">对象数量:</span>{{showData.data.num}}</p>
|
||||||
|
<p><span class="drawerInfoTitle">参数数量:</span>{{showData.data.num2}}</p>
|
||||||
|
<p><span class="drawerInfoTitle">方法标准数量:</span>{{showData.data.num3}}</p>
|
||||||
|
<p><span class="drawerInfoTitle">产品标准数量:</span>{{showData.data.num4}}</p>
|
||||||
<p v-if="showData.data.file_"><span style="display: inline-block;width: 100px;font-weight: bold;">上传文件:</span>{{showData.data.file_.name}}</p>
|
<p v-if="showData.data.file_"><span style="display: inline-block;width: 100px;font-weight: bold;">上传文件:</span>{{showData.data.file_.name}}</p>
|
||||||
<el-link v-if="showData.data.file_" :href="showData.data.file_.file" target="_blank" type="primary">{{showData.data.file_.name}}</el-link>
|
<el-link v-if="showData.data.file_" :href="showData.data.file_.file" target="_blank" type="primary">{{showData.data.file_.name}}</el-link>
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
|
|
@ -716,8 +721,9 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
getQtaskMy,
|
getQtaskMy,
|
||||||
|
qtaskDetail,
|
||||||
getQtask,
|
getQtask,
|
||||||
qactionMy,
|
qactionList,
|
||||||
getQualityMy,
|
getQualityMy,
|
||||||
createQuali,
|
createQuali,
|
||||||
qactionDelete,
|
qactionDelete,
|
||||||
|
|
@ -732,7 +738,7 @@
|
||||||
} from "@/api/ability";
|
} from "@/api/ability";
|
||||||
import {getDictList} from "@/api/dict";
|
import {getDictList} from "@/api/dict";
|
||||||
import {genTree} from "@/utils";
|
import {genTree} from "@/utils";
|
||||||
import {getOrgList} from "@/api/org";
|
import {getOrgList, getSubOrgList} from "@/api/org";
|
||||||
import {getCMAGroup} from "@/api/cma";
|
import {getCMAGroup} from "@/api/cma";
|
||||||
import checkPermission from "@/utils/permission";
|
import checkPermission from "@/utils/permission";
|
||||||
import Pagination from "@/components/Pagination"; // secondary package based on el-pagination
|
import Pagination from "@/components/Pagination"; // secondary package based on el-pagination
|
||||||
|
|
@ -854,6 +860,9 @@
|
||||||
this.afieldOptions = [];
|
this.afieldOptions = [];
|
||||||
this.getOptions();
|
this.getOptions();
|
||||||
this.getProvince();
|
this.getProvince();
|
||||||
|
// debugger;
|
||||||
|
console.log(this.$store.state.user.dept);
|
||||||
|
this.pageForm.org = this.$store.state.user.dept;
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -943,8 +952,9 @@
|
||||||
},
|
},
|
||||||
checkPermission,
|
checkPermission,
|
||||||
getTableList() {
|
getTableList() {
|
||||||
this.listLoading = true;
|
this.listLoading = true;getQtaskMy
|
||||||
getQtaskMy(this.pageForm).then((response) => {
|
getQtaskMy(this.pageForm).then((response) => {
|
||||||
|
// qtaskDetail(this.pageForm).then((response) => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
debugger;
|
debugger;
|
||||||
this.taskList = response.data;
|
this.taskList = response.data;
|
||||||
|
|
@ -956,9 +966,15 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getGroup() {
|
getGroup() {
|
||||||
getOrgList({can_supervision: true}).then((res) => {
|
if (this.checkPermission(["record_confirm"])) {
|
||||||
|
getOrgList({ can_supervision: true }).then((res) => {
|
||||||
this.orgData = res.data;
|
this.orgData = res.data;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
getSubOrgList().then((res) => {
|
||||||
|
this.orgData = res.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getrecordlist() {
|
getrecordlist() {
|
||||||
this.getTableList();
|
this.getTableList();
|
||||||
|
|
@ -970,16 +986,18 @@
|
||||||
this.checkedItem = obj;
|
this.checkedItem = obj;
|
||||||
this.qtask = obj.qtask;
|
this.qtask = obj.qtask;
|
||||||
this.qtaskName = obj.qtask_.name;
|
this.qtaskName = obj.qtask_.name;
|
||||||
this.buttonsShow = true;
|
if(obj.org== this.$store.state.user.dept){
|
||||||
|
this.buttonsShow = true;
|
||||||
|
}else{
|
||||||
|
this.buttonsShow = false;
|
||||||
|
}
|
||||||
this.listQuery.qtask = obj.qtask;
|
this.listQuery.qtask = obj.qtask;
|
||||||
qactionMy(this.listQuery).then((res) => {
|
qactionList(this.listQuery).then((res) => {
|
||||||
this.recordList = res.data;
|
this.recordList = res.data;
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
///////
|
|
||||||
getList() {
|
getList() {
|
||||||
qactionMy(this.listQuery).then((res) => {
|
qactionList(this.listQuery).then((res) => {
|
||||||
this.recordList = res.data;
|
this.recordList = res.data;
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
@ -1134,6 +1152,9 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleDetail(type,item){
|
handleDetail(type,item){
|
||||||
|
debugger;
|
||||||
|
console.log(type)
|
||||||
|
console.log(item)
|
||||||
let that = this;
|
let that = this;
|
||||||
that.drawer = true;
|
that.drawer = true;
|
||||||
this.showData.id = item.id;
|
this.showData.id = item.id;
|
||||||
|
|
@ -1148,7 +1169,9 @@
|
||||||
if (res.code>=200) {
|
if (res.code>=200) {
|
||||||
let updateDetail = res.data.update_detail;//更改字段
|
let updateDetail = res.data.update_detail;//更改字段
|
||||||
updateDetail.forEach(item=>{
|
updateDetail.forEach(item=>{
|
||||||
|
// debugger;
|
||||||
this.fieldList2.push(item.field);//所有字段
|
this.fieldList2.push(item.field);//所有字段
|
||||||
|
debugger;
|
||||||
});
|
});
|
||||||
this.drawer = true;
|
this.drawer = true;
|
||||||
}
|
}
|
||||||
|
|
@ -1177,5 +1200,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.drawerInfoTitle{
|
||||||
|
display: inline-block;
|
||||||
|
width: 120px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -278,12 +278,15 @@
|
||||||
<p style="font-size: 20px;font-weight: bold;">新增能力</p>
|
<p style="font-size: 20px;font-weight: bold;">新增能力</p>
|
||||||
<p><span style="display: inline-block;width: 100px;font-weight: bold;">能力类型 :</span>{{atype_name}}</p>
|
<p><span style="display: inline-block;width: 100px;font-weight: bold;">能力类型 :</span>{{atype_name}}</p>
|
||||||
<p><span style="display: inline-block;width: 100px;font-weight: bold;">能力领域:</span>{{afield_name}}</p>
|
<p><span style="display: inline-block;width: 100px;font-weight: bold;">能力领域:</span>{{afield_name}}</p>
|
||||||
<p><span style="display: inline-block;width: 160px;font-weight: bold;">新增对象数量:</span>{{item.num}}</p>
|
<p><span class="drawerInfoTitle">对象数量:</span>{{showData.data.num}}</p>
|
||||||
<p><span style="display: inline-block;width: 160px;font-weight: bold;">新增参数数量:</span>{{item.num2}}</p>
|
<p><span class="drawerInfoTitle">参数数量:</span>{{showData.data.num2}}</p>
|
||||||
<p><span style="display: inline-block;width: 160px;font-weight: bold;">新增方法标准数量:</span>{{item.num3}}</p>
|
<p><span class="drawerInfoTitle">方法标准数量:</span>{{showData.data.num3}}</p>
|
||||||
<p><span style="display: inline-block;width: 160px;font-weight: bold;">新增产品标准数量:</span>{{item.num4}}</p>
|
<p><span class="drawerInfoTitle">产品标准数量:</span>{{showData.data.num4}}</p>
|
||||||
<p><span style="display: inline-block;width: 100px;font-weight: bold;">上传文件:</span>{{showData.data.file_.name}}</p>
|
<p><span style="display: inline-block;width: 100px;font-weight: bold;">上传文件:</span>
|
||||||
<el-link :href="showData.data.file_.file" target="_blank" type="primary">{{showData.data.file_.name}}</el-link>
|
<text v-if="showData.data.file_">{{showData.data.file_.name}}</text>
|
||||||
|
<text v-else>无上传文件</text>
|
||||||
|
</p>
|
||||||
|
<el-link v-if="showData.data.file_" :href="showData.data.file_.file" target="_blank" type="primary">{{showData.data.file_.name}}</el-link>
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="actionType==='confirm'"
|
v-if="actionType==='confirm'"
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,6 @@
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
max-height="600"
|
max-height="600"
|
||||||
@sort-change="changeTableSort"
|
@sort-change="changeTableSort"
|
||||||
:span-method="objectSpanMethod"
|
|
||||||
>
|
>
|
||||||
<el-table-column type="index" width="50" />
|
<el-table-column type="index" width="50" />
|
||||||
|
|
||||||
|
|
@ -116,7 +115,7 @@
|
||||||
<template slot-scope="scope" v-if="scope.row.files">
|
<template slot-scope="scope" v-if="scope.row.files">
|
||||||
<el-link
|
<el-link
|
||||||
v-if="scope.row.files.length > 1"
|
v-if="scope.row.files.length > 1"
|
||||||
@click="handleRecord({ action: 'view', record: scope.row })"
|
@click="handleRecord(scope.row)"
|
||||||
>有
|
>有
|
||||||
<span style="color: red">{{ scope.row.files.length }}</span>
|
<span style="color: red">{{ scope.row.files.length }}</span>
|
||||||
个文件</el-link
|
个文件</el-link
|
||||||
|
|
@ -138,33 +137,56 @@
|
||||||
@pagination="getList"
|
@pagination="getList"
|
||||||
/>
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
<el-drawer
|
||||||
|
:visible.sync="drawerLiminted"
|
||||||
|
:with-header="false"
|
||||||
|
size="40%">
|
||||||
|
<div class="drawerTitle">资质能力报送记录</div>
|
||||||
|
<el-form class="drawerBody" v-if="record.content_">
|
||||||
|
<el-form-item label="材料名称">
|
||||||
|
{{ record.content_.name }}
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="材料详情" v-if="record.content_">
|
||||||
|
{{ record.content_.desc }}
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报送状态">
|
||||||
|
{{ record.state }}
|
||||||
|
<el-tag v-if="record.is_self" style="margin-left: 2px" effect="plain">主动报送</el-tag>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属任务" v-if="record.task_">
|
||||||
|
{{ record.task_.name }}/{{ record.task_.end_date }}
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="执行组织" v-if="record.belong_dept_">
|
||||||
|
{{ record.belong_dept_.name }}
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="报送人" v-if="record.up_user_">
|
||||||
|
{{ record.up_user_.name }}/{{ record.up_date }}
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="文件列表">
|
||||||
|
</el-form-item>
|
||||||
|
<div
|
||||||
|
class="recordfiles"
|
||||||
|
v-for="(item, index) in record.files_"
|
||||||
|
v-bind:key="item.id"
|
||||||
|
>
|
||||||
|
<el-link :href="item.path" target="_blank" type="primary">{{
|
||||||
|
item.name
|
||||||
|
}}</el-link>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style lang="scss">
|
|
||||||
.el-transfer-panel {
|
|
||||||
width: 470px;
|
|
||||||
}
|
|
||||||
.el-transfer__buttons {
|
|
||||||
padding: 0 2px;
|
|
||||||
.el-button {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script>
|
<script>
|
||||||
import { getOrgList, getSubOrgList } from "@/api/org";
|
import { getOrgList, getSubOrgList } from "@/api/org";
|
||||||
import { getRecordList } from "@/api/ability";
|
import { getRecordList } from "@/api/ability";
|
||||||
import checkPermission from "@/utils/permission";
|
import checkPermission from "@/utils/permission";
|
||||||
import Pagination from "@/components/Pagination"; // secondary package based on el-pagination
|
import Pagination from "@/components/Pagination"; // secondary package based on el-pagination
|
||||||
const defaultrecord = {
|
|
||||||
name: "",
|
|
||||||
};
|
|
||||||
export default {
|
export default {
|
||||||
components: { Pagination},
|
components: { Pagination},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
record: defaultrecord,
|
record: {},
|
||||||
recordList: {
|
recordList: {
|
||||||
count: 0,
|
count: 0,
|
||||||
},
|
},
|
||||||
|
|
@ -215,7 +237,9 @@ export default {
|
||||||
},
|
},
|
||||||
listLoading: false,
|
listLoading: false,
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
|
drawerLiminted:false,
|
||||||
dialogType: "new",
|
dialogType: "new",
|
||||||
|
orgData:[],
|
||||||
rule1: {
|
rule1: {
|
||||||
name: [{ required: true, message: "请输入", trigger: "blur" }],
|
name: [{ required: true, message: "请输入", trigger: "blur" }],
|
||||||
},
|
},
|
||||||
|
|
@ -225,10 +249,21 @@ export default {
|
||||||
watch: {},
|
watch: {},
|
||||||
created() {
|
created() {
|
||||||
this.getState();
|
this.getState();
|
||||||
|
this.getGroup();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
checkPermission,
|
checkPermission,
|
||||||
|
getGroup() {
|
||||||
|
if (this.checkPermission(["record_confirm"])) {
|
||||||
|
getOrgList({ can_supervision: true }).then((res) => {
|
||||||
|
this.orgData = res.data;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
getSubOrgList().then((res) => {
|
||||||
|
this.orgData = res.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
getState() {
|
getState() {
|
||||||
if(this.checkPermission(["record_confirm"])){
|
if(this.checkPermission(["record_confirm"])){
|
||||||
this.listQuery = {
|
this.listQuery = {
|
||||||
|
|
@ -248,7 +283,14 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
changeTableSort(val) {
|
||||||
|
if (val.order == "ascending") {
|
||||||
|
this.listQuery.ordering = val.prop;
|
||||||
|
} else {
|
||||||
|
this.listQuery.ordering = "-" + val.prop;
|
||||||
|
}
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
handleFilter() {
|
handleFilter() {
|
||||||
this.listQuery.page = 1;
|
this.listQuery.page = 1;
|
||||||
this.getList();
|
this.getList();
|
||||||
|
|
@ -261,8 +303,32 @@ export default {
|
||||||
};
|
};
|
||||||
this.getList();
|
this.getList();
|
||||||
},
|
},
|
||||||
|
handleRecord(item){
|
||||||
|
this.drawerLiminted = true;
|
||||||
|
this.record = item;
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.el-transfer-panel {
|
||||||
|
width: 470px;
|
||||||
|
}
|
||||||
|
.el-transfer__buttons {
|
||||||
|
padding: 0 2px;
|
||||||
|
.el-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.drawerTitle{
|
||||||
|
padding: 20px 0 20px 20px;
|
||||||
|
}
|
||||||
|
.drawerBody{
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.recordfiles{
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<el-button type="primary" @click="handleAdd" icon="el-icon-plus">新增</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="tableData" style="width: 100%;margin-top:10px;" border fit v-loading="listLoading"
|
||||||
|
highlight-current-row max-height="600">
|
||||||
|
<el-table-column type="index" width="50"></el-table-column>
|
||||||
|
<el-table-column align="center" label="名称">
|
||||||
|
<template slot-scope="scope">{{ scope.row.name }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建日期">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.create_time }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="操作">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="primary" size="small" @click="handleEdit(scope)" icon="el-icon-edit"
|
||||||
|
:disabled="!checkPermission(['questioncat_update'])"></el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDelete(scope)" icon="el-icon-delete"
|
||||||
|
:disabled="!checkPermission(['questioncat_delete'])"></el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination v-show="total > 0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit"
|
||||||
|
@pagination="getList" />
|
||||||
|
<el-dialog :visible.sync="dialogVisible" :title="dialogType === 'edit' ? '编辑分类' : '新增分类'">
|
||||||
|
<el-form :model="questioncat" label-width="80px" label-position="right" :rules="rule1" ref="commonForm">
|
||||||
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model="questioncat.name" placeholder="名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div style="text-align:right;">
|
||||||
|
<el-button type="danger" @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirm('commonForm')">确认</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getQuestioncatList,createQuestioncat,updateQuestioncat,deleteQuestioncat } from "@/api//exam";
|
||||||
|
import checkPermission from "@/utils/permission";
|
||||||
|
import Pagination from "@/components/Pagination";
|
||||||
|
|
||||||
|
const defaultObj = {
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
const listQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 20
|
||||||
|
};
|
||||||
|
export default {
|
||||||
|
components: { Pagination },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
questioncat: defaultObj,
|
||||||
|
search: "",
|
||||||
|
total: 0,
|
||||||
|
listQuery: listQuery,
|
||||||
|
tableData: [],
|
||||||
|
typeOptions: [],
|
||||||
|
listLoading: false,
|
||||||
|
dialogVisible: false,
|
||||||
|
dialogType: "new",
|
||||||
|
rule1: {
|
||||||
|
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
|
||||||
|
type: [{ required: true, message: "请选择分类", trigger: "change" }]
|
||||||
|
},
|
||||||
|
typeData: [{
|
||||||
|
value: '公共',
|
||||||
|
label: '公共'
|
||||||
|
}, {
|
||||||
|
value: '专业',
|
||||||
|
label: '专业'
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted(){
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkPermission,
|
||||||
|
|
||||||
|
getList(query = this.listQuery) {
|
||||||
|
this.listLoading = true;
|
||||||
|
getQuestioncatList(query).then(response => {
|
||||||
|
this.tableData = response.data.results;
|
||||||
|
this.total = response.data.count;
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
resetFilter() {
|
||||||
|
this.search = ""
|
||||||
|
this.listQuery = listQuery
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleFilter() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleAdd() {
|
||||||
|
this.questioncat = Object.assign({}, defaultObj);
|
||||||
|
this.dialogType = "new";
|
||||||
|
this.dialogVisible = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs["commonForm"].clearValidate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleEdit(scope) {
|
||||||
|
this.questioncat = Object.assign({}, scope.row); // copy obj
|
||||||
|
this.dialogType = "edit";
|
||||||
|
this.dialogVisible = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs["commonForm"].clearValidate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleDelete(scope) {
|
||||||
|
this.$confirm("确认删除该分类吗?将丢失数据!", "警告", {
|
||||||
|
confirmButtonText: "确认",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "error"
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await deleteQuestioncat(scope.row.id);
|
||||||
|
this.getList();
|
||||||
|
this.$message({
|
||||||
|
type: "success",
|
||||||
|
message: "成功删除!"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
// console.error(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async confirm(form) {
|
||||||
|
let that = this;
|
||||||
|
that.$refs[form].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
let isEdit = that.dialogType === "edit";
|
||||||
|
if (isEdit) {
|
||||||
|
updateQuestioncat(that.questioncat.id, that.questioncat).then(
|
||||||
|
() => {
|
||||||
|
that.getList();
|
||||||
|
that.dialogVisible = false;
|
||||||
|
that.$message.success('成功')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
createQuestioncat(that.questioncat).then(res => {
|
||||||
|
that.getList();
|
||||||
|
that.dialogVisible = false;
|
||||||
|
that.$message.success('成功')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<el-select
|
||||||
|
v-model="listQuery.is_pass"
|
||||||
|
placeholder="是否通过"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
class="filter-item"
|
||||||
|
@change="handleFilter"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in passOptions"
|
||||||
|
:key="item.key"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-date-picker
|
||||||
|
v-model="value"
|
||||||
|
type="daterange"
|
||||||
|
align="right"
|
||||||
|
unlink-panels
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:picker-options="pickerOptions">
|
||||||
|
</el-date-picker>
|
||||||
|
<el-input
|
||||||
|
v-model="listQuery.search"
|
||||||
|
placeholder="输入用户名或用户单位名搜索"
|
||||||
|
style="width: 200px;"
|
||||||
|
class="filter-item"
|
||||||
|
@keyup.enter.native="handleFilter"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
class="filter-item"
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-refresh-left"
|
||||||
|
@click="resetFilter"
|
||||||
|
>刷新重置</el-button>
|
||||||
|
<el-button type="primary" icon="el-icon-download" @click="exportTest" >导出Excel</el-button>
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="tableData.results"
|
||||||
|
style="width: 100%;margin-top:10px;"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
fit
|
||||||
|
v-loading="listLoading"
|
||||||
|
highlight-current-row
|
||||||
|
max-height="600"
|
||||||
|
@sort-change="changeSort"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" width="50"></el-table-column>
|
||||||
|
<el-table-column align="left" label="类型">
|
||||||
|
<template slot-scope="scope">{{ scope.row.type }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="用户">
|
||||||
|
<template slot-scope="scope">{{ scope.row.create_by_name}}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!-- <el-table-column align="left" label="所属部门">
|
||||||
|
<template slot-scope="scope">{{ scope.row.dept_name }}</template>
|
||||||
|
</el-table-column> -->
|
||||||
|
<el-table-column align="left" label="是否通过">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag v-if="scope.row.is_pass" type="success">是</el-tag>
|
||||||
|
<el-tag v-else type="danger">否</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="得分" sortable='custom' prop="score">
|
||||||
|
<template slot-scope="scope">{{ scope.row.score }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="总分">
|
||||||
|
<template slot-scope="scope">{{ scope.row.total_score }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="耗时(时分秒)" sortable='custom' prop="took">
|
||||||
|
<template slot-scope="scope">{{ scope.row.took_format }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="答题时间">
|
||||||
|
<template slot-scope="scope">{{ scope.row.start_time }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="操作" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.type=='正式考试'"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleExport(scope)"
|
||||||
|
>生成Word</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.type=='正式考试'"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="handleExport2(scope)"
|
||||||
|
>重新生成</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.type=='正式考试'"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(scope)"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination
|
||||||
|
v-show="tableData.count>0"
|
||||||
|
:total="tableData.count"
|
||||||
|
:page.sync="listQuery.page"
|
||||||
|
:limit.sync="listQuery.limit"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getExamRecordList, exportTest, exportwTest, deleteExamRecord,issue } from "@/api/exam";
|
||||||
|
import checkPermission from "@/utils/permission";
|
||||||
|
import Pagination from "@/components/Pagination";
|
||||||
|
|
||||||
|
const listQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
type:'正式考试',
|
||||||
|
search:''
|
||||||
|
};
|
||||||
|
export default {
|
||||||
|
components: { Pagination },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
listQuery: Object.assign({}, listQuery),
|
||||||
|
tableData: {
|
||||||
|
count:0,
|
||||||
|
results:[],
|
||||||
|
},
|
||||||
|
listLoading: true,
|
||||||
|
typeOptions: [
|
||||||
|
{ key: "自助模考", label: "自助模考", value: "自助模考" },
|
||||||
|
{ key: "押卷模考", label: "押卷模考", value: "押卷模考"},
|
||||||
|
{ key: "正式考试", label: "正式考试", value: "正式考试"},
|
||||||
|
],
|
||||||
|
passOptions: [
|
||||||
|
{ key: true, label: "通过", value: true },
|
||||||
|
{ key: false, label: "未通过", value: false},
|
||||||
|
],
|
||||||
|
pickerOptions: {
|
||||||
|
shortcuts: [{
|
||||||
|
text: '最近一天',
|
||||||
|
onClick(picker) {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setTime(start.getTime() - 3600 * 1000 * 24);
|
||||||
|
picker.$emit('pick', [start, end]);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: '最近一周',
|
||||||
|
onClick(picker) {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
|
||||||
|
picker.$emit('pick', [start, end]);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: '最近一个月',
|
||||||
|
onClick(picker) {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||||
|
picker.$emit('pick', [start, end]);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: '最近三个月',
|
||||||
|
onClick(picker) {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
||||||
|
picker.$emit('pick', [start, end]);
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
watch:{
|
||||||
|
value:'setTimeRange',
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getQuery();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkPermission,
|
||||||
|
getQuery() {
|
||||||
|
if(this.$route.params.exam){
|
||||||
|
this.listQuery.exam = this.$route.params.exam;
|
||||||
|
this.getList()
|
||||||
|
}else{
|
||||||
|
this.getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.listLoading = true;
|
||||||
|
getExamRecordList(this.listQuery).then(response => {
|
||||||
|
this.tableData = response.data;
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleFilter() {
|
||||||
|
this.listQuery.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
this.listQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
type:'正式考试',
|
||||||
|
search:'',
|
||||||
|
};
|
||||||
|
this.value = []
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleExport(scope) {
|
||||||
|
const loading = this.$loading({text: '正在生成word...',});
|
||||||
|
exportwTest(scope.row.id).then(res=>{
|
||||||
|
loading.close()
|
||||||
|
window.open(res.data.path, "_blank");
|
||||||
|
}).catch(e=>{loading.close()})
|
||||||
|
},
|
||||||
|
handleDelete(scope){
|
||||||
|
this.$confirm("确认删除该考试记录吗?将丢失数据!", "警告", {
|
||||||
|
confirmButtonText: "确认",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "error"
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await deleteExamtest(scope.row.id);
|
||||||
|
this.getList()
|
||||||
|
this.$message({
|
||||||
|
type: "success",
|
||||||
|
message: "成功删除!"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleExport2(scope) {
|
||||||
|
const loading = this.$loading({text: '正在重新生成word...',});
|
||||||
|
exportwTest(scope.row.id, {anew:true}).then(res=>{
|
||||||
|
loading.close()
|
||||||
|
window.open(res.data.path, "_blank");
|
||||||
|
}).catch(e=>{loading.close()})
|
||||||
|
},
|
||||||
|
exportTest() {
|
||||||
|
const loading = this.$loading();
|
||||||
|
exportTest(this.listQuery).then(response => {
|
||||||
|
loading.close()
|
||||||
|
window.open(response.data.path, "_blank");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setTimeRange(){
|
||||||
|
this.listQuery.start = this.value[0],
|
||||||
|
this.listQuery.end = this.value[1],
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
changeSort (val) {
|
||||||
|
if(val.order == 'ascending'){
|
||||||
|
this.listQuery.ordering = val.prop
|
||||||
|
}else{
|
||||||
|
this.listQuery.ordering = '-' + val.prop
|
||||||
|
}
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,282 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<el-input
|
||||||
|
v-model="listQuery.search"
|
||||||
|
placeholder="输入考试名称进行搜索"
|
||||||
|
style="width: 300px;"
|
||||||
|
class="filter-item"
|
||||||
|
@keyup.enter.native="handleFilter"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="handleAdd" icon="el-icon-plus" v-if ="checkPermission(['exam_create'])">新增</el-button>
|
||||||
|
<el-button
|
||||||
|
class="filter-item"
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-refresh-left"
|
||||||
|
@click="resetFilter"
|
||||||
|
>刷新重置</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="tableData.results"
|
||||||
|
style="width: 100%;margin-top:10px;"
|
||||||
|
border
|
||||||
|
fit
|
||||||
|
v-loading="listLoading"
|
||||||
|
highlight-current-row
|
||||||
|
max-height="600"
|
||||||
|
row-key="id"
|
||||||
|
default-expand-all
|
||||||
|
>
|
||||||
|
<el-table-column label="考试名称">
|
||||||
|
<template slot-scope="scope">{{ scope.row.name }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="考试试卷">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{scope.row.paper_.name }}
|
||||||
|
<!-- <text v-if="scope.row.paper_">{{scope.row.paper_.name }}</text> -->
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="考试地点">
|
||||||
|
<template slot-scope="scope">{{ scope.row.place }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="参考机会">
|
||||||
|
<template slot-scope="scope">{{ scope.row.chance }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="开启时间">
|
||||||
|
<template slot-scope="scope">{{ scope.row.open_time }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="关闭时间">
|
||||||
|
<template slot-scope="scope">{{ scope.row.close_time }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建人">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.create_admin_username }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="操作" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleView(scope)"
|
||||||
|
:disabled="!checkPermission(['exam_view'])"
|
||||||
|
>详情</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
@click="handleEdit(scope)"
|
||||||
|
:disabled="!checkPermission(['exam_update'])"
|
||||||
|
>编辑</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(scope)"
|
||||||
|
:disabled="!checkPermission(['exam_delete'])"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination
|
||||||
|
v-show="tableData.count>0"
|
||||||
|
:total="tableData.count"
|
||||||
|
:page.sync="listQuery.page"
|
||||||
|
:limit.sync="listQuery.limit"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
<el-dialog :visible.sync="dialogVisible" :title="dialogType==='edit'?'编辑考试':'新增考试'" >
|
||||||
|
<el-form :model="exam" label-width="120px" :rules="rule1" ref="examForm">
|
||||||
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model="exam.name" placeholder="名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="考试地点" prop="place">
|
||||||
|
<el-input v-model="exam.place" placeholder="考试地点" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="参考机会" prop="chance">
|
||||||
|
<el-input-number v-model="exam.chance" placeholder="参考机会" :min="1"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="开启时间" prop="open_time">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="exam.open_time"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="开启时间"
|
||||||
|
style="width:100%">
|
||||||
|
</el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="关闭时间" prop="close_time">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="exam.close_time"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="关闭时间"
|
||||||
|
style="width:100%"
|
||||||
|
></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="监考人姓名" prop="proctor_name" label-width="120px">
|
||||||
|
<el-input v-model="exam.proctor_name" placeholder="监考人姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="监考人电话" prop="proctor_phone" label-width="120px">
|
||||||
|
<el-input v-model="exam.proctor_phone" placeholder="监考人联系方式" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选定试卷" prop="paper" >
|
||||||
|
<el-select v-model="exam.paper" placeholder="可指定试卷" style="width:100%" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="item in paperOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name"
|
||||||
|
:value="item.id"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div style="text-align:right;">
|
||||||
|
<el-button type="danger" @click="dialogVisible=false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="confirmexam('examForm')">确认</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { getPaperList,getExamList, createExam, updateExam,deleteExam} from "@/api/exam";
|
||||||
|
import checkPermission from "@/utils/permission";
|
||||||
|
import Pagination from "@/components/Pagination"
|
||||||
|
|
||||||
|
const defaultexam = {
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
place: "",
|
||||||
|
open_time: null,
|
||||||
|
close_time: null,
|
||||||
|
proctor_name:'',
|
||||||
|
proctor_phone:'',
|
||||||
|
chance:3,
|
||||||
|
paper:''
|
||||||
|
};
|
||||||
|
const listQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
search: ""
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
components: { Pagination },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selects:[],
|
||||||
|
exam: {
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
listQuery:listQuery,
|
||||||
|
tableData: {count:0},
|
||||||
|
listLoading: true,
|
||||||
|
dialogVisible: false,
|
||||||
|
dialogType: "new",
|
||||||
|
workscopeOptions:[],
|
||||||
|
paperOptions:[],
|
||||||
|
rule1: {
|
||||||
|
name: [{ required: true, message: "请输入", trigger: "blur" }],
|
||||||
|
place: [{ required: true, message: "请输入", trigger: "change" }],
|
||||||
|
workscope: [{ required: true, message: "请选择", trigger: "change" }],
|
||||||
|
open_time: [{ required: true, message: "请选择", trigger: "change" }],
|
||||||
|
close_time: [{ required: true, message: "请选择", trigger: "change" }],
|
||||||
|
proctor_name: [{ required: true, message: "请输入", trigger: "change" }],
|
||||||
|
proctor_phone: [{ required: true, message: "请输入", trigger: "change" }],
|
||||||
|
chance: [{ required: true, message: "请输入", trigger: "change" }]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
mounted() {
|
||||||
|
this.getPaperOptions();
|
||||||
|
this.getList();
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkPermission,
|
||||||
|
getPaperOptions(){
|
||||||
|
getPaperList({page:0}).then(res=>{
|
||||||
|
debugger;
|
||||||
|
console.log(res)
|
||||||
|
this.paperOptions = res.data
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.listLoading = true;
|
||||||
|
debugger;
|
||||||
|
getExamList(this.listQuery).then(response => {
|
||||||
|
this.tableData = response.data
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
this.listQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
search: ""
|
||||||
|
};
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleFilter() {
|
||||||
|
this.listQuery.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleAdd() {
|
||||||
|
this.exam = Object.assign({}, defaultexam);
|
||||||
|
this.dialogType = "new";
|
||||||
|
this.dialogVisible = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs["examForm"].clearValidate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleEdit(scope) {
|
||||||
|
this.exam = Object.assign({}, scope.row); // copy obj
|
||||||
|
this.dialogType = "edit";
|
||||||
|
this.dialogVisible = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs["examForm"].clearValidate();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleDelete(scope) {
|
||||||
|
this.$confirm("确认删除该考试吗?将丢失数据!", "警告", {
|
||||||
|
confirmButtonText: "确认",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "error"
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await deleteExam(scope.row.id);
|
||||||
|
this.getList()
|
||||||
|
this.$message({
|
||||||
|
type: "success",
|
||||||
|
message: "成功删除!"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
// console.error(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleView(scope){
|
||||||
|
this.$router.push({ path: "/exam/record", query: { exam: scope.row.id } })
|
||||||
|
},
|
||||||
|
async confirmexam(form) {
|
||||||
|
this.$refs[form].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
const isEdit = this.dialogType === "edit";
|
||||||
|
if (isEdit) {
|
||||||
|
updateExam(this.exam.id, this.exam).then(() => {
|
||||||
|
this.getList();
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.$message.success('成功')
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createExam(this.exam).then(res => {
|
||||||
|
this.getList();
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.$message.success('成功')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
<template>
|
||||||
|
<el-dialog title="选取试题" :visible.sync="chooseVisible_" width="80%" >
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<el-select v-model="questioncatC" placeholder="分类" clearable style="width: 200px" class="filter-item" @change="handleFilter">
|
||||||
|
<el-option v-for="item in questioncatData" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
<el-select
|
||||||
|
v-model="listQuery.type"
|
||||||
|
placeholder="题型"
|
||||||
|
clearable
|
||||||
|
style="width: 120px"
|
||||||
|
class="filter-item"
|
||||||
|
@change="handleFilter"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in typeOptions"
|
||||||
|
:key="item.key"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
<el-input
|
||||||
|
v-model="search"
|
||||||
|
placeholder="输入题干进行搜索"
|
||||||
|
style="width: 200px;"
|
||||||
|
class="filter-item"
|
||||||
|
@keyup.enter.native="handleSearch"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
class="filter-item"
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-search"
|
||||||
|
@click="handleSearch"
|
||||||
|
>搜索</el-button>
|
||||||
|
<el-button
|
||||||
|
class="filter-item"
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-refresh-left"
|
||||||
|
@click="resetFilter"
|
||||||
|
>刷新重置</el-button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="tableData.results"
|
||||||
|
style="width: 100%;margin-top:10px;"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
fit
|
||||||
|
v-loading="listLoading"
|
||||||
|
highlight-current-row
|
||||||
|
max-height="400"
|
||||||
|
ref="table"
|
||||||
|
>
|
||||||
|
<el-table-column
|
||||||
|
type="selection"
|
||||||
|
width="55">
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="题干">
|
||||||
|
<template slot-scope="scope">{{ scope.row.name }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="所属题库">
|
||||||
|
<template slot-scope="scope">{{ scope.row.questioncat_name }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="题型">
|
||||||
|
<template slot-scope="scope">{{ scope.row.type }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="难易度">
|
||||||
|
<template slot-scope="scope">{{ scope.row.level }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建日期">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.create_time }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="操作">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleDetail(scope)"
|
||||||
|
icon="el-icon-more"
|
||||||
|
></el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination
|
||||||
|
v-show="tableData.count>0"
|
||||||
|
:total="tableData.count"
|
||||||
|
:page.sync="listQuery.page"
|
||||||
|
:limit.sync="listQuery.limit"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
<el-dialog
|
||||||
|
title="题目详情"
|
||||||
|
:visible.sync="dialogVisible"
|
||||||
|
width="30%"
|
||||||
|
append-to-body>
|
||||||
|
<div>{{question.type}}</div>
|
||||||
|
<div>{{question.name}}</div>
|
||||||
|
<ul id="repeat">
|
||||||
|
<li v-for="(value,key,index) in question.options">
|
||||||
|
{{ key }}:{{value}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div>正确答案{{question.right}}</div>
|
||||||
|
<div>{{question.resolution}}</div>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
<div style="text-align:left;">
|
||||||
|
<el-button type="primary" @click="choseQuestions('table')">选中</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getQuestioncatList,
|
||||||
|
getQuestionList,
|
||||||
|
} from "@/api/exam";
|
||||||
|
import Pagination from "@/components/Pagination";
|
||||||
|
|
||||||
|
const listQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
search:''
|
||||||
|
};
|
||||||
|
export default {
|
||||||
|
name:'Questionchoose',
|
||||||
|
components: { Pagination },
|
||||||
|
props: {
|
||||||
|
chooseVisible: Boolean,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
questioncat: {
|
||||||
|
id: "",
|
||||||
|
name: ""
|
||||||
|
},
|
||||||
|
listQuery: listQuery,
|
||||||
|
search:"",
|
||||||
|
tableData: {
|
||||||
|
count:0,
|
||||||
|
results:[]
|
||||||
|
},
|
||||||
|
questioncatData:[],
|
||||||
|
listLoading: true,
|
||||||
|
dialogVisible: false,
|
||||||
|
typeOptions: [
|
||||||
|
{ key: 1, label: "单选", value: "单选" },
|
||||||
|
{ key: 2, label: "多选", value: "多选"},
|
||||||
|
{ key: 3, label: "判断", value: "判断" }
|
||||||
|
],
|
||||||
|
question:{},
|
||||||
|
questioncatC:[]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
chooseVisible_: {
|
||||||
|
get() {
|
||||||
|
return this.chooseVisible;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('closeDg',val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList();
|
||||||
|
this.getQuestioncatAll();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList(query = this.listQuery) {
|
||||||
|
this.listLoading = true;
|
||||||
|
getQuestionList(query).then(response => {
|
||||||
|
if(response.data.results){
|
||||||
|
this.tableData = response.data
|
||||||
|
}
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getQuestioncatAll() {
|
||||||
|
getQuestioncatList().then(response => {
|
||||||
|
this.questioncatData = response.data.results;
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
handleFilter() {
|
||||||
|
if(this.questioncatC.length) {
|
||||||
|
this.listQuery.questioncat = this.questioncatC[this.questioncatC.length-1]
|
||||||
|
}else{
|
||||||
|
this.listQuery.questioncat = ''
|
||||||
|
}
|
||||||
|
this.listQuery.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
this.search = ""
|
||||||
|
this.listQuery = listQuery
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.getList({ search: this.search });
|
||||||
|
},
|
||||||
|
handleDetail(scope) {
|
||||||
|
this.dialogVisible = true
|
||||||
|
this.question = scope.row
|
||||||
|
},
|
||||||
|
choseQuestions(table) {
|
||||||
|
const _selectData = this.$refs.table.selection
|
||||||
|
this.$emit('choseQ',_selectData);
|
||||||
|
this.$emit('closeDg',false);
|
||||||
|
this.$refs.table.clearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-card style=" min-height: calc(100vh - 65px);">
|
||||||
|
<el-form :model="Form" :rules="rules" ref="Form" label-width="100px" status-icon>
|
||||||
|
<el-form-item label="题型" prop="type">
|
||||||
|
<el-select v-model="Form.type" style="width: 400px">
|
||||||
|
<el-option v-for="item in typeOptions" :key="item.key" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类" prop="type">
|
||||||
|
<el-select v-model="Form.questioncat" placeholder="分类" clearable style="width: 200px" class="filter-item">
|
||||||
|
<el-option v-for="item in catOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="题干" prop="name">
|
||||||
|
<el-input v-model="Form.name" height="100" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="题干图片" prop="img">
|
||||||
|
<el-upload
|
||||||
|
class="avatar-uploader"
|
||||||
|
:headers="upHeaders"
|
||||||
|
:action="upUrl"
|
||||||
|
accept="image/jpeg, image/gif, image/png, image/bmp"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="handleImgSuccess"
|
||||||
|
:before-upload="beforeImgUpload">
|
||||||
|
<img v-if="Form.img" :src="Form.img" style="width: 200px;height: 100px;display: block;" />
|
||||||
|
|
||||||
|
<el-button size="small" type="primary" v-else>点击上传</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-button type="text" @click="delImg()" v-if="Form.img">删除</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项A" prop="optionA">
|
||||||
|
<el-input v-model="Form.options.A" height="30" width="800px" :disabled="inputDisable" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项B" prop="optionB">
|
||||||
|
<el-input v-model="Form.options.B" height="30" width="800px" :disabled="inputDisable" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项C" v-show="Form.type != '判断'">
|
||||||
|
<el-input v-model="Form.options.C" height="30" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项D" v-show="Form.type != '判断'">
|
||||||
|
<el-input v-model="Form.options.D" height="30" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项E" v-show="Form.type != '判断'">
|
||||||
|
<el-input v-model="Form.options.E" height="30" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项F" v-show="Form.type != '判断'">
|
||||||
|
<el-input v-model="Form.options.F" height="30" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="正确答案" v-if="Form.type == '多选'">
|
||||||
|
<el-checkbox-group v-model="Form.right">
|
||||||
|
<el-checkbox label="A"></el-checkbox>
|
||||||
|
<el-checkbox label="B"></el-checkbox>
|
||||||
|
<el-checkbox label="C"></el-checkbox>
|
||||||
|
<el-checkbox label="D"></el-checkbox>
|
||||||
|
<el-checkbox label="E"></el-checkbox>
|
||||||
|
<el-checkbox label="F"></el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="正确答案" v-else-if="Form.type == '单选'">
|
||||||
|
<el-radio-group v-model="Form.right">
|
||||||
|
<el-radio label="A"></el-radio>
|
||||||
|
<el-radio label="B"></el-radio>
|
||||||
|
<el-radio label="C"></el-radio>
|
||||||
|
<el-radio label="D"></el-radio>
|
||||||
|
<el-radio label="E"></el-radio>
|
||||||
|
<el-radio label="F"></el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="正确答案" v-else>
|
||||||
|
<el-radio-group v-model="Form.right">
|
||||||
|
<el-radio label="A"></el-radio>
|
||||||
|
<el-radio label="B"></el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="解析">
|
||||||
|
<el-input v-model="Form.resolution" style="width:600px" type="textarea" :rows=3></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="真题年份" prop="year">
|
||||||
|
<el-input v-model="Form.year"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm('Form')" :loading="submitLoding">立即创建</el-button>
|
||||||
|
<el-button @click="resetForm('Form')">重置</el-button>
|
||||||
|
<el-button type="warning" @click="goBack()">返回</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { createQuestion,getQuestioncatList } from "@/api/exam";
|
||||||
|
import { upUrl } from "@/api/file";
|
||||||
|
import { getToken } from "@/utils/auth";
|
||||||
|
export default {
|
||||||
|
components: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
upHeaders: { Authorization: "JWT " + getToken() },
|
||||||
|
upUrl: upUrl(),
|
||||||
|
Form: {
|
||||||
|
name: "",
|
||||||
|
type: "",
|
||||||
|
img: null,
|
||||||
|
questioncat: null,
|
||||||
|
year: null,
|
||||||
|
right: '',
|
||||||
|
options: {
|
||||||
|
A: '',
|
||||||
|
B: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
catOptions: [],
|
||||||
|
inputDisable: false,
|
||||||
|
submitLoding: false,
|
||||||
|
rules: {
|
||||||
|
type: [
|
||||||
|
{ required: true, message: "请选择", trigger: "blur" }
|
||||||
|
],
|
||||||
|
name: [
|
||||||
|
{ required: true, message: "请输入", trigger: "blur" }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
typeOptions: [
|
||||||
|
{ key: 1, label: "单选", value: "单选" },
|
||||||
|
{ key: 2, label: "多选", value: "多选" },
|
||||||
|
{ key: 3, label: "判断", value: "判断" }
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'Form.type': 'setOptions'
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getQuestioncatAll()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleImgSuccess(res, file) {
|
||||||
|
this.Form.img = res.data.path
|
||||||
|
},
|
||||||
|
beforeImgUpload(file) {
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 0.6;
|
||||||
|
if (!isLt2M) {
|
||||||
|
this.$message.error("上传图片大小不能超过 600KB!");
|
||||||
|
}
|
||||||
|
return isLt2M;
|
||||||
|
},
|
||||||
|
submitForm(formName) {
|
||||||
|
let that =this;
|
||||||
|
that.$refs[formName].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
that.submitLoding = true
|
||||||
|
for (let key in that.Form.options) {
|
||||||
|
if (!that.Form.options[key]) {
|
||||||
|
delete that.Form.options[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debugger;
|
||||||
|
console.log(that.Form)
|
||||||
|
createQuestion(that.Form).then(response => {
|
||||||
|
that.submitLoding = false
|
||||||
|
if (response.code >= 200) {
|
||||||
|
that.$message({
|
||||||
|
type: "success",
|
||||||
|
message: "新建成功!"
|
||||||
|
});
|
||||||
|
that.goBack()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetForm(formName) {
|
||||||
|
this.$refs[formName].resetFields();
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.$router.go(-1)
|
||||||
|
},
|
||||||
|
getQuestioncatAll() {
|
||||||
|
getQuestioncatList().then(response => {
|
||||||
|
this.catOptions = response.data.results;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
delImg() {
|
||||||
|
this.Form.img = null
|
||||||
|
},
|
||||||
|
setOptions() {
|
||||||
|
if (this.Form.type == '判断') {
|
||||||
|
this.Form.options = {
|
||||||
|
A: '对',
|
||||||
|
B: '错'
|
||||||
|
}
|
||||||
|
this.inputDisable = true
|
||||||
|
} else {
|
||||||
|
this.Form.options = {
|
||||||
|
A: '',
|
||||||
|
B: ''
|
||||||
|
}
|
||||||
|
this.inputDisable = false
|
||||||
|
}
|
||||||
|
if (this.Form.type == '多选') {
|
||||||
|
this.Form.right = []
|
||||||
|
} else {
|
||||||
|
this.Form.right = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.app-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<el-select v-model="listQuery.questioncat" placeholder="题目分类" clearable style="width: 200px" class="filter-item"
|
||||||
|
@change="handleFilter">
|
||||||
|
<el-option v-for="item in questioncatData" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-select v-model="listQuery.type" placeholder="题型" clearable style="width: 120px" class="filter-item"
|
||||||
|
@change="handleFilter">
|
||||||
|
<el-option v-for="item in typeOptions" :key="item.key" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-input v-model="listQuery.search" placeholder="输入题干进行搜索" style="width: 200px;" class="filter-item"
|
||||||
|
@keyup.enter.native="handleSearch" />
|
||||||
|
<el-button class="filter-item" type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button class="filter-item" type="primary" icon="el-icon-refresh-left" @click="resetFilter">刷新重置
|
||||||
|
</el-button>
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<el-button type="primary" slot="reference" @click="handleAdd()">新增</el-button>
|
||||||
|
<el-button @click="handleEnabled">启用</el-button>
|
||||||
|
<el-popover placement="top" width="160" v-if="checkPermission(['question_import'])"
|
||||||
|
v-model="popovervisible">
|
||||||
|
<p>导入题目前,请下载模板并按格式录入.</p>
|
||||||
|
<div style="text-align: left; margin: 0;">
|
||||||
|
<el-link href="/media/muban/question.xlsx" target="_blank" @click="popovervisible = false"
|
||||||
|
type="primary">下载模板</el-link>
|
||||||
|
<el-upload :action="upUrl" :on-success="handleUploadSuccess" accept=".xlsx" :headers="upHeaders"
|
||||||
|
:show-file-list="false">
|
||||||
|
<el-button size="small" type="primary" @click="popovervisible = false">上传导入</el-button>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-button slot="reference">Excel导入</el-button>
|
||||||
|
</el-popover>
|
||||||
|
<el-button type="primary" icon="el-icon-download" @click="exportQuestion">导出Excel</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-table :data="tableData" style="width: 100%;margin-top:10px;" border stripe fit v-loading="listLoading"
|
||||||
|
highlight-current-row max-height="600" @sort-change="changeSort" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55"></el-table-column>
|
||||||
|
<el-table-column label="题干" sortable="custom" prop="name" width="400px">
|
||||||
|
<template slot-scope="scope">{{ scope.row.name }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="所属分类">
|
||||||
|
<template slot-scope="scope">{{ scope.row.questioncat_name }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="题型">
|
||||||
|
<template slot-scope="scope">{{ scope.row.type }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="是否启用">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag v-if="scope.row.enabled" type="success">是</el-tag>
|
||||||
|
<el-tag v-else type="danger">否</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="真题年份">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.year">{{ scope.row.year }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建日期" sortable='custom' prop="create_time">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ scope.row.create_time }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="操作">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="primary" size="small" @click="handleDetail(scope)" icon="el-icon-more"></el-button>
|
||||||
|
<el-button type="primary" size="small" @click="handleEdit(scope)" icon="el-icon-edit"></el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDelete(scope)" icon="el-icon-delete"
|
||||||
|
:disabled="!checkPermission(['question_delete'])"></el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination v-show="total > 0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.limit"
|
||||||
|
@pagination="getList" />
|
||||||
|
<el-dialog title="题目详情" :visible.sync="dialogVisible" width="30%">
|
||||||
|
<div>{{ question.type }}</div>
|
||||||
|
<div>{{ question.name }}</div>
|
||||||
|
<ul id="repeat">
|
||||||
|
<li v-for="(value, key) in question.options" v-bind:key="key">
|
||||||
|
{{ key }}:
|
||||||
|
<span>{{ value }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div>正确答案{{ question.right }}</div>
|
||||||
|
<div>{{ question.resolution }}</div>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getQuestioncatList,
|
||||||
|
getQuestionList,
|
||||||
|
deleteQuestion,
|
||||||
|
importQuestion,
|
||||||
|
exportQuestion,
|
||||||
|
enableQuestions,
|
||||||
|
} from "@/api/exam";
|
||||||
|
import checkPermission from "@/utils/permission";
|
||||||
|
import Pagination from "@/components/Pagination";
|
||||||
|
import { upUrl, upHeaders } from "@/api/file";
|
||||||
|
|
||||||
|
const defaultObj = {
|
||||||
|
id: "",
|
||||||
|
name: ""
|
||||||
|
};
|
||||||
|
const listQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
search: '',
|
||||||
|
questioncat:''
|
||||||
|
};
|
||||||
|
export default {
|
||||||
|
components: { Pagination },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
popovervisible: false,
|
||||||
|
upUrl: upUrl(),
|
||||||
|
upHeaders: upHeaders(),
|
||||||
|
questioncat: {
|
||||||
|
id: "",
|
||||||
|
name: ""
|
||||||
|
},
|
||||||
|
total: 0,
|
||||||
|
listQuery: listQuery,
|
||||||
|
tableData: [],
|
||||||
|
questioncatData: [],
|
||||||
|
listLoading: false,
|
||||||
|
dialogVisible: false,
|
||||||
|
dialogType: "new",
|
||||||
|
rule1: {
|
||||||
|
name: [{ required: true, message: "请输入名称", trigger: "blur" }]
|
||||||
|
},
|
||||||
|
typeOptions: [
|
||||||
|
{ key: 1, label: "单选", value: "单选" },
|
||||||
|
{ key: 2, label: "多选", value: "多选" },
|
||||||
|
{ key: 3, label: "判断", value: "判断" }
|
||||||
|
],
|
||||||
|
question: {},
|
||||||
|
selects: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
created() {
|
||||||
|
this.getList();
|
||||||
|
this.getQuestioncatList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkPermission,
|
||||||
|
handleUploadSuccess(res, file) {
|
||||||
|
if (res.code == 200) {
|
||||||
|
const loading = this.$loading({ text: "正在导入中..." })
|
||||||
|
importQuestion(res.data).then(response => {
|
||||||
|
loading.close()
|
||||||
|
if (response.code == 200) {
|
||||||
|
this.$message({
|
||||||
|
message: '导入成功',
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
this.getList(listQuery)
|
||||||
|
} else if (response.code == 206) {
|
||||||
|
this.$message({
|
||||||
|
message: '部分未成功' + response.data,
|
||||||
|
type: 'success'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$message.error(response.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$message.error("Excel上传失败!");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.listLoading = true;
|
||||||
|
getQuestionList(this.listQuery).then(response => {
|
||||||
|
this.tableData = response.data.results;
|
||||||
|
this.total = response.data.count;
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getQuestioncatList() {
|
||||||
|
getQuestioncatList().then(response => {
|
||||||
|
this.questioncatData = response.data.results;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleFilter() {
|
||||||
|
this.listQuery.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
this.listQuery = listQuery
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.listQuery.page = 1
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleAdd() {
|
||||||
|
this.$router.push({ path: "/exam/questionCreate"})
|
||||||
|
},
|
||||||
|
handleDetail(scope) {
|
||||||
|
this.dialogVisible = true
|
||||||
|
this.question = scope.row
|
||||||
|
},
|
||||||
|
handleEdit(scope) {
|
||||||
|
this.$router.push({ path: "/exam/questionUpdate", query: { id: scope.row.id } })
|
||||||
|
},
|
||||||
|
handleDelete(scope) {
|
||||||
|
this.$confirm("确认删除该题目吗?将丢失数据!", "警告", {
|
||||||
|
confirmButtonText: "确认",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "error"
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await deleteQuestion(scope.row.id);
|
||||||
|
this.getList();
|
||||||
|
this.$message({
|
||||||
|
type: "success",
|
||||||
|
message: "成功删除!"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
// console.error(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
exportQuestion() {
|
||||||
|
const loading = this.$loading({
|
||||||
|
text: '正在准备..'
|
||||||
|
});
|
||||||
|
exportQuestion(this.listQuery).then(response => {
|
||||||
|
loading.close()
|
||||||
|
window.open(response.data.path, "_blank");
|
||||||
|
}).catch(e => { loading.close() });
|
||||||
|
},
|
||||||
|
changeSort(val) {
|
||||||
|
if (val.order == "ascending") {
|
||||||
|
this.listQuery.ordering = val.prop;
|
||||||
|
} else {
|
||||||
|
this.listQuery.ordering = "-" + val.prop;
|
||||||
|
}
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleSelectionChange(val) {
|
||||||
|
let selects = [];
|
||||||
|
for (var i = 0; i < val.length; i++) {
|
||||||
|
selects.push(val[i].id);
|
||||||
|
}
|
||||||
|
this.selects = selects;
|
||||||
|
},
|
||||||
|
handleEnabled() {
|
||||||
|
if (this.selects.length) {
|
||||||
|
enableQuestions({ ids: this.selects }).then(res => {
|
||||||
|
this.$message.success("成功");
|
||||||
|
this.getList();
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$message.warning("请先选择题目");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="Form" :rules="rules" ref="Form" label-width="100px" status-icon>
|
||||||
|
<el-form-item label="题型" prop="type">
|
||||||
|
<el-select v-model="Form.type" style="width: 400px" :disabled="true">
|
||||||
|
<el-option
|
||||||
|
v-for="item in typeOptions"
|
||||||
|
:key="item.key"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类" prop="type">
|
||||||
|
<el-select v-model="Form.questioncat" placeholder="分类" clearable style="width: 200px" class="filter-item">
|
||||||
|
<el-option v-for="item in catOptions" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="题干" prop="name">
|
||||||
|
<el-input v-model="Form.name" height="100" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="题干图片" prop="img" >
|
||||||
|
<el-upload
|
||||||
|
class="avatar-uploader"
|
||||||
|
:headers="upHeaders"
|
||||||
|
:action="upUrl"
|
||||||
|
accept="image/jpeg, image/gif, image/png, image/bmp"
|
||||||
|
:show-file-list="false"
|
||||||
|
:on-success="handleImgSuccess"
|
||||||
|
:before-upload="beforeImgUpload"
|
||||||
|
>
|
||||||
|
<img v-if="Form.img" :src="Form.img" style="width: 200px;height: 100px;display: block;"/>
|
||||||
|
<el-button size="small" type="primary" v-else>点击上传</el-button>
|
||||||
|
</el-upload>
|
||||||
|
<el-button type="text" @click="delImg()" v-if="Form.img">删除</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项A" prop="optionA">
|
||||||
|
<el-input v-model="Form.options.A" height="30" width="800px" :disabled="inputDisable" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项B" prop="optionB">
|
||||||
|
<el-input v-model="Form.options.B" height="30" width="800px" :disabled="inputDisable" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项C" v-show="Form.type!='判断'">
|
||||||
|
<el-input v-model="Form.options.C" height="30" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项D" v-show="Form.type!='判断'">
|
||||||
|
<el-input v-model="Form.options.D" height="30" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项E" v-show="Form.type!='判断'">
|
||||||
|
<el-input v-model="Form.options.E" height="30" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选项F" v-show="Form.type!='判断'">
|
||||||
|
<el-input v-model="Form.options.F" height="30" width="800px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="正确答案" v-if="Form.type =='多选'">
|
||||||
|
<el-checkbox-group v-model="Form.right">
|
||||||
|
<el-checkbox label="A"></el-checkbox>
|
||||||
|
<el-checkbox label="B"></el-checkbox>
|
||||||
|
<el-checkbox label="C"></el-checkbox>
|
||||||
|
<el-checkbox label="D"></el-checkbox>
|
||||||
|
<el-checkbox label="E"></el-checkbox>
|
||||||
|
<el-checkbox label="F"></el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="正确答案" v-else-if="Form.type =='单选'">
|
||||||
|
<el-radio-group v-model="Form.right">
|
||||||
|
<el-radio label="A"></el-radio>
|
||||||
|
<el-radio label="B"></el-radio>
|
||||||
|
<el-radio label="C"></el-radio>
|
||||||
|
<el-radio label="D"></el-radio>
|
||||||
|
<el-radio label="E"></el-radio>
|
||||||
|
<el-radio label="F"></el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="正确答案" v-else>
|
||||||
|
<el-radio-group v-model="Form.right">
|
||||||
|
<el-radio label="A"></el-radio>
|
||||||
|
<el-radio label="B"></el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="解析">
|
||||||
|
<el-input v-model="Form.resolution" style="width:600px" type="textarea" :rows="3"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="真题年份" prop="year">
|
||||||
|
<el-input v-model="Form.year"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm('Form')" :loading="submitLoding">保存</el-button>
|
||||||
|
<el-button type="warning" @click="goBack()">返回</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { getQuestionDetail,updateQuestion,getQuestioncatList } from "@/api/exam";
|
||||||
|
import { upUrl } from "@/api/file";
|
||||||
|
import { getToken } from "@/utils/auth";
|
||||||
|
export default {
|
||||||
|
components:{ },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
upHeaders: { Authorization: "JWT " + getToken() },
|
||||||
|
upUrl: upUrl(),
|
||||||
|
Form: {
|
||||||
|
id:null,
|
||||||
|
name: "",
|
||||||
|
img: null,
|
||||||
|
type:"",
|
||||||
|
questioncat:null,
|
||||||
|
year:null,
|
||||||
|
right:'',
|
||||||
|
options:{
|
||||||
|
A:'',
|
||||||
|
B:''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
catOptions:[],
|
||||||
|
inputDisable: false,
|
||||||
|
submitLoding:false,
|
||||||
|
rules: {
|
||||||
|
type: [
|
||||||
|
{ required: true, message: "请选择", trigger: "blur" }
|
||||||
|
],
|
||||||
|
name: [
|
||||||
|
{ required: true, message: "请输入", trigger: "blur" }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
typeOptions: [
|
||||||
|
{ key: 1, label: "单选", value: "单选" },
|
||||||
|
{ key: 2, label: "多选", value: "多选"},
|
||||||
|
{ key: 3, label: "判断", value: "判断" }
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch:{
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.Form.id = this.$route.query.id //接收参数
|
||||||
|
this.getQuestion();
|
||||||
|
this.getQuestioncatAll()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleImgSuccess(res, file) {
|
||||||
|
this.Form.img = res.data.path
|
||||||
|
},
|
||||||
|
beforeImgUpload(file) {
|
||||||
|
const isLt2M = file.size / 1024 / 1024 < 0.6;
|
||||||
|
if (!isLt2M) {
|
||||||
|
this.$message.error("上传图片大小不能超过 600KB!");
|
||||||
|
}
|
||||||
|
return isLt2M;
|
||||||
|
},
|
||||||
|
submitForm(formName) {
|
||||||
|
this.$refs[formName].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.submitLoding = true
|
||||||
|
if(this.Form.questioncat instanceof Array){
|
||||||
|
this.Form.questioncat = this.Form.questioncat.pop()
|
||||||
|
}
|
||||||
|
for(let key in this.Form.options){
|
||||||
|
if(!this.Form.options[key]){
|
||||||
|
delete this.Form.options[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateQuestion(this.Form.id, this.Form).then(response => {
|
||||||
|
this.submitLoding = false
|
||||||
|
if(response.code >= 200){
|
||||||
|
this.$message({
|
||||||
|
type: "success",
|
||||||
|
message: "修改成功!"
|
||||||
|
});
|
||||||
|
this.goBack()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getQuestion() {
|
||||||
|
getQuestionDetail(this.Form.id).then(response => {
|
||||||
|
this.Form = response.data ;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.$router.go(-1)
|
||||||
|
},
|
||||||
|
delImg() {
|
||||||
|
this.Form.img = null
|
||||||
|
},
|
||||||
|
getQuestioncatAll() {
|
||||||
|
getQuestioncatList().then(response => {
|
||||||
|
this.catOptions = response.data.results;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<el-input
|
||||||
|
v-model="listQuery.search"
|
||||||
|
placeholder="名称"
|
||||||
|
style="width: 200px;"
|
||||||
|
class="filter-item"
|
||||||
|
@keyup.enter.native="handleSearch"
|
||||||
|
/>
|
||||||
|
<el-button class="filter-item" type="primary" icon="el-icon-search" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button
|
||||||
|
class="filter-item"
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-refresh-left"
|
||||||
|
@click="resetFilter"
|
||||||
|
>刷新重置</el-button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<el-button type="primary" @click="handleAdd" icon="el-icon-plus">新增</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="tableData.results"
|
||||||
|
style="width: 100%;margin-top:10px;"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
fit
|
||||||
|
v-loading="listLoading"
|
||||||
|
highlight-current-row
|
||||||
|
max-height="600"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" width="50"></el-table-column>
|
||||||
|
<el-table-column align="left" label="名称">
|
||||||
|
<template slot-scope="scope">{{ scope.row.name }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<!-- <el-table-column align="left" label="工作类别">
|
||||||
|
<template slot-scope="scope">{{ scope.row.workscope_name }}</template>
|
||||||
|
</el-table-column> -->
|
||||||
|
<el-table-column align="left" label="总分">
|
||||||
|
<template slot-scope="scope">{{ scope.row.total_score }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="通过分">
|
||||||
|
<template slot-scope="scope">{{ scope.row.pass_score }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="left" label="限时(分钟)">
|
||||||
|
<template slot-scope="scope">{{ scope.row.limit }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column align="center" label="操作">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleEdit(scope)"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
:disabled="!checkPermission(['paper_update'])"
|
||||||
|
></el-button>
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
@click="handleClone(scope)"
|
||||||
|
:disabled="!checkPermission(['paper_clone'])"
|
||||||
|
>克隆</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(scope)"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
:disabled="!checkPermission(['paper_delete'])"
|
||||||
|
></el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination
|
||||||
|
v-show="tableData.count>0"
|
||||||
|
:total="tableData.count"
|
||||||
|
:page.sync="listQuery.page"
|
||||||
|
:limit.sync="listQuery.limit"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getPaperList, deletePaper, clonePaper } from "@/api/exam";
|
||||||
|
import checkPermission from "@/utils/permission";
|
||||||
|
import Pagination from "@/components/Pagination";
|
||||||
|
|
||||||
|
const listQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
search:''
|
||||||
|
};
|
||||||
|
export default {
|
||||||
|
components: { Pagination },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
listQuery: Object.assign({}, listQuery),
|
||||||
|
tableData: {
|
||||||
|
count:0,
|
||||||
|
results:[],
|
||||||
|
},
|
||||||
|
listLoading: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
created() {
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkPermission,
|
||||||
|
getList(query = this.listQuery) {
|
||||||
|
this.listLoading = true;
|
||||||
|
getPaperList(query).then(response => {
|
||||||
|
if(response.data.results){
|
||||||
|
this.tableData = response.data;
|
||||||
|
}
|
||||||
|
this.listLoading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetFilter() {
|
||||||
|
this.listQuery = {
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
search:''
|
||||||
|
};
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.listQuery.page = 1;
|
||||||
|
this.getList();
|
||||||
|
},
|
||||||
|
handleAdd() {
|
||||||
|
this.$router.push({path:"/exam/paperCreate"})
|
||||||
|
},
|
||||||
|
handleEdit(scope) {
|
||||||
|
this.$router.push({path:"/exam/paperUpdate",query:{id:scope.row.id}})
|
||||||
|
},
|
||||||
|
handleClone(scope) {
|
||||||
|
const loading = this.$loading({
|
||||||
|
text: '克隆中..',
|
||||||
|
});
|
||||||
|
clonePaper(scope.row.id).then(res=>{
|
||||||
|
this.getList()
|
||||||
|
loading.close()
|
||||||
|
}).catch(e=>{loading.close()})
|
||||||
|
},
|
||||||
|
handleDelete(scope) {
|
||||||
|
this.$confirm('确认删除?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
deletePaper(scope.row.id).then(response => {
|
||||||
|
this.$message({
|
||||||
|
type: 'success',
|
||||||
|
message: '删除成功!'
|
||||||
|
});
|
||||||
|
this.getList()
|
||||||
|
});
|
||||||
|
|
||||||
|
}).catch(() => {
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="8">
|
||||||
|
<h3>基本信息</h3>
|
||||||
|
<el-form :model="Form" :rules="rules" ref="Form" label-width="100px" status-icon>
|
||||||
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model="Form.name" style="width:80%"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="时间限制" prop="limit">
|
||||||
|
<el-input-number v-model="Form.limit" :min="0"></el-input-number>分钟
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="试卷信息">
|
||||||
|
<div>
|
||||||
|
单选题
|
||||||
|
<span style="color:darkred;font-weight:bold">{{Form.danxuan_count}} 道</span>
|
||||||
|
每道
|
||||||
|
<el-input-number v-model="Form.danxuan_score" :min="0" @change="calScore"></el-input-number>分
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
多选题
|
||||||
|
<span style="color:darkred;font-weight:bold">{{Form.duoxuan_count}} 道</span>
|
||||||
|
每道
|
||||||
|
<el-input-number v-model="Form.duoxuan_score" :min="0" @change="calScore"></el-input-number>分
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
判断题
|
||||||
|
<span style="color:darkred;font-weight:bold">{{Form.panduan_count}} 道</span>
|
||||||
|
每道
|
||||||
|
<el-input-number v-model="Form.panduan_score" :min="0" @change="calScore"></el-input-number>分
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
总分
|
||||||
|
<span style="color:darkred;font-weight:bold">{{Form.total_score}}</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="及格分数" prop="pass_score">
|
||||||
|
<el-input-number v-model="Form.pass_score" :min="0"></el-input-number>分
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm('Form')" :loading="submitLoding">保存</el-button>
|
||||||
|
<el-button type="warning" @click="goBack()">返回</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<h3>选题信息</h3>
|
||||||
|
<el-button type="primary" @click="handleChoose" icon="el-icon-plus">选择试题</el-button>
|
||||||
|
<div v-for="(item, index) in questions">
|
||||||
|
<h4>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(index)"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
></el-button>
|
||||||
|
{{ index+1 }} -
|
||||||
|
<el-tag>{{item.type}}</el-tag>
|
||||||
|
{{ item.name }}
|
||||||
|
<span>(正确答案:{{item.right}})</span>
|
||||||
|
</h4>
|
||||||
|
<div v-for="(value, name) in item.options">{{ name }}: {{ value }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<Questionchoose v-bind:chooseVisible="chooseVisible" @closeDg="closeDg" @choseQ="choseQ"></Questionchoose>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { getQuestioncatList, createPaper } from "@/api/exam";
|
||||||
|
import Questionchoose from "@/views/exam/questionChoose";
|
||||||
|
export default {
|
||||||
|
components: { Questionchoose },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
questions: [],
|
||||||
|
Form: {
|
||||||
|
name: "",
|
||||||
|
// workscope: null,
|
||||||
|
limit: 60,
|
||||||
|
total_score: 0,
|
||||||
|
pass_score: 60,
|
||||||
|
questions_: [],
|
||||||
|
danxuan_score: 2,
|
||||||
|
danxuan_count: 0,
|
||||||
|
duoxuan_score: 4,
|
||||||
|
duoxuan_count: 0,
|
||||||
|
panduan_score: 2,
|
||||||
|
panduan_count: 0
|
||||||
|
},
|
||||||
|
submitLoding: false,
|
||||||
|
rules: {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: "名称不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
limit: [
|
||||||
|
{ required: true, message: "时间限制不能为空" },
|
||||||
|
{ type: "number", message: "时间限制必须是数字" }
|
||||||
|
],
|
||||||
|
pass_score: [
|
||||||
|
{ required: true, message: "及格分数不能为空" },
|
||||||
|
{ type: "number", message: "及格分数必须是数字" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
workscopeData: [],
|
||||||
|
chooseVisible: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
questions: "calScore"
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getQuestioncat();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getQuestioncat() {
|
||||||
|
getQuestioncatList().then(response => {
|
||||||
|
this.workscopeData = response.data.results;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitForm(formName) {
|
||||||
|
this.$refs[formName].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.submitLoding = true;
|
||||||
|
createPaper(this.Form).then(response => {
|
||||||
|
this.submitLoding = false;
|
||||||
|
this.$message({
|
||||||
|
type: "success",
|
||||||
|
message: "编辑成功!"
|
||||||
|
});
|
||||||
|
this.goBack();
|
||||||
|
}).catch(res=>{
|
||||||
|
this.submitLoding = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetForm(formName) {
|
||||||
|
this.$refs[formName].resetFields();
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.$router.replace("/exam/testpaper/");
|
||||||
|
},
|
||||||
|
handleChoose() {
|
||||||
|
this.chooseVisible = true;
|
||||||
|
},
|
||||||
|
closeDg(val) {
|
||||||
|
this.chooseVisible = val;
|
||||||
|
},
|
||||||
|
choseQ(val) {
|
||||||
|
this.questions = this.questions.concat(val);
|
||||||
|
},
|
||||||
|
handleDelete(val) {
|
||||||
|
this.questions.splice(val, 1);
|
||||||
|
},
|
||||||
|
calScore() {
|
||||||
|
let danxuan_count = 0,
|
||||||
|
duoxuan_count = 0,
|
||||||
|
panduan_count = 0,
|
||||||
|
questions = [];
|
||||||
|
for (var i = 0, len = this.questions.length; i < len; i++) {
|
||||||
|
var total_score = 0
|
||||||
|
switch (this.questions[i].type) {
|
||||||
|
case "单选":
|
||||||
|
danxuan_count = danxuan_count + 1;
|
||||||
|
total_score = this.Form.danxuan_score
|
||||||
|
break;
|
||||||
|
case "多选":
|
||||||
|
duoxuan_count = duoxuan_count + 1;
|
||||||
|
total_score = this.Form.duoxuan_score
|
||||||
|
break;
|
||||||
|
case "判断":
|
||||||
|
panduan_count = panduan_count + 1;
|
||||||
|
total_score = this.Form.panduan_score
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
questions.push({question:this.questions[i].id,total_score:total_score})
|
||||||
|
}
|
||||||
|
this.Form.danxuan_count = danxuan_count;
|
||||||
|
this.Form.duoxuan_count = duoxuan_count;
|
||||||
|
this.Form.panduan_count = panduan_count;
|
||||||
|
let form = this.Form;
|
||||||
|
let score = form.danxuan_count * form.danxuan_score + form.duoxuan_count * form.duoxuan_score + form.panduan_count * form.panduan_score;
|
||||||
|
this.Form.total_score = score;
|
||||||
|
this.Form.questions_ = questions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="8">
|
||||||
|
<h3>基本信息</h3>
|
||||||
|
<el-form :model="Form" :rules="rules" ref="Form" label-width="100px" status-icon>
|
||||||
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model="Form.name" style="width:80%"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="时间限制" prop="limit">
|
||||||
|
<el-input-number v-model="Form.limit" :min="0"></el-input-number>分钟
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="试卷信息">
|
||||||
|
<div>
|
||||||
|
单选题
|
||||||
|
<span style="color:darkred;font-weight:bold">{{Form.danxuan_count}} 道</span>
|
||||||
|
每道
|
||||||
|
<el-input-number v-model="Form.danxuan_score" :min="0"></el-input-number>分
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
多选题
|
||||||
|
<span style="color:darkred;font-weight:bold">{{Form.duoxuan_count}} 道</span>
|
||||||
|
每道
|
||||||
|
<el-input-number v-model="Form.duoxuan_score" :min="0"></el-input-number>分
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
判断题
|
||||||
|
<span style="color:darkred;font-weight:bold">{{Form.panduan_count}} 道</span>
|
||||||
|
每道
|
||||||
|
<el-input-number v-model="Form.panduan_score" :min="0"></el-input-number>分
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
总分
|
||||||
|
<span style="color:darkred;font-weight:bold">{{Form.total_score}}</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="及格分数" prop="pass_score">
|
||||||
|
<el-input-number v-model="Form.pass_score" :min="0"></el-input-number>分
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitForm('Form')" :loading="submitLoding">保存</el-button>
|
||||||
|
<el-button type="warning" @click="goBack()">返回</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<h3>选题信息</h3>
|
||||||
|
<el-button type="primary" @click="handleChoose" icon="el-icon-plus">选择试题</el-button>
|
||||||
|
<div v-for="(item, index) in questions">
|
||||||
|
<h4>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDelete(index)"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
></el-button>
|
||||||
|
{{ index+1 }} -
|
||||||
|
<el-tag>{{item.type}}</el-tag>
|
||||||
|
{{ item.name }}
|
||||||
|
<span>(正确答案:{{item.right}})</span>
|
||||||
|
</h4>
|
||||||
|
<div v-for="(value, name) in item.options">{{ name }}: {{ value }}</div>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<Questionchoose v-bind:chooseVisible="chooseVisible" @closeDg="closeDg" @choseQ="choseQ"></Questionchoose>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { getQuestioncatList, updatePaper,getPaperDetail } from "@/api/exam";
|
||||||
|
import { genTree } from "@/utils";
|
||||||
|
import Questionchoose from "@/views/exam/questionChoose";
|
||||||
|
export default {
|
||||||
|
components: { Questionchoose },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
questions: [],
|
||||||
|
Form: {
|
||||||
|
name: "",
|
||||||
|
limit: 60,
|
||||||
|
total_score: 0,
|
||||||
|
pass_score: 60,
|
||||||
|
questions_: [],
|
||||||
|
danxuan_score: 2,
|
||||||
|
danxuan_count: 0,
|
||||||
|
duoxuan_score: 4,
|
||||||
|
duoxuan_count: 0,
|
||||||
|
panduan_score: 2,
|
||||||
|
panduan_count: 0
|
||||||
|
},
|
||||||
|
submitLoding: false,
|
||||||
|
rules: {
|
||||||
|
name: [
|
||||||
|
{ required: true, message: "名称不能为空", trigger: "blur" }
|
||||||
|
// { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
limit: [
|
||||||
|
{ required: true, message: "时间限制不能为空" },
|
||||||
|
{ type: "number", message: "时间限制必须是数字" }
|
||||||
|
],
|
||||||
|
pass_score: [
|
||||||
|
{ required: true, message: "及格分数不能为空" },
|
||||||
|
{ type: "number", message: "及格分数必须是数字" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
workscopeData: [],
|
||||||
|
chooseVisible: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
questions: "calScore"
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getQuestioncat();
|
||||||
|
this.Form.id = this.$route.query.id //接收参数
|
||||||
|
this.getPaperDetail();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getQuestioncat() {
|
||||||
|
getQuestioncatList().then(response => {
|
||||||
|
this.workscopeData = genTree(response.data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getPaperDetail() {
|
||||||
|
let that = this;
|
||||||
|
getPaperDetail(this.Form.id).then(response => {
|
||||||
|
that.Form = response.data;
|
||||||
|
debugger;
|
||||||
|
that.questions = response.data.questions_;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitForm(formName) {
|
||||||
|
this.$refs[formName].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.submitLoding = true;
|
||||||
|
updatePaper(this.Form.id,this.Form).then(response => {
|
||||||
|
this.submitLoding = false;
|
||||||
|
this.$message({
|
||||||
|
type: "success",
|
||||||
|
message: "编辑成功!"
|
||||||
|
});
|
||||||
|
this.goBack();
|
||||||
|
}).catch(res=>{
|
||||||
|
this.submitLoding = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetForm(formName) {
|
||||||
|
this.$refs[formName].resetFields();
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.$router.replace("/exam/testpaper/");
|
||||||
|
},
|
||||||
|
handleChoose() {
|
||||||
|
this.chooseVisible = true;
|
||||||
|
},
|
||||||
|
closeDg(val) {
|
||||||
|
this.chooseVisible = val;
|
||||||
|
},
|
||||||
|
choseQ(val) {
|
||||||
|
this.questions = this.questions.concat(val);
|
||||||
|
},
|
||||||
|
handleDelete(val) {
|
||||||
|
this.questions.splice(val, 1);
|
||||||
|
},
|
||||||
|
calScore() {
|
||||||
|
let that = this;
|
||||||
|
let danxuan_count = 0,
|
||||||
|
duoxuan_count = 0,
|
||||||
|
panduan_count = 0,
|
||||||
|
questions = [];
|
||||||
|
for (var i = 0, len = that.questions.length; i < len; i++) {
|
||||||
|
var total_score = 0
|
||||||
|
switch (that.questions[i].type) {
|
||||||
|
case "单选":
|
||||||
|
danxuan_count = danxuan_count + 1;
|
||||||
|
total_score = that.Form.danxuan_score;
|
||||||
|
break;
|
||||||
|
case "多选":
|
||||||
|
duoxuan_count = duoxuan_count + 1;
|
||||||
|
total_score = that.Form.duoxuan_score;
|
||||||
|
break;
|
||||||
|
case "判断":
|
||||||
|
panduan_count = panduan_count + 1;
|
||||||
|
total_score = that.Form.panduan_score;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
questions.push({question:that.questions[i].id,total_score:total_score})
|
||||||
|
}
|
||||||
|
that.Form.danxuan_count = danxuan_count;
|
||||||
|
that.Form.duoxuan_count = duoxuan_count;
|
||||||
|
that.Form.panduan_count = panduan_count;
|
||||||
|
let form = that.Form;
|
||||||
|
let score = form.danxuan_count * form.danxuan_score + form.duoxuan_count * form.duoxuan_score + form.panduan_count * form.panduan_score;
|
||||||
|
that.Form.total_score = score;
|
||||||
|
that.Form.questions_ = questions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header style="height: 80px;padding: 0">
|
<el-header style="height: 80px;padding: 0;position: absolute;">
|
||||||
<el-row class="biaotou">
|
<el-row class="biaotou">
|
||||||
<el-col :span="20" style="text-align:center;color:seashell;font-size:32px;line-height: 70px;">
|
<el-col :span="20" style="text-align:center;color:seashell;font-size:32px;line-height: 70px;">
|
||||||
{{ video.name }}
|
{{ video.name }}
|
||||||
|
|
@ -8,8 +8,10 @@
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main style="margin-top: 70px;">
|
<el-main style="margin-top: 70px;">
|
||||||
<div style="margin:20px 12%;">
|
<el-row>
|
||||||
<div class="content" @click="clicknub">
|
<el-col :sm="24" :lg="15" :xl="13">
|
||||||
|
<div style="margin:20px;">
|
||||||
|
<div class="content">
|
||||||
<video
|
<video
|
||||||
:id="tcPlayerId"
|
:id="tcPlayerId"
|
||||||
width="1000"
|
width="1000"
|
||||||
|
|
@ -26,8 +28,8 @@
|
||||||
<el-col class="firstLineDetail">
|
<el-col class="firstLineDetail">
|
||||||
<div class="firstLineText">{{ video.name }}</div>
|
<div class="firstLineText">{{ video.name }}</div>
|
||||||
<div>
|
<div>
|
||||||
<el-button class="firstLineBtn" type="error" icon="el-icon-view">{{video.views}}</el-button>
|
<el-button class="firstLineBtn" type="error" icon="el-icon-view">{{video.views_n}}</el-button>
|
||||||
<el-button class="firstLineBtn" type="error" icon="el-icon-s-custom">{{video.viewsp}}</el-button>
|
<el-button class="firstLineBtn" type="error" icon="el-icon-s-custom">{{video.viewsp_n}}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<div style="font-size: 15px">
|
<div style="font-size: 15px">
|
||||||
|
|
@ -35,11 +37,27 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :sm="24" :lg="9" :xl="11">
|
||||||
|
<div style="margin:20px;height:calc(100% - 140px);">
|
||||||
|
<p class="firstLineText">观看记录:</p>
|
||||||
|
<div class="viewRecordList">
|
||||||
|
<div class="viewRecordItemWrap" v-for="item in recordList" :key="item.id"
|
||||||
|
@click="recordItemPlay(item.video)">
|
||||||
|
<div class="recordName" v-if="item.video_"><span>{{item.video_.name}}</span></div>
|
||||||
|
<div class="viewInfo">上次观看时间:<span class="viewInfo_tiem">{{item.update_time}}</span></div>
|
||||||
|
<div class="viewInfo">上次观看进度:<span class="viewInfo_current">{{item.current}}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div></el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import {getVideo, getMyView, getVideoPlayCode} from "@/api/video";
|
import {getVideo,videoStart,myViewItem, myView,getMyView,refreshViewItem, getVideoPlayCode,viewItemComplete} from "@/api/video";
|
||||||
export default {
|
export default {
|
||||||
name: 'TencentPlayer',
|
name: 'TencentPlayer',
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -60,57 +78,65 @@
|
||||||
video: {id: 0},
|
video: {id: 0},
|
||||||
description: '',
|
description: '',
|
||||||
name: '',
|
name: '',
|
||||||
videoFileid:false,
|
userName:"",
|
||||||
userName:""
|
playTimer:null,
|
||||||
|
isFirstView:true,
|
||||||
|
id:'',
|
||||||
|
videoViId:'',
|
||||||
|
recordList:[],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted(){
|
mounted(){
|
||||||
//debugger;
|
|
||||||
this.userName = this.$store.state.user.name;
|
this.userName = this.$store.state.user.name;
|
||||||
if(this.player!==null){
|
if(this.player!==null){
|
||||||
this.player.null;
|
this.player.null;
|
||||||
}
|
}
|
||||||
if(this.$route.params.id){
|
if(this.$route.query.id){
|
||||||
this.id = this.$route.params.id;
|
this.id = this.$route.query.id;
|
||||||
this.videoFileId = this.$route.params.fileid;
|
|
||||||
let videoId = sessionStorage.getItem('videoId');
|
let videoId = sessionStorage.getItem('videoId');
|
||||||
let videoFileId = sessionStorage.getItem('videoFileId');
|
if(videoId!==undefined&&videoId!==null&&videoId!==''){
|
||||||
if(videoId){
|
|
||||||
sessionStorage.removeItem('videoId');
|
sessionStorage.removeItem('videoId');
|
||||||
sessionStorage.setItem('videoId',this.$route.params.id);
|
sessionStorage.setItem('videoId',this.$route.params.id);
|
||||||
}else{
|
}else{
|
||||||
sessionStorage.setItem('videoId',this.$route.params.id);
|
sessionStorage.setItem('videoId',this.$route.params.id);
|
||||||
}
|
}
|
||||||
if(videoFileId){
|
|
||||||
sessionStorage.removeItem('videoFileId');
|
|
||||||
sessionStorage.setItem('videoFileId',this.$route.params.fileid);
|
|
||||||
}else{
|
|
||||||
sessionStorage.setItem('videoFileId',this.$route.params.fileid);
|
|
||||||
}
|
|
||||||
}else{
|
}else{
|
||||||
this.id = sessionStorage.getItem('videoId');
|
this.id = sessionStorage.getItem('videoId');
|
||||||
this.videoFileId = sessionStorage.getItem('videoFileId');
|
|
||||||
}
|
}
|
||||||
this.getPlayCode(this.videoFileId);
|
|
||||||
this.getVideo();
|
this.getVideo();
|
||||||
|
this.clicknub();
|
||||||
|
this.getMyVideoView();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
//获取视频详情
|
||||||
getVideo() {
|
getVideo() {
|
||||||
getVideo(this.id).then((response) => {
|
let that = this;
|
||||||
|
getVideo(that.id).then((response) => {
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
this.video = response.data;
|
that.video = response.data;
|
||||||
|
that.videoFileId = response.data.fileid;
|
||||||
|
that.getPlayCode(response.data.fileid);
|
||||||
}
|
}
|
||||||
this.listLoading = false;
|
that.listLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
//获取饿哦的观看记录
|
||||||
|
getMyVideoView() {
|
||||||
|
let that = this;
|
||||||
|
myViewItem().then((response) => {
|
||||||
|
if (response.data) {
|
||||||
|
that.recordList = response.data.results;
|
||||||
|
// debugger;
|
||||||
|
console.log(that.recordList)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
//获取播放码
|
||||||
getPlayCode(id){
|
getPlayCode(id){
|
||||||
let that = this;
|
let that = this;
|
||||||
getVideoPlayCode(id).then(res=>{
|
getVideoPlayCode(id).then(res=>{
|
||||||
if(res.data){
|
if(res.data){
|
||||||
//debugger;
|
|
||||||
console.log(res.data);
|
|
||||||
that.videoFileid = true;
|
|
||||||
that.videoPsign = res.data.psign;
|
that.videoPsign = res.data.psign;
|
||||||
that.initVideo();
|
that.initVideo();
|
||||||
}
|
}
|
||||||
|
|
@ -118,49 +144,107 @@
|
||||||
},
|
},
|
||||||
//视频观看次数
|
//视频观看次数
|
||||||
clicknub() {
|
clicknub() {
|
||||||
debugger;
|
myView(this.id).then((response) => {
|
||||||
getMyView(this.id).then((response) => {});
|
// debugger;
|
||||||
|
console.log(response.data);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
//初始化视频播放器
|
||||||
initVideo() {
|
initVideo() {
|
||||||
//debugger;
|
|
||||||
console.log(this.userName);
|
|
||||||
let that = this;
|
let that = this;
|
||||||
let text = "用户:"+that.userName;
|
getMyView(that.id,{}).then((response) => {
|
||||||
const playerParm = {
|
// debugger;
|
||||||
fileID: that.videoFileId,
|
// console.log(response.data);
|
||||||
appID: that.videoAppId,
|
let current = response.data.current;
|
||||||
psign: that.videoPsign,
|
let playerParm = {
|
||||||
plugins:{
|
fileID: that.videoFileId,
|
||||||
DynamicWatermark: {
|
appID: that.videoAppId,
|
||||||
speed: 0.1,
|
psign: that.videoPsign,
|
||||||
content: "用户:"+that.userName
|
plugins:{
|
||||||
}
|
DynamicWatermark: {
|
||||||
}
|
speed: 0.1,
|
||||||
};
|
content: "用户:"+that.userName
|
||||||
/*
|
},
|
||||||
* plugins: {
|
|
||||||
ContinuePlay: { // 开启续播功能
|
|
||||||
// auto: true, //[可选] 是否在视频播放后自动续播
|
|
||||||
// text:'上次播放至 ', //[可选] 提示文案
|
|
||||||
// btnText: '恢复播放' //[可选] 按钮文案
|
|
||||||
},
|
},
|
||||||
|
controlBar:{
|
||||||
|
playbackRateMenuButton:false
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (!that.player) {
|
||||||
|
that.player = window.TCPlayer(that.tcPlayerId, playerParm);
|
||||||
|
} else {
|
||||||
|
that.player.loadVideoByID(playerParm);
|
||||||
|
that.player.loadVideoByID(that.tcPlayerId);
|
||||||
}
|
}
|
||||||
* */
|
that.player.currentTime(current);
|
||||||
//debugger;
|
//视频播放
|
||||||
if (!that.player) {
|
|
||||||
that.player = window.TCPlayer(that.tcPlayerId, playerParm);
|
|
||||||
that.player.on('play', function(error) {
|
that.player.on('play', function(error) {
|
||||||
// 做一些处理
|
// debugger;
|
||||||
getMyView(that.id).then((response) => {});
|
if(that.playTimer!==null){
|
||||||
|
clearInterval(that.playTimer);
|
||||||
|
that.playTimer = null;
|
||||||
|
}
|
||||||
|
if(that.isFirstView){
|
||||||
|
that.isFirstView = false;
|
||||||
|
videoStart(that.id).then(ews=>{
|
||||||
|
// debugger;
|
||||||
|
that.videoViId = ews.data.vi;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
that.playTimer = setInterval(function(){
|
||||||
|
let currentTimeNum = 0;
|
||||||
|
currentTimeNum = that.player.currentTime();
|
||||||
|
refreshViewItem(that.videoViId,{current:currentTimeNum,seconds:10})
|
||||||
|
.then((response) => {})
|
||||||
|
.catch(response=>{
|
||||||
|
debugger;
|
||||||
|
clearInterval(that.playTimer);
|
||||||
|
that.playTimer = null;
|
||||||
|
that.player.pause();
|
||||||
|
});
|
||||||
|
},10000)
|
||||||
});
|
});
|
||||||
} else {
|
//视频暂停
|
||||||
that.player.loadVideoByID(playerParm);
|
that.player.on('pause', function(error) {
|
||||||
that.player.loadVideoByID(that.tcPlayerId);
|
let currentTimeNum = 0;
|
||||||
}
|
currentTimeNum = that.player.currentTime();
|
||||||
|
refreshViewItem(that.videoViId,{current:currentTimeNum,seconds:0}).then((response) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
clearInterval(that.playTimer);
|
||||||
|
that.playTimer = null;
|
||||||
|
});
|
||||||
|
//视频观看中
|
||||||
|
that.player.on('playing', function(error) {
|
||||||
|
|
||||||
|
});
|
||||||
|
//视频播放已结束时触发,此时 currentTime 值等于媒体资源最大值。
|
||||||
|
that.player.on('ended', function(error) {
|
||||||
|
viewItemComplete(that.videoViId).then(res=>{
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
recordItemPlay(id){
|
||||||
|
debugger;
|
||||||
|
let routeData = this.$router.resolve({
|
||||||
|
path: "/test/index",
|
||||||
|
query: {
|
||||||
|
id: id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//必要操作,否则不会打开新页面
|
||||||
|
window.open(routeData.href, '_blank');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
this.player.dispose()
|
let that = this;
|
||||||
|
if(that.playTimer!==null){
|
||||||
|
clearInterval(that.playTimer);
|
||||||
|
that.playTimer = null;
|
||||||
|
}
|
||||||
|
that.player.dispose()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -173,7 +257,7 @@
|
||||||
color: #333;
|
color: #333;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
position: fixed;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 1010;
|
z-index: 1010;
|
||||||
}
|
}
|
||||||
|
|
@ -224,4 +308,30 @@
|
||||||
border: 1px solid #e74e4e;
|
border: 1px solid #e74e4e;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
|
.viewRecordList{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.viewRecordItemWrap{
|
||||||
|
margin: 10px 0;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid #dddddd;
|
||||||
|
}
|
||||||
|
.recordName{
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.viewInfo{
|
||||||
|
font-size: 14px;
|
||||||
|
color: #a2a2a2;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
.viewInfo_tiem{
|
||||||
|
color:#6090e6
|
||||||
|
}
|
||||||
|
.viewInfo_current{
|
||||||
|
color:#86c793
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,610 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-card>
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dataValue"
|
||||||
|
type="daterange"
|
||||||
|
align="right"
|
||||||
|
unlink-panels
|
||||||
|
range-separator="至"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:picker-options="pickerOptions"
|
||||||
|
value-format="yyyy-MM-dd HH:mm:ss"
|
||||||
|
:default-time="['00:00:00', '23:59:59']">
|
||||||
|
</el-date-picker>
|
||||||
|
<el-button type="primary" icon="el-icon-search" @click="handleFilter">搜索</el-button>
|
||||||
|
<div style="margin-top:10px"></div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
<el-card style="margin-top:10px">
|
||||||
|
<div id="main" style="width:1000px;height:600px;margin-top: 50px;"></div>
|
||||||
|
<div id="userMain" style="width:1000px;height:600px;margin-top: 50px;"></div>
|
||||||
|
<div id="orgMain" style="width:1000px;height:600px;margin-top: 50px;"></div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import FileSaver from "file-saver";
|
||||||
|
import XLSX from "xlsx";
|
||||||
|
import { groupByCategoryView,groupByUserView,groupByOrgView } from '@/api/video'
|
||||||
|
export default{
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
cateChart:null,
|
||||||
|
userChart:null,
|
||||||
|
orgChart:null,
|
||||||
|
dataValue:'',
|
||||||
|
dataQuery:{
|
||||||
|
start_time:'',
|
||||||
|
end_time:'',
|
||||||
|
limit:500
|
||||||
|
},
|
||||||
|
pickerOptions: {
|
||||||
|
shortcuts: [{
|
||||||
|
text: '最近一天',
|
||||||
|
onClick(picker) {
|
||||||
|
let end = new Date();
|
||||||
|
let start = new Date();
|
||||||
|
start.setTime(start.getTime() - 3600 * 1000 * 24);
|
||||||
|
picker.$emit('pick', [start, end]);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: '最近一周',
|
||||||
|
onClick(picker) {
|
||||||
|
let end = new Date();
|
||||||
|
let start = new Date();
|
||||||
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
|
||||||
|
picker.$emit('pick', [start, end]);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: '最近一个月',
|
||||||
|
onClick(picker) {
|
||||||
|
let end = new Date();
|
||||||
|
let start = new Date();
|
||||||
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
|
||||||
|
picker.$emit('pick', [start, end]);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
text: '最近三个月',
|
||||||
|
onClick(picker) {
|
||||||
|
let end = new Date();
|
||||||
|
let start = new Date();
|
||||||
|
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
|
||||||
|
picker.$emit('pick', [start, end]);
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted(){
|
||||||
|
let dateNow = new Date();
|
||||||
|
let yeear = dateNow.getFullYear();
|
||||||
|
let month = dateNow.getMonth()+1;
|
||||||
|
let day = dateNow.getDate();
|
||||||
|
this.dataQuery.start_time=yeear+'-'+month+'-'+'01 00:00:00';
|
||||||
|
this.dataQuery.end_time=yeear+'-'+month+'-'+day+' 23:59:59';
|
||||||
|
this.dataValue = [this.dataQuery.start_time,this.dataQuery.end_time];
|
||||||
|
this.getCategoryView();
|
||||||
|
this.getUserView();
|
||||||
|
this.getGroupView();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCategoryView(){
|
||||||
|
groupByCategoryView(this.dataQuery).then(res=>{
|
||||||
|
let data = res.data;
|
||||||
|
let xAxisOptions = [],data1 = [],data2 = [],data3 = [];
|
||||||
|
data.forEach(item=>{
|
||||||
|
xAxisOptions.push(item.视频大类);
|
||||||
|
data1.push(item.视频数量);
|
||||||
|
data2.push(item.观看总次数);
|
||||||
|
data3.push(item.观看总人数);
|
||||||
|
})
|
||||||
|
let chartDom = document.getElementById('main');
|
||||||
|
this.cateChart = echarts.init(chartDom);
|
||||||
|
let labelOption = {
|
||||||
|
show: true,
|
||||||
|
position: 'insideBottom',
|
||||||
|
distance: 5,
|
||||||
|
align: 'left',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
rotate: 90,
|
||||||
|
formatter: '{c}',
|
||||||
|
fontSize: 12,
|
||||||
|
rich: {
|
||||||
|
name: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let option = {
|
||||||
|
title: { text: '视频大类观看统计' },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['视频数量', '观看总次数', '观看总人数']
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
show: true,
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'right',
|
||||||
|
top: 'center',
|
||||||
|
feature: {
|
||||||
|
mark: { show: true },
|
||||||
|
dataView: {
|
||||||
|
show: true,
|
||||||
|
title: '数据视图',
|
||||||
|
lang: ['视频大类观看统计', '关闭', '导出Excel'], // 按钮
|
||||||
|
contentToOption: function (opts) {
|
||||||
|
$('#tableExcel').table2excel({
|
||||||
|
exclude: '.noExl', //过滤位置的 css 类名, 有class = “noExl” 的行不被导出
|
||||||
|
filename: '最大需量', // 文件名称
|
||||||
|
name: 'Excel Document Name.xls',
|
||||||
|
exclude_img: true,
|
||||||
|
exclude_links: true,
|
||||||
|
exclude_inputs: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
optionToContent: function (opt) {
|
||||||
|
var axisData = opt.xAxis[0].data //坐标轴
|
||||||
|
var series = opt.series //折线图的数据
|
||||||
|
console.log(series);
|
||||||
|
var tdHeads =
|
||||||
|
'<td style="margin-top:10px; padding: 0 15px">视频大类</td>' //表头
|
||||||
|
var tdBodys = ''
|
||||||
|
series.forEach(function (item) {
|
||||||
|
tdHeads += `<td style="padding:5px 15px">${item.name}</td>`
|
||||||
|
})
|
||||||
|
var table = `<table id='table-content' border="1" style="margin-left:20px;border-collapse:collapse;font-size:14px;text-align:center;"><tbody><tr>${tdHeads} </tr>`
|
||||||
|
for (var i = 0, l = axisData.length; i < l; i++) {
|
||||||
|
for (var j = 0; j < series.length; j++) {
|
||||||
|
if (series[j].data[i] == undefined) {
|
||||||
|
tdBodys += `<td>${'-'}</td>`
|
||||||
|
} else {
|
||||||
|
tdBodys += `<td>${series[j].data[i]}</td>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table += `<tr><td style="padding: 0 15px">${axisData[i]}</td>${tdBodys}</tr>`
|
||||||
|
tdBodys = ''
|
||||||
|
}
|
||||||
|
table += '</tbody></table>'
|
||||||
|
return table
|
||||||
|
},
|
||||||
|
contentToOption: function (HTMLDomElement, opt) {
|
||||||
|
let et = XLSX.utils.table_to_book(
|
||||||
|
document.getElementById("table-content")
|
||||||
|
); //此处传入table的DOM节点
|
||||||
|
let etout = XLSX.write(et, {
|
||||||
|
bookType: "xlsx",
|
||||||
|
bookSST: true,
|
||||||
|
type: "array",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
FileSaver.saveAs(
|
||||||
|
new Blob([etout], {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
}),
|
||||||
|
"统计数据.xlsx"
|
||||||
|
); //trade-publish.xlsx 为导出的文件名
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
return etout;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
magicType: { show: true, type: ['line', 'stack'] },
|
||||||
|
restore: { show: true },
|
||||||
|
saveAsImage: { show: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
axisTick: { show: false },
|
||||||
|
data: xAxisOptions,
|
||||||
|
axisLabel: {
|
||||||
|
interval:0,
|
||||||
|
rotate:60//角度顺时针计算的
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '视频数量',
|
||||||
|
type: 'bar',
|
||||||
|
barGap: 0,
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '观看总次数',
|
||||||
|
type: 'bar',
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '观看总人数',
|
||||||
|
type: 'bar',
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
this.cateChart.setOption(option);
|
||||||
|
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getGroupView(){
|
||||||
|
groupByOrgView(this.dataQuery).then(res=>{
|
||||||
|
debugger;console.log(res.data)
|
||||||
|
let data = res.data;
|
||||||
|
let xAxisOptions = [],data1 = [],data2 = [],data3 = [],data4=[];
|
||||||
|
data.forEach(item=>{
|
||||||
|
xAxisOptions.push(item.单位名称);
|
||||||
|
data1.push(item.观看完成视频总数);
|
||||||
|
data2.push(item.观看视频总数);
|
||||||
|
data3.push(item.观看总次数);
|
||||||
|
data4.push(item.观看总时间);
|
||||||
|
})
|
||||||
|
let chartDom = document.getElementById('orgMain');
|
||||||
|
this.orgChart = echarts.init(chartDom);
|
||||||
|
let labelOption = {
|
||||||
|
show: true,
|
||||||
|
position: 'insideBottom',
|
||||||
|
distance: 5,
|
||||||
|
align: 'left',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
rotate: 90,
|
||||||
|
formatter: '{c}',
|
||||||
|
fontSize: 12,
|
||||||
|
rich: {
|
||||||
|
name: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let option = {
|
||||||
|
title: { text: '单位观看量统计' },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['观看完成视频总数','观看视频总数', '观看总次数', '观看总时间']
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
show: true,
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'right',
|
||||||
|
top: 'center',
|
||||||
|
feature: {
|
||||||
|
mark: { show: true },
|
||||||
|
dataView: {
|
||||||
|
show: true,
|
||||||
|
title: '数据视图',
|
||||||
|
lang: ['单位观看量统计', '关闭', '导出Excel'], // 按钮
|
||||||
|
contentToOption: function (opts) {
|
||||||
|
$('#tableExcel').table2excel({
|
||||||
|
exclude: '.noExl', //过滤位置的 css 类名, 有class = “noExl” 的行不被导出
|
||||||
|
filename: '最大需量', // 文件名称
|
||||||
|
name: 'Excel Document Name.xls',
|
||||||
|
exclude_img: true,
|
||||||
|
exclude_links: true,
|
||||||
|
exclude_inputs: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
optionToContent: function (opt) {
|
||||||
|
var axisData = opt.yAxis[0].data //坐标轴
|
||||||
|
var series = opt.series //折线图的数据
|
||||||
|
console.log(series);
|
||||||
|
var tdHeads =
|
||||||
|
'<td style="margin-top:10px; padding: 0 15px">视频大类</td>' //表头
|
||||||
|
var tdBodys = ''
|
||||||
|
series.forEach(function (item) {
|
||||||
|
tdHeads += `<td style="padding:5px 15px">${item.name}</td>`
|
||||||
|
})
|
||||||
|
var table = `<table id='table-content' border="1" style="margin-left:20px;border-collapse:collapse;font-size:14px;text-align:center;"><tbody><tr>${tdHeads} </tr>`
|
||||||
|
for (var i = 0, l = axisData.length; i < l; i++) {
|
||||||
|
for (var j = 0; j < series.length; j++) {
|
||||||
|
if (series[j].data[i] == undefined) {
|
||||||
|
tdBodys += `<td>${'-'}</td>`
|
||||||
|
} else {
|
||||||
|
tdBodys += `<td>${series[j].data[i]}</td>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table += `<tr><td style="padding: 0 15px">${axisData[i]}</td>${tdBodys}</tr>`
|
||||||
|
tdBodys = ''
|
||||||
|
}
|
||||||
|
table += '</tbody></table>'
|
||||||
|
return table
|
||||||
|
},
|
||||||
|
contentToOption: function (HTMLDomElement, opt) {
|
||||||
|
let et = XLSX.utils.table_to_book(
|
||||||
|
document.getElementById("table-content")
|
||||||
|
); //此处传入table的DOM节点
|
||||||
|
let etout = XLSX.write(et, {
|
||||||
|
bookType: "xlsx",
|
||||||
|
bookSST: true,
|
||||||
|
type: "array",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
FileSaver.saveAs(
|
||||||
|
new Blob([etout], {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
}),
|
||||||
|
"统计数据.xlsx"
|
||||||
|
); //trade-publish.xlsx 为导出的文件名
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
return etout;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
magicType: { show: true, type: [ 'stack'] },
|
||||||
|
restore: { show: true },
|
||||||
|
saveAsImage: { show: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
axisTick: { show: false },
|
||||||
|
data: xAxisOptions,
|
||||||
|
// axisLabel: {
|
||||||
|
// interval:0,
|
||||||
|
// rotate:60//角度顺时针计算的
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'value'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '观看完成视频总数',
|
||||||
|
type: 'bar',
|
||||||
|
barGap: 0,
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '观看视频总数',
|
||||||
|
type: 'bar',
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '观看总次数',
|
||||||
|
type: 'bar',
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data3
|
||||||
|
}
|
||||||
|
,
|
||||||
|
{
|
||||||
|
name: '观看总时间',
|
||||||
|
type: 'bar',
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
this.orgChart.setOption(option);
|
||||||
|
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getUserView(){
|
||||||
|
groupByUserView(this.dataQuery).then(res=>{
|
||||||
|
debugger;
|
||||||
|
console.log(res.data);
|
||||||
|
let data = res.data;
|
||||||
|
let xAxisOptions = [],data1 = [],data2 = [],data3 = [],data4=[];
|
||||||
|
data.forEach(item=>{
|
||||||
|
xAxisOptions.push(item.姓名);
|
||||||
|
data1.push(item.观看完成视频总数);
|
||||||
|
data2.push(item.观看视频总数);
|
||||||
|
data3.push(item.观看总次数);
|
||||||
|
data4.push(item.观看总时间);
|
||||||
|
})
|
||||||
|
let chartDom = document.getElementById('userMain');
|
||||||
|
this.userChart = echarts.init(chartDom);
|
||||||
|
let labelOption = {
|
||||||
|
show: true,
|
||||||
|
position: 'insideBottom',
|
||||||
|
distance: 5,
|
||||||
|
align: 'left',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
rotate: 90,
|
||||||
|
formatter: '{c}',
|
||||||
|
fontSize: 12,
|
||||||
|
rich: {
|
||||||
|
name: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let option = {
|
||||||
|
title: { text: '个人观看量统计' },
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['观看完成视频总数','观看视频总数', '观看总次数', '观看总时间']
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
show: true,
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'right',
|
||||||
|
top: 'center',
|
||||||
|
feature: {
|
||||||
|
mark: { show: true },
|
||||||
|
dataView: {
|
||||||
|
show: true,
|
||||||
|
title: '数据视图',
|
||||||
|
lang: ['个人观看量统计', '关闭', '导出Excel'], // 按钮
|
||||||
|
contentToOption: function (opts) {
|
||||||
|
$('#tableExcel').table2excel({
|
||||||
|
exclude: '.noExl', //过滤位置的 css 类名, 有class = “noExl” 的行不被导出
|
||||||
|
filename: '最大需量', // 文件名称
|
||||||
|
name: 'Excel Document Name.xls',
|
||||||
|
exclude_img: true,
|
||||||
|
exclude_links: true,
|
||||||
|
exclude_inputs: true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
optionToContent: function (opt) {
|
||||||
|
var axisData = opt.yAxis[0].data //坐标轴
|
||||||
|
var series = opt.series //折线图的数据
|
||||||
|
console.log(series);
|
||||||
|
var tdHeads =
|
||||||
|
'<td style="margin-top:10px; padding: 0 15px">视频大类</td>' //表头
|
||||||
|
var tdBodys = ''
|
||||||
|
series.forEach(function (item) {
|
||||||
|
tdHeads += `<td style="padding:5px 15px">${item.name}</td>`
|
||||||
|
})
|
||||||
|
var table = `<table id='table-content' border="1" style="margin-left:20px;border-collapse:collapse;font-size:14px;text-align:center;"><tbody><tr>${tdHeads} </tr>`
|
||||||
|
for (var i = 0, l = axisData.length; i < l; i++) {
|
||||||
|
for (var j = 0; j < series.length; j++) {
|
||||||
|
if (series[j].data[i] == undefined) {
|
||||||
|
tdBodys += `<td>${'-'}</td>`
|
||||||
|
} else {
|
||||||
|
tdBodys += `<td>${series[j].data[i]}</td>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table += `<tr><td style="padding: 0 15px">${axisData[i]}</td>${tdBodys}</tr>`
|
||||||
|
tdBodys = ''
|
||||||
|
}
|
||||||
|
table += '</tbody></table>'
|
||||||
|
return table
|
||||||
|
},
|
||||||
|
contentToOption: function (HTMLDomElement, opt) {
|
||||||
|
let et = XLSX.utils.table_to_book(
|
||||||
|
document.getElementById("table-content")
|
||||||
|
); //此处传入table的DOM节点
|
||||||
|
let etout = XLSX.write(et, {
|
||||||
|
bookType: "xlsx",
|
||||||
|
bookSST: true,
|
||||||
|
type: "array",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
FileSaver.saveAs(
|
||||||
|
new Blob([etout], {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
}),
|
||||||
|
"统计数据.xlsx"
|
||||||
|
); //trade-publish.xlsx 为导出的文件名
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
return etout;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
magicType: { show: true, type: [ 'stack'] },
|
||||||
|
restore: { show: true },
|
||||||
|
saveAsImage: { show: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
axisTick: { show: false },
|
||||||
|
data: xAxisOptions,
|
||||||
|
// axisLabel: {
|
||||||
|
// interval:0,
|
||||||
|
// rotate:60//角度顺时针计算的
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'value'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '观看完成视频总数',
|
||||||
|
type: 'bar',
|
||||||
|
barGap: 0,
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '观看视频总数',
|
||||||
|
type: 'bar',
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '观看总次数',
|
||||||
|
type: 'bar',
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data3
|
||||||
|
}
|
||||||
|
,
|
||||||
|
{
|
||||||
|
name: '观看总时间',
|
||||||
|
type: 'bar',
|
||||||
|
label: labelOption,
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: data4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
this.userChart.setOption(option);
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
handleFilter(){
|
||||||
|
this.cateChart.resize();
|
||||||
|
this.userChart.resize();
|
||||||
|
this.orgChart.resize();
|
||||||
|
this.dataQuery.start_time = this.dataValue[0];
|
||||||
|
this.dataQuery.end_time = this.dataValue[1];
|
||||||
|
this.getCategoryView();
|
||||||
|
this.getGroupView();
|
||||||
|
this.getUserView();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -54,12 +54,12 @@
|
||||||
<div class="bottom clearfix">
|
<div class="bottom clearfix">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-button type="warning" style="border: none;float: left;padding: 2px; color:#dcae07;background-color: white;" icon="el-icon-view">{{o.views}}
|
<el-button type="warning" style="border: none;float: left;padding: 2px; color:#dcae07;background-color: white;" icon="el-icon-view">{{o.views_n}}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="6">
|
<el-col :span="6">
|
||||||
<el-button type="warning" style="border: none;float: left;padding: 2px; color:#dcae07;
|
<el-button type="warning" style="border: none;float: left;padding: 2px; color:#dcae07;
|
||||||
background-color: white;" icon="el-icon-s-custom">{{o.viewsp}}
|
background-color: white;" icon="el-icon-s-custom">{{o.viewsp_n}}
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
|
|
@ -182,7 +182,16 @@
|
||||||
}else{
|
}else{
|
||||||
sessionStorage.removeItem('videoType');
|
sessionStorage.removeItem('videoType');
|
||||||
}
|
}
|
||||||
this.$router.push({name: "Index", params: {fileid: a.fileid, id: a.id}})
|
let routeData = this.$router.resolve({
|
||||||
|
path: "/test/index",
|
||||||
|
query: {
|
||||||
|
id: a.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//必要操作,否则不会打开新页面
|
||||||
|
window.open(routeData.href, '_blank');
|
||||||
|
// this.$router.push({path: "index", query: {fileid: a.fileid, id: a.id}})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -60,4 +60,5 @@
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "uview-ui/index.scss";
|
@import "uview-ui/index.scss";
|
||||||
@import "common/demo.scss";
|
@import "common/demo.scss";
|
||||||
|
@import "common/uni-ui.scss";
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,15 @@ const install = (Vue, vm) => {
|
||||||
let getVideos = (params={})=>vm.$u.get('/vod/video/', params);//点播视频
|
let getVideos = (params={})=>vm.$u.get('/vod/video/', params);//点播视频
|
||||||
let getDickey = (params={})=>vm.$u.get('/system/dict/', params);//查询字典
|
let getDickey = (params={})=>vm.$u.get('/system/dict/', params);//查询字典
|
||||||
let putMyVideoView =(id)=>vm.$u.put(`/vod/video/${id}/myview/`);//更新本人观看信息
|
let putMyVideoView =(id)=>vm.$u.put(`/vod/video/${id}/myview/`);//更新本人观看信息
|
||||||
|
|
||||||
|
|
||||||
|
//考试有关
|
||||||
|
let getExamList = (params={})=>vm.$u.get('/exam/exam/', params);//考试列表
|
||||||
|
let startExam = (id)=>vm.$u.post(`/exam/exam/${id}/start/`);//开始考试
|
||||||
|
let submitExam = (id,params={})=>vm.$u.post(`/exam/examrecord/${id}/submit/`,params);//开始考试
|
||||||
|
let examRecord = (id,params={})=>vm.$u.get(`/exam/examrecord/self/`,params);//我的考试记录
|
||||||
|
let examRecordDetail = (id,params={})=>vm.$u.get(`/exam/examrecord/${id}/`,params);//我的考试记录
|
||||||
|
|
||||||
vm.$u.api = {getUserInfo,
|
vm.$u.api = {getUserInfo,
|
||||||
getCode,
|
getCode,
|
||||||
codeLogin,
|
codeLogin,
|
||||||
|
|
@ -64,7 +73,13 @@ const install = (Vue, vm) => {
|
||||||
getDocument,
|
getDocument,
|
||||||
getVideos,
|
getVideos,
|
||||||
getDickey,
|
getDickey,
|
||||||
putMyVideoView
|
putMyVideoView,
|
||||||
|
|
||||||
|
getExamList,
|
||||||
|
startExam,
|
||||||
|
submitExam,
|
||||||
|
examRecord,
|
||||||
|
examRecordDetail
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
.uni-flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uni-flex-row {
|
||||||
|
@extend .uni-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uni-flex-column {
|
||||||
|
@extend .uni-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uni-color-gary {
|
||||||
|
color: #3b4144;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题 */
|
||||||
|
.uni-title {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: $uni-spacing-col-base;
|
||||||
|
font-size: $uni-font-size-lg;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3b4144;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uni-title-sub {
|
||||||
|
display: flex;
|
||||||
|
// margin-bottom: $uni-spacing-col-base;
|
||||||
|
font-size: $uni-font-size-base;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #3b4144;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 描述 额外文本 */
|
||||||
|
.uni-note {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #999;
|
||||||
|
font-size: $uni-font-size-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表内容 */
|
||||||
|
.uni-list-box {
|
||||||
|
@extend .uni-flex-row;
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 略缩图 */
|
||||||
|
.uni-thumb {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: $uni-spacing-row-base;
|
||||||
|
width: 125px;
|
||||||
|
height: 75px;
|
||||||
|
border-radius: $uni-border-radius-lg;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px #f5f5f5 solid;
|
||||||
|
image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uni-media-box {
|
||||||
|
@extend .uni-flex-row;
|
||||||
|
// margin-bottom: $uni-spacing-col-base;
|
||||||
|
border-radius: $uni-border-radius-lg;
|
||||||
|
overflow: hidden;
|
||||||
|
.uni-thumb {
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
flex-shrink: 1;
|
||||||
|
width: 33%;
|
||||||
|
border-radius:0;
|
||||||
|
&:first-child {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容 */
|
||||||
|
.uni-content {
|
||||||
|
@extend .uni-flex-column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表footer */
|
||||||
|
.uni-footer {
|
||||||
|
@extend .uni-flex-row;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-top: $uni-spacing-col-lg;
|
||||||
|
}
|
||||||
|
.uni-footer-text {
|
||||||
|
font-size: $uni-font-size-sm;
|
||||||
|
color: $uni-text-color-grey;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签 */
|
||||||
|
|
||||||
|
.uni-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 5px;
|
||||||
|
border: 1px $uni-border-color solid;
|
||||||
|
margin-right: $uni-spacing-row-sm;
|
||||||
|
border-radius: $uni-border-radius-base;
|
||||||
|
background: $uni-bg-color-grey;
|
||||||
|
color: $uni-text-color;
|
||||||
|
font-size: $uni-font-size-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 链接 */
|
||||||
|
.uni-link {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: $uni-text-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
@ -11,15 +11,6 @@
|
||||||
// }]
|
// }]
|
||||||
// },
|
// },
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
|
||||||
"path" : "pages/login/login",
|
|
||||||
"style" :
|
|
||||||
{
|
|
||||||
"navigationBarTitleText": "验证码登录",
|
|
||||||
"enablePullDownRefresh": false
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path" : "pages/home/home",
|
"path" : "pages/home/home",
|
||||||
"style" :
|
"style" :
|
||||||
|
|
@ -27,8 +18,16 @@
|
||||||
"navigationBarTitleText": "",
|
"navigationBarTitleText": "",
|
||||||
"enablePullDownRefresh": false
|
"enablePullDownRefresh": false
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path" : "pages/login/login",
|
||||||
|
"style" :
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "验证码登录",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path" : "pages/my/my",
|
"path" : "pages/my/my",
|
||||||
"style" :
|
"style" :
|
||||||
|
|
@ -124,7 +123,60 @@
|
||||||
"path" : "pages/vod/video",
|
"path" : "pages/vod/video",
|
||||||
"style" :
|
"style" :
|
||||||
{
|
{
|
||||||
"navigationBarTitleText": "云点播",
|
"navigationBarTitleText": "视频观看",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
,{
|
||||||
|
"path" : "pages/exam/index",
|
||||||
|
"style" :
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "考试列表",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
,{
|
||||||
|
"path" : "pages/exam/preview",
|
||||||
|
"style" :
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "考试须知",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
,{
|
||||||
|
"path" : "pages/exam/main",
|
||||||
|
"style" :
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "答题中",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
,{
|
||||||
|
"path" : "pages/exam/record",
|
||||||
|
"style" :
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "答题记录",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
,{
|
||||||
|
"path" : "pages/exam/result",
|
||||||
|
"style" :
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "答题结束",
|
||||||
|
"enablePullDownRefresh": false
|
||||||
|
}
|
||||||
|
|
||||||
|
},{
|
||||||
|
"path" : "pages/exam/detail",
|
||||||
|
"style" :
|
||||||
|
{
|
||||||
|
"navigationBarTitleText": "答题详情",
|
||||||
"enablePullDownRefresh": false
|
"enablePullDownRefresh": false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,12 +199,18 @@
|
||||||
"selectedIconPath": "static/common/homec.png",
|
"selectedIconPath": "static/common/homec.png",
|
||||||
"text": "主页"
|
"text": "主页"
|
||||||
},
|
},
|
||||||
/* {
|
{
|
||||||
|
"pagePath": "pages/exam/index",
|
||||||
|
"iconPath": "static/common/dati.png",
|
||||||
|
"selectedIconPath": "static/common/datic.png",
|
||||||
|
"text": "答题"
|
||||||
|
},
|
||||||
|
{
|
||||||
"pagePath": "pages/vod/video",
|
"pagePath": "pages/vod/video",
|
||||||
"iconPath": "static/common/play.png",
|
"iconPath": "static/common/play.png",
|
||||||
"selectedIconPath": "static/common/playc.png",
|
"selectedIconPath": "static/common/playc.png",
|
||||||
"text": "点播"
|
"text": "点播"
|
||||||
}, */
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/my/my",
|
"pagePath": "pages/my/my",
|
||||||
"iconPath": "static/common/me.png",
|
"iconPath": "static/common/me.png",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,326 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<!-- 大标题 -->
|
||||||
|
<view class="header" id="header">
|
||||||
|
<span > {{currentExam.type}}</span>
|
||||||
|
</view>
|
||||||
|
<!-- 小标题栏 -->
|
||||||
|
<view class="sub-header">
|
||||||
|
<u-row>
|
||||||
|
<u-col span="4">
|
||||||
|
<view><text style="color:red">{{currentExam.total_score}}</text>分</view>
|
||||||
|
</u-col>
|
||||||
|
<u-col span="4">
|
||||||
|
<view style="text-align: center;"><span class="header-card">{{currentQuestion.type}}</span></view>
|
||||||
|
</u-col>
|
||||||
|
<u-col span="4">
|
||||||
|
<view style="text-align: right;"><span>第{{currentIndex+1}}/{{currentExam.questions_.length}} 题</span></view>
|
||||||
|
</u-col>
|
||||||
|
</u-row>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="content" scroll-y="true" v-bind:style="{height:scollHeight+'px'}">
|
||||||
|
<view class="name">
|
||||||
|
<view><text style="margin-right: 10rpx;">{{currentIndex+1}} 、</text> {{currentQuestion.name}}</view>
|
||||||
|
<view v-if="currentQuestion.img">
|
||||||
|
{{currentQuestion.img}}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="options">
|
||||||
|
<checkbox-group v-if="currentQuestion.type=='多选'">
|
||||||
|
<label class="option" v-for="item in currentOptions" :key="item.id" >
|
||||||
|
<view class="option-item1" >
|
||||||
|
<checkbox :value="item.value" :checked="item.checked" color="#2979ff" :disabled="item.disabled"/>
|
||||||
|
</view >
|
||||||
|
<view class="option-item2" >{{item.value}}.{{item.text}}</view>
|
||||||
|
</label>
|
||||||
|
</checkbox-group>
|
||||||
|
<radio-group v-else>
|
||||||
|
<label class="option" v-for="item in currentOptions" :key="item.id">
|
||||||
|
<view class="option-item1">
|
||||||
|
<radio :value="item.value" :checked="item.checked" color="#2979ff" :disabled="item.disabled"></radio>
|
||||||
|
</view>
|
||||||
|
<view class="option-item2">
|
||||||
|
{{item.value}}.{{item.text}}
|
||||||
|
</view>
|
||||||
|
</label>
|
||||||
|
</radio-group>
|
||||||
|
</view>
|
||||||
|
<view class="answer">
|
||||||
|
<view v-if="currentQuestion.type=='多选'">
|
||||||
|
<view>正确答案:{{currentQuestion.right.join("")}}</view>
|
||||||
|
<view v-if="currentQuestion.user_answer">您的答案:{{currentQuestion.user_answer.join("")}}</view>
|
||||||
|
<view v-else style="color:red">您未作答</view>
|
||||||
|
</view>
|
||||||
|
<view v-else>
|
||||||
|
<view>正确答案:{{currentQuestion.right}}</view>
|
||||||
|
<view v-if="currentQuestion.user_answer">您的答案:{{currentQuestion.user_answer}}</view>
|
||||||
|
<view v-else style="color:red">您未作答</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="currentQuestion.user_answer">
|
||||||
|
<view v-if="currentQuestion.is_right" style="color:green;font-weight: bold;">回答正确!</view>
|
||||||
|
<view v-else style="color:red;font-weight: bold;">回答有误!</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view style="height:20rpx"></view>
|
||||||
|
</scroll-view>
|
||||||
|
<u-popup v-model="showM" mode="bottom" height="40%">
|
||||||
|
<view class="questionArea" style="display:flex">
|
||||||
|
<block v-for="(item, index) in currentExam.questions_" :key="index">
|
||||||
|
<view class="questionItem questionItem-select" v-if="item.user_answer&&item.is_right" @click="jumpQuestion(index)">{{index+1}}</view>
|
||||||
|
<view class="questionItem questionItem-wrong" v-else-if="item.user_answer&&!item.is_right" @click="jumpQuestion(index)">{{index+1}}</view>
|
||||||
|
<view class="questionItem questionItem-unselect" v-else @click="jumpQuestion(index)">{{index+1}}</view>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
<!-- 底部栏 -->
|
||||||
|
<view class="footer" id="footer">
|
||||||
|
<u-button @click='previousQ()' throttle-time="200" :plain="true" type="primary">上一题</u-button>
|
||||||
|
<u-button @click="showM = !showM" type="primary">答题卡</u-button>
|
||||||
|
<u-button @click='nextQ()' throttle-time="200" :plain="true" type="primary">下一题</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentExam:{},
|
||||||
|
currentIndex:0,
|
||||||
|
currentOptions:[],
|
||||||
|
currentQuestion:{question_:{}},
|
||||||
|
showM:false,
|
||||||
|
keyid:0,
|
||||||
|
start_time:null,
|
||||||
|
scollHeight:0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
this.currentExam = uni.getStorageSync('currentExam');
|
||||||
|
debugger;
|
||||||
|
console.log(this.currentExam)
|
||||||
|
let res = uni.getSystemInfoSync()
|
||||||
|
let ratio = 750 / res.windowWidth;
|
||||||
|
this.scollHeight = res.windowHeight*ratio - 230
|
||||||
|
this.initQuestion()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initQuestion(){
|
||||||
|
var currentQuestion = this.currentExam.questions_[this.currentIndex]
|
||||||
|
this.currentQuestion = currentQuestion
|
||||||
|
let options_ = []
|
||||||
|
let origin = currentQuestion.options
|
||||||
|
this.currentOptions = []
|
||||||
|
for (let key in origin) {
|
||||||
|
let option = {
|
||||||
|
value:key,
|
||||||
|
text:origin[key],
|
||||||
|
id: this.keyid++,
|
||||||
|
checked:false,
|
||||||
|
disabled:true,
|
||||||
|
}
|
||||||
|
if (currentQuestion.user_answer) {
|
||||||
|
if (key == currentQuestion.user_answer || currentQuestion.user_answer.indexOf(key) != -1) {
|
||||||
|
option.checked = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
option.checked = false
|
||||||
|
}
|
||||||
|
options_.push(option)
|
||||||
|
}
|
||||||
|
this.currentOptions = options_;
|
||||||
|
},
|
||||||
|
nextQ(){
|
||||||
|
let index = this.currentIndex + 1
|
||||||
|
if(index<this.currentExam.questions_.length){
|
||||||
|
this.currentIndex = index
|
||||||
|
this.initQuestion()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previousQ(){
|
||||||
|
let index = this.currentIndex - 1
|
||||||
|
if(index >= 0){
|
||||||
|
this.currentIndex = index
|
||||||
|
this.initQuestion()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
jumpQuestion(index){
|
||||||
|
this.currentIndex = index
|
||||||
|
this.initQuestion()
|
||||||
|
this.showM = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
page {
|
||||||
|
background-color: $u-bg-color;
|
||||||
|
|
||||||
|
}
|
||||||
|
.content{
|
||||||
|
margin-top:8rpx;
|
||||||
|
margin-bottom: 120rpx;
|
||||||
|
.name {
|
||||||
|
font-size:34rpx;
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
color:$u-content-color;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
margin-top:8rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
.option {
|
||||||
|
padding: 10rpx 0rpx;
|
||||||
|
display: flex;
|
||||||
|
font-size: 36rpx;
|
||||||
|
.option-item1{
|
||||||
|
justify-content: flex-start
|
||||||
|
}
|
||||||
|
.option-item2{
|
||||||
|
justify-content: flex-start;
|
||||||
|
color:$u-main-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.answer{
|
||||||
|
margin-top:8rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
.resolution{
|
||||||
|
margin-top:8rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sub-header {
|
||||||
|
//width: 750rpx;
|
||||||
|
// position: fixed;
|
||||||
|
// margin-top: 130rpx;
|
||||||
|
padding: 4rpx 20rpx;
|
||||||
|
color: #000;
|
||||||
|
font-size: 33rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.header-card {
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
// border: 1px solid $u-type-primary-dark;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: $u-type-primary-dark;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
width: 750rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
padding: 30rpx 60rpx;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 32rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #4c8af3;
|
||||||
|
box-shadow: 0 0 5px 1px #eee;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
|
||||||
|
&-card {
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border: 1px solid $theme-color;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: $theme-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// .header{
|
||||||
|
// display: flex;
|
||||||
|
// height: 70rpx;
|
||||||
|
// background-color: orange;
|
||||||
|
// font-size: 36rpx;
|
||||||
|
// .content {
|
||||||
|
// align-self: center;
|
||||||
|
// .text{
|
||||||
|
// margin-left:10rpx
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .rbutton {
|
||||||
|
// margin-left: auto;
|
||||||
|
// align-self: center;
|
||||||
|
// margin-right: 10rpx
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
.header {
|
||||||
|
width: 750rpx;
|
||||||
|
//position: fixed;
|
||||||
|
//position: relative;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 60rpx;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $theme-color;
|
||||||
|
// letter-spacing: 10rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
line-height: 40rpx;
|
||||||
|
position: absolute;
|
||||||
|
top: 4rpx;
|
||||||
|
right: 10rpx;
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: $u-type-error;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreText {
|
||||||
|
color: #00b060;
|
||||||
|
font-size: 35rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.questionArea {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.questionItem {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
margin: 10rpx 22rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
font-size: 35rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.questionItem-select {
|
||||||
|
// color: #FFFFFF;
|
||||||
|
background-color: $theme-color;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.questionItem-unselect {
|
||||||
|
// color: #FFFFFF;
|
||||||
|
background-color: #bbbbbb;
|
||||||
|
}
|
||||||
|
.questionItem-wrong {
|
||||||
|
// color: #FFFFFF;
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<uni-list>
|
||||||
|
<!-- 垂直排列,无略缩图,主标题+副标题显示 -->
|
||||||
|
<uni-list-item direction="column" :key="item.id" v-for="(item, index) in examList">
|
||||||
|
<template v-slot:header>
|
||||||
|
<view class="uni-title">{{item.name}}</view>
|
||||||
|
</template>
|
||||||
|
<template v-slot:body>
|
||||||
|
<view class="uni-list-box">
|
||||||
|
<view class="uni-content">
|
||||||
|
<view class="uni-title-sub uni-ellipsis-2">开启时间: {{item.open_time}}至{{item.close_time}}</view>
|
||||||
|
<view class="uni-note">考试机会: {{item.chance}}次</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<view class="uni-footer">
|
||||||
|
<u-button size="mini" type="primary" @click="attendExam(item)">我要参加</u-button>
|
||||||
|
<u-button size="mini" type="info">成绩排名</u-button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</uni-list-item>
|
||||||
|
</uni-list>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
query:{
|
||||||
|
page: 1
|
||||||
|
},
|
||||||
|
examList: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.getExamList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getExamList(){
|
||||||
|
this.$u.api.getExamList(this.query).then(res=>{
|
||||||
|
this.examList = res.data.results
|
||||||
|
})
|
||||||
|
},
|
||||||
|
attendExam(val){
|
||||||
|
console.log(val)
|
||||||
|
uni.setStorageSync('currentExam', val)
|
||||||
|
uni.navigateTo({
|
||||||
|
url:"/pages/exam/preview"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,399 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<!-- 大标题 -->
|
||||||
|
<view class="header" id="header">
|
||||||
|
<span class="header-button">第{{currentIndex+1}}/{{currentExam.questions_.length}} 题</span>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
<!-- 小标题栏 -->
|
||||||
|
<view class="sub-header">
|
||||||
|
<u-row>
|
||||||
|
<u-col span="4">
|
||||||
|
<u-count-down :timestamp="currentExam.paper_.limit*60" :show-days="false" @end="end" border-color="#2979ff">
|
||||||
|
</u-count-down>
|
||||||
|
</u-col>
|
||||||
|
<u-col span="4">
|
||||||
|
<view style="text-align: center;"><span class="header-card">{{currentQuestion.type}}</span></view>
|
||||||
|
</u-col>
|
||||||
|
<u-col span="4">
|
||||||
|
<view style="text-align: right;"><span class="submitButton" @click='handleSubmit()' >交卷</span></view>
|
||||||
|
</u-col>
|
||||||
|
</u-row>
|
||||||
|
</view>
|
||||||
|
<scroll-view class="content" scroll-y="true" v-bind:style="{height:scollHeight+'rpx'}">
|
||||||
|
<view class="name">
|
||||||
|
<view>{{currentIndex}}·{{currentQuestion.name}}</view>
|
||||||
|
<!-- <rich-text :nodes="currentQuestion.name"></rich-text> -->
|
||||||
|
<view v-if="currentQuestion.img">
|
||||||
|
{{currentQuestion.img}}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="options">
|
||||||
|
<checkbox-group @change="checkboxGroupChange" v-if="currentQuestion.type=='多选'">
|
||||||
|
<label class="option" v-for="item in currentOptions" :key="item.id" >
|
||||||
|
<view class="option-item1">
|
||||||
|
<checkbox :value="item.value" :checked="item.checked" color="#2979ff"/>
|
||||||
|
</view >
|
||||||
|
<view class="option-item2">{{item.value}}.{{item.text}}</view>
|
||||||
|
</label>
|
||||||
|
</checkbox-group>
|
||||||
|
<radio-group v-else @change="checkboxGroupChange">
|
||||||
|
<label class="option" v-for="item in currentOptions" :key="item.id">
|
||||||
|
<view class="option-item1">
|
||||||
|
<radio :value="item.value" :checked="item.checked" color="#2979ff"></radio>
|
||||||
|
</view>
|
||||||
|
<view class="option-item2">
|
||||||
|
{{item.value}}.{{item.text}}
|
||||||
|
</view>
|
||||||
|
</label>
|
||||||
|
</radio-group>
|
||||||
|
</view>
|
||||||
|
<view style="height:20rpx"></view>
|
||||||
|
</scroll-view>
|
||||||
|
<u-popup v-model="showM" mode="bottom" height="40%">
|
||||||
|
<view class="questionArea" style="display:flex">
|
||||||
|
<block v-for="(item, index) in currentExam.questions_" :key="index">
|
||||||
|
<view class="questionItem questionItem-select" v-if="item.user_answer" @click="jumpQuestion(index)">{{index+1}}</view>
|
||||||
|
<view class="questionItem questionItem-unselect" v-else @click="jumpQuestion(index)">{{index+1}}</view>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
</u-popup>
|
||||||
|
<!-- 底部栏 -->
|
||||||
|
<view class="footer" id="footer">
|
||||||
|
<u-button @click='previousQ()' throttle-time="200" :plain="true" type="primary">上一题</u-button>
|
||||||
|
<u-button @click="showM = !showM" type="primary">答题卡</u-button>
|
||||||
|
<u-button @click='nextQ()' throttle-time="200" :plain="true" type="primary">下一题</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentExam:{questions_:[]},
|
||||||
|
currentIndex:0,
|
||||||
|
currentOptions:[],
|
||||||
|
currentQuestion:{type:'单选'},
|
||||||
|
showM:false,
|
||||||
|
keyid:0,
|
||||||
|
start_time:null,
|
||||||
|
scollHeight:0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
//#ifdef MP-WEIXIN
|
||||||
|
uni.hideHomeButton()
|
||||||
|
//#endif
|
||||||
|
this.start_time= (new Date()).getTime()
|
||||||
|
this.currentExam = uni.getStorageSync('currentExam')
|
||||||
|
let res = uni.getSystemInfoSync();
|
||||||
|
let ratio = 750 / res.windowWidth;
|
||||||
|
this.scollHeight = res.windowHeight*ratio - 230
|
||||||
|
this.initQuestion()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
end(){
|
||||||
|
var that = this
|
||||||
|
uni.showModal({
|
||||||
|
title: '警告',
|
||||||
|
content: '时间到,请交卷',
|
||||||
|
showCancel:false,
|
||||||
|
success: function (res) {
|
||||||
|
if (res.confirm) {
|
||||||
|
that.handIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
change(){
|
||||||
|
|
||||||
|
},
|
||||||
|
handleSubmit(){
|
||||||
|
var that = this
|
||||||
|
let questions = that.currentExam.questions_;
|
||||||
|
for(var i=0;i<questions.length;i++){
|
||||||
|
if(!questions[i].user_answer){
|
||||||
|
uni.showModal({
|
||||||
|
title: '警告',
|
||||||
|
content: '答卷未完成,确认交卷吗?',
|
||||||
|
success: function (res) {
|
||||||
|
if (res.confirm) {
|
||||||
|
that.handIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确认交卷吗?',
|
||||||
|
success: function (res) {
|
||||||
|
if (res.confirm) {
|
||||||
|
//直接交卷,不需要判卷了
|
||||||
|
that.handIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handIn(){
|
||||||
|
var that = this
|
||||||
|
uni.showLoading({
|
||||||
|
title:'正在提交...',
|
||||||
|
mask:true
|
||||||
|
})
|
||||||
|
let questions_ = [];
|
||||||
|
for (let i = 0; i < that.currentExam.questions_.length; i++) {
|
||||||
|
let obj = {};
|
||||||
|
obj.id=that.currentExam.questions_[i].id;
|
||||||
|
obj.user_answer=that.currentExam.questions_[i].user_answer;
|
||||||
|
questions_.push(obj);
|
||||||
|
}
|
||||||
|
that.$u.api.submitExam(that.currentExam.examrecord,{questions_:questions_}).then(res=>{
|
||||||
|
uni.setStorageSync('currentExam',res.data)
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.redirectTo({
|
||||||
|
url:'/pages/exam/result'
|
||||||
|
})
|
||||||
|
}).catch(e=>{
|
||||||
|
if(res.msg){
|
||||||
|
uni.showModal({
|
||||||
|
title:'提交失败',
|
||||||
|
content:res.msg,
|
||||||
|
showCancel:false,
|
||||||
|
success(res) {
|
||||||
|
uni.reLaunch({
|
||||||
|
url:'/pages/index/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
// let questions = this.currentExam.questions
|
||||||
|
// let score=0
|
||||||
|
// for(var i=0, lenI =questions.length;i<lenI;i++){
|
||||||
|
// var ret=this.panTi(questions[i])
|
||||||
|
// questions[i].is_right=ret.is_right
|
||||||
|
// questions[i].score=ret.score
|
||||||
|
// score = score+ret.score
|
||||||
|
// }
|
||||||
|
// this.currentExam.score = score
|
||||||
|
// this.currentExam.start_time = this.$u.timeFormat(this.start_time, 'yyyy-mm-dd hh:MM:ss');
|
||||||
|
// this.currentExam.end_time = this.$u.timeFormat((new Date()).getTime(), 'yyyy-mm-dd hh:MM:ss')
|
||||||
|
// this.currentExam.took = Math.floor(((new Date()).getTime() - this.start_time) / 1000)
|
||||||
|
// if(score>=this.currentExam.pass_score){
|
||||||
|
// this.currentExam.is_pass=true
|
||||||
|
// }else{
|
||||||
|
// this.currentExam.is_pass=false
|
||||||
|
// }
|
||||||
|
// if(this.vuex_user.id){
|
||||||
|
|
||||||
|
// }
|
||||||
|
// else{
|
||||||
|
// uni.setStorageSync('currentExam',this.currentExam)
|
||||||
|
// uni.hideLoading()
|
||||||
|
// uni.redirectTo({
|
||||||
|
// url:'/pages/exam/result'
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
panTi(tm_current) {
|
||||||
|
// 返回当前题目是否正确,得分多少
|
||||||
|
let is_right = false, score = 0
|
||||||
|
if (tm_current.type == '多选') {
|
||||||
|
if (tm_current.user_answer) {
|
||||||
|
if (tm_current.user_answer.sort().toString() == tm_current.right.sort().toString()) {
|
||||||
|
is_right = true
|
||||||
|
score = tm_current.total_score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(tm_current.right == tm_current.user_answer){
|
||||||
|
is_right = true
|
||||||
|
score = tm_current.total_score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {'is_right':is_right,'score':score}
|
||||||
|
},
|
||||||
|
initQuestion(){
|
||||||
|
var currentQuestion = this.currentExam.questions_[this.currentIndex];
|
||||||
|
this.currentQuestion = currentQuestion;
|
||||||
|
let options_ = [];
|
||||||
|
let origin = currentQuestion.options;
|
||||||
|
this.currentOptions = [];
|
||||||
|
for (let key in origin) {
|
||||||
|
let option = {
|
||||||
|
value:key,
|
||||||
|
text:origin[key],
|
||||||
|
id: this.keyid++,
|
||||||
|
checked:false
|
||||||
|
}
|
||||||
|
if (currentQuestion.user_answer) {
|
||||||
|
if (key == currentQuestion.user_answer || currentQuestion.user_answer.indexOf(key) != -1) {
|
||||||
|
option.checked = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
option.checked = false
|
||||||
|
}
|
||||||
|
options_.push(option)
|
||||||
|
}
|
||||||
|
this.currentOptions = options_
|
||||||
|
},
|
||||||
|
nextQ(){
|
||||||
|
let index = this.currentIndex + 1
|
||||||
|
if(index<this.currentExam.questions_.length){
|
||||||
|
this.currentIndex = index
|
||||||
|
this.initQuestion()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previousQ(){
|
||||||
|
let index = this.currentIndex - 1
|
||||||
|
if(index >= 0){
|
||||||
|
this.currentIndex = index
|
||||||
|
this.initQuestion()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkboxGroupChange(e){
|
||||||
|
// debugger;
|
||||||
|
console.log(e)
|
||||||
|
this.currentExam.questions_[this.currentIndex].user_answer = e.detail.value
|
||||||
|
},
|
||||||
|
jumpQuestion(index){
|
||||||
|
this.currentIndex = index
|
||||||
|
this.initQuestion()
|
||||||
|
this.showM = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
page {
|
||||||
|
background-color: $u-bg-color;
|
||||||
|
}
|
||||||
|
.content{
|
||||||
|
margin-top:8rpx;
|
||||||
|
.name {
|
||||||
|
font-size:34rpx;
|
||||||
|
padding: 25rpx 30rpx;
|
||||||
|
color:$u-content-color;
|
||||||
|
line-height:130%;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
margin-top:8rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
padding: 6rpx 30rpx;
|
||||||
|
.option {
|
||||||
|
padding: 10rpx 0rpx;
|
||||||
|
display: flex;
|
||||||
|
font-size: 36rpx;
|
||||||
|
.option-item1{
|
||||||
|
justify-content: flex-start
|
||||||
|
}
|
||||||
|
.option-item2{
|
||||||
|
justify-content: flex-start;
|
||||||
|
color:$u-main-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 750rpx;
|
||||||
|
text-align: center;
|
||||||
|
height: 60rpx;
|
||||||
|
line-height: 60rpx;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $theme-color;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 10rpx;
|
||||||
|
font-size:34rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreText {
|
||||||
|
color: #00b060;
|
||||||
|
font-size: 35rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sub-header {
|
||||||
|
padding: 4rpx 20rpx;
|
||||||
|
color: #000;
|
||||||
|
font-size: 33rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.submitButton{
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: $u-type-error;
|
||||||
|
}
|
||||||
|
.header-card {
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: $u-type-primary-dark;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
width: 750rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
padding: 30rpx 60rpx;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 32rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: #4c8af3;
|
||||||
|
box-shadow: 0 0 5px 1px #eee;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
|
||||||
|
&-card {
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
border: 1px solid $theme-color;
|
||||||
|
border-radius: 15rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: $theme-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.questionArea {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.questionItem {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
margin: 10rpx 22rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
font-size: 35rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.questionItem-select {
|
||||||
|
background-color: $theme-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.questionItem-unselect {
|
||||||
|
background-color: #bbbbbb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<view class='wrap'>
|
||||||
|
<view class="subTitle">
|
||||||
|
考试信息
|
||||||
|
</view>
|
||||||
|
<view class="examContent">
|
||||||
|
<view>考试名称:<text>{{currentExam.name}}</text> </view>
|
||||||
|
<view>考试总分:<text>{{currentExam.paper_.total_score}}</text>分</view>
|
||||||
|
<view v-if="currentExam.limit>0">考试时长:<text>{{currentExam.limit}}</text>分钟</view>
|
||||||
|
<view v-else>考试时长:<text>不限时长</text></view>
|
||||||
|
<view>题目分布:单选<text>{{currentExam.paper_.danxuan_count}}</text>道;多选<text>{{currentExam.paper_.duoxuan_count}}</text>道;判断<text>{{currentExam.paper_.panduan_count}}</text>道</view>
|
||||||
|
<view>判分规则:单选{{currentExam.paper_.danxuan_score}}分,多选{{currentExam.paper_.duoxuan_score}}分,多选{{currentExam.paper_.panduan_score}}分,错选少选均不得分</view>
|
||||||
|
</view>
|
||||||
|
<view class="subTitle">答题须知</view>
|
||||||
|
<view class="tipsArea">
|
||||||
|
<ul>
|
||||||
|
<li>1.进入答题后请不要后退或返回。</li>
|
||||||
|
<li>2.可点击上一题/下一题切换。</li>
|
||||||
|
<li>3.可点击答题卡复查</li>
|
||||||
|
<li>4.请合理安排时间答题,可提前交卷</li>
|
||||||
|
</ul>
|
||||||
|
</view>
|
||||||
|
<u-button type="primary" @click="start()">开始答题</u-button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentExam:{}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.currentExam = uni.getStorageSync('currentExam')
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
start(){
|
||||||
|
this.$u.api.startExam(this.currentExam.id).then(res=>{
|
||||||
|
let currentExam =uni.getStorageSync('currentExam');
|
||||||
|
currentExam.examrecord = res.data.examrecord;
|
||||||
|
currentExam.questions_ = res.data.questions_;
|
||||||
|
uni.setStorageSync('currentExam',currentExam)
|
||||||
|
uni.reLaunch({
|
||||||
|
url:'/pages/exam/main'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wrap{
|
||||||
|
padding: 24rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
.subTitle {
|
||||||
|
font-size: $u-font-size-title;
|
||||||
|
font-weight: bold;
|
||||||
|
color: $theme-color;
|
||||||
|
text-align: center;
|
||||||
|
margin: 40rpx auto 20rpx auto;
|
||||||
|
}
|
||||||
|
.examContent {
|
||||||
|
line-height: 50rpx;
|
||||||
|
margin-left: 80rpx;
|
||||||
|
text {
|
||||||
|
font-weight: bold;
|
||||||
|
color:$u-type-warning-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tipsArea {
|
||||||
|
padding: 0 40rpx;
|
||||||
|
margin-left: 0rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0 30rpx;
|
||||||
|
line-height: 50rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<uni-list>
|
||||||
|
<uni-list-item v-for="item in list" :key="item.id" @click="goDetail(item.id)" :clickable="true" link>
|
||||||
|
<!-- 自定义 body -->
|
||||||
|
<template slot="body" style="display: block;">
|
||||||
|
<view>
|
||||||
|
<!-- <text v-if="item.type=='自助模考'">模拟练习</text> -->
|
||||||
|
<text style="font-weight: bold;color:orange">{{item.type}}</text>
|
||||||
|
<text v-if="item.name.indexOf('(补)') != -1">(补)</text>
|
||||||
|
</view>
|
||||||
|
<view style="color:gray;font-size: 26rpx;">
|
||||||
|
<span>耗时:
|
||||||
|
<span style="color:darkblue;font-weight: bold;">{{item.took_format}}</span>
|
||||||
|
</span>
|
||||||
|
-
|
||||||
|
<text>提交时间:{{item.create_time}}</text>
|
||||||
|
</view>
|
||||||
|
<view style="color:gray;font-size: 26rpx;">
|
||||||
|
<span>总分:{{item.total_score}}分</span>
|
||||||
|
-
|
||||||
|
<span>得分:
|
||||||
|
<span style="color:green;font-weight: bold;">{{item.score}}</span>
|
||||||
|
分</span>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</uni-list-item>
|
||||||
|
</uni-list>
|
||||||
|
<view style="color:gray;text-align: center;margin-top:20upx">{{loadingText}}</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
listQuery: {
|
||||||
|
page: 1,
|
||||||
|
page_size: 20
|
||||||
|
},
|
||||||
|
list: [],
|
||||||
|
loadingText: '加载中...'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
var that = this
|
||||||
|
that.$u.api.examRecord(that.listQuery).then(res => {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
uni.setNavigationBarTitle({
|
||||||
|
title: res.data.count + '条答题记录'
|
||||||
|
})
|
||||||
|
if (that.listQuery.page == 1) {
|
||||||
|
if (res.data.results.length == 0) {
|
||||||
|
that.loadingText = '暂无答题记录'
|
||||||
|
} else {
|
||||||
|
that.loadingText = ''
|
||||||
|
that.list = res.data.results
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
that.loadingText = ''
|
||||||
|
that.list = that.list.concat(res.data.results)
|
||||||
|
}
|
||||||
|
}).catch(res => {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
if (res.code == 404) {
|
||||||
|
that.loadingText = '到底了'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goDetail(id) {
|
||||||
|
uni.showLoading({
|
||||||
|
title:"正在获取答题详情",
|
||||||
|
})
|
||||||
|
this.$u.api.examRecordDetail(id).then(res=>{
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.setStorageSync('currentExam', res.data)
|
||||||
|
if (res.data.questions_.length>0){
|
||||||
|
uni.navigateTo({
|
||||||
|
url:'/pages/exam/detail?examrecord='+id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
uni.showToast({
|
||||||
|
title:'获取失败',
|
||||||
|
icon:'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}).catch(e=>{
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.listQuery.page = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
onPullDownRefresh() {
|
||||||
|
this.listQuery.page = 1
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
onReachBottom() {
|
||||||
|
this.listQuery.page = this.listQuery.page + 1
|
||||||
|
this.getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang='scss'>
|
||||||
|
page {
|
||||||
|
background-color: $u-bg-color;
|
||||||
|
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
<template>
|
||||||
|
<view>
|
||||||
|
<image v-if="currentExam.is_pass" class="examImage" :src="imageSrcPass" mode="aspectFit"></image>
|
||||||
|
<image v-else class="examImage" :src="imageSrc" mode="aspectFit"></image>
|
||||||
|
<view v-if="currentExam.is_pass" class="finishText">恭喜您完成考试!</view>
|
||||||
|
<view v-else class="finishText">很遗憾,本次考试您未达标!</view>
|
||||||
|
<view class="finishText">
|
||||||
|
<view><text>{{currentExam.name}}</text></view>
|
||||||
|
<view>总分:<text>{{currentExam.total_score}}</text>分</view>
|
||||||
|
<view>得分:<text>{{currentExam.score}}</text> 分 </view>
|
||||||
|
</view>
|
||||||
|
<view class="btnArea">
|
||||||
|
<u-button class="btnClass" type="primary" :ripple="true" shape="circle" @click="goDetail">查看答卷</u-button>
|
||||||
|
<u-button class="btnClass" :ripple="true" shape="circle" @click="backToHome">返回首页</u-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
imageSrcPass: '/static/exam/result.png',
|
||||||
|
imageSrc: '/static/exam/error.png',
|
||||||
|
currentExam:{}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goDetail(){
|
||||||
|
this.$u.api.examRecordDetail(this.currentExam.id).then(res=>{
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.setStorageSync('currentExam', res.data);
|
||||||
|
debugger;
|
||||||
|
if (res.data.questions_.length>0){
|
||||||
|
uni.navigateTo({
|
||||||
|
url:'/pages/exam/detail?examrecord='+res.data.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
uni.showToast({
|
||||||
|
title:'获取失败',
|
||||||
|
icon:'none'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}).catch(e=>{
|
||||||
|
})
|
||||||
|
},
|
||||||
|
backToHome(){
|
||||||
|
uni.reLaunch({
|
||||||
|
url:'/pages/exam/index'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options){
|
||||||
|
this.currentExam = uni.getStorageSync('currentExam');
|
||||||
|
},
|
||||||
|
beforeRouteUpdate(){
|
||||||
|
uni.removeStorageSync('currentExam');
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.examImage {
|
||||||
|
width: 200rpx;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 100rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
margin: auto;
|
||||||
|
display: block;
|
||||||
|
margin-top: 100rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finishText {
|
||||||
|
// margin: 100rpx 200rpx;
|
||||||
|
padding: 50rpx;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
font-size: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
text {
|
||||||
|
color: $theme-color;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnArea {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 50rpx;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
.btnClass {
|
||||||
|
width: 300rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoArea {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -23,25 +23,10 @@
|
||||||
<u-icon name="arrow-right" color="#969799" size="28"></u-icon>
|
<u-icon name="arrow-right" color="#969799" size="28"></u-icon>
|
||||||
</view> -->
|
</view> -->
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- <view class="u-m-t-20">
|
|
||||||
<u-cell-group>
|
|
||||||
<u-cell-item icon="rmb-circle" title="支付"></u-cell-item>
|
|
||||||
</u-cell-group>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="u-m-t-20">
|
|
||||||
<u-cell-group>
|
|
||||||
<u-cell-item icon="star" title="收藏"></u-cell-item>
|
|
||||||
<u-cell-item icon="photo" title="相册"></u-cell-item>
|
|
||||||
<u-cell-item icon="coupon" title="卡券"></u-cell-item>
|
|
||||||
<u-cell-item icon="heart" title="关注"></u-cell-item>
|
|
||||||
</u-cell-group>
|
|
||||||
</view> -->
|
|
||||||
|
|
||||||
<view class="u-m-t-20">
|
<view class="u-m-t-20">
|
||||||
<u-cell-group>
|
<u-cell-group>
|
||||||
<u-cell-item icon="weixin-fill" title="绑定微信" :arrow="false" @click="bindMP" v-if="!vuex_user.wxmp_openid"></u-cell-item>
|
<u-cell-item icon="weixin-fill" title="绑定微信" :arrow="false" @click="bindMP" v-if="!vuex_user.wxmp_openid"></u-cell-item>
|
||||||
|
<u-cell-item icon="list-dot" title="考试记录" @click="examRecord"></u-cell-item>
|
||||||
<u-cell-item icon="close" title="退出" @click="Logout"></u-cell-item>
|
<u-cell-item icon="close" title="退出" @click="Logout"></u-cell-item>
|
||||||
</u-cell-group>
|
</u-cell-group>
|
||||||
</view>
|
</view>
|
||||||
|
|
@ -83,7 +68,13 @@
|
||||||
}).catch(e=>{})
|
}).catch(e=>{})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
|
examRecord(){
|
||||||
|
uni.navigateTo({
|
||||||
|
url:'/pages/exam/record'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
</video>
|
</video>
|
||||||
<view style="color:darkblue;margin-left:4upx">
|
<view style="color:darkblue;margin-left:4upx">
|
||||||
<text>{{video.name}}</text>
|
<text>{{video.name}}</text>
|
||||||
<text style="float:right;color:gray;margin-right: 8upx;">{{video.viewsp}}人观看</text>
|
<text style="float:right;color:gray;margin-right: 8upx;">{{video.views}}次播放</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// uni.scss中引入的样式会同时混入到全局样式文件和单独每一个页面的样式中,造成微信程序包太大,
|
// uni.scss中引入的样式会同时混入到全局样式文件和单独每一个页面的样式中,造成微信程序包太大,
|
||||||
// 故uni.scss只建议放scss变量名相关样式,其他的样式可以通过main.js或者App.vue引入
|
// 故uni.scss只建议放scss变量名相关样式,其他的样式可以通过main.js或者App.vue引入
|
||||||
|
|
||||||
|
$theme-color:#0080d1;
|
||||||
$u-main-color: #303133;
|
$u-main-color: #303133;
|
||||||
$u-content-color: #606266;
|
$u-content-color: #606266;
|
||||||
$u-tips-color: #909399;
|
$u-tips-color: #909399;
|
||||||
|
|
@ -36,3 +37,9 @@ $u-type-info-light: #f4f4f5;
|
||||||
|
|
||||||
$u-form-item-height: 70rpx;
|
$u-form-item-height: 70rpx;
|
||||||
$u-form-item-border-color: #dcdfe6;
|
$u-form-item-border-color: #dcdfe6;
|
||||||
|
|
||||||
|
/* 文字尺寸 */
|
||||||
|
$u-font-size-sm:24rpx;
|
||||||
|
$u-font-size-lg:32rpx;
|
||||||
|
$u-font-size-base:28rpx;
|
||||||
|
$u-font-size-title:36rpx;
|
||||||
|
|
@ -19,6 +19,7 @@ from apps.supervision.models import Content, Record
|
||||||
from apps.supervision.serializers import ContentSerializer, RecordCreateSerializer, RecordSerializer
|
from apps.supervision.serializers import ContentSerializer, RecordCreateSerializer, RecordSerializer
|
||||||
from apps.system.mixins import CreateUpdateCustomMixin
|
from apps.system.mixins import CreateUpdateCustomMixin
|
||||||
from utils.queryset import get_child_queryset2
|
from utils.queryset import get_child_queryset2
|
||||||
|
from apps.system.permission_data import RbacFilterSet
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from apps.supervision.permission import RecordPermission
|
from apps.supervision.permission import RecordPermission
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
|
@ -60,7 +61,7 @@ class AbilityContentViewSet(CreateUpdateCustomMixin, ModelViewSet):
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
serializer.save(update_by = self.request.user)
|
serializer.save(update_by = self.request.user)
|
||||||
|
|
||||||
class AbilityRecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
|
class AbilityRecordViewSet(RbacFilterSet, PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
|
||||||
perms_map = {'get': '*', 'post': '*',
|
perms_map = {'get': '*', 'post': '*',
|
||||||
'put': '*', 'delete': '*'}
|
'put': '*', 'delete': '*'}
|
||||||
queryset = Record.objects.filter(content__cate=2)
|
queryset = Record.objects.filter(content__cate=2)
|
||||||
|
|
@ -70,25 +71,6 @@ class AbilityRecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
|
||||||
ordering = ['-task', 'content__sortnum', '-create_time']
|
ordering = ['-task', 'content__sortnum', '-create_time']
|
||||||
filterset_fields = ['content','content__cate', 'belong_dept', 'state']
|
filterset_fields = ['content','content__cate', 'belong_dept', 'state']
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = self.queryset
|
|
||||||
if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
|
|
||||||
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
pass
|
|
||||||
if hasattr(queryset.model, 'belong_dept'):
|
|
||||||
user = self.request.user
|
|
||||||
roles = user.roles
|
|
||||||
data_range = roles.values_list('datas', flat=True)
|
|
||||||
if '全部' in data_range:
|
|
||||||
pass
|
|
||||||
elif '本级及以下' in data_range:
|
|
||||||
belong_depts = get_child_queryset2(user.dept)
|
|
||||||
queryset = queryset.filter(belong_dept__in = belong_depts)
|
|
||||||
elif '本级' in data_range:
|
|
||||||
queryset = queryset.filter(belong_dept = user.dept)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
if not self.request.query_params.get('pageoff', None):
|
if not self.request.query_params.get('pageoff', None):
|
||||||
queryset = queryset.exclude(state='待发布')
|
queryset = queryset.exclude(state='待发布')
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ from utils.pagination import PageOrNot
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
|
from utils.queryset import get_child_queryset2
|
||||||
|
from apps.system.permission_data import RbacFilterSet
|
||||||
|
|
||||||
|
|
||||||
class QualiLibViewSet(PageOrNot, ListModelMixin, GenericViewSet):
|
class QualiLibViewSet(PageOrNot, ListModelMixin, GenericViewSet):
|
||||||
perms_map = {'get': '*'}
|
perms_map = {'get': '*'}
|
||||||
|
|
@ -20,21 +23,24 @@ class QualiLibViewSet(PageOrNot, ListModelMixin, GenericViewSet):
|
||||||
search_fields = ['name']
|
search_fields = ['name']
|
||||||
ordering = ['-create_time']
|
ordering = ['-create_time']
|
||||||
|
|
||||||
|
|
||||||
class QualiViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
class QualiViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||||
perms_map = {'get': '*'}
|
perms_map = {'get': '*'}
|
||||||
queryset = Quali.objects.select_related('org', 'province', 'city', 'file').prefetch_related('citys')
|
queryset = Quali.objects.select_related(
|
||||||
|
'org', 'province', 'city', 'file').prefetch_related('citys')
|
||||||
serializer_class = QualiListSerializer
|
serializer_class = QualiListSerializer
|
||||||
search_fields = ['name', 'type', 'grade', 'scope', 'level', 'description']
|
search_fields = ['name', 'type', 'grade', 'scope', 'level', 'description']
|
||||||
ordering = ['org', 'org__sort', 'create_time']
|
ordering = ['org', 'org__sort', 'create_time']
|
||||||
filterset_fields = ['org', 'type', 'grade', 'province', 'city', 'citys']
|
filterset_fields = ['org', 'type', 'grade', 'province', 'city', 'citys']
|
||||||
|
|
||||||
@action(methods=['get'], detail=False, perms_map = {'get':'*'})
|
@action(methods=['get'], detail=False, perms_map={'get': '*'})
|
||||||
def my(self, request, *args, **kwargs):
|
def my(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
我的资质
|
我的资质
|
||||||
"""
|
"""
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
queryset = self.filter_queryset(self.get_queryset().filter(org=user.dept))
|
queryset = self.filter_queryset(
|
||||||
|
self.get_queryset().filter(org=user.dept))
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
page = self.paginate_queryset(queryset)
|
||||||
if page is not None:
|
if page is not None:
|
||||||
|
|
@ -44,8 +50,10 @@ class QualiViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class QTaskViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
class QTaskViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet):
|
||||||
perms_map = {'get': 'qtask_view', 'post': 'qtask_create', 'put': 'qtask_update'}
|
perms_map = {'get': 'qtask_view',
|
||||||
|
'post': 'qtask_create', 'put': 'qtask_update'}
|
||||||
queryset = QTask.objects.all()
|
queryset = QTask.objects.all()
|
||||||
serializer_class = QTaskListSerializer
|
serializer_class = QTaskListSerializer
|
||||||
ordering = ['-create_time']
|
ordering = ['-create_time']
|
||||||
|
|
@ -54,8 +62,8 @@ class QTaskViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, Up
|
||||||
if self.action in ['create', 'update']:
|
if self.action in ['create', 'update']:
|
||||||
return QTaskCreateUpdateSerializer
|
return QTaskCreateUpdateSerializer
|
||||||
return super().get_serializer_class()
|
return super().get_serializer_class()
|
||||||
|
|
||||||
@action(methods=['put'], detail=True, perms_map = {'put':'qtask_start'}, serializer_class=serializers.Serializer)
|
@action(methods=['put'], detail=True, perms_map={'put': 'qtask_start'}, serializer_class=serializers.Serializer)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def start(self, request, *args, **kwargs):
|
def start(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -68,6 +76,7 @@ class QTaskViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, Up
|
||||||
return Response()
|
return Response()
|
||||||
return Response('任务状态错误', status=status.HTTP_400_BAD_REQUEST)
|
return Response('任务状态错误', status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
class QOrgViewSet(ListModelMixin, GenericViewSet):
|
class QOrgViewSet(ListModelMixin, GenericViewSet):
|
||||||
perms_map = {'get': 'qtask_view'}
|
perms_map = {'get': 'qtask_view'}
|
||||||
queryset = QOrg.objects.select_related('qtask', 'org')
|
queryset = QOrg.objects.select_related('qtask', 'org')
|
||||||
|
|
@ -75,22 +84,27 @@ class QOrgViewSet(ListModelMixin, GenericViewSet):
|
||||||
serializer_class = QOrgListSerializer
|
serializer_class = QOrgListSerializer
|
||||||
ordering = ['-create_time']
|
ordering = ['-create_time']
|
||||||
|
|
||||||
@action(methods=['get'], detail=False, perms_map = {'get':'qtask_my'})
|
@action(methods=['get'], detail=False, perms_map={'get': 'qtask_my'})
|
||||||
def my(self, request, *args, **kwargs):
|
def my(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
我的报送任务
|
我的报送任务
|
||||||
"""
|
"""
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
queryset = self.filter_queryset(self.get_queryset().filter(org=user.dept, qtask__is_deleted=False).exclude(qtask__state='待发布'))
|
queryset = self.filter_queryset(self.get_queryset().filter(qtask__is_deleted=False).exclude(qtask__state='待发布'))
|
||||||
|
mydept = user.dept
|
||||||
|
belong_depts = get_child_queryset2(mydept)
|
||||||
|
queryset = queryset.filter(org__in = belong_depts)
|
||||||
|
deptId = self.request.query_params.get('org', mydept.id)
|
||||||
|
if deptId:
|
||||||
|
queryset = queryset.filter(org__id=deptId)
|
||||||
page = self.paginate_queryset(queryset)
|
page = self.paginate_queryset(queryset)
|
||||||
if page is not None:
|
if page is not None:
|
||||||
serializer = self.get_serializer(page, many=True)
|
serializer = self.get_serializer(page, many=True)
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
def cal_count(qtask, org):
|
def cal_count(qtask, org):
|
||||||
qs = QAction.objects.filter(qtask=qtask, belong_dept=org)
|
qs = QAction.objects.filter(qtask=qtask, belong_dept=org)
|
||||||
qorg = QOrg.objects.get(qtask=qtask, org=org)
|
qorg = QOrg.objects.get(qtask=qtask, org=org)
|
||||||
|
|
@ -98,9 +112,11 @@ def cal_count(qtask, org):
|
||||||
qorg.count_confirmed = qs.filter(confirmed=True).count()
|
qorg.count_confirmed = qs.filter(confirmed=True).count()
|
||||||
qorg.save()
|
qorg.save()
|
||||||
|
|
||||||
class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModelMixin,GenericViewSet):
|
|
||||||
|
class QActionViewSet(RbacFilterSet, PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||||
perms_map = {'get': '*', 'delete': 'qaction_delete'}
|
perms_map = {'get': '*', 'delete': 'qaction_delete'}
|
||||||
queryset = QAction.objects.select_related('file', 'atype', 'afield', 'qtask', 'belong_dept', 'create_by')
|
queryset = QAction.objects.select_related(
|
||||||
|
'file', 'atype', 'afield', 'qtask', 'belong_dept', 'create_by')
|
||||||
filterset_fields = ['qtask', 'belong_dept', 'atype', 'afield']
|
filterset_fields = ['qtask', 'belong_dept', 'atype', 'afield']
|
||||||
serializer_class = QActionListSerializer
|
serializer_class = QActionListSerializer
|
||||||
|
|
||||||
|
|
@ -112,22 +128,24 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
return super().get_serializer_class()
|
return super().get_serializer_class()
|
||||||
|
|
||||||
|
|
||||||
@action(methods=['get'], detail=False, perms_map = {'get':'qaction_my'})
|
|
||||||
def my(self, request, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
我的报送操作
|
|
||||||
"""
|
|
||||||
user = self.request.user
|
|
||||||
queryset = self.filter_queryset(self.get_queryset().filter(belong_dept=user.dept))
|
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
# @action(methods=['get'], detail=False, perms_map={'get': '*'})
|
||||||
if page is not None:
|
# def my(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(page, many=True)
|
# """
|
||||||
return self.get_paginated_response(serializer.data)
|
# 我的报送操作
|
||||||
|
# """
|
||||||
|
# user = self.request.user
|
||||||
|
# queryset = self.filter_queryset(
|
||||||
|
# self.get_queryset().filter(belong_dept=user.dept))
|
||||||
|
|
||||||
|
# page = self.paginate_queryset(queryset)
|
||||||
|
# if page is not None:
|
||||||
|
# serializer = self.get_serializer(page, many=True)
|
||||||
|
# return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
# serializer = self.get_serializer(queryset, many=True)
|
||||||
|
# return Response(serializer.data)
|
||||||
|
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
if not instance.confirmed:
|
if not instance.confirmed:
|
||||||
|
|
@ -137,7 +155,7 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
raise ParseError('报送已确认, 不可删除')
|
raise ParseError('报送已确认, 不可删除')
|
||||||
|
|
||||||
@action(methods=['post'], detail=False,
|
@action(methods=['post'], detail=False,
|
||||||
perms_map = {'post':'qaction_create'}, serializer_class=QActionServiceSerializer)
|
perms_map={'post': 'qaction_create'}, serializer_class=QActionServiceSerializer)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def service_update(self, request, *args, **kwargs):
|
def service_update(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -152,7 +170,7 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['post'], detail=False,
|
@action(methods=['post'], detail=False,
|
||||||
perms_map = {'post':'qaction_create'}, serializer_class=QActionQualiCreateSerializer)
|
perms_map={'post': 'qaction_create'}, serializer_class=QActionQualiCreateSerializer)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def quali_create(self, request, *args, **kwargs):
|
def quali_create(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -167,7 +185,7 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['post'], detail=False,
|
@action(methods=['post'], detail=False,
|
||||||
perms_map = {'post':'qaction_create'}, serializer_class=QActionQualiUpdateSerializer)
|
perms_map={'post': 'qaction_create'}, serializer_class=QActionQualiUpdateSerializer)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def quali_update(self, request, *args, **kwargs):
|
def quali_update(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -186,22 +204,25 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
obj.save()
|
obj.save()
|
||||||
for k, v in value2.items():
|
for k, v in value2.items():
|
||||||
if v != old_data[k]:
|
if v != old_data[k]:
|
||||||
QActionItem.objects.create(action='update', field=k, value1=old_data[k], value2=v, qaction=obj)
|
QActionItem.objects.create(
|
||||||
|
action='update', field=k, value1=old_data[k], value2=v, qaction=obj)
|
||||||
if k == 'citys':
|
if k == 'citys':
|
||||||
old_citys = set(old_data['citys'])
|
old_citys = set(old_data['citys'])
|
||||||
new_citys = set(v)
|
new_citys = set(v)
|
||||||
removes = old_citys.difference(new_citys)
|
removes = old_citys.difference(new_citys)
|
||||||
adds = new_citys.difference(old_citys)
|
adds = new_citys.difference(old_citys)
|
||||||
for i in removes:
|
for i in removes:
|
||||||
QActionItem.objects.create(action='city:remove', field='citys', city=City.objects.get(id=i), qaction=obj)
|
QActionItem.objects.create(
|
||||||
|
action='city:remove', field='citys', city=City.objects.get(id=i), qaction=obj)
|
||||||
for i in adds:
|
for i in adds:
|
||||||
QActionItem.objects.create(action='city:add', field='citys', city=City.objects.get(id=i), qaction=obj)
|
QActionItem.objects.create(
|
||||||
|
action='city:add', field='citys', city=City.objects.get(id=i), qaction=obj)
|
||||||
|
|
||||||
cal_count(vdata['qtask'], user.dept)
|
cal_count(vdata['qtask'], user.dept)
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['post'], detail=False,
|
@action(methods=['post'], detail=False,
|
||||||
perms_map = {'post':'qaction_create'}, serializer_class=QActionACreateSerializer)
|
perms_map={'post': 'qaction_create'}, serializer_class=QActionACreateSerializer)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def ability_create(self, request, *args, **kwargs):
|
def ability_create(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -216,7 +237,7 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['post'], detail=False,
|
@action(methods=['post'], detail=False,
|
||||||
perms_map = {'post':'qaction_create'}, serializer_class=QActionNoChangeSerializer)
|
perms_map={'post': 'qaction_create'}, serializer_class=QActionNoChangeSerializer)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def ability_nochange(self, request, *args, **kwargs):
|
def ability_nochange(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -225,13 +246,14 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
user = request.user
|
user = request.user
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save(create_by=user, belong_dept=user.dept, action='ability:nochange')
|
serializer.save(create_by=user, belong_dept=user.dept,
|
||||||
|
action='ability:nochange')
|
||||||
vdata = serializer.validated_data
|
vdata = serializer.validated_data
|
||||||
cal_count(vdata['qtask'], user.dept)
|
cal_count(vdata['qtask'], user.dept)
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
@action(methods=['post'], detail=False,
|
@action(methods=['post'], detail=False,
|
||||||
perms_map = {'post':'qaction_create'}, serializer_class=QActionNoChangeSerializer)
|
perms_map={'post': 'qaction_create'}, serializer_class=QActionNoChangeSerializer)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def quali_nochange(self, request, *args, **kwargs):
|
def quali_nochange(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -240,13 +262,13 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
user = request.user
|
user = request.user
|
||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save(create_by=user, belong_dept=user.dept, action='quali:nochange')
|
serializer.save(create_by=user, belong_dept=user.dept,
|
||||||
|
action='quali:nochange')
|
||||||
vdata = serializer.validated_data
|
vdata = serializer.validated_data
|
||||||
cal_count(vdata['qtask'], user.dept)
|
cal_count(vdata['qtask'], user.dept)
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
@action(methods=['put'], detail=True, perms_map={'put': 'qaction_confirm'},
|
||||||
@action(methods=['put'], detail=True, perms_map = {'put':'qaction_confirm'},
|
|
||||||
serializer_class=serializers.Serializer)
|
serializer_class=serializers.Serializer)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def confirm(self, request, *args, **kwargs):
|
def confirm(self, request, *args, **kwargs):
|
||||||
|
|
@ -263,9 +285,11 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
elif obj.action == 'quali:create':
|
elif obj.action == 'quali:create':
|
||||||
serializer = QualiCreateSerializer(data=obj.value2)
|
serializer = QualiCreateSerializer(data=obj.value2)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
instance = serializer.save(org=obj.belong_dept, create_by=obj.create_by)
|
instance = serializer.save(
|
||||||
|
org=obj.belong_dept, create_by=obj.create_by)
|
||||||
if instance.type == 'OTHER':
|
if instance.type == 'OTHER':
|
||||||
qualiLib, _ = QualiLib.objects.get_or_create(name=instance.name)
|
qualiLib, _ = QualiLib.objects.get_or_create(
|
||||||
|
name=instance.name)
|
||||||
levels = qualiLib.levels
|
levels = qualiLib.levels
|
||||||
levels.append(instance.level)
|
levels.append(instance.level)
|
||||||
le = list(set(levels))
|
le = list(set(levels))
|
||||||
|
|
@ -284,8 +308,3 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
|
||||||
obj.save()
|
obj.save()
|
||||||
cal_count(obj.qtask, obj.belong_dept)
|
cal_count(obj.qtask, obj.belong_dept)
|
||||||
return Response(status=status.HTTP_200_OK)
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ExamConfig(AppConfig):
|
||||||
|
name = 'exam'
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from openpyxl.workbook import Workbook
|
||||||
|
from django.conf import settings
|
||||||
|
from datetime import datetime
|
||||||
|
from openpyxl.styles import Font, Fill
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
def export_question(questions):
|
||||||
|
'''
|
||||||
|
params: serializer questions
|
||||||
|
return: xlsx path
|
||||||
|
'''
|
||||||
|
wb = Workbook()
|
||||||
|
ws1 = wb.active
|
||||||
|
ws1.title = '题目表'
|
||||||
|
ws1.append(['分类','题型', '题干', '选项', '正确答案', '解析'])
|
||||||
|
row = ws1.row_dimensions[1]
|
||||||
|
row.font = Font(bold=True)
|
||||||
|
for i in questions:
|
||||||
|
ws1.append([i.questioncat.name, i.type, i.name, json.dumps(i.options, ensure_ascii=False), ''.join(sorted(i.right)), i.resolution])
|
||||||
|
filename = 'questions' + datetime.now().strftime("%Y%m%d%H%M%S") +'.xlsx'
|
||||||
|
path = '/media/temp/'
|
||||||
|
full_path = settings.BASE_DIR + '/media/temp/'
|
||||||
|
if not os.path.exists(full_path):
|
||||||
|
os.makedirs(full_path)
|
||||||
|
wb.save(full_path+filename)
|
||||||
|
return path + filename
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
from django_filters import rest_framework as filters
|
||||||
|
from .models import ExamRecord, Exam
|
||||||
|
class ExamRecordFilter(filters.FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = ExamRecord
|
||||||
|
fields = {
|
||||||
|
'start_time': ['exact', 'gte', 'lte'],
|
||||||
|
'is_pass': ['exact'],
|
||||||
|
'type': ['exact'],
|
||||||
|
'is_submited': ['exact'],
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExamFilter(filters.FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Exam
|
||||||
|
fields = {
|
||||||
|
'close_time': ['exact', 'gte', 'lte'],
|
||||||
|
'paper': ['exact'],
|
||||||
|
'code': ['exact'],
|
||||||
|
'is_open': ['exact']
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-10-31 02:26
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Questioncat',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='名称')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questioncat_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questioncat_parent', to='exam.Questioncat', verbose_name='父')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questioncat_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '题目分类',
|
||||||
|
'verbose_name_plural': '题目分类',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Question',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('name', models.TextField(verbose_name='题干')),
|
||||||
|
('img', models.CharField(blank=True, max_length=1000, null=True, verbose_name='题干图片')),
|
||||||
|
('type', models.CharField(choices=[('单选', '单选'), ('多选', '多选'), ('判断', '判断')], default='单选', max_length=50, verbose_name='题型')),
|
||||||
|
('level', models.CharField(choices=[('低', '低'), ('中', '中'), ('高', '高')], default='低', max_length=50, verbose_name='难度')),
|
||||||
|
('options', django.contrib.postgres.fields.jsonb.JSONField(verbose_name='选项')),
|
||||||
|
('right', django.contrib.postgres.fields.jsonb.JSONField(verbose_name='正确答案')),
|
||||||
|
('resolution', models.TextField(blank=True, verbose_name='解析')),
|
||||||
|
('enabled', models.BooleanField(default=False, verbose_name='是否启用')),
|
||||||
|
('year', models.IntegerField(blank=True, null=True, verbose_name='真题年份')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='question_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('questioncat', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questioncat', to='exam.Questioncat', verbose_name='所属题库')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='question_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '题目',
|
||||||
|
'verbose_name_plural': '题目',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-11-07 05:56
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('exam', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AnswerDetail',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('user_answer', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True)),
|
||||||
|
('score', models.FloatField(default=0, verbose_name='本题得分')),
|
||||||
|
('is_right', models.BooleanField(default=False, verbose_name='是否正确')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '答题记录',
|
||||||
|
'verbose_name_plural': '答题记录',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Exam',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('code', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='考试编号')),
|
||||||
|
('name', models.CharField(max_length=100, verbose_name='名称')),
|
||||||
|
('place', models.CharField(blank=True, max_length=100, null=True, verbose_name='考试地点')),
|
||||||
|
('open_time', models.DateTimeField(blank=True, null=True, verbose_name='开启时间')),
|
||||||
|
('close_time', models.DateTimeField(blank=True, null=True, verbose_name='关闭时间')),
|
||||||
|
('proctor_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='监考人姓名')),
|
||||||
|
('proctor_phone', models.CharField(blank=True, max_length=100, null=True, verbose_name='监考人联系方式')),
|
||||||
|
('chance', models.IntegerField(default=3, verbose_name='考试机会')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='exam_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Paper',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='名称')),
|
||||||
|
('limit', models.IntegerField(default=0, verbose_name='限时(分钟)')),
|
||||||
|
('total_score', models.FloatField(default=0, verbose_name='满分')),
|
||||||
|
('pass_score', models.FloatField(default=0, verbose_name='通过分数')),
|
||||||
|
('danxuan_count', models.IntegerField(default=0, verbose_name='单选数量')),
|
||||||
|
('danxuan_score', models.FloatField(default=2, verbose_name='单选分数')),
|
||||||
|
('duoxuan_count', models.IntegerField(default=0, verbose_name='多选数量')),
|
||||||
|
('duoxuan_score', models.FloatField(default=4, verbose_name='多选分数')),
|
||||||
|
('panduan_count', models.IntegerField(default=0, verbose_name='判断数量')),
|
||||||
|
('panduan_score', models.FloatField(default=2, verbose_name='判断分数')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paper_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '押题卷',
|
||||||
|
'verbose_name_plural': '押题卷',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PaperQuestion',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('total_score', models.FloatField(default=0, verbose_name='单题满分')),
|
||||||
|
('sort', models.PositiveSmallIntegerField(default=1)),
|
||||||
|
('paper', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Paper', verbose_name='试卷')),
|
||||||
|
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Question', verbose_name='试题')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='paper',
|
||||||
|
name='questions',
|
||||||
|
field=models.ManyToManyField(through='exam.PaperQuestion', to='exam.Question'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='paper',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paper_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExamRecord',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('name', models.CharField(max_length=200, verbose_name='名称')),
|
||||||
|
('type', models.CharField(choices=[('自助模考', '自助模考'), ('押卷模考', '押卷模考'), ('正式考试', '正式考试')], default='自助模考', max_length=50, verbose_name='考试类型')),
|
||||||
|
('limit', models.IntegerField(default=0, verbose_name='限时(分钟)')),
|
||||||
|
('total_score', models.FloatField(default=0, verbose_name='总分')),
|
||||||
|
('score', models.FloatField(default=0, verbose_name='得分')),
|
||||||
|
('took', models.IntegerField(default=0, verbose_name='耗时(秒)')),
|
||||||
|
('start_time', models.DateTimeField(verbose_name='开始答题时间')),
|
||||||
|
('end_time', models.DateTimeField(verbose_name='结束答题时间')),
|
||||||
|
('is_pass', models.BooleanField(default=True, verbose_name='是否通过')),
|
||||||
|
('questions', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=list, verbose_name='下发的题目列表')),
|
||||||
|
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='examrecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
|
||||||
|
('detail', models.ManyToManyField(through='exam.AnswerDetail', to='exam.Question', verbose_name='答题记录')),
|
||||||
|
('exam', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='exam.Exam', verbose_name='关联的正式考试')),
|
||||||
|
('paper', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='exam.Paper', verbose_name='所用试卷')),
|
||||||
|
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='examrecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '考试记录',
|
||||||
|
'verbose_name_plural': '考试记录',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='paper',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='exam.Paper', verbose_name='使用的试卷'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='update_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='exam_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='answerdetail',
|
||||||
|
name='examrecord',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.ExamRecord'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='answerdetail',
|
||||||
|
name='question',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='exam.Question'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-11-08 01:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('exam', '0002_auto_20221107_1356'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='examrecord',
|
||||||
|
name='questions',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='paperquestion',
|
||||||
|
name='sort',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='answerdetail',
|
||||||
|
name='total_score',
|
||||||
|
field=models.FloatField(default=0, verbose_name='该题满分'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='examrecord',
|
||||||
|
name='is_submited',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-11-14 03:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('exam', '0003_auto_20221108_0901'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='examrecord',
|
||||||
|
name='end_time',
|
||||||
|
field=models.DateTimeField(blank=True, null=True, verbose_name='结束答题时间'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-11-22 02:14
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('exam', '0004_auto_20221114_1108'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='exam',
|
||||||
|
name='is_open',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='是否公开'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
from django.db import models
|
||||||
|
from apps.system.models import CommonAModel
|
||||||
|
from django.contrib.postgres.fields import JSONField
|
||||||
|
from utils.model import BaseModel
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
class Questioncat(CommonAModel):
|
||||||
|
name = models.CharField(max_length=200, verbose_name='名称')
|
||||||
|
parent = models.ForeignKey('self', verbose_name='父', null=True, blank=True, on_delete=models.CASCADE, related_name='questioncat_parent')
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '题目分类'
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tmtotal(self):
|
||||||
|
return self.questioncat.count()
|
||||||
|
|
||||||
|
|
||||||
|
class Question(CommonAModel):
|
||||||
|
type_choices = (
|
||||||
|
('单选', '单选'),
|
||||||
|
('多选', '多选'),
|
||||||
|
('判断', '判断'),
|
||||||
|
)
|
||||||
|
level_choices = (
|
||||||
|
('低', '低'),
|
||||||
|
('中', '中'),
|
||||||
|
('高', '高'),
|
||||||
|
)
|
||||||
|
name = models.TextField(verbose_name='题干')
|
||||||
|
img = models.CharField(max_length=1000, null=True, blank=True, verbose_name='题干图片')
|
||||||
|
type = models.CharField(max_length=50, default='单选', choices=type_choices, verbose_name='题型')
|
||||||
|
level = models.CharField(max_length=50, default='低', choices=level_choices, verbose_name='难度')
|
||||||
|
questioncat = models.ForeignKey(Questioncat, blank=True, null=True, on_delete=models.SET_NULL, verbose_name='所属题库', related_name='questioncat')
|
||||||
|
options = JSONField(verbose_name='选项')
|
||||||
|
right = JSONField(verbose_name='正确答案')
|
||||||
|
resolution = models.TextField(verbose_name='解析', blank=True)
|
||||||
|
enabled = models.BooleanField('是否启用', default=False)
|
||||||
|
year = models.IntegerField('真题年份', null=True, blank=True)
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '题目'
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class Paper(CommonAModel):
|
||||||
|
name = models.CharField(max_length=200, verbose_name='名称')
|
||||||
|
questions = models.ManyToManyField(Question, through='PaperQuestion')
|
||||||
|
limit = models.IntegerField(default=0, verbose_name='限时(分钟)')
|
||||||
|
total_score = models.FloatField(default=0, verbose_name='满分')
|
||||||
|
pass_score = models.FloatField(default=0, verbose_name='通过分数')
|
||||||
|
danxuan_count = models.IntegerField(default=0, verbose_name='单选数量')
|
||||||
|
danxuan_score = models.FloatField(default=2, verbose_name='单选分数')
|
||||||
|
duoxuan_count = models.IntegerField(default=0, verbose_name='多选数量')
|
||||||
|
duoxuan_score = models.FloatField(default=4, verbose_name='多选分数')
|
||||||
|
panduan_count = models.IntegerField(default=0, verbose_name='判断数量')
|
||||||
|
panduan_score = models.FloatField(default=2, verbose_name='判断分数')
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '押题卷'
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class PaperQuestion(BaseModel):
|
||||||
|
paper = models.ForeignKey(Paper, on_delete=models.CASCADE, verbose_name='试卷')
|
||||||
|
question = models.ForeignKey(Question, on_delete=models.CASCADE, verbose_name='试题')
|
||||||
|
total_score = models.FloatField(default=0, verbose_name='单题满分')
|
||||||
|
|
||||||
|
|
||||||
|
class Exam(CommonAModel):
|
||||||
|
"""
|
||||||
|
组织的正式考试
|
||||||
|
"""
|
||||||
|
code = models.CharField('考试编号', max_length=100, null=True, blank=True, unique=True)
|
||||||
|
name = models.CharField('名称', max_length=100)
|
||||||
|
place = models.CharField('考试地点', max_length=100, null=True, blank=True)
|
||||||
|
open_time = models.DateTimeField('开启时间', null=True, blank=True)
|
||||||
|
close_time = models.DateTimeField('关闭时间', null=True, blank=True)
|
||||||
|
proctor_name = models.CharField('监考人姓名', max_length=100, null=True, blank=True)
|
||||||
|
proctor_phone = models.CharField('监考人联系方式', max_length=100, null=True, blank=True)
|
||||||
|
chance = models.IntegerField('考试机会', default=3)
|
||||||
|
paper = models.ForeignKey(Paper, verbose_name='使用的试卷', on_delete=models.CASCADE, null=True, blank=True)
|
||||||
|
is_open = models.BooleanField('是否公开', default=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class ExamRecord(CommonAModel):
|
||||||
|
'''
|
||||||
|
考试记录表
|
||||||
|
'''
|
||||||
|
type_choices = (
|
||||||
|
('自助模考', '自助模考'),
|
||||||
|
('押卷模考', '押卷模考'),
|
||||||
|
('正式考试', '正式考试')
|
||||||
|
)
|
||||||
|
name = models.CharField(max_length=200, verbose_name='名称')
|
||||||
|
type = models.CharField(max_length=50, default='自助模考',choices = type_choices, verbose_name='考试类型')
|
||||||
|
limit = models.IntegerField(default=0, verbose_name='限时(分钟)')
|
||||||
|
paper = models.ForeignKey(Paper, on_delete=models.SET_NULL, verbose_name='所用试卷', null=True, blank=True)
|
||||||
|
total_score = models.FloatField(default=0, verbose_name='总分')
|
||||||
|
score = models.FloatField(default=0, verbose_name='得分')
|
||||||
|
took = models.IntegerField(default=0, verbose_name='耗时(秒)')
|
||||||
|
start_time = models.DateTimeField(verbose_name='开始答题时间')
|
||||||
|
end_time = models.DateTimeField(verbose_name='结束答题时间', null=True, blank=True)
|
||||||
|
detail = models.ManyToManyField(Question, verbose_name='答题记录', through='AnswerDetail')
|
||||||
|
is_pass = models.BooleanField(default=True, verbose_name='是否通过')
|
||||||
|
exam = models.ForeignKey(Exam, verbose_name='关联的正式考试', null=True, blank=True, on_delete= models.SET_NULL)
|
||||||
|
is_submited = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '考试记录'
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerDetail(BaseModel):
|
||||||
|
examrecord = models.ForeignKey(ExamRecord, on_delete=models.CASCADE)
|
||||||
|
total_score = models.FloatField(default=0, verbose_name='该题满分')
|
||||||
|
question = models.ForeignKey(Question, on_delete=models.CASCADE)
|
||||||
|
user_answer = JSONField(null=True,blank=True)
|
||||||
|
score = models.FloatField(default=0, verbose_name='本题得分')
|
||||||
|
is_right = models.BooleanField(default=False, verbose_name='是否正确')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '答题记录'
|
||||||
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
from rest_framework.serializers import ModelSerializer, CharField, Serializer, SerializerMethodField, FloatField
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from apps.exam.models import Question, Questioncat, Paper, Exam, PaperQuestion, ExamRecord, AnswerDetail
|
||||||
|
|
||||||
|
|
||||||
|
class QuestioncatSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Questioncat
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionSerializer(ModelSerializer):
|
||||||
|
questioncat_name = serializers.CharField(source='questioncat.name', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = Question
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class PaperSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Paper
|
||||||
|
exclude = ('questions',)
|
||||||
|
|
||||||
|
|
||||||
|
class PaperQuestionSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PaperQuestion
|
||||||
|
fields = ['question', 'total_score']
|
||||||
|
|
||||||
|
|
||||||
|
class PaperCreateUpdateSerializer(ModelSerializer):
|
||||||
|
questions_ = PaperQuestionSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Paper
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class PaperQuestionDetailSerializer(ModelSerializer):
|
||||||
|
name = serializers.ReadOnlyField(source='question.name')
|
||||||
|
options = serializers.ReadOnlyField(source='question.options')
|
||||||
|
right = serializers.ReadOnlyField(source='question.right')
|
||||||
|
type = serializers.ReadOnlyField(source='question.type')
|
||||||
|
img = serializers.ReadOnlyField(source='question.img')
|
||||||
|
questioncat_name = serializers.ReadOnlyField(
|
||||||
|
source='question.questioncat.name')
|
||||||
|
level = serializers.ReadOnlyField(source='question.level')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PaperQuestion
|
||||||
|
fields = ('id', 'name', 'options', 'right', 'type', 'level',
|
||||||
|
'total_score', 'questioncat_name', 'img', 'question')
|
||||||
|
|
||||||
|
|
||||||
|
class PaperQuestionShortSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = PaperQuestion
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class PaperDetailSerializer(ModelSerializer):
|
||||||
|
questions_ = SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Paper
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
def get_questions_(self, instance):
|
||||||
|
pqs = PaperQuestion.objects.filter(paper=instance)
|
||||||
|
return PaperQuestionDetailSerializer(pqs, many=True).data
|
||||||
|
|
||||||
|
|
||||||
|
class ExamCreateUpdateSerializer(ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Exam
|
||||||
|
fields = ['name', 'place', 'open_time',
|
||||||
|
'close_time', 'proctor_name', 'proctor_phone', 'chance', 'paper']
|
||||||
|
|
||||||
|
|
||||||
|
class ExamListSerializer(ModelSerializer):
|
||||||
|
create_by_name = CharField(source='create_by.name', read_only=True)
|
||||||
|
paper_ = PaperSerializer(source='paper', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Exam
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class ExamDetailSerializer(ModelSerializer):
|
||||||
|
create_by_name = CharField(source='create_by.name', read_only=True)
|
||||||
|
paper_ = PaperSerializer(source='paper', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Exam
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class ExamAttendSerializer(Serializer):
|
||||||
|
code = CharField(label="考试编号")
|
||||||
|
|
||||||
|
|
||||||
|
class ExamRecordListSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
考试列表序列化
|
||||||
|
"""
|
||||||
|
took_format = serializers.SerializerMethodField()
|
||||||
|
create_by_name = serializers.CharField(
|
||||||
|
source='create_by.name', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ExamRecord
|
||||||
|
exclude = ('detail',)
|
||||||
|
|
||||||
|
def get_took_format(self, obj):
|
||||||
|
m, s = divmod(obj.took, 60)
|
||||||
|
h, m = divmod(m, 60)
|
||||||
|
return "%02d:%02d:%02d" % (h, m, s)
|
||||||
|
|
||||||
|
|
||||||
|
class ExamRecordDetailSerializer(serializers.ModelSerializer):
|
||||||
|
"""
|
||||||
|
考试详情序列化
|
||||||
|
"""
|
||||||
|
took_format = serializers.SerializerMethodField()
|
||||||
|
questions_ = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ExamRecord
|
||||||
|
exclude = ('detail',)
|
||||||
|
|
||||||
|
def get_took_format(self, obj):
|
||||||
|
m, s = divmod(obj.took, 60)
|
||||||
|
h, m = divmod(m, 60)
|
||||||
|
return "%02d:%02d:%02d" % (h, m, s)
|
||||||
|
|
||||||
|
def get_questions_(self, obj):
|
||||||
|
objs = AnswerDetail.objects.select_related('question').filter(
|
||||||
|
examrecord=obj).order_by('id')
|
||||||
|
return AnswerDetailSerializer(instance=objs, many=True).data
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerDetailUpdateSerializer(serializers.Serializer):
|
||||||
|
id = serializers.CharField(label='下发ID')
|
||||||
|
user_answer = serializers.JSONField(label='作答')
|
||||||
|
|
||||||
|
|
||||||
|
class ExamRecordSubmitSerializer(serializers.ModelSerializer):
|
||||||
|
questions_ = AnswerDetailUpdateSerializer(many=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ExamRecord
|
||||||
|
fields = ['questions_']
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerDetailSerializer(ModelSerializer):
|
||||||
|
name = serializers.ReadOnlyField(source='question.name')
|
||||||
|
options = serializers.ReadOnlyField(source='question.options')
|
||||||
|
right = serializers.ReadOnlyField(source='question.right')
|
||||||
|
type = serializers.ReadOnlyField(source='question.type')
|
||||||
|
img = serializers.ReadOnlyField(source='question.img')
|
||||||
|
questioncat_name = serializers.ReadOnlyField(
|
||||||
|
source='question.questioncat.name')
|
||||||
|
level = serializers.ReadOnlyField(source='question.level')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AnswerDetail
|
||||||
|
fields = ['id', 'question', 'name', 'options', 'right', 'type', 'level',
|
||||||
|
'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right']
|
||||||
|
|
||||||
|
|
||||||
|
class AnswerDetailOutSerializer(ModelSerializer):
|
||||||
|
name = serializers.ReadOnlyField(source='question.name')
|
||||||
|
options = serializers.ReadOnlyField(source='question.options')
|
||||||
|
type = serializers.ReadOnlyField(source='question.type')
|
||||||
|
img = serializers.ReadOnlyField(source='question.img')
|
||||||
|
questioncat_name = serializers.ReadOnlyField(
|
||||||
|
source='question.questioncat.name')
|
||||||
|
level = serializers.ReadOnlyField(source='question.level')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AnswerDetail
|
||||||
|
fields = ['id', 'question', 'name', 'options', 'type', 'level',
|
||||||
|
'total_score', 'questioncat_name', 'img', 'user_answer', 'score', 'is_right']
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
|
from apps.exam.views import QuestionViewSet, QuestioncatViewSet, PaperViewSet, ExamViewSet, ExamRecordViewSet
|
||||||
|
|
||||||
|
API_BASE_URL = 'api/exam/'
|
||||||
|
HTML_BASE_URL = 'exam/'
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register('questioncat', QuestioncatViewSet, basename='questioncat')
|
||||||
|
router.register('question', QuestionViewSet, basename="question")
|
||||||
|
router.register('paper', PaperViewSet, basename="paper")
|
||||||
|
router.register('exam', ExamViewSet, basename="exam")
|
||||||
|
router.register('examrecord', ExamRecordViewSet, basename="examrecord")
|
||||||
|
urlpatterns = [
|
||||||
|
path(API_BASE_URL, include(router.urls))
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,421 @@
|
||||||
|
from django.shortcuts import render
|
||||||
|
from rest_framework.viewsets import ModelViewSet, GenericViewSet
|
||||||
|
from rest_framework.mixins import ListModelMixin, DestroyModelMixin, RetrieveModelMixin
|
||||||
|
from apps.exam.exports import export_question
|
||||||
|
from apps.exam.models import Question, Questioncat, PaperQuestion
|
||||||
|
from apps.exam.serializers import (QuestionSerializer, QuestioncatSerializer, PaperSerializer, ExamDetailSerializer, ExamRecordDetailSerializer, ExamListSerializer,
|
||||||
|
ExamCreateUpdateSerializer, ExamListSerializer, ExamRecordSubmitSerializer, PaperDetailSerializer, PaperCreateUpdateSerializer, AnswerDetailOutSerializer, ExamRecordListSerializer)
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
from openpyxl import Workbook, load_workbook
|
||||||
|
from django.conf import settings
|
||||||
|
from apps.exam.models import Paper, Exam, ExamRecord, AnswerDetail
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db import transaction
|
||||||
|
from rest_framework.serializers import Serializer
|
||||||
|
from datetime import datetime
|
||||||
|
from apps.exam.filters import ExamRecordFilter, ExamFilter
|
||||||
|
from datetime import timedelta
|
||||||
|
from apps.system.mixins import CreateUpdateCustomMixin
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
||||||
|
|
||||||
|
def enctry(s):
|
||||||
|
k = 'ez9z3a4m*$%srn9ve_t71yd!v+&xn9@0k(e(+l6#g1h=e5i4da'
|
||||||
|
encry_str = ""
|
||||||
|
for i, j in zip(s, k):
|
||||||
|
# i为字符,j为秘钥字符
|
||||||
|
temp = str(ord(i)+ord(j))+'_' # 加密字符 = 字符的Unicode码 + 秘钥的Unicode码
|
||||||
|
encry_str = encry_str + temp
|
||||||
|
return encry_str
|
||||||
|
|
||||||
|
|
||||||
|
# 解密
|
||||||
|
def dectry(p):
|
||||||
|
k = 'ez9z3a4m*$%srn9ve_t71yd!v+&xn9@0k(e(+l6#g1h=e5i4da'
|
||||||
|
dec_str = ""
|
||||||
|
for i, j in zip(p.split("_")[:-1], k):
|
||||||
|
# i 为加密字符,j为秘钥字符
|
||||||
|
# 解密字符 = (加密Unicode码字符 - 秘钥字符的Unicode码)的单字节字符
|
||||||
|
temp = chr(int(i) - ord(j))
|
||||||
|
dec_str = dec_str+temp
|
||||||
|
return dec_str
|
||||||
|
|
||||||
|
|
||||||
|
class QuestioncatViewSet(CreateUpdateCustomMixin, ModelViewSet):
|
||||||
|
perms_map = {'get': '*', 'post':'question', 'put':'question', 'delete':'question'}
|
||||||
|
queryset = Questioncat.objects.all()
|
||||||
|
serializer_class = QuestioncatSerializer
|
||||||
|
filterset_fields = ['parent']
|
||||||
|
search_fields = ['name']
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionViewSet(CreateUpdateCustomMixin, ModelViewSet):
|
||||||
|
perms_map = {'get': '*', 'post':'question', 'put':'question', 'delete':'question'}
|
||||||
|
queryset = Question.objects.all()
|
||||||
|
serializer_class = QuestionSerializer
|
||||||
|
filterset_fields = ['level', 'type', 'year']
|
||||||
|
search_fields = ['name', 'options', 'resolution']
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=False,
|
||||||
|
url_path='export', url_name='export_question', perms_map=[{'get': '*'}], serializer_class=Serializer)
|
||||||
|
def export_question(self, request):
|
||||||
|
"""
|
||||||
|
导出题目
|
||||||
|
|
||||||
|
导出题目
|
||||||
|
"""
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
path = export_question(queryset)
|
||||||
|
return Response({'path': path})
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=False, url_name='enable_question', perms_map={'post': 'question'}, serializer_class=Serializer)
|
||||||
|
def enable(self, request):
|
||||||
|
"""
|
||||||
|
启用题目
|
||||||
|
|
||||||
|
启用题目
|
||||||
|
"""
|
||||||
|
ids = request.data.get('ids', None)
|
||||||
|
if ids:
|
||||||
|
Question.objects.filter(pk__in=ids).update(enabled=True)
|
||||||
|
return Response(status=200)
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=False,
|
||||||
|
url_path='import', url_name='import_question', perms_map={'post': 'question'}, serializer_class=Serializer)
|
||||||
|
def import_question(self, request):
|
||||||
|
"""
|
||||||
|
导入题目
|
||||||
|
|
||||||
|
导入题目
|
||||||
|
"""
|
||||||
|
xlsxpath = request.data['path']
|
||||||
|
fullpath = settings.BASE_DIR + xlsxpath
|
||||||
|
wb = load_workbook(fullpath)
|
||||||
|
sheet = wb.worksheets[0]
|
||||||
|
qlist = ['A', 'B', 'C', 'D', 'E', 'F']
|
||||||
|
leveldict = {'低': '低', '中': '中', '高': '高'}
|
||||||
|
notinlist = []
|
||||||
|
# 验证文件内容
|
||||||
|
if sheet['a2'].value != '题目类型':
|
||||||
|
return Response({"error": "类型列错误!"})
|
||||||
|
if sheet['b2'].value != '分类':
|
||||||
|
return Response({"error": "分类列错误!"})
|
||||||
|
if sheet['c2'].value != '题目':
|
||||||
|
return Response({"error": "题目列错误!"})
|
||||||
|
questioncatdict = {}
|
||||||
|
questioncats = Questioncat.objects.all()
|
||||||
|
for i in questioncats:
|
||||||
|
questioncatdict[i.name] = i.id
|
||||||
|
i = 3
|
||||||
|
while sheet['c'+str(i)].value:
|
||||||
|
type = sheet['a'+str(i)].value.replace(' ', '')
|
||||||
|
questioncat = sheet['b'+str(i)].value
|
||||||
|
if questioncat:
|
||||||
|
questioncat = questioncat.replace(' ', '')
|
||||||
|
else:
|
||||||
|
return Response(str(i)+'行没有分类', status=400)
|
||||||
|
name = sheet['c'+str(i)].value
|
||||||
|
|
||||||
|
answer = {}
|
||||||
|
if sheet['d'+str(i)].value:
|
||||||
|
answer['A'] = sheet['d'+str(i)].value
|
||||||
|
if sheet['e'+str(i)].value:
|
||||||
|
answer['B'] = sheet['e'+str(i)].value
|
||||||
|
if sheet['f'+str(i)].value:
|
||||||
|
answer['C'] = sheet['f'+str(i)].value
|
||||||
|
if sheet['g'+str(i)].value:
|
||||||
|
answer['D'] = sheet['g'+str(i)].value
|
||||||
|
if sheet['h'+str(i)].value:
|
||||||
|
answer['E'] = sheet['h'+str(i)].value
|
||||||
|
if sheet['i'+str(i)].value:
|
||||||
|
answer['F'] = sheet['i'+str(i)].value
|
||||||
|
right = sheet['j'+str(i)].value
|
||||||
|
if right:
|
||||||
|
right = right.replace(' ', '')
|
||||||
|
else:
|
||||||
|
return Response(str(i)+'行没有答案', status=400)
|
||||||
|
resolution = sheet['k'+str(i)].value
|
||||||
|
level = sheet['l'+str(i)].value
|
||||||
|
year = sheet['m' + str(i)].value
|
||||||
|
if level:
|
||||||
|
level = level.replace(' ', '')
|
||||||
|
cateobj = None
|
||||||
|
if questioncat not in questioncatdict:
|
||||||
|
return Response(str(i)+"行不存在分类("+questioncat+")!请先新建", status=400)
|
||||||
|
else:
|
||||||
|
cateobj = Questioncat.objects.get(
|
||||||
|
id=questioncatdict[questioncat])
|
||||||
|
if type == '单选':
|
||||||
|
if Question.objects.filter(type='单选', name=name, year=year, options=answer, questioncat=cateobj).exists():
|
||||||
|
notinlist.append(i)
|
||||||
|
else:
|
||||||
|
if right in ['A', 'B', 'C', 'D', 'E', 'F']:
|
||||||
|
obj = Question()
|
||||||
|
obj.type = '单选'
|
||||||
|
if cateobj:
|
||||||
|
obj.questioncat = cateobj
|
||||||
|
obj.name = name
|
||||||
|
obj.options = answer
|
||||||
|
obj.right = right
|
||||||
|
obj.resolution = resolution if resolution else ''
|
||||||
|
obj.year = year if year else None
|
||||||
|
if level in leveldict:
|
||||||
|
obj.level = leveldict[level]
|
||||||
|
else:
|
||||||
|
obj.level = '低'
|
||||||
|
obj.save()
|
||||||
|
elif type == '多选':
|
||||||
|
right = list(right.strip())
|
||||||
|
if Question.objects.filter(type='多选', name=name, year=year, options=answer, questioncat=cateobj).exists():
|
||||||
|
notinlist.append(i)
|
||||||
|
else:
|
||||||
|
if [False for c in right if c not in qlist]:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
obj = Question()
|
||||||
|
obj.type = '多选'
|
||||||
|
obj.questioncat = cateobj
|
||||||
|
obj.name = name
|
||||||
|
obj.options = answer
|
||||||
|
obj.right = right
|
||||||
|
obj.resolution = resolution if resolution else ''
|
||||||
|
obj.year = year if year else None
|
||||||
|
if level in leveldict:
|
||||||
|
obj.level = leveldict[level]
|
||||||
|
else:
|
||||||
|
obj.level = '低'
|
||||||
|
obj.save()
|
||||||
|
elif type == '判断':
|
||||||
|
if right == 'A' or right == '对' or right == '正确':
|
||||||
|
right = 'A'
|
||||||
|
else:
|
||||||
|
right = 'B'
|
||||||
|
if Question.objects.filter(type='判断', name=name, is_delete=0, options={'A': '对', 'B': '错'}, questioncat=cateobj).exists():
|
||||||
|
notinlist.append(i)
|
||||||
|
else:
|
||||||
|
obj = Question()
|
||||||
|
obj.type = '判断'
|
||||||
|
obj.questioncat = cateobj
|
||||||
|
obj.name = name
|
||||||
|
obj.options = {'A': '对', 'B': '错'}
|
||||||
|
obj.right = right
|
||||||
|
obj.resolution = resolution if resolution else ''
|
||||||
|
obj.year = year if year else None
|
||||||
|
if level in leveldict:
|
||||||
|
obj.level = leveldict[level]
|
||||||
|
else:
|
||||||
|
obj.level = '低'
|
||||||
|
obj.save()
|
||||||
|
i = i + 1
|
||||||
|
return Response(notinlist, status=200)
|
||||||
|
|
||||||
|
|
||||||
|
class PaperViewSet(ModelViewSet):
|
||||||
|
"""
|
||||||
|
试卷增删改查
|
||||||
|
"""
|
||||||
|
perms_map = {'get': '*', 'post':'paper', 'put':'paper', 'delete':'paper'}
|
||||||
|
queryset = Paper.objects.all()
|
||||||
|
serializer_class = PaperSerializer
|
||||||
|
ordering = ['id']
|
||||||
|
search_fields = ('name',)
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action in ['retrieve']:
|
||||||
|
return PaperDetailSerializer
|
||||||
|
elif self.action in ['create', 'update']:
|
||||||
|
return PaperCreateUpdateSerializer
|
||||||
|
return super().get_serializer_class()
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
sr = PaperCreateUpdateSerializer(data=request.data)
|
||||||
|
sr.is_valid(raise_exception=True)
|
||||||
|
vdata = sr.validated_data
|
||||||
|
vdata['create_by'] = request.user
|
||||||
|
questions_ = vdata.pop('questions_')
|
||||||
|
paper = Paper.objects.create(**vdata)
|
||||||
|
q_list = []
|
||||||
|
for i in questions_:
|
||||||
|
q_list.append(PaperQuestion(question=i['question'], total_score=i['total_score'], paper=paper))
|
||||||
|
PaperQuestion.objects.bulk_create(q_list)
|
||||||
|
return Response(status=201)
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
# 有考试在执行,不可更新
|
||||||
|
now = timezone.now()
|
||||||
|
paper = self.get_object()
|
||||||
|
if Exam.objects.filter(close_time__gte=now, paper=paper).exists():
|
||||||
|
raise ParseError('存在考试,不可编辑')
|
||||||
|
sr = PaperCreateUpdateSerializer(instance=paper, data=request.data)
|
||||||
|
sr.is_valid(raise_exception=True)
|
||||||
|
vdata = sr.validated_data
|
||||||
|
questions_ = vdata.pop('questions_')
|
||||||
|
vdata['update_by'] = request.user
|
||||||
|
Paper.objects.filter(id=paper.id).update(**vdata)
|
||||||
|
q_list = []
|
||||||
|
for i in questions_:
|
||||||
|
q_list.append(PaperQuestion(question=i['question'], total_score=i['total_score'], paper=paper))
|
||||||
|
PaperQuestion.objects.filter(paper=paper).delete()
|
||||||
|
PaperQuestion.objects.bulk_create(q_list)
|
||||||
|
return Response()
|
||||||
|
|
||||||
|
|
||||||
|
class ExamViewSet(CreateUpdateCustomMixin, ModelViewSet):
|
||||||
|
perms_map = {'get': '*', 'post':'exam', 'put':'exam', 'delete':'exam'}
|
||||||
|
queryset = Exam.objects.all().select_related('paper', 'create_by')
|
||||||
|
ordering = ['-id']
|
||||||
|
search_fields = ('name',)
|
||||||
|
serializer_class = ExamListSerializer
|
||||||
|
filterset_class = ExamFilter
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action in ['create', 'update']:
|
||||||
|
return ExamCreateUpdateSerializer
|
||||||
|
elif self.action in ['retrieve']:
|
||||||
|
return ExamDetailSerializer
|
||||||
|
return super().get_serializer_class()
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
instance = self.get_object()
|
||||||
|
if ExamRecord.objects.filter(exam=instance).exists():
|
||||||
|
raise ParseError('存在考试记录,禁止删除')
|
||||||
|
instance.delete(soft=False)
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=Serializer, permission_classes = [IsAuthenticated])
|
||||||
|
@transaction.atomic
|
||||||
|
def start(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
开始考试
|
||||||
|
|
||||||
|
开始考试具体题目信息
|
||||||
|
"""
|
||||||
|
exam = self.get_object()
|
||||||
|
now = timezone.now()
|
||||||
|
if now < exam.open_time or now > exam.close_time:
|
||||||
|
raise ParseError('不在考试时间范围')
|
||||||
|
tests = ExamRecord.objects.filter(
|
||||||
|
exam=exam, create_by=request.user)
|
||||||
|
chance_used = tests.count()
|
||||||
|
if chance_used > exam.chance:
|
||||||
|
raise ParseError('考试机会已用完')
|
||||||
|
if exam.paper:
|
||||||
|
er = ExamRecord()
|
||||||
|
er.type = '正式考试'
|
||||||
|
er.name = '正式考试' + datetime.now().strftime('%Y%m%d%H%M')
|
||||||
|
er.limit = exam.paper.limit
|
||||||
|
er.paper = exam.paper
|
||||||
|
er.total_score = exam.paper.total_score
|
||||||
|
er.start_time = now
|
||||||
|
er.is_pass = False
|
||||||
|
er.exam = exam
|
||||||
|
er.create_by = request.user
|
||||||
|
er.save()
|
||||||
|
ret = {}
|
||||||
|
ret['examrecord'] = er.id
|
||||||
|
pqs = PaperQuestion.objects.filter(paper=exam.paper).order_by('id')
|
||||||
|
details = []
|
||||||
|
for i in pqs:
|
||||||
|
details.append(AnswerDetail(examrecord=er, question=i.question, total_score=i.total_score))
|
||||||
|
AnswerDetail.objects.bulk_create(details)
|
||||||
|
ads = AnswerDetail.objects.select_related('question').filter(examrecord=er).order_by('id')
|
||||||
|
ret['questions_'] = AnswerDetailOutSerializer(instance=ads, many=True).data
|
||||||
|
return Response(ret)
|
||||||
|
raise ParseError('暂不支持')
|
||||||
|
|
||||||
|
|
||||||
|
class ExamRecordViewSet(ListModelMixin, DestroyModelMixin, RetrieveModelMixin, GenericViewSet):
|
||||||
|
"""
|
||||||
|
考试记录列表和详情
|
||||||
|
"""
|
||||||
|
perms_map = {'get': '*', 'post': '*', 'delete':'examrecord'}
|
||||||
|
queryset = ExamRecord.objects.select_related('create_by')
|
||||||
|
serializer_class = ExamRecordListSerializer
|
||||||
|
ordering_fields = ['create_time', 'score', 'took', 'update_time']
|
||||||
|
ordering = ['-update_time']
|
||||||
|
search_fields = ('create_by__name', 'create_by__username')
|
||||||
|
filterset_class = ExamRecordFilter
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action == 'retrieve':
|
||||||
|
return ExamRecordDetailSerializer
|
||||||
|
return super().get_serializer_class()
|
||||||
|
|
||||||
|
def perform_destroy(self, instance): # 考试记录物理删除
|
||||||
|
instance.delete(soft=False)
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=False, perms_map=[{'post': '*'}], serializer_class=Serializer, permission_classes = [IsAuthenticated])
|
||||||
|
def clear(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
清除七日前未提交的考试记录
|
||||||
|
|
||||||
|
清除七日前未提交的考试记录
|
||||||
|
"""
|
||||||
|
now = timezone.now
|
||||||
|
days7_ago = now - timedelta(days=7)
|
||||||
|
ExamRecord.objects.filter(create_time__lte=days7_ago, is_submited=False).delete(soft=False)
|
||||||
|
return Response(status=False)
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
||||||
|
def self(self, request, pk=None):
|
||||||
|
'''
|
||||||
|
个人考试记录
|
||||||
|
|
||||||
|
个人考试记录
|
||||||
|
'''
|
||||||
|
queryset = ExamRecord.objects.filter(create_by=request.user).order_by('-update_time')
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=True, perms_map=[{'post': '*'}], serializer_class=ExamRecordSubmitSerializer, permission_classes = [IsAuthenticated])
|
||||||
|
@transaction.atomic
|
||||||
|
def submit(self, request, pk=None):
|
||||||
|
'''
|
||||||
|
提交答卷
|
||||||
|
|
||||||
|
提交答卷
|
||||||
|
'''
|
||||||
|
er = self.get_object()
|
||||||
|
now = timezone.now()
|
||||||
|
if er.create_by != request.user:
|
||||||
|
raise ParseError('提交人有误')
|
||||||
|
exam = er.exam
|
||||||
|
if not exam:
|
||||||
|
raise ParseError('暂不支持')
|
||||||
|
if now > exam.close_time + timedelta(minutes=30):
|
||||||
|
raise ParseError('考试时间已过, 提交失败')
|
||||||
|
serializer = ExamRecordSubmitSerializer(data = request.data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
vdata = serializer.validated_data
|
||||||
|
questions_ = vdata['questions_']
|
||||||
|
# 后端判卷
|
||||||
|
ads = AnswerDetail.objects.select_related('question').filter(examrecord=er).order_by('id')
|
||||||
|
total_score = 0
|
||||||
|
try:
|
||||||
|
for index, ad in enumerate(ads):
|
||||||
|
ad.user_answer = questions_[index]['user_answer']
|
||||||
|
if ad.question.type == '多选':
|
||||||
|
if set(ad.question.right) == set(ad.user_answer):
|
||||||
|
ad.is_right = True
|
||||||
|
ad.score = ad.total_score
|
||||||
|
else:
|
||||||
|
if ad.question.right == ad.user_answer:
|
||||||
|
ad.is_right = True
|
||||||
|
ad.score = ad.total_score
|
||||||
|
ad.save()
|
||||||
|
total_score = total_score + ad.score
|
||||||
|
except Exception as e:
|
||||||
|
raise ParseError('判卷失败, 请检查试卷:' + str(e))
|
||||||
|
er.score = total_score
|
||||||
|
if er.score > 0.6*er.total_score:
|
||||||
|
er.is_pass = True
|
||||||
|
er.took = (now - er.create_time).total_seconds()
|
||||||
|
er.end_time = now
|
||||||
|
er.is_submited = True
|
||||||
|
er.save()
|
||||||
|
return Response(ExamRecordListSerializer(instance=er).data)
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class OpsConfig(AppConfig):
|
||||||
|
name = 'ops'
|
||||||
|
verbose_name = '系统监控'
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
LOG_NOT_FONED = {"code": "log_not_found", "detail": "日志不存在"}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django_filters import rest_framework as filters
|
||||||
|
|
||||||
|
|
||||||
|
class DrfLogFilterSet(filters.FilterSet):
|
||||||
|
start_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='gte')
|
||||||
|
end_request = filters.DateTimeFilter(field_name="requested_at", lookup_expr='lte')
|
||||||
|
id = filters.CharFilter()
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-10-12 06:04
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DrfRequestLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||||
|
('requested_at', models.DateTimeField(db_index=True)),
|
||||||
|
('response_ms', models.PositiveIntegerField(default=0)),
|
||||||
|
('path', models.CharField(db_index=True, help_text='请求地址', max_length=400)),
|
||||||
|
('view', models.CharField(blank=True, db_index=True, help_text='执行视图', max_length=400, null=True)),
|
||||||
|
('view_method', models.CharField(blank=True, db_index=True, max_length=20, null=True)),
|
||||||
|
('remote_addr', models.GenericIPAddressField()),
|
||||||
|
('host', models.URLField()),
|
||||||
|
('method', models.CharField(max_length=10)),
|
||||||
|
('query_params', models.TextField(blank=True, null=True)),
|
||||||
|
('data', models.TextField(blank=True, null=True)),
|
||||||
|
('response', models.TextField(blank=True, null=True)),
|
||||||
|
('errors', models.TextField(blank=True, null=True)),
|
||||||
|
('agent', models.TextField(blank=True, null=True)),
|
||||||
|
('status_code', models.PositiveIntegerField(blank=True, db_index=True, null=True)),
|
||||||
|
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'DRF请求日志',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-10-12 06:55
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields.jsonb
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ops', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='drfrequestlog',
|
||||||
|
name='data',
|
||||||
|
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='drfrequestlog',
|
||||||
|
name='query_params',
|
||||||
|
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='drfrequestlog',
|
||||||
|
name='response',
|
||||||
|
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from django.utils.timezone import now
|
||||||
|
import traceback
|
||||||
|
from django.db import connection
|
||||||
|
from apps.ops.models import DrfRequestLog
|
||||||
|
import ipaddress
|
||||||
|
from user_agents import parse
|
||||||
|
|
||||||
|
# 实例化myLogger
|
||||||
|
myLogger = logging.getLogger('log')
|
||||||
|
|
||||||
|
class MyLoggingMixin(object):
|
||||||
|
"""Mixin to log requests"""
|
||||||
|
|
||||||
|
CLEANED_SUBSTITUTE = "********************"
|
||||||
|
|
||||||
|
# logging_methods = "__all__"
|
||||||
|
logging_methods = '__all__'
|
||||||
|
sensitive_fields = {}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
assert isinstance(
|
||||||
|
self.CLEANED_SUBSTITUTE, str
|
||||||
|
), "CLEANED_SUBSTITUTE must be a string."
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def initial(self, request, *args, **kwargs):
|
||||||
|
request_id = uuid.uuid4()
|
||||||
|
self.log = {"requested_at": now(), "id": request_id}
|
||||||
|
setattr(request, 'request_id', request_id)
|
||||||
|
if not getattr(self, "decode_request_body", False):
|
||||||
|
self.log["data"] = ""
|
||||||
|
else:
|
||||||
|
self.log["data"] = self._clean_data(request.body)
|
||||||
|
|
||||||
|
super().initial(request, *args, **kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Accessing request.data *for the first time* parses the request body, which may raise
|
||||||
|
# ParseError and UnsupportedMediaType exceptions. It's important not to swallow these,
|
||||||
|
# as (depending on implementation details) they may only get raised this once, and
|
||||||
|
# DRF logic needs them to be raised by the view for error handling to work correctly.
|
||||||
|
data = self.request.data.dict()
|
||||||
|
except AttributeError:
|
||||||
|
data = self.request.data
|
||||||
|
self.log["data"] = self._clean_data(data)
|
||||||
|
|
||||||
|
def handle_exception(self, exc):
|
||||||
|
response = super().handle_exception(exc)
|
||||||
|
self.log["errors"] = traceback.format_exc()
|
||||||
|
return response
|
||||||
|
|
||||||
|
def finalize_response(self, request, response, *args, **kwargs):
|
||||||
|
response = super().finalize_response(
|
||||||
|
request, response, *args, **kwargs
|
||||||
|
)
|
||||||
|
# Ensure backward compatibility for those using _should_log hook
|
||||||
|
should_log = (
|
||||||
|
self._should_log if hasattr(self, "_should_log") else self.should_log
|
||||||
|
)
|
||||||
|
if should_log(request, response):
|
||||||
|
if (connection.settings_dict.get("ATOMIC_REQUESTS") and
|
||||||
|
getattr(response, "exception", None) and connection.in_atomic_block):
|
||||||
|
# response with exception (HTTP status like: 401, 404, etc)
|
||||||
|
# pointwise disable atomic block for handle log (TransactionManagementError)
|
||||||
|
connection.set_rollback(True)
|
||||||
|
connection.set_rollback(False)
|
||||||
|
|
||||||
|
if response.streaming:
|
||||||
|
rendered_content = None
|
||||||
|
elif hasattr(response, "rendered_content"):
|
||||||
|
rendered_content = response.rendered_content
|
||||||
|
else:
|
||||||
|
rendered_content = response.getvalue()
|
||||||
|
|
||||||
|
self.log.update(
|
||||||
|
{
|
||||||
|
"remote_addr": self._get_ip_address(request),
|
||||||
|
"view": self._get_view_name(request),
|
||||||
|
"view_method": self._get_view_method(request),
|
||||||
|
"path": self._get_path(request),
|
||||||
|
"host": request.get_host(),
|
||||||
|
"method": request.method,
|
||||||
|
"query_params": self._clean_data(request.query_params.dict()),
|
||||||
|
"user": self._get_user(request),
|
||||||
|
"response_ms": self._get_response_ms(),
|
||||||
|
"response": self._clean_data(rendered_content),
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"agent": self._get_agent(request),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.handle_log()
|
||||||
|
except Exception:
|
||||||
|
# ensure that all exceptions raised by handle_log
|
||||||
|
# doesn't prevent API call to continue as expected
|
||||||
|
myLogger.exception("Logging API call raise exception!")
|
||||||
|
return response
|
||||||
|
|
||||||
|
def handle_log(self):
|
||||||
|
"""
|
||||||
|
Hook to define what happens with the log.
|
||||||
|
|
||||||
|
Defaults on saving the data on the db.
|
||||||
|
"""
|
||||||
|
DrfRequestLog(**self.log).save()
|
||||||
|
|
||||||
|
def _get_path(self, request):
|
||||||
|
"""Get the request path and truncate it"""
|
||||||
|
return request.path
|
||||||
|
|
||||||
|
def _get_ip_address(self, request):
|
||||||
|
"""Get the remote ip address the request was generated from."""
|
||||||
|
ipaddr = request.META.get("HTTP_X_FORWARDED_FOR", None)
|
||||||
|
if ipaddr:
|
||||||
|
ipaddr = ipaddr.split(",")[0]
|
||||||
|
else:
|
||||||
|
ipaddr = request.META.get("REMOTE_ADDR", "")
|
||||||
|
|
||||||
|
# Account for IPv4 and IPv6 addresses, each possibly with port appended. Possibilities are:
|
||||||
|
# <ipv4 address>
|
||||||
|
# <ipv6 address>
|
||||||
|
# <ipv4 address>:port
|
||||||
|
# [<ipv6 address>]:port
|
||||||
|
# Note that ipv6 addresses are colon separated hex numbers
|
||||||
|
possibles = (ipaddr.lstrip("[").split("]")[0], ipaddr.split(":")[0])
|
||||||
|
|
||||||
|
for addr in possibles:
|
||||||
|
try:
|
||||||
|
return str(ipaddress.ip_address(addr))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ipaddr
|
||||||
|
|
||||||
|
def _get_view_name(self, request):
|
||||||
|
"""Get view name."""
|
||||||
|
method = request.method.lower()
|
||||||
|
try:
|
||||||
|
attributes = getattr(self, method)
|
||||||
|
return (
|
||||||
|
type(attributes.__self__).__module__ + "." + type(attributes.__self__).__name__
|
||||||
|
)
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_view_method(self, request):
|
||||||
|
"""Get view method."""
|
||||||
|
if hasattr(self, "action"):
|
||||||
|
return self.action or None
|
||||||
|
return request.method.lower()
|
||||||
|
|
||||||
|
def _get_user(self, request):
|
||||||
|
"""Get user."""
|
||||||
|
user = request.user
|
||||||
|
if user.is_anonymous:
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
||||||
|
def _get_agent(self, request):
|
||||||
|
"""Get os string"""
|
||||||
|
return str(parse(request.META['HTTP_USER_AGENT']))
|
||||||
|
|
||||||
|
def _get_response_ms(self):
|
||||||
|
"""
|
||||||
|
Get the duration of the request response cycle is milliseconds.
|
||||||
|
In case of negative duration 0 is returned.
|
||||||
|
"""
|
||||||
|
response_timedelta = now() - self.log["requested_at"]
|
||||||
|
response_ms = int(response_timedelta.total_seconds() * 1000)
|
||||||
|
return max(response_ms, 0)
|
||||||
|
|
||||||
|
def should_log(self, request, response):
|
||||||
|
"""
|
||||||
|
Method that should return a value that evaluated to True if the request should be logged.
|
||||||
|
By default, check if the request method is in logging_methods.
|
||||||
|
"""
|
||||||
|
return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \
|
||||||
|
or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])
|
||||||
|
|
||||||
|
def _clean_data(self, data):
|
||||||
|
"""
|
||||||
|
Clean a dictionary of data of potentially sensitive info before
|
||||||
|
sending to the database.
|
||||||
|
Function based on the "_clean_credentials" function of django
|
||||||
|
(https://github.com/django/django/blob/stable/1.11.x/django/contrib/auth/__init__.py#L50)
|
||||||
|
|
||||||
|
Fields defined by django are by default cleaned with this function
|
||||||
|
|
||||||
|
You can define your own sensitive fields in your view by defining a set
|
||||||
|
eg: sensitive_fields = {'field1', 'field2'}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
data = data.decode(errors="replace")
|
||||||
|
try:
|
||||||
|
data = json.loads(data)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if isinstance(data, list):
|
||||||
|
return [self._clean_data(d) for d in data]
|
||||||
|
|
||||||
|
if isinstance(data, dict):
|
||||||
|
SENSITIVE_FIELDS = {
|
||||||
|
"api",
|
||||||
|
"token",
|
||||||
|
"key",
|
||||||
|
"secret",
|
||||||
|
"password",
|
||||||
|
"signature",
|
||||||
|
"access",
|
||||||
|
"refresh"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = dict(data)
|
||||||
|
if self.sensitive_fields:
|
||||||
|
SENSITIVE_FIELDS = SENSITIVE_FIELDS | {
|
||||||
|
field.lower() for field in self.sensitive_fields
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in data.items():
|
||||||
|
try:
|
||||||
|
value = ast.literal_eval(value)
|
||||||
|
except (ValueError, SyntaxError):
|
||||||
|
pass
|
||||||
|
if isinstance(value, (list, dict)):
|
||||||
|
data[key] = self._clean_data(value)
|
||||||
|
if key.lower() in SENSITIVE_FIELDS:
|
||||||
|
data[key] = self.CLEANED_SUBSTITUTE
|
||||||
|
return data
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import uuid
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.postgres.fields import JSONField
|
||||||
|
|
||||||
|
class DrfRequestLog(models.Model):
|
||||||
|
"""Logs Django rest framework API requests"""
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
'system.user',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
requested_at = models.DateTimeField(db_index=True)
|
||||||
|
response_ms = models.PositiveIntegerField(default=0)
|
||||||
|
path = models.CharField(
|
||||||
|
max_length=400,
|
||||||
|
db_index=True,
|
||||||
|
help_text="请求地址",
|
||||||
|
)
|
||||||
|
view = models.CharField(
|
||||||
|
max_length=400,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
db_index=True,
|
||||||
|
help_text="执行视图",
|
||||||
|
)
|
||||||
|
view_method = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
remote_addr = models.GenericIPAddressField()
|
||||||
|
host = models.URLField()
|
||||||
|
method = models.CharField(max_length=10)
|
||||||
|
query_params = JSONField(null=True, blank=True)
|
||||||
|
data = JSONField(null=True, blank=True)
|
||||||
|
response = JSONField(null=True, blank=True)
|
||||||
|
errors = models.TextField(null=True, blank=True)
|
||||||
|
agent = models.TextField(null=True, blank=True)
|
||||||
|
status_code = models.PositiveIntegerField(null=True, blank=True, db_index=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "DRF请求日志"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "{} {}".format(self.method, self.path)
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Create your tasks here
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
from datetime import timedelta
|
||||||
|
from apps.ops.models import DrfRequestLog
|
||||||
|
# from celery import shared_task
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
# @shared_task()
|
||||||
|
# def clear_drf_log():
|
||||||
|
# """清除7天前的日志记录
|
||||||
|
|
||||||
|
# 清除7天前的日志记录
|
||||||
|
# """
|
||||||
|
# now = timezone.now()
|
||||||
|
# days7_ago = now - timedelta(days=7)
|
||||||
|
# DrfRequestLog.objects.filter(create_time__lte=days7_ago).delete()
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework import routers
|
||||||
|
from .views import CpuView, DiskView, DrfRequestLogViewSet, LogView, LogDetailView, MemoryView
|
||||||
|
|
||||||
|
API_BASE_URL = 'api/ops/'
|
||||||
|
HTML_BASE_URL = 'ops/'
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register('request_log', DrfRequestLogViewSet, basename="request_log")
|
||||||
|
urlpatterns = [
|
||||||
|
path(API_BASE_URL, include(router.urls)),
|
||||||
|
path(API_BASE_URL + 'log/', LogView.as_view()),
|
||||||
|
path(API_BASE_URL + 'log/<str:name>/', LogDetailView.as_view()),
|
||||||
|
path(API_BASE_URL + 'server/cpu/', CpuView.as_view()),
|
||||||
|
path(API_BASE_URL + 'server/memory/', MemoryView.as_view()),
|
||||||
|
path(API_BASE_URL + 'server/disk/', DiskView.as_view())
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
|
||||||
|
from django.shortcuts import render
|
||||||
|
import psutil
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from django.conf import settings
|
||||||
|
import os
|
||||||
|
from rest_framework import serializers
|
||||||
|
from drf_yasg import openapi
|
||||||
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
|
from rest_framework.exceptions import NotFound
|
||||||
|
from rest_framework.mixins import ListModelMixin
|
||||||
|
from apps.ops.filters import DrfLogFilterSet
|
||||||
|
from apps.ops.models import DrfRequestLog
|
||||||
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
from apps.ops.errors import LOG_NOT_FONED
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
||||||
|
class CpuView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
获取服务器cpu当前状态
|
||||||
|
|
||||||
|
获取服务器cpu当前状态
|
||||||
|
"""
|
||||||
|
ret = {'cpu': {}}
|
||||||
|
ret['cpu']['count'] = psutil.cpu_count()
|
||||||
|
ret['cpu']['lcount'] = psutil.cpu_count(logical=False)
|
||||||
|
ret['cpu']['percent'] = psutil.cpu_percent(interval=1)
|
||||||
|
return Response(ret)
|
||||||
|
|
||||||
|
class MemoryView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
获取服务器内存当前状态
|
||||||
|
|
||||||
|
获取服务器内存当前状态
|
||||||
|
"""
|
||||||
|
ret = {'memory': {}}
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
ret['memory']['total'] = round(memory.total/1024/1024/1024, 2)
|
||||||
|
ret['memory']['used'] = round(memory.used/1024/1024/1024, 2)
|
||||||
|
ret['memory']['percent'] = memory.percent
|
||||||
|
return Response(ret)
|
||||||
|
|
||||||
|
class DiskView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
获取服务器硬盘当前状态
|
||||||
|
|
||||||
|
获取服务器硬盘当前状态
|
||||||
|
"""
|
||||||
|
ret = {'disk': {}}
|
||||||
|
disk = psutil.disk_usage('/')
|
||||||
|
ret['disk']['total'] = round(disk.total/1024/1024/1024, 2)
|
||||||
|
ret['disk']['used'] = round(disk.used/1024/1024/1024, 2)
|
||||||
|
ret['disk']['percent'] = disk.percent
|
||||||
|
return Response(ret)
|
||||||
|
|
||||||
|
def get_file_list(file_path):
|
||||||
|
dir_list = os.listdir(file_path)
|
||||||
|
if not dir_list:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# 注意,这里使用lambda表达式,将文件按照最后修改时间顺序升序排列
|
||||||
|
# os.path.getmtime() 函数是获取文件最后修改时间
|
||||||
|
# os.path.getctime() 函数是获取文件最后创建时间
|
||||||
|
dir_list = sorted(dir_list, key=lambda x: os.path.getmtime(
|
||||||
|
os.path.join(file_path, x)), reverse=True)
|
||||||
|
# print(dir_list)
|
||||||
|
return dir_list
|
||||||
|
|
||||||
|
|
||||||
|
class LogView(APIView):
|
||||||
|
|
||||||
|
@swagger_auto_schema(manual_parameters=[
|
||||||
|
openapi.Parameter('name', openapi.IN_QUERY,
|
||||||
|
description='日志文件名', type=openapi.TYPE_STRING)
|
||||||
|
])
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
查看最近的日志列表
|
||||||
|
|
||||||
|
查看最近的日志列表
|
||||||
|
"""
|
||||||
|
logs = []
|
||||||
|
name = request.GET.get('name', None)
|
||||||
|
# for root, dirs, files in os.walk(settings.LOG_PATH):
|
||||||
|
# files.reverse()
|
||||||
|
for file in get_file_list(settings.LOG_PATH):
|
||||||
|
if len(logs) > 50:
|
||||||
|
break
|
||||||
|
filepath = os.path.join(settings.LOG_PATH, file)
|
||||||
|
if name:
|
||||||
|
if name in filepath:
|
||||||
|
fsize = os.path.getsize(filepath)
|
||||||
|
if fsize:
|
||||||
|
logs.append({
|
||||||
|
"name": file,
|
||||||
|
"filepath": filepath,
|
||||||
|
"size": round(fsize/1024, 1)
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
fsize = os.path.getsize(filepath)
|
||||||
|
if fsize:
|
||||||
|
logs.append({
|
||||||
|
"name": file,
|
||||||
|
"filepath": filepath,
|
||||||
|
"size": round(fsize/1024, 1)
|
||||||
|
})
|
||||||
|
return Response(logs)
|
||||||
|
|
||||||
|
|
||||||
|
class LogDetailView(APIView):
|
||||||
|
|
||||||
|
def get(self, request, name):
|
||||||
|
"""
|
||||||
|
查看日志详情
|
||||||
|
|
||||||
|
查看日志详情
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(os.path.join(settings.LOG_PATH, name)) as f:
|
||||||
|
data = f.read()
|
||||||
|
return Response(data)
|
||||||
|
except Exception:
|
||||||
|
raise NotFound(**LOG_NOT_FONED)
|
||||||
|
|
||||||
|
|
||||||
|
class DrfRequestLogSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = DrfRequestLog
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class DrfRequestLogViewSet(ListModelMixin, GenericViewSet):
|
||||||
|
"""请求日志
|
||||||
|
|
||||||
|
请求日志
|
||||||
|
"""
|
||||||
|
perms_map = {'get': '*'}
|
||||||
|
queryset = DrfRequestLog.objects.all()
|
||||||
|
serializer_class = DrfRequestLogSerializer
|
||||||
|
ordering = ['-requested_at']
|
||||||
|
filterset_class = DrfLogFilterSet
|
||||||
|
|
||||||
|
@action(methods=['delete'], detail=False, perms_map = {'delete':'log_delete'})
|
||||||
|
def clear(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
清空日志
|
||||||
|
"""
|
||||||
|
DrfRequestLog.objects.all().delete()
|
||||||
|
return Response()
|
||||||
|
|
@ -98,7 +98,7 @@ class SubtaskViewSet(PageOrNot, CreateUpdateCustomMixin, OptimizationMixin, Mode
|
||||||
if has_permission('inspecttask_create', self.request.user):
|
if has_permission('inspecttask_create', self.request.user):
|
||||||
return queryset
|
return queryset
|
||||||
else:
|
else:
|
||||||
return queryset.filter(team_subtask__member=self.request.user).exclude(state='待发布')
|
return queryset.filter(team_subtask__member__id=self.request.user.id).exclude(state='待发布')
|
||||||
|
|
||||||
@action(methods=['get'], detail=False, perms_map = {'get':'*'})
|
@action(methods=['get'], detail=False, perms_map = {'get':'*'})
|
||||||
def self(self, request, *args, **kwargs):
|
def self(self, request, *args, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ from utils.queryset import get_child_queryset2
|
||||||
|
|
||||||
from .permission import RecordPermission
|
from .permission import RecordPermission
|
||||||
|
|
||||||
class RecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
|
class RecordViewSet(RbacFilterSet, PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
|
||||||
perms_map = {'get': '*', 'post': 'record_create',
|
perms_map = {'get': '*', 'post': 'record_create',
|
||||||
'put': 'record_update', 'delete': 'record_delete'}
|
'put': 'record_update', 'delete': 'record_delete'}
|
||||||
queryset = Record.objects.filter(content__cate=1)
|
queryset = Record.objects.filter(content__cate=1)
|
||||||
|
|
@ -219,25 +219,6 @@ class RecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
|
||||||
ordering = ['-task', 'content__sortnum', '-create_time']
|
ordering = ['-task', 'content__sortnum', '-create_time']
|
||||||
filter_class = RecordFilter # 过滤类
|
filter_class = RecordFilter # 过滤类
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
queryset = self.queryset
|
|
||||||
if hasattr(self.get_serializer_class(), 'setup_eager_loading'):
|
|
||||||
queryset = self.get_serializer_class().setup_eager_loading(queryset)
|
|
||||||
if self.request.user.is_superuser:
|
|
||||||
pass
|
|
||||||
if hasattr(queryset.model, 'belong_dept'):
|
|
||||||
user = self.request.user
|
|
||||||
roles = user.roles
|
|
||||||
data_range = roles.values_list('datas', flat=True)
|
|
||||||
if '全部' in data_range:
|
|
||||||
pass
|
|
||||||
elif '本级及以下' in data_range:
|
|
||||||
belong_depts = get_child_queryset2(user.dept)
|
|
||||||
queryset = queryset.filter(belong_dept__in = belong_depts)
|
|
||||||
elif '本级' in data_range:
|
|
||||||
queryset = queryset.filter(belong_dept = user.dept)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
def filter_queryset(self, queryset):
|
||||||
if not self.request.query_params.get('pageoff', None):
|
if not self.request.query_params.get('pageoff', None):
|
||||||
queryset = queryset.exclude(state='待发布')
|
queryset = queryset.exclude(state='待发布')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from simple_history.admin import SimpleHistoryAdmin
|
|
||||||
from .models import User, Organization, Role, Permission, DictType, Dict, File
|
from .models import User, Organization, Role, Permission, DictType, Dict, File
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
admin.site.register(User)
|
admin.site.register(User)
|
||||||
|
|
@ -7,5 +6,5 @@ admin.site.register(Organization)
|
||||||
admin.site.register(Role)
|
admin.site.register(Role)
|
||||||
admin.site.register(Permission)
|
admin.site.register(Permission)
|
||||||
admin.site.register(DictType)
|
admin.site.register(DictType)
|
||||||
admin.site.register(Dict, SimpleHistoryAdmin)
|
admin.site.register(Dict)
|
||||||
admin.site.register(File)
|
admin.site.register(File)
|
||||||
|
|
@ -4,7 +4,6 @@ from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
import simple_history.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
@ -54,7 +53,6 @@ class Migration(migrations.Migration):
|
||||||
'ordering': ('-history_date', '-history_id'),
|
'ordering': ('-history_date', '-history_id'),
|
||||||
'get_latest_by': 'history_date',
|
'get_latest_by': 'history_date',
|
||||||
},
|
},
|
||||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='File',
|
name='File',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-10-12 06:04
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('system', '0021_auto_20220530_1520'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='HistoricalDict',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -4,7 +4,6 @@ import django.utils.timezone as timezone
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
from utils.model import SoftModel, BaseModel
|
from utils.model import SoftModel, BaseModel
|
||||||
from simple_history.models import HistoricalRecords
|
|
||||||
|
|
||||||
class Province(models.Model):
|
class Province(models.Model):
|
||||||
id = models.CharField('id', primary_key=True, max_length=20)
|
id = models.CharField('id', primary_key=True, max_length=20)
|
||||||
|
|
@ -160,7 +159,6 @@ class Dict(SoftModel):
|
||||||
pid = models.ForeignKey('self', null=True, blank=True,
|
pid = models.ForeignKey('self', null=True, blank=True,
|
||||||
on_delete=models.SET_NULL, verbose_name='父')
|
on_delete=models.SET_NULL, verbose_name='父')
|
||||||
is_used = models.BooleanField('是否有效', default=True)
|
is_used = models.BooleanField('是否有效', default=True)
|
||||||
history = HistoricalRecords()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = '字典'
|
verbose_name = '字典'
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ def get_permission_list(user):
|
||||||
perms_list = ['admin']
|
perms_list = ['admin']
|
||||||
else:
|
else:
|
||||||
perms = Permission.objects.none()
|
perms = Permission.objects.none()
|
||||||
roles = user.roles.all()
|
roles = user.roles.all() if hasattr(user, 'roles') else None
|
||||||
if roles:
|
if roles:
|
||||||
for i in roles:
|
for i in roles:
|
||||||
perms = perms | i.perms.all()
|
perms = perms | i.perms.all()
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ class RbacFilterSet(object):
|
||||||
|
|
||||||
if hasattr(queryset.model, 'belong_dept'):
|
if hasattr(queryset.model, 'belong_dept'):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
roles = user.roles
|
roles = user.roles if hasattr(user, 'roles') else []
|
||||||
|
if not roles:
|
||||||
|
return queryset.none()
|
||||||
data_range = roles.values_list('datas', flat=True)
|
data_range = roles.values_list('datas', flat=True)
|
||||||
if '全部' in data_range:
|
if '全部' in data_range:
|
||||||
return queryset
|
return queryset
|
||||||
|
|
@ -57,6 +59,8 @@ class RbacFilterSet(object):
|
||||||
elif '仅本人' in data_range:
|
elif '仅本人' in data_range:
|
||||||
queryset = queryset.filter(Q(create_by=user)|Q(update_by=user))
|
queryset = queryset.filter(Q(create_by=user)|Q(update_by=user))
|
||||||
return queryset
|
return queryset
|
||||||
|
else:
|
||||||
|
return queryset.none()
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from re import L
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import check_password, make_password
|
from django.contrib.auth.hashers import check_password, make_password
|
||||||
|
|
@ -23,6 +24,7 @@ from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
from rest_framework_simplejwt.tokens import RefreshToken
|
||||||
|
from apps.ops.mixins import MyLoggingMixin
|
||||||
from utils.pagination import PageOrNot
|
from utils.pagination import PageOrNot
|
||||||
from utils.queryset import get_child_queryset2
|
from utils.queryset import get_child_queryset2
|
||||||
|
|
||||||
|
|
@ -30,6 +32,7 @@ from .filters import UserFilter
|
||||||
from .models import (City, Dict, DictType, File, Message, Organization, Permission,
|
from .models import (City, Dict, DictType, File, Message, Organization, Permission,
|
||||||
Position, Province, Role, User, UserThird)
|
Position, Province, Role, User, UserThird)
|
||||||
from .permission import RbacPermission, get_permission_list
|
from .permission import RbacPermission, get_permission_list
|
||||||
|
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||||
from .permission_data import RbacFilterSet
|
from .permission_data import RbacFilterSet
|
||||||
from .serializers import (CitySerializer, DictSerializer, DictTypeSerializer, FileSerializer,
|
from .serializers import (CitySerializer, DictSerializer, DictTypeSerializer, FileSerializer,
|
||||||
OrganizationSerializer, PermissionSerializer,
|
OrganizationSerializer, PermissionSerializer,
|
||||||
|
|
@ -71,8 +74,11 @@ def get_tokens_for_user(user):
|
||||||
}
|
}
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
class MyTokenView(MyLoggingMixin, TokenObtainPairView):
|
||||||
|
def should_log(self, request, response):
|
||||||
|
return response.status_code == 200
|
||||||
|
|
||||||
class Login2View(APIView):
|
class Login2View(MyLoggingMixin, APIView):
|
||||||
"""
|
"""
|
||||||
邮箱验证码登录
|
邮箱验证码登录
|
||||||
"""
|
"""
|
||||||
|
|
@ -88,6 +94,9 @@ class Login2View(APIView):
|
||||||
user = User.objects.get(username=mail)
|
user = User.objects.get(username=mail)
|
||||||
return Response(get_tokens_for_user(user), status=status.HTTP_200_OK)
|
return Response(get_tokens_for_user(user), status=status.HTTP_200_OK)
|
||||||
return Response('验证码错误', status=status.HTTP_400_BAD_REQUEST)
|
return Response('验证码错误', status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def should_log(self, request, response):
|
||||||
|
return response.status_code == 200
|
||||||
|
|
||||||
class sendMsg(APIView):
|
class sendMsg(APIView):
|
||||||
authentication_classes = []
|
authentication_classes = []
|
||||||
|
|
@ -407,7 +416,7 @@ class UserViewSet(PageOrNot, ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WXMPlogin(APIView):
|
class WXMPlogin(MyLoggingMixin, APIView):
|
||||||
authentication_classes=[]
|
authentication_classes=[]
|
||||||
permission_classes=[]
|
permission_classes=[]
|
||||||
|
|
||||||
|
|
@ -428,6 +437,9 @@ class WXMPlogin(APIView):
|
||||||
return Response(get_tokens_for_user(user), status=status.HTTP_200_OK)
|
return Response(get_tokens_for_user(user), status=status.HTTP_200_OK)
|
||||||
except:
|
except:
|
||||||
raise AuthenticationFailed
|
raise AuthenticationFailed
|
||||||
|
|
||||||
|
def should_log(self, request, response):
|
||||||
|
return response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-10-12 08:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('vod', '0005_video_sort_str'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='viewrecord',
|
||||||
|
name='total_seconds',
|
||||||
|
field=models.PositiveIntegerField(default=0, verbose_name='总观看秒数'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-11-16 06:46
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('vod', '0006_viewrecord_total_seconds'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='video',
|
||||||
|
name='views',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='总观看次数'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='video',
|
||||||
|
name='viewsp',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='总观看人数'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='viewrecord',
|
||||||
|
name='video',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='viewrecord_video', to='vod.Video', verbose_name='点播视频'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='viewrecord',
|
||||||
|
name='views',
|
||||||
|
field=models.IntegerField(default=0, verbose_name='总观看次数'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ViewItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('current', models.IntegerField(default=0, verbose_name='当前观看进度(秒)')),
|
||||||
|
('total_seconds', models.PositiveIntegerField(default=0, verbose_name='本次总观看秒数')),
|
||||||
|
('create_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='viewitem_user', to=settings.AUTH_USER_MODEL, verbose_name='观看人')),
|
||||||
|
('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='viewitem_video', to='vod.Video', verbose_name='点播视频')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='View2',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('current', models.IntegerField(default=0, verbose_name='当前观看进度(秒)')),
|
||||||
|
('views', models.IntegerField(default=0, verbose_name='总观看次数')),
|
||||||
|
('total_seconds', models.PositiveIntegerField(default=0, verbose_name='总观看秒数')),
|
||||||
|
('is_completed', models.BooleanField(default=False)),
|
||||||
|
('create_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='view2_user', to=settings.AUTH_USER_MODEL, verbose_name='观看人')),
|
||||||
|
('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='view2_video', to='vod.Video', verbose_name='点播视频')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='View1',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
|
||||||
|
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
|
||||||
|
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
|
||||||
|
('views', models.IntegerField(default=0, verbose_name='总观看次数')),
|
||||||
|
('viewsp', models.IntegerField(default=0, verbose_name='总观看人数')),
|
||||||
|
('video', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='view1_video', to='vod.Video')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-11-16 06:53
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('vod', '0007_auto_20221116_1446'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='view2',
|
||||||
|
unique_together={('create_by', 'video')},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Generated by Django 3.0.5 on 2022-11-17 01:41
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('system', '0022_delete_historicaldict'),
|
||||||
|
('vod', '0008_auto_20221116_1453'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='video',
|
||||||
|
name='category_big',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='video_catebig', to='system.Dict', verbose_name='视频大类'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='video',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='video_cate', to='system.Dict', verbose_name='视频分类'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from tabnanny import verbose
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from utils.model import BaseModel
|
from utils.model import BaseModel
|
||||||
from apps.system.models import User, CommonAModel, Dict
|
from apps.system.models import User, CommonAModel, Dict
|
||||||
|
|
@ -6,14 +7,15 @@ from apps.system.models import User, CommonAModel, Dict
|
||||||
class Video(CommonAModel):
|
class Video(CommonAModel):
|
||||||
|
|
||||||
name = models.CharField(verbose_name='视频名称', max_length=100)
|
name = models.CharField(verbose_name='视频名称', max_length=100)
|
||||||
category = models.ForeignKey(Dict, verbose_name='视频分类', on_delete=models.DO_NOTHING)
|
category_big = models.ForeignKey(Dict, verbose_name='视频大类', on_delete=models.SET_NULL, null=True, blank=True, related_name='video_catebig')
|
||||||
|
category = models.ForeignKey(Dict, verbose_name='视频分类', on_delete=models.SET_NULL, null=True, blank=True, related_name='video_cate')
|
||||||
description = models.TextField(verbose_name='视频描述', default='')
|
description = models.TextField(verbose_name='视频描述', default='')
|
||||||
fileid = models.CharField(verbose_name='云点播视频id', unique=True, max_length=200)
|
fileid = models.CharField(verbose_name='云点播视频id', unique=True, max_length=200)
|
||||||
mediaurl = models.CharField(verbose_name='视频地址', max_length=200)
|
mediaurl = models.CharField(verbose_name='视频地址', max_length=200)
|
||||||
coverurl = models.CharField(verbose_name='封面地址', max_length=200)
|
coverurl = models.CharField(verbose_name='封面地址', max_length=200)
|
||||||
duration = models.IntegerField(verbose_name='时长(秒)', default=0)
|
duration = models.IntegerField(verbose_name='时长(秒)', default=0)
|
||||||
views = models.IntegerField(verbose_name='观看次数', default=0)
|
views = models.IntegerField(verbose_name='总观看次数', default=0)
|
||||||
viewsp = models.IntegerField(verbose_name='观看人数', default=0)
|
viewsp = models.IntegerField(verbose_name='总观看人数', default=0)
|
||||||
sort_str = models.CharField('排序字符', max_length=10, null=True, blank=True)
|
sort_str = models.CharField('排序字符', max_length=10, null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
@ -24,15 +26,51 @@ class Video(CommonAModel):
|
||||||
|
|
||||||
|
|
||||||
class ViewRecord(BaseModel):
|
class ViewRecord(BaseModel):
|
||||||
|
"""
|
||||||
# 观看记录
|
某视频-某人的观看记录统计
|
||||||
|
"""
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='观看人', related_name='viewrecord_user')
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='观看人', related_name='viewrecord_user')
|
||||||
views = models.IntegerField(verbose_name='观看次数', default=0)
|
|
||||||
current = models.IntegerField(verbose_name='当前观看进度(秒)', default=0)
|
current = models.IntegerField(verbose_name='当前观看进度(秒)', default=0)
|
||||||
video = models.ForeignKey(Video, verbose_name='点播视频', on_delete=models.CASCADE, related_name='record_video')
|
video = models.ForeignKey(Video, verbose_name='点播视频', on_delete=models.CASCADE, related_name='viewrecord_video')
|
||||||
|
views = models.IntegerField(verbose_name='总观看次数', default=0)
|
||||||
|
total_seconds = models.PositiveIntegerField(verbose_name='总观看秒数', default=0)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = '点播观看记录'
|
verbose_name = '点播观看记录'
|
||||||
verbose_name_plural = verbose_name
|
verbose_name_plural = verbose_name
|
||||||
|
|
||||||
|
|
||||||
|
class ViewItem(BaseModel):
|
||||||
|
"""
|
||||||
|
单次观看记录
|
||||||
|
"""
|
||||||
|
create_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='观看人', related_name='viewitem_user')
|
||||||
|
video = models.ForeignKey(Video, verbose_name='点播视频', on_delete=models.CASCADE, related_name='viewitem_video')
|
||||||
|
current = models.IntegerField(verbose_name='当前观看进度(秒)', default=0)
|
||||||
|
total_seconds = models.PositiveIntegerField(verbose_name='本次总观看秒数', default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class View1(BaseModel):
|
||||||
|
"""
|
||||||
|
视频播放统计
|
||||||
|
"""
|
||||||
|
video = models.OneToOneField(Video, on_delete=models.CASCADE, related_name='view1_video')
|
||||||
|
views = models.IntegerField(verbose_name='总观看次数', default=0)
|
||||||
|
viewsp = models.IntegerField(verbose_name='总观看人数', default=0)
|
||||||
|
|
||||||
|
|
||||||
|
class View2(BaseModel):
|
||||||
|
"""
|
||||||
|
某视频-某人的观看记录统计
|
||||||
|
"""
|
||||||
|
create_by = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='观看人', related_name='view2_user')
|
||||||
|
current = models.IntegerField(verbose_name='当前观看进度(秒)', default=0)
|
||||||
|
video = models.ForeignKey(Video, verbose_name='点播视频', on_delete=models.CASCADE, related_name='view2_video')
|
||||||
|
views = models.IntegerField(verbose_name='总观看次数', default=0)
|
||||||
|
total_seconds = models.PositiveIntegerField(verbose_name='总观看秒数', default=0)
|
||||||
|
is_completed = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (
|
||||||
|
('create_by','video'), # 联合唯一
|
||||||
|
)
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Video, ViewRecord
|
from .models import Video, ViewRecord, ViewItem, View1, View2
|
||||||
from apps.system.serializers import UserSimpleSerializer
|
from apps.system.serializers import UserSimpleSerializer
|
||||||
|
|
||||||
class VideoSerializer(serializers.ModelSerializer):
|
class VideoSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -7,13 +7,34 @@ class VideoSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Video
|
model = Video
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
video = super().create(validated_data)
|
||||||
|
cate = video.category
|
||||||
|
video.category_big = cate
|
||||||
|
if cate.pid:
|
||||||
|
video.category_big = cate.pid
|
||||||
|
video.save()
|
||||||
|
View1.objects.get_or_create(video=video, defaults={'video': video})
|
||||||
|
return video
|
||||||
|
|
||||||
class VideoUpdateSerializer(serializers.ModelSerializer):
|
class VideoUpdateSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Video
|
model = Video
|
||||||
fields = ['name', 'category', 'description', 'sort_str']
|
fields = ['name', 'category', 'description', 'sort_str']
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
video = super().update(instance, validated_data)
|
||||||
|
cate = video.category
|
||||||
|
video.category_big = cate
|
||||||
|
if cate.pid:
|
||||||
|
video.category_big = cate.pid
|
||||||
|
video.save()
|
||||||
|
return video
|
||||||
|
|
||||||
class VideoListDetailSerializer(serializers.ModelSerializer):
|
class VideoListDetailSerializer(serializers.ModelSerializer):
|
||||||
|
views_n = serializers.IntegerField(source='view1_video.views', read_only=True, label="总观看次数")
|
||||||
|
viewsp_n = serializers.IntegerField(source='view1_video.viewsp', read_only=True, label="总观看秒数")
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Video
|
model = Video
|
||||||
exclude = ['mediaurl']
|
exclude = ['mediaurl']
|
||||||
|
|
@ -38,4 +59,29 @@ class VRecordSerializer(serializers.ModelSerializer):
|
||||||
class VRecordUpdateSerializer(serializers.ModelSerializer):
|
class VRecordUpdateSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ViewRecord
|
model = ViewRecord
|
||||||
fields=['num', 'current']
|
fields=['num', 'current']
|
||||||
|
|
||||||
|
|
||||||
|
class ViewItemSerializer(serializers.ModelSerializer):
|
||||||
|
create_by_ = UserSimpleSerializer(source='create_by', read_only=True)
|
||||||
|
video_ = VideoSimpleSerializer(source='video', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = ViewItem
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class ViewItemUpdateSerializer(serializers.Serializer):
|
||||||
|
current = serializers.IntegerField(min_value=1)
|
||||||
|
seconds = serializers.IntegerField(min_value=0)
|
||||||
|
|
||||||
|
|
||||||
|
class View2Serializer(serializers.Serializer):
|
||||||
|
create_by_ = UserSimpleSerializer(source='create_by', read_only=True)
|
||||||
|
video_ = VideoSimpleSerializer(source='video', read_only=True)
|
||||||
|
class Meta:
|
||||||
|
model = View2
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
class DatetimeSerializer(serializers.Serializer):
|
||||||
|
start_time = serializers.DateTimeField(label="开始时间")
|
||||||
|
end_time = serializers.DateTimeField(label="结束时间")
|
||||||
|
limit = serializers.IntegerField(min_value=10, label="展示数量")
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
from django.db.models import base
|
from django.db.models import base
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from .views import ClassView, PlayCodeAPIView, SignatureAPIView, VideoView, VideoViewSet, VRecordViewSet, MyViewRecordAPIView
|
from .views import ClassView, PlayCodeAPIView, SignatureAPIView, ViewItemViewSet, VideoViewSet, VRecordViewSet, MyViewRecordAPIView, View2ViewSet
|
||||||
|
from .views2 import AnalyseViewSet
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register('video', VideoViewSet, basename="video")
|
router.register('video', VideoViewSet, basename="video")
|
||||||
router.register('viewrecord', VRecordViewSet, basename='viewrecord')
|
router.register('viewrecord', VRecordViewSet, basename='viewrecord')
|
||||||
|
router.register('viewitem', ViewItemViewSet, basename='viewitem')
|
||||||
|
router.register('view2', View2ViewSet, basename='view2')
|
||||||
|
router.register('analyse', AnalyseViewSet, basename='analyse')
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', include(router.urls)),
|
path('', include(router.urls)),
|
||||||
path('video/<int:id>/myview/', MyViewRecordAPIView.as_view()),
|
path('video/<int:id>/myview/', MyViewRecordAPIView.as_view()),
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from time import timezone
|
from time import timezone
|
||||||
from apps.system.models import Dict
|
from apps.system.models import Dict
|
||||||
from rest_framework.mixins import ListModelMixin
|
from rest_framework.mixins import ListModelMixin, UpdateModelMixin
|
||||||
from apps.vod.serializers import VRecordSerializer, VRecordUpdateSerializer, VideoListDetailSerializer, VideoSerializer, VideoUpdateSerializer
|
from apps.vod.serializers import VRecordSerializer, VRecordUpdateSerializer, VideoListDetailSerializer, VideoSerializer, VideoUpdateSerializer, ViewItemSerializer, ViewItemUpdateSerializer, View2Serializer, DatetimeSerializer
|
||||||
from apps.vod.models import Video, ViewRecord
|
from apps.vod.models import Video, ViewRecord, ViewItem, View1, View2
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from .vodclient import getAllClass, getPlayCode, searchMedia, getSignature
|
from .vodclient import getAllClass, getPlayCode, searchMedia, getSignature
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
@ -17,6 +17,8 @@ from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
from utils.queryset import get_child_queryset2
|
from utils.queryset import get_child_queryset2
|
||||||
|
from django.db.models import Sum
|
||||||
|
from django.db import connection
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
class ClassView(APIView):
|
class ClassView(APIView):
|
||||||
|
|
@ -69,6 +71,8 @@ class VideoViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet):
|
||||||
@action(methods=['get'], detail=False, perms_map={'get':'video_view'})
|
@action(methods=['get'], detail=False, perms_map={'get':'video_view'})
|
||||||
def myview(self, request, *args, **kwargs):
|
def myview(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
废弃接口
|
||||||
|
|
||||||
个人观看记录
|
个人观看记录
|
||||||
"""
|
"""
|
||||||
queryset = ViewRecord.objects.filter(user=request.user).order_by('-id')
|
queryset = ViewRecord.objects.filter(user=request.user).order_by('-id')
|
||||||
|
|
@ -78,10 +82,72 @@ class VideoViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet):
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
serializer = VRecordSerializer(queryset, many=True)
|
serializer = VRecordSerializer(queryset, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=True, perms_map={'get':'*'})
|
||||||
|
def my(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
本视频的我的观看统计
|
||||||
|
|
||||||
|
本视频的我的观看统计
|
||||||
|
"""
|
||||||
|
video = self.get_object()
|
||||||
|
user = request.user
|
||||||
|
ins, _ = View2.objects.get_or_create(create_by=user, video=video, defaults={'video': video, 'create_by': user})
|
||||||
|
return Response(View2Serializer(instance=ins).data)
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=True, perms_map={'get':'*'})
|
||||||
|
def start(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
开始播放
|
||||||
|
|
||||||
|
本次播放开始返回记录ID,后续计时使用
|
||||||
|
"""
|
||||||
|
video = self.get_object()
|
||||||
|
user = request.user
|
||||||
|
vi = ViewItem.objects.create(create_by=user, video=video)
|
||||||
|
cal_view1(vi)
|
||||||
|
return Response({"vi": vi.id})
|
||||||
|
|
||||||
|
# @action(methods=['get'], detail=False, perms_map={'get':'*'})
|
||||||
|
# def correct_cate(self, request, *args, **kwargs):
|
||||||
|
# for video in Video.objects.get_queryset(all=True).all():
|
||||||
|
# cate = video.category
|
||||||
|
# video.category_big = cate
|
||||||
|
# if cate.pid:
|
||||||
|
# video.category_big = cate.pid
|
||||||
|
# video.save()
|
||||||
|
# return Response()
|
||||||
|
|
||||||
|
def cal_view1(vi: ViewItem):
|
||||||
|
"""
|
||||||
|
统计视频播放数量
|
||||||
|
|
||||||
|
统计视频播放数量
|
||||||
|
"""
|
||||||
|
v1, _ = View1.objects.get_or_create(video=vi.video, defaults={"video": vi.video})
|
||||||
|
v2, _ = View2.objects.get_or_create(video=vi.video, create_by=vi.create_by, defaults={"video": vi.video, "create_by": vi.create_by})
|
||||||
|
v1.views = ViewItem.objects.filter(video=vi.video).count()
|
||||||
|
v1.viewp = View2.objects.filter(video=vi.video).count()
|
||||||
|
v1.save()
|
||||||
|
|
||||||
|
def cal_view2(vi: ViewItem):
|
||||||
|
"""
|
||||||
|
统计个人播放记录
|
||||||
|
|
||||||
|
统计个人播放记录
|
||||||
|
"""
|
||||||
|
v2, _ = View2.objects.get_or_create(video=vi.video, create_by=vi.create_by, defaults={"video": vi.video, "create_by": vi.create_by})
|
||||||
|
v2.views = ViewItem.objects.filter(video=vi.video).count()
|
||||||
|
v2.total_seconds = ViewItem.objects.filter(video=vi.video).aggregate(total=Sum('total_seconds'))['total']
|
||||||
|
v2.save()
|
||||||
|
|
||||||
|
|
||||||
class VRecordViewSet(ListModelMixin, GenericViewSet):
|
class VRecordViewSet(ListModelMixin, GenericViewSet):
|
||||||
|
"""
|
||||||
|
废弃接口
|
||||||
|
|
||||||
|
废弃接口
|
||||||
|
"""
|
||||||
perms_map = {'get':'viewrecord_view'}
|
perms_map = {'get':'viewrecord_view'}
|
||||||
queryset = ViewRecord.objects.all()
|
queryset = ViewRecord.objects.all()
|
||||||
search_fields = ['user__name', 'video__name']
|
search_fields = ['user__name', 'video__name']
|
||||||
|
|
@ -89,24 +155,111 @@ class VRecordViewSet(ListModelMixin, GenericViewSet):
|
||||||
ordering = ['-update_time']
|
ordering = ['-update_time']
|
||||||
|
|
||||||
|
|
||||||
|
class ViewItemViewSet(ListModelMixin, UpdateModelMixin, GenericViewSet):
|
||||||
|
perms_map = {'get': 'viewitem_view', 'put': '*'}
|
||||||
|
queryset = ViewItem.objects.select_related('create_by', 'video').all()
|
||||||
|
search_fields = ['create_by__name', 'video__name']
|
||||||
|
serializer_class = ViewItemSerializer
|
||||||
|
ordering = ['-id']
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.action == "update":
|
||||||
|
return ViewItemUpdateSerializer
|
||||||
|
return super().get_serializer_class()
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=False, perms_map={'get':'*'})
|
||||||
|
def my(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
我的观看记录
|
||||||
|
|
||||||
|
我的观看记录
|
||||||
|
"""
|
||||||
|
queryset = ViewItem.objects.filter(create_by=request.user).order_by('-id')
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = ViewItemSerializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
serializer = ViewItemSerializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
user = request.user
|
||||||
|
data = request.data
|
||||||
|
obj = self.get_object()
|
||||||
|
if obj.create_by != user:
|
||||||
|
raise ParseError('非创建人')
|
||||||
|
if ViewItem.objects.filter(video=obj.video, create_by=user).order_by('-id').first() != obj:
|
||||||
|
raise ParseError('存在新播放记录')
|
||||||
|
obj.current = data['current']
|
||||||
|
obj.total_seconds = obj.total_seconds + data['seconds']
|
||||||
|
obj.save()
|
||||||
|
cal_view2(obj)
|
||||||
|
return Response({'id': obj.id, 'current': obj.current, 'total_seconds': obj.total_seconds})
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=True, perms_map={'get':'*'})
|
||||||
|
def complete(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
完成播放
|
||||||
|
|
||||||
|
完成播放
|
||||||
|
"""
|
||||||
|
obj = self.get_object()
|
||||||
|
obj.current = 0
|
||||||
|
v2 = View2.objects.get(video=obj.video, create_by=obj.create_by)
|
||||||
|
if v2.total_seconds >= obj.video.duration * 0.6:
|
||||||
|
v2.is_completed = True
|
||||||
|
v2.save()
|
||||||
|
return Response()
|
||||||
|
raise ParseError('观看时长不足')
|
||||||
|
|
||||||
|
class View2ViewSet(ListModelMixin, GenericViewSet):
|
||||||
|
perms_map = {'get': 'view2_view', 'put': '*'}
|
||||||
|
queryset = View2.objects.select_related('create_by', 'video').all()
|
||||||
|
search_fields = ['create_by__name', 'video__name']
|
||||||
|
serializer_class = View2Serializer
|
||||||
|
ordering = ['-id']
|
||||||
|
|
||||||
|
@action(methods=['get'], detail=False, perms_map={'get':'*'})
|
||||||
|
def my(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
我的观看统计
|
||||||
|
|
||||||
|
我的观看统计
|
||||||
|
"""
|
||||||
|
queryset = View2.objects.filter(create_by=request.user).order_by('-id')
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = View2Serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
serializer = View2Serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class MyViewRecordAPIView(APIView):
|
class MyViewRecordAPIView(APIView):
|
||||||
|
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
def get(self, request, id , format=None):
|
def get(self, request, id , format=None):
|
||||||
"""
|
"""
|
||||||
|
废弃接口
|
||||||
|
|
||||||
该视频的本人观看信息
|
该视频的本人观看信息
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
video = Video.objects.get(pk=id)
|
video = Video.objects.get(pk=id)
|
||||||
except:
|
except:
|
||||||
return Response('视频不存在', status=HTTP_400_BAD_REQUEST)
|
return Response('视频不存在', status=HTTP_400_BAD_REQUEST)
|
||||||
record, _ = ViewRecord.objects.get_or_create(video=video, user=request.user, defaults={'video':video, 'user':request.user})
|
record = ViewRecord.objects.filter(video=video, user=request.user).first()
|
||||||
|
if record:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
record = ViewRecord.objects.create(video=video, user=request.user)
|
||||||
serializer = VRecordSerializer(instance=record)
|
serializer = VRecordSerializer(instance=record)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
def put(self, request, id, format=None):
|
def put(self, request, id, format=None):
|
||||||
"""
|
"""
|
||||||
|
废弃接口
|
||||||
|
|
||||||
更新该视频本人的观看信息
|
更新该视频本人的观看信息
|
||||||
params: {current:int}
|
params: {current:int}
|
||||||
"""
|
"""
|
||||||
|
|
@ -129,10 +282,13 @@ class MyViewRecordAPIView(APIView):
|
||||||
video.save()
|
video.save()
|
||||||
if request.data.get('current', None):
|
if request.data.get('current', None):
|
||||||
record.current = request.data.get('current')
|
record.current = request.data.get('current')
|
||||||
if timezone.now() > record.update_time + timedelta(hours=6):
|
if timezone.now() > record.update_time + timedelta(minutes=30):
|
||||||
record.views = record.views + 1
|
record.views = record.views + 1
|
||||||
video.views = video.views + 1
|
video.views = video.views + 1
|
||||||
video.save()
|
video.save()
|
||||||
|
else:
|
||||||
|
record.total_seconds = record.total_seconds + 10
|
||||||
|
record.save()
|
||||||
record.save()
|
record.save()
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
from apps.vod.serializers import DatetimeSerializer
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from utils.sql import query_all_dict
|
||||||
|
|
||||||
|
class AnalyseViewSet(GenericViewSet):
|
||||||
|
perms_map = {'post': '*'}
|
||||||
|
serializer_class = DatetimeSerializer
|
||||||
|
|
||||||
|
def is_valid(self, request):
|
||||||
|
data = request.data
|
||||||
|
sr = self.get_serializer(data=data)
|
||||||
|
sr.is_valid(raise_exception=True)
|
||||||
|
vdata = sr.validated_data
|
||||||
|
return vdata
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=False)
|
||||||
|
def group_by_video_category_big(self, request):
|
||||||
|
"""
|
||||||
|
视频大类播放量统计
|
||||||
|
|
||||||
|
视频大类播放量统计
|
||||||
|
"""
|
||||||
|
vdata = self.is_valid(request)
|
||||||
|
sql_str = """select
|
||||||
|
d.name as "视频大类",
|
||||||
|
count(v.id) as "视频数量",
|
||||||
|
count(vi.id) as "观看总次数",
|
||||||
|
count(distinct vi.create_by_id) as "观看总人数"
|
||||||
|
from vod_video v
|
||||||
|
left join system_dict d on d.id = v.category_big_id
|
||||||
|
left join vod_viewitem vi on vi.video_id = v.id
|
||||||
|
and vi.create_time >= %s
|
||||||
|
and vi.create_time <= %s
|
||||||
|
group by d.id
|
||||||
|
order by "视频数量" desc, d.sort
|
||||||
|
limit %s
|
||||||
|
"""
|
||||||
|
return Response(query_all_dict(sql_str, [vdata['start_time'], vdata['end_time'], vdata['limit']]))
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=False)
|
||||||
|
def group_by_user_view(self, request):
|
||||||
|
"""
|
||||||
|
个人观看量统计
|
||||||
|
|
||||||
|
个人观看量统计
|
||||||
|
"""
|
||||||
|
vdata = self.is_valid(request)
|
||||||
|
sql_str = """select
|
||||||
|
u.name as "姓名",
|
||||||
|
u.username as "账号",
|
||||||
|
count(v2.is_completed is true) as "观看完成视频总数",
|
||||||
|
count(distinct vi.video_id) as "观看视频总数",
|
||||||
|
count(vi.id) as "观看总次数" ,
|
||||||
|
sum(vi.total_seconds/60) as "观看总时间"
|
||||||
|
from vod_viewitem vi
|
||||||
|
left join vod_video v on v.id = vi.video_id
|
||||||
|
left join system_user u on u.id = vi.create_by_id
|
||||||
|
left join vod_view2 v2 on v2.create_by_id = vi.create_by_id and v2.video_id = vi.video_id
|
||||||
|
where vi.create_time >= %s and vi.create_time <= %s
|
||||||
|
group by u.id
|
||||||
|
order by "观看完成视频总数" desc, "账号"
|
||||||
|
limit %s
|
||||||
|
"""
|
||||||
|
return Response(query_all_dict(sql_str, [vdata['start_time'], vdata['end_time'], vdata['limit']]))
|
||||||
|
|
||||||
|
|
||||||
|
@action(methods=['post'], detail=False)
|
||||||
|
def group_by_org_view(self, request):
|
||||||
|
"""
|
||||||
|
单位观看量统计
|
||||||
|
|
||||||
|
单位观看量统计
|
||||||
|
"""
|
||||||
|
vdata = self.is_valid(request)
|
||||||
|
sql_str = """select
|
||||||
|
o.name as "单位名称",
|
||||||
|
count(v2.is_completed is true) as "观看完成视频总数",
|
||||||
|
count(distinct vi.video_id) as "观看视频总数",
|
||||||
|
count(vi.id) as "观看总次数" ,
|
||||||
|
sum(vi.total_seconds/60) as "观看总时间"
|
||||||
|
from vod_viewitem vi
|
||||||
|
left join vod_video v on v.id = vi.video_id
|
||||||
|
left join system_user u on u.id = vi.create_by_id
|
||||||
|
left join system_organization o on o.id = u.dept_id
|
||||||
|
left join vod_view2 v2 on v2.create_by_id = vi.create_by_id and v2.video_id = vi.video_id
|
||||||
|
where vi.create_time >= %s and vi.create_time <= %s
|
||||||
|
group by o.id
|
||||||
|
order by "观看完成视频总数" desc, o.sort
|
||||||
|
limit %s
|
||||||
|
"""
|
||||||
|
return Response(query_all_dict(sql_str, [vdata['start_time'], vdata['end_time'], vdata['limit']]))
|
||||||
Binary file not shown.
|
|
@ -48,7 +48,9 @@ INSTALLED_APPS = [
|
||||||
'apps.supervision',
|
'apps.supervision',
|
||||||
'apps.quality',
|
'apps.quality',
|
||||||
'apps.vod',
|
'apps.vod',
|
||||||
'apps.consulting'
|
'apps.consulting',
|
||||||
|
'apps.exam',
|
||||||
|
'apps.ops'
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -167,8 +169,8 @@ REST_FRAMEWORK = {
|
||||||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S',
|
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S',
|
||||||
'DATE_FORMAT': '%Y-%m-%d',
|
'DATE_FORMAT': '%Y-%m-%d',
|
||||||
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
|
||||||
'UNAUTHENTICATED_USER': None,
|
# 'UNAUTHENTICATED_USER': None,
|
||||||
'UNAUTHENTICATED_TOKEN': None,
|
# 'UNAUTHENTICATED_TOKEN': None,
|
||||||
}
|
}
|
||||||
# simplejwt配置
|
# simplejwt配置
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
from .settings import *
|
|
||||||
DEBUG = False
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,17 +19,13 @@ from django.contrib import admin
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
from rest_framework.documentation import include_docs_urls
|
from rest_framework.documentation import include_docs_urls
|
||||||
from rest_framework_simplejwt.views import (TokenObtainPairView,
|
from rest_framework_simplejwt.views import TokenRefreshView
|
||||||
TokenRefreshView)
|
|
||||||
|
|
||||||
from apps.system.views import FileViewSet, LogoutView, Login2View
|
from apps.system.views import FileViewSet, LogoutView, Login2View, MyTokenView
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.register('file', FileViewSet, basename="file")
|
router.register('file', FileViewSet, basename="file")
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
|
||||||
from rest_framework_simplejwt.views import TokenViewBase
|
|
||||||
from apps.system.views import WXMPlogin,mediaauth
|
from apps.system.views import WXMPlogin,mediaauth
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
|
|
@ -54,7 +50,7 @@ urlpatterns = [
|
||||||
path('api/admin/', admin.site.urls),
|
path('api/admin/', admin.site.urls),
|
||||||
path('api/mediaauth/',mediaauth),
|
path('api/mediaauth/',mediaauth),
|
||||||
path('api/wxmplogin/',WXMPlogin.as_view()),
|
path('api/wxmplogin/',WXMPlogin.as_view()),
|
||||||
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
|
path('api/token/', MyTokenView.as_view(), name='token_obtain_pair'),
|
||||||
path('api/token2/', Login2View.as_view(), name='token_obtain_2'),
|
path('api/token2/', Login2View.as_view(), name='token_obtain_2'),
|
||||||
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
|
||||||
path('api/token/black/', LogoutView.as_view(), name='token_black'),
|
path('api/token/black/', LogoutView.as_view(), name='token_black'),
|
||||||
|
|
@ -64,6 +60,8 @@ urlpatterns = [
|
||||||
path('api/quality/', include('apps.quality.urls')),
|
path('api/quality/', include('apps.quality.urls')),
|
||||||
path('api/vod/', include('apps.vod.urls')),
|
path('api/vod/', include('apps.vod.urls')),
|
||||||
path('api/consulting/', include('apps.consulting.urls')),
|
path('api/consulting/', include('apps.consulting.urls')),
|
||||||
|
path('', include('apps.ops.urls')),
|
||||||
|
path('', include('apps.exam.urls')),
|
||||||
|
|
||||||
path('api/docs/', include_docs_urls(title="接口文档",authentication_classes=[], permission_classes=[])),
|
path('api/docs/', include_docs_urls(title="接口文档",authentication_classes=[], permission_classes=[])),
|
||||||
url(r'^api/swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
url(r'^api/swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,15 @@ class SoftDeletableManagerMixin(object):
|
||||||
'''
|
'''
|
||||||
_queryset_class = SoftDeletableQuerySet
|
_queryset_class = SoftDeletableQuerySet
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self, all=False):
|
||||||
'''
|
'''
|
||||||
Return queryset limited to not deleted entries.
|
Return queryset limited to not deleted entries.
|
||||||
'''
|
'''
|
||||||
kwargs = {'model': self.model, 'using': self._db}
|
kwargs = {'model': self.model, 'using': self._db}
|
||||||
if hasattr(self, '_hints'):
|
if hasattr(self, '_hints'):
|
||||||
kwargs['hints'] = self._hints
|
kwargs['hints'] = self._hints
|
||||||
|
if all:
|
||||||
|
return self._queryset_class(**kwargs)
|
||||||
return self._queryset_class(**kwargs).filter(is_deleted=False)
|
return self._queryset_class(**kwargs).filter(is_deleted=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -49,6 +50,9 @@ class SoftDeletableManager(SoftDeletableManagerMixin, models.Manager):
|
||||||
|
|
||||||
|
|
||||||
class BaseModel(models.Model):
|
class BaseModel(models.Model):
|
||||||
|
"""
|
||||||
|
自增ID,带有create_time, update_time, is_deleted
|
||||||
|
"""
|
||||||
create_time = models.DateTimeField(
|
create_time = models.DateTimeField(
|
||||||
default=timezone.now, verbose_name='创建时间', help_text='创建时间')
|
default=timezone.now, verbose_name='创建时间', help_text='创建时间')
|
||||||
update_time = models.DateTimeField(
|
update_time = models.DateTimeField(
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ class MyPagination(PageNumberPagination):
|
||||||
page_size = 10
|
page_size = 10
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = 'page_size'
|
||||||
|
|
||||||
|
def paginate_queryset(self, queryset, request, view=None):
|
||||||
|
if request.query_params.get('pageoff', None) or request.query_params.get('page', None) == '0':
|
||||||
|
if queryset.count() < 500:
|
||||||
|
return None
|
||||||
|
raise ParseError('单次请求数据量大,请分页获取')
|
||||||
|
return super().paginate_queryset(queryset, request, view=view)
|
||||||
|
|
||||||
class PageOrNot:
|
class PageOrNot:
|
||||||
def paginate_queryset(self, queryset):
|
pass
|
||||||
if (self.paginator is None):
|
|
||||||
return None
|
|
||||||
elif (self.request.query_params.get('pageoff', None)) and queryset.count()<500:
|
|
||||||
return None
|
|
||||||
elif (self.request.query_params.get('pageoff', None)) and queryset.count()>=500:
|
|
||||||
raise ParseError('单次请求数据量大,请求中止')
|
|
||||||
return self.paginator.paginate_queryset(queryset, self.request, view=self)
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
def query_all_dict(sql, params=None):
|
||||||
|
'''
|
||||||
|
查询所有结果返回字典类型数据
|
||||||
|
:param sql:
|
||||||
|
:param params:
|
||||||
|
:return:
|
||||||
|
'''
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
if params:
|
||||||
|
cursor.execute(sql, params=params)
|
||||||
|
else:
|
||||||
|
cursor.execute(sql)
|
||||||
|
columns = [desc[0] for desc in cursor.description]
|
||||||
|
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
def query_one_dict(sql, params=None):
|
||||||
|
"""
|
||||||
|
查询一个结果返回字典类型数据
|
||||||
|
:param sql:
|
||||||
|
:param params:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
if params:
|
||||||
|
cursor.execute(sql, params=params)
|
||||||
|
else:
|
||||||
|
cursor.execute(sql)
|
||||||
|
columns = [desc[0] for desc in cursor.description]
|
||||||
|
row = cursor.fetchone()
|
||||||
|
return dict(zip(columns, row))
|
||||||
Loading…
Reference in New Issue