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

update viewer module

上级 d69faf21
......@@ -104,8 +104,22 @@ export default class CourseAction extends BaseACTION {
unit: item.lecturer_title || ''
})
}
const findChapter = function (id, list) {
for (const item of list) {
if (item.resource_id === id) {
return item
}
if (item.children && item.children.length) {
const found = findChapter(id, item.children)
if (found) {
return found
}
}
}
}
/* 课程内容 */
json.tabs1ChapterList = {
currentChapter: findChapter(cur.latest_play, data.chapters),
currentChapterId: cur.latest_play || '',
currentVideoProvider: cur.latest_play_type || '1',
course: cur.chapters.map((_, i) => {
......@@ -173,35 +187,39 @@ export default class CourseAction extends BaseACTION {
title: '课程大作业',
isUp: true,
chapters: [],
type: 'course_work',
id: 'course_work',
sid: sid,
cid: cid
cid: cid,
type: 99
})
json.tabs1ChapterList.course.push({
title: '课程资料',
isUp: true,
chapters: [],
type: 'course_info',
id: 'course_info',
sid: sid,
cid: cid
cid: cid,
type: 100
})
json.tabs1ChapterList.course.push({
title: '教学评估',
isUp: true,
chapters: [],
type: 'teach_evaluation',
id: 'teach_evaluation',
sid: sid,
cid: cid
cid: cid,
type: 102
})
if (cur.course_examination) {
json.tabs1ChapterList.course.push({
title: '课程考试',
isUp: true,
chapters: [],
type: 'exam',
id: 'course_exam',
sid: sid,
cid: cid,
examId: cur.course_examination
examId: cur.course_examination,
type: 102
})
}
/* 课程考核 考核标准文案读取 */
......
......@@ -35,6 +35,27 @@ export function getChapterVideoAliyun(vid) {
)
}
/**
* 获取章节视频播放进度
* @param {string} semesterId 学期ID
* @param {string} resourseId 章节的资源ID
* @param {Object} params
*/
export function getChapterVideoProgress(semesterId, resourseId, params) {
return httpRequest.get(
`/v2/education/video/${semesterId}/${resourseId}/device`,
params
)
}
/**
* 更新章节视频播放进度
* @param {Object} params
*/
export function updateChapterVideoProgress(params) {
return httpRequest.get('/v2/analytics/upload-video', params)
}
/**
* 获取答题信息
* @param {string} semesterId 学期ID
......@@ -130,8 +151,9 @@ export function submitCourseExam(semesterId, courseId, examId, data) {
* @param {string} courseId 课程ID
* @param {string} examId 试题ID
*/
export function getCourseExamResult(semesterId, courseId, examId) {
export function getCourseExamResult(semesterId, courseId, examId, params) {
return httpRequest.get(
`/v2/education/${semesterId}/${courseId}/examination/${examId}/sheet`
`/v2/education/${semesterId}/${courseId}/examination/${examId}/sheet`,
params
)
}
<template>
<ul class="chapter-list">
<li class="chapter-item" v-for="item in data" :key="item.id">
<li class="chapter-item" v-for="item in chapters" :key="item.id">
<h4>{{item.name}}</h4>
<ul class="chapter-item-list">
<li
......@@ -9,7 +9,7 @@
@click="onClick(subItem)"
:class="{'is-active': subItem.id === (active ? active.id : '')}"
>
<span class="chapter-item-list__name">{{subItem.name | showName(subItem.type)}}</span>
<span class="chapter-item-list__name">{{subItem.name | showName(subItem)}}</span>
<i class="el-icon" :class="genIconClass(subItem.type)"></i>
</li>
</ul>
......@@ -20,7 +20,14 @@
<script>
export default {
props: {
data: { type: Array, default: () => [] },
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
},
chapters: { type: Array, default: () => [] },
// 当前选中的章节
active: {
type: Object,
......@@ -30,26 +37,13 @@ export default {
}
},
data() {
return {
otherList: [
{
name: '大作业及资料',
children: [
{ name: '课程大作业', id: 'course_work' },
{ name: '课程资料', id: 'course_info' },
{ name: '教学评估', id: 'teach_evaluation' }
]
}
]
}
},
computed: {
list() {
return this.data.concat(this.otherList)
}
return {}
},
filters: {
showName(name, type) {
showName(name, data) {
if (data.type === 5) {
return `${name}(${data.live.start_time})`
}
return name
}
},
......@@ -63,16 +57,20 @@ export default {
return map[type] || 'el-icon-self-cc-book'
},
onClick(data) {
if (data.type === 1) {
return
}
// 课程大作业
// if (data.id === 'course_work') {
// this.$router.push({ name: 'viewerCourseWork' })
// return
// }
// 课程资料
// if (data.id === 'course_info') {
// this.$router.push({ name: 'viewerCourseFile' })
// 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 }
......
......@@ -3,12 +3,12 @@
<el-tabs v-model="activeName">
<el-tab-pane label="章节" name="0">
<div class="tab-pane">
<aside-chapter :data="chapters" :active="active"></aside-chapter>
<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 :data="ppts" :pptIndex="pptIndex" v-on="$listeners"></aside-lecture>
<aside-lecture :ppts="ppts" :pptIndex="pptIndex" v-on="$listeners"></aside-lecture>
</div>
</el-tab-pane>
</el-tabs>
......@@ -21,6 +21,13 @@ import AsideLecture from './lecture.vue'
export default {
props: {
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
},
// 章节
chapters: { type: Array, default: () => [] },
// 讲义
......
<template>
<ul class="lecture-list">
<li
v-for="(item, index) in data"
v-for="(item, index) in ppts"
:key="item.id"
@click="onClick(index)"
:class="{'is-active': index === activeIndex}"
......@@ -16,7 +16,7 @@ export default {
props: {
// 当前选择的PPT
pptIndex: { type: Number, default: 0 },
data: { type: Array, default: () => [] }
ppts: { type: Array, default: () => [] }
},
data() {
return {
......
<template>
<div class="upload">
<el-upload action :show-file-list="false" :http-request="httpRequest">
<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>
......@@ -15,6 +15,17 @@
<i class="el-icon-document"></i>
{{ fileUrl | fileName }}
</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="fileUrl" :download="fileUrl | fileName" target="_blank">
<el-tooltip effect="dark" content="下载">
<i class="el-icon-download"></i>
......@@ -23,6 +34,7 @@
</div>
</div>
</div>
</div>
</template>
<script>
......@@ -31,7 +43,8 @@ import * as api from '../../api'
export default {
name: 'VUpload',
props: {
value: { type: [String, Array] }
value: { type: [String, Array] },
disabled: { type: Boolean, default: false }
},
data() {
return {
......@@ -39,9 +52,18 @@ export default {
}
},
watch: {
value(value) {
value: {
immediate: true,
handler(value) {
if (value) {
this.fileList = Array.isArray(value) ? value : [value]
if (Array.isArray(value)) {
this.fileList = value.map(item => {
return item.url || item
})
} else {
this.fileList = [value]
}
}
}
}
},
......@@ -63,6 +85,10 @@ export default {
}
}
})
},
handleRemove(index) {
this.fileList.splice(index, 1)
this.$emit('input', Array.isArray(this.value) ? this.fileList : '')
}
}
}
......
......@@ -13,6 +13,7 @@
// 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' // 课程大作业
......@@ -25,6 +26,7 @@ export default {
ChapterPlayer,
ChapterWork,
ChapterRead,
ChapterExam,
ChapterLive,
CourseWork,
CourseRead,
......@@ -45,6 +47,7 @@ export default {
3: 'ChapterWork', // 作业
4: 'ChapterRead', // 资料
5: 'ChapterLive', // 直播
9: 'ChapterExam', // 考试
99: 'CourseWork', // 课程大作业
100: 'CourseRead', // 课程资料
101: 'CourseExam' // 课程考试
......
......@@ -7,15 +7,18 @@
:isSkip="isSkip"
: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="pptVisible = false"
@close="onPPTClose"
@fullscreen="onPPTFullscreen"
@videoSyncTime="onVideoSyncTime"
></ppt-player>
</div>
</div>
......@@ -30,6 +33,8 @@
</template>
<script>
import Cookies from 'js-cookie'
import { throttle } from 'lodash'
// api
import * as api from '../../api'
// components
......@@ -46,11 +51,23 @@ export default {
pptIndex: { type: Number, default: 0 }
},
data() {
// 是否跳过片头
const isSkip = window.localStorage.getItem('isSkip') === 'true'
return {
videoVisible: true,
pptVisible: false,
isSkip: false,
chatperResources: null
isSkip,
chatperResources: null,
throttled: null,
throttleWait: 10, // 秒
progress: {
cpt: 0, // 当前播放时间
mpt: 0, // 视频时长
progress: 0, // 进度
pt: 0 // 累计播放时间
},
player: null,
watchedTimePoint: [] // 视频观看的时间点
}
},
watch: {
......@@ -59,6 +76,14 @@ export default {
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 视频资源ID
resourceId() {
return this.chapter.resource_id
......@@ -95,11 +120,29 @@ export default {
// 始终跳过片头
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) {
const ppts = this.chatperResources.ppts || []
......@@ -108,12 +151,29 @@ export default {
)
index = index !== -1 ? index - 1 : ppts.length - 1
this.$emit('change-ppt', index)
const durations = this.player.getDuration()
// 更新视频时间
this.progress.cpt = parseInt(time)
// 更新视频时长
this.progress.mpt = parseInt(durations)
const hasTimePoint = this.watchedTimePoint.includes(this.progress.cpt)
if (!hasTimePoint) {
this.watchedTimePoint.push(this.progress.cpt)
}
// 更新视频进度,10秒更新一次
if (this.throttled) {
this.throttled(time, durations)
} else {
this.throttled = throttle(
this.updateChapterVideoProgress,
this.throttleWait * 1000
)
}
},
// 更新视频当前播放时间
updateVideoCurrentTime() {
const player = this.$refs.videoPlayer.player
const ppt = this.chatperResources.ppts[this.pptIndex]
ppt && player.seek(ppt.ppt_point) // 增加2秒
ppt && this.player.seek(ppt.ppt_point) // 增加2秒
},
// 获取章节视频详情
getChapterVideo() {
......@@ -125,14 +185,66 @@ export default {
})
} else {
api.getChapterVideo(this.resourceId).then(response => {
this.chatperResources = response
Array.isArray(response.ppts) && this.$emit('pptupdate', response.ppts)
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: Cookies.get('_idt')
})
.then(response => {
this.progress = response
// 跳转播放进度
if (this.player && response.cpt) {
this.player.seek(response.cpt)
}
})
},
// 更新章节视频进度
updateChapterVideoProgress(time, durations) {
this.progress.pt += this.throttleWait
// 登录用户信息
const user = window.G.UserInfo
const params = {
sid: user.student_info.id,
uid: user.uid,
d: Cookies.get('_idt'),
i: Cookies.get('_idt'),
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 = []
}
},
beforeMount() {
// 获取视频
this.getChapterVideo()
// 获取视频进度
this.getChapterVideoProgress()
}
}
</script>
......@@ -148,6 +260,7 @@ export default {
.player-main {
display: flex;
flex: 1;
overflow: hidden;
}
.player-column {
flex: 1;
......
......@@ -19,10 +19,18 @@
<span>{{ppts.length}}</span>
</div>
<div class="ppt-player-controls__tools">
<i :class="['el-icon-self-xuexiao', (currentSync ? 'active' : '')]" @click="onToggleSync"></i>
<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>
......@@ -34,22 +42,23 @@ export default {
name: 'ppt-player',
props: {
ppts: { type: Array },
index: { type: Number, default: 0 },
isSync: { type: Boolean, default: false }
index: { type: Number, default: 0 }
},
data() {
return {
currentIndex: this.index,
currentSync: this.isSync,
isSync: true,
isFullscreen: false
}
},
watch: {
index: {
handler(value) {
if (this.isSync) {
this.currentIndex = value
}
}
}
},
computed: {
pptUrl() {
......@@ -67,20 +76,18 @@ export default {
},
prev() {
this.currentIndex = this.getIndex(this.currentIndex - 1)
this.currentSync = false
this.isSync = false
},
next(e) {
this.currentIndex = this.getIndex(this.currentIndex + 1)
this.currentSync = false
this.isSync = false
},
onToggleSync(e) {
this.currentSync = !this.currentSync
this.currentIndex = this.currentSync
? this.currentIndex
: this.currentIndex
this.isSync = !this.isSync
},
setVideoTime(e) {
this.$emit('onVideoSyncTime', this.ppts[this.currentIndex].ppt_point)
this.isSync = true
this.$emit('videoSyncTime', this.ppts[this.currentIndex].ppt_point)
},
// 全屏
fullscreen() {
......
......@@ -13,13 +13,22 @@ export default {
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({ FD, LD, SD }),
width: '100%',
height: '100%',
autoplay: true,
autoplay: false,
isLive: false,
controlBarVisibility: 'always',
components: [
......@@ -30,6 +39,11 @@ export default {
]
},
function(player) {
player.on('ready', function() {
// 跳过片头
_this.isSkip && player.seek(6)
_this.$emit('ready', player)
})
player.on('sourceloaded', function(params) {
const paramData = params.paramData
const desc = paramData.desc
......
......@@ -6,7 +6,7 @@
<i class="el-icon-document"></i>
{{ file.file_name }}
</a>
<span v-if="file.file_size">{{ file.file_size }}</span>
<!-- <span v-if="file.file_size">{{ file.file_size }}K</span> -->
<a :href="file.file_url" :download="file.file_name" target="_blank">
<el-tooltip effect="dark" content="下载">
<i class="el-icon-download"></i>
......
<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 unorderedQuestions"
: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" @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
}
},
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
.getChapterExam(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) {
api.sbumitChapterExam(params).then(response => {
if (response.status) {
this.getDetail()
} else {
this.$message.error(response.data.error)
}
})
}
},
beforeMount() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
width: 240px;
margin: 40px auto;
}
}
</style>
......@@ -214,12 +214,12 @@ export default {
)
// 提交的答案数据
const answers = this.questions.map(item => {
if (item.question_type === 3) {
item.formModel.user_answer = Base64.encode(item.formModel.user_answer)
}
return {
question_id: item.id,
descreption: item.formModel.user_answer,
descreption:
item.question_type === 3
? Base64.encode(item.formModel.user_answer)
: item.formModel.user_answer,
file_url: item.formModel.attachments,
is_encoded: 1
}
......
<template>
<container :title="detail.title" v-loading="loading">
<template v-slot:header-aside v-if="isExamComplete">分数:{{exam.score.total}}</template>
<div class="exam">
<template v-if="status.examination_status === '00'">
<div class="no-exam">暂无考试</div>
......@@ -17,32 +18,12 @@
@click="onStartExam"
>{{startExamButtonText}}</el-button>
</div>
<!-- 考试完成 -->
<div class="exam-finish" v-if="isExamComplete">
<table class="exam-table">
<tr>
<th>单选</th>
<th>多选</th>
<th>简答</th>
</tr>
<tr>
<td>{{exam.score.radio}}</td>
<td>{{exam.score.checkbox}}</td>
<td>{{exam.score.shortAnswer}}</td>
</tr>
<tr>
<td colspan="3">
<div class="exam-total">总分:{{exam.score.total}}</div>
</td>
</tr>
</table>
<el-button type="text" @click="examVisible = !examVisible">查看试卷</el-button>
</div>
<!-- 考试试题 -->
<div class="exam-form" v-if="isStartExam" v-show="examVisible">
<div class="exam-form" v-if="isStartExam">
<el-form :disabled="isSubmited">
<template v-for="items in questions">
<exam-item
v-for="(item, index) in questions"
v-for="(item, index) in items"
:index="index"
:type="item.type"
:data="item"
......@@ -50,6 +31,7 @@
:disabled="isSubmited"
:key="item.id"
></exam-item>
</template>
<div class="exam-buttons">
<el-tooltip effect="dark" content="提交之后就不能修改了哦" placement="right">
<el-button type="primary" @click="onSubmit">{{submitText}}</el-button>
......@@ -97,18 +79,11 @@ export default {
detail: {},
status: {},
questions: [],
values: [], // 提交的答案
messageInstance: null,
exam: {},
isStartExam: false, // 是否开始考试
autoSubmitTimer: null, // 自动提交定时器
checkStatusTimer: null, // 考试状态定时器
examVisible: true
}
},
watch: {
isExamComplete(value) {
this.examVisible = !value
checkStatusTimer: null // 考试状态定时器
}
},
computed: {
......@@ -163,7 +138,7 @@ export default {
this.detail = Array.isArray(response) ? null : response
// 设置问题列表数据
this.questions = this.detail
? this.genQuenstions(this.detail.examination)
? this.genQuestions(this.detail.examination)
: []
callback && callback()
})
......@@ -172,10 +147,11 @@ export default {
})
},
// 组装问题数据
genQuenstions(data) {
if (!data) {
return
genQuestions(list) {
if (!list) {
return []
}
return list.map(data => {
let { radioList, checkboxList, shortAnswerList } = data
// 单选
radioList = radioList.map(item => {
......@@ -200,14 +176,15 @@ export default {
formModel: {
id: item.id,
user_answer: item.user_answer
? Base64.decode(item.user_answer)
? Base64.decode(item.user_answer.replace(/ /gi, '+'))
: '',
attachments: []
attachments: item.attachments || []
}
}
return Object.assign({}, item, temp)
})
return [...radioList, ...checkboxList, ...shortAnswerList]
})
},
// 获取考试状态
getExamStatus() {
......@@ -234,7 +211,7 @@ export default {
if (response.code !== 8001) {
this.isStartExam = true
this.exam = response
this.questions = this.genQuenstions(response.sheet)
this.questions = this.genQuestions(response.sheet)
// 自动提交
if (this.isStartExam && !this.isSubmited && !this.isExamComplete) {
this.autoSubmit()
......@@ -244,13 +221,15 @@ export default {
},
// 提交校验
checkSubmit() {
const quenstions = this.questions
for (let i = 0; i < quenstions.length; i++) {
const value = quenstions[i].formModel.user_answer
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
},
// 提交
......@@ -262,12 +241,7 @@ export default {
return
}
// 提交的答案数据
const answers = this.questions.map(item => {
if (item.type === 3) {
item.formModel.user_answer = Base64.encode(item.formModel.user_answer)
}
return item.formModel
})
const answers = this.handleSubmitData()
// 提交参数
const params = { answers: JSON.stringify(answers), type: 1 }
// 请求接口
......@@ -279,19 +253,38 @@ export default {
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.autoSubmitTimer = setInterval(() => {
// 提交的答案数据
const answers = this.questions.map(item => {
if (item.type === 3) {
item.formModel.user_answer = Base64.encode(
item.formModel.user_answer
)
}
return item.formModel
})
const answers = this.handleSubmitData()
const params = { answers: JSON.stringify(answers), type: 0 }
// 请求接口
this.handleSubmitRequest(params)
}, 10000)
},
// 处理请求接口答案数据
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) {
api
......@@ -344,27 +337,6 @@ export default {
font-size: 30px;
text-align: center;
}
.exam-finish {
margin: 40px 0;
}
.exam-table {
width: 100%;
border-collapse: collapse;
th {
background-color: #ccc;
}
td,
th {
padding: 10px;
border: 1px solid #999;
text-align: center;
}
}
.exam-total {
font-size: 18px;
text-align: right;
padding: 0 40px;
}
.exam-welcome {
padding: 40px;
line-height: 30px;
......
......@@ -109,7 +109,7 @@ export default {
rules: {
essay_name: [
{ required: true, message: '请输入主题', trigger: 'blur' },
{ max: 5, message: '最多输入 50 个字符', trigger: 'blur' }
{ max: 50, message: '最多输入 50 个字符', trigger: 'blur' }
],
essay_description: [
{ required: true, message: '请输入正文', trigger: 'blur' }
......
......@@ -2,8 +2,11 @@
<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">{{data.title}}</div>
<div class="q-item-aside" v-if="typeText">({{typeText}})</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">
<!-- 单选 -->
......@@ -21,17 +24,18 @@
<!-- 简答题 -->
<template v-if="type === 3">
<v-editor v-model="currentValue.user_answer" :disabled="disabled"></v-editor>
<v-upload v-model="currentValue.attachments">请上传对应的文件附件:</v-upload>
<v-upload :disabled="disabled" v-model="currentValue.attachments">请上传对应的文件附件:</v-upload>
</template>
</div>
<div class="q-item-ft" v-if="disabled">
<template v-if="type === 3">
<p>
<span>老师评语:</span>
<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>
......@@ -40,7 +44,24 @@
<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>
......@@ -77,7 +98,8 @@ export default {
},
data() {
return {
currentValue: {}
currentValue: {},
showAnalyze: false
}
},
watch: {
......@@ -116,6 +138,14 @@ export default {
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
})
},
......@@ -190,14 +220,16 @@ export default {
}
.q-item-title {
flex: 1;
padding: 0 10px;
::v-deep img {
max-width: 100%;
}
}
.q-item-aside {
padding-left: 20px;
// align-self: flex-end;
}
.q-option-item {
padding-left: 30px;
padding-left: 20px;
margin-bottom: 14px;
}
.is-success {
......@@ -229,13 +261,36 @@ export default {
}
}
.q-item-ft {
padding: 10px 0;
p {
font-size: 14px;
margin: 0 0 10px 0;
}
.result {
display: flex;
justify-content: flex-end;
padding: 10px 0;
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-left: 20px;
padding: 0;
max-width: 100%;
}
}
}
</style>
......@@ -12,11 +12,11 @@
<script>
// componets
import ChapterWork from './chapterWork.vue'
import ChapterExam from './chapterExam.vue'
import ChapterTest from './ChapterTest.vue'
export default {
name: 'ViewerWork',
components: { ChapterWork, ChapterExam },
components: { ChapterWork, ChapterTest },
props: {
// 当前选中的
chapter: {
......@@ -36,7 +36,7 @@ export default {
computed: {
currentCompoent() {
const componentNames = {
1: 'ChapterExam', // 考试
1: 'ChapterTest', // 课后测验
2: 'ChapterWork' // 作业
}
const homework = this.chapter.homework
......
......@@ -9,7 +9,7 @@
<h1 class="course-viewer-main-hd__title">{{ detail.course_name }}</h1>
<!-- 直播的时候显示帮助按钮 -->
<template v-if="isLive">
<router-link to="/app/account/feedbackCreate" target="_blank">
<router-link to="/app/feedback/feedback-create" target="_blank">
<el-tooltip effect="light" content="意见反馈">
<i class="el-icon-self-fankuiyijian"></i>
</el-tooltip>
......@@ -35,6 +35,7 @@
</div>
<!-- 侧边栏 -->
<v-aside
:data="detail"
:chapters="chapters"
:active="activeChapter"
:ppts="ppts"
......@@ -92,10 +93,17 @@ export default {
children: [
{ name: '课程大作业', id: 'course_work', type: 99 },
{ name: '课程资料', id: 'course_info', type: 100 },
{ name: '教学评估', id: 'teach_evaluation', type: 102 },
{ name: '课程考试', id: 'course_exam', type: 101 }
{ 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
},
......
......@@ -193,7 +193,7 @@ export default {
},
/* 直接进直播 */
goLive () {
this.$router.push({ path: `/player/${this.newLiveMsg.semester_id}/${this.newLiveMsg.course_id}/live/${this.newLiveMsg.live.id}` })
this.$router.push({ name: 'viewerCourseChapter', params: { sid: this.newLiveMsg.semester_id, cid: this.newLiveMsg.course_id, id: this.newLiveMsg.chapter_id } })
}
}
}
......
// import viewerRoutes from '@/modules/viewer/routes.js'
import viewerRoutes from '@/modules/viewer/routes.js'
export default [
{ path: '/', redirect: '/app/learn/course' },
......@@ -291,7 +291,7 @@ export default [
// /* survey-phone 内未找到页面时 - 指向 */
// { path: '/survey-phone/*', redirect: '/learn-error/learn-error' },
/* 如果所有页面都没找到 - 指向 */
{ path: '*', component: () => import('@/components/errorPages/404.vue') }
{ path: '*', component: () => import('@/components/errorPages/404.vue') },
// viewer module routes
// ...viewerRoutes
...viewerRoutes
]
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论