fix conflict

This commit is contained in:
曹前明 2022-11-28 10:15:38 +08:00
commit 7ab775d1cb
94 changed files with 7186 additions and 291 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
.vs/
venv/
media/
temp/
*.log
static/
__pycache__/

View File

@ -3,7 +3,7 @@ ENV = 'development'
# base 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 = 'http://47.95.0.242:9101/api'

View File

@ -17,6 +17,7 @@
"dependencies": {
"@riophae/vue-treeselect": "^0.4.0",
"axios": "0.18.1",
"echarts": "^5.4.0",
"element-china-area-data": "^5.0.2",
"element-ui": "2.13.0",
"file-saver": "^2.0.2",
@ -48,9 +49,9 @@
"eslint-plugin-vue": "5.2.2",
"html-webpack-plugin": "3.2.0",
"mockjs": "1.0.1-beta3",
"node-sass": "^4.13.1",
"node-sass": "^6.0.1",
"runjs": "^4.3.2",
"sass-loader": "^7.1.0",
"sass-loader": "^10.0.1",
"script-ext-html-webpack-plugin": "2.1.3",
"script-loader": "0.7.2",
"serve-static": "^1.13.2",

213
client/src/api/exam.js Normal file
View File

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

View File

@ -49,11 +49,108 @@ export function getVideoPlayCode(id) {
method: 'get'
})
}
//已弃用
export function getMyView(id, data) {
return request({
url: `/vod/video/${id}/myview/`,
method: 'get',
data
})
}
//已弃用
export function refreshMyView(id, data) {
return request({
url: `/vod/video/${id}/myview/`,
method: 'put',
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
})
}

View File

@ -300,12 +300,19 @@ export const asyncRoutes = [
meta: { title: '上传视频', perms: ['video_create'] }
},
{
path: 'index/:id',
name: 'Index',
path: 'index',
name: 'index',
component: () => import('@/views/testvideo/index.vue'),
meta: { title: '视频播放', perms: ['video_view'] },
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',
component: Layout,

View File

@ -7,7 +7,8 @@ const getDefaultState = () => {
token: getToken(),
name: '',
avatar: '',
perms: []
perms: [],
dept:''
}
}

View File

@ -249,24 +249,24 @@
<el-row>
<el-col :span="12">
<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-col>
<el-col :span="12">
<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-col>
</el-row>
<el-row>
<el-col :span="12">
<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-col>
<el-col :span="12">
<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-col>
</el-row>
@ -682,8 +682,13 @@
style="padding: 10px 20px;position: relative"
>
<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 style="display: inline-block;width: 100px;font-weight: bold;">能力领域</span>{{showData.data.afield_name}}</p>
<p><span class="drawerInfoTitle">能力类型 </span>{{showData.data.atype_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>
<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>
@ -716,8 +721,9 @@
<script>
import {
getQtaskMy,
qtaskDetail,
getQtask,
qactionMy,
qactionList,
getQualityMy,
createQuali,
qactionDelete,
@ -732,7 +738,7 @@
} from "@/api/ability";
import {getDictList} from "@/api/dict";
import {genTree} from "@/utils";
import {getOrgList} from "@/api/org";
import {getOrgList, getSubOrgList} from "@/api/org";
import {getCMAGroup} from "@/api/cma";
import checkPermission from "@/utils/permission";
import Pagination from "@/components/Pagination"; // secondary package based on el-pagination
@ -854,6 +860,9 @@
this.afieldOptions = [];
this.getOptions();
this.getProvince();
// debugger;
console.log(this.$store.state.user.dept);
this.pageForm.org = this.$store.state.user.dept;
},
methods: {
@ -943,8 +952,9 @@
},
checkPermission,
getTableList() {
this.listLoading = true;
this.listLoading = true;getQtaskMy
getQtaskMy(this.pageForm).then((response) => {
// qtaskDetail(this.pageForm).then((response) => {
if (response.data) {
debugger;
this.taskList = response.data;
@ -956,9 +966,15 @@
});
},
getGroup() {
getOrgList({can_supervision: true}).then((res) => {
if (this.checkPermission(["record_confirm"])) {
getOrgList({ can_supervision: true }).then((res) => {
this.orgData = res.data;
});
} else {
getSubOrgList().then((res) => {
this.orgData = res.data;
});
}
},
getrecordlist() {
this.getTableList();
@ -970,16 +986,18 @@
this.checkedItem = obj;
this.qtask = obj.qtask;
this.qtaskName = obj.qtask_.name;
if(obj.org== this.$store.state.user.dept){
this.buttonsShow = true;
}else{
this.buttonsShow = false;
}
this.listQuery.qtask = obj.qtask;
qactionMy(this.listQuery).then((res) => {
qactionList(this.listQuery).then((res) => {
this.recordList = res.data;
})
},
///////
getList() {
qactionMy(this.listQuery).then((res) => {
qactionList(this.listQuery).then((res) => {
this.recordList = res.data;
})
},
@ -1134,6 +1152,9 @@
});
},
handleDetail(type,item){
debugger;
console.log(type)
console.log(item)
let that = this;
that.drawer = true;
this.showData.id = item.id;
@ -1148,7 +1169,9 @@
if (res.code>=200) {
let updateDetail = res.data.update_detail;//更改字段
updateDetail.forEach(item=>{
// debugger;
this.fieldList2.push(item.field);//所有字段
debugger;
});
this.drawer = true;
}
@ -1177,5 +1200,9 @@
</script>
<style scoped>
.drawerInfoTitle{
display: inline-block;
width: 120px;
font-weight: bold;
}
</style>

View File

@ -278,12 +278,15 @@
<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>{{afield_name}}</p>
<p><span style="display: inline-block;width: 160px;font-weight: bold;">新增对象数量</span>{{item.num}}</p>
<p><span style="display: inline-block;width: 160px;font-weight: bold;">新增参数数量</span>{{item.num2}}</p>
<p><span style="display: inline-block;width: 160px;font-weight: bold;">新增方法标准数量</span>{{item.num3}}</p>
<p><span style="display: inline-block;width: 160px;font-weight: bold;">新增产品标准数量</span>{{item.num4}}</p>
<p><span style="display: inline-block;width: 100px;font-weight: bold;">上传文件</span>{{showData.data.file_.name}}</p>
<el-link :href="showData.data.file_.file" target="_blank" type="primary">{{showData.data.file_.name}}</el-link>
<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><span style="display: inline-block;width: 100px;font-weight: bold;">上传文件</span>
<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-button
v-if="actionType==='confirm'"

View File

@ -60,7 +60,6 @@
highlight-current-row
max-height="600"
@sort-change="changeTableSort"
:span-method="objectSpanMethod"
>
<el-table-column type="index" width="50" />
@ -116,7 +115,7 @@
<template slot-scope="scope" v-if="scope.row.files">
<el-link
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>
个文件</el-link
@ -138,33 +137,56 @@
@pagination="getList"
/>
</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>
</template>
<style lang="scss">
.el-transfer-panel {
width: 470px;
}
.el-transfer__buttons {
padding: 0 2px;
.el-button {
display: block;
}
}
</style>
<script>
import { getOrgList, getSubOrgList } from "@/api/org";
import { getRecordList } from "@/api/ability";
import checkPermission from "@/utils/permission";
import Pagination from "@/components/Pagination"; // secondary package based on el-pagination
const defaultrecord = {
name: "",
};
export default {
components: { Pagination},
data() {
return {
record: defaultrecord,
record: {},
recordList: {
count: 0,
},
@ -215,7 +237,9 @@ export default {
},
listLoading: false,
dialogVisible: false,
drawerLiminted:false,
dialogType: "new",
orgData:[],
rule1: {
name: [{ required: true, message: "请输入", trigger: "blur" }],
},
@ -225,10 +249,21 @@ export default {
watch: {},
created() {
this.getState();
this.getGroup();
},
methods: {
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() {
if(this.checkPermission(["record_confirm"])){
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() {
this.listQuery.page = 1;
this.getList();
@ -261,8 +303,32 @@ export default {
};
this.getList();
},
handleRecord(item){
this.drawerLiminted = true;
this.record = item;
},
},
};
</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<template>
<el-container>
<el-header style="height: 80px;padding: 0">
<el-header style="height: 80px;padding: 0;position: absolute;">
<el-row class="biaotou">
<el-col :span="20" style="text-align:center;color:seashell;font-size:32px;line-height: 70px;">
{{ video.name }}
@ -8,8 +8,10 @@
</el-row>
</el-header>
<el-main style="margin-top: 70px;">
<div style="margin:20px 12%;">
<div class="content" @click="clicknub">
<el-row>
<el-col :sm="24" :lg="15" :xl="13">
<div style="margin:20px;">
<div class="content">
<video
:id="tcPlayerId"
width="1000"
@ -26,8 +28,8 @@
<el-col class="firstLineDetail">
<div class="firstLineText">{{ video.name }}</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-s-custom">{{video.viewsp}}</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_n}}</el-button>
</div>
</el-col>
<div style="font-size: 15px">
@ -35,11 +37,27 @@
</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-container>
</template>
<script>
import {getVideo, getMyView, getVideoPlayCode} from "@/api/video";
import {getVideo,videoStart,myViewItem, myView,getMyView,refreshViewItem, getVideoPlayCode,viewItemComplete} from "@/api/video";
export default {
name: 'TencentPlayer',
props: {
@ -60,57 +78,65 @@
video: {id: 0},
description: '',
name: '',
videoFileid:false,
userName:""
userName:"",
playTimer:null,
isFirstView:true,
id:'',
videoViId:'',
recordList:[],
};
},
mounted(){
//debugger;
this.userName = this.$store.state.user.name;
if(this.player!==null){
this.player.null;
}
if(this.$route.params.id){
this.id = this.$route.params.id;
this.videoFileId = this.$route.params.fileid;
if(this.$route.query.id){
this.id = this.$route.query.id;
let videoId = sessionStorage.getItem('videoId');
let videoFileId = sessionStorage.getItem('videoFileId');
if(videoId){
if(videoId!==undefined&&videoId!==null&&videoId!==''){
sessionStorage.removeItem('videoId');
sessionStorage.setItem('videoId',this.$route.params.id);
}else{
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{
this.id = sessionStorage.getItem('videoId');
this.videoFileId = sessionStorage.getItem('videoFileId');
}
this.getPlayCode(this.videoFileId);
this.getVideo();
this.clicknub();
this.getMyVideoView();
},
methods: {
//获取视频详情
getVideo() {
getVideo(this.id).then((response) => {
let that = this;
getVideo(that.id).then((response) => {
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){
let that = this;
getVideoPlayCode(id).then(res=>{
if(res.data){
//debugger;
console.log(res.data);
that.videoFileid = true;
that.videoPsign = res.data.psign;
that.initVideo();
}
@ -118,15 +144,19 @@
},
//视频观看次数
clicknub() {
debugger;
getMyView(this.id).then((response) => {});
myView(this.id).then((response) => {
// debugger;
console.log(response.data);
});
},
//初始化视频播放器
initVideo() {
//debugger;
console.log(this.userName);
let that = this;
let text = "用户:"+that.userName;
const playerParm = {
getMyView(that.id,{}).then((response) => {
// debugger;
// console.log(response.data);
let current = response.data.current;
let playerParm = {
fileID: that.videoFileId,
appID: that.videoAppId,
psign: that.videoPsign,
@ -134,33 +164,87 @@
DynamicWatermark: {
speed: 0.1,
content: "用户:"+that.userName
}
}
};
/*
* plugins: {
ContinuePlay: { // 开启续播功能
// auto: true, //[可选] 是否在视频播放后自动续播
// text:'上次播放至 ', //[可选] 提示文案
// btnText: '恢复播放' //[可选] 按钮文案
},
}
* */
//debugger;
},
controlBar:{
playbackRateMenuButton:false
},
};
if (!that.player) {
that.player = window.TCPlayer(that.tcPlayerId, playerParm);
that.player.on('play', function(error) {
// 做一些处理
getMyView(that.id).then((response) => {});
});
} else {
that.player.loadVideoByID(playerParm);
that.player.loadVideoByID(that.tcPlayerId);
}
that.player.currentTime(current);
//视频播放
that.player.on('play', function(error) {
// debugger;
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)
});
//视频暂停
that.player.on('pause', function(error) {
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 () {
this.player.dispose()
let that = this;
if(that.playTimer!==null){
clearInterval(that.playTimer);
that.playTimer = null;
}
that.player.dispose()
}
};
</script>
@ -173,7 +257,7 @@
color: #333;
text-align: center;
line-height: 40px;
position: fixed;
position: absolute;
width: 100%;
z-index: 1010;
}
@ -224,4 +308,30 @@
border: 1px solid #e74e4e;
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>

View File

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

View File

@ -54,12 +54,12 @@
<div class="bottom clearfix">
<el-row>
<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-col>
<el-col :span="6">
<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-col>
<el-col :span="12">
@ -182,7 +182,16 @@
}else{
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}})
}
},
};

View File

@ -60,4 +60,5 @@
<style lang="scss">
@import "uview-ui/index.scss";
@import "common/demo.scss";
@import "common/uni-ui.scss";
</style>

View File

@ -44,6 +44,15 @@ const install = (Vue, vm) => {
let getVideos = (params={})=>vm.$u.get('/vod/video/', params);//点播视频
let getDickey = (params={})=>vm.$u.get('/system/dict/', params);//查询字典
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,
getCode,
codeLogin,
@ -64,7 +73,13 @@ const install = (Vue, vm) => {
getDocument,
getVideos,
getDickey,
putMyVideoView
putMyVideoView,
getExamList,
startExam,
submitExam,
examRecord,
examRecordDetail
};
}

View File

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

View File

@ -11,15 +11,6 @@
// }]
// },
"pages": [
{
"path" : "pages/login/login",
"style" :
{
"navigationBarTitleText": "验证码登录",
"enablePullDownRefresh": false
}
},
{
"path" : "pages/home/home",
"style" :
@ -27,8 +18,16 @@
"navigationBarTitleText": "",
"enablePullDownRefresh": false
}
},
{
"path" : "pages/login/login",
"style" :
{
"navigationBarTitleText": "验证码登录",
"enablePullDownRefresh": false
}
},
{
"path" : "pages/my/my",
"style" :
@ -124,7 +123,60 @@
"path" : "pages/vod/video",
"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
}
@ -147,12 +199,18 @@
"selectedIconPath": "static/common/homec.png",
"text": "主页"
},
/* {
{
"pagePath": "pages/exam/index",
"iconPath": "static/common/dati.png",
"selectedIconPath": "static/common/datic.png",
"text": "答题"
},
{
"pagePath": "pages/vod/video",
"iconPath": "static/common/play.png",
"selectedIconPath": "static/common/playc.png",
"text": "点播"
}, */
},
{
"pagePath": "pages/my/my",
"iconPath": "static/common/me.png",

View File

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

View File

@ -0,0 +1,22 @@
<template>
<view>
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
}
}
</script>
<style>
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,25 +23,10 @@
<u-icon name="arrow-right" color="#969799" size="28"></u-icon>
</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">
<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="list-dot" title="考试记录" @click="examRecord"></u-cell-item>
<u-cell-item icon="close" title="退出" @click="Logout"></u-cell-item>
</u-cell-group>
</view>
@ -83,7 +68,13 @@
}).catch(e=>{})
}
});
}
},
examRecord(){
uni.navigateTo({
url:'/pages/exam/record'
})
},
}
}
</script>

View File

@ -31,7 +31,7 @@
</video>
<view style="color:darkblue;margin-left:4upx">
<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 File

@ -2,6 +2,7 @@
// uni.scss中引入的样式会同时混入到全局样式文件和单独每一个页面的样式中造成微信程序包太大
// 故uni.scss只建议放scss变量名相关样式其他的样式可以通过main.js或者App.vue引入
$theme-color:#0080d1;
$u-main-color: #303133;
$u-content-color: #606266;
$u-tips-color: #909399;
@ -36,3 +37,9 @@ $u-type-info-light: #f4f4f5;
$u-form-item-height: 70rpx;
$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;

View File

@ -19,6 +19,7 @@ from apps.supervision.models import Content, Record
from apps.supervision.serializers import ContentSerializer, RecordCreateSerializer, RecordSerializer
from apps.system.mixins import CreateUpdateCustomMixin
from utils.queryset import get_child_queryset2
from apps.system.permission_data import RbacFilterSet
from django.utils import timezone
from apps.supervision.permission import RecordPermission
from django.utils.decorators import method_decorator
@ -60,7 +61,7 @@ class AbilityContentViewSet(CreateUpdateCustomMixin, ModelViewSet):
def perform_update(self, serializer):
serializer.save(update_by = self.request.user)
class AbilityRecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
class AbilityRecordViewSet(RbacFilterSet, PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
perms_map = {'get': '*', 'post': '*',
'put': '*', 'delete': '*'}
queryset = Record.objects.filter(content__cate=2)
@ -70,25 +71,6 @@ class AbilityRecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
ordering = ['-task', 'content__sortnum', '-create_time']
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):
if not self.request.query_params.get('pageoff', None):
queryset = queryset.exclude(state='待发布')

View File

@ -12,6 +12,9 @@ from utils.pagination import PageOrNot
from rest_framework.exceptions import ParseError
from rest_framework import serializers
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):
perms_map = {'get': '*'}
@ -20,21 +23,24 @@ class QualiLibViewSet(PageOrNot, ListModelMixin, GenericViewSet):
search_fields = ['name']
ordering = ['-create_time']
class QualiViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
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
search_fields = ['name', 'type', 'grade', 'scope', 'level', 'description']
ordering = ['org', 'org__sort', 'create_time']
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):
"""
我的资质
"""
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)
if page is not None:
@ -44,8 +50,10 @@ class QualiViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
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()
serializer_class = QTaskListSerializer
ordering = ['-create_time']
@ -55,7 +63,7 @@ class QTaskViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, Up
return QTaskCreateUpdateSerializer
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
def start(self, request, *args, **kwargs):
"""
@ -68,6 +76,7 @@ class QTaskViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, Up
return Response()
return Response('任务状态错误', status=status.HTTP_400_BAD_REQUEST)
class QOrgViewSet(ListModelMixin, GenericViewSet):
perms_map = {'get': 'qtask_view'}
queryset = QOrg.objects.select_related('qtask', 'org')
@ -75,22 +84,27 @@ class QOrgViewSet(ListModelMixin, GenericViewSet):
serializer_class = QOrgListSerializer
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):
"""
我的报送任务
"""
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)
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)
def cal_count(qtask, org):
qs = QAction.objects.filter(qtask=qtask, belong_dept=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.save()
class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModelMixin,GenericViewSet):
class QActionViewSet(RbacFilterSet, PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModelMixin, GenericViewSet):
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']
serializer_class = QActionListSerializer
@ -112,21 +128,23 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
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)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
# @action(methods=['get'], detail=False, perms_map={'get': '*'})
# def my(self, request, *args, **kwargs):
# """
# 我的报送操作
# """
# user = self.request.user
# queryset = self.filter_queryset(
# self.get_queryset().filter(belong_dept=user.dept))
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
# 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)
def perform_destroy(self, instance):
user = self.request.user
@ -137,7 +155,7 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
raise ParseError('报送已确认, 不可删除')
@action(methods=['post'], detail=False,
perms_map = {'post':'qaction_create'}, serializer_class=QActionServiceSerializer)
perms_map={'post': 'qaction_create'}, serializer_class=QActionServiceSerializer)
@transaction.atomic
def service_update(self, request, *args, **kwargs):
"""
@ -152,7 +170,7 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
return Response()
@action(methods=['post'], detail=False,
perms_map = {'post':'qaction_create'}, serializer_class=QActionQualiCreateSerializer)
perms_map={'post': 'qaction_create'}, serializer_class=QActionQualiCreateSerializer)
@transaction.atomic
def quali_create(self, request, *args, **kwargs):
"""
@ -167,7 +185,7 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
return Response()
@action(methods=['post'], detail=False,
perms_map = {'post':'qaction_create'}, serializer_class=QActionQualiUpdateSerializer)
perms_map={'post': 'qaction_create'}, serializer_class=QActionQualiUpdateSerializer)
@transaction.atomic
def quali_update(self, request, *args, **kwargs):
"""
@ -186,22 +204,25 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
obj.save()
for k, v in value2.items():
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':
old_citys = set(old_data['citys'])
new_citys = set(v)
removes = old_citys.difference(new_citys)
adds = new_citys.difference(old_citys)
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:
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)
return Response()
@action(methods=['post'], detail=False,
perms_map = {'post':'qaction_create'}, serializer_class=QActionACreateSerializer)
perms_map={'post': 'qaction_create'}, serializer_class=QActionACreateSerializer)
@transaction.atomic
def ability_create(self, request, *args, **kwargs):
"""
@ -216,7 +237,7 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
return Response()
@action(methods=['post'], detail=False,
perms_map = {'post':'qaction_create'}, serializer_class=QActionNoChangeSerializer)
perms_map={'post': 'qaction_create'}, serializer_class=QActionNoChangeSerializer)
@transaction.atomic
def ability_nochange(self, request, *args, **kwargs):
"""
@ -225,13 +246,14 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
user = request.user
serializer = self.get_serializer(data=request.data)
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
cal_count(vdata['qtask'], user.dept)
return Response()
@action(methods=['post'], detail=False,
perms_map = {'post':'qaction_create'}, serializer_class=QActionNoChangeSerializer)
perms_map={'post': 'qaction_create'}, serializer_class=QActionNoChangeSerializer)
@transaction.atomic
def quali_nochange(self, request, *args, **kwargs):
"""
@ -240,13 +262,13 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
user = request.user
serializer = self.get_serializer(data=request.data)
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
cal_count(vdata['qtask'], user.dept)
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)
@transaction.atomic
def confirm(self, request, *args, **kwargs):
@ -263,9 +285,11 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
elif obj.action == 'quali:create':
serializer = QualiCreateSerializer(data=obj.value2)
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':
qualiLib, _ = QualiLib.objects.get_or_create(name=instance.name)
qualiLib, _ = QualiLib.objects.get_or_create(
name=instance.name)
levels = qualiLib.levels
levels.append(instance.level)
le = list(set(levels))
@ -284,8 +308,3 @@ class QActionViewSet(PageOrNot, ListModelMixin, DestroyModelMixin, RetrieveModel
obj.save()
cal_count(obj.qtask, obj.belong_dept)
return Response(status=status.HTTP_200_OK)

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
server/apps/exam/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ExamConfig(AppConfig):
name = 'exam'

View File

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

View File

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

View File

@ -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': '题目',
},
),
]

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -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='结束答题时间'),
),
]

View File

@ -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='是否公开'),
),
]

View File

135
server/apps/exam/models.py Normal file
View File

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

View File

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

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
server/apps/exam/urls.py Normal file
View File

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

421
server/apps/exam/views.py Normal file
View File

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

View File

3
server/apps/ops/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
server/apps/ops/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class OpsConfig(AppConfig):
name = 'ops'
verbose_name = '系统监控'

View File

@ -0,0 +1 @@
LOG_NOT_FONED = {"code": "log_not_found", "detail": "日志不存在"}

View File

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

View File

@ -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请求日志',
},
),
]

View File

@ -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),
),
]

View File

235
server/apps/ops/mixins.py Normal file
View File

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

49
server/apps/ops/models.py Normal file
View File

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

17
server/apps/ops/tasks.py Normal file
View File

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

3
server/apps/ops/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
server/apps/ops/urls.py Normal file
View File

@ -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())
]

159
server/apps/ops/views.py Normal file
View File

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

View File

@ -98,7 +98,7 @@ class SubtaskViewSet(PageOrNot, CreateUpdateCustomMixin, OptimizationMixin, Mode
if has_permission('inspecttask_create', self.request.user):
return queryset
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':'*'})
def self(self, request, *args, **kwargs):

View File

@ -209,7 +209,7 @@ from utils.queryset import get_child_queryset2
from .permission import RecordPermission
class RecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
class RecordViewSet(RbacFilterSet, PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
perms_map = {'get': '*', 'post': 'record_create',
'put': 'record_update', 'delete': 'record_delete'}
queryset = Record.objects.filter(content__cate=1)
@ -219,25 +219,6 @@ class RecordViewSet(PageOrNot, CreateUpdateCustomMixin, ModelViewSet):
ordering = ['-task', 'content__sortnum', '-create_time']
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):
if not self.request.query_params.get('pageoff', None):
queryset = queryset.exclude(state='待发布')

View File

@ -1,5 +1,4 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin
from .models import User, Organization, Role, Permission, DictType, Dict, File
# Register your models here.
admin.site.register(User)
@ -7,5 +6,5 @@ admin.site.register(Organization)
admin.site.register(Role)
admin.site.register(Permission)
admin.site.register(DictType)
admin.site.register(Dict, SimpleHistoryAdmin)
admin.site.register(Dict)
admin.site.register(File)

View File

@ -4,7 +4,6 @@ from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import simple_history.models
class Migration(migrations.Migration):
@ -54,7 +53,6 @@ class Migration(migrations.Migration):
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='File',

View 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',
),
]

View File

@ -4,7 +4,6 @@ import django.utils.timezone as timezone
from django.db.models.query import QuerySet
from utils.model import SoftModel, BaseModel
from simple_history.models import HistoricalRecords
class Province(models.Model):
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,
on_delete=models.SET_NULL, verbose_name='')
is_used = models.BooleanField('是否有效', default=True)
history = HistoricalRecords()
class Meta:
verbose_name = '字典'

View File

@ -12,7 +12,7 @@ def get_permission_list(user):
perms_list = ['admin']
else:
perms = Permission.objects.none()
roles = user.roles.all()
roles = user.roles.all() if hasattr(user, 'roles') else None
if roles:
for i in roles:
perms = perms | i.perms.all()

View File

@ -34,7 +34,9 @@ class RbacFilterSet(object):
if hasattr(queryset.model, 'belong_dept'):
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)
if '全部' in data_range:
return queryset
@ -57,6 +59,8 @@ class RbacFilterSet(object):
elif '仅本人' in data_range:
queryset = queryset.filter(Q(create_by=user)|Q(update_by=user))
return queryset
else:
return queryset.none()
return queryset

View File

@ -1,4 +1,5 @@
import logging
from re import L
from django.conf import settings
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.viewsets import GenericViewSet, ModelViewSet
from rest_framework_simplejwt.tokens import RefreshToken
from apps.ops.mixins import MyLoggingMixin
from utils.pagination import PageOrNot
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,
Position, Province, Role, User, UserThird)
from .permission import RbacPermission, get_permission_list
from rest_framework_simplejwt.views import TokenObtainPairView
from .permission_data import RbacFilterSet
from .serializers import (CitySerializer, DictSerializer, DictTypeSerializer, FileSerializer,
OrganizationSerializer, PermissionSerializer,
@ -71,8 +74,11 @@ def get_tokens_for_user(user):
}
import datetime
class MyTokenView(MyLoggingMixin, TokenObtainPairView):
def should_log(self, request, response):
return response.status_code == 200
class Login2View(APIView):
class Login2View(MyLoggingMixin, APIView):
"""
邮箱验证码登录
"""
@ -89,6 +95,9 @@ class Login2View(APIView):
return Response(get_tokens_for_user(user), status=status.HTTP_200_OK)
return Response('验证码错误', status=status.HTTP_400_BAD_REQUEST)
def should_log(self, request, response):
return response.status_code == 200
class sendMsg(APIView):
authentication_classes = []
permission_classes = []
@ -407,7 +416,7 @@ class UserViewSet(PageOrNot, ModelViewSet):
class WXMPlogin(APIView):
class WXMPlogin(MyLoggingMixin, APIView):
authentication_classes=[]
permission_classes=[]
@ -429,6 +438,9 @@ class WXMPlogin(APIView):
except:
raise AuthenticationFailed
def should_log(self, request, response):
return response.status_code == 200
class ProviceViewSet(PageOrNot, ListModelMixin, GenericViewSet):

View File

@ -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='总观看秒数'),
),
]

View File

@ -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,
},
),
]

View File

@ -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')},
),
]

View File

@ -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='视频分类'),
),
]

View File

@ -1,3 +1,4 @@
from tabnanny import verbose
from django.db import models
from utils.model import BaseModel
from apps.system.models import User, CommonAModel, Dict
@ -6,14 +7,15 @@ from apps.system.models import User, CommonAModel, Dict
class Video(CommonAModel):
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='')
fileid = models.CharField(verbose_name='云点播视频id', unique=True, max_length=200)
mediaurl = models.CharField(verbose_name='视频地址', max_length=200)
coverurl = models.CharField(verbose_name='封面地址', max_length=200)
duration = models.IntegerField(verbose_name='时长(秒)', default=0)
views = models.IntegerField(verbose_name='观看次数', default=0)
viewsp = models.IntegerField(verbose_name='观看人数', default=0)
views = models.IntegerField(verbose_name='观看次数', default=0)
viewsp = models.IntegerField(verbose_name='观看人数', default=0)
sort_str = models.CharField('排序字符', max_length=10, null=True, blank=True)
class Meta:
@ -24,15 +26,51 @@ class Video(CommonAModel):
class ViewRecord(BaseModel):
# 观看记录
"""
某视频-某人的观看记录统计
"""
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)
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:
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'), # 联合唯一
)

View File

@ -1,5 +1,5 @@
from rest_framework import serializers
from .models import Video, ViewRecord
from .models import Video, ViewRecord, ViewItem, View1, View2
from apps.system.serializers import UserSimpleSerializer
class VideoSerializer(serializers.ModelSerializer):
@ -8,12 +8,33 @@ class VideoSerializer(serializers.ModelSerializer):
model = Video
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 Meta:
model = Video
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):
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:
model = Video
exclude = ['mediaurl']
@ -39,3 +60,28 @@ class VRecordUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = ViewRecord
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="展示数量")

View File

@ -1,11 +1,15 @@
from django.db.models import base
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
router = routers.DefaultRouter()
router.register('video', VideoViewSet, basename="video")
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 = [
path('', include(router.urls)),
path('video/<int:id>/myview/', MyViewRecordAPIView.as_view()),

View File

@ -1,9 +1,9 @@
from datetime import timedelta
from time import timezone
from apps.system.models import Dict
from rest_framework.mixins import ListModelMixin
from apps.vod.serializers import VRecordSerializer, VRecordUpdateSerializer, VideoListDetailSerializer, VideoSerializer, VideoUpdateSerializer
from apps.vod.models import Video, ViewRecord
from rest_framework.mixins import ListModelMixin, UpdateModelMixin
from apps.vod.serializers import VRecordSerializer, VRecordUpdateSerializer, VideoListDetailSerializer, VideoSerializer, VideoUpdateSerializer, ViewItemSerializer, ViewItemUpdateSerializer, View2Serializer, DatetimeSerializer
from apps.vod.models import Video, ViewRecord, ViewItem, View1, View2
from django.shortcuts import render
from .vodclient import getAllClass, getPlayCode, searchMedia, getSignature
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 rest_framework.exceptions import ParseError
from utils.queryset import get_child_queryset2
from django.db.models import Sum
from django.db import connection
# Create your views here.
class ClassView(APIView):
@ -69,6 +71,8 @@ class VideoViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet):
@action(methods=['get'], detail=False, perms_map={'get':'video_view'})
def myview(self, request, *args, **kwargs):
"""
废弃接口
个人观看记录
"""
queryset = ViewRecord.objects.filter(user=request.user).order_by('-id')
@ -79,9 +83,71 @@ class VideoViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet):
serializer = VRecordSerializer(queryset, many=True)
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):
"""
废弃接口
废弃接口
"""
perms_map = {'get':'viewrecord_view'}
queryset = ViewRecord.objects.all()
search_fields = ['user__name', 'video__name']
@ -89,24 +155,111 @@ class VRecordViewSet(ListModelMixin, GenericViewSet):
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):
permission_classes = [IsAuthenticated]
def get(self, request, id , format=None):
"""
废弃接口
该视频的本人观看信息
"""
try:
video = Video.objects.get(pk=id)
except:
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)
return Response(serializer.data)
def put(self, request, id, format=None):
"""
废弃接口
更新该视频本人的观看信息
params: {current:int}
"""
@ -129,10 +282,13 @@ class MyViewRecordAPIView(APIView):
video.save()
if request.data.get('current', None):
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
video.views = video.views + 1
video.save()
else:
record.total_seconds = record.total_seconds + 10
record.save()
record.save()
return Response()

93
server/apps/vod/views2.py Normal file
View File

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

View File

@ -48,7 +48,9 @@ INSTALLED_APPS = [
'apps.supervision',
'apps.quality',
'apps.vod',
'apps.consulting'
'apps.consulting',
'apps.exam',
'apps.ops'
]
@ -167,8 +169,8 @@ REST_FRAMEWORK = {
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S',
'DATE_FORMAT': '%Y-%m-%d',
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
'UNAUTHENTICATED_USER': None,
'UNAUTHENTICATED_TOKEN': None,
# 'UNAUTHENTICATED_USER': None,
# 'UNAUTHENTICATED_TOKEN': None,
}
# simplejwt配置
SIMPLE_JWT = {

View File

@ -1,8 +0,0 @@
from .settings import *
DEBUG = False
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}

View File

@ -19,17 +19,13 @@ from django.contrib import admin
from django.urls import include, path
from rest_framework import routers
from rest_framework.documentation import include_docs_urls
from rest_framework_simplejwt.views import (TokenObtainPairView,
TokenRefreshView)
from rest_framework_simplejwt.views import 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
router = routers.DefaultRouter()
router.register('file', FileViewSet, basename="file")
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 drf_yasg import openapi
from drf_yasg.views import get_schema_view
@ -54,7 +50,7 @@ urlpatterns = [
path('api/admin/', admin.site.urls),
path('api/mediaauth/',mediaauth),
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/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/token/black/', LogoutView.as_view(), name='token_black'),
@ -64,6 +60,8 @@ urlpatterns = [
path('api/quality/', include('apps.quality.urls')),
path('api/vod/', include('apps.vod.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=[])),
url(r'^api/swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),

View File

@ -33,14 +33,15 @@ class SoftDeletableManagerMixin(object):
'''
_queryset_class = SoftDeletableQuerySet
def get_queryset(self):
def get_queryset(self, all=False):
'''
Return queryset limited to not deleted entries.
'''
kwargs = {'model': self.model, 'using': self._db}
if hasattr(self, '_hints'):
kwargs['hints'] = self._hints
if all:
return self._queryset_class(**kwargs)
return self._queryset_class(**kwargs).filter(is_deleted=False)
@ -49,6 +50,9 @@ class SoftDeletableManager(SoftDeletableManagerMixin, models.Manager):
class BaseModel(models.Model):
"""
自增ID,带有create_time, update_time, is_deleted
"""
create_time = models.DateTimeField(
default=timezone.now, verbose_name='创建时间', help_text='创建时间')
update_time = models.DateTimeField(

View File

@ -6,12 +6,12 @@ class MyPagination(PageNumberPagination):
page_size = 10
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:
def paginate_queryset(self, queryset):
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)
pass

32
server/utils/sql.py Normal file
View File

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