提交 ffb996ec authored 作者: 王鹏飞's avatar 王鹏飞

对接菜单权限

上级 b96bb8fe
module.exports = {
domain: 'dev.ezijing.com',
url: 'https://msg.ezijing.com/api',
url: 'https://msg2.ezijing.com/api',
webpack: {
externals: {
CKEDITOR: 'window.CKEDITOR',
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -34,3 +34,9 @@ export function getUser() {
export function getUserGrade() {
return httpRequest.get('/api/opera/v1/api/user/level')
}
/**
* 获取权限列表
*/
export function getPermissions(params) {
return httpRequest.get('/api/permissions/api/v1/user/permissions', { params })
}
......@@ -12,16 +12,10 @@ export default {
window.sessionStorage.isLogin = 1
this.addLogs('登录')
}
console.log(111)
},
methods: {
addLogs(msg) {
api
.addLog({ description: msg })
.then(response => {
})
.finally(() => {
})
api.addLog({ description: msg })
}
}
}
......
<template>
<div class="dialog-box">
<div class="dialog-cont">
<div class="close el-icon-circle-close" @click="$emit('dialogClose')"></div>
<div class="title">提示</div>
<div class="conts">
<div class="text">
您确定要进入案例背景选择界面吗?如果进入当前的操作记录可能丢失。
</div>
</div>
<div class="btn-box">
<div class="btn" @click="$emit('dialogClose')">取消</div>
<div class="btn" @click="$router.push({ path: '/' })">确定</div>
</div>
<!-- <img :src="data.url" alt=""> -->
<!-- <slot name="btn"></slot> -->
</div>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object
}
}
}
</script>
<style lang="scss" scoped>
.btn-box{
width: 160px;
display: flex;
margin: 0 auto;
padding: 20px 0;
.btn{
cursor: pointer;
&:nth-child(1){
width: 60px;
height: 32px;
border-radius: 4px;
border: 1px solid #999999;
text-align: center;
line-height: 32px;
font-size: 14px;
color: #999999;
}
&:nth-child(2){
width: 60px;
height: 32px;
background: linear-gradient(312deg, rgba(192, 21, 64, 0.67) 0%, #C01540 100%);
border-radius: 4px;
color: #fff;
text-align: center;
line-height: 32px;
font-size: 14px;
margin-left: auto;
}
}
}
.dialog-box{
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
background: rgba(0,0,0,.3);
z-index: 999;
.dialog-cont{
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
width: 480px;
// height: 264px;
background: #FFFFFF;
border-radius: 5px;
text-align: center;
.title{
max-width: 250px;
margin: 0 auto;
font-size: 18px;
color: #222222;
line-height: 20px;
padding: 20px 0;
font-weight: bold;
text-align: center;
}
.conts{
width: 440px;
border-top: 1px solid #f1f1f1;
border-bottom: 1px solid #f1f1f1;
margin: 0 auto;
padding: 40px 0;
.text{
text-align: left;
width: 300px;
height: 40px;
font-size: 14px;
color: #262626;
line-height: 20px;
margin: 0 auto;
}
}
}
}
.close{
cursor: pointer;
position: absolute;
top: 10px;
right: -50px;
font-size: 30px;
color: #fff;
}
</style>
<template>
<div class="dialog-box">
<div class="dialog-cont">
<div class="close el-icon-circle-close" @click="$emit('dialogClose')"></div>
<div class="title">{{ data.title }}</div>
<img :src="data.url" alt="">
<slot name="btn"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object
}
}
}
</script>
<style lang="scss" scoped>
.dialog-box{
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
background: rgba(0,0,0,.3);
z-index: 999;
.dialog-cont{
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
width: 348px;
// height: 264px;
background: #FFFFFF;
border-radius: 5px;
text-align: center;
.title{
max-width: 250px;
margin: 0 auto;
font-size: 14px;
color: #222222;
line-height: 20px;
padding: 16px 0;
text-align: center;
}
}
}
.close{
cursor: pointer;
position: absolute;
top: 10px;
right: -50px;
font-size: 30px;
color: #fff;
}
</style>
......@@ -3,19 +3,24 @@
<img src="http://zws-imgs-pub.oss-cn-beijing.aliyuncs.com/static/ezijing/logo/ezijing-logo.svg" class="logo" />
<el-menu :router="true" :default-active="$route.path" class="menu">
<template v-for="(item, index) in defaultMenus">
<el-submenu v-if="item.children" :index="item.path" :key="index">
<el-submenu v-if="item.children" :index="item.path" :key="index" v-show="menuVisible(item.tag)">
<template slot="title">
<i :class="item.icon"></i>
<span>{{ item.title }}</span>
</template>
<el-menu-item v-for="(subitem, index) in item.children" :index="subitem.path" :key="index">
<el-menu-item
v-for="(subitem, index) in item.children"
:index="subitem.path"
:key="index"
v-show="menuVisible(subitem.tag)"
>
{{ subitem.title }}
</el-menu-item>
</el-submenu>
<el-menu-item v-else :key="index" :index="item.path" class="menu-box"
><div class="icon"></div>
<span><i :class="item.icon"></i>{{ item.title }}</span></el-menu-item
>
<el-menu-item v-else :key="index" :index="item.path" class="menu-box" v-show="menuVisible(item.tag)">
<div class="icon"></div>
<span><i :class="item.icon"></i>{{ item.title }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
......@@ -31,36 +36,43 @@ export default {
return {
defaultMenus: [
{
tag: 'menu_message_board',
title: '消息看板',
path: '/msgView',
icon: 'el-icon-menu'
},
{
tag: 'menu_message_template',
title: '消息模板',
path: '/msgTemplate',
icon: 'el-icon-s-order'
},
{
tag: 'menu_sms_manage',
title: '短信管理',
path: '/sms',
icon: 'el-icon-s-promotion',
children: [
{
tag: 'menu_sms_template',
title: '短信模板',
path: '/sms/smsTemplate'
},
{
tag: 'menu_sms_send_records',
title: '分发短信记录',
path: '/sms/smsRecord'
}
]
},
{
tag: 'menu_user_group',
title: '用户群组',
path: '/userGroup',
icon: 'el-icon-s-custom'
},
{
tag: 'menu_logs',
title: '日志',
path: '/log',
icon: 'el-icon-s-management'
......@@ -69,16 +81,17 @@ export default {
}
},
computed: {
// datalist() {
// if (this.menus && this.menus.length) {
// return this.menus
// }
// return this.$store.state.level === 1 ? this.defaultMenus2 : this.defaultMenus
// }
// 菜单权限
menuPermissions() {
return this.$store.state.permissions.filter(item => item.type === 2)
}
},
methods: {
backgroundImg(url) {
return `background: url(${url});background-size: 100% 100%`
menuVisible(tag) {
if (!tag) {
return true
}
return !!this.menuPermissions.find(item => item.tag === tag)
}
}
}
......
import BaseACTION from '@/action/base_action'
import { Exam } from '../api'
export default class ExamAction extends BaseACTION {
getPower(obj) {
return Exam.getPower(obj).then(res => res)
}
getExam(obj, url) {
return Exam.getExam(obj, url).then(res => {
if (res.code !== 0) {
return res.code
}
const _data = JSON.parse(res.data)
let num = 0
const question = _data.sheet.questions.question_items.map(function(_, index) {
return {
type: _.question_type,
score: _.score,
id: _.question_item_id,
question_list: _.question_list.map((e, i) => {
num++
return {
num: num,
question_analysis: e.question_analysis,
question_answer: e.question_answer,
question_content: e.question_content,
common_content: e.common_content,
is_collection: e.is_collection,
id: e.id,
question_options: e.question_options.map(res => {
if (_.question_type === '2' || _.question_type === '5') {
if (_data.sheet.answers === null || Array.isArray(_data.sheet.answers)) {
return res
}
if (_data.sheet.answers[_.question_item_id] && _data.sheet.answers[_.question_item_id][e.id]) {
if (_data.sheet.answers[_.question_item_id][e.id].answer.find(item => { return item === res.id })) {
res.active = true
}
}
}
return res
}),
question_title: e.question_title,
answerOpt: e.question_options.map((i, index) => {
const answer = Array.isArray(e.question_answer) ? e.question_answer : [e.question_answer]
const val = answer.find((item) => { return item === i.id })
if (val) {
const opa = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
return opa[index]
}
}),
answer_count: e.answer_count
}
})
}
})
const json = {
user_id: _data.user_id,
sheet: {
id: _data.sheet.id,
question_count: _data.sheet.questions.total_question_count,
score: _data.sheet.questions.total_score,
remainingTime: _data.sheet.remaining_times,
duration: _data.sheet.duration
},
answers: _data.sheet.answers,
questions: question,
score_items: _data.sheet.score_items,
created_time: _data.sheet.created_time,
score: _data.sheet.score,
total_score: _data.sheet.questions.total_score,
status: _data.sheet.status
}
return json
})
}
examSubmit(obj, url) {
return Exam.examSubmit(obj, url).then(res => {
return res
})
}
deleteCollect(obj) {
return Exam.deleteCollect(obj).then(res => {
return res
})
}
collectQuestion(obj) {
return Exam.collectQuestion(obj).then(res => {
return res
})
}
}
import ExamAction from './ExamAction'
const Exam = new ExamAction()
export default Exam
import BaseAPI from '@/api/base_api'
export default class Exam extends BaseAPI {
getExam = (obj, url) => this.get(url, obj)
getPower = (obj, url) => this.post(url, obj, { headers: { 'Content-Type': 'multipart/form-data' } })
examSubmit = (obj, url) => this.post(url, obj, { headers: { 'Content-Type': 'multipart/form-data' } })
deleteCollect = obj =>
this.post('/api/zy/v2/examination/delete-my-question', obj, { headers: { 'Content-Type': 'multipart/form-data' } })
collectQuestion = obj =>
this.post('/api/zy/v2/examination/add-collection', obj, { headers: { 'Content-Type': 'multipart/form-data' } })
}
import ExamAPI from './exam_api'
const Exam = new ExamAPI(webConf)
export {
Exam
}
{
"DiscussModule": {
"DiscussList": {
"answers": "Answers",
"votes": "Votes",
"noData": "No discussion"
},
"DiscussDetail": {
"title": "Problem details",
"like": "Like",
"discuss": "Discuss",
"reply": "Reply",
"delete": "Delete",
"send": "Send",
"noAnswer": "No answer",
"deleteSuccess": "Delete success",
"answering": "Answer"
}
}
}
\ No newline at end of file
import Cookies from 'js-cookie'
import VueI18n from 'vue-i18n'
import language from './language'
// import zhCNLocale from 'element-ui/lib/locale/lang/zh-CN'
// import enLocale from 'element-ui/lib/locale/lang/en'
export default () => {
let _locale = 'zh-CN'
/* 国际化初始化 */
const _defaultLocale = 'zh-CN'
const _lang = Cookies.get('lang') || window.navigator.language || window.navigator.userLanguage || ''
if (_lang) {
if (language[_lang]) {
_locale = _lang
} else {
let flag = true
/* 做一下 兼容性处理 */
for (const k in language) {
const reg = new RegExp(k, 'gi')
if (reg.test(_lang)) {
_locale = k
flag = false
break
}
}
if (flag) {
/* 当前语言版本 - 不再我们的语言库中,那么默认 en */
_locale = _defaultLocale
Cookies.set('lang', _defaultLocale, { expires: 30, domain: '.ezijing.com' })
}
}
}
return new VueI18n({
locale: _locale, // 定义默认语言为中文
messages: {
'zh-CN': Object.assign(require('./zh-CN.json')),
en: Object.assign(require('./en.json'))
}
})
}
/* 定义语言模型 - key 值定义 跟 languages i18n 中 保持一致 */
const language = {
'zh-CN': { show: '语言', arr: [{ 'zh-CN': '中文' }, { en: 'English' }] },
en: { show: 'Language', arr: [{ 'zh-CN': '中文' }, { en: 'English' }] }
}
export default language
{
"DiscussModule": {
"DiscussList": {
"answers": "回答",
"votes": "投票",
"noData": "暂无相关评论"
},
"DiscussDetail": {
"title": "问题详情",
"like": "点赞",
"discuss": "讨论",
"reply": "回复",
"delete": "删除",
"send": "发送",
"noAnswer": "暂无回答",
"deleteSuccess": "删除成功",
"answering": "回答问题"
}
}
}
<template>
<div>
<div class="title">
<div class="type">{{ dItem.question_title }}</div>
<div class="count">{{ currentNum }}/{{ questionsData.sheet.question_count }}</div>
</div>
<div class="topic" v-html="dItem.question_content">
</div>
<ul class="option" v-if="item.type === '1' || item.type === '6' || item.type === '5' && dItem.answer_count <= 1">
<template v-for="(opt, oIndex) in dItem.question_options">
<template v-if="requestData2 != 0 && requestData[item.id]">
<li :key="oIndex" v-if="requestData[item.id][dItem.id]" :class="requestData[item.id][dItem.id].answer.find(res => { return res === opt.id }) ? 'active' : ''" @click="selectRadio(dItem, opt.id, oIndex, item)" :data-id="opt.id">{{opaKey[oIndex]}}.{{ opt.option }}</li>
<li :key="oIndex" v-else :class="dItem.activeIndex == oIndex ? 'active' : ''" @click="selectRadio(dItem, opt.id, oIndex, item)" :data-id="opt.id">{{opaKey[oIndex]}}.{{ opt.option }}</li>
</template>
<template v-else>
<li :key="oIndex" :class="dItem.activeIndex == oIndex ? 'active' : ''" @click="selectRadio(dItem, opt.id, oIndex, item)" :data-id="opt.id">{{opaKey[oIndex]}}{{ opt.option }}</li>
</template>
</template>
</ul>
<ul class="option" v-if="item.type === '2' || item.type === '5' && dItem.answer_count > 1">
<template v-for="(opt, oIndex) in dItem.question_options">
<template v-if="requestData2 != 0 && requestData[item.id]">
<li :key="oIndex+'-'" v-if="requestData[item.id][dItem.id]" :class="opt.active ? 'active' : ''" @click="selectCheckbox(opt, opt.id, dItem, item, oIndex)" :data-id="opt.id">{{opaKey[oIndex]}}.{{ opt.option }}</li>
<li :key="oIndex+'-'" v-else :class="opt.active ? 'active' : ''" @click="selectCheckbox(opt, opt.id, dItem, item, oIndex)" :data-id="opt.id">{{opaKey[oIndex]}}.{{ opt.option }}</li>
</template>
<template v-else>
<li :key="oIndex+'-'" :class="opt.active ? 'active' : ''" @click="selectCheckbox(opt, opt.id, dItem, item, oIndex)" :data-id="opt.id">{{opaKey[oIndex]}}.{{ opt.option }}</li>
</template>
</template>
</ul>
<!-- <div class="analy" v-if="isAnalysis">
<div class="tit">答案解析</div>
<div class="txt">正确答案:<span>{{ dItem.answerOpt }}</span></div>
<div class="txt">您的答案:<span>{{ dItem.opaVal }}</span></div>
<div class="exp">
<p class="name">解析:</p>
<p class="nr" v-html="dItem.question_analysis"></p>
</div>
</div> -->
</div>
</template>
<script>
export default {
props: ['dItem', 'currentNum', 'questionsData', 'item', 'requestData2', 'requestData'],
data() {
return {
opaKey: ['A', 'B', 'C', 'D', 'E', 'F', 'G']
}
},
methods: {
selectRadio(cData, checkId, index, oData) {
if (!this.isExamEnd) {
return false
}
const opa = this.opaKey
cData.opaVal = opa[index]
cData.activeIndex = index
if (this.requestData[oData.id]) {
if (!this.requestData[oData.id][cData.id]) {
this.requestData[oData.id][cData.id] = {
sign: false,
answer: [checkId]
}
} else {
this.requestData[oData.id][cData.id].answer = [checkId]
}
} else {
this.requestData[oData.id] = {
[cData.id]: {
sign: false,
answer: [checkId]
}
}
}
this.$forceUpdate()
},
// 多选事件
selectCheckbox(opt, checkId, cData, oData, index) {
if (!this.isExamEnd) {
return false
}
opt.active === undefined ? opt.active = true : opt.active = !opt.active
if (opt.active) {
if (this.requestData[oData.id]) {
if (this.requestData[oData.id][cData.id]) {
const arr = this.requestData[oData.id][cData.id].answer
this.requestData[oData.id][cData.id] = {
sign: this.requestData[oData.id][cData.id].sign
}
arr.push(checkId)
this.requestData[oData.id][cData.id].answer = arr
} else {
this.requestData[oData.id][cData.id] = {
sign: false,
answer: [checkId]
}
}
} else {
this.requestData[oData.id] = {
[cData.id]: {
sign: false,
answer: [checkId]
}
}
}
} else {
const index = this.requestData[oData.id][cData.id].answer.indexOf(checkId)
this.requestData[oData.id][cData.id].answer.splice(index, 1)
}
const opa = this.opaKey
const opaArr = []
cData.question_options.map((item, i) => {
if (item.active) {
opaArr.push(opa[i])
}
cData.opaVal = opaArr.toString().replace(new RegExp(',', 'g'), '')
})
console.log(this.requestData)
this.$forceUpdate()
}
}
// props: {
// dItem: {
// type: Object,
// default () {
// return {}
// }
// },
// currentNum:
// }
// :dItem="dItem" :currentNum="currentNum" :questionsData="questionsData"
}
</script>
<style lang="scss" scoped>
</style>
<template>
<div class="card_box">
<div class="head" id="top-view2">
<i class="el-icon-arrow-left"></i>
</div>
<div id="bottom-view2">
<div class="card_con">
<div class="flag">
<ul>
<li>
<div class="circle active1"></div>
<div class="txt">已答</div>
</li>
<li>
<div class="circle active2"></div>
<div class="txt">标记</div>
</li>
<li>
<div class="circle active3"></div>
<div class="txt">未答</div>
</li>
</ul>
</div>
<div class="question">
<template v-if="answerData['1'].data.length !=0">
<div class="title" >{{ answerData['1'].title }}</div>
<ul>
<template v-for="(item, index) in answerData['1'].data">
<li @click="go(item.id, item.num)" :data-id="item.id" :key="index" :class="item.sign == true ? 'active2' : item.answer == undefined ? 'active3' : item.answer.length == 0 ? 'active3' : 'active1'">{{ item.num }}</li>
</template>
</ul>
</template>
<template v-if="answerData['2'].data.length !=0">
<div class="title" >{{ answerData['2'].title }}</div>
<ul>
<template v-for="(item, index) in answerData['2'].data">
<li @click="go(item.id, item.num)" :data-id="item.id" :key="index" :class="item.sign == true ? 'active2' : item.answer == undefined ? 'active3' : item.answer.length == 0 ? 'active3' : 'active1'">{{ item.num }}</li>
</template>
</ul>
</template>
<template v-if="answerData['6'].data.length !=0">
<div class="title" >{{ answerData['6'].title }}</div>
<ul>
<template v-for="(item, index) in answerData['6'].data">
<li @click="go(item.id, item.num)" :data-id="item.id" :key="index" :class="item.sign == true ? 'active2' : item.answer == undefined ? 'active3' : item.answer.length == 0 ? 'active3' : 'active1'">{{ item.num }}</li>
</template>
</ul>
</template>
<template v-if="answerData['5'].data.length !=0">
<div class="title" >{{ answerData['5'].title }}</div>
<ul>
<template v-for="(item, index) in answerData['5'].data">
<li @click="go(item.id, item.num)" :data-id="item.id" :key="index" :class="item.sign == true ? 'active2' : item.answer == undefined ? 'active3' : item.answer.length == 0 ? 'active3' : 'active1'">{{ item.num }}</li>
</template>
</ul>
</template>
</div>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash'
export default {
props: {
questionsData: {
type: Object,
default () {
return {}
}
},
requestData: {
type: Object,
default () {
return {}
}
},
clickStatus: {
type: Number,
default () {
return {}
}
}
},
mounted() {
},
data() {
return {
answerData: {
1: {
title: '单选题:',
data: []
},
2: {
title: '多选题:',
data: []
},
5: {
title: '案例题:',
data: []
},
6: {
title: '判断题:',
data: []
}
},
cardData: {},
countIndex: 1,
questionType: {
1: '单选题:',
2: '多选题:',
5: '案例题:',
6: '判断题:'
}
}
},
methods: {
go(id, num) {
setTimeout(() => {
this.$emit('showCard')
}, 200)
this.$emit('cardChange', { id: id, num: parseInt(num / 10) })
},
scrollDom() {
const topViewH = document.getElementById('top-view2').offsetHeight
const clientHeight = document.documentElement.clientHeight
const bottomView = document.getElementById('bottom-view2')
bottomView.style.height = (clientHeight - topViewH) + 'px'
},
clone(origin) {
const originProto = Object.getPrototypeOf(origin)
return Object.assign(Object.create(originProto), origin)
}
},
computed: {
dataList() {
// const clone = this.clone(this.questionsData)
// const data = this.duplicates(clone.questions)
let num = 1
return this.questionsData.questions.map(item => {
item.question_list.map((subitem, index) => {
subitem.num = num + index
return subitem
})
num += item.question_list.length
return item
})
},
changeData() {
return this.clickStatus
}
},
watch: {
changeData() {
this.answerData = {
1: {
title: '单选题:',
data: []
},
2: {
title: '多选题:',
data: []
},
5: {
title: '案例题:',
data: []
},
6: {
title: '判断题:',
data: []
}
}
let num = 1
this.clone(this.questionsData).questions.map(item => {
item.question_list.map((c, index) => {
c.num = num + index
if (Object.prototype.hasOwnProperty.call(this.requestData, item.id)) {
if (Object.prototype.hasOwnProperty.call(this.requestData[item.id], c.id)) {
c = Object.assign(this.requestData[item.id][c.id], _.omit(c, ['answerOpt', 'answer_count', 'common_content', 'question_analysis', 'question_answer', 'question_content', 'question_options', 'question_title']))
}
}
this.answerData[item.type].data.push(c)
})
num += item.question_list.length
})
}
}
}
</script>
<style lang="scss" scoped>
.card_box{
width: 100%;
height: 100%;
background: #fff;
.card_con{
padding: 0 .4rem;
overflow-y: scroll;
.flag{
border-bottom: 0.01rem solid #EEEEEE;
ul{
list-style: none;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
li{
margin: 0 .4rem 0.1rem .4rem;
text-align: center;
.circle{
width: .6rem;
height: .6rem;
border-radius: 50%;
font-size: .26rem;
text-align: center;
line-height: .6rem;
color: #fff;
}
.active1{
background: #67C23A;
}
.active2{
background:rgba(255,128,0,1);
}
.active3{
background:rgba(204,204,204,1);
}
.txt{
color: #222222;
font-size: .26rem;
margin-top: 0.1rem;
}
}
}
}
.question{
.title{
font-size: .3rem;
color: #333333;
margin-top: .1rem;
margin-bottom: .4rem;
}
ul{
list-style: none;
margin-top: .3rem;
padding: 0 0.6rem;
border-bottom: 0.01rem solid #EEEEEE;
display: flex;
flex-wrap: wrap;
li{
width: .6rem;
height: .6rem;
border-radius: 50%;
font-size: .26rem;
text-align: center;
line-height: .6rem;
color: #fff;
margin-right: .6rem;
margin-bottom: .4rem;
}
:nth-child(5n+5){
margin: 0;
}
.active1{
background: #67C23A;
}
.active2{
background:rgba(255,128,0,1);
}
.active3{
background:rgba(204,204,204,1);
}
}
}
}
}
.head{
color: #222;
display: flex;
padding: .4rem 0;
align-items: center;
i{
font-size: .35rem;
margin-left: .28rem;
}
.title{
margin-left: .03rem;
font-size: .3rem;
}
}
#bottom-view2{
overflow: scroll;
}
</style>
<template>
<div class="end_box">
<div class="head" id="top-view3">
<i class="el-icon-arrow-left"></i>
<div class="title">成绩报告</div>
</div>
<div id="bottom-view3">
<div class="end_con">
<div class="cs_type"><span>测试内容:</span>能力自测</div>
<div class="cs_time"><span>测试时间:</span>{{ questionsData.created_time }}</div>
<div class="charts">
<svg width="86%" height="86%" viewBox="0 0 100 100">
<circle r="25" cx="50" cy="50" fill="none" stroke="rgba(255,193,79,1)" stroke-width="50" stroke-dasharray="158 158" />
</svg>
<svg class="w_svg" width="100%" height="100%" viewBox="0 0 100 100">
<circle r="25" cx="50" cy="50" fill="none" stroke="#07cb78" stroke-width="50" :stroke-dasharray="ratio" />
</svg>
<div class="circle">
<p class="p1">正确率</p>
<p class="p2">{{ratioNum}}%</p>
</div>
</div>
<div class="assess">
<div class="tit">测试评估:</div>
<img v-show="ratioNum < 20" src="../assets/images/mountain0.png" alt="">
<img v-show="ratioNum >= 20 && ratioNum < 40" src="../assets/images/mountain1.png" alt="">
<img v-show="ratioNum >= 40 && ratioNum < 60" src="../assets/images/mountain2.png" alt="">
<img v-show="ratioNum >= 60 && ratioNum < 99" src="../assets/images/mountain3.png" alt="">
<img v-show="ratioNum == 100" src="../assets/images/mountain4.png" alt="">
<div v-if="ratioNum == 100" class="txt">成功近在眼前,再接再厉!</div>
<div v-else class="txt">您离成功还有一段距离,继续努力!</div>
</div>
<div class="card">
<div class="tit">答题卡:</div>
<div class="card_con">
<div class="flag">
<ul>
<li>
<div class="circle active1">1</div>
<div class="txt">正确</div>
</li>
<li>
<div class="circle active2">1</div>
<div class="txt">错误</div>
</li>
<li>
<div class="circle active3">1</div>
<div class="txt">未答</div>
</li>
</ul>
</div>
<div class="question">
<template v-for="(item, index) in questionsData.questions">
<div :key="index">
<div class="title">{{ questionType[item.type] }}</div>
<ul>
<template v-for="(cItem, cIndex) in item.question_list">
<template v-if="requestData[item.id]">
<template v-if="requestData[item.id][cItem.id]">
<!-- <template v-if="questionsData.score_items[item.id]">
{{ questionsData.score_items[item.id] }}
</template> -->
<template v-if="requestData[item.id][cItem.id].answer.length != 0">
<template v-if="questionsData.score_items[item.id]">
<li @click="go(cItem.id)" :data-id="cItem.id" :key="cIndex" :class="questionsData.score_items[item.id][cItem.id].is_right ? 'active1' : 'active2'">{{ cIndex+1 }}</li>
</template>
<template v-else>
<li @click="go(cItem.id)" :data-id="cItem.id" :key="cIndex" class="active3">{{ cIndex+1 }}</li>
</template>
</template>
<template v-else>
<li @click="go(cItem.id)" :data-id="cItem.id" :key="cIndex" class="active3">{{ cIndex+1 }}</li>
</template>
<!-- <li @click="go(cItem.id)" :data-id="cItem.id" v-else :key="cIndex" class="active3">{{ cIndex+1 }}</li> -->
</template>
<template v-else>
<li @click="go(cItem.id)" :data-id="cItem.id" :key="cIndex" class="active3">{{ cIndex+1 }}</li>
</template>
</template>
<template v-else>
<li @click="go(cItem.id)" :data-id="cItem.id" :key="cIndex" class="active3">{{ cIndex+1 }}</li>
</template>
</template>
</ul>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
questionType: {
1: '单选题:',
2: '多选题:',
5: '案例题:',
6: '判断题:'
},
ratio: 0,
ratioNum: 0
}
},
props: {
questionsData: {
type: Object,
default () {
return {}
}
},
requestData: {
type: Object,
default () {
return {}
}
}
},
methods: {
go(id) {
this.$emit('cardChange', id)
},
scrollDom() {
const topViewH = document.getElementById('top-view3').offsetHeight
const clientHeight = document.documentElement.clientHeight
const bottomView = document.getElementById('bottom-view3')
bottomView.style.height = (clientHeight - topViewH) + 'px'
console.log(bottomView.style.height)
}
},
created() {
const count = this.questionsData.score / this.questionsData.total_score
this.ratio = `${count * 158} 158`
this.ratioNum = parseInt(count * 100)
console.log(this.questionsData.score, this.questionsData.total_score)
console.log(this.ratio)
},
mounted() {
this.scrollDom()
}
}
</script>
<style lang="scss" scoped>
.end_box{
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 9999999;
background: #fff;
}
.head{
color: #222;
display: flex;
padding: .4rem 0;
align-items: center;
i{
font-size: .35rem;
margin-left: .28rem;
}
.title{
margin-left: .03rem;
font-size: .3rem;
}
}
#bottom-view3{
overflow: scroll;
}
.end_con{
padding: 0 .4rem;
.cs_type{
font-size: .3rem;
color: #222;
line-height: 100%;
span{
font-weight: bold;
}
}
.cs_time{
font-size: .3rem;
color: #222;
line-height: 100%;
margin-top: .2rem;
span{
font-weight: bold;
}
}
.charts{
width: 3rem;
height: 3rem;
margin: .4rem auto;
position: relative;
svg{
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%,-50%);
}
.w_svg{
-webkit-transform:rotate(-90deg) translate(50%,-50%);
}
.circle{
width: .96rem;
height: .96rem;
position: absolute;
top: 50%;
left: 50%;
z-index: 999;
-webkit-transform: translate(-50%,-50%);
background: #FF8000;
border-radius: 50%;
text-align: center;
.p1{
font-size: .2rem;
color: #fff;
line-height: 100%;
margin: 0.18rem 0 0.15rem 0;
opacity: .7;
}
.p2{
font-size: .26rem;
color: #fff;
line-height: 100%;
font-weight: bold;
margin: 0;
}
}
}
.assess{
.tit{
font-size: .3rem;
color: #333;
font-weight: bold;
}
img{
width: 4rem;
height: 1.93rem;
display: block;
margin: 0 auto;
}
.txt{
font-weight: bold;
font-size: .3rem;
color: #333;
text-align: center;
margin-top: .18rem;
}
}
.card{
margin-top: .38rem;
.tit{
font-size: .3rem;
color: #333;
font-weight: bold;
}
.card_con{
overflow-y: scroll;
.flag{
border-bottom: 0.01rem solid #EEEEEE;
ul{
list-style: none;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
li{
margin: 0 .4rem 0.1rem .4rem;
text-align: center;
.circle{
width: .6rem;
height: .6rem;
border-radius: 50%;
font-size: .26rem;
text-align: center;
line-height: .6rem;
color: #fff;
}
.active1{
background: #67C23A;
}
.active2{
background:#FA5555;
}
.active3{
background:rgba(204,204,204,1);
}
.txt{
color: #222222;
font-size: .26rem;
margin-top: 0.1rem;
}
}
}
}
.question{
.title{
font-size: .3rem;
color: #333333;
margin-top: .1rem;
margin-bottom: .4rem;
}
ul{
list-style: none;
margin: 0;
padding: 0 0.6rem;
border-bottom: 0.01rem solid #EEEEEE;
display: flex;
flex-wrap: wrap;
li{
width: .6rem;
height: .6rem;
border-radius: 50%;
font-size: .26rem;
text-align: center;
line-height: .6rem;
color: #fff;
margin-right: .6rem;
margin-bottom: .4rem;
}
:nth-child(5n+5){
margin: 0;
}
.active1{
background: #67C23A;
}
.active2{
background:#FA5555;
}
.active3{
background:rgba(204,204,204,1);
}
}
}
}
}
}
</style>
import './index.scss'
import ExamTopic from './src/ExamTopic.vue'
import ExamRnd from './src/ExamEnd.vue'
const components = [
ExamTopic,
ExamRnd
]
const install = function (Vue, opts = {}) {
/* 存在国际化 */
if (opts.i18n) {
const msgs = opts.i18n.messages
for (const k in msgs) {
opts.i18n.setLocaleMessage(k, Object.assign(msgs[k], require('./assets/languages/' + k + '.json')))
}
}
components.forEach(component => {
Vue.component(component.name, component)
})
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export default {
install,
ExamTopic,
ExamRnd
}
/* Extra small devices (portrait phones, less than 576px) */
@media (max-width: 575px) {}
/* Small devices (landscape phones, 576px and up) */
@media (min-width: 576px) and (max-width: 767px) {}
/* Medium devices (tablets, 768px and up) */
@media (min-width: 768px) and (max-width: 991px) {}
/* Large devices (desktops, 992px and up) */
@media (min-width: 992px) and (max-width: 1199px) {}
/* Extra large devices (large desktops, 1200px and up) */
@media (min-width: 1200px) {}
<template>
<div class="end_box">
<div id="bottom-view3">
<div class="end_con">
<div class="cs_type"><span>测试内容:</span>{{ title }}</div>
<div class="cs_time"><span>测试时间:</span>{{ questionsData.created_time }}</div>
<div class="charts">
<svg width="86%" height="86%" viewBox="0 0 100 100">
<circle r="25" cx="50" cy="50" fill="none" stroke="rgba(255,193,79,1)" stroke-width="50" stroke-dasharray="158 158" />
</svg>
<svg v-show="ratioNum!==0" class="w_svg" width="100%" height="100%" viewBox="0 0 100 100">
<circle r="25" cx="50" cy="50" fill="none" stroke="#07cb78" stroke-width="50" :stroke-dasharray="ratio" />
</svg>
<template v-if="title === '真题实战'">
<div class="circle">
<p class="p1">得分</p>
<p class="p2">{{questionsData.score}}</p>
</div>
</template>
<template v-else>
<div class="circle">
<p class="p1">正确率</p>
<p class="p2">{{ratioNum}}%</p>
</div>
</template>
</div>
<div class="assess">
<div class="tit">测试评估:</div>
<img v-show="ratioNum < 20" src="../assets/images/mountain0.png" alt="">
<img v-show="ratioNum >= 20 && ratioNum < 40" src="../assets/images/mountain1.png" alt="">
<img v-show="ratioNum >= 40 && ratioNum < 60" src="../assets/images/mountain2.png" alt="">
<img v-show="ratioNum >= 60 && ratioNum < 99" src="../assets/images/mountain3.png" alt="">
<img v-show="ratioNum == 100" src="../assets/images/mountain4.png" alt="">
<template v-if="title != '考前摸底'">
<div v-if="ratioNum == 100" class="txt">成功近在眼前,再接再厉!</div>
<div v-else class="txt">您离成功还有一段距离,继续努力!</div>
</template>
<template v-else>
<div v-if="ratioNum >= 90" class="txt" style="text-align: left;">您的成绩不错呦,保持住,想进一步提升成绩,推荐您使用全部考试服务!</div>
<div v-else class="txt" style="text-align: left;">您距离拿证还有一段距离,急速拿证推荐您使用全部考试服务!</div>
</template>
</div>
<div class="nl-btn" v-if="title == '考前摸底'">
<!-- <div class="btn" @click="goAbilityExam">重做测试</div>
<div class="btn" @click="goCourse">学习课程</div> -->
<div class="new-btn" @click="goVip">全部考试服务</div>
</div>
<div class="card">
<div class="tit">答题卡:</div>
<div class="card_con">
<div class="flag">
<ul>
<li>
<div class="circle active1"></div>
<div class="txt">正确</div>
</li>
<li>
<div class="circle active2"></div>
<div class="txt">错误</div>
</li>
<li>
<div class="circle active3"></div>
<div class="txt">未答</div>
</li>
</ul>
</div>
<div class="question">
<template v-if="answerData['1'].data.length !=0">
<div class="title" >{{ answerData['1'].title }}</div>
<ul>
<template v-for="(item, index) in answerData['1'].data">
<li @click="go(item.id)" :data-id="item.id" :key="index" :class="item.answer == undefined || item.answer.length == 0 ? 'active3' : item.is_right ? 'active1' : 'active2'">{{ item.num }}</li>
</template>
</ul>
</template>
<template v-if="answerData['2'].data.length !=0">
<div class="title" >{{ answerData['2'].title }}</div>
<ul>
<template v-for="(item, index) in answerData['2'].data">
<li @click="go(item.id)" :data-id="item.id" :key="index" :class="item.answer == undefined || item.answer.length == 0 ? 'active3' : item.is_right ? 'active1' : 'active2'">{{ item.num }}</li>
</template>
</ul>
</template>
<template v-if="answerData['6'].data.length !=0">
<div class="title" >{{ answerData['6'].title }}</div>
<ul>
<template v-for="(item, index) in answerData['6'].data">
<li @click="go(item.id)" :data-id="item.id" :key="index" :class="item.answer == undefined || item.answer.length == 0 ? 'active3' : item.is_right ? 'active1' : 'active2'">{{ item.num }}</li>
</template>
</ul>
</template>
<template v-if="answerData['5'].data.length !=0">
<div class="title" >{{ answerData['5'].title }}</div>
<ul>
<template v-for="(item, index) in answerData['5'].data">
<li @click="go(item.id)" :data-id="item.id" :key="index" :class="item.answer == undefined || item.answer.length == 0 ? 'active3' : item.is_right ? 'active1' : 'active2'">{{ item.num }}</li>
</template>
</ul>
</template>
</div>
</div>
</div>
<div :class="ratioNum == 100 ? 'st-btn cent' : 'st-btn'">
<div class="btn" @click="go(1)">全部解析</div>
<div class="btn" @click="go(-1)" v-if="ratioNum !== 100">错题解析</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Exam from '../action'
import { Toast } from 'vant'
// import { Toast } from 'vant'
export default {
name: 'ExamEnd',
data() {
return {
answerData: {
1: {
title: '单选题:',
data: []
},
2: {
title: '多选题:',
data: []
},
5: {
title: '案例题:',
data: []
},
6: {
title: '判断题:',
data: []
}
},
questionType: {
1: '单选题:',
2: '多选题:',
5: '案例题:',
6: '判断题:'
},
ratio: 0,
ratioNum: 0,
questionsData: {
questions: []
},
requestData: {},
title: ''
}
},
created() {
Toast.loading({
message: '加载中...',
duration: 5000
})
},
mounted() {
if (this.$route.query.course_id || this.$route.query.tag_id) {
this.$route.query.type === 2 || this.$route.query.type === '2' ? this.title = '考点练习' : this.title = '章节练习'
} else {
this.$route.query.type === 1 || this.$route.query.type === '1' ? this.title = '考前摸底' : this.title = '真题实战'
}
// this.title
this.initData()
},
computed: {
isWeapp() {
return this.$store.state.isWeapp
},
dataList() {
let num = 1
return this.questionsData.questions.map(item => {
item.question_list.map((subitem, index) => {
subitem.num = num + index
return subitem
})
num += item.question_list.length
return item
})
}
},
methods: {
goVip() {
// const isLogin = await this.$store.dispatch('checkLogin')
// if (!isLogin) {
// if (this.isWeapp) {
// // 小程序
// wx.miniProgram.navigateTo({
// url: `/pages/login/index?redirect_uri=${encodeURIComponent(
// `/exam/result?type=${this.$route.query.type}&is_create=0&papersUrl=${this.$route.query.papersUrl}`
// )}`
// })
// } else {
// this.$router.push({
// path: '/login',
// query: {
// redirect_uri: `/exam/result?type=${this.$route.query.type}&is_create=0&papersUrl=${this.$route.query.papersUrl}`
// }
// })
// }
// return false
// }
this.$router.push({
path: '/payPage'
})
},
goAbilityExam() {
this.$router.replace({
path: '/exam/index',
query: {
t: 1
}
})
},
goCourse() {
if (this.isWeapp) {
wx.miniProgram.navigateTo({ url: `/pages/web/index?src=${window.location.origin}/course/learn` })
} else {
this.$router.push({
path: '/course/learn'
})
}
},
go(id) {
const param = this.$route.query
param.id = id
this.$emit('cardChange', param)
},
initData() {
const param = this.$route.query
Exam.getExam(param, param.papersUrl).then(res => {
this.questionsData = res
this.requestData = res.answers === null ? [] : res.answers
const count = parseInt(this.questionsData.score) / parseInt(this.questionsData.total_score)
if (!isNaN(count)) {
this.ratio = `${count * 158} 158`
this.ratioNum = parseInt(count * 100)
} else {
this.ratioNum = 0
}
Toast.clear()
this.changeData()
})
},
changeData() {
this.answerData = {
1: {
title: '单选题:',
data: []
},
2: {
title: '多选题:',
data: []
},
5: {
title: '案例题:',
data: []
},
6: {
title: '判断题:',
data: []
}
}
let num = 1
this.questionsData.questions.map(item => {
item.question_list.map((c, index) => {
c.num = num + index
c = Object.assign(this.questionsData.score_items[item.id][c.id], c)
if (Object.prototype.hasOwnProperty.call(this.questionsData.answers, item.id)) {
if (Object.prototype.hasOwnProperty.call(this.questionsData.answers[item.id], c.id)) {
c = Object.assign(this.questionsData.answers[item.id][c.id], c)
}
}
this.answerData[item.type].data.push(c)
})
num += item.question_list.length
})
}
}
}
</script>
<style lang="scss" scoped>
.end_box{
width: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 99;
background: #fff;
}
.head{
color: #222;
display: flex;
padding: .4rem 0;
align-items: center;
i{
font-size: .35rem;
margin-left: .28rem;
}
.title{
margin-left: .03rem;
font-size: .3rem;
}
}
#bottom-view3{
width: 100%;
overflow: scroll;
}
.end_con{
height: 100%;
padding: .4rem 0.4rem 1rem .4rem;
.cs_type{
font-size: .3rem;
color: #222;
line-height: 100%;
span{
font-weight: bold;
}
}
.cs_time{
font-size: .3rem;
color: #222;
line-height: 100%;
margin-top: .2rem;
span{
font-weight: bold;
}
}
.charts{
width: 3rem;
height: 3rem;
margin: .4rem auto;
position: relative;
svg{
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%,-50%);
}
.w_svg{
-webkit-transform:rotate(-90deg) translate(50%,-50%);
}
.circle{
width: .96rem;
height: .96rem;
position: absolute;
top: 50%;
left: 50%;
z-index: 999;
-webkit-transform: translate(-50%,-50%);
background: #FF8000;
border-radius: 50%;
text-align: center;
.p1{
font-size: .2rem;
color: #fff;
line-height: 100%;
margin: 0.18rem 0 0.15rem 0;
opacity: .7;
}
.p2{
font-size: .26rem;
color: #fff;
line-height: 100%;
font-weight: bold;
margin: 0;
}
}
}
.assess{
.tit{
font-size: .3rem;
color: #333;
font-weight: bold;
}
img{
width: 4rem;
height: 1.93rem;
display: block;
margin: 0 auto;
}
.txt{
font-weight: bold;
font-size: .3rem;
color: #333;
text-align: center;
margin-top: .18rem;
}
}
.card{
padding-bottom: env(safe-area-inset-bottom);
margin-top: .38rem;
.tit{
font-size: .3rem;
color: #333;
font-weight: bold;
}
.card_con{
overflow-y: scroll;
.flag{
border-bottom: 0.01rem solid #EEEEEE;
ul{
list-style: none;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
li{
margin: 0 .4rem 0.1rem .4rem;
text-align: center;
.circle{
width: .6rem;
height: .6rem;
border-radius: 50%;
font-size: .26rem;
text-align: center;
line-height: .6rem;
color: #fff;
}
.active1{
background: #67C23A;
}
.active2{
background:#FA5555;
}
.active3{
background:rgba(204,204,204,1);
}
.txt{
color: #222222;
font-size: .26rem;
margin-top: 0.1rem;
}
}
}
}
.question{
.title{
font-size: .3rem;
color: #333333;
margin-top: .1rem;
margin-bottom: .4rem;
}
ul{
list-style: none;
margin-top: .3rem;
padding: 0 0.6rem;
// border-bottom: 0.01rem solid #EEEEEE;
display: flex;
flex-wrap: wrap;
li{
width: .6rem;
height: .6rem;
border-radius: 50%;
font-size: .26rem;
text-align: center;
line-height: .6rem;
color: #fff;
margin-right: .6rem;
margin-bottom: .4rem;
}
:nth-child(5n+5){
margin: 0;
}
.active1{
background: #67C23A;
}
.active2{
background:#FA5555;
}
.active3{
background:rgba(204,204,204,1);
}
}
}
}
}
}
.nl-btn{
display: flex;
margin-top: .3rem;
.btn{
width:3rem;
height:.7rem;
background:linear-gradient(90deg,rgba(43,124,233,1) 0%,rgba(103,168,255,1) 100%);
border-radius:.12rem;
font-size: .3rem;
color: #fff;
text-align: center;
line-height: .7rem;
}
.btn:nth-child(2){
margin-left: auto;
}
.new-btn{
width: 3.2rem;
height: .9rem;
background: #C62245;
text-align: center;
line-height: .9rem;
color: #fff;
font-size: .3rem;
background: url(../assets/images/ability-r-btn.png);
background-size: 100% 100%;
margin: 0 auto;
}
}
.st-btn{
width: 100%;
position: fixed;
bottom: 0;
left: 0;
height: 1rem;
background:rgba(255,255,255,1);
box-shadow:0px 0px 6px 0px rgba(0,0,0,0.05);
z-index: 99;
display: flex;
align-items: center;
// padding: 0 .4rem;
padding-bottom: env(safe-area-inset-bottom);
.btn{width:3rem;
height:.7rem;
background:#C62245;
border-radius:.12rem;
color: #fff;
font-size: .3rem;
text-align: center;
line-height: .7rem;
margin-left: .4rem;
}
.btn:nth-child(2){
margin-left: auto;
margin-right: .4rem;
}
}
.cent{
justify-content: center;
}
</style>
<template>
<div class="exam-box">
<div class="no-swiping" v-if="countPage.noSwiping"></div>
<div id="top-view" v-show="!topicCard.isShow">
<div class="tool-box">
<div class="time">{{ $route.query.id !== undefined ? '' : $route.query.course_id || $route.query.tag_id ? '' : '倒计时' }}{{ remainingTime }}</div>
<div class="tag-box">
<span @click.prevent.stop="signQuestion" v-if="isExamEnd" :class="signText == '标记' ? '' : 'active'">{{ signText }}</span>
<span @click="showCard" v-if="isExamEnd">答题卡</span>
<span @click="submitShowPop" v-if="isExamEnd">交卷</span>
</div>
</div>
</div>
<div :class="classHide ? 'topic-box' : 'topic-box hide'" id="bottom-view" v-if="questionsData">
<swiper ref="mySwiper" :options="swiperOptions">
<template v-for="(item) in questionsData.questions">
<template v-for="(dItem, dIndex) in item.question_list">
<swiper-slide
:key="dIndex+dItem.id"
:data-dId="dItem.id"
:data-cId="item.id"
v-if="dItem.num <= countPage.currentPage * countPage.showQuestionNum && dItem.num >= countPage.currentPage * countPage.showQuestionNum - 20"
:data-num="dItem.num">
<!-- v-if="dItem.num <= countPage.currentPage * countPage.showQuestionNum && dItem.num >= countPage.currentPage * countPage.showQuestionNum - 20" -->
<div class="title">
<div class="type" v-if="item.type == 1">单选题</div>
<div class="type" v-if="item.type == 2">多选题</div>
<div class="type" v-if="item.type == 5">案例题</div>
<div class="type" v-if="item.type == 6">判断题</div>
<div class="count">{{ dItem.num }}/{{ questionsData.sheet.question_count }}</div>
</div>
<div class="topic-bt" v-if="item.type == '5'" v-html="dItem.common_content"></div>
<div class="topic" v-html="dItem.question_content">
</div>
<ul class="option" v-if="item.type === '1' || item.type === '6' || item.type === '5' && dItem.answer_count <= 1">
<template v-for="(opt, oIndex) in dItem.question_options">
<template v-if="requestData2 != 0 && requestData[item.id]">
<li :key="oIndex" v-if="requestData[item.id][dItem.id]" :class="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id) ? 'active2' : requestData[item.id][dItem.id].answer.find(res => { return res === opt.id }) ? 'active' : ''" @click="selectRadio(dItem, opt.id, oIndex, item)" :data-id="opt.id">
<van-icon v-if="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id)" class="icon" name="checked"/>
<div>
<span v-if="!(dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id))">
{{opaKey[oIndex]}}.
</span>
{{ opt.option }}
</div>
</li>
<li :key="oIndex" v-else :class="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id) ? 'active2' : dItem.activeIndex == oIndex ? 'active' : ''" @click="selectRadio(dItem, opt.id, oIndex, item)" :data-id="opt.id">
<van-icon v-if="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id)" class="icon" name="checked"/>
<div>
<span v-if="!(dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id))">
{{opaKey[oIndex]}}.
</span>
{{ opt.option }}
</div>
</li>
</template>
<template v-else>
<li :key="oIndex" :class="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id) ? 'active2' : dItem.activeIndex == oIndex ? 'active' : ''" @click="selectRadio(dItem, opt.id, oIndex, item)" :data-id="opt.id">
<van-icon v-if="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id)" class="icon" name="checked"/>
<div>
<span v-if="!(dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id))">
{{opaKey[oIndex]}}.
</span>
{{ opt.option }}
</div>
</li>
</template>
</template>
</ul>
<ul class="option" v-if="item.type === '2' || item.type === '5' && dItem.answer_count > 1">
<template v-for="(opt, oIndex) in dItem.question_options">
<template v-if="requestData2 != 0 && requestData[item.id]">
<li :key="oIndex+'-'" v-if="requestData[item.id][dItem.id]" :class="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id) ? 'active2' : opt.active ? 'active' : ''" @click="selectCheckbox(opt, opt.id, dItem, item, oIndex)" :data-id="opt.id">
<van-icon v-if="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id)" class="icon" name="checked"/>
<div>
<span v-if="!(dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id))">
{{opaKey[oIndex]}}.
</span>
{{ opt.option }}
</div>
</li>
<li :key="oIndex+'-'" v-else :class="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id) ? 'active2' :opt.active ? 'active' : ''" @click="selectCheckbox(opt, opt.id, dItem, item, oIndex)" :data-id="opt.id">
<van-icon v-if="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id)" class="icon" name="checked"/>
<div>
<span v-if="!(dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id))">
{{opaKey[oIndex]}}.
</span>
{{ opt.option }}
</div>
</li>
</template>
<template v-else>
<li :key="oIndex+'-'" :class="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id) ? 'active2' :opt.active ? 'active' : ''" @click="selectCheckbox(opt, opt.id, dItem, item, oIndex)" :data-id="opt.id">
<van-icon v-if="dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id)" class="icon" name="checked"/>
<div>
<span v-if="!(dItem.answerOpt.find(cla => { return cla == opaKey[oIndex] }) && (isAnalysis || $route.query.id))">
{{opaKey[oIndex]}}.
</span>
{{ opt.option }}
</div>
</li>
</template>
</template>
</ul>
<div class="analy" v-if="isAnalysis || $route.query.id">
<div class="tit">答案解析</div>
<div class="txt">正确答案:<span>{{ dItem.answerOpt.toString().replace(new RegExp(',', 'g'), '') }}</span></div>
<template v-if="requestData[item.id]">
<!-- <div class="txt" v-if="requestData[item.id][dItem.id]">您的答案:<span>{{ requestData[item.id][dItem.id].opaVal.sort() }}</span></div> -->
<div class="txt" v-if="requestData[item.id][dItem.id]">您的答案:<span>{{ sorts(requestData[item.id][dItem.id].opaVal) }}</span></div>
<div class="txt" v-else>您的答案:<span>{{ dItem.opaVal2 }}</span></div>
</template>
<template v-else>
<div class="txt">您的答案:<span>{{ dItem.opaVal2 }}</span></div>
</template>
<!-- <div class="txt">您的答案:<span>{{ dItem.opaVal }}</span></div> -->
<div class="exp">
<p class="name">解析:</p>
<p class="nr" v-html="dItem.question_analysis"></p>
</div>
</div>
</swiper-slide>
</template>
</template>
<swiper-slide v-show="questionsData.questions.length">
<card @cardChange="cardChange" @showCard="showCard" :questionsData="questionsData" :requestData="requestData" :clickStatus="clickStatus"></card>
</swiper-slide>
</swiper>
</div>
<template v-if="$route.query.id == undefined">
<div class="btn-box" id="bottom-view-btn">
<div class="padd" v-if="!topicCard.isShow">
<div>
<div v-if="currentNum" :class="collect.currentPages ? 'icon1 active' : 'icon1'" @click="collectQuestion()"></div>
</div>
<div class="analysis" @click="analyShow" v-if="$route.query.id == undefined && ($route.query.course_id || $route.query.tag_id)">查看解析</div>
<div :class="$route.query.course_id && this.$route.query.tag_id ? 'btn left' : 'btn left btn-w'" @click="switchQuestions('left')" v-show="currentNum !== 1">上一题</div>
<div :class="$route.query.course_id && this.$route.query.tag_id ? 'btn right' : 'btn right btn-w'" @click="switchQuestions('right')" v-show="nextBtnShow">下一题</div>
<div :class="$route.query.course_id && this.$route.query.tag_id ? 'btn right' : 'btn right btn-w'" @click="submitShowPop" v-show="!nextBtnShow && isExamEnd">交卷</div>
</div>
<div class="padd new-btn" v-else>
<div :class="$route.query.course_id && this.$route.query.tag_id ? 'btn left' : 'btn left btn-w'" @click="swiper.slidePrev()" v-if="isExamEnd">返回答题</div>
<div :class="$route.query.course_id && this.$route.query.tag_id ? 'btn right' : 'btn right btn-w'" @click="submitShowPop" v-if="isExamEnd">交卷</div>
</div>
</div>
</template>
<template v-else>
<div class="btn-box" id="bottom-view-btn" v-if="questionCount != 1">
<div class="padd" v-if="!topicCard.isShow">
<div v-if="currentNum" :class="collect.currentPages ? 'icon1 active' : 'icon1'" @click="collectQuestion()"></div>
<div class="analysis" @click="analyShow" v-if="$route.query.id == undefined && ($route.query.course_id || $route.query.tag_id)">查看解析</div>
<div :class="$route.query.course_id && this.$route.query.tag_id ? 'btn left' : 'btn left btn-w'" @click="switchQuestions('left')" v-show="currentNum !== 1">上一题</div>
<div :class="$route.query.course_id && this.$route.query.tag_id ? 'btn right' : 'btn right btn-w'" @click="switchQuestions('right')" v-show="nextBtnShow">下一题</div>
<div :class="$route.query.course_id && this.$route.query.tag_id ? 'btn right' : 'btn right btn-w'" @click="submitShowPop" v-show="!nextBtnShow && isExamEnd">交卷</div>
</div>
<div class="padd new-btn" v-else>
<div :class="$route.query.course_id && this.$route.query.tag_id ? 'btn right' : 'btn right btn-w'" @click="submitShowPop" v-if="isExamEnd">交卷</div>
</div>
</div>
</template>
<!-- <exam-end @cardChange="cardChange;examIsShow = false;isExamSubPop = false" @examEndBack="examEndBack" v-if="examIsShow" :questionsData="questionsData" :requestData="requestData"></exam-end> -->
<div class="exam_submit" v-if="isExamSubPop">
<div class="pop">
<div class="tit">交卷</div>
<div class="txt">{{ subPopText }}</div>
<div class="btn_box">
<div class="btn" @click="isExamSubPop = false">继续做题</div>
<div class="btn btn2" @click="examSubmit(1, true)">交卷</div>
</div>
</div>
</div>
<div class="exam-end-pop" v-if="timePopIsShow">
<div class="pop">
<div class="tit">提示</div>
<img src="../assets/images/time.png" alt="">
<div class="txt">考试时间到!答题结束</div>
<div class="btn" @click="submitShowPop(1)">确定</div>
</div>
</div>
</div>
</template>
<script>
import card from '../components/answerCard.vue'
// import answer from '../components/answer.vue'
import examEnd from '../components/examEnd.vue'
import { Toast, Dialog } from 'vant'
import Exam from '../action'
import { Swiper, SwiperSlide, directive } from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
export default {
name: 'ExamTopic',
components: {
Swiper,
SwiperSlide,
card,
examEnd,
[Toast.name]: Toast,
[Dialog.name]: Dialog
},
directives: {
swiper: directive
},
metaInfo () {
if (this.$route.query.id !== undefined) {
return {
title: '题目解析',
meta: [
// { vmid: 'description', name: 'description', content: this.description }
]
}
}
},
data () {
const _this = this
return {
errPageOrder: 1,
countPage: {
currentPage: 1,
showQuestionNum: 10,
noSwiping: false
},
collect: {
currentPages: false,
currentId: '',
currentIdParent: ''
},
currentCollect: false,
clickStatus: 0,
questionCount: Number,
classHide: true,
nextBtnShow: true,
isCard: false,
opaKey: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
remainingTime: '01:00:00',
currentNum: 1,
isAnalysis: false,
swiperSign: 0,
swiperOptions: {
observer: true,
autoHeight: true,
on: {
init() {
setTimeout(() => {
if (_this.$route.query.id !== undefined && _this.$route.query.id !== -1 && _this.$route.query.id !== 1) {
_this.questionsData.questions.map(i => {
const data = i.question_list.find(item => { return item.id === _this.$route.query.id })
if (data) {
_this.cardChange({ id: _this.$route.query.id, num: parseInt(data.num / 10) })
}
})
}
_this.setCurrentCollect()
_this.scrollDom()
_this.getIsSign()
}, 500)
},
slideChangeTransitionStart: function() {
const dataNum = this.slides[this.activeIndex].attributes['data-num']
_this.setCurrentCollect()
_this.getIsSign()
if (!dataNum) {
_this.topicCard.isShow = true
} else {
_this.topicCard.isShow = false
}
document.getElementById('bottom-view').scrollTop = 0
_this.isAnalysis = false
dataNum && (_this.currentNum = parseInt(dataNum.nodeValue))
_this.currentNum === _this.questionsData.sheet.question_count ? _this.nextBtnShow = false : _this.nextBtnShow = true
dataNum && (_this.requestData.answerPage = parseInt(dataNum.nodeValue))
_this.errPageOrder = this.activeIndex + 1
_this.sildesHeightCount()
},
slidePrevTransitionEnd: function() {
if (this.activeIndex === this.slides.length - 2) {
console.log(1)
for (let i = 0; i < this.slides.length; i++) {
if (this.slides[i].attributes['data-num']) {
if (parseInt(this.slides[i].attributes['data-num'].nodeValue) === parseInt(_this.swiperSign)) {
this.slideTo(i, 0, false)
}
}
}
_this.currentNum = parseInt(this.slides[this.activeIndex].attributes['data-num'].nodeValue)
_this.currentNum === _this.questionsData.sheet.question_count ? _this.nextBtnShow = false : _this.nextBtnShow = true
_this.setCurrentCollect()
_this.getIsSign()
}
if (this.activeIndex === 0) {
parseInt(this.slides[this.activeIndex].attributes['data-num'].nodeValue) !== 1 && (_this.prevQuestionLoading())
}
_this.sildesHeightCount()
},
slideNextTransitionEnd: function() {
if (this.activeIndex !== this.slides.length - 1) {
_this.swiperSign = this.activeIndex + 1
}
const dataNum = this.slides[this.activeIndex].attributes['data-num']
if (!dataNum) {
_this.clickStatus = Date.parse(new Date())
} else {
if (this.activeIndex === this.slides.length - 2 && parseInt(dataNum.nodeValue) !== parseInt(_this.questionsData.sheet.question_count)) {
_this.nextQuestionLoading()
}
}
_this.sildesHeightCount()
},
slideNextTransitionStart: function() {
const dataNum = this.slides[this.activeIndex].attributes['data-num']
if (this.activeIndex === this.slides.length - 2 && parseInt(dataNum.nodeValue) !== parseInt(_this.questionsData.sheet.question_count)) {
_this.countPage.noSwiping = true
Toast.loading({
message: '加载中...',
duration: 1000
})
setTimeout(() => {
_this.countPage.noSwiping = false
}, 500)
}
}
}
},
questionsData: {
questions: []
},
requestData: {},
requestData2: {},
clockCount: null,
isExamEnd: true,
// examIsShow: false,
isExamSubPop: false,
timePopIsShow: false,
initTime: null,
cache: null,
topicCard: {
isShow: false
},
signText: '标记',
subPopText: '您还有题目未回答,确定交卷吗?'
}
},
computed: {
sorts() {
return function(data) {
const dSort = Array.isArray(data) ? data.slice().sort() : data
// console.log(data.sort, '======').toString().replace(new RegExp(',', 'g'), '')
return dSort.toString().replace(new RegExp(',', 'g'), '')
}
},
isWeapp() {
return this.$store.state.isWeapp
},
swiper() {
return this.$refs.mySwiper.$swiper
}
},
props: {
requestParam: {
type: Object,
required: false,
default () {
return {}
}
}
},
beforeDestroy() {
clearInterval(this.initTime)
clearInterval(this.clockCount)
this.examSubmit(0)
},
mounted () {
const body = document.querySelector('body')
// 设置页面点击清除 提示框
body.addEventListener('click', (e) => {
Toast.clear()
}, false)
this.answerInit()
},
methods: {
prevQuestionLoading() {
Toast.loading({
message: '加载中...',
duration: 1000
})
const dId = this.swiper.slides[this.swiper.activeIndex].attributes['data-dId'].nodeValue
if (this.countPage.currentPage !== 1) {
this.countPage.currentPage--
}
this.$nextTick(() => {
for (let i = 0; i < this.swiper.slides.length; i++) {
if (this.swiper.slides[i].attributes['data-dId']) {
this.swiper.slides[i].attributes['data-dId'].nodeValue === dId && (this.swiper.slideTo(i, 0, false))
}
}
this.currentNum = parseInt(this.slides[this.activeIndex].attributes['data-num'].nodeValue)
Toast.clear()
})
},
nextQuestionLoading() {
const dId = this.swiper.slides[this.swiper.activeIndex].attributes['data-dId'].nodeValue
this.countPage.currentPage++
this.$nextTick(() => {
for (let i = 0; i < this.swiper.slides.length; i++) {
if (this.swiper.slides[i].attributes['data-dId']) {
this.swiper.slides[i].attributes['data-dId'].nodeValue === dId && (this.swiper.slideTo(i, 0, false))
}
}
})
},
sildesHeightCount() {
// slides高度重新赋值
if (document.getElementById('bottom-view-btn') === null) {
return false
}
const computedStyle = window.getComputedStyle(this.swiper.slides[this.swiper.activeIndex], null)
const topView = document.getElementById('top-view').clientHeight
const bottomView = document.getElementById('bottom-view-btn').clientHeight
const slidesClentH = document.body.clientHeight - topView - bottomView
const slidesH = this.swiper.slides[this.swiper.activeIndex].clientHeight
const countNum = slidesClentH - slidesH
const sliedsRealH = slidesClentH - (slidesH - parseInt(computedStyle.paddingBottom))
if (countNum >= 0) {
this.swiper.slides[this.swiper.activeIndex].style.paddingBottom = `${sliedsRealH}px`
} else {
this.swiper.slides[this.swiper.activeIndex].style.paddingBottom = '1.2rem'
}
},
collectQuestion() {
this.setCurrentCollect()
const queIds = this.collect.currentId
this.collect.currentPages ? this.removeColl(queIds, () => {
Toast('取消收藏')
this.collect.currentPages = false
this.setCollect(false)
}) : this.addColl(queIds, () => {
Toast('收藏成功')
this.collect.currentPages = true
this.setCollect(true)
})
},
setCollect(state) {
this.questionsData.questions.map(item => {
if (item.id === this.collect.currentIdParent) {
const data = item.question_list.find(i => { return i.id === this.collect.currentId })
data.is_collection = state
}
})
},
addColl(id, callback) {
Exam.collectQuestion({ question_id: id }).then(res => {
if (res.code === 0) {
callback()
}
})
},
removeColl(id, callback) {
Exam.deleteCollect({ type: 2, question_id: id }).then(res => {
if (res.code === 0) {
callback()
}
})
},
// 页面初始化
answerInit() {
Toast.loading({ message: '加载中...', duration: 5000 })
if (this.$route.query.id === undefined) {
clearInterval(this.initTime)
this.initTime = setInterval(() => {
this.examSubmit(0)
}, 3000)
if (this.$route.query.tag_id !== undefined) {
Exam.getExam(this.requestParam, decodeURIComponent(this.requestParam.papersUrl)).then(res => {
parseInt(res.status) === 1 || parseInt(res.status) === 2
? this.requestParam.is_create = 1
: this.requestParam.is_create = 0
this.initData()
})
} else {
this.initData()
}
return false
}
this.examEnd()
this.initData()
},
getIsSign() {
const currentSlides = this.swiper.slides[this.swiper.activeIndex]
if (currentSlides.attributes['data-dId']) {
const itemDid = currentSlides.attributes['data-dId'].nodeValue
const itemCid = currentSlides.attributes['data-cId'].nodeValue
if (this.requestData[itemCid]) {
this.requestData[itemCid][itemDid]
? this.requestData[itemCid][itemDid].sign
? this.signText = '已标记'
: this.signText = '标记'
: this.signText = '标记'
return false
}
this.signText = '标记'
}
},
submitShowPop(n) {
// const isLogin = await this.$store.dispatch('checkLogin')
// if (!isLogin) {
// if (this.isWeapp) {
// // 小程序
// wx.miniProgram.navigateTo({
// url: `/pages/login/index?redirect_uri=${encodeURIComponent(
// `/exam/result?type=${this.$route.query.type}&is_create=0&papersUrl=${this.$route.query.papersUrl}`
// )}`
// })
// } else {
// this.$router.push({
// path: '/login',
// query: {
// redirect_uri: `/exam/result?type=${this.$route.query.type}&is_create=0&papersUrl=${this.$route.query.papersUrl}`
// }
// })
// }
// return false
// }
if (n === 1) {
this.examSubmit(1, true)
return false
}
let count = 0
Object.keys(this.requestData).map((item, index) => {
for (let i = 0; i < Object.keys(this.requestData[item]).length; i++) {
count++
}
})
if (count === this.questionsData.sheet.question_count) {
this.subPopText = '确定交卷吗?'
} else {
this.subPopText = '您还有题目未回答,确定交卷吗?'
}
this.isExamSubPop = true
},
// 答题结束禁止答题
examEnd() {
this.isExamEnd = false
},
// 答题卡点击跳题
cardChange(data) {
Toast.loading({
message: '加载中...',
duration: 3000,
loadingType: 'spinner'
})
const e = data.id
this.countPage.currentPage = data.num + 1
this.$nextTick(() => {
for (let i = 0; i < this.swiper.slides.length; i++) {
if (this.swiper.slides[i].attributes['data-dId']) {
if (this.swiper.slides[i].attributes['data-dId'].nodeValue === e) {
setTimeout(() => {
document.getElementById('bottom-view').scrollTop = 0
this.swiper.slideTo(i, 0, false)
this.topicCard.isShow = false
setTimeout(() => {
this.requestData.answerPage = this.swiper.slides[i].attributes['data-num'].nodeValue
this.currentNum = parseInt(this.swiper.slides[i].attributes['data-num'].nodeValue)
this.currentNum === this.questionsData.sheet.question_count
? this.nextBtnShow = false
: this.nextBtnShow = true
this.setCurrentCollect()
}, 100)
this.getIsSign()
}, 200)
}
}
}
})
},
showCard() {
const dataNum = parseInt(this.swiper.slides[this.swiper.activeIndex].attributes['data-num'].nodeValue)
this.requestData.answerPage = dataNum
this.swiperSign = dataNum
this.clickStatus = Date.parse(new Date())
document.getElementById('bottom-view').scrollTop = 0
this.topicCard.isShow = true
this.swiper.slideTo(this.swiper.slides.length - 1, 0, false)
},
// 标记
signQuestion() {
const itemDid = this.swiper.slides[this.swiper.activeIndex].attributes['data-dId'].nodeValue
const itemCid = this.swiper.slides[this.swiper.activeIndex].attributes['data-cId'].nodeValue
const on = () => {
this.signText = '已标记'
Toast({ duration: 500, message: '已完成标记' })
this.clickStatus = Date.parse(new Date())
}
const off = () => {
this.signText = '标记'
Toast({ duration: 500, message: '已取消标记' })
this.clickStatus = Date.parse(new Date())
}
if (!this.requestData[itemCid]) {
this.requestData[itemCid] = {
[itemDid]: { sign: true, answer: [], opaVal: [] }
}
on()
return false
}
if (!this.requestData[itemCid][itemDid]) {
this.requestData[itemCid][itemDid] = { sign: true, answer: [], opaVal: [] }
on()
return false
}
if (this.requestData[itemCid][itemDid].sign) {
this.requestData[itemCid][itemDid].sign = false
off()
return false
}
this.requestData[itemCid][itemDid].sign = true
on()
},
// 时间倒计时
setClock(time) {
let sec = parseInt(time)
clearInterval(this.clockCount)
this.clockCount = setInterval(() => {
if (this.requestParam.course_id || this.requestParam.tag_id) {
this.remainingTime = this.secondToDate(sec)
sec++
} else {
if (this.$route.query.id) {
this.remainingTime = '00:00:00'
return false
}
sec--
if (sec === 0) {
clearInterval(this.clockCount)
this.remainingTime = '00:00:00'
this.timePopIsShow = true
return false
}
if (sec === 600) {
Toast({ duration: 1000, message: '距离考试结束还有10分钟!' })
}
this.remainingTime = this.secondToDate(sec)
}
this.questionsData.sheet.duration++
}, 1000)
},
secondToDate(result) {
const h = Math.floor(result / 3600) < 10 ? '0' + Math.floor(result / 3600) : Math.floor(result / 3600)
const m = Math.floor((result / 60 % 60)) < 10 ? '0' + Math.floor((result / 60 % 60)) : Math.floor((result / 60 % 60))
const s = Math.floor((result % 60)) < 10 ? '0' + Math.floor((result % 60)) : Math.floor((result % 60))
if (h === 0) {
result = m + ':' + s
} else {
result = h + ':' + m + ':' + s
}
return result
},
// 点击上一题下一题按钮切换
switchQuestions(e) {
e === 'left' ? this.swiper.slidePrev() : this.swiper.slideNext()
// 切换时关闭解析
this.isAnalysis = false
},
// 显示隐藏解析
analyShow() {
this.isAnalysis = !this.isAnalysis
setTimeout(() => {
this.sildesHeightCount()
}, 200)
},
// 单选事件 判断题事件
selectRadio(cData, checkId, index, oData) {
if (!this.isExamEnd) {
return false
}
const opa = this.opaKey
cData.opaVal = opa[index]
cData.activeIndex = index
if (this.requestData[oData.id]) {
if (!this.requestData[oData.id][cData.id]) {
this.requestData[oData.id][cData.id] = {
sign: false,
answer: [checkId],
opaVal: [cData.opaVal]
}
} else {
this.requestData[oData.id][cData.id].answer = [checkId]
this.requestData[oData.id][cData.id].opaVal = [cData.opaVal]
}
} else {
this.requestData[oData.id] = {
[cData.id]: {
sign: false,
answer: [checkId],
opaVal: [cData.opaVal]
}
}
}
// this.clickStatus = Date.parse(new Date())
this.$forceUpdate()
},
// 多选事件
selectCheckbox(opt, checkId, cData, oData, index) {
if (!this.isExamEnd) {
return false
}
opt.active === undefined ? opt.active = true : opt.active = !opt.active
if (opt.active) {
if (this.requestData[oData.id]) {
if (this.requestData[oData.id][cData.id]) {
const arr = this.requestData[oData.id][cData.id].answer
const arr2 = this.requestData[oData.id][cData.id].opaVal
this.requestData[oData.id][cData.id].sign === undefined
? this.requestData[oData.id][cData.id].sign = false
: this.requestData[oData.id][cData.id].sign = this.requestData[oData.id][cData.id].sign
arr.push(checkId)
arr2.push(this.opaKey[index])
this.requestData[oData.id][cData.id].answer = arr
this.requestData[oData.id][cData.id].opaVal = arr2
} else {
this.requestData[oData.id][cData.id] = {
sign: false,
answer: [checkId],
opaVal: [this.opaKey[index]]
}
}
} else {
this.requestData[oData.id] = {
[cData.id]: {
sign: false,
answer: [checkId],
opaVal: [this.opaKey[index]]
}
}
}
} else {
if (this.requestData[oData.id][cData.id].answer.length > 1) {
const index = this.requestData[oData.id][cData.id].answer.indexOf(checkId)
this.requestData[oData.id][cData.id].answer.splice(index, 1)
this.requestData[oData.id][cData.id].opaVal.splice(index, 1)
} else {
opt.active = true
}
}
const opa = this.opaKey
const opaArr = []
cData.question_options.map((item, i) => {
if (item.active) {
opaArr.push(opa[i])
}
cData.opaVal2 = opaArr.toString().replace(new RegExp(',', 'g'), '')
})
this.$forceUpdate()
},
// 页面过高局部滚动
scrollDom() {
const topViewH = document.getElementById('top-view').offsetHeight
const clientHeight = document.documentElement.clientHeight
const bottomView = document.getElementById('bottom-view')
bottomView.style.height = (clientHeight - topViewH) + 'px'
},
initData(n) {
const param = this.requestParam
// if (window.localStorage.userId !== 'undefined') {
// param.user_id = window.localStorage.userId
// }
Exam.getExam(param, decodeURIComponent(this.requestParam.papersUrl)).then(res => {
Toast.clear()
let times = this.$route.query.id === undefined ? res.sheet.remainingTime ? res.sheet.remainingTime : res.sheet.duration : res.sheet.duration
times === undefined && (times = 0)
parseInt(this.$route.query.id) === -1 ? this.errorQuestion(res) : this.questionsData = res
this.questionCount = res.sheet.question_count
this.remainingTime = this.secondToDate(times)
if (this.$route.query.id === undefined) {
this.setClock(times)
}
this.currentNum === this.questionsData.sheet.question_count
? this.nextBtnShow = false
: this.nextBtnShow = true
if (res.answers !== null) {
if (res.answers.answerPage !== undefined && this.$route.query.id === undefined && res.answers.answerPage !== this.questionsData.sheet.question_count) {
this.countPage.currentPage = parseInt(res.answers.answerPage / 10) + 1
this.$nextTick(() => {
for (let i = 0; i < this.swiper.slides.length; i++) {
if (this.swiper.slides[i].attributes['data-num']) {
parseInt(this.swiper.slides[i].attributes['data-num'].nodeValue) === parseInt(res.answers.answerPage) && (this.swiper.slideTo(i, 0, false))
}
}
this.currentNum = res.answers.answerPage
this.currentNum === this.questionsData.sheet.question_count ? this.nextBtnShow = false : this.nextBtnShow = true
})
}
const datas = res.answers
if (datas.length === 0) {
this.requestData = {}
this.requestData2 = 0
return false
}
this.requestData = datas
this.requestData2 = Object.keys(datas).length
} else {
this.requestData = {}
this.requestData2 = 0
}
})
},
setCurrentCollect() {
if (this.swiper.activeIndex + 1 === this.swiper.slides.length) {
return false
}
const itemDid = this.swiper.slides[this.swiper.activeIndex].attributes['data-dId'].nodeValue
const itemCid = this.swiper.slides[this.swiper.activeIndex].attributes['data-cId'].nodeValue
this.questionsData.questions.find(item => {
if (item.id === itemCid) {
const currentItem = item.question_list.find(i => { return i.id === itemDid })
this.collect.currentPages = currentItem.is_collection
this.collect.currentId = itemDid
this.collect.currentIdParent = itemCid
}
})
},
errorQuestion(data) {
let count = 0
const result = data.questions.map(item => {
const scroeList = data.score_items[item.id]
const list = item.question_list.filter(subitem => {
const score = scroeList[subitem.id]
return !score.is_right
})
item.question_list = list
return item
})
result.map(item => {
item.question_list.map(i => {
count++
i.num = count
})
})
data.sheet.question_count = count
data.questions = result
console.log(data)
this.questionsData = data
},
getSecond(time) {
let s = ''
const hour = time.split(':')[0]
const min = time.split(':')[1]
const sec = time.split(':')[2]
s = Number(hour * 3600) + Number(min * 60) + Number(sec)
return s
},
goExamResult(n) {
const param = this.requestParam
param.is_create = 0
this.$emit('goExamResult', this.requestParam)
},
examSubmit(status, isSub) {
if (status === 0 && this.swiper.activeIndex + 1 === this.swiper.slides.length) {
return false
}
const reqData = this.requestData
const param = {
sheet_id: this.questionsData.sheet.id,
answers: JSON.stringify(reqData),
duration: this.questionsData.sheet.duration
}
parseInt(this.$route.query.type) === 1 && (param.type = 1)
param.status = status
Exam.examSubmit(param, this.requestParam.papersUrl).then(res => {
if (res.exm_status === 1) {
clearInterval(this.initTime)
clearInterval(this.clockCount)
if (this.requestParam.course_id !== undefined || this.requestParam.tag_id !== undefined || this.questionsData.sheet.remainingTime > 0) {
if (isSub) {
this.goExamResult()
}
return false
}
this.remainingTime = '00:00:00'
this.timePopIsShow = true
}
})
}
}
}
</script>
<style lang="scss">
.no-swiping{
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 999999;
}
.exam-box{
.swiper-wrapper{
padding-bottom: env(safe-area-inset-bottom);
.swiper-slide{
padding-bottom: 1.2rem;
}
}
.head{
color: #222;
display: flex;
padding: .4rem 0;
align-items: center;
i{
font-size: .35rem;
margin-left: .28rem;
}
.title{
margin-left: .03rem;
font-size: .3rem;
}
}
.tool-box{
width: 6.7rem;
border-bottom: .01rem solid rgba(238,238,238,1);
padding: .35rem 0;
display: flex;
align-items: center;
margin: 0 auto;
color: #222;
.time{
font-size: .3rem;
}
.tag-box{
margin-left: auto;
display: flex;
span{
padding: .1rem .2rem;
font-size: .2rem;
color: #fff;
background: #F47885;
border-radius: .25rem;
margin-left: .2rem;
}
.active{
background: #FF8000;
}
}
}
.topic-box{
width: 100%;
margin: 0 auto;
font-size: .3rem;
color: #222;
overflow-y: scroll;
.title{
padding: .4rem;
display: flex;
align-items: center;
.count{
margin-left: auto;
}
}
.topic{
font-size: .26rem;
line-height: .4rem;
padding: 0 .4rem;
span{
width: 100%;
display: block;
}
img{
width: 100%;
display: block;
}
}
.topic-bt{
font-size: .26rem;
line-height: .4rem;
padding: 0 .4rem;
// font-weight: bold;
margin-bottom: .2rem;
span{
width: 100%;
display: block;
}
img{
width: 100%;
display: block;
}
}
.option{
list-style: none;
margin: .35rem 0 0 0;
padding: 0 .4rem;
li{
background:rgba(247,247,247,1);
border-radius: .2rem;
font-weight: bold;
padding: .35rem .2rem;
margin-bottom: .2rem;
word-wrap:break-word;
display: flex;
align-items: center;
.icon{
margin-right: 0.1rem;
color: #5CB9A2;
}
}
.active{
color: #5F95DE;
background:#DDF1FF;
}
.active2{
color: #40A38B;
background: #E4F9ED;
}
}
.analy{
font-size: .3rem;
color: #222;
padding: 0.2rem .4rem 1.2rem .4rem;
.tit{
font-weight: bold;
line-height: 100%;
}
.txt{
line-height: 100%;
margin-top: .4rem;
span{
font-weight: bold;
}
}
.exp{
margin-top: .4rem;
overflow: hidden;
p{
float: left;
}
.nr{
// width: 5.8rem;
p{
width: 100%;
}
img{
width: 100%;
}
}
}
}
}
.topic-box.hide{
overflow: hidden;
}
.btn-box{
position: fixed;
bottom: 0;
left: 0;
height: 1rem;
box-shadow:0px 0px 6px 0px rgba(0,0,0,0.05);
background: #fff;
color: #222;
font-size: .3rem;
width: 100%;
z-index: 99;
padding-bottom: env(safe-area-inset-bottom);
.padd{
display: flex;
padding: 0 .4rem;
align-items: center;
height: 100%;
}
.analysis{
white-space: nowrap;
margin-right: .3rem;
}
.icon1{
width: .36rem;
height: .36rem;
background: url('../assets/images/collect.png');
background-size: 100% 100%;
margin-right: .3rem;
}
.icon1.active{
background: url('../assets/images/collect2.png');
background-size: 100% 100%;
}
.btn{
width:1.97rem;
height:.7rem;
background:#C62245;
border-radius:.12rem;
text-align: center;
line-height: .7rem;
color: #fff;
}
.btn-w{
width: 2.77rem;
}
.left{
margin-right: .4rem;
}
.right{
margin-left: auto;
// background:rgba(255,103,103,1);
}
}
.exam_submit{
position: fixed;
top: 0;
left: 0;
z-index: 999999;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
.pop{
width: 5.9rem;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%,-50%);
background:rgba(255,255,255,1);
border-radius:.12rem;
padding: 0.4rem 0;
.tit{
font-weight:bold;
color:rgba(34,34,34,1);
font-size:.3rem;
text-align: center;
line-height: 100%;
}
.txt{
color:rgba(34,34,34,1);
font-size:.3rem;
text-align: center;
line-height: 100%;
margin-top: .8rem;
}
.btn_box{
padding:0 0.2rem;
display: flex;
margin-top: .8rem;
.btn{
width:2.6rem;
height:.7rem;
background:#C62245;
border-radius:.12rem;
text-align: center;
line-height: .7rem;
color: #fff;
font-size: .3rem;
}
.btn2{
margin-left: auto;
}
}
}
}
.exam-end-pop{
position: fixed;
top: 0;
left: 0;
z-index: 999999;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.6);
.pop{
width: 5.9rem;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%,-50%);
background:rgba(255,255,255,1);
border-radius:.12rem;
padding: .4rem 0;
.tit{
font-size: .3rem;
color: #222;
text-align: center;
font-weight: bold;
}
img{
width: 1.59rem;
height: 1.35rem;
display: block;
margin: 0.4rem auto 0.2rem auto;
}
.txt{
color: #222;
font-size: .3rem;
line-height: 100%;
text-align: center;
}
.btn{
margin: .4rem auto 0 auto;
width:5.5rem;
height:.7rem;
background:#C62245;
border-radius:.12rem;
text-align: center;
line-height: .7rem;
color: #fff;
font-size: .3rem;
}
}
}
}
</style>
/* 模块基于 element-ui,一定在 element-ui后加载 */
import ExamModule from './exam-module'
const components = [ExamModule]
const install = function(Vue, opts = {}) {
components.forEach(component => {
Vue.use(component, opts)
})
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export default {
install,
ExamModule
}
import httpRequest from '@/utils/axios'
/**
* 获取课程详情
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function getCourse(semesterId, courseId) {
return httpRequest.get(`/api/zy/v2/education/courses/${courseId}`).then(response => {
response.chapters = response.chapters.filter(item => {
item.children = item.children.filter(child => child.type === 2)
return item.children.length
})
return response
})
}
/**
* 获取章节资源详情
* @param {string} vid 资源ID
*/
export function getChapterVideo(vid) {
return httpRequest.post(
'/api/zy/v2/education/video-streaming',
{ vid },
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取章节资源详情
* @param {string} vid 章节的资源ID
*/
export function getChapterVideoAliyun(vid) {
return httpRequest.post(
'/api/zy/v2/education/aliyun-video-streaming',
{ vid },
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取章节视频播放进度
* @param {string} semesterId 学期ID
* @param {string} resourseId 章节的资源ID
* @param {Object} params
*/
export function getChapterVideoProgress(semesterId, resourseId, params) {
return httpRequest.get(`/api/zy/v2/education/video/${resourseId}/device`, { params })
}
/**
* 更新章节视频播放进度
* @param {Object} params
*/
export function updateChapterVideoProgress(params) {
return httpRequest.get('/api/zy/v2/analytics/upload-video', { params })
}
/**
* 获取章节作业
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} resourseId 章节的资源ID
*/
export function getChapterHomework(semesterId, courseId, resourseId) {
return httpRequest.get(`/api/zy/v2/education/homeworks/${courseId}/${resourseId}`)
}
/**
* 获取提交作业截止时间
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} chapterId 章节ID
*/
export function getChapterHomeworkDeadline(semesterId, courseId, chapterId) {
return httpRequest.get(`/api/zy/v2/education/homeworks/${courseId}/${chapterId}/deadline`)
}
/**
* 提交考试
*/
export function sbumitChapterHomework(data) {
return httpRequest.post('/api/zy/v2/education/homeworks', data, {
headers: { 'Content-Type': 'application/json' }
})
}
/**
* 上传文件
*/
export function uploadFile(data) {
return httpRequest.post('/api/zy/util/upload-file', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* 获取课程大作业详情
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function getCourseWork(semesterId, courseId) {
return httpRequest.get(`/api/zy/v2/education/courses/${courseId}/essay`)
}
/**
* 提交课程大作业
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function updateCourseWork(semesterId, courseId, data) {
return httpRequest.post(`/api/zy/v2/education/courses/${courseId}/essay`, data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* 获取课程考试试题
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function getCourseExam(semesterId, courseId) {
return httpRequest.get(`/api/zy/v2/education/${courseId}/examination`)
}
/**
* 获取课程考试状态
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} examId 试题ID
*/
export function getCourseExamStatus(semesterId, courseId, examId) {
return httpRequest.get(`/api/zy/v2/education/${courseId}/examination/${examId}/status`)
}
/**
* 提交课程考试
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} examId 试题ID
*/
export function submitCourseExam(semesterId, courseId, examId, data) {
return httpRequest.post(`/api/zy/v2/education/${courseId}/examination/${examId}/sheet`, data, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
})
}
/**
* 获取课程考试结果
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} examId 试题ID
*/
export function getCourseExamResult(semesterId, courseId, examId, params) {
return httpRequest.get(`/api/zy/v2/education/${courseId}/examination/${examId}/sheet`, { params })
}
<template>
<ul class="chapter-list">
<li class="chapter-item" v-for="item in chapters" :key="item.id">
<h4>{{ item.name }}</h4>
<ul class="chapter-item-list">
<li
v-for="subItem in item.children"
:key="subItem.id"
@click="onClick(subItem)"
:class="{ 'is-active': subItem.id === (active ? active.id : '') }"
>
<span class="chapter-item-list__name">{{ subItem.name | showName(subItem) }}</span>
<i class="el-icon" :class="genIconClass(subItem.type)"></i>
</li>
</ul>
</li>
</ul>
</template>
<script>
export default {
props: {
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
},
chapters: { type: Array, default: () => [] },
// 当前选中的章节
active: {
type: Object,
default() {
return {}
}
}
},
data() {
return {}
},
filters: {
showName(name, data) {
if ([5, 8].includes(data.type) && data.live) {
return `${name}(${data.live.start_time})`
}
return name
}
},
methods: {
genIconClass(type) {
const map = {
2: 'el-icon-self-iconset0481',
3: 'el-icon-edit-outline',
4: 'el-icon-self-cc-book'
}
return map[type] || 'el-icon-self-cc-book'
},
onClick(data) {
if (data.type === 1) {
return
}
// zoom直播
if (data.type === 8) {
const live = data.live
const hasRecordUrl = live.enable_record && live.record_url
if ([3, 5].includes(live.live_status) && !hasRecordUrl) {
this.$message.error('直播结束')
return
}
window.open(live.record_url || live.join_url)
return
}
// 课程大作业
if (data.id === 'course_work' && !this.data.survey) {
this.$message('请先填写教学评估,然后完成大作业。')
return
}
// 教学评估
if (data.id === 'teach_evaluation') {
const { sid, cid } = this.$route.params
this.$router.push({ name: 'survey', params: { sid, cid } })
return
}
this.$router.push({ name: 'viewerCourseChapter', params: { id: data.id } })
}
}
}
</script>
<style lang="scss" scoped>
/* 章列表样式 */
.chapter-list {
margin: 0;
padding: 0;
line-height: 1.6;
overflow: hidden;
.chapter-item {
h4 {
padding: 10px 22px;
margin: 0;
font-size: 15px;
color: #b0b0b0;
background-color: #2f2f2f;
}
/* 节列表样式 */
.chapter-item-list {
margin: 0;
padding: 0;
line-height: 1.6;
overflow: hidden;
li {
position: relative;
&.is-active {
background: #3c3c3c;
.chapter-item-list__name {
color: #c01540;
}
}
&:hover {
background: #3c3c3c;
}
&:before {
display: block;
content: '';
position: absolute;
left: 13px;
top: 16px;
z-index: 10;
width: 18px;
height: 18px;
background: #5b5b5b;
border: 2px solid #5b5b5b;
border-radius: 50%;
}
&:after {
display: block;
content: '';
position: absolute;
left: 22px;
top: 0;
z-index: 5;
width: 1px;
height: 100px;
background: #616161;
}
}
.chapter-item-list__name {
display: block;
padding: 15px 35px 15px 40px;
font-size: 14px;
color: #909090;
text-decoration: none;
cursor: pointer;
}
}
/* 章节后面小图标的样式 */
.el-icon {
position: absolute;
font-size: 16px;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #a0a0a0;
}
}
}
</style>
<template>
<aside class="course-viewer-aside">
<el-tabs v-model="activeName">
<el-tab-pane label="章节列表" name="0">
<div class="tab-pane">
<aside-chapter :data="data" :chapters="chapters" :active="active"></aside-chapter>
</div>
</el-tab-pane>
<el-tab-pane label="学习资料" name="1" v-if="active && active.type === 2">
<div class="tab-pane">
<aside-lecture :ppts="ppts" :pptIndex="pptIndex" v-on="$listeners"></aside-lecture>
</div>
</el-tab-pane>
</el-tabs>
</aside>
</template>
<script>
import AsideChapter from './chapter.vue'
import AsideLecture from './lecture.vue'
export default {
props: {
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
},
// 章节
chapters: { type: Array, default: () => [] },
// 讲义
ppts: { type: Array, default: () => [] },
// 当前选中的章节
active: {
type: Object,
default() {
return {}
}
},
// 当前选择的PPT
pptIndex: { type: Number, default: 0 }
},
components: { AsideChapter, AsideLecture },
data() {
return {
activeName: '0'
}
}
}
</script>
<style lang="scss" scoped>
.course-viewer-aside {
width: 350px;
min-height: 100vh;
background-color: #232323;
}
.tab-pane {
height: calc(100vh - 56px);
overflow-y: auto;
}
::v-deep .el-tabs__header {
margin: 0;
}
::v-deep .el-tabs__nav {
float: none;
display: flex;
}
::v-deep .el-tabs__item {
flex: 1;
height: 56px;
font-size: 16px;
line-height: 56px;
color: #909090;
text-align: center;
&.is-active {
color: #c01540;
}
}
::v-deep .el-tabs__active-bar,
::v-deep .el-tabs__nav-wrap::after {
display: none;
}
</style>
<template>
<ul class="lecture-list">
<li
v-for="(item, index) in ppts"
:key="item.id"
@click="onClick(index)"
:class="{'is-active': index === activeIndex}"
>
<img :src="item.ppt_url" />
</li>
</ul>
</template>
<script>
export default {
props: {
// 当前选择的PPT
pptIndex: { type: Number, default: 0 },
ppts: { type: Array, default: () => [] }
},
data() {
return {
activeIndex: this.pptIndex
}
},
watch: {
pptIndex(index) {
this.activeIndex = index
}
},
methods: {
// 点击PPT
onClick(index) {
this.activeIndex = index
this.$emit('change-ppt', index)
}
}
}
</script>
<style lang="scss" scoped>
.lecture-list {
padding: 0 16px;
li {
padding: 8px 16px;
cursor: pointer;
&.is-active {
background: #888;
}
img {
width: 100%;
}
}
}
</style>
<template>
<div class="course-viewer-content">
<div class="course-viewer-content-hd">
<slot name="header">
<h3 class="course-viewer-content-hd__title">
<slot name="title">{{title}}</slot>
</h3>
<div class="course-viewer-content-hd__aside">
<slot name="header-aside"></slot>
</div>
</slot>
</div>
<div class="course-viewer-content-bd">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'Continaer',
props: { title: String }
}
</script>
<template>
<div class="editor">
<textarea name="editor" :id="textareaElementId" :disabled="disabled"></textarea>
</div>
</template>
<script>
import { uniqueId } from 'lodash'
export default {
name: 'VEditor',
props: {
value: { type: String },
disabled: { type: Boolean, default: false }
},
data() {
return {
textareaElementId: uniqueId('editor_'),
ckEditor: null
}
},
watch: {
value(val) {
if (this.ckEditor && this.ckEditor.getData() !== val) {
this.ckEditor.setData(val)
}
},
disabled(val) {
if (this.ckEditor && this.ckEditor.instanceReady) {
this.ckEditor.setReadOnly(val)
}
}
},
methods: {
createEditor() {
const config = {
height: 400,
uiColor: '#eeeeee',
filebrowserImageUploadUrl: '/api/ck/form/ckeditor-upload',
fileTools_requestHeaders: { tenant: 'sofia' },
// resize_enabled: typeof this.props.resizable === 'boolean' ? this.props.resizable : true,
toolbar: [
// { name: 'document', items: ['Source', '-', 'Save', 'NewPage', 'Preview'] },
{ name: 'styles', items: ['Styles', 'Format', 'Font', 'FontSize'] },
{ name: 'colors', items: ['TextColor', 'BGColor'] },
{ name: 'tools', items: ['Maximize', 'ShowBlocks'] },
// { name: 'clipboard', items: ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo'] },
{ name: 'editing', items: ['Find', 'Replace'] },
// { name: 'forms', items: ['Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', 'Button', 'ImageButton', 'HiddenField'] },
'/',
{
name: 'basicstyles',
items: ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat']
},
{
name: 'paragraph',
items: [
'NumberedList',
'BulletedList',
'-',
'Outdent',
'Indent',
'-',
'Blockquote',
'CreateDiv',
'-',
'JustifyLeft',
'JustifyCenter',
'JustifyRight',
'JustifyBlock',
'-',
'BidiLtr',
'BidiRtl'
]
},
{ name: 'links', items: ['Link', 'Unlink', 'Anchor'] },
{ name: 'insert', items: ['Image', 'Table', 'HorizontalRule'] }
]
}
// if (this.disabled !== null) {
// console.log(this.disabled)
// config.readOnly = this.disabled
// }
const editor = (this.ckEditor = CKEDITOR.replace(this.textareaElementId, config))
editor.on('instanceReady', () => {
const data = this.value
editor.fire('lockSnapshot')
editor.setData(data, {
callback: () => {
this.bindEvent()
const newData = editor.getData()
// Locking the snapshot prevents the 'change' event.
// Trigger it manually to update the bound data.
if (data !== newData) {
this.$once('input', () => {
this.$emit('ready', editor)
})
this.$emit('input', newData)
} else {
this.$emit('ready', editor)
}
editor.fire('unlockSnapshot')
}
})
editor.setReadOnly(this.disabled)
})
},
bindEvent() {
const editor = this.ckEditor
editor.on('change', evt => {
const data = editor.getData()
if (this.value !== data) {
this.$emit('input', data, evt, editor)
}
})
editor.on('focus', evt => {
this.$emit('focus', evt, editor)
})
editor.on('blur', evt => {
this.$emit('blur', evt, editor)
})
}
},
mounted() {
this.createEditor()
},
beforeDestroy() {
this.ckEditor && this.ckEditor.destroy()
this.ckEditor = null
}
}
</script>
<style lang="scss" scoped>
* {
margin: 0;
padding: 0;
}
</style>
<template>
<div class="upload">
<el-upload action :disabled="disabled" :show-file-list="false" :http-request="httpRequest">
<slot></slot>
<el-button type="text" icon="el-icon-upload">点击上传</el-button>
<template v-slot:tip>
<div class="el-upload__tips">
<slot name="tip"></slot>
</div>
</template>
</el-upload>
<div class="file-list" v-if="fileList.length">
<div class="file-list-item" v-for="(item, index) in fileList" :key="index">
<a :href="item.url" :download="item.name" target="_blank">
<i class="el-icon-document"></i>
{{ item.name }}
</a>
<div>
<a href="javascript:;" @click="handleRemove(index)" style="margin-right: 10px" v-if="!disabled">
<el-tooltip effect="dark" content="删除">
<i class="el-icon-delete"></i>
</el-tooltip>
</a>
<a :href="item.url" :download="item.name" target="_blank">
<el-tooltip effect="dark" content="下载">
<i class="el-icon-download"></i>
</el-tooltip>
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import * as api from '../../api'
export default {
name: 'VUpload',
props: {
value: { type: [String, Array] },
disabled: { type: Boolean, default: false }
},
data() {
return {
fileList: []
}
},
watch: {
value: {
immediate: true,
handler(value) {
if (!value) {
return
}
let fileList = []
if (Array.isArray(value)) {
fileList = value.map(item => {
return { name: item.name || item, url: item.url || item }
})
} else {
fileList.push({ name: '附件下载', url: value })
}
this.fileList = fileList
}
}
},
methods: {
httpRequest(xhr) {
api
.uploadFile({ file: xhr.file })
.then(response => {
if (response.success) {
if (Array.isArray(this.value)) {
this.fileList.push({ name: xhr.file.name, url: response.url })
this.$emit('input', this.fileList)
} else {
this.fileList = [response.url]
this.$emit('input', response.url)
}
}
})
.catch(error => {
this.$message.error(error.message)
})
},
handleRemove(index) {
this.fileList.splice(index, 1)
this.$emit('input', Array.isArray(this.value) ? this.fileList : '')
}
}
}
</script>
<style lang="scss" scoped>
.file-list-item {
display: flex;
margin-bottom: 10px;
padding: 0 10px;
justify-content: space-between;
line-height: 30px;
background-color: #fff;
border-radius: 4px;
a {
text-decoration: none;
color: #333;
&:hover {
color: #c01540;
}
}
}
</style>
<template>
<component :is="currentCompoent" :chapter="chapter" v-bind="$attrs" v-on="$listeners" v-if="chapter" :key="pid" />
</template>
<script>
// components
import ChapterPlayer from './player/chapterPlayer.vue' // 章节视频
import ChapterWork from './work/index.vue' // 章节作业
import ChapterExam from './work/chapterExam.vue' // 章节考试
import ChapterRead from './read/chapterRead.vue' // 章节资料
import ChapterLive from './live/chapterLive.vue' // 章节直播
import CourseWork from './work/courseWork.vue' // 课程大作业
import CourseRead from './read/courseRead.vue' // 课程资料
import CourseExam from './work/courseExam.vue' // 课程考试
export default {
name: 'ViewerLayout',
components: {
ChapterPlayer,
ChapterWork,
ChapterRead,
ChapterExam,
ChapterLive,
CourseWork,
CourseRead,
CourseExam
},
props: {
chapter: {
type: Object,
default() {
return {}
}
}
},
computed: {
currentCompoent() {
const componentNames = {
2: 'ChapterPlayer', // 视频
3: 'ChapterWork', // 作业
4: 'ChapterRead', // 资料
5: 'ChapterLive', // CC直播
8: 'ChapterLive', // CC直播
9: 'ChapterExam', // 考试
99: 'CourseWork', // 课程大作业
100: 'CourseRead', // 课程资料
101: 'CourseExam' // 课程考试
}
return this.chapter ? componentNames[this.chapter.type] || '' : ''
},
pid() {
return this.$route.params.id
}
}
}
</script>
<template>
<div style="width: 100%; height: 100%">
<div class="course-viewer-content" v-if="isLiveEnd && !hasRecord">
<div class="empty">直播已结束</div>
</div>
<iframe
:src="iframeUrl"
frameborder="0"
width="100%"
height="100%"
allow="autoplay;geolocation;microphone;camera;midi;encrypted-media;"
v-else
></iframe>
</div>
</template>
<script>
// 章节视频
export default {
name: 'ChapterLive',
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
user() {
return this.$store.state.user
},
nickName() {
return this.user.personal_name || '匿名'
},
live() {
const live = this.chapter.live || {}
live.live_status = parseInt(live.live_status)
return live
},
// 是否直播结束
isLiveEnd() {
return [3, 5].includes(this.live.live_status)
},
// 是否有回放
hasRecord() {
// enable_record 0:不启用回放 1:开启回放
return this.live.enable_record === 1 && this.live.record_url
},
iframeUrl() {
if (this.live.type === 5) {
return this.ccUrl
}
if (this.live.type === 8) {
return this.zoomUrl
}
},
// cc直播
ccUrl() {
const live = this.live
if (this.isLiveEnd && this.hasRecord) {
// 查看回放
return live.record_url.replace(/^http:|^https:/, '')
} else {
// 直播
live.user_name = live.user_name || this.nickName
return `https://view.csslcloud.net/api/view/index?roomid=${live.room_id}&userid=${live.account_id}&autoLogin=true&viewername=${live.user_name}&viewertoken=${live.play_pass}`
}
},
// zoom直播
zoomUrl() {
return this.live.record_url || this.live.join_url
}
}
}
</script>
<style scoped>
.empty {
padding: 100px;
font-size: 30px;
text-align: center;
}
</style>
<template>
<div class="player" v-if="chatperResources">
<div class="player-main">
<div class="player-column" v-show="videoVisible">
<!-- 视频 -->
<video-player
:isSkip="isSkip"
:skipTime="skipTime"
:video="chatperResources.video"
@timeupdate="onTimeupdate"
@ready="onReady"
ref="videoPlayer"
></video-player>
</div>
<div class="player-column" v-if="pptVisible">
<!-- ppt -->
<ppt-player
:index="pptIndex"
:ppts="chatperResources.ppts"
@close="onPPTClose"
@fullscreen="onPPTFullscreen"
@videoSyncTime="onVideoSyncTime"
></ppt-player>
</div>
</div>
<div class="player-footer">
<em class="player-button player-button-download" v-if="chapter.pdf">
<a :href="chapter.pdf" download target="_blank">下载PPT</a>
</em>
<em :class="pptClass" @click="togglePPTVisible" v-if="chatperResources.ppts.length">同步显示PPT</em>
<em :class="skipClass" @click="toggleSkip">始终跳过片头</em>
</div>
</div>
</template>
<script>
import { throttle } from 'lodash'
// api
import * as api from '../../api'
// components
import videoPlayer from './videoPlayer.vue'
import pptPlayer from './pptPlayer.vue'
export default {
name: 'ChapterPlayer',
components: { videoPlayer, pptPlayer },
props: {
// 当前章节
chapter: { type: Object },
// 是否是PPT播放跳转
isSeek: { type: Boolean, default: false },
// PPT当前选中的索引
pptIndex: { type: Number, default: 0 }
},
data() {
// 是否跳过片头
const isSkip = window.localStorage.getItem('isSkip') === 'true'
return {
deviceId: 'jjhz92fn0.le2a6c06c9g0.thhg7ekb1f8',
videoVisible: true,
pptVisible: false,
isSkip,
skipTime: 6,
chatperResources: null,
throttled: null,
throttleWait: 5, // 秒
progress: {
cpt: 0, // 当前播放时间
mpt: 0, // 当前播放最大时间
progress: 0, // 进度
pt: 0 // 累计观看时间
},
player: null,
watchedTime: 0,
watchedTimePoint: [] // 视频观看的时间点
}
},
watch: {
pptIndex(index) {
this.isSeek && this.updateVideoCurrentTime(index)
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 视频资源ID
resourceId() {
return this.chapter.resource_id
},
/**
* 视频提供者
* @return 1是CC加密; 2是非加密; 3是阿里云
*/
videoProvider() {
const video = this.chapter.video || {}
return video.video_provider || 3
},
pptClass() {
return {
'player-button': true,
'player-button-ppt': !this.pptVisible,
'player-button-ppt__active': this.pptVisible
}
},
skipClass() {
return {
'player-button': true,
'player-button-skip': !this.isSkip,
'player-button-skip__active': this.isSkip
}
}
},
methods: {
// 同步显示PPT
togglePPTVisible() {
this.videoVisible = true
this.pptVisible = !this.pptVisible
},
// 始终跳过片头
toggleSkip() {
this.isSkip = !this.isSkip
window.localStorage.setItem('isSkip', this.isSkip)
},
// 关闭PPT
onPPTClose() {
this.pptVisible = false
this.videoVisible = true
},
// PPT全屏
onPPTFullscreen(value) {
this.videoVisible = !value
},
// 设置视频时间为当前PPT时间
onVideoSyncTime(time) {
this.player.seek(time)
},
// 播放器ready
onReady(player) {
this.player = player
// 跳转播放进度
if (this.progress.cpt) {
this.player.seek(this.progress.cpt)
}
},
// 当前播放时间更新
onTimeupdate(time) {
time = Math.floor(time)
const ppts = this.chatperResources.ppts || []
let index = this.chatperResources.ppts.findIndex(item => item.ppt_point > time)
index = index !== -1 ? index - 1 : ppts.length - 1
this.$emit('change-ppt', index)
const durations = this.player.getDuration()
// 更新当前播放时间
this.progress.cpt = time
// 观看的最大点
this.progress.mpt = Math.max(time, this.progress.mpt)
const hasTimePoint = this.watchedTimePoint.includes(this.progress.cpt)
if (!hasTimePoint) {
this.watchedTimePoint.push(this.progress.cpt)
}
// 更新视频观看总时长
this.updateWatchTime(time)
// 更新视频进度,10秒更新一次
if (this.throttled) {
this.throttled(time, durations)
} else {
this.throttled = throttle(this.updateChapterVideoProgress, this.throttleWait * 1000, { leading: false })
}
},
// 更新视频当前播放时间
updateVideoCurrentTime() {
const ppt = this.chatperResources.ppts[this.pptIndex]
ppt && this.player.seek(ppt.ppt_point) // 增加2秒
},
// 获取章节视频详情
getChapterVideo() {
// 视频播放类型 1是CC加密; 2是非加密; 3是阿里云
if (this.videoProvider === 3) {
api.getChapterVideoAliyun(this.resourceId).then(response => {
this.chatperResources = response
Array.isArray(response.ppts) && this.$emit('pptupdate', response.ppts)
})
} else {
api.getChapterVideo(this.resourceId).then(response => {
let { video, audio, ppts } = response
video = video.reduce(
(result, item) => {
if (item.quality === '10') {
result.LD = item.playurl
}
if (item.quality === '20') {
result.SD = item.playurl
}
return result
},
{ LD: '', SD: '' }
)
this.chatperResources = { video, audio, ppts }
Array.isArray(ppts) && this.$emit('pptupdate', ppts)
})
}
},
// 获取章节视频进度
getChapterVideoProgress() {
api
.getChapterVideoProgress(this.sid, this.resourceId, {
device_id: this.deviceId
})
.then(response => {
this.progress = response
// 跳转播放进度
if (this.player && response.cpt) {
this.player.seek(response.cpt)
}
})
},
// 更新章节视频进度
updateChapterVideoProgress(time, durations) {
// 登录用户信息
const user = this.$store.state.user
const params = {
sid: user.student_info.id,
uid: user.id,
d: this.deviceId,
i: this.deviceId,
c: this.cid, // 课程ID
s: this.sid, // 学期ID
v: this.resourceId, // 视频资源ID
_p: this.progress.pt, // 累计时间
_m: this.progress.mpt, // 当前播放最大时间
_c: this.progress.cpt, // 当前播放位置
ps: this.watchedTimePoint.join(',') // 播放时,统计帧
}
api.updateChapterVideoProgress(params)
// 清空已经上传过的观看时间点
this.watchedTimePoint = []
},
// 更新观看总时长
updateWatchTime(time) {
if (time === this.watchedTime) {
return
}
this.watchedTime = time
// 增加跳过片头时间
if (this.isSkip && !this.progress.pt) {
this.progress.pt = this.skipTime + 20
}
// 默认增加时间
this.progress.pt = this.progress.pt || 20
this.progress.pt++
}
},
beforeMount() {
// 获取视频
this.getChapterVideo()
// 获取视频进度
this.getChapterVideoProgress()
}
}
</script>
<style lang="scss" scoped>
.player {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: #3f3f3f;
}
.player-main {
display: flex;
flex: 1;
overflow: hidden;
}
.player-column {
flex: 1;
height: 100%;
}
.player-footer {
display: flex;
align-items: center;
height: 54px;
padding: 0 20px;
font-size: 14px;
color: #a0a0a0;
a {
color: #a0a0a0;
text-decoration: none;
}
em {
margin-right: 40px;
cursor: pointer;
}
}
.player-button {
display: inline-block;
color: #a0a0a0;
padding-left: 25px;
font-size: 14px;
line-height: 18px;
margin: 0 20px;
background: url(../../assets/play-icons.png) no-repeat 0 0;
cursor: pointer;
}
.player-button-download {
background-position: 0 -240px;
}
.player-button-ppt {
background-position: 0 -240px;
}
.player-button-ppt__active {
background-position: 0 -280px;
color: #b19241;
}
.player-button-skip {
background-position: 0 -160px;
}
.player-button-skip__active {
background-position: 0 -200px;
color: #b19241;
}
</style>
<template>
<div class="ppt-player">
<template v-if="ppts.length">
<div class="ppt-player-preview">
<img :src="pptUrl" v-if="pptUrl" />
</div>
<div class="ppt-player-controls">
<div class="ppt-player-controls__page">
<template v-if="currentIndex >= 0">
<i class="el-icon-arrow-left" @click="prev"></i>
</template>
<template v-if="currentIndex + 1 < ppts.length">
<i class="el-icon-arrow-right" @click="next"></i>
</template>
</div>
<div class="ppt-player-controls__pages">
<span class="is-active">{{currentIndex + 1}}</span>
/
<span>{{ppts.length}}</span>
</div>
<div class="ppt-player-controls__tools">
<el-tooltip content="PPT同步视频播放">
<i :class="['el-icon-self-xuexiao', (isSync ? 'active' : '')]" @click="onToggleSync"></i>
</el-tooltip>
<el-tooltip content="放大PPT">
<i class="el-icon-self-quanping" @click="fullscreen"></i>
</el-tooltip>
<el-tooltip content="切换视频到当前PPT页">
<i class="el-icon-self-shipin" @click="setVideoTime"></i>
</el-tooltip>
<el-tooltip content="关闭PPT">
<i class="el-icon-self-guanbi" @click="$emit('close')"></i>
</el-tooltip>
</div>
</div>
</template>
</div>
</template>
<script>
export default {
name: 'ppt-player',
props: {
ppts: { type: Array },
index: { type: Number, default: 0 }
},
data() {
return {
currentIndex: this.index,
isSync: true,
isFullscreen: false
}
},
watch: {
index: {
handler(value) {
if (this.isSync) {
this.currentIndex = value
}
}
}
},
computed: {
pptUrl() {
return this.ppts[this.currentIndex]
? this.ppts[this.currentIndex].ppt_url
: ''
}
},
methods: {
gotoIndex(index) {
this.currentIndex = index
},
getIndex(index) {
return Math.min(this.ppts.length - 1, Math.max(0, index))
},
prev() {
this.currentIndex = this.getIndex(this.currentIndex - 1)
this.isSync = false
},
next(e) {
this.currentIndex = this.getIndex(this.currentIndex + 1)
this.isSync = false
},
onToggleSync(e) {
this.isSync = !this.isSync
},
setVideoTime(e) {
this.isSync = true
this.$emit('videoSyncTime', this.ppts[this.currentIndex].ppt_point)
},
// 全屏
fullscreen() {
this.isFullscreen = !this.isFullscreen
this.$emit('fullscreen', this.isFullscreen)
}
}
}
</script>
<style lang="scss" scoped>
.ppt-player {
position: relative;
width: 100%;
height: 100%;
background-color: #000;
}
.ppt-player-preview {
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.ppt-player-controls {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 44px;
line-height: 44px;
padding: 0 14px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
}
.ppt-player-controls__page {
width: 90px;
color: #fff;
i {
padding: 0 10px;
font-size: 18px;
cursor: pointer;
}
}
.ppt-player-controls__pages {
flex: 1;
color: #fff;
text-align: center;
}
.ppt-player-controls__pages .is-active {
color: #d29f29;
}
.ppt-player-controls__tools {
float: right;
}
.ppt-player-controls__tools i {
color: #fff;
margin: 0 10px;
cursor: pointer;
}
.ppt-player-controls__tools i.active,
.ppt-player-controls__tools i:hover {
color: #d29f29;
}
.ppt-player-controls__tools .icon-rotate {
font-size: 1.125em;
}
</style>
<template>
<div class="video-player" id="player"></div>
</template>
<script>
export default {
name: 'VideoPlayer',
props: {
isSkip: Boolean,
video: Object,
autoplay: { type: Boolean, default: false }
},
data() {
return { player: null }
},
methods: {
createPlayer() {
const _this = this
const { FD, LD, SD } = this.video
/*
"OD" : "原画"
"FD" : "流畅"
"LD" : "标清"
"SD" : "高清"
"HD" : "超清"
"2K" : "2K"
"4K" : "4K"
*/
this.player = new Aliplayer(
{
id: 'player',
source: JSON.stringify({ SD, LD, FD }),
width: '100%',
height: '100%',
autoplay: this.autoplay,
isLive: false,
controlBarVisibility: 'always',
useHlsPluginForSafari: true
},
function (player) {
player.on('ready', function () {
// 跳过片头
_this.isSkip && player.seek(6)
_this.$emit('ready', player)
})
player.on('timeupdate', function (event) {
_this.$emit('timeupdate', player.getCurrentTime())
})
player.on('error', function (event) {
console.log(event)
})
}
)
}
},
mounted() {
this.createPlayer()
},
beforeDestroy() {
this.player && this.player.dispose()
}
}
</script>
<style lang="scss" scoped>
.video-player {
width: 100%;
height: 100%;
}
</style>
<template>
<container :title="chapter.name">
<file-list :files="files"></file-list>
</container>
</template>
<script>
// components
import Container from '../common/container.vue'
import FileList from './fileList.vue'
// 章节阅读资料
export default {
name: 'ChapterRead',
components: { Container, FileList },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
files() {
const reading = this.chapter.reading
const file = {
file_name: reading.reading_content,
file_url: reading.reading_attachment
}
return [file]
}
}
}
</script>
<template>
<container :title="chapter.name">
<file-list :files="files"></file-list>
</container>
</template>
<script>
// components
import Container from '../common/container.vue'
import FileList from './fileList.vue'
// 课程阅读资料
export default {
name: 'CourseRead',
components: { Container, FileList },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
files() {
return this.data.files || []
}
}
}
</script>
<template>
<div>
<ul class="file-list" v-if="files.length">
<li class="file-list-item" v-for="file in files" :key="file.id">
<a :href="file.file_url" target="_blank">
<i class="el-icon-document"></i>
<div class="file-list-item__inner" v-html="file.file_name"></div>
</a>
<!-- <span v-if="file.file_size">{{ file.file_size }}</span> -->
<a :href="file.file_url" :download="file.file_name" target="_blank">
<el-tooltip effect="dark" content="下载">
<i class="el-icon-download"></i>
</el-tooltip>
</a>
</li>
</ul>
<div class="empty" v-else>
<slot name="empty">暂无课程资料</slot>
</div>
</div>
</template>
<script>
export default {
name: 'FilePanel',
props: {
// 标题
title: { type: String, default: '课程资料' },
// 文件列表
files: { type: Array, default: () => [] }
}
}
</script>
<style lang="scss" scoped>
.file-list {
padding: 0;
}
.file-list-item {
display: flex;
font-size: 16px;
padding: 20px 30px;
margin-bottom: 10px;
background-color: #fff;
list-style: none;
border-radius: 32px;
justify-content: space-between;
a {
display: flex;
align-items: center;
text-decoration: none;
color: #333;
&:hover {
color: #c01540;
}
::v-deep * {
margin: 0;
padding: 0;
}
}
}
.empty {
font-size: 18px;
line-height: 80px;
background-color: #fff;
text-align: center;
border-radius: 40px;
}
.file-list-item__inner {
margin: 0 10px !important;
}
</style>
<template>
<container :title="detail.paper_title" v-loading="loading">
<template v-slot:header-aside v-if="isExamComplete">分数:{{exam.score.total}}</template>
<div class="exam">
<template v-if="isSubmited && !isExamComplete">
<div class="no-exam">试卷批改中,请耐心等待</div>
</template>
<template v-else>
<!-- 考试期间,未开始考试 -->
<div class="exam-welcome" v-if="!isStartExam">
<div v-if="detail.paper_deadline">考试截止时间:{{detail.paper_deadline}}</div>
<el-button
type="primary"
:disabled="!isExamTime"
@click="onStartExam"
>{{startExamButtonText}}</el-button>
</div>
<!-- 考试试题 -->
<div class="exam-form" v-if="isStartExam">
<el-form :disabled="isSubmited">
<template v-for="items in questions">
<exam-item
v-for="(item, index) in items"
:index="index"
:type="item.type"
:data="item"
:value="item.formModel"
:disabled="isSubmited"
:key="item.id"
></exam-item>
</template>
<div class="exam-buttons">
<el-tooltip effect="dark" content="提交之后就不能修改了哦" placement="right">
<el-button type="primary" :loading="submitLoading" @click="onSubmit">{{submitText}}</el-button>
</el-tooltip>
</div>
</el-form>
</div>
</template>
</div>
</container>
</template>
<script>
import Base64 from 'Base64'
// components
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节测试题
export default {
name: 'ChapterExam',
components: { Container, ExamItem },
props: {
// 当前选中的章节
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
detail: {},
questions: [],
messageInstance: null,
exam: {},
isStartExam: false, // 是否开始考试
autoSubmitTimer: null, // 自动提交定时器
submitLoading: false
}
},
watch: {
chapter: {
immediate: true,
handler(data) {
this.detail = data.paper
this.questions = data.paper
? this.genQuestions(data.paper.examination)
: []
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 是否是考试时间
isExamTime() {
if (!this.detail.paper_deadline) {
return true
}
// 大于开始时间,小于结束时间
const endTime = +new Date(this.exam.paper_deadline)
const currentTime = new Date().getTime()
return currentTime < endTime
},
// 考试按钮
startExamButtonText() {
return this.isExamTime ? '开始考试' : '考试结束'
},
// 考试完成
isExamComplete() {
// 考试完成,批改完成并且公布成绩
return this.exam.is_published === 1 && this.exam.type === 2
},
// 是否提交
isSubmited() {
return this.exam.type === 1 || this.exam.type === 2
},
// 提交按钮文本
submitText() {
return this.isSubmited ? '已提交' : '提交'
}
},
methods: {
// 开始考试
onStartExam() {
this.isStartExam = true
// 自动提交答题
this.autoSubmit()
},
// 组装问题数据
genQuestions(list) {
if (!list) {
return []
}
return list.map(data => {
let { radioList, checkboxList, shortAnswerList } = data
// 单选
radioList = radioList.map(item => {
const temp = {
type: 1,
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
return Object.assign({}, item, temp)
})
// 多选
checkboxList = checkboxList.map(item => {
const temp = {
type: 2,
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
return Object.assign({}, item, temp)
})
// 问答
shortAnswerList = shortAnswerList.map(item => {
const temp = {
type: 3,
formModel: {
id: item.id,
user_answer: item.user_answer
? Base64.decode(item.user_answer.replace(/ /gi, '+'))
: '',
attachments: item.attachments || []
}
}
return Object.assign({}, item, temp)
})
return [...radioList, ...checkboxList, ...shortAnswerList]
})
},
// 获取考试结果
getExamResult() {
api
.getCourseExamResult(this.sid, this.cid, this.pid, { paper_type: 0 })
.then(response => {
// 设置问题列表数据
if (response.code !== 8001) {
this.isStartExam = true
this.exam = response
this.questions = this.genQuestions(response.sheet)
// 自动提交
if (this.isStartExam && !this.isSubmited && !this.isExamComplete) {
this.autoSubmit()
}
}
})
},
// 提交校验
checkSubmit() {
for (let i = 0; i < this.questions.length; i++) {
const questions = this.questions[i]
for (let k = 0; k < questions.length; k++) {
const value = questions[k].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
return false
}
}
}
return true
},
// 提交
onSubmit() {
// 校验
if (!this.checkSubmit()) {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error('还有题目未做,不能提交')
return
}
// 提交的答案数据
const answers = this.handleSubmitData()
// 提交参数
const params = { answers: JSON.stringify(answers), type: 1 }
// 请求接口
this.submitLoading = true
this.handleSubmitRequest(params)
},
// 自动提交
autoSubmit() {
// 10秒提交一次
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.autoSubmitTimer = setInterval(() => {
// 提交的答案数据
const answers = this.handleSubmitData()
const params = { answers: JSON.stringify(answers), type: 0 }
// 请求接口
this.handleSubmitRequest(params)
}, 3000)
},
// 处理请求接口答案数据
handleSubmitData() {
return this.questions.map(questions => {
return questions.reduce(
(result, item) => {
// 单选题
if (item.type === 1) {
result.radioList.push(item.formModel)
}
// 多选题
if (item.type === 2) {
result.checkboxList.push(item.formModel)
}
// 简答题
if (item.type === 3) {
const formModel = Object.assign({}, item.formModel, {
user_answer: Base64.encode(item.formModel.user_answer)
})
result.shortAnswerList.push(formModel)
}
return result
},
{ radioList: [], checkboxList: [], shortAnswerList: [] }
)
})
},
// 请求提交接口
handleSubmitRequest(params) {
params.paper_type = 0
api
.submitCourseExam(this.sid, this.cid, this.pid, params)
.then(response => {
if (params.type === 0) {
console.log('暂存成功')
return
}
if (response.code === 200) {
this.$message.success('考试答卷提交成功')
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.getExamResult()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
}
},
beforeMount() {
// 获取考试结果
this.getExamResult()
},
destroyed() {
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
width: 240px;
margin: 40px auto;
}
}
.no-exam {
padding: 100px;
font-size: 30px;
text-align: center;
}
.exam-welcome {
padding: 40px;
line-height: 30px;
text-align: center;
::v-deep .el-button {
margin-top: 30px;
}
}
</style>
<template>
<container :title="chapter.name" v-loading="loading">
<template v-slot:header-aside v-if="isSubmited">正确率:{{ detail.score }}%</template>
<div class="exam">
<div class="exam-form">
<el-form :disabled="isSubmited">
<exam-item
v-for="(item, index) in questions"
:index="index"
:type="item.question_type"
:data="item"
:value="item.formModel"
:disabled="isSubmited"
:key="item.id"
></exam-item>
<div class="exam-buttons">
<el-tooltip effect="dark" content="提交之后就不能修改了哦" placement="right">
<el-button type="primary" :loading="submitLoading" @click="onSubmit">{{ submitText }}</el-button>
</el-tooltip>
</div>
</el-form>
</div>
</div>
</container>
</template>
<script>
// libs
import { shuffle } from 'lodash'
// components
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节测试题
export default {
name: 'ChapterTest',
components: { Container, ExamItem },
props: {
// 当前选中的章节
chapter: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
detail: null,
questions: [], // 问题列表
startTime: new Date().getTime(), // 进入时间
messageInstance: null,
submitLoading: false
}
},
watch: {
chapter: {
immediate: true,
handler(data) {
this.questions = data.homework ? this.genQuenstions(data.homework.questions) : []
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 资源ID
resourceId() {
return this.chapter.resource_id
},
// 打乱顺序的问题列表
unorderedQuestions() {
const ids = this.questions.map(item => item.id)
const sortIds = shuffle(ids)
return sortIds.map(id => this.questions.find(item => item.id === id))
},
// 是否提交
isSubmited() {
return this.detail ? !!this.detail.work_contents : false
},
// 提交按钮文本
submitText() {
return this.isSubmited ? '已提交' : '提交'
}
},
methods: {
// 获取测试答题详情
getDetail() {
this.loading = true
api
.getChapterHomework(this.sid, this.cid, this.resourceId)
.then(response => {
this.detail = Array.isArray(response) ? null : response
if (this.detail) {
const parseAnswers = JSON.parse(this.detail.work_contents)
// 设置答案
this.questions = this.questions.map(item => {
const found = parseAnswers.find(answer => answer.question_id === item.id)
if (found) {
const selectedIds = found.options.reduce((result, item) => {
item.selected && result.push(item.id)
return result
}, [])
item.user_answer = item.question_type === 2 ? selectedIds : selectedIds[0]
}
return item
})
this.questions = this.genQuenstions(this.questions)
}
})
.finally(() => {
this.loading = false
})
},
// 组装问题数据
genQuenstions(list) {
if (!list) {
return []
}
return list.map(item => {
let temp = null
if (item.question_type === 1) {
// 单选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
} else if (item.question_type === 2) {
// 多选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
} else if (item.question_type === 3) {
// 简答
temp = {
formModel: {
id: item.id,
user_answer: item.user_answer ? Base64.decode(item.user_answer) : '',
attachments: item.attachments || ''
}
}
}
return Object.assign(
{},
item,
{
content: item.question_content,
options: item.question_options ? JSON.parse(item.question_options) : []
},
temp
)
})
},
// 提交校验
checkSubmit() {
const quenstions = this.questions
for (let i = 0; i < quenstions.length; i++) {
const value = quenstions[i].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
return false
}
}
return true
},
// 提交
onSubmit() {
// 校验
if (!this.checkSubmit()) {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error('还有题目未做,不能提交')
return
}
// 计算答题时间
const duration = Math.floor((new Date().getTime() - this.startTime) / 1000)
// 答案数据
const data = this.handleSubmitData()
// 计算分数
const score = data.reduce((result, item) => {
item.is_correct && result++
return result
}, 0)
const total = this.questions.length
const params = {
semester_id: this.sid,
course_id: this.cid,
chapter_id: this.pid,
work_id: this.resourceId,
work_contents: JSON.stringify(data),
duration,
score: ((score / total) * 100).toFixed(1)
}
// 请求接口
this.handleSubmitRequest(params)
},
// 提交的答案数据
handleSubmitData() {
const result = this.questions.map(item => {
// 设置提交选中状态
let isCorrect = true
const options = item.options.map(option => {
// 选择的项
const answers = item.formModel.user_answer
// 是否选中该项
const selected = Array.isArray(answers) ? answers.includes(option.id) : option.id === answers
// 是否选择正确
if (option.checked !== selected && isCorrect) {
isCorrect = false
}
return {
id: option.id,
checked: option.checked,
option: option.option,
selected
}
})
return {
question_id: item.id,
is_correct: isCorrect ? 1 : 0,
options
}
})
return result
},
// 请求提交接口
handleSubmitRequest(params) {
this.submitLoading = true
api
.sbumitChapterHomework(params)
.then(response => {
if (response.status) {
this.getDetail()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
}
},
beforeMount() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
width: 240px;
margin: 40px auto;
}
}
</style>
<template>
<container :title="chapter.name" v-loading="loading">
<div class="exam-form">
<el-form :disabled="disabled || !isWorkTime">
<exam-item
v-for="(item, index) in questions"
:index="index"
:type="item.question_type"
:data="item"
:value="item.formModel"
:disabled="disabled || !isWorkTime"
:key="item.id"
></exam-item>
</el-form>
</div>
<p style="color:red;" v-if="deadline">请于截止日期 {{ deadline }} 前提交</p>
<!-- 驳回状态 -->
<template v-if="detail && detail.status === 1">
<div class="work-bottom">
<div class="info">
<div class="paper-check">
<h4>作业被驳回,点击“重新编辑”按钮重新编辑内容再次提交</h4>
<div class="paper-check-item">
<b>驳回时间:</b>
{{ detail.checker_time }}
</div>
<div class="paper-check-item">
<b>驳回说明:</b>
<div class="edit_html" v-html="detail.check_comments"></div>
</div>
</div>
</div>
</div>
<div class="buttons">
<el-button type="primary" @click="onReEdit" :disabled="!isWorkTime">重新编辑</el-button>
</div>
</template>
<!-- 正常状态 -->
<template v-else>
<div class="work-bottom" v-if="detail">
<div class="info">
<template v-if="isRevised">
<div class="paper-check">
<p>批改时间:{{ detail.checker_time }}</p>
<div class="paper-check-item">
<b>评分:</b>
{{ detail.score }}
</div>
<div class="paper-check-item">
<b>评语:</b>
<div class="edit_html" v-html="detail.check_comments"></div>
</div>
</div>
</template>
<template v-else-if="detail.created_time">
<p class="help">已于 {{ detail.created_time }} 提交,等待老师批改中。</p>
<template
v-if="
detail.updated_time &&
detail.updated_time !== detail.created_time
"
>
<p class="help">最近提交时间: {{ detail.updated_time }}</p>
</template>
</template>
</div>
</div>
<div class="buttons">
<el-tooltip content="在获老师批改之前,可以多次提交,将以最后一次提交为准" placement="right">
<el-button
type="primary"
:disabled="disabled || !isWorkTime"
:loading="submitLoading"
@click="onSubmit"
>{{ submitText }}</el-button>
</el-tooltip>
</div>
</template>
</container>
</template>
<script>
import Base64 from 'Base64'
// componets
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节作业
export default {
name: 'ChapterWork',
components: { Container, ExamItem },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
detail: null,
questions: [], // 问题列表
startTime: new Date().getTime(), // 进入时间
messageInstance: null,
deadline: '', // 截止时间
disabled: false,
submitLoading: false
}
},
watch: {
chapter: {
immediate: true,
handler(data) {
this.questions = data.homework
? this.genQuenstions(data.homework.questions)
: []
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 资源ID
resourceId() {
return this.chapter.resource_id
},
// 是否批改
isRevised() {
return this.detail ? this.detail.status === 0 : false
},
// 提交按钮文本
submitText() {
return this.isRevised ? '已批改' : '提交'
},
// 是否是提交作业时间
isWorkTime() {
if (!this.deadline) {
return true
}
// 大于开始时间,小于结束时间
const endTime = +new Date(this.deadline)
const currentTime = new Date().getTime()
return currentTime < endTime
}
},
methods: {
// 获取作业截止时间
getDeadline() {
api
.getChapterHomeworkDeadline(this.sid, this.cid, this.pid)
.then(response => {
this.deadline = response.dead_line
})
},
// 获取详情
getDetail() {
this.loading = true
api
.getChapterHomework(this.sid, this.cid, this.resourceId)
.then(response => {
this.detail = Array.isArray(response) ? null : response
if (this.detail) {
// -1未处理 0已处理 1驳回
this.disabled = [0, 1].includes(this.detail.status)
const parseAnswers = JSON.parse(this.detail.work_contents)
// 设置答案
this.questions = this.questions.map(item => {
const found = parseAnswers.find(
answer => answer.question_id === item.id
)
if (found) {
item.user_answer = found.descreption
item.attachments = found.file_url
}
return item
})
this.questions = this.genQuenstions(this.questions)
}
})
.finally(() => {
this.loading = false
})
},
// 组装问题数据
genQuenstions(list) {
if (!list) {
return []
}
return list.map(item => {
let temp = null
if (item.question_type === 1) {
// 单选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
} else if (item.question_type === 2) {
// 多选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
} else if (item.question_type === 3) {
// 简答
temp = {
formModel: {
id: item.id,
user_answer: item.user_answer
? Base64.decode(item.user_answer)
: '',
attachments: item.attachments || ''
}
}
}
return Object.assign(
{},
item,
{
content: item.question_content,
options: item.question_options
? JSON.parse(item.question_options)
: []
},
temp
)
})
},
// 提交校验
checkSubmit() {
const quenstions = this.questions
for (let i = 0; i < quenstions.length; i++) {
const value = quenstions[i].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
return false
}
}
return true
},
// 提交
onSubmit() {
// 校验
if (!this.checkSubmit()) {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error(
'答题内容不能为空,请检查并输入内容'
)
return
}
// 计算答题时间
const duration = Math.floor(
(new Date().getTime() - this.startTime) / 1000
)
// 提交的答案数据
const answers = this.questions.map(item => {
return {
question_id: item.id,
descreption:
item.question_type === 3
? Base64.encode(item.formModel.user_answer)
: item.formModel.user_answer,
file_url: item.formModel.attachments,
is_encoded: 1
}
})
// 提交参数
const params = {
semester_id: this.sid,
course_id: this.cid,
chapter_id: this.pid,
work_id: this.resourceId,
work_contents: JSON.stringify(answers),
duration
}
// 请求接口
this.handleSubmitRequest(params)
},
// 请求提交接口
handleSubmitRequest(params) {
this.submitLoading = true
api
.sbumitChapterHomework(params)
.then(response => {
if (response.status) {
this.$message.success('提交成功,等待批改')
this.getDetail()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
},
// 重新编辑
onReEdit() {
this.disabled = false
this.detail.status = -1
}
},
beforeMount() {
this.getDetail()
this.getDeadline()
}
}
</script>
<style lang="scss" scoped>
.work-bottom {
margin-top: 20px;
.info {
color: #999;
line-height: 28px;
}
}
.buttons {
padding: 20px 0;
::v-deep .el-button {
width: 120px;
}
}
.paper-check {
padding: 10px;
color: #000;
border: 1px solid #dedede;
h4 {
margin: 0 0 10px;
}
}
.paper-check-item {
display: flex;
b {
white-space: nowrap;
}
}
</style>
<template>
<container :title="exam.title" v-loading="!loaded">
<template v-slot:header-aside>
<template v-if="isCompleted">分数:{{ exam.score.total }}</template>
<template v-else>考试时间:{{ status.start_time }} ~ {{ status.terminate_time }}</template>
</template>
<div class="exam">
<template v-if="status.examination_status === '00'">
<div class="no-exam">暂无考试</div>
</template>
<template v-else-if="isSubmited && !isCompleted && !isMultipleExams">
<div class="no-exam">试卷批改中,请耐心等待</div>
</template>
<template v-else>
<!-- 考试试题 -->
<div class="exam-form" v-if="loaded">
<el-form :disabled="!canEditable">
<template v-for="items in questions">
<exam-item
v-for="(item, index) in items"
:index="index"
:type="item.type"
:data="item"
:value="item.formModel"
:disabled="!canEditable"
:showResult="isCompleted"
:key="item.id"
></exam-item>
</template>
</el-form>
<div class="exam-buttons">
<!-- 允许多次提交 -->
<template v-if="isMultipleExams">
<el-button type="primary" @click="handlePrev" v-if="hasPrev">上一套试卷</el-button>
<el-button type="primary" @click="handleNext" v-if="hasNext">下一套试卷</el-button>
<el-button type="primary" @click="handleNewExam" v-if="hasResubmit">再考一次</el-button>
</template>
<template v-if="!(isSubmited && isMultipleExams)">
<el-tooltip effect="dark" content="提交之后就不能修改了哦" placement="right">
<el-button type="primary" :disabled="!canEditable" :loading="submitLoading" @click="onSubmit">{{
submitText
}}</el-button>
</el-tooltip>
</template>
</div>
</div>
</template>
</div>
</container>
</template>
<script>
import Base64 from 'Base64'
// components
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节测试题
export default {
name: 'CourseExam',
components: { Container, ExamItem },
props: {
// 当前选中的章节
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loaded: false,
detail: {},
status: {},
questions: [],
messageInstance: null,
exam: {},
autoSubmitTimer: null, // 自动提交定时器
checkStatusTimer: null, // 考试状态定时器
submitLoading: false,
isMultipleExams: false, // 是否可以多次考试
maxExams: 3, // 最多考试几次
examCount: this.data.exist_examination.length || 0 // 试卷数量
}
},
watch: {
offset: {
immediate: true,
handler() {
this.init()
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.data.course_examination
},
// 是否是考试时间
isExamTime() {
// 大于开始时间,小于结束时间
return this.status.examination_status === '20'
},
// 是否提交
isSubmited() {
return this.exam.type === 1 || this.exam.type === 2
},
// 考试完成
isCompleted() {
// 考试完成,批改完成并且公布成绩
return this.exam.is_published === 1 && this.exam.type === 2
},
// 可以编辑
canEditable() {
return !this.isSubmited && this.isExamTime
},
// 提交按钮文本
submitText() {
return this.isSubmited ? '已提交' : '提交'
},
// 试卷页码
offset() {
const { query } = this.$route
return parseInt(query.offset) || 0
},
// 是否显示上一套试题
hasPrev() {
return !!this.offset
},
// 是否显示下一套试题
hasNext() {
return this.offset < this.examCount - 1
},
// 是否显示再考一次
hasResubmit() {
if (this.examList.length >= this.maxExams) {
return false
}
// 判断状态是否还有未提交的试题
for (const exam of this.examList) {
if (!['1', '2'].includes(exam.status)) {
return false
}
}
return true
// return this.isSubmited && this.isExamTime && this.examCount < this.maxExams
},
// 已存在的试题列表
examList() {
return this.data.exist_examination
}
},
methods: {
// 初始化
async init() {
this.clearTimer()
// 自动获取考试状态
await this.autoCheckExamStatus()
// 获取试题
this.getExam()
},
// 获取考试状态
async getExamStatus() {
await api.getCourseExamStatus(this.sid, this.cid, this.pid).then(response => {
this.status = response
if (this.isSubmited || response.examination_status === '90') {
this.checkStatusTimer && clearInterval(this.checkStatusTimer)
}
})
},
// 自动获取考试状态
async autoCheckExamStatus() {
// 获取试题状态
await this.getExamStatus()
this.checkStatusTimer && clearInterval(this.checkStatusTimer)
this.checkStatusTimer = setInterval(this.getExamStatus, 5000)
},
// 获取试题
getExam() {
this.loaded = false
api
.getCourseExamResult(this.sid, this.cid, this.pid, { offset: this.offset })
.then(response => {
this.exam = response
this.questions = this.genQuestions(response.sheet)
// 自动提交
this.canEditable && this.autoSubmit()
// 更新菜单
this.isMultipleExams && this.$emit('update')
})
.finally(() => {
this.loaded = true
})
},
// 组装问题数据
genQuestions(list) {
if (!list) {
return []
}
return list.map(data => {
let { radioList, checkboxList, shortAnswerList } = data
// 单选
radioList = radioList.map(item => {
const temp = {
type: 1,
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
return Object.assign({}, item, temp)
})
// 多选
checkboxList = checkboxList.map(item => {
const temp = {
type: 2,
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
return Object.assign({}, item, temp)
})
// 问答
shortAnswerList = shortAnswerList.map(item => {
const temp = {
type: 3,
formModel: {
id: item.id,
user_answer: item.user_answer ? Base64.decode(item.user_answer.replace(/ /gi, '+')) : '',
attachments: item.attachments || []
}
}
return Object.assign({}, item, temp)
})
return [...radioList, ...checkboxList, ...shortAnswerList]
})
},
// 提交校验
checkSubmit() {
for (let i = 0; i < this.questions.length; i++) {
const questions = this.questions[i]
for (let k = 0; k < questions.length; k++) {
const value = questions[k].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
return false
}
}
}
return true
},
// 提交
onSubmit() {
// 校验
if (!this.checkSubmit()) {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error('还有题目未做,不能提交')
return
}
// 提交的答案数据
const answers = this.handleSubmitData()
// 提交参数
const params = { answers: JSON.stringify(answers), type: 1 }
// 请求接口
this.submitLoading = true
this.handleSubmitRequest(params)
},
// 自动提交
autoSubmit() {
// 10秒提交一次
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.autoSubmitTimer = setInterval(() => {
// 提交的答案数据
const answers = this.handleSubmitData()
const params = { answers: JSON.stringify(answers), type: 0 }
// 请求接口
this.handleSubmitRequest(params)
}, 3000)
},
// 处理请求接口答案数据
handleSubmitData() {
return this.questions.map(questions => {
return questions.reduce(
(result, item) => {
// 单选题
if (item.type === 1) {
result.radioList.push(item.formModel)
}
// 多选题
if (item.type === 2) {
result.checkboxList.push(item.formModel)
}
// 简答题
if (item.type === 3) {
const formModel = Object.assign({}, item.formModel, {
user_answer: Base64.encode(item.formModel.user_answer)
})
result.shortAnswerList.push(formModel)
}
return result
},
{ radioList: [], checkboxList: [], shortAnswerList: [] }
)
})
},
// 请求提交接口
handleSubmitRequest(params) {
params.offset = this.offset
api
.submitCourseExam(this.sid, this.cid, this.pid, params)
.then(response => {
if (params.type === 0) {
console.log('暂存成功')
return
}
if (response.code === 200) {
this.$message.success('考试答卷提交成功')
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.getExam()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
},
// 上一套试卷
handlePrev() {
const offset = this.offset - 1
this.$router.push({ query: { offset } })
},
handleNext() {
const offset = this.offset + 1
this.$router.push({ query: { offset } })
},
handleNewExam() {
this.$router.push({ query: { offset: this.examCount } })
this.examCount++
},
// 清除定时器
clearTimer() {
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.checkStatusTimer && clearInterval(this.checkStatusTimer)
}
},
beforeMount() {
// // 自动获取考试状态
// this.autoCheckExamStatus()
// // 获取试题
// this.getExam()
},
destroyed() {
this.clearTimer()
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
min-width: 160px;
margin: 40px auto;
}
}
.no-exam {
padding: 100px;
font-size: 30px;
text-align: center;
}
.exam-welcome {
padding: 40px;
line-height: 30px;
text-align: center;
::v-deep .el-button {
margin-top: 30px;
}
}
</style>
<template>
<container :title="chapter.name" v-loading="loading">
<el-steps direction="vertical" v-if="data.curriculum">
<el-step title="阅读大作业要求" status="process">
<template v-slot:description>
<div v-html="data.curriculum.curriculum_essay"></div>
<p>截止日期:{{data.essay_date}}</p>
</template>
</el-step>
<el-step title="填写作业主题、正文,上传附件(点击“提交”保存)" status="process">
<template v-slot:description>
<el-form
:model="ruleForm"
:rules="rules"
:hide-required-asterisk="true"
:disabled="isRevised"
label-position="top"
ref="ruleForm"
>
<el-form-item label="主题" prop="essay_name">
<el-input v-model="ruleForm.essay_name" placeholder="主题" maxlength="50"></el-input>
</el-form-item>
<el-form-item label="正文" prop="essay_description">
<!-- 编辑器 -->
<v-editor :disabled="isRevised" v-model="ruleForm.essay_description"></v-editor>
</el-form-item>
<el-form-item prop="url">
<!-- 文件上传 -->
<v-upload v-model="ruleForm.url">
请上传对应的文件附件:
<!-- <template v-slot:tip>只支持docx格式的文件,文件小于10M</template> -->
</v-upload>
</el-form-item>
</el-form>
</template>
</el-step>
<el-step title="截止日期前提交" status="process">
<template v-slot:description>
<div class="work-bottom" v-if="detail">
<div class="info">
<template v-if="isRevised">
<div class="paper-check">
<p>批改时间:{{detail.check_date}}</p>
<div class="paper-check-item">
<b>评分:</b>
{{detail.score}}
</div>
<div class="paper-check-item">
<b>评语:</b>
<div class="edit_html" v-html="detail.check_comments"></div>
</div>
</div>
</template>
<template v-else-if="detail.created_time">
<p class="help">已于 {{detail.created_time}} 提交,等待老师批改中。</p>
<template v-if="detail.updated_time && detail.updated_time !== detail.created_time">
<p class="help">最近提交时间: {{detail.updated_time}}</p>
</template>
</template>
</div>
</div>
<div class="buttons">
<el-tooltip content="在获老师批改之前,可以多次提交,将以最后一次提交为准" placement="right">
<el-button
type="primary"
:disabled="isRevised"
:loading="submitLoading"
@click="onSubmit"
>{{submitText}}</el-button>
</el-tooltip>
</div>
</template>
</el-step>
</el-steps>
</container>
</template>
<script>
// componets
import Container from '../common/container.vue'
import VEditor from '../common/editor.vue'
import VUpload from '../common/upload.vue'
// api
import * as api from '../../api'
// 课程大作业
export default {
name: 'CourseWork',
components: { Container, VEditor, VUpload },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
ruleForm: {
essay_name: '',
essay_description: '',
url: ''
},
rules: {
essay_name: [
{ required: true, message: '请输入主题', trigger: 'blur' }
],
essay_description: [
{ required: true, message: '请输入正文', trigger: 'change' }
],
url: [{ required: true, message: '请上传附件', trigger: 'change' }]
},
detail: null,
loading: false,
messageInstance: null,
submitLoading: false
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 是否批改
isRevised() {
return this.detail ? !!this.detail.check_date : false
},
// 提交按钮文本
submitText() {
return this.isRevised ? '已批改' : '提交'
}
},
methods: {
// 获取大作业详情
getDetail() {
this.loading = true
api
.getCourseWork(this.sid, this.cid)
.then(response => {
this.detail = Array.isArray(response) ? null : response
if (this.detail) {
this.ruleForm.essay_name = this.detail.essay_name
this.ruleForm.essay_description = this.detail.essay_description
this.ruleForm.url = this.detail.file_url
}
})
.finally(() => {
this.loading = false
})
},
// 提交
onSubmit() {
this.messageInstance && this.messageInstance.close()
if (!this.ruleForm.essay_name) {
this.messageInstance = this.$message.error('请输入主题')
return
}
if (!this.ruleForm.essay_description) {
this.messageInstance = this.$message.error('请输入正文')
return
}
if (!this.ruleForm.url) {
this.messageInstance = this.$message.error('请上传附件')
return
}
const params = Object.assign(this.ruleForm, {
semester_id: this.sid,
course_id: this.cid
})
this.handleSubmitRequest(params)
},
// 请求提交接口
handleSubmitRequest(params) {
this.submitLoading = true
api
.updateCourseWork(this.sid, this.cid, params)
.then(response => {
if (response.status) {
this.$message.success('提交成功,等待批改')
this.getDetail()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
}
},
beforeMount() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
p {
margin: 0;
}
::v-deep .el-step__title {
border-bottom: 1px dashed #cecece;
}
::v-deep .el-step__description {
padding: 20px 0 30px;
font-size: 14px;
}
::v-deep .el-form-item__label {
font-weight: bold;
line-height: 24px;
padding: 0 0 5px;
}
.work-bottom {
.info {
color: #999;
line-height: 28px;
}
}
.buttons {
padding: 20px 0;
::v-deep .el-button {
width: 120px;
}
}
.paper-check {
padding: 10px;
color: #000;
border: 1px solid #dedede;
}
.paper-check-item {
display: flex;
b {
white-space: nowrap;
}
}
</style>
<template>
<div class="q-item">
<div class="q-item-hd">
<div class="q-item-num">{{ index + 1 }}.</div>
<div class="q-item-title" v-html="data.content"></div>
<div class="q-item-aside">
<template v-if="typeText">({{ typeText }})</template>
<template v-if="data.hasOwnProperty('score')">({{ data.score }}分)</template>
</div>
</div>
<div class="q-item-bd">
<!-- 单选 -->
<el-radio-group v-model="currentValue.user_answer" v-if="type === 1">
<div class="q-option-item" v-for="item in currentOptions" :key="item.id">
<el-radio :class="genClass(item)" :label="item.id">
<div class="q-option-item__answer" v-html="item.abc_option"></div>
</el-radio>
</div>
</el-radio-group>
<!-- 多选 -->
<el-checkbox-group v-model="currentValue.user_answer" v-if="type === 2">
<div class="q-option-item" v-for="item in currentOptions" :key="item.id">
<el-checkbox :class="genClass(item)" :label="item.id">
<div class="q-option-item__answer" v-html="item.abc_option"></div>
</el-checkbox>
</div>
</el-checkbox-group>
<!-- 简答题 -->
<template v-if="type === 3">
<v-editor v-model="currentValue.user_answer" :disabled="disabled"></v-editor>
<v-upload :disabled="disabled" v-model="currentValue.attachments">请上传对应的文件附件:</v-upload>
</template>
</div>
<div class="q-item-ft" v-if="disabled && showResult">
<template v-if="type === 3">
<p v-if="data.check_comment">
<span>评语:</span>
<span>{{ data.check_comment }}</span>
</p>
</template>
<template v-else>
<div class="result">
<p>
<span>学生答案:</span>
<span :class="isCorrect ? 'is-success' : 'is-error'">{{ submitAnswerText }}</span>
</p>
<p>
<span>正确答案:</span>
<span>{{ correctAnswerText }}</span>
</p>
</div>
</template>
<p v-if="data.hasOwnProperty('get_score')">
<span>评分:</span>
<span>{{ data.get_score }}分</span>
</p>
<div class="analyze" v-if="data.analysis">
<span>解析:</span>
<div class="analyze-main">
<span style="color: blue; cursor: pointer" @click="showAnalyze = !showAnalyze">查看解析</span>
<div v-html="data.analysis" v-if="data.analysis" v-show="showAnalyze" class="analyze-content"></div>
</div>
</div>
</div>
</div>
</template>
<script>
// components
import VEditor from '../common/editor.vue'
import VUpload from '../common/upload.vue'
export default {
name: 'ExamItem',
components: { VEditor, VUpload },
props: {
// 索引
index: { type: Number },
// 问题类型
type: { type: Number },
// 单条数据
data: {
type: Object,
default() {
return {}
}
},
// 提交的答案
value: {
type: Object,
default() {
return {}
}
},
// 是否禁用,提交过的是禁用状态
disabled: { type: Boolean, default: false },
showResult: { type: Boolean, default: true }
},
data() {
return {
currentValue: {},
showAnalyze: false
}
},
watch: {
value: {
immediate: true,
handler(value) {
this.currentValue = value
}
}
},
computed: {
// 26个英文字母
A_Z() {
const result = []
for (let i = 0; i < 26; i++) {
result.push(String.fromCharCode(65 + i))
}
return result
},
// 选项类型
typeText() {
const map = { 1: '单选题', 2: '多选题' }
return map[this.type]
},
// 处理后的options数据
currentOptions() {
if (!this.data.options) {
return []
}
return this.data.options.map((item, index) => {
// 英文字母 + 名称
item.abc = this.A_Z[index]
item.abc_option = `${this.A_Z[index]}. ${item.option}`
// 提交时的选中状态
const value = this.value.user_answer || ''
item.selected = Array.isArray(value) ? value.includes(item.id) : value === item.id
// 处理正确的选中状态
const hasChecked = Object.prototype.hasOwnProperty.call(item, 'checked')
const rightAnswer = this.data.right_answer || ''
if (!hasChecked && rightAnswer) {
item.checked = Array.isArray(rightAnswer) ? rightAnswer.includes(item.id) : rightAnswer === item.id
}
return item
})
},
// 正确答案显示的英文字母
correctAnswerText() {
const result = this.currentOptions.reduce((result, item) => {
item.checked && result.push(item.abc)
return result
}, [])
return result.join('、')
},
// 提交答案显示的英文字母
submitAnswerText() {
const result = this.currentOptions.reduce((result, item) => {
item.selected && result.push(item.abc)
return result
}, [])
return result.join('、')
},
// 是否回答正确
isCorrect() {
const options = this.currentOptions
for (let i = 0; i < options.length; i++) {
if (options[i].checked !== !!options[i].selected) {
return false
}
}
return true
}
},
methods: {
// 生成class
genClass(item) {
if (!this.disabled || !this.showResult) {
return null
}
return {
'is-error': !this.isCorrect && item.selected,
'is-success': this.isCorrect && item.selected
}
}
}
}
</script>
<style lang="scss" scoped>
.q-item {
font-size: 16px;
padding: 10px 0;
border-bottom: 1px solid #c9c9c97a;
.upload {
font-size: 14px;
}
}
.q-item-hd {
display: flex;
padding: 10px 0 20px;
::v-deep p {
margin: 0;
padding: 0;
}
::v-deep ul {
margin: 0;
padding: 0;
list-style: none;
}
}
.q-item-num {
width: 20px;
text-align: center;
}
.q-item-title {
flex: 1;
::v-deep img {
max-width: 100%;
}
}
.q-item-aside {
padding-left: 20px;
// align-self: flex-end;
}
.q-option-item {
padding-left: 20px;
margin-bottom: 14px;
}
.q-option-item__answer {
display: inline;
::v-deep * {
display: inline;
}
}
.is-success {
color: #090;
}
.is-error {
color: #d80000;
}
::v-deep .el-radio {
&.is-disabled .el-radio__label {
color: #3c3c3c;
}
&.is-error .el-radio__label {
color: #d80000;
}
&.is-success .el-radio__label {
color: #090;
}
}
::v-deep .el-checkbox {
&.is-disabled .el-checkbox__label {
color: #3c3c3c;
}
&.is-error .el-checkbox__label {
color: #d80000;
}
&.is-success .el-checkbox__label {
color: #090;
}
}
.q-item-ft {
padding: 10px 0;
p {
font-size: 14px;
margin: 0 0 10px 0;
}
.result {
display: flex;
justify-content: flex-end;
p {
padding-left: 20px;
}
}
.analyze {
display: flex;
font-size: 14px;
}
.analyze-main {
flex: 1;
overflow: hidden;
}
.analyze-content {
margin-top: 10px;
background-color: #c9c9c97a;
border: 1px solid #c9c9c97a;
padding: 10px;
::v-deep * {
margin: 0;
padding: 0;
max-width: 100%;
}
}
}
</style>
<template>
<component :is="currentCompoent" :chapter="chapter" :data="data" v-bind="$attrs" v-on="$listeners" v-if="chapter" />
</template>
<script>
// componets
import ChapterWork from './chapterWork.vue'
import ChapterTest from './chapterTest.vue'
export default {
name: 'ViewerWork',
components: { ChapterWork, ChapterTest },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
currentCompoent() {
const componentNames = {
1: 'ChapterTest', // 课后测验
2: 'ChapterWork' // 作业
}
const homework = this.chapter.homework
return homework ? componentNames[homework.work_type] : ''
}
}
}
</script>
<template>
<div class="course-viewer" element-loading-text="加载中..." v-loading="!loaded">
<div class="course-viewer-main">
<!-- 顶部区域 -->
<div class="course-viewer-main-hd">
<router-link :to="`/course/learn/${cid}`">
<i class="el-icon-arrow-left"></i>
</router-link>
<h1 class="course-viewer-main-hd__title">{{ detail.course_name }}</h1>
<div class="course-menu" @click="menuVisible = !menuVisible">
<i class="el-icon-s-unfold" v-if="menuVisible"></i>
<i class="el-icon-s-fold" v-else></i>
</div>
</div>
<!-- 主体区域 -->
<div class="course-viewer-main-bd">
<router-view
:data="detail"
:chapter="activeChapter"
:pptIndex="pptIndex"
:isSeek="isSeek"
:key="pid"
@pptupdate="handlePPTupdate"
@change-ppt="handleChangePPT(...arguments, false)"
@update="getCourse"
/>
</div>
</div>
<!-- 侧边栏 -->
<v-aside
:data="detail"
:chapters="chapters"
:active="activeChapter"
:ppts="ppts"
:pptIndex="pptIndex"
@change-ppt="handleChangePPT(...arguments, true)"
v-if="detail.chapters"
v-show="menuVisible"
></v-aside>
</div>
</template>
<script>
// api
import * as api from './api'
// components
import VAside from './components/aside/index.vue'
export default {
name: 'CourseViewer',
components: { VAside },
data() {
return {
detail: {},
ppts: [],
pptIndex: 0,
isSeek: false,
menuVisible: true,
loaded: false
}
},
watch: {
activeChapter() {
this.ppts = []
this.pptIndex = 0
},
isLive(value) {
if (value) {
this.menuVisible = false
}
},
isCourseExam(value) {
if (value) {
this.menuVisible = false
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 章节列表
chapters() {
const chapters = this.detail.chapters || []
if (!chapters.length) {
return []
}
const customeChapter = {
name: '大作业及资料',
children: [
{ name: '课程大作业', id: 'course_work', type: 99 },
{ name: '课程资料', id: 'course_info', type: 100 },
{ name: '教学评估', id: 'teach_evaluation', type: 102 }
]
}
// 课程考试
if (this.detail.course_examination) {
customeChapter.children.push({
name: '课程考试',
id: 'course_exam',
type: 101
})
}
// chapters.push(customeChapter)
return chapters
},
// 当前选中的章节
activeChapter() {
const id = this.pid
const list = this.chapters
return this.findChapter(id, list)
},
// 直播
isLive() {
return this.activeChapter ? [5, 8].includes(this.activeChapter.type) : false
},
// 课程考试
isCourseExam() {
return this.activeChapter ? this.activeChapter.type === 101 : false
}
},
methods: {
// 查找当前章节
findChapter(id, list) {
for (const item of list) {
if (item.id === id) {
return item
}
if (item.children && item.children.length) {
const found = this.findChapter(id, item.children)
if (found) {
return found
}
}
}
return null
},
// 获取课程详情
getCourse() {
this.loaded = false
api
.getCourse(this.sid, this.cid)
.then(response => {
this.detail = response
})
.finally(() => {
this.loaded = true
})
},
// PPT列表更新
handlePPTupdate(list) {
this.ppts = list
},
// 右侧菜单选中的PPT修改
handleChangePPT(index, isSeek) {
this.pptIndex = index
this.isSeek = isSeek
}
},
beforeMount() {
this.getCourse()
}
}
</script>
<style lang="scss">
.course-viewer {
display: flex;
height: 100vh;
overflow: hidden;
}
.course-viewer-main {
flex: 1;
display: flex;
flex-direction: column;
}
.course-viewer-main-hd {
display: flex;
align-items: center;
background-color: #3f3f3f;
height: 56px;
a {
color: #fff;
padding: 10px;
}
i {
font-size: 24px;
}
}
.course-viewer-main-hd__title {
flex: 1;
font-size: 1.5em;
// text-align: center;
color: #a0a0a0;
}
.course-viewer-main-bd {
flex: 1;
height: calc(100vh - 56px);
overflow-y: auto;
}
.course-viewer-content {
// min-height: 50%;
max-width: 900px;
padding: 40px 120px 80px;
margin: 40px auto;
background-color: #f2f2f2;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.05);
}
.course-viewer-content-hd {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40px 0;
// text-align: center;
}
.course-viewer-content-hd__title {
position: relative;
display: inline-block;
margin: 0 0 0 20px;
padding: 0 0 5px;
font-size: 20px;
border-bottom: 3px solid #707070;
&::before {
content: '·';
position: absolute;
left: -30px;
top: 50%;
font-size: 30px;
transform: translateY(-50%);
}
&::after {
content: '';
position: absolute;
left: 0;
bottom: -8px;
width: 100%;
height: 1px;
background-color: #707070;
}
}
.course-viewer-content-hd__aside {
font-size: 18px;
// border-bottom: 3px solid #707070;
}
.course-menu {
width: 24px;
height: 24px;
padding: 12px;
margin-right: 10px;
color: #fff;
text-align: center;
border-radius: 50%;
cursor: pointer;
&:hover {
background-color: rgba(255, 255, 255, 0.08);
}
}
</style>
export default [
{
path: '/viewer/:cid',
component: () => import('./index.vue'),
children: [{ name: 'viewerCourseChapter', path: ':id', component: () => import('./components/layout.vue') }]
}
]
<template>
<div class="container">
<svg width="251" height="294">
<g fill="none" fill-rule="evenodd">
<path
d="M0 129.023v-2.084C0 58.364 55.591 2.774 124.165 2.774h2.085c68.574 0 124.165 55.59 124.165 124.165v2.084c0 68.575-55.59 124.166-124.165 124.166h-2.085C55.591 253.189 0 197.598 0 129.023"
fill="#E4EBF7"
></path>
<path d="M41.417 132.92a8.231 8.231 0 1 1-16.38-1.65 8.231 8.231 0 0 1 16.38 1.65" fill="#FFF"></path>
<path d="M38.652 136.36l10.425 5.91M49.989 148.505l-12.58 10.73" stroke="#FFF" stroke-width="2"></path>
<path
d="M41.536 161.28a5.636 5.636 0 1 1-11.216-1.13 5.636 5.636 0 0 1 11.216 1.13M59.154 145.261a5.677 5.677 0 1 1-11.297-1.138 5.677 5.677 0 0 1 11.297 1.138M100.36 29.516l29.66-.013a4.562 4.562 0 1 0-.004-9.126l-29.66.013a4.563 4.563 0 0 0 .005 9.126M111.705 47.754l29.659-.013a4.563 4.563 0 1 0-.004-9.126l-29.66.013a4.563 4.563 0 1 0 .005 9.126"
fill="#FFF"
></path>
<path
d="M114.066 29.503V29.5l15.698-.007a4.563 4.563 0 1 0 .004 9.126l-15.698.007v-.002a4.562 4.562 0 0 0-.004-9.122M185.405 137.723c-.55 5.455-5.418 9.432-10.873 8.882-5.456-.55-9.432-5.418-8.882-10.873.55-5.455 5.418-9.432 10.873-8.882 5.455.55 9.432 5.418 8.882 10.873"
fill="#FFF"
></path>
<path d="M180.17 143.772l12.572 7.129M193.841 158.42L178.67 171.36" stroke="#FFF" stroke-width="2"></path>
<path
d="M185.55 171.926a6.798 6.798 0 1 1-13.528-1.363 6.798 6.798 0 0 1 13.527 1.363M204.12 155.285a6.848 6.848 0 1 1-13.627-1.375 6.848 6.848 0 0 1 13.626 1.375"
fill="#FFF"
></path>
<path
d="M152.988 194.074a2.21 2.21 0 1 1-4.42 0 2.21 2.21 0 0 1 4.42 0zM225.931 118.217a2.21 2.21 0 1 1-4.421 0 2.21 2.21 0 0 1 4.421 0zM217.09 153.051a2.21 2.21 0 1 1-4.421 0 2.21 2.21 0 0 1 4.42 0zM177.84 109.842a2.21 2.21 0 1 1-4.422 0 2.21 2.21 0 0 1 4.421 0zM196.114 94.454a2.21 2.21 0 1 1-4.421 0 2.21 2.21 0 0 1 4.421 0zM202.844 182.523a2.21 2.21 0 1 1-4.42 0 2.21 2.21 0 0 1 4.42 0z"
stroke="#FFF"
stroke-width="2"
></path>
<path
stroke="#FFF"
stroke-width="2"
d="M215.125 155.262l-1.902 20.075-10.87 5.958M174.601 176.636l-6.322 9.761H156.98l-4.484 6.449M175.874 127.28V111.56M221.51 119.404l-12.77 7.859-15.228-7.86V96.668"
></path>
<path
d="M180.68 29.32C180.68 13.128 193.806 0 210 0c16.193 0 29.32 13.127 29.32 29.32 0 16.194-13.127 29.322-29.32 29.322-16.193 0-29.32-13.128-29.32-29.321"
fill="#A26EF4"
></path>
<path
d="M221.45 41.706l-21.563-.125a1.744 1.744 0 0 1-1.734-1.754l.071-12.23a1.744 1.744 0 0 1 1.754-1.734l21.562.125c.964.006 1.74.791 1.735 1.755l-.071 12.229a1.744 1.744 0 0 1-1.754 1.734"
fill="#FFF"
></path>
<path
d="M215.106 29.192c-.015 2.577-2.049 4.654-4.543 4.64-2.494-.014-4.504-2.115-4.489-4.693l.04-6.925c.016-2.577 2.05-4.654 4.543-4.64 2.494.015 4.504 2.116 4.49 4.693l-.04 6.925zm-4.53-14.074a6.877 6.877 0 0 0-6.916 6.837l-.043 7.368a6.877 6.877 0 0 0 13.754.08l.042-7.368a6.878 6.878 0 0 0-6.837-6.917zM167.566 68.367h-3.93a4.73 4.73 0 0 1-4.717-4.717 4.73 4.73 0 0 1 4.717-4.717h3.93a4.73 4.73 0 0 1 4.717 4.717 4.73 4.73 0 0 1-4.717 4.717"
fill="#FFF"
></path>
<path
d="M168.214 248.838a6.611 6.611 0 0 1-6.61-6.611v-66.108a6.611 6.611 0 0 1 13.221 0v66.108a6.611 6.611 0 0 1-6.61 6.61"
fill="#5BA02E"
></path>
<path
d="M176.147 248.176a6.611 6.611 0 0 1-6.61-6.61v-33.054a6.611 6.611 0 1 1 13.221 0v33.053a6.611 6.611 0 0 1-6.61 6.611"
fill="#92C110"
></path>
<path
d="M185.994 293.89h-27.376a3.17 3.17 0 0 1-3.17-3.17v-45.887a3.17 3.17 0 0 1 3.17-3.17h27.376a3.17 3.17 0 0 1 3.17 3.17v45.886a3.17 3.17 0 0 1-3.17 3.17"
fill="#F2D7AD"
></path>
<path
d="M81.972 147.673s6.377-.927 17.566-1.28c11.729-.371 17.57 1.086 17.57 1.086s3.697-3.855.968-8.424c1.278-12.077 5.982-32.827.335-48.273-1.116-1.339-3.743-1.512-7.536-.62-1.337.315-7.147-.149-7.983-.1l-15.311-.347s-3.487-.17-8.035-.508c-1.512-.113-4.227-1.683-5.458-.338-.406.443-2.425 5.669-1.97 16.077l8.635 35.642s-3.141 3.61 1.219 7.085"
fill="#FFF"
></path>
<path
d="M75.768 73.325l-.9-6.397 11.982-6.52s7.302-.118 8.038 1.205c.737 1.324-5.616.993-5.616.993s-1.836 1.388-2.615 2.5c-1.654 2.363-.986 6.471-8.318 5.986-1.708.284-2.57 2.233-2.57 2.233"
fill="#FFC6A0"
></path>
<path
d="M52.44 77.672s14.217 9.406 24.973 14.444c1.061.497-2.094 16.183-11.892 11.811-7.436-3.318-20.162-8.44-21.482-14.496-.71-3.258 2.543-7.643 8.401-11.76M141.862 80.113s-6.693 2.999-13.844 6.876c-3.894 2.11-10.137 4.704-12.33 7.988-6.224 9.314 3.536 11.22 12.947 7.503 6.71-2.651 28.999-12.127 13.227-22.367"
fill="#FFB594"
></path>
<path
d="M76.166 66.36l3.06 3.881s-2.783 2.67-6.31 5.747c-7.103 6.195-12.803 14.296-15.995 16.44-3.966 2.662-9.754 3.314-12.177-.118-3.553-5.032.464-14.628 31.422-25.95"
fill="#FFC6A0"
></path>
<path
d="M64.674 85.116s-2.34 8.413-8.912 14.447c.652.548 18.586 10.51 22.144 10.056 5.238-.669 6.417-18.968 1.145-20.531-.702-.208-5.901-1.286-8.853-2.167-.87-.26-1.611-1.71-3.545-.936l-1.98-.869zM128.362 85.826s5.318 1.956 7.325 13.734c-.546.274-17.55 12.35-21.829 7.805-6.534-6.94-.766-17.393 4.275-18.61 4.646-1.121 5.03-1.37 10.23-2.929"
fill="#FFF"
></path>
<path
d="M78.18 94.656s.911 7.41-4.914 13.078"
stroke="#E4EBF7"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M87.397 94.68s3.124 2.572 10.263 2.572c7.14 0 9.074-3.437 9.074-3.437"
stroke="#E4EBF7"
stroke-width=".932"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M117.184 68.639l-6.781-6.177s-5.355-4.314-9.223-.893c-3.867 3.422 4.463 2.083 5.653 4.165 1.19 2.082.848 1.143-2.083.446-5.603-1.331-2.082.893 2.975 5.355 2.091 1.845 6.992.955 6.992.955l2.467-3.851z"
fill="#FFC6A0"
></path>
<path
d="M105.282 91.315l-.297-10.937-15.918-.027-.53 10.45c-.026.403.17.788.515.999 2.049 1.251 9.387 5.093 15.799.424.287-.21.443-.554.431-.91"
fill="#FFB594"
></path>
<path
d="M107.573 74.24c.817-1.147.982-9.118 1.015-11.928a1.046 1.046 0 0 0-.965-1.055l-4.62-.365c-7.71-1.044-17.071.624-18.253 6.346-5.482 5.813-.421 13.244-.421 13.244s1.963 3.566 4.305 6.791c.756 1.041.398-3.731 3.04-5.929 5.524-4.594 15.899-7.103 15.899-7.103"
fill="#5C2552"
></path>
<path
d="M88.426 83.206s2.685 6.202 11.602 6.522c7.82.28 8.973-7.008 7.434-17.505l-.909-5.483c-6.118-2.897-15.478.54-15.478.54s-.576 2.044-.19 5.504c-2.276 2.066-1.824 5.618-1.824 5.618s-.905-1.922-1.98-2.321c-.86-.32-1.897.089-2.322 1.98-1.04 4.632 3.667 5.145 3.667 5.145"
fill="#FFC6A0"
></path>
<path
stroke="#DB836E"
stroke-width="1.145"
stroke-linecap="round"
stroke-linejoin="round"
d="M100.843 77.099l1.701-.928-1.015-4.324.674-1.406"
></path>
<path
d="M105.546 74.092c-.022.713-.452 1.279-.96 1.263-.51-.016-.904-.607-.882-1.32.021-.713.452-1.278.96-1.263.51.016.904.607.882 1.32M97.592 74.349c-.022.713-.452 1.278-.961 1.263-.509-.016-.904-.607-.882-1.32.022-.713.452-1.279.961-1.263.51.016.904.606.882 1.32"
fill="#552950"
></path>
<path
d="M91.132 86.786s5.269 4.957 12.679 2.327"
stroke="#DB836E"
stroke-width="1.145"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M99.776 81.903s-3.592.232-1.44-2.79c1.59-1.496 4.897-.46 4.897-.46s1.156 3.906-3.457 3.25"
fill="#DB836E"
></path>
<path
d="M102.88 70.6s2.483.84 3.402.715M93.883 71.975s2.492-1.144 4.778-1.073"
stroke="#5C2552"
stroke-width="1.526"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M86.32 77.374s.961.879 1.458 2.106c-.377.48-1.033 1.152-.236 1.809M99.337 83.719s1.911.151 2.509-.254"
stroke="#DB836E"
stroke-width="1.145"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M87.782 115.821l15.73-3.012M100.165 115.821l10.04-2.008"
stroke="#E4EBF7"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M66.508 86.763s-1.598 8.83-6.697 14.078"
stroke="#E4EBF7"
stroke-width="1.114"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M128.31 87.934s3.013 4.121 4.06 11.785"
stroke="#E4EBF7"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M64.09 84.816s-6.03 9.912-13.607 9.903"
stroke="#DB836E"
stroke-width=".795"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M112.366 65.909l-.142 5.32s5.993 4.472 11.945 9.202c4.482 3.562 8.888 7.455 10.985 8.662 4.804 2.766 8.9 3.355 11.076 1.808 4.071-2.894 4.373-9.878-8.136-15.263-4.271-1.838-16.144-6.36-25.728-9.73"
fill="#FFC6A0"
></path>
<path
d="M130.532 85.488s4.588 5.757 11.619 6.214"
stroke="#DB836E"
stroke-width=".75"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M121.708 105.73s-.393 8.564-1.34 13.612"
stroke="#E4EBF7"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M115.784 161.512s-3.57-1.488-2.678-7.14"
stroke="#648BD8"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M101.52 290.246s4.326 2.057 7.408 1.03c2.842-.948 4.564.673 7.132 1.186 2.57.514 6.925 1.108 11.772-1.269-.104-5.551-6.939-4.01-12.048-6.763-2.582-1.39-3.812-4.757-3.625-8.863h-9.471s-1.402 10.596-1.169 14.68"
fill="#CBD1D1"
></path>
<path
d="M101.496 290.073s2.447 1.281 6.809.658c3.081-.44 3.74.485 7.479 1.039 3.739.554 10.802-.07 11.91-.9.415 1.108-.347 2.077-.347 2.077s-1.523.608-4.847.831c-2.045.137-5.843.293-7.663-.507-1.8-1.385-5.286-1.917-5.77-.243-3.947.958-7.41-.288-7.41-.288l-.16-2.667z"
fill="#2B0849"
></path>
<path d="M108.824 276.19h3.116s-.103 6.751 4.57 8.62c-4.673.624-8.62-2.32-7.686-8.62" fill="#A4AABA"></path>
<path
d="M57.65 272.52s-2.122 7.47-4.518 12.396c-1.811 3.724-4.255 7.548 5.505 7.548 6.698 0 9.02-.483 7.479-6.648-1.541-6.164.268-13.296.268-13.296H57.65z"
fill="#CBD1D1"
></path>
<path
d="M51.54 290.04s2.111 1.178 6.682 1.178c6.128 0 8.31-1.662 8.31-1.662s.605 1.122-.624 2.18c-1 .862-3.624 1.603-7.444 1.559-4.177-.049-5.876-.57-6.786-1.177-.831-.554-.692-1.593-.138-2.078"
fill="#2B0849"
></path>
<path
d="M58.533 274.438s.034 1.529-.315 2.95c-.352 1.431-1.087 3.127-1.139 4.17-.058 1.16 4.57 1.592 5.194.035.623-1.559 1.303-6.475 1.927-7.306.622-.831-4.94-2.135-5.667.15"
fill="#A4AABA"
></path>
<path
d="M100.885 277.015l13.306.092s1.291-54.228 1.843-64.056c.552-9.828 3.756-43.13.997-62.788l-12.48-.64-22.725.776s-.433 3.944-1.19 9.921c-.062.493-.677.838-.744 1.358-.075.582.42 1.347.318 1.956-2.35 14.003-6.343 32.926-8.697 46.425-.116.663-1.227 1.004-1.45 2.677-.04.3.21 1.516.112 1.785-6.836 18.643-10.89 47.584-14.2 61.551l14.528-.014s2.185-8.524 4.008-16.878c2.796-12.817 22.987-84.553 22.987-84.553l3-.517 1.037 46.1s-.223 1.228.334 2.008c.558.782-.556 1.117-.39 2.233l.39 1.784s-.446 7.14-.892 11.826c-.446 4.685-.092 38.954-.092 38.954"
fill="#7BB2F9"
></path>
<path
d="M77.438 220.434c1.146.094 4.016-2.008 6.916-4.91M107.55 223.931s2.758-1.103 6.069-3.862"
stroke="#648BD8"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M108.459 220.905s2.759-1.104 6.07-3.863"
stroke="#648BD8"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M76.099 223.557s2.608-.587 6.47-3.346M87.33 150.82c-.27 3.088.297 8.478-4.315 9.073M104.829 149.075s.11 13.936-1.286 14.983c-2.207 1.655-2.975 1.934-2.975 1.934M101.014 149.63s.035 12.81-1.19 24.245M94.93 174.965s7.174-1.655 9.38-1.655M75.671 204.754c-.316 1.55-.64 3.067-.973 4.535 0 0-1.45 1.822-1.003 3.756.446 1.934-.943 2.034-4.96 15.273-1.686 5.559-4.464 18.49-6.313 27.447-.078.38-4.018 18.06-4.093 18.423M77.043 196.743a313.269 313.269 0 0 1-.877 4.729M83.908 151.414l-1.19 10.413s-1.091.148-.496 2.23c.111 1.34-2.66 15.692-5.153 30.267M57.58 272.94h13.238"
stroke="#648BD8"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
<path
d="M117.377 147.423s-16.955-3.087-35.7.199c.157 2.501-.002 4.128-.002 4.128s14.607-2.802 35.476-.31c.251-2.342.226-4.017.226-4.017"
fill="#192064"
></path>
<path
d="M107.511 150.353l.004-4.885a.807.807 0 0 0-.774-.81c-2.428-.092-5.04-.108-7.795-.014a.814.814 0 0 0-.784.81l-.003 4.88c0 .456.371.82.827.808a140.76 140.76 0 0 1 7.688.017.81.81 0 0 0 .837-.806"
fill="#FFF"
></path>
<path
d="M106.402 149.426l.002-3.06a.64.64 0 0 0-.616-.643 94.135 94.135 0 0 0-5.834-.009.647.647 0 0 0-.626.643l-.001 3.056c0 .36.291.648.651.64 1.78-.04 3.708-.041 5.762.012.36.009.662-.279.662-.64"
fill="#192064"
></path>
<path
d="M101.485 273.933h12.272M102.652 269.075c.006 3.368.04 5.759.11 6.47M102.667 263.125c-.009 1.53-.015 2.98-.016 4.313M102.204 174.024l.893 44.402s.669 1.561-.224 2.677c-.892 1.116 2.455.67.893 2.231-1.562 1.562.893 1.116 0 3.347-.592 1.48-.988 20.987-1.09 34.956"
stroke="#648BD8"
stroke-width="1.051"
stroke-linecap="round"
stroke-linejoin="round"
></path>
</g>
</svg>
<p class="tips">很抱歉,你暂时无权限访问...</p>
<a :href="loginUrl"><el-button round type="primary">重新登录</el-button></a>
</div>
</template>
<script>
export default {
computed: {
loginUrl() {
return `${webConf.others.loginUrl}?rd=${encodeURIComponent(window.location.href)}`
}
}
}
</script>
<style lang="scss" scoped>
.container {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tips {
margin: 30px;
font-size: 24px;
color: #313131;
}
</style>
......@@ -12,15 +12,27 @@ export default [
/* 消息模板 */
{ path: '/msgTemplate', component: () => import(/* webpackChunkName: "feedback" */ '@/pages/msgTemplate/index') },
/* 消息模板新建 */
{ path: '/msgTemplate/detail', component: () => import(/* webpackChunkName: "feedback" */ '@/pages/msgTemplate/detail') },
{
path: '/msgTemplate/detail',
component: () => import(/* webpackChunkName: "feedback" */ '@/pages/msgTemplate/detail')
},
/* 用户群组 */
{ path: '/userGroup', component: () => import(/* webpackChunkName: "feedback" */ '@/pages/userGroup/index') },
/* 日志 */
{ path: '/log', component: () => import(/* webpackChunkName: "feedback" */ '@/pages/log/index') },
/* 短信管理 */
{ path: '/sms/smsTemplate', component: () => import(/* webpackChunkName: "feedback" */ '@/pages/sms/smsTemplate/index') },
{ path: '/sms/smsRecord', component: () => import(/* webpackChunkName: "feedback" */ '@/pages/sms/smsRecord/index') },
{ path: '/smsTemplate/detail', component: () => import(/* webpackChunkName: "feedback" */ '@/pages/sms/smsTemplate/detail') }
{
path: '/sms/smsTemplate',
component: () => import(/* webpackChunkName: "feedback" */ '@/pages/sms/smsTemplate/index')
},
{
path: '/sms/smsRecord',
component: () => import(/* webpackChunkName: "feedback" */ '@/pages/sms/smsRecord/index')
},
{
path: '/smsTemplate/detail',
component: () => import(/* webpackChunkName: "feedback" */ '@/pages/sms/smsTemplate/detail')
}
// /* 修改密码 */
// {
// path: '/account/password',
......@@ -46,5 +58,9 @@ export default [
// component: () => import(/* webpackChunkName: "userTrigger" */ '@/pages/test.vue')
// }
]
},
{
path: '/401',
component: () => import(/* webpackChunkName: "error" */ '@/pages/error/401.vue')
}
]
import Vue from 'vue'
import Vuex from 'vuex'
import { getUser, logout, getUserGrade } from '@/api/account'
import { getUser, logout, getUserGrade, getPermissions } from '@/api/account'
Vue.use(Vuex)
const store = new Vuex.Store({
......@@ -10,7 +10,8 @@ const store = new Vuex.Store({
isLogin: false,
isIos: /iphone|ipad|ipod/i.test(navigator.userAgent),
isAndroid: /android/i.test(navigator.userAgent),
level: {}
level: {},
permissions: [] // 权限列表
},
mutations: {
setUser(state, user) {
......@@ -21,9 +22,19 @@ const store = new Vuex.Store({
},
setUserGrade(state, level) {
state.level = level
},
setPermissions(state, permissions) {
state.permissions = permissions
}
},
actions: {
getPermissions({ commit }) {
getPermissions().then(res => {
if (res.data && res.data.items) {
commit('setPermissions', res.data.items)
}
})
},
getUser({ commit }) {
getUser().then(response => {
commit('setUser', response)
......@@ -64,5 +75,6 @@ const store = new Vuex.Store({
}
}
})
store.dispatch('getPermissions')
export default store
import axios from 'axios'
import qs from 'qs'
import { Message } from 'element-ui'
import router from '../router'
const httpRequest = axios.create({
// baseURL: process.env.VUE_APP_BASE_API,
......@@ -15,6 +16,21 @@ const httpRequest = axios.create({
// 请求拦截
httpRequest.interceptors.request.use(
function(config) {
// 权限接口单独签名
// https://gitlab.ezijing.com/root/api-documents/-/blob/master/ezijing_permissions/%E7%AD%BE%E5%90%8D%E9%AA%8C%E8%AF%81.md
if (/^\/api\/permissions/.test(config.url)) {
// 默认参数
const defaultHeaders = {
timestamp: parseInt(Date.now() / 1000),
nonce: Math.random()
.toString(36)
.slice(-8),
'secret-id': 'ezijing_4d63be3530c31f04bdec3c1dcf158ceb',
'secret-key': 'c365340babb2dd2a8160575e2a5bb1b0',
signature: 'UG7wBenexQhiuD2wpCwuxkU0jqcj006d'
}
config.headers = Object.assign(config.headers, defaultHeaders)
}
if (config.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
config.data = qs.stringify(config.data)
}
......@@ -35,12 +51,10 @@ httpRequest.interceptors.request.use(
// 响应拦截
httpRequest.interceptors.response.use(
function(response) {
const { data } = response
if (data.msg) {
const { data, config } = response
if (data.msg === '请先登录') {
window.location.href = `${webConf.others.loginUrl}?rd=${encodeURIComponent(window.location.href)}`
}
}
if (parseInt(data.code) === 1) {
return Message.error(data.msg)
}
......@@ -48,6 +62,11 @@ httpRequest.interceptors.response.use(
if (data.code === 4001) {
window.location.href = `${webConf.others.loginUrl}?rd=${encodeURIComponent(window.location.href)}`
}
// 无权限
if (/^\/api\/permissions/.test(config.url) && data.code === 403) {
router.push('/401')
return Promise.reject(data)
}
return data.message ? Message.error(data.message) : Message.error(data.msg)
}
return data
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论