提交 c63d00a8 authored 作者: lihuihui's avatar lihuihui
<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
}
},
methods: {
createEditor() {
const editor = (this.ckEditor = CKEDITOR.replace(this.textareaElementId, {
height: 400,
uiColor: '#eeeeee',
filebrowserImageUploadUrl: '/api/ckeditor/img/upload',
// 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'] }
]
}))
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')
}
})
})
},
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 :show-file-list="false" :http-request="httpRequest">
<slot></slot>
<el-button type="text" size="small" 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="value">
<div class="file-list-item">
<a :href="value" target="_blank">
<i class="el-icon-document"></i>
{{ fileName }}
</a>
<a :href="value" :download="fileName" target="_blank">
<el-tooltip effect="dark" content="下载">
<i class="el-icon-download"></i>
</el-tooltip>
</a>
</div>
</div>
</div>
</template>
<script>
import cAction from '@action'
export default {
name: 'VUpload',
props: {
value: { type: String }
},
data() {
return {}
},
computed: {
fileName() {
return this.value ? this.value.split('/').pop() : ''
}
},
methods: {
httpRequest(xhr) {
cAction.Player.uploadFile({ file: xhr.file })
.then(response => {
if (response.success) {
this.$emit('input', response.url)
}
})
.catch(error => {
console.log(error)
})
},
handleRemove() {
this.$emit('input', '')
}
}
}
</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: #b49441;
}
}
}
</style>
import BaseAPI from '@/api/base_api' import BaseAPI from '@/api/base_api'
const httpRequest = new BaseAPI(webConf) const httpRequest = new BaseAPI(webConf)
/** /**
* 获取课程详情 * 获取课程详情
* @param {string} courseId 课程ID
* @param {string} semesterId 学期ID * @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/ */
export function getCourse(courseId, semesterId) { export function getCourse(semesterId, courseId) {
return httpRequest.get(`/v2/education/courses/${courseId}/${semesterId}`) return httpRequest.get(`/v2/education/courses/${semesterId}/${courseId}`)
} }
/** /**
...@@ -33,3 +34,57 @@ export function getChapterVideoAliyun(vid) { ...@@ -33,3 +34,57 @@ export function getChapterVideoAliyun(vid) {
{ headers: { 'Content-Type': 'application/json' } } { headers: { 'Content-Type': 'application/json' } }
) )
} }
/**
* 获取答题信息
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} resourseId 章节的资源ID
*/
export function getChapterExam(semesterId, courseId, resourseId) {
return httpRequest.get(
`/v2/education/homeworks/${semesterId}/${courseId}/${resourseId}`
)
}
/**
* 提交考试
*/
export function sbumitChapterExam(params) {
return httpRequest.post('/v2/education/homeworks', params, {
headers: { 'Content-Type': 'application/json' }
})
}
/**
* 上传文件
*/
export function uploadFile(data) {
return httpRequest.post('/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(
`/v2/education/courses/${semesterId}/${courseId}/essay`
)
}
/**
* 提交课程大作业
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function updateCourseWork(semesterId, courseId, data) {
return httpRequest.post(
`/v2/education/courses/${semesterId}/${courseId}/essay`,
data,
{ headers: { 'Content-Type': 'multipart/form-data' } }
)
}
<template> <template>
<ul class="chapter-list"> <ul class="chapter-list">
<li class="chapter-item" v-for="item in list" :key="item.id"> <li class="chapter-item" v-for="item in data" :key="item.id">
<h4>{{item.name}}</h4> <h4>{{item.name}}</h4>
<ul class="knot-list"> <ul class="chapter-item-list">
<li v-for="subItem in item.children" :key="subItem.id" @click="onClick(subItem)"> <li
<span class="knot-name">{{subItem.name | showName(subItem.type)}}</span> 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.type)}}</span>
<i class="el-icon" :class="genIconClass(subItem.type)"></i>
</li> </li>
</ul> </ul>
</li> </li>
...@@ -14,12 +20,9 @@ ...@@ -14,12 +20,9 @@
<script> <script>
export default { export default {
props: { props: {
data: { data: { type: Array, default: () => [] },
type: Array, // 当前选中的章节
default() { active: { type: Object, default: () => {} }
return []
}
}
}, },
data() { data() {
return { return {
...@@ -46,8 +49,29 @@ export default { ...@@ -46,8 +49,29 @@ export default {
} }
}, },
methods: { 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) { onClick(data) {
console.log(data) // 课程大作业
// if (data.id === 'course_work') {
// this.$router.push({ name: 'viewerCourseWork' })
// return
// }
// 课程资料
// if (data.id === 'course_info') {
// this.$router.push({ name: 'viewerCourseFile' })
// return
// }
this.$router.push({
name: 'viewerCourseChapter',
params: { id: data.id }
})
} }
} }
} }
...@@ -62,23 +86,23 @@ export default { ...@@ -62,23 +86,23 @@ export default {
overflow: hidden; overflow: hidden;
.chapter-item { .chapter-item {
h4 { h4 {
padding: 10px 32px; padding: 10px 22px;
margin: 0; margin: 0;
font-size: 15px; font-size: 15px;
color: #b0b0b0; color: #b0b0b0;
background-color: #2f2f2f; background-color: #2f2f2f;
} }
/* 节列表样式 */ /* 节列表样式 */
.knot-list { .chapter-item-list {
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 1.6; line-height: 1.6;
overflow: hidden; overflow: hidden;
li { li {
position: relative; position: relative;
&.on { &.is-active {
background: #3c3c3c; background: #3c3c3c;
a { .chapter-item-list__name {
color: #b49441; color: #b49441;
} }
} }
...@@ -110,7 +134,7 @@ export default { ...@@ -110,7 +134,7 @@ export default {
background: #616161; background: #616161;
} }
} }
.knot-name { .chapter-item-list__name {
display: block; display: block;
padding: 15px 35px 15px 40px; padding: 15px 35px 15px 40px;
font-size: 14px; font-size: 14px;
...@@ -126,6 +150,7 @@ export default { ...@@ -126,6 +150,7 @@ export default {
right: 10px; right: 10px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
color: #a0a0a0;
} }
} }
} }
......
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
<el-tabs v-model="activeName"> <el-tabs v-model="activeName">
<el-tab-pane label="章节" name="0"> <el-tab-pane label="章节" name="0">
<div class="tab-pane"> <div class="tab-pane">
<aside-chapter :data="chapters"></aside-chapter> <aside-chapter :data="chapters" :active="active"></aside-chapter>
</div> </div>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="讲义" name="1"> <el-tab-pane label="讲义" name="1" v-if="active && active.type === 2">
<div class="tab-pane"> <div class="tab-pane">
<aside-lecture :data="ppts"></aside-lecture> <aside-lecture :data="ppts" :pptIndex="pptIndex" v-on="$listeners"></aside-lecture>
</div> </div>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
...@@ -16,25 +16,19 @@ ...@@ -16,25 +16,19 @@
</template> </template>
<script> <script>
import AsideChapter from './asideChapter.vue' import AsideChapter from './chapter.vue'
import AsideLecture from './asideLecture.vue' import AsideLecture from './lecture.vue'
export default { export default {
props: { props: {
// 章节 // 章节
chapters: { chapters: { type: Array, default: () => [] },
type: Array,
default() {
return []
}
},
// 讲义 // 讲义
ppts: { ppts: { type: Array, default: () => [] },
type: Array, // 当前选中的章节
default() { active: { type: Object, default: () => {} },
return [] // 当前选择的PPT
} pptIndex: { type: Number, default: 0 }
}
}, },
components: { AsideChapter, AsideLecture }, components: { AsideChapter, AsideLecture },
data() { data() {
......
<template> <template>
<ul class="lecture-list"> <ul class="lecture-list">
<li v-for="item in data" :key="item.id" @click="onClick(item)"> <li
v-for="(item, index) in data"
:key="item.id"
@click="onClick(index)"
:class="{'is-active': index === activeIndex}"
>
<img :src="item.ppt_url" /> <img :src="item.ppt_url" />
</li> </li>
</ul> </ul>
...@@ -9,23 +14,25 @@ ...@@ -9,23 +14,25 @@
<script> <script>
export default { export default {
props: { props: {
data: { // 当前选择的PPT
type: Array, pptIndex: { type: Number, default: 0 },
default() { data: { type: Array, default: () => [] }
return []
}
}
}, },
data() { data() {
return { return {
activeIndex: 0 activeIndex: this.pptIndex
}
},
watch: {
pptIndex(index) {
this.activeIndex = index
} }
}, },
methods: { methods: {
// 点击PPT // 点击PPT
onClick(data) { onClick(index) {
this.activeIndex = data.id this.activeIndex = index
this.$emit('clickPPT', data) this.$emit('change-ppt', index)
} }
} }
} }
......
<template>
<div class="course-viewer-content">
<div class="course-viewer-content-hd">
<h3 class="course-viewer-content-hd__title">{{title}}</h3>
</div>
<div class="course-viewer-content-bd">
<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>
{{ file.file_name }}
</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>
</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 {
text-decoration: none;
color: #333;
&:hover {
color: #b49441;
}
}
}
.empty {
font-size: 18px;
line-height: 80px;
background-color: #fff;
text-align: center;
border-radius: 40px;
}
</style>
<template>
<file-panel :title="chapter.name" :files="files"></file-panel>
</template>
<script>
import FilePanel from './filePanel.vue'
export default {
name: 'ViewerFile',
components: { FilePanel },
props: {
// 当前选中的
chapter: { type: Object, default: () => {} },
// 课程详情接口返回的数据
data: { type: Object, default: () => {} }
},
computed: {
files() {
// 课程资料
if (this.chapter.id === 'course_info') {
return this.data.files || []
}
// 章节资料
if (this.chapter.reading) {
const reading = this.chapter.reading
const file = {
file_name: reading.reading_content,
file_url: reading.reading_attachment
}
return [file]
}
return []
}
}
}
</script>
<template>
<component
:is="currentCompoent"
:chapter="chapter"
v-bind="$attrs"
v-on="$listeners"
v-if="chapter"
:key="pid"
/>
</template>
<script>
import VPlayer from './player/index.vue'
import VWork from './work/index.vue'
import VFile from './file/index.vue'
export default {
name: 'ViewerLayout',
components: { VPlayer, VWork, VFile },
props: {
chapter: { type: Object, default: () => {} }
},
computed: {
currentCompoent() {
const componentNames = {
2: 'VPlayer', // 视频
3: 'VWork', // 作业
4: 'VFile' // 资料
}
return this.chapter ? componentNames[this.chapter.type] || '' : ''
},
pid() {
return this.$route.params.id
}
}
}
</script>
<template> <template>
<div class="player"> <div class="player" v-if="chatperResources">
<div class="player-main"> <div class="player-main">
<div class="player-column" v-show="videoVisible"> <div class="player-column" v-show="videoVisible">
<!-- 视频 --> <!-- 视频 -->
<video-player :video="video"></video-player> <video-player
:isSkip="isSkip"
:video="chatperResources.video"
@timeupdate="onTimeupdate"
ref="videoPlayer"
></video-player>
</div> </div>
<div class="player-column" v-if="pptVisible"> <div class="player-column" v-if="pptVisible">
<!-- ppt --> <!-- ppt -->
<ppt-player :ppts="ppts" @close="pptVisible = false" @fullscreen="onPPTFullscreen"></ppt-player> <ppt-player
:ppts="chatperResources.ppts"
@close="pptVisible = false"
@fullscreen="onPPTFullscreen"
></ppt-player>
</div> </div>
</div> </div>
<div class="player-footer"> <div class="player-footer">
<em class="player-button player-button-download" v-if="pdf"> <em class="player-button player-button-download" v-if="chapter.pdf">
<a :href="pdf" target="_blank">下载PPT</a> <a :href="chapter.pdf" download target="_blank">下载PPT</a>
</em> </em>
<em :class="pptClass" @click="togglePPTVisible" v-if="ppts.length">同步显示PPT</em> <em :class="pptClass" @click="togglePPTVisible" v-if="chatperResources.ppts.length">同步显示PPT</em>
<em :class="skipClass" @click="toggleSkip">始终跳过片头</em> <em :class="skipClass" @click="toggleSkip">始终跳过片头</em>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
// api
import * as api from '../../api/index'
// components
import videoPlayer from './videoPlayer.vue' import videoPlayer from './videoPlayer.vue'
import pptPlayer from './pptPlayer.vue' import pptPlayer from './pptPlayer.vue'
export default { export default {
name: 'Player', name: 'ViewerPlayer',
components: { videoPlayer, pptPlayer }, components: { videoPlayer, pptPlayer },
props: { props: {
video: { type: Object }, // 当前章节
pdf: { type: String }, chapter: { type: Object },
ppts: { // PPT当前选中的索引
type: Array, pptIndex: { type: Number, default: 0 }
default() {
return []
}
}
}, },
data() { data() {
return { return {
videoVisible: true, videoVisible: true,
pptVisible: false, pptVisible: false,
isSkip: false isSkip: false,
chatperResources: null
}
},
watch: {
pptIndex(index) {
this.updateVideoCurrentTime(index)
} }
}, },
computed: { computed: {
// 视频资源ID
resourceId() {
return this.chapter.resource_id
},
/**
* 视频提供者
* @return 1是CC加密; 2是非加密; 3是阿里云
*/
videoProvider() {
const video = this.chapter.video || {}
return video.video_provider || 3
},
pptClass() { pptClass() {
return { return {
'player-button': true, 'player-button': true,
...@@ -72,7 +98,40 @@ export default { ...@@ -72,7 +98,40 @@ export default {
// PPT全屏 // PPT全屏
onPPTFullscreen(value) { onPPTFullscreen(value) {
this.videoVisible = !value this.videoVisible = !value
},
// 当前播放时间更新
onTimeupdate(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)
},
// 更新视频当前播放时间
updateVideoCurrentTime() {
const player = this.$refs.videoPlayer.player
const ppt = this.chatperResources.ppts[this.pptIndex]
ppt && 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 => {
this.chatperResources = response
Array.isArray(response.ppts) && this.$emit('pptupdate', response.ppts)
})
}
} }
},
beforeMount() {
this.getChapterVideo()
} }
} }
</script> </script>
...@@ -83,6 +142,7 @@ export default { ...@@ -83,6 +142,7 @@ export default {
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: #3f3f3f;
} }
.player-main { .player-main {
display: flex; display: flex;
......
...@@ -5,12 +5,13 @@ ...@@ -5,12 +5,13 @@
<script> <script>
export default { export default {
name: 'VideoPlayer', name: 'VideoPlayer',
props: { video: Object }, props: { isSkip: Boolean, video: Object },
data() { data() {
return { player: null } return { player: null }
}, },
methods: { methods: {
createPlayer() { createPlayer() {
const _this = this
const { FD, LD, SD } = this.video const { FD, LD, SD } = this.video
this.player = new Aliplayer( this.player = new Aliplayer(
{ {
...@@ -29,16 +30,17 @@ export default { ...@@ -29,16 +30,17 @@ export default {
] ]
}, },
function(player) { function(player) {
console.log('The player is created')
/* Register the sourceloaded of the player, query the resolution of the video, invoke the resolution component, and call the setCurrentQuality method to set the resolution. */
player.on('sourceloaded', function(params) { player.on('sourceloaded', function(params) {
var paramData = params.paramData const paramData = params.paramData
var desc = paramData.desc const desc = paramData.desc
var definition = paramData.definition const definition = paramData.definition
player player
.getComponent('QualityComponent') .getComponent('QualityComponent')
.setCurrentQuality(desc, definition) .setCurrentQuality(desc, definition)
}) })
player.on('timeupdate', function(event) {
_this.$emit('timeupdate', player.getCurrentTime())
})
} }
) )
} }
......
<template>
<div class="course-viewer-content" v-loading="loading">
<div class="course-viewer-content-hd">
<h3 class="course-viewer-content-hd__title">{{chapter.name}}</h3>
<div class="course-viewer-content-hd__aside" v-if="isSubmited">正确率:{{detail.score}}%</div>
</div>
<div class="course-viewer-content-bd">
<div class="exam">
<div class="exam-form">
<el-form :disabled="isSubmited" size="medium">
<chapter-exam-item
v-for="(item, index) in unorderedQuestions"
:disabled="isSubmited"
:data="item"
:value="findAnswerById(item.id)"
:index="index"
:key="item.id"
@change="onChange(item.id, ...arguments)"
></chapter-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>
</div>
</div>
</template>
<script>
// libs
import { shuffle } from 'lodash'
// components
import ChapterExamItem from './chapterExamItem.vue'
// api
import * as api from '../../api/index'
export default {
name: 'ChapterExam',
components: { ChapterExamItem },
props: {
// 当前选中的章节
chapter: { type: Object, default: () => {} }
},
data() {
return {
loading: false,
detail: null,
values: [], // 提交的答案
startTime: new Date().getTime(), // 进入时间
messageInstance: null
}
},
computed: {
// 学期ID
sid() {
return '6552021107166150656'
},
// 课程ID
cid() {
return '6568035374902280192'
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 资源ID
resourceId() {
return this.chapter.resource_id
},
// 问题列表
questions() {
const homework = this.chapter.homework
return homework ? homework.questions : []
},
// 打乱顺序的问题列表
unorderedQuestions() {
const ids = this.questions.map(item => item.id)
const sortIds = shuffle(ids)
return sortIds.map(id => this.questions.find(item => item.id === id))
},
/**
* 解析用户提交的数据
* @return [{{ question_id: 'xxx', value: ['xxx', 'xxx'] }}]
*/
answers() {
const answers = this.isSubmited
? JSON.parse(this.detail.work_contents)
: []
return answers.map(item => {
const ids = item.options.reduce((result, subitem) => {
subitem.selected && result.push(subitem.id)
return result
}, [])
return { question_id: item.question_id, value: ids }
})
},
// 是否提交
isSubmited() {
return this.detail ? !!this.detail.work_contents : false
},
// 提交按钮文本
submitText() {
return this.isSubmited ? '已提交' : '提交'
}
},
methods: {
// 获取测试答题详情
getChapterExam() {
this.loading = true
api
.getChapterExam(this.sid, this.cid, this.resourceId)
.then(response => {
this.detail = Array.isArray(response) ? null : response
})
.finally(() => {
this.loading = false
})
},
// 通过问题ID查找答案
findAnswerById(id) {
const found = this.answers.find(item => item.question_id === id)
return found ? found.value : []
},
onChange(qid, value) {
const index = this.values.findIndex(item => item.question_id === qid)
if (index === -1) {
this.values.push({ question_id: qid, value })
} else {
this.values.splice(index, 1, { question_id: qid, value })
}
},
// 提交校验
checkSubmit() {
if (this.values.length !== this.questions.length) {
return false
}
const values = this.values
for (let i = 0; i < values.length; i++) {
const options = values[i].value
if (!options.length) {
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 => {
// 查找提交的option id
const found = this.values.find(
subitem => subitem.question_id === item.id
)
const ids = found ? found.value : []
// 解析
const parseOptions = JSON.parse(item.question_options)
// 设置提交选中状态
let isCorrect = true
const options = parseOptions.map(option => {
option.selected = ids.includes(option.id) ? 1 : 0
if (option.checked !== !!option.selected && isCorrect) {
isCorrect = false
}
return option
})
return {
question_id: item.id,
is_correct: isCorrect ? 1 : 0,
options
}
})
return result
},
// 请求提交接口
handleSubmitRequest(params) {
api.sbumitChapterExam(params).then(response => {
if (response.status) {
this.getChapterExam()
} else {
this.$message.error(response.data.error)
}
})
}
},
beforeMount() {
this.getChapterExam()
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
width: 240px;
margin: 40px auto;
}
}
</style>
<template>
<div class="q-item">
<div class="q-item-hd">
<div class="q-item-num">{{currentIndex}}.</div>
<div class="q-item-title" v-html="data.question_content"></div>
<div class="q-item-aside">({{currentTypeText}})</div>
</div>
<div class="q-item-bd">
<!-- 单选 -->
<el-radio-group v-model="radioValue" @change="onRadioChange" v-if="currentType === 1">
<div class="q-option-item" v-for="item in currentOptions" :key="item.id">
<el-radio :class="genClass(item)" :label="item.id">{{item.abc_option}}</el-radio>
</div>
</el-radio-group>
<!-- 多选 -->
<el-checkbox-group
v-model="checkboxValue"
@change="onCheckboxChange"
v-if="currentType === 2"
>
<div class="q-option-item" v-for="item in currentOptions" :key="item.id">
<el-checkbox :class="genClass(item)" :label="item.id">{{item.abc_option}}</el-checkbox>
</div>
</el-checkbox-group>
</div>
<div class="q-item-ft" v-if="disabled">
<p>
<span>学生答案:</span>
<span :class="isCorrect ? 'is-success' : 'is-error'">{{submitAnswerText}}</span>
</p>
<p>
<span>正确答案:</span>
<span>{{correctAnswerText}}</span>
</p>
</div>
</div>
</template>
<script>
export default {
name: 'ChapterExamItem',
props: {
// 索引
index: { type: Number },
// 单条数据
data: { type: Object, default: () => {} },
// 提交的答案
value: { type: Array, default: () => [] },
// 是否禁用,提交过的是禁用状态
disabled: { type: Boolean, default: false }
},
data() {
return {
radioValue: '',
checkboxValue: []
}
},
watch: {
value: {
immediate: true,
handler(value) {
if (this.currentType === 1) {
this.radioValue = value[0] || ''
} else {
this.checkboxValue = value
}
}
}
},
computed: {
// 26个英文字母
A_Z() {
const result = []
for (let i = 0; i < 26; i++) {
result.push(String.fromCharCode(65 + i))
}
return result
},
// 序号
currentIndex() {
return this.index + 1
},
// 当前类型
currentType() {
return this.data.question_type
},
// 选项类型
currentTypeText() {
const map = { 1: '单选题', 2: '多选题' }
return map[this.currentType]
},
// 接口返回的options数据
options() {
return this.data.question_options
? JSON.parse(this.data.question_options)
: []
},
// 处理后的options数据
currentOptions() {
return this.options.map((item, index) => {
// 英文字母 + 名称
item.abc = this.A_Z[index]
item.abc_option = `${this.A_Z[index]}. ${item.option}`
// 提交时的选中状态
item.selected = this.value.includes(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) {
return null
}
return {
'is-error': item.selected !== item.checked,
'is-success': item.checked
}
},
// 单选
onRadioChange(value) {
this.$emit('change', [value])
},
// 多选
onCheckboxChange(value) {
this.$emit('change', value)
}
}
}
</script>
<style lang="scss" scoped>
.q-item {
font-size: 16px;
padding: 10px 0;
border-bottom: 1px solid #c9c9c97a;
}
.q-item-hd {
display: flex;
padding: 10px 0 20px;
::v-deep p {
margin: 0;
padding: 0;
}
}
.q-item-num {
width: 20px;
text-align: center;
}
.q-item-title {
flex: 1;
padding: 0 10px;
}
.q-item-aside {
padding-left: 20px;
// align-self: flex-end;
}
.q-option-item {
padding-left: 30px;
margin-bottom: 14px;
}
.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 {
display: flex;
justify-content: flex-end;
padding: 10px 0;
p {
font-size: 14px;
margin: 0;
padding-left: 20px;
}
}
</style>
<template>
<div></div>
</template>
<script>
export default {
name: 'ChapterWork'
}
</script>
<style lang="scss" scoped>
</style>
<template>
<div class="course-viewer-content" v-loading="loading">
<div class="course-viewer-content-hd">
<h3 class="course-viewer-content-hd__title">课程大作业</h3>
</div>
<div class="course-viewer-content-bd">
<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="主题"></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 label="附件" 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.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" @click="onSubmit" :disabled="isRevised">{{submitText}}</el-button>
</el-tooltip>
</div>
</template>
</el-step>
</el-steps>
</div>
</div>
</template>
<script>
// componetns
import VEditor from './editor.vue'
import VUpload from './upload.vue'
// api
import * as api from '../../api/index'
export default {
name: 'CourseWork',
components: { VEditor, VUpload },
props: {
// 课程详情接口返回的数据
data: { type: Object, default: () => {} }
},
data() {
return {
ruleForm: {
essay_name: '',
essay_description: '',
url: ''
},
rules: {
essay_name: [
{ required: true, message: '请输入主题', trigger: 'blur' },
{ max: 5, message: '最多输入 50 个字符', trigger: 'blur' }
],
essay_description: [
{ required: true, message: '请输入正文', trigger: 'blur' }
],
url: [{ required: true, message: '请上传附件', trigger: 'change' }]
},
detail: null,
loading: false,
messageInstance: null
}
},
computed: {
// 学期ID
sid() {
return '6552021107166150656'
},
// 课程ID
cid() {
return '6568035374902280192'
},
// 是否批改
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.$refs.ruleForm
.validate()
.then(response => {
const params = Object.assign(this.ruleForm, {
semester_id: this.sid,
course_id: this.cid
})
this.handleSubmitRequest(params)
})
.catch(() => {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error('还有题目未做,不能提交')
})
},
// 请求提交接口
handleSubmitRequest(params) {
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)
})
}
},
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;
}
</style>
<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
}
},
methods: {
createEditor() {
const editor = (this.ckEditor = CKEDITOR.replace(this.textareaElementId, {
height: 400,
uiColor: '#eeeeee',
filebrowserImageUploadUrl: '/api/ckeditor/img/upload',
// 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'] }
]
}))
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')
}
})
})
},
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>
<component
:is="currentCompoent"
:chapter="chapter"
:data="data"
v-bind="$attrs"
v-on="$listeners"
v-if="chapter"
/>
</template>
<script>
import CourseWork from './courseWork.vue'
import ChapterExam from './chapterExam.vue'
export default {
name: 'ViewerWork',
components: { CourseWork, ChapterExam },
props: {
// 当前选中的
chapter: { type: Object, default: () => {} },
// 课程详情接口返回的数据
data: { type: Object, default: () => {} }
},
computed: {
currentCompoent() {
const componentNames = {
1: 'ChapterExam', // 考试
2: 'CourseWork' // 作业
}
const homework = this.chapter.homework
return homework ? componentNames[homework.work_type] || '' : 'CourseWork'
}
}
}
</script>
<template>
<div class="upload">
<el-upload action :show-file-list="false" :http-request="httpRequest">
<el-button size="small" 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="value">
<div class="file-list-item">
<a :href="value" target="_blank">
<i class="el-icon-document"></i>
{{ fileName }}
</a>
<a :href="value" :download="fileName" target="_blank">
<el-tooltip effect="dark" content="下载">
<i class="el-icon-download"></i>
</el-tooltip>
</a>
</div>
</div>
</div>
</template>
<script>
import * as api from '../../api/index'
export default {
name: 'VUpload',
props: {
value: { type: String }
},
data() {
return {}
},
computed: {
fileName() {
return this.value ? this.value.split('/').pop() : ''
}
},
methods: {
httpRequest(xhr) {
api
.uploadFile({ file: xhr.file })
.then(response => {
if (response.success) {
this.$emit('input', response.url)
}
})
.catch(error => {
console.log(error)
})
},
handleRemove() {
this.$emit('input', '')
}
}
}
</script>
<style lang="scss" scoped>
.file-list-item {
display: flex;
margin-bottom: 10px;
padding: 0 10px;
justify-content: space-between;
background-color: #fff;
border-radius: 4px;
a {
text-decoration: none;
color: #333;
&:hover {
color: #b49441;
}
}
}
</style>
<template> <template>
<div class="course-viewer"> <div class="course-viewer">
<div class="course-viewer-main"> <div class="course-viewer-main">
<!-- 顶部区域 --> <!-- 顶部区域 -->
<div class="course-viewer-hd"> <div class="course-viewer-main-hd">
<router-link to="/mobile/help/student"> <router-link to="/mobile/help/student">
<i class="el-icon-arrow-left"></i> <i class="el-icon-arrow-left"></i>
</router-link> </router-link>
<h1 class="course-viewer-hd__title">{{detail.course_name}}</h1> <h1 class="course-viewer-main-hd__title">{{ detail.course_name }}</h1>
<router-link to="/app/account/feedbackCreate" target="_blank"> <router-link to="/app/account/feedbackCreate" target="_blank">
<el-tooltip effect="light" content="意见反馈"> <el-tooltip effect="light" content="意见反馈">
<i class="el-icon-self-fankuiyijian"></i> <i class="el-icon-self-fankuiyijian"></i>
...@@ -20,17 +19,27 @@ ...@@ -20,17 +19,27 @@
</router-link> </router-link>
</div> </div>
<!-- 主体区域 --> <!-- 主体区域 -->
<div class="course-viewer-bd"> <div class="course-viewer-main-bd">
<player <router-view
:video="chatperResources.video" :data="detail"
pdf="https://img1.ezijing.com/ppts/6437335122927681536/PPT_3.2%20%E6%A1%88%E4%BE%8B%E7%A0%94%E7%A9%B6%E6%96%B9%E6%B3%95%EF%BC%88%E4%B8%80%EF%BC%89.pdf" :chapter="activeChapter"
:ppts="chatperResources.ppts" :files="files"
v-if="chatperResources.video" :pptIndex="pptIndex"
:key="pid"
@pptupdate="handlePPTupdate"
@change-ppt="handleChangePPT"
/> />
</div> </div>
</div> </div>
<!-- 侧边栏 --> <!-- 侧边栏 -->
<v-aside :chapters="detail.chapters" :ppts="chatperResources.ppts"></v-aside> <v-aside
:chapters="chapters"
:active="activeChapter"
:ppts="ppts"
:pptIndex="pptIndex"
@change-ppt="handleChangePPT"
v-if="detail.chapters"
></v-aside>
</div> </div>
</template> </template>
...@@ -38,80 +47,115 @@ ...@@ -38,80 +47,115 @@
// api // api
import * as api from './api/index' import * as api from './api/index'
// components // components
import VAside from './components/aside/aside.vue' import VAside from './components/aside/index.vue'
import Player from './components/player/player.vue'
export default { export default {
name: 'CourseViewer', name: 'CourseViewer',
components: { VAside, Player }, components: { VAside },
data() { data() {
return { return {
detail: {}, detail: {},
chatperResources: {} ppts: [],
pptIndex: 0
} }
}, },
computed: { watch: {
// 当前章节
activeChapter() { activeChapter() {
return { this.ppts = []
resource_id: '6414747439944695808' this.pptIndex = 0
} }
},
computed: {
// 学期ID
sid() {
return '6552021107166150656'
}, },
// 视频资源ID // 课程ID
resourceId() { cid() {
return this.activeChapter.resource_id return '6568035374902280192'
}, },
/** // 当前页面的ID
* 视频提供者 pid() {
* @return 1是CC加密; 2是非加密; 3是阿里云 return this.$route.params.id
*/ },
videoProvider() { // 章节列表
const video = this.activeChapter.video || {} chapters() {
return video.video_provider || 3 const chapters = this.detail.chapters || []
return chapters.concat([
{
name: '大作业及资料',
children: [
{ name: '课程大作业', id: 'course_work', type: 3 },
{ name: '课程资料', id: 'course_info', type: 4 },
{ name: '教学评估', id: 'teach_evaluation' }
]
}
])
},
// 当前选中的章节
activeChapter() {
const id = this.pid
const list = this.chapters
return this.findChapter(id, list)
},
// 课程资料
files() {
return this.detail.files || []
} }
}, },
methods: { 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() { getCourse() {
api api.getCourse(this.sid, this.cid).then(response => {
.getCourse('6437296642994470912', '6437335122927681536') this.detail = response
.then(response => { })
this.detail = response
})
}, },
// 获取章节视频详情 // PPT列表更新
getChapterVideo() { handlePPTupdate(list) {
// 视频播放类型 1是CC加密; 2是非加密; 3是阿里云 this.ppts = list
if (this.videoProvider === 3) { },
api.getChapterVideoAliyun(this.resourceId).then(response => { // 右侧菜单选中的PPT修改
this.chatperResources = response handleChangePPT(index) {
}) this.pptIndex = index
} else {
api.getChapterVideo(this.resourceId).then(response => {
this.chatperResources = response
})
}
} }
}, },
beforeMount() { beforeMount() {
this.getCourse() this.getCourse()
this.getChapterVideo()
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
.course-viewer { .course-viewer {
display: flex; display: flex;
background-color: #3f3f3f; height: 100vh;
overflow: hidden;
} }
.course-viewer-main { .course-viewer-main {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.course-viewer-hd { .course-viewer-main-hd {
display: flex; display: flex;
align-items: center; align-items: center;
background-color: #3f3f3f;
height: 56px; height: 56px;
a { a {
color: #fff; color: #fff;
...@@ -121,13 +165,59 @@ export default { ...@@ -121,13 +165,59 @@ export default {
font-size: 24px; font-size: 24px;
} }
} }
.course-viewer-hd__title { .course-viewer-main-hd__title {
flex: 1; flex: 1;
font-size: 1.5em; font-size: 1.5em;
text-align: center; text-align: center;
color: #a0a0a0; color: #a0a0a0;
} }
.course-viewer-bd { .course-viewer-main-bd {
flex: 1; 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;
} }
</style> </style>
export default [
{
path: '/viewer',
component: () => import('./index.vue'),
children: [
{
name: 'viewerCourseChapter',
path: ':id',
component: () => import('./components/layout.vue')
}
]
}
]
<template> <template>
<div class="play-paper"> <div class="play-paper">
<div class="play-paper-body"> <div class="play-paper-body">
<div class="play-paper-title"><div><h3>{{chapterName}}</h3></div></div> <div class="play-paper-title">
<div class="play-paper-content play-chapter-work"> <div>
<template v-if="chapterWork.questions && chapterWork.questions.length" > <h3>{{chapterName}}</h3>
</div>
</div>
<div class="play-paper-content play-chapter-work">
<ul> <ul>
<template v-for="(item, index) in chapterWork.questions"> <li v-for="(item, index) in questions" :key="index">
<li v-bind:key="index"> <div class="work-number">{{index + 1}}.</div>
<div class="work-number">{{index + 1}}.</div> <div class="work-title">
<div class="work-title"> <div class="edit_html" v-html="item.question_content"></div>
<div class="edit_html" v-html="item.question_content"></div> </div>
</div> <!-- 文本内容 -->
<textarea id="editor-chapterWork"></textarea> <v-editor v-model="item.descreption"></v-editor>
<div style="height: 20px;"></div> <!-- 上传附件 -->
<!-- <el-upload <v-upload v-model="item.file_url">请上传对应的文件附件:</v-upload>
ref="upFile"
class="upload-demo"
action=""
:multiple="false"
:limit="1"
:show-file-list="false"
:on-change="handleChange"
:http-request="uploadFile"
:file-list="filesArr">
请上传对应的文件附件:<el-button type="text">点击上传</el-button>
<template v-if="successFileUrl">
{{successFileUrl.replace(/.*\/([^\/]*\.docx)$/gi, '$1')}}
</template>
</el-upload> -->
<template v-if="successFileUrl">
<a :href="successFileUrl">下载已上传文件</a>
</template>
<!-- <div style="height: 20px;"></div> -->
<!-- <p class="help help-file">只支持docx格式的文件,文件小于10M</p> -->
<!-- {answer.file_url && <a style={{display: 'block', marginBottom: '20px', color: 'blue'}} href={answer.file_url} >下载附件</a> } -->
</li> </li>
</template>
</ul> </ul>
</template> <template v-if="deadLine">
<template v-else> <p style="color: red">请于截止日期 {{deadLine}} 前提交</p>
<!-- <p class="no-data">暂无数据</p> -->
</template>
<!-- <p class="text-danger">{this.state.error}</p> -->
<template v-if="this.deadLine">
<p style="color: red">请于截止日期 {{this.deadLine}} 前提交</p>
</template>
<div class="area-btns">
<el-button type="primary" @click="submitWork" :disabled="!!homeData.checker_time || deadLineFlag">{{homeData.checker_time ? '已批改' : '提交'}}</el-button>
<span class="help-info">&emsp;&emsp;在获老师批改之前,可以多次提交,将以最后一次提交为准</span>
<template v-if="homeData.checker_time">
<div class="play-paper-check">
<h4>已获批改 <small>批改于{{homeData.checker_time}}</small></h4>
<div class="play-paper-check-item"><b>评分:</b>{{homeData.score}}</div>
<div class="play-paper-check-item">
<b>评语:</b>
<div class="edit_html" v-html="homeData.check_comments"></div>
</div>
</div>
</template>
<template v-else-if="homeData.created_time">
<p class="help">已于 {{homeData.created_time}} 提交,等待批改中</p>
</template> </template>
<div class="area-btns">
<el-button
type="primary"
@click="submitWork"
:disabled="!!homeData.checker_time || deadLineFlag"
>{{homeData.checker_time ? '已批改' : '提交'}}</el-button>
<span class="help-info">&emsp;&emsp;在获老师批改之前,可以多次提交,将以最后一次提交为准</span>
<template v-if="homeData.checker_time">
<div class="play-paper-check">
<h4>
已获批改
<small>批改于{{homeData.checker_time}}</small>
</h4>
<div class="play-paper-check-item">
<b>评分:</b>
{{homeData.score}}
</div>
<div class="play-paper-check-item">
<b>评语:</b>
<div class="edit_html" v-html="homeData.check_comments"></div>
</div>
</div>
</template>
<template v-else-if="homeData.created_time">
<p class="help">已于 {{homeData.created_time}} 提交,等待批改中</p>
</template>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
import cAction from '@action' import cAction from '@action'
import Base64 from 'Base64' import Base64 from 'Base64'
import CKEDITOR from 'CKEDITOR' import VEditor from '@/components/editor.vue'
import VUpload from '@/components/upload.vue'
export default { export default {
components: { VEditor, VUpload },
props: { props: {
chapterId: { type: String, require: false }, chapterId: { type: String, require: false },
chapterWork: { type: Object, require: false }, chapterWork: { type: Object, require: false },
...@@ -83,149 +71,114 @@ export default { ...@@ -83,149 +71,114 @@ export default {
cid: { type: String, require: false }, cid: { type: String, require: false },
id: { type: String, require: false } id: { type: String, require: false }
}, },
data () { data() {
return { return {
ckeditor: null,
successFileUrl: '',
filesArr: [],
file: {
id: 'WU_FILE_0',
name: '',
type: '',
lastModifiedDate: '',
size: '',
file: ''
},
homeData: {}, homeData: {},
/* 设置是否可以初始化 ckeditor */
setTime: null,
isInit: false,
deadLine: '', deadLine: '',
deadLineFlag: false deadLineFlag: false,
questions: []
} }
}, },
/* 本组件 仅支持 单个 ckeditor 存在 */ watch: {
mounted () { id: {
this.loadAjax() handler() {
}, this.loadAjax()
updated () {}, }
destroyed () { },
/* 清空 ckeditor 需要调用方法删除 并 在DOM结构中也移除 */ chapterWork: {
this.ckeditor && this.ckeditor.destroy(true) handler(data) {
this.ckeditor = null this.questions = data.questions.map(item => {
return Object.assign({}, item, { file_url: '', descreption: '' })
})
this.loadAjax()
}
}
}, },
methods: { methods: {
handleChange (file, filelist) { loadAjax() {
this.file.name = file.raw.name const loading = this.$loading({
this.file.type = file.raw.type lock: true,
this.file.lastModifiedDate = file.raw.lastModifiedDate text: '',
this.file.size = file.raw.size spinner: '',
this.file.file = file.raw background: 'rgba(255, 255, 255, 0.9)'
}, })
loadAjax () { cAction.Player.getHomework(this.sid, this.cid, this.id)
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' }) .then(data => {
cAction.Player.getHomework(this.sid, this.cid, this.id).then(data => { this.homeData = data
this.homeData = data
}).catch(e => { this.filesArr.pop(); this.$message.error(e.message) }).finally(() => { const parseAnswers = JSON.parse(data.work_contents)
this.setTime = setInterval(() => { this.questions = this.questions.map(item => {
if (document.querySelector('#editor-chapterWork')) { const found = parseAnswers.find(
this.initckeditor() answer => answer.question_id === item.id
if (this.homeData.work_contents) { )
const json = JSON.parse(this.homeData.work_contents) if (found) {
if (json[0].is_encoded) { return Object.assign({}, item, {
json[0].descreption = Base64.decode(json[0].descreption) file_url: found.file_url,
} descreption: Base64.decode(found.descreption)
this.successFileUrl = json[0].file_url })
this.ckeditor.setData(json[0].descreption)
} else { } else {
this.successFileUrl = '' return item
this.ckeditor.setData('')
} }
/* 滚动到头部 */ })
document.querySelector('.play-paper').scrollTop = 0 })
clearInterval(this.setTime) .catch(e => {
} this.filesArr.pop()
}, 50) this.$message.error(e.message)
loading.close() })
}) .finally(() => {
loading.close()
})
setTimeout(() => { setTimeout(() => {
cAction.Player.getHomeworkStopTime(this.sid, this.cid, this.chapterId).then(data => { cAction.Player.getHomeworkStopTime(this.sid, this.cid, this.chapterId)
this.deadLine = data.dead_line || '' .then(data => {
const deadLine = data.dead_line ? new Date(data.dead_line).getTime() : '' this.deadLine = data.dead_line || ''
// deadLine = new Date().getTime() - 100 const deadLine = data.dead_line
this.deadLineFlag = ((new Date().getTime() > deadLine) && !!deadLine) ? new Date(data.dead_line).getTime()
// console.log(this.deadLine) : ''
}).catch(e => { this.$message.error(e.message) }).finally(() => {}) this.deadLineFlag = new Date().getTime() > deadLine && !!deadLine
})
.catch(e => {
this.$message.error(e.message)
})
}, 500) }, 500)
}, },
submitWork () { submitWork() {
if (!this.ckeditor.getData()) { const loading = this.$loading({
this.$message.error('请填写内容') lock: true,
return text: '',
} spinner: '',
/* 只能提交 单个问题 */ background: 'rgba(255, 255, 255, 0.9)'
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' }) })
const str = JSON.stringify([{ // 组装提交的数据
question_id: this.chapterWork.questions[0].id, const answers = this.questions.map(item => {
descreption: Base64.encode(this.ckeditor.getData()), return {
file_url: this.successFileUrl, question_id: item.id,
is_encoded: 1 descreption: Base64.encode(item.descreption),
}]) file_url: item.file_url,
is_encoded: 1
}
})
cAction.Player.updateHomework({ cAction.Player.updateHomework({
semester_id: this.sid, semester_id: this.sid,
course_id: this.cid, course_id: this.cid,
chapter_id: this.chapterId, chapter_id: this.chapterId,
work_id: this.id, work_id: this.id,
work_contents: str, work_contents: JSON.stringify(answers),
duration: 30 + Math.floor(Math.random() * 1000) duration: 30 + Math.floor(Math.random() * 1000)
}).then(data => { })
if (data.status) { .then(data => {
this.$message({ type: 'success', message: '提交成功,等待批改' }) if (data.status) {
this.loadAjax() this.$message({ type: 'success', message: '提交成功,等待批改' })
} this.loadAjax()
}).catch(e => { this.filesArr.pop(); this.$message.error(e.message) }).finally(() => { loading.close() }) }
}, })
uploadFile () { .catch(e => {
if (!/\.(docx)$/gi.test(this.file.name)) { this.filesArr.pop()
this.$message.error('文件格式不对,请重新上传') this.$message.error(e.message)
this.filesArr.pop() })
return .finally(() => {
} loading.close()
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' }) })
cAction.Player.uploadFile(this.file).then(data => {
this.successFileUrl = data.url
this.filesArr.pop()
}).catch(e => { this.filesArr.pop(); this.$message.error(e.message) }).finally(() => { loading.close() })
},
/* 初始化 ckeditor */
initckeditor () {
!this.ckeditor && (this.ckeditor = CKEDITOR.replace('editor-chapterWork', {
height: 300,
uiColor: '#eeeeee',
filebrowserImageUploadUrl: '/api/ckeditor/img/upload',
// 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'] }
]
}))
}
},
watch: {
id: {
handler () {
this.loadAjax()
}
} }
} }
} }
......
// import viewerRoutes from '@/modules/viewer/routes.js'
export default [ export default [
{ path: '/', redirect: '/app/learn/course' }, { path: '/', redirect: '/app/learn/course' },
{ {
...@@ -284,5 +286,6 @@ export default [ ...@@ -284,5 +286,6 @@ export default [
// { path: '/survey-phone/*', redirect: '/learn-error/learn-error' }, // { path: '/survey-phone/*', redirect: '/learn-error/learn-error' },
/* 如果所有页面都没找到 - 指向 */ /* 如果所有页面都没找到 - 指向 */
{ path: '*', component: () => import('@/components/errorPages/404.vue') } { path: '*', component: () => import('@/components/errorPages/404.vue') }
// { path: '/viewer', component: () => import('@/modules/viewer/index.vue') } // viewer module routes
// ...viewerRoutes
] ]
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"js-md5": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ=="
},
"vuex": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.3.tgz",
"integrity": "sha512-k8vZqNMSNMgKelVZAPYw5MNb2xWSmVgCKtYKAptvm9YtZiOXnRXFWu//Y9zQNORTrm3dNj1n/WaZZI26tIX6Mw=="
}
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论