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

merge...

......@@ -332,4 +332,126 @@ export default class PlayerAction extends BaseACTION {
getLiveList () { return Player.getLiveList().then(res => res) }
/* 获取云课堂 url */
getCloudUrl () { return Player.getCloudUrl().then(res => res) }
/* 获取考卷信息 */
getExamInfo (cid, sid) {
return Player.getExamInfo(cid, sid).then(_res => {
const exam = {}
exam.id = _res.id
exam.title = _res.title
exam.score = {}
exam.radioList = _res.examination.radioList
for (let i = 0; i < exam.radioList.length; i++) {
exam.radioList[i].user_answer = ''
exam.radioList[i].right_answer = ''
exam.radioList[i].get_score = -1
}
exam.checkboxList = _res.examination.checkboxList
for (let i = 0; i < exam.checkboxList.length; i++) {
exam.checkboxList[i].user_answer = []
exam.checkboxList[i].right_answer = []
exam.checkboxList[i].get_score = -1
}
exam.shortAnswerList = _res.examination.shortAnswerList
for (let i = 0; i < exam.shortAnswerList.length; i++) {
exam.shortAnswerList[i].user_answer = ''
exam.shortAnswerList[i].get_score = -1
exam.shortAnswerList[i].attachments = []
exam.shortAnswerList[i].upload = {
type: 'upload-form',
label: '附件上传:',
model: 'attachments',
action: webConf.apiBaseURL + '/util/upload-file',
data: {
special: 'exam'
},
attrs: {
multiple: true,
headers: {
tenant: 'sofia'
}
},
html: `
<div style="color: #72818c; font-size: 14px;">
<p style="margin: 0;">支持doc,docx,ppt,xls,txt,rar,zip,pdf,jpg,pic,png格式的文件,文件小于30M。</p>
</div>
`
}
}
return exam
})
}
/* 获取考卷结果 */
getExamAnswer (cid, sid, eid) {
return Player.getExamAnswer(cid, sid, eid).then(_res => {
if (_res.code) { return _res }
const exam = {}
let tmp = null
exam.id = _res.id
exam.title = _res.title
exam.type = _res.type
exam.score = _res.score
exam.isPublished = _res.is_published || ''
exam.submitted_time = _res.submitted_time
exam.radioList = _res.sheet.radioList
for (let i = 0; i < exam.radioList.length; i++) {
tmp = exam.radioList[i]
if (!tmp.user_answer) tmp.user_answer = ''
if (!tmp.right_answer) tmp.right_answer = ''
if (!tmp.get_score) tmp.get_score = -1
}
exam.checkboxList = _res.sheet.checkboxList
for (let i = 0; i < exam.checkboxList.length; i++) {
tmp = exam.checkboxList[i]
if (!tmp.user_answer || !tmp.user_answer.length) tmp.user_answer = []
if (!tmp.right_answer || !tmp.right_answer.length) tmp.right_answer = []
if (!tmp.get_score) tmp.get_score = -1
}
exam.shortAnswerList = _res.sheet.shortAnswerList
for (let i = 0; i < exam.shortAnswerList.length; i++) {
tmp = exam.shortAnswerList[i]
tmp.user_answer = Base64.decode(tmp.user_answer.replace(/ /gi, '+'))
if (!tmp.attachments || !tmp.attachments.length) tmp.attachments = []
tmp.upload = {
type: 'upload-form',
label: '附件上传:',
model: 'attachments',
action: webConf.apiBaseURL + '/util/upload-file',
data: {
special: 'exam'
},
attrs: {
multiple: true,
headers: {
tenant: 'sofia'
}
},
html: `
<div style="color: #72818c; font-size: 14px;">
<p style="margin: 0;">支持doc,docx,ppt,xls,txt,rar,zip,pdf,jpg,pic,png格式的文件,文件小于30M。</p>
</div>
`
}
}
return exam
})
}
/* 获取考试状态 */
getExamStatus (cid, sid, eid) {
return Player.getExamStatus(cid, sid, eid).then(_res => {
/* 00: 考场未开放,不允许进入
10:考场开放,允许进入
20:开始答题
90:考试已结束 */
return _res
})
}
/* 提交考卷 */
submitExam (cid, sid, eid, obj) {
return Player.submitExam(cid, sid, eid, obj).then(_res => {
return _res
})
}
}
......@@ -103,7 +103,7 @@ export default class API {
if (data && data.code !== undefined) {
if (data.code !== 0) {
if (!/account\/get-user-info/gi.test(res.config.url)) {
Message({ type: 'error', message: data.msg })
data.msg && Message({ type: 'error', message: data.msg })
}
}
}
......
......@@ -102,4 +102,32 @@ export default class PlayerAPI extends BaseAPI {
* 跨域接口请求 - 直接获取云课堂设置
*/
getCloudUrl = (obj = {}) => this.get('https://node-server.ezijing.com/get/cloud-class', obj)
/**
* 获取考卷信息
* @param {[string]} course_id -> cid
* @param {[string]} semester_id -> sid
*/
getExamInfo = (cid, sid) => this.get(`/v2/education/${sid}/${cid}/examination`, {})
/**
* 获取考卷结果
* @param {[string]} course_id -> cid
* @param {[string]} semester_id -> sid
* @param {[string]} exam_id -> eid
*/
getExamAnswer = (cid, sid, eid) => this.get(`/v2/education/${sid}/${cid}/examination/${eid}/sheet`, {})
/**
* 获取考试状态
* @param {[string]} course_id -> cid
* @param {[string]} semester_id -> sid
* @param {[string]} exam_id -> eid
*/
getExamStatus = (cid, sid, eid) => this.get(`/v2/education/${sid}/${cid}/examination/${eid}/status`, {})
/**
* 提交考卷
* @param {[string]} course_id -> cid
* @param {[string]} semester_id -> sid
* @param {[string]} exam_id -> eid
* @param {[object]} obj -> 提交对象类
*/
submitExam = (cid, sid, eid, obj = {}) => this.post(`/v2/education/${sid}/${cid}/examination/${eid}/sheet`, obj, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })
}
......@@ -165,7 +165,10 @@ export default {
this.fetchData()
},
// 刷新
refersh() {
refersh(isForce = false) {
if (isForce) {
this.page.currentPage = 1
}
this.fetchData()
}
},
......
<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>
## 组件简介
| 字段值 | 说明 | 字段属性 | 默认值 |
| ------- | ------------------------- | ------- | ----- |
| `type` | 类型:`String`; 说明:组件类型名 | 自定义字段 | `upload-form` |
| `action` | 类型:`String`; 说明:上传请求接口path | 自定义字段 | `` |
| `deleteAction` | 类型:`String`; 说明:删除请求接口path | 自定义字段 | `` |
| `html` | 类型:`String`; 说明:上传说明,支持html | 自定义字段 | `` |
| `label` | 类型:`String`; 说明:组件左侧显示名称 | element-ui el-form-item对应字段 | `''` |
| `label-width` | 类型:`String`; 说明:组件左侧显示名称宽度(加单位),父级设置可以子级继承 | element-ui el-form-item对应字段 | `''` |
| `required` | 类型:`Boolean`; 说明:标识是否必填 | element-ui el-form-item对应字段 | `false` |
| `disabled` | 类型:`Boolean`; 说明:标识是否只读 | element-ui el-form-item对应字段 | `false` |
| `model` | 类型:`String`; 说明:表单提交name值和回显对照字段 | 自定义字段 | `''` |
| `placeholder` | 类型:`String`; 说明:组件input框中,默认提示文字 | element-ui el-input对应字段 | `''` |
| `attrs` | 类型:`Object`; 说明:定义标签上Data属性值 | element-ui对应字段 | `{}` |
| `rules` | 类型:`Array`; 说明:组件错误提示规则 | element-ui el-form-item对应字段 | `[]` |
### Demo Example:
``` js
return {
type: 'upload-form',
label: '姓名',
labeWidth: '160px',
required: true,
disabled: false,
model: 'uploadArrs',
action: '',
data: {
},
deleteAction: '',
deleteData: {
},
html: `
<div style="color: #72818c; font-size: 14px;">
<p style="margin: 0;">申请者需要将有效身份证件原件扫描或者拍照后提交。</p>
<p style="margin: 0;">请您提供有效身份证件的扫描件,身份证与台港澳居民大陆通行证应包括正反两面扫描件。</p>
<p style="margin: 0;">只上传一个文件,多份文件需合并到一个文件后打印出来检查无误后再上传。</p>
<p style="margin: 0;">上传文件仅限“jpg,jpeg,gif,png”格式,文件小于10MB。</p>
</div>
`,
attrs: {
multiple: false,
limit: 1
},
rules: [
{
required: true,
message: '请上传',
trigger: 'blur'
}
]
}
```
* 其他属性 [参考文档]([https://](https://element.eleme.cn/#/zh-CN/component/input))
import Upload from './src/uploadForm.vue'
/* istanbul ignore next */
Upload.install = function (Vue) {
Vue.component(Upload.name, Upload)
}
export default Upload
function getError (action, option, xhr) {
let msg
if (xhr.response) {
msg = `${xhr.response.error || xhr.response}`
} else if (xhr.responseText) {
msg = `${xhr.responseText}`
} else {
msg = `fail to post ${action} ${xhr.status}`
}
const err = new Error(msg)
err.status = xhr.status
err.method = 'post'
err.url = action
return err
}
function getBody (xhr) {
const text = xhr.responseText || xhr.response
if (!text) {
return text
}
try {
return JSON.parse(text)
} catch (e) {
return text
}
}
export function deleteFile (option) {
if (typeof XMLHttpRequest === 'undefined') {
return
}
// eslint-disable-next-line no-undef
const xhr = new XMLHttpRequest()
let action = option.action
xhr.onerror = function error (e) {
option.onError(e)
}
xhr.onload = function onload () {
if (xhr.status < 200 || xhr.status >= 300) {
return option.onError(getError(action, option, xhr))
}
option.onSuccess(getBody(xhr))
}
xhr.open('delete', action, true)
if (option.withCredentials && 'withCredentials' in xhr) {
xhr.withCredentials = true
}
const headers = option.headers || {}
for (let item in headers) {
if (headers.hasOwnProperty(item) && headers[item] !== null) {
xhr.setRequestHeader(item, headers[item])
}
}
xhr.send()
return xhr
}
export default function upload (option) {
if (typeof XMLHttpRequest === 'undefined') {
return
}
// eslint-disable-next-line no-undef
const xhr = new XMLHttpRequest()
const action = option.action
if (xhr.upload) {
xhr.upload.onprogress = function progress (e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100
}
option.onProgress(e)
}
}
// eslint-disable-next-line no-undef
const formData = new FormData()
if (option.data) {
Object.keys(option.data).forEach(key => {
formData.append(key, option.data[key])
})
}
formData.append(option.filename, option.file, option.file.name)
xhr.onerror = function error (e) {
option.onError(e)
}
xhr.onload = function onload () {
if (xhr.status < 200 || xhr.status >= 300) {
return option.onError(getError(action, option, xhr))
}
option.onSuccess(getBody(xhr))
}
xhr.open('post', action, true)
if (option.withCredentials && 'withCredentials' in xhr) {
xhr.withCredentials = true
}
const headers = option.headers || {}
for (let item in headers) {
if (headers.hasOwnProperty(item) && headers[item] !== null) {
xhr.setRequestHeader(item, headers[item])
}
}
xhr.send(formData)
return xhr
}
<template>
<div class="upload-form">
<div class="u-babel">{{ item.label }}</div>
<el-upload style="display: inline-block;"
:action="item.action"
:data="item.data"
:before-upload="beforeUploadFile"
:on-success="onSuccessFile"
:with-credentials="true"
:show-file-list="false"
:disabled="(item.disabled || false) || !isUpload"
v-bind="item.attrs || {}"
>
<el-button type="primary" size="small" :disabled="!isUpload">点击上传</el-button>
<template v-if="formData[item.model] !== null && formData[item.model] !== '' && formData[item.model] !== undefined">
<div class="self-icon el-icon-circle-check" style="color: #237f00;"></div>
</template>
<div class="self-icon el-icon-circle-close" style="color: #b01c40;"></div>
</el-upload>
<div style="overflow: hidden; padding: 10px 0 0 0;">
<template v-if="filesArr.length">
<!-- 遍历显示文件 -->
<template v-for="(item, index) in filesArr">
<template v-if="/(jpeg)|(jpg)|(png)|(gif)/gi.test(item.url)">
<div v-bind:key="item.id" class="show-file">
<template v-if="!(item.disabled || false) && isUpload">
<div class="close" @click="deleteFiles(index)">X</div>
</template>
<el-avatar shape="square" :size="100" fit="contain" :src="item.url"></el-avatar>
<span class="title">{{ item.sso_file_name }}</span>
<div class="hover">
<a target="_blank" :href="item.url">下载</a>
</div>
</div>
</template>
<template v-else>
<div v-bind:key="item.id" class="show-file">
<template v-if="!(item.disabled || false) && isUpload">
<div class="close" @click="deleteFiles(index)">X</div>
</template>
<el-avatar shape="square" :size="100" fit="contain" :src="item.url"></el-avatar>
<span class="title">{{ item.sso_file_name }}</span>
<div class="hover">
<a target="_blank" :href="item.url">下载</a>
</div>
</div>
</template>
</template>
</template>
</div>
<div class='info' style="line-height: 1.5;" v-html="item.html"></div>
</div>
</template>
<script>
// import { deleteFile } from './ajax'
export default {
name: 'UploadForm',
componentName: 'UploadForm',
props: {
item: {
type: Object,
default () {
return {}
}
},
formData: {
type: Object,
default () {
return {}
}
},
isUpload: {
type: Boolean,
default () {
return true
}
}
},
data () {
const tmpArr = this.formData[this.item.model] || []
this.formData[this.item.model] = tmpArr
return {
project_id: '',
filesArr: tmpArr
}
},
methods: {
beforeUploadFile (file) {},
onSuccessFile (response, file, fileList) {
response.url = response.url || response.file || ''
response.sso_file_name = file.name
this.filesArr.push(response)
// this.$emit('onSubmit')
},
deleteFiles (index) {
this.filesArr.splice(index, 1)
// let temp = this.filesArr[index]
// deleteFile({
// action: this.item.deleteAction + '/' + temp.id + '?project_id=' + this.item.data.project_id,
// onError: () => {},
// onSuccess: (res) => {
// if (res.status === 200) {
// this.filesArr.splice(index, 1)
// }
// }
// })
}
},
watch: {
filesArr: {
immediate: true,
deep: true,
handler (value) {
if (this.formData[this.item.model].length !== value.length) {
this.formData[this.item.model] = value
}
}
}
}
}
</script>
<style lang="scss">
.u-babel {
display: inline-block;
margin-right: 10px;
}
.self-icon {
display: none !important;
vertical-align: middle;
margin-left: 10px;
font-size: 21px;
line-height: 22px;
}
.is-error .self-icon.el-icon-circle-close { display: inline-block !important; }
.is-success .self-icon.el-icon-circle-check { display: inline-block !important; }
.show-file {
position: relative;
float: left;
margin-right: 10px;
.close {
position: absolute;
z-index: 10;
right: -10px;
top: -10px;
width: 20px;
height: 20px;
color: #fff;
font-size: 12px;
line-height: 20px;
text-align: center;
background: #efefef;
border-radius: 50%;
cursor: pointer;
}
.el-avatar {
img {
width: 100%;
}
}
.title {
position: absolute;
left: 0;
bottom: 8px;
width: 100%;
padding-left: 5px;
font-size: 12px;
line-height: 20px;
background: #efefef;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
box-sizing: border-box;
}
.hover {
display: none;
position: absolute;
z-index: 9;
top: 0;
left: 0;
height: 100px;
width: 100%;
background: rgba(0, 0, 0, 0.2);
line-height: 100px;
text-align: center;
a {
color: #f1f1f1;
}
}
&:hover {
.hover {
display: block;
}
}
}
</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>
......@@ -5,6 +5,7 @@ import VueI18n from 'vue-i18n' // 使用 国际化
import createI18n from './assets/languages' // 国际化定义
import App from './app.vue' // 初始化 vue页面
import UploadForm from './components/upload-form'
import './style.scss' // 公共样式
import MetaInfo from 'vue-meta-info'
import Element from 'element-ui'
......@@ -19,6 +20,8 @@ require('promise.prototype.finally').shim()
/* 兼容处理 end */
Vue.use(VueRouter)
Vue.use(UploadForm)
Vue.component(UploadForm.name, UploadForm)
const router = createRouter()
Vue.use(VueI18n)
const i18n = createI18n()
......
......@@ -2,55 +2,94 @@ import BaseAPI from '@/api/base_api'
const httpRequest = new BaseAPI(webConf)
/**
* 获取课程详情
* @param {string} courseId 课程ID
* @param {string} semesterId 学期ID
* 获取课程讨论列表
*/
export function getCourse(courseId, semesterId) {
return httpRequest.get(`/v2/education/courses/${courseId}/${semesterId}`)
export const getDiscussList = (param) => {
const paramPath = param.path || ''
return httpRequest.get(
`/v2/qa/questions${paramPath}`,
param.dataJson
)
}
/**
* 获取我的课程讨论列表
*/
export const getCourseDiscussList = (param) => {
const paramPath = param.path || ''
return httpRequest.get(
`/v2/qa/questions/course${paramPath}`,
param.dataJson
)
}
/**
* 删除提问
*/
export const deleteDiscuss = (qid) => {
return httpRequest.delete(
`/v2/qa/questions/${qid}`
)
}
/**
* 获取章节资源详情
* @param {string} vid 资源ID
* 获取问题详情
*/
export function getChapterVideo(vid) {
export const getDiscussDetail = (qid) => {
return httpRequest.get(
`/v2/qa/questions/${qid}`
)
}
/**
* 删除评论
*/
export const deleteComment = (cid) => {
return httpRequest.delete(
`/v2/qa/comments/${cid}`
)
}
/**
* 回复评论
*/
export const callbackComment = (param) => {
return httpRequest.post(
'/v2/education/video-streaming',
{ vid },
'/v2/qa/comments',
param,
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取章节资源详情
* @param {string} vid 章节的资源ID
* 回答问题
*/
export function getChapterVideoAliyun(vid) {
export const answerQues = (param) => {
return httpRequest.post(
'/v2/education/aliyun-video-streaming',
{ vid },
'/v2/qa/answers',
param,
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取课程讨论列表
* 删除回答
*/
export const getDiscussList = (param) => {
const paramPath = param.path || ''
return httpRequest.get(
`/v2/qa/questions${paramPath}`,
param.dataJson
export const deleteAnswer = (aid) => {
return httpRequest.delete(
`/v2/qa/answers/${aid}`
)
}
/**
* 获取我的课程讨论列表
* 取消点赞
*/
export const getCourseDiscussList = (param) => {
const paramPath = param.path || ''
return httpRequest.get(
`/v2/qa/questions/course${paramPath}`,
param.dataJson
export const unlike = (tagid) => {
return httpRequest.delete(
`/v2/qa/tags/${tagid}`
)
}
/**
* 点赞
*/
export const like = (param) => {
return httpRequest.post(
'/v2/qa/tags',
param,
{ headers: { 'Content-Type': 'application/json' } }
)
}
......@@ -4,6 +4,17 @@
"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
......@@ -4,6 +4,17 @@
"answers": "回答",
"votes": "投票",
"noData": "暂无相关评论"
},
"DiscussDetail": {
"title": "问题详情",
"like": "点赞",
"discuss": "讨论",
"reply": "回复",
"delete": "删除",
"send": "发送",
"noAnswer": "暂无回答",
"deleteSuccess": "删除成功",
"answering": "回答问题"
}
}
}
<template>
<div>
<div class="ask">
<div class="user-1">
<img class="img-1" :src="avatar" />
<div class="right-1">
<div class="name-1">{{data.replier.nickname}}</div>
<div class="time-1">{{data.created_time}}</div>
</div>
</div>
<div class="text" v-html="data.contents"></div>
<div class="user">
<template v-if="data.mine">
<div
class="right-txt"
@click="deleteAnswer(data.id)"
>{{ $t('pages.learn.discussDetail.delete') }}</div>
</template>
<div class="right-txt" @click="$emit('reply', {answer_id: data.id})">{{ $t('pages.learn.discussDetail.reply') }}</div>
<div
class="right-txt"
@click="commentVisible = !commentVisible"
>{{ $t('pages.learn.discussDetail.discuss') }}({{data.comments.length}})</div>
<div class="right-txt" @click="$emit('btnlike', {tagId: data.tag ? data.tag.id : null, ansId: data.id})">点赞({{data.tag_count}})</div>
</div>
<template v-if="commentVisible">
<!-- 评论列表 -->
<template v-for="item in data.comments">
<reply-item v-on="$listeners" :data="item" :dataId="data.id" :key="item.id"></reply-item>
</template>
</template>
</div>
</div>
</template>
<script>
import * as api from '../api/index.js'
import replyItem from './replyItem.vue'
export default {
components: { replyItem },
props: {
data: { type: Object, default: () => {} }
},
data() {
console.log(this.data)
return {
commentVisible: false
}
},
computed: {
avatar() {
return this.data.replier.avatar || '../assets/images/person-default.jpg'
}
},
methods: {
deleteAnswer (id) {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
api.deleteAnswer(id).then(json => {
this.$emit('updateList')
this.$message({ type: 'success', message: this.$t('pages.learn.discussDetail.deleteSuccess') })
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
}
}
}
</script>
<style lang="sass" scoped>
</style>
<template>
<div class="item-list">
<div class="user">
<div class="name">{{data.observer.nickname}}</div>
<div class="time">{{data.created_time}}</div>
<template v-if="data.mine">
<div
class="right-txt"
@click="deleteComment(data.id)"
>{{ $t('pages.learn.discussDetail.delete') }}</div>
</template>
<div class="right-txt" @click="$emit('reply', {to: data.observer.nickname, question_id: dataId})">{{ $t('pages.learn.discussDetail.reply') }}</div>
</div>
<div class="text" v-html="data.comments"></div>
</div>
</template>
<script>
import * as api from '../api/index.js'
export default {
props: {
data: { type: Object, default: () => {} },
dataId: { type: String, default: () => {} }
},
data() {
return {}
},
methods: {
deleteComment (id) {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
api.deleteComment(id).then(json => {
this.$emit('updateList')
this.$message({ type: 'success', message: this.$t('pages.learn.discussDetail.deleteSuccess') })
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
}
}
}
</script>
<template>
<div class="item-list">
<div class="user">
<div class="name">{{data.observer.nickname}}</div>
<div class="time">{{data.created_time}}</div>
<template v-if="data.mine">
<div
class="right-txt"
@click="deleteComment(data.id)"
>{{ $t('pages.learn.discussDetail.delete') }}</div>
</template>
<div class="right-txt" @click="$emit('reply', {answer_id: dataId, to: data.observer.nickname})">{{ $t('pages.learn.discussDetail.reply') }}</div>
</div>
<div class="text" v-html="data.comments"></div>
</div>
</template>
<script>
import * as api from '../api/index.js'
export default {
props: {
data: { type: Object, default: () => {} },
dataId: { type: String, default: () => {} }
},
data() {
return {}
},
mounted() {
},
methods: {
deleteComment (id) {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
api.deleteComment(id).then(json => {
this.$emit('updateList')
this.$message({ type: 'success', message: this.$t('pages.learn.discussDetail.deleteSuccess') })
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
}
}
}
</script>
import './index.scss'
import Discuss from './src/discuss.vue'
import DiscussDetail from './src/discussDetail.vue'
const components = [
Discuss
Discuss,
DiscussDetail
]
const install = function (Vue, opts = {}) {
......@@ -26,5 +28,6 @@ if (typeof window !== 'undefined' && window.Vue) {
export default {
install,
Discuss
Discuss,
DiscussDetail
}
......@@ -28,6 +28,19 @@
</template>
<script>
/**
* 调用:通过组件传值监听watch来请求渲染
*
params: {
path: '/my', 地址参数
request: 'getDiscussList', 请求方法
dataJson: { 参数
limit: 10,
offset: 0
}
}
*/
import * as api from '../api/index'
export default {
......
<template>
<div>
<div class='discuss-detail-scroll'>
<div class='ques'>
<div class='title'>{{detail.title}}</div>
<div class='text' v-html="detail.contents"></div>
<div class='user'>
<div class='name'>{{user.nickname}}</div>
<div class='time'>{{detail.created_time}}</div>
<template v-if='detail.mine'><div class='right-txt' @click='deleteDiscuss'>{{ $t('DiscussModule.DiscussDetail.delete') }}</div></template>
<div class='right-txt' @click='replyComposeParam({question_id: detail.id})'>{{ $t('DiscussModule.DiscussDetail.reply') }}</div>
<div class='right-txt' @click='commentVisible = !commentVisible'>{{ $t('DiscussModule.DiscussDetail.discuss') }}({{detail.comments ? detail.comments.length : 0}})</div>
<div class='right-txt' @click='btnlike({tagId: detail.tag ? detail.tag.id : null})'>
{{ $t('DiscussModule.DiscussDetail.like') }}({{detail.tag_count}})</div>
</div>
<div v-show='commentVisible'>
<template v-for="item in detail.comments">
<reply-item :data="item" :key="item.id" :dataId="detail.id" @reply="replyComposeParam" @updateList="updateList"></reply-item>
</template>
</div>
</div>
<div class='result'>{{detail.answer_count}} {{ $t('DiscussModule.DiscussList.answers') }}<div style='display: inline-block; width: 0.2rem;'></div>{{detail.tag_total_count || 0}} {{ $t('DiscussModule.DiscussList.votes') }}</div>
<!-- 回答列表 -->
<template v-for="item in detail.answers">
<answer-item :data="item" :key="item.id" @btnlike="btnlike" @updateList="updateList" @reply="replyComposeParam"></answer-item>
</template>
<template v-if='!answersLength'>
<div class='no-data'>{{ $t('DiscussModule.DiscussDetail.noAnswer') }}</div>
</template>
<div style='width: 750rpx; height: 200rpx;'></div>
</div>
<div style="width: 100%; height: 1.7rem;"></div>
<div class='input-publish'>
<textarea id="editor" v-model="replyText" ref="focusTextarea"></textarea>
<el-button type="primary" @click="publishContent">{{ $t('DiscussModule.DiscussDetail.send') }}</el-button><em class="send">({{inputStatus.placeholder}})</em>
</div>
</div>
</template>
<script>
import replyItem from '../components/replyDetailItem.vue'
import AnswerItem from '../components/answerItem.vue'
import * as api from '../api/index.js'
export default {
components: { AnswerItem, replyItem },
name: 'DiscussDetail',
props: {
paramId: { type: Object, require: false }
// sid: { type: String, require: false },
// cid: { type: String, require: false },
// id: { type: String, require: false } // 章节id
},
data () {
return {
replyText: '',
commentVisible: false,
detail: {},
/* 回复内容状态 */
call: {},
/* 存储状态值 的对象, 记录上次用户操作 */
disQus: {
isShowComment: false
},
inputStatus: {
canFocus: false,
placeholder: `${this.$t('DiscussModule.DiscussDetail.answering')} ...`,
input: ''
},
answer: true,
replyParam: {}
}
},
created() {
this.updateList()
},
computed: {
user() {
return this.detail.questioner || {}
},
answersLength() {
return this.detail.answers ? this.detail.answers.length : false
}
},
methods: {
// 点赞
btnlike (e) {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
if (e.tagId) {
api.unlike(e.tagId).then(json => {
this.updateList()
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
} else {
const param = {}
if (this.detail.id) { param.question_id = this.detail.id }
if (e.ansId) { param.answer_id = e.ansId }
if (this.detail.semester_id) { param.semester_id = this.detail.semester_id }
api.like(param).then(json => {
this.updateList()
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
}
},
// 删除问题
deleteDiscuss () {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
api.deleteDiscuss(this.paramId.id).then(json => {
this.$message({ type: 'success', message: this.$t('DiscussModule.DiscussDetail.deleteSuccess') })
/* 返回上一级 菜单 */
setTimeout(() => {
this.$router.go(-1)
}, 1000)
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
},
// 更新列表
updateList () {
api.getDiscussDetail(this.paramId.id).then(data => {
this.detail = data
})
},
/**
* 点击 键盘发送按钮时
*/
publishContent () {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
this.replyParam.comments = this.replyParam.to ? `${this.$t('DiscussModule.DiscussDetail.reply')}${this.replyParam.to}:${this.replyText}` : this.replyText
if (this.answer) {
this.replyComposeParam({
answer: true,
contents: this.replyText,
question_id: this.detail.id
}, true)
api.answerQues(this.replyParam).then(json => {
this.updateList()
this.replyText = ''
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
} else {
api.callbackComment(this.replyParam).then(json => {
this.updateList()
this.replyText = ''
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
}
this.$refs.focusTextarea.focus()
},
// 通过点击不同的回复按钮 拼成所要提交不同的参数
replyComposeParam (obj, answer) {
this.$refs.focusTextarea.focus()
this.inputStatus.placeholder = obj.to !== undefined ? `回复${obj.to}:` : '回复:'
this.answer = answer || false
const commonParam = {
questionId: this.detail.id,
semester_id: this.detail.semester_id
}
this.replyParam = Object.assign(commonParam, obj)
}
}
}
</script>
<style lang="scss" scoped>
<style lang="scss">
// .discuss-detail-scroll { }
.discuss-detail-scroll .ques { padding: 0.3rem 0.26rem; padding-top: 0; margin-bottom: 0.2rem; background: #fff; box-shadow: 0 2px 4px rgba(10, 4, 6, 0.1); overflow: hidden; }
/* 显示标题、描述、讨论、人等 */
.discuss-detail-scroll .user { margin-top: 0.15rem; overflow: hidden; }
.discuss-detail-scroll .user .name { float: left; font-size: 0.16rem; color: #313131; line-height: 0.2rem; }
.discuss-detail-scroll .user .time { float: left; margin-left: 0.4rem; font-size: 0.14rem; color: #a0a0a0; line-height: 0.2rem; }
.discuss-detail-scroll .user .right-txt { float: right; margin-left: 0.2rem; font-size: 0.14rem; color: #a27c1b; cursor: pointer; }
.discuss-detail-scroll .user .right-txt .img { display: inline-block; margin-top: 1px; width: 0.22rem; height: 0.2rem; }
.discuss-detail-scroll .title { margin: 0.15rem 0; font-size: 0.24rem; color: #313131; line-height: 1.5; text-align: justify; }
.discuss-detail-scroll .text { font-size: 0.18rem; color: #535353; line-height: 1.5; text-align: justify; }
/* 显示回答和投票 */
.discuss-detail-scroll .result { margin-left: 0.26rem; margin-top: 0.15rem; font-size: 0.14rem; color: #313131; }
.discuss-detail-scroll .ask { position: relative; margin-top: 0.2rem; padding: 0 0.26rem; background: #fff; overflow: hidden; }
/* 显示回答 和 讨论 */
.discuss-detail-scroll .ask .user-1 { position: relative; overflow: hidden; margin-top: 0.25rem; margin-bottom: 0.15rem; }
.discuss-detail-scroll .ask .user-1 .img-1 { float: left; width: 0.6rem; height: 0.6rem; border-radius: 50%; }
.discuss-detail-scroll .ask .user-1 .right-1 { position: absolute; left: 0.72rem; top: 50%; -webkit-transform: translateY(-50%); transform: translateY(-50%); }
.discuss-detail-scroll .ask .user-1 .right-1 .name-1 { font-size: 0.14rem; color: #313131; text-overflow: ellipsis; overflow: hidden; word-break:break-all; }
.discuss-detail-scroll .ask .user-1 .right-1 .time-1 { margin-top: 5px; font-size: 0.14rem; color: #a0a0a0; }
.discuss-detail-scroll .item-list { position: relative; padding: 0.3rem 0; border-bottom: 1px solid #c9c9c9; }
.discuss-detail-scroll .item-list:last-child { border-bottom: none; }
.discuss-detail-scroll .item-list .user { margin-top: 0; overflow: hidden; }
.discuss-detail-scroll .item-list .user .name { float: left; font-size: 0.16rem; color: #313131; line-height: 0.2rem; }
.discuss-detail-scroll .item-list .user .time { float: left; margin-left: 0.4rem; font-size: 0.14rem; color: #a0a0a0; line-height: 0.2rem; }
.discuss-detail-scroll .item-list .user .right-txt { float: right; margin-left: 0.2rem; font-size: 0.14rem; color: #a27c1b; }
.discuss-detail-scroll .item-list .text { margin-top: 0.15rem; font-size: 0.16rem; color: #535353; }
.discuss-detail-scroll .item-list .text.on { color: #2263d9; }
.discuss-detail-scroll .no-data { padding: 1rem 0; font-size: 0.24rem; color: #c9c9c9; text-align: center; }
.input-publish { position: fixed; z-index: 2; height: 1.5rem; left: 295px; right: 30px; bottom: 0; padding: 0.2rem; background: #fff; box-sizing: border-box; }
.input-publish #editor { width: 100%; height: 0.7rem; font-size: 18px; line-height: 1.5; outline: none; }
.input-publish .send { font-size: 14px; color: #ddd; margin-left: 10px; }
.input-publish .ask { position: relative; margin: 12px auto; width: 90%; height: 56px; border: 1px solid #dcdcdc; box-sizing: border-box; -webkit-box-sizing: border-box; }
.input-publish .ask .img { position: absolute; left: 24px; top: 13px; width: 28px; height: 0.26rem; }
.input-publish .ask .txt { position: absolute; left: 63px; top: 0; height: 54px; width: 100%; border: none; line-height: 2; font-size: 0.18rem; color: #313131; }
</style>
import BaseAPI from '@/api/base_api'
const httpRequest = new BaseAPI(webConf)
/**
* 获取课程详情
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function getCourse(semesterId, courseId) {
return httpRequest.get(`/v2/education/courses/${semesterId}/${courseId}`)
}
/**
* 获取章节资源详情
* @param {string} vid 资源ID
*/
export function getChapterVideo(vid) {
return httpRequest.post(
'/v2/education/video-streaming',
{ vid },
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取章节资源详情
* @param {string} vid 章节的资源ID
*/
export function getChapterVideoAliyun(vid) {
return httpRequest.post(
'/v2/education/aliyun-video-streaming',
{ vid },
{ 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' } }
)
}
/**
* 获取课程考试试题
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function getCourseExam(semesterId, courseId) {
return httpRequest.get(`/v2/education/${semesterId}/${courseId}/examination`)
}
/**
* 获取课程考试状态
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} examId 试题ID
*/
export function getCourseExamStatus(semesterId, courseId, examId) {
return httpRequest.get(
`/v2/education/${semesterId}/${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(
`/v2/education/${semesterId}/${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) {
return httpRequest.get(
`/v2/education/${semesterId}/${courseId}/examination/${examId}/sheet`
)
}
import BaseAPI from '@/api/base_api'
const httpRequest = new BaseAPI(webConf)
/**
* 获取课程详情
* @param {string} courseId 课程ID
* @param {string} semesterId 学期ID
*/
export function getCourse(courseId, semesterId) {
return httpRequest.get(`/v2/education/courses/${courseId}/${semesterId}`)
}
/**
* 获取章节资源详情
* @param {string} vid 资源ID
*/
export function getChapterVideo(vid) {
return httpRequest.post(
'/v2/education/video-streaming',
{ vid },
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取章节资源详情
* @param {string} vid 章节的资源ID
*/
export function getChapterVideoAliyun(vid) {
return httpRequest.post(
'/v2/education/aliyun-video-streaming',
{ vid },
{ headers: { 'Content-Type': 'application/json' } }
)
}
<template>
<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>
<ul class="knot-list">
<li v-for="subItem in item.children" :key="subItem.id" @click="onClick(subItem)">
<span class="knot-name">{{subItem.name | showName(subItem.type)}}</span>
<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.type)}}</span>
<i class="el-icon" :class="genIconClass(subItem.type)"></i>
</li>
</ul>
</li>
......@@ -14,10 +20,12 @@
<script>
export default {
props: {
data: {
type: Array,
data: { type: Array, default: () => [] },
// 当前选中的章节
active: {
type: Object,
default() {
return []
return {}
}
}
},
......@@ -46,8 +54,29 @@ export default {
}
},
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) {
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 +91,23 @@ export default {
overflow: hidden;
.chapter-item {
h4 {
padding: 10px 32px;
padding: 10px 22px;
margin: 0;
font-size: 15px;
color: #b0b0b0;
background-color: #2f2f2f;
}
/* 节列表样式 */
.knot-list {
.chapter-item-list {
margin: 0;
padding: 0;
line-height: 1.6;
overflow: hidden;
li {
position: relative;
&.on {
&.is-active {
background: #3c3c3c;
a {
.chapter-item-list__name {
color: #b49441;
}
}
......@@ -110,7 +139,7 @@ export default {
background: #616161;
}
}
.knot-name {
.chapter-item-list__name {
display: block;
padding: 15px 35px 15px 40px;
font-size: 14px;
......@@ -126,6 +155,7 @@ export default {
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #a0a0a0;
}
}
}
......
......@@ -3,12 +3,12 @@
<el-tabs v-model="activeName">
<el-tab-pane label="章节" name="0">
<div class="tab-pane">
<aside-chapter :data="chapters"></aside-chapter>
<aside-chapter :data="chapters" :active="active"></aside-chapter>
</div>
</el-tab-pane>
<!-- <el-tab-pane label="讲义" name="1">
<div class="tab-pane">
<aside-lecture :data="ppts"></aside-lecture>
<aside-lecture :data="ppts" :pptIndex="pptIndex" v-on="$listeners"></aside-lecture>
</div>
</el-tab-pane> -->
</el-tabs>
......@@ -16,26 +16,25 @@
</template>
<script>
import AsideChapter from './asideChapter.vue'
import AsideLecture from './asideLecture.vue'
import AsideChapter from './chapter.vue'
import AsideLecture from './lecture.vue'
export default {
props: {
// 章节
chapters: {
type: Array,
default() {
return []
}
},
chapters: { type: Array, default: () => [] },
// 讲义
ppts: {
type: Array,
ppts: { type: Array, default: () => [] },
// 当前选中的章节
active: {
type: Object,
default() {
return []
}
return {}
}
},
// 当前选择的PPT
pptIndex: { type: Number, default: 0 }
},
components: { AsideChapter, AsideLecture },
data() {
return {
......
<template>
<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" />
</li>
</ul>
......@@ -9,23 +14,25 @@
<script>
export default {
props: {
data: {
type: Array,
default() {
return []
}
}
// 当前选择的PPT
pptIndex: { type: Number, default: 0 },
data: { type: Array, default: () => [] }
},
data() {
return {
activeIndex: 0
activeIndex: this.pptIndex
}
},
watch: {
pptIndex(index) {
this.activeIndex = index
}
},
methods: {
// 点击PPT
onClick(data) {
this.activeIndex = data.id
this.$emit('clickPPT', data)
onClick(index) {
this.activeIndex = index
this.$emit('change-ppt', index)
}
}
}
......
<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
}
},
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>
<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 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;
white-space: nowrap;
&:hover {
color: #b49441;
}
::v-deep * {
margin: 0;
padding: 0;
}
}
}
.empty {
font-size: 18px;
line-height: 80px;
background-color: #fff;
text-align: center;
border-radius: 40px;
}
</style>
<template>
<div class="upload">
<el-upload action :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="(fileUrl, index) in fileList" :key="index">
<a :href="fileUrl" target="_blank">
<i class="el-icon-document"></i>
{{ fileUrl | fileName }}
</a>
<a :href="fileUrl" :download="fileUrl | 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'
export default {
name: 'VUpload',
props: {
value: { type: [String, Array] }
},
data() {
return {
fileList: []
}
},
watch: {
value(value) {
if (value) {
this.fileList = Array.isArray(value) ? value : [value]
}
}
},
filters: {
fileName(value) {
return value ? value.split('/').pop() : ''
}
},
methods: {
httpRequest(xhr) {
api.uploadFile({ file: xhr.file }).then(response => {
if (response.success) {
if (Array.isArray(this.value)) {
this.fileList.push(response.url)
this.$emit('input', this.fileList)
} else {
this.fileList = [response.url]
this.$emit('input', response.url)
}
}
})
}
}
}
</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>
<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 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,
ChapterLive,
CourseWork,
CourseRead,
CourseExam
},
props: {
chapter: {
type: Object,
default() {
return {}
}
}
},
computed: {
currentCompoent() {
const componentNames = {
2: 'ChapterPlayer', // 视频
3: 'ChapterWork', // 作业
4: 'ChapterRead', // 资料
5: 'ChapterLive', // 直播
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%;">
<iframe
:src="iframeUrl"
frameborder="0"
width="100%"
height="100%"
allow="autoplay;geolocation;microphone;camera;midi;encrypted-media;"
></iframe>
</div>
</template>
<script>
// 章节视频
export default {
name: 'ChapterLive',
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
user() {
return window.G.UserInfo ? window.G.UserInfo.student_info : {}
},
nickName() {
return this.user.personal_name || '匿名'
},
iframeUrl() {
const live = this.chapter.live
const liveStatus = live.live_status
live.viewer_name = live.viewer_name || this.nickName
if (liveStatus === 103 && live.enable_record === 1) {
// enable_record 0:不启用回放 1:开启回放
// 查看回放
return `https://view.csslcloud.net/api/view/callback?recordid=${live.record_id}&roomid=${live.room_id}&userid=${live.user_id}&autoLogin=true&viewername=${live.viewer_name}&viewertoken=${live.viewer_token}`
} else {
// 直播
return `https://view.csslcloud.net/api/view/index?roomid=${live.room_id}&userid=${live.user_id}&autoLogin=true&viewername=${live.viewer_name}&viewertoken=${live.viewer_token}`
}
}
}
}
</script>
<template>
<div class="player">
<div class="player" v-if="chatperResources">
<div class="player-main">
<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 class="player-column" v-if="pptVisible">
<!-- 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 class="player-footer">
<em class="player-button player-button-download" v-if="pdf">
<a :href="pdf" target="_blank">下载PPT</a>
<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="ppts.length">同步显示PPT</em>
<em :class="pptClass" @click="togglePPTVisible" v-if="chatperResources.ppts.length">同步显示PPT</em>
<em :class="skipClass" @click="toggleSkip">始终跳过片头</em>
</div>
</div>
</template>
<script>
// api
import * as api from '../../api'
// components
import videoPlayer from './videoPlayer.vue'
import pptPlayer from './pptPlayer.vue'
export default {
name: 'Player',
name: 'ChapterPlayer',
components: { videoPlayer, pptPlayer },
props: {
video: { type: Object },
pdf: { type: String },
ppts: {
type: Array,
default() {
return []
}
}
// 当前章节
chapter: { type: Object },
// PPT当前选中的索引
pptIndex: { type: Number, default: 0 }
},
data() {
return {
videoVisible: true,
pptVisible: false,
isSkip: false
isSkip: false,
chatperResources: null
}
},
watch: {
pptIndex(index) {
this.updateVideoCurrentTime(index)
}
},
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() {
return {
'player-button': true,
......@@ -72,7 +99,40 @@ export default {
// PPT全屏
onPPTFullscreen(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>
......@@ -83,6 +143,7 @@ export default {
flex-direction: column;
width: 100%;
height: 100%;
background-color: #3f3f3f;
}
.player-main {
display: flex;
......
......@@ -5,12 +5,13 @@
<script>
export default {
name: 'VideoPlayer',
props: { video: Object },
props: { isSkip: Boolean, video: Object },
data() {
return { player: null }
},
methods: {
createPlayer() {
const _this = this
const { FD, LD, SD } = this.video
this.player = new Aliplayer(
{
......@@ -29,16 +30,17 @@ export default {
]
},
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) {
var paramData = params.paramData
var desc = paramData.desc
var definition = paramData.definition
const paramData = params.paramData
const desc = paramData.desc
const definition = paramData.definition
player
.getComponent('QualityComponent')
.setCurrentQuality(desc, definition)
})
player.on('timeupdate', function(event) {
_this.$emit('timeupdate', player.getCurrentTime())
})
}
)
}
......
<template>
<container :title="chapter.name">
<file-list :files="files"></file-list>
</container>
</template>
<script>
// components
import Container from '../common/container.vue'
import FileList from '../common/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 '../common/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>
{{ 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>
</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>
<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: 'ChapterExam',
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>
<template>
<container :title="chapter.name" v-loading="loading">
<div class="exam-form">
<el-form :disabled="isRevised">
<exam-item
v-for="(item, index) in questions"
:index="index"
:type="item.question_type"
:data="item"
:value="item.formModel"
:disabled="isRevised"
:key="item.id"
></exam-item>
</el-form>
</div>
<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" @click="onSubmit" :disabled="isRevised">{{submitText}}</el-button>
</el-tooltip>
</div>
</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
}
},
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.check_date : false
},
// 提交按钮文本
submitText() {
return this.isRevised ? '已批改' : '提交'
}
},
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) {
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 => {
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,
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) {
api
.sbumitChapterExam(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>
<template>
<container :title="detail.title" v-loading="loading">
<div class="exam">
<template v-if="status.examination_status === '00'">
<div class="no-exam">暂无考试</div>
</template>
<template v-else-if="isSubmited && !isExamComplete">
<div class="no-exam">试卷批改中,请耐心等待</div>
</template>
<template v-else>
<!-- 考试期间,未开始考试 -->
<div class="exam-welcome" v-if="!isStartExam">
<div>考试时间:{{status.start_time}} ~ {{status.terminate_time}}</div>
<el-button
type="primary"
:disabled="!isExamTime"
@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">
<el-form :disabled="isSubmited">
<exam-item
v-for="(item, index) in questions"
:index="index"
:type="item.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>
</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 {
loading: false,
detail: {},
status: {},
questions: [],
values: [], // 提交的答案
messageInstance: null,
exam: {},
isStartExam: false, // 是否开始考试
autoSubmitTimer: null, // 自动提交定时器
checkStatusTimer: null, // 考试状态定时器
examVisible: true
}
},
watch: {
isExamComplete(value) {
this.examVisible = !value
}
},
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'
},
// 考试按钮
startExamButtonText() {
return this.status.examination_status === '90' ? '考试结束' : '开始考试'
},
// 考试完成
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()
},
// 获取试题
getDetail(callback) {
this.loading = true
api
.getCourseExam(this.sid, this.cid)
.then(response => {
this.detail = Array.isArray(response) ? null : response
// 设置问题列表数据
this.questions = this.detail
? this.genQuenstions(this.detail.examination)
: []
callback && callback()
})
.finally(() => {
this.loading = false
})
},
// 组装问题数据
genQuenstions(data) {
if (!data) {
return
}
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)
: '',
attachments: []
}
}
return Object.assign({}, item, temp)
})
return [...radioList, ...checkboxList, ...shortAnswerList]
},
// 获取考试状态
getExamStatus() {
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)
}
})
},
// 自动获取考试状态
autoCheckExamStatus() {
// 获取试题状态
this.getExamStatus()
this.checkStatusTimer && clearInterval(this.checkStatusTimer)
this.checkStatusTimer = setInterval(() => {
this.getExamStatus()
}, 3000)
},
// 获取考试结果
getExamResult() {
api.getCourseExamResult(this.sid, this.cid, this.pid).then(response => {
// 设置问题列表数据
if (response.code !== 8001) {
this.isStartExam = true
this.exam = response
this.questions = this.genQuenstions(response.sheet)
// 自动提交
if (this.isStartExam && !this.isSubmited && !this.isExamComplete) {
this.autoSubmit()
}
}
})
},
// 提交校验
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 answers = this.questions.map(item => {
if (item.type === 3) {
item.formModel.user_answer = Base64.encode(item.formModel.user_answer)
}
return item.formModel
})
// 提交参数
const params = { answers: JSON.stringify(answers), type: 1 }
// 请求接口
this.handleSubmitRequest(params)
},
// 自动提交
autoSubmit() {
// 10秒提交一次
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 params = { answers: JSON.stringify(answers), type: 0 }
// 请求接口
this.handleSubmitRequest(params)
}, 10000)
},
// 请求提交接口
handleSubmitRequest(params) {
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)
})
}
},
beforeMount() {
// 获取试题
this.getDetail(() => {
// 获取考试结果
this.getExamResult()
// 自动获取考试状态
this.autoCheckExamStatus()
})
},
destroyed() {
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.checkStatusTimer && clearInterval(this.checkStatusTimer)
}
}
</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-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;
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="主题"></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" @click="onSubmit" :disabled="isRevised">{{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' },
{ 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 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.$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="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>
<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">{{item.abc_option}}</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">{{item.abc_option}}</el-checkbox>
</div>
</el-checkbox-group>
<!-- 简答题 -->
<template v-if="type === 3">
<v-editor v-model="currentValue.user_answer" :disabled="disabled"></v-editor>
<v-upload 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>{{data.check_comment}}</span>
</p>
</template>
<template v-else>
<p>
<span>学生答案:</span>
<span :class="isCorrect ? 'is-success' : 'is-error'">{{submitAnswerText}}</span>
</p>
<p>
<span>正确答案:</span>
<span>{{correctAnswerText}}</span>
</p>
</template>
</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 }
},
data() {
return {
currentValue: {}
}
},
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
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': !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;
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>
<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 ChapterExam from './chapterExam.vue'
export default {
name: 'ViewerWork',
components: { ChapterWork, ChapterExam },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
currentCompoent() {
const componentNames = {
1: 'ChapterExam', // 考试
2: 'ChapterWork' // 作业
}
const homework = this.chapter.homework
return homework ? componentNames[homework.work_type] : ''
}
}
}
</script>
<template>
<div class="course-viewer">
<div class="course-viewer-main">
<!-- 顶部区域 -->
<div class="course-viewer-hd">
<router-link to="/mobile/help/student">
<div class="course-viewer-main-hd">
<router-link :to="`/app/learn/course-detail/${sid}/${cid}`">
<i class="el-icon-arrow-left"></i>
</router-link>
<h1 class="course-viewer-hd__title">{{detail.course_name}}</h1>
<h1 class="course-viewer-main-hd__title">{{ detail.course_name }}</h1>
<!-- 直播的时候显示帮助按钮 -->
<template v-if="isLive">
<router-link to="/app/account/feedbackCreate" target="_blank">
<el-tooltip effect="light" content="意见反馈">
<i class="el-icon-self-fankuiyijian"></i>
......@@ -18,100 +19,153 @@
<i class="el-icon-self-icon-test"></i>
</el-tooltip>
</router-link>
</template>
</div>
<!-- 主体区域 -->
<div class="course-viewer-bd">
<player
:video="chatperResources.video"
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"
:ppts="chatperResources.ppts"
v-if="chatperResources.video"
<div class="course-viewer-main-bd">
<router-view
:data="detail"
:chapter="activeChapter"
:pptIndex="pptIndex"
:key="pid"
@pptupdate="handlePPTupdate"
@change-ppt="handleChangePPT"
/>
</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-show="!isLive && !isCourseExam"
></v-aside>
</div>
</template>
<script>
// api
import * as api from './api/index'
import * as api from './api'
// components
import VAside from './components/aside/aside.vue'
import Player from './components/player/player.vue'
import VAside from './components/aside/index.vue'
export default {
name: 'CourseViewer',
components: { VAside, Player },
components: { VAside },
data() {
return {
detail: {},
chatperResources: {}
ppts: [],
pptIndex: 0
}
},
computed: {
// 当前章节
watch: {
activeChapter() {
return {
resource_id: '6414747439944695808'
this.ppts = []
this.pptIndex = 0
}
},
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 },
{ 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)
},
// 视频资源ID
resourceId() {
return this.activeChapter.resource_id
// 直播
isLive() {
return this.activeChapter ? this.activeChapter.type === 5 : false
},
/**
* 视频提供者
* @return 1是CC加密; 2是非加密; 3是阿里云
*/
videoProvider() {
const video = this.activeChapter.video || {}
return video.video_provider || 3
// 课程考试
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() {
api
.getCourse('6437296642994470912', '6437335122927681536')
.then(response => {
api.getCourse(this.sid, this.cid).then(response => {
this.detail = response
})
},
// 获取章节视频详情
getChapterVideo() {
// 视频播放类型 1是CC加密; 2是非加密; 3是阿里云
if (this.videoProvider === 3) {
api.getChapterVideoAliyun(this.resourceId).then(response => {
this.chatperResources = response
})
} else {
api.getChapterVideo(this.resourceId).then(response => {
this.chatperResources = response
})
}
// PPT列表更新
handlePPTupdate(list) {
this.ppts = list
},
// 右侧菜单选中的PPT修改
handleChangePPT(index) {
this.pptIndex = index
}
},
beforeMount() {
this.getCourse()
this.getChapterVideo()
}
}
</script>
<style lang="scss" scoped>
<style lang="scss">
.course-viewer {
display: flex;
background-color: #3f3f3f;
height: 100vh;
overflow: hidden;
}
.course-viewer-main {
flex: 1;
display: flex;
flex-direction: column;
}
.course-viewer-hd {
.course-viewer-main-hd {
display: flex;
align-items: center;
background-color: #3f3f3f;
height: 56px;
a {
color: #fff;
......@@ -121,13 +175,59 @@ export default {
font-size: 24px;
}
}
.course-viewer-hd__title {
.course-viewer-main-hd__title {
flex: 1;
font-size: 1.5em;
text-align: center;
color: #a0a0a0;
}
.course-viewer-bd {
.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;
}
</style>
export default [
{
path: '/viewer/:sid/:cid',
component: () => import('./index.vue'),
children: [
{
name: 'viewerCourseChapter',
path: ':id',
component: () => import('./components/layout.vue')
}
]
}
]
......@@ -21,7 +21,7 @@
@click="golearningAdd('/app/affairs-hall/again-add/-1')"
>申请重修</el-button>
<div style="width: 100%; height: 0.2rem;"></div>
<table-list :key="affairId" v-bind="tableOption" v-if="affairId"></table-list>
<table-list :key="affairId" v-bind="tableOption" v-if="affairId" ref="tableList"></table-list>
</div>
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<span>确定删除?</span>
......@@ -249,7 +249,8 @@ export default {
.then(data => {
if (data.success) {
this.$message({ type: 'success', message: '删除成功' })
this.$router.go(0)
// 刷新列表,强刷新,返回第一页
this.$refs.tableList && this.$refs.tableList.refersh(true)
}
})
.catch(e => {
......
......@@ -115,13 +115,13 @@ export default {
clearInterval(this.timeInterval)
this.timeInterval = null
}
// this.timeInterval = setInterval(() => {
// cAction.chapterAction.getNewLiveMsg().then(json => {
// if (json.status === 200) {
// this.newLiveMsg = json.data
// }
// }).catch(e => { this.$message.error(e.message) }).finally(() => { })
// }, 3000)
this.timeInterval = setInterval(() => {
cAction.Player.getNewLiveMsg().then(json => {
if (json.status === 200) {
this.newLiveMsg = json.data
}
}).catch(e => { this.$message.error(e.message) }).finally(() => { })
}, 3000)
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Course.getLearnFind().then(data => {
......
......@@ -8,7 +8,7 @@
<el-button class="rbtn" type="primary" size="mini" @click='startLearn' :data-cid='cid' :data-sid='sid' :data-type='tabs[1].chapterList.currentVideoProvider' :data-vid='tabs[1].chapterList.currentChapterId'>继续学习</el-button>
</template>
<template v-else-if='headerInfo.isStart'>
<el-button class="rbtn" type="primary" size="mini" @click='startLearn' :data-cid='cid' :data-sid='sid' :data-type='tabs[1].chapterList.course[0].chapters[0].video_provider' :data-vid='tabs[1].chapterList.course[0].chapters[0].vid'>开始学习</el-button>
<el-button class="rbtn" type="primary" size="mini" @click='startLearn' :data-cid='cid' :data-sid='sid' :data-type='firstVideo.video_provider' :data-vid='firstVideo.vid'>开始学习</el-button>
</template>
<template v-else>
<el-button class="rbtn" type="primary" size="mini" @click='wantThisCourse'>选课</el-button>
......@@ -420,7 +420,8 @@ export default {
isOpenNewTabFlag: false,
arrFn: [],
cloudClassUrls: {},
timeHeart: null
timeHeart: null,
firstVideo: {}
}
},
mounted () {
......@@ -444,6 +445,23 @@ export default {
this.tabs[0].content = json.tabs0Content
this.tabs[1].chapterList = json.tabs1ChapterList
json.tabs3richTest && (this.tabs[3].richText = json.tabs3richTest)
// 设置开始学习的视频
const courseList = json.tabs1ChapterList.course
for (let i = 0; i < courseList.length; i++) {
const children = courseList[i].chapters || []
if (this.firstVideo && this.firstVideo.vid) {
break
}
for (let k = 0; k < children.length; k++) {
const item = children[k]
if (item.vid && item.video_provider) {
this.firstVideo = item
break
}
}
}
cAction.Course.getCourseAssess(this.cid, this.sid).then(json1 => {
const _courseArr = json.tabs1ChapterList.course
/* 进行一次 对照,将 视频 vid 赋值 */
......@@ -461,7 +479,6 @@ export default {
}
}
}
this.tabs[3].assess = json1
loading.close()
}).catch(e => { this.$message.error(e.message) }).finally(() => { })
......
......@@ -2,7 +2,8 @@
<div>
<div class="con-title">{{ $t('pages.learn.discussDetail.title') }}</div>
<div class="con-box">
<div class='discuss-detail-scroll'>
<discuss-detail :paramId='paramId'></discuss-detail>
<!-- <div class='discuss-detail-scroll'>
<div class='ques'>
<div class='title'>{{discussQues.title}}</div>
<div class='text' v-html="discussQues.text"></div>
......@@ -13,7 +14,6 @@
<div class='right-txt' @click='callbackComment' :data-sid='discussQues.sid' :data-qid='discussQues.qid' :data-quesid='discussQues.qid'>{{ $t('pages.learn.discussDetail.reply') }}</div>
<div class='right-txt' @click='openOrcloseDis' data-key='disQus'>{{ $t('pages.learn.discussDetail.discuss') }}({{discussQues.comCnt}})</div>
<div class='right-txt' @click='btnlike' :data-quesid='discussQues.qid' :data-sid='discussQues.sid' :data-tagid='discussQues.tag_id'>
<!-- <image class='img' src='{{discussQues.has_tag ? "./icons/like-on.png" : "./icons/like.png"}}'></image> -->
{{ $t('pages.learn.discussDetail.like') }}({{discussQues.likeCnt}})</div>
</div>
<template v-if='disQus.isShowComment'>
......@@ -53,7 +53,6 @@
<div class='right-txt' @click='callbackComment' :data-sid='discussQues.sid' :data-qid='discussQues.qid' :data-ansid='item.aid'>{{ $t('pages.learn.discussDetail.reply') }}</div>
<div class='right-txt' @click='openOrcloseDis' :data-key='answers' :data-index='index'>{{ $t('pages.learn.discussDetail.discuss') }}({{item.comCnt}})</div>
<div class='right-txt' @click='btnlike' :data-sid='discussQues.sid' :data-quesid='discussQues.qid' :data-ansid='item.aid' :data-tagid='item.tag_id'>
<!-- <image class='img' src='{{item.has_tag ? "./icons/like-on.png" : "./icons/like.png"}}'></image> -->
点赞({{item.likeCnt}})</div>
</div>
<template v-if='answers[index].isShowComment'>
......@@ -76,24 +75,24 @@
<div class='no-data'>{{ $t('pages.learn.discussDetail.noAnswer') }}</div>
</template>
<div style='width: 750rpx; height: 200rpx;'></div>
</div> -->
</div>
</div>
<div style="width: 100%; height: 1.7rem;"></div>
<!-- <div style="width: 100%; height: 1.7rem;"></div>
<div class='input-publish'>
<textarea id="editor"></textarea>
<el-button type="primary" @click="publishContent">{{ $t('pages.learn.discussDetail.send') }}</el-button><em class="send">({{inputStatus.placeholder}})</em>
</div> -->
<!-- <div class='ask'> -->
<!-- <image class='img' src='./icons/ask.png' mode='aspectFill'></image> -->
<!-- <input type='text' class="txt" placeholder='{{inputStatus.placeholder}}' focus='{{inputStatus.canFocus}}' bindblur='blurInput' confirm-type='send' bindconfirm='publishContent' value='{{inputStatus.input}}' cursor-spacing='20'/> -->
<!-- </div> -->
<el-button type="primary" @click="publishContent">{{ $t('pages.learn.discussDetail.send') }}</el-button><em class="send">({{inputStatus.placeholder}})</em>
</div>
</div>
</template>
<script>
import cAction from '@action'
// import cAction from '@action'
import CKEDITOR from 'CKEDITOR'
// import CKEDITOR from 'CKEDITOR'
export default {
props: {
......@@ -103,276 +102,284 @@ export default {
},
data () {
return {
paramId: {},
ckeditor: null,
courseTitle: '课程问题',
discussQues: {
qid: '', user: { url: './icons/default.jpg', name: '用户名000', time: '2018-2-12 15:28:47' }, // eslint-disable-line
title: '这是一个一句话问题这是一个一句话问题这是一个一句话问题标题这是一个一句话问题', // eslint-disable-line
text: '<p>王家有三兄弟甲、乙、丙,丙幼年时送给胡某作养子,丙结婚时,胡某为其盖了新房,后因失火致使该房屋被烧毁。丙的生父母就将自己<p>', // eslint-disable-line
askCnt: 20, TouCnt: 100, likeCnt: 100, comCnt: 100, mine: true, isShowComment: false, has_tag: false, tag_id: null, // eslint-disable-line
comments: [ // eslint-disable-line
{ cid: '', user: { url: '', name: '用户名000', time: '2018-2-12 15:28:47' }, text: '在线学习课程', mine: true }, // eslint-disable-line
{ cid: '', user: { url: '', name: '用户名000', time: '2018-2-12 15:28:47' }, text: '在线学习课程', mine: false } // eslint-disable-line
] // eslint-disable-line
},
answersList: [
// {
// aid: '', user: { url: '', name: '用户名000', time: '2018-2-12 15:28:47' }, // eslint-disable-line
courseTitle: '课程问题'
// discussQues: {
// qid: '', user: { url: './icons/default.jpg', name: '用户名000', time: '2018-2-12 15:28:47' }, // eslint-disable-line
// title: '这是一个一句话问题这是一个一句话问题这是一个一句话问题标题这是一个一句话问题', // eslint-disable-line
// text: '<p>王家有三兄弟甲、乙、丙,丙幼年时送给胡某作养子,丙结婚时,胡某为其盖了新房,后因失火致使该房屋被烧毁。丙的生父母就将自己<p>', // eslint-disable-line
// likeCnt: 100, comCnt: 100, mine: true, isShowComment: false, has_tag: false, tag_id: null, // eslint-disable-line
// askCnt: 20, TouCnt: 100, likeCnt: 100, comCnt: 100, mine: true, isShowComment: false, has_tag: false, tag_id: null, // eslint-disable-line
// comments: [ // eslint-disable-line
// { cid: '', user: { url: '', name: '用户名000', time: '2018-2-12 15:28:47' }, text: '在线学习课程', mine: true } // eslint-disable-line
// { cid: '', user: { url: '', name: '用户名000', time: '2018-2-12 15:28:47' }, text: '在线学习课程', mine: true }, // eslint-disable-line
// { cid: '', user: { url: '', name: '用户名000', time: '2018-2-12 15:28:47' }, text: '在线学习课程', mine: false } // eslint-disable-line
// ] // eslint-disable-line
// }
],
// },
// answersList: [
// // {
// // aid: '', user: { url: '', name: '用户名000', time: '2018-2-12 15:28:47' }, // eslint-disable-line
// // text: '<p>王家有三兄弟甲、乙、丙,丙幼年时送给胡某作养子,丙结婚时,胡某为其盖了新房,后因失火致使该房屋被烧毁。丙的生父母就将自己<p>', // eslint-disable-line
// // likeCnt: 100, comCnt: 100, mine: true, isShowComment: false, has_tag: false, tag_id: null, // eslint-disable-line
// // comments: [ // eslint-disable-line
// // { cid: '', user: { url: '', name: '用户名000', time: '2018-2-12 15:28:47' }, text: '在线学习课程', mine: true } // eslint-disable-line
// // ] // eslint-disable-line
// // }
// ],
/* 存储状态值 的对象, 记录上次用户操作 */
disQus: {
isShowComment: false
},
answers: [ // 数组 跟 上面 answerList数组对应, 存储某些状态值,目前存储讨论打开关闭状态
{ aid: '', isShowComment: false } // eslint-disable-line
],
inputStatus: {
canFocus: false,
placeholder: `${this.$t('pages.learn.discussDetail.answering')} ...`,
input: ''
},
qid: '6447416971762860032',
// disQus: {
// isShowComment: false
// },
// answers: [ // 数组 跟 上面 answerList数组对应, 存储某些状态值,目前存储讨论打开关闭状态
// { aid: '', isShowComment: false } // eslint-disable-line
// ],
// inputStatus: {
// canFocus: false,
// placeholder: `${this.$t('pages.learn.discussDetail.answering')} ...`,
// input: ''
// },
// qid: '6447416971762860032',
/* 回复内容状态 */
call: {}
}
},
mounted () {
this.call = { questionId: this.id, semester_id: '', contents: '', question_id: this.id, answer: true }
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Discuss.getDiscussDetail(this.id).then(json => {
this.discussQues = json.ques
this.call.semester_id = this.discussQues.sid
this.disQus.isShowComment = json.ques.isShowComment
const answers = []
for (let i = 0; i < json.answer.length; i++) {
answers.push({
aid: json.answer[i].aid,
isShowComment: json.answer[i].isShowComment
})
}
this.answersList = json.answer
this.answers = answers
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// this.initckeditor()
$('#editor').on('blur', () => {
this.blurInput()
})
},
destroyed () {
/* 清空 ckeditor 需要调用方法删除 并 在DOM结构中也移除 */
this.ckeditor && this.ckeditor.destroy(true)
this.ckeditor = null
$('#editor').off('blur')
},
methods: {
/**
* 打开或关闭 讨论
*/
openOrcloseDis (e) {
const key = e.currentTarget.dataset.key
if (key === 'disQus') {
this.disQus.isShowComment = !this.disQus.isShowComment
} else {
const index = e.currentTarget.dataset.index
this.answers[index].isShowComment = !this.answers[index].isShowComment
}
},
/**
* 删除评论
*/
deleteComment (e) {
const cid = e.currentTarget.dataset.cid
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Discuss.deleteComment(cid).then(json => {
this.updateList()
this.$message({ type: 'success', message: this.$t('pages.learn.discussDetail.deleteSuccess') })
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
},
/**
* 删除回答
*/
deleteAnswer (e) {
const aid = e.currentTarget.dataset.aid
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Discuss.deleteAnswer(aid).then(json => {
this.updateList()
this.$message({ type: 'success', message: this.$t('pages.learn.discussDetail.deleteSuccess') })
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
},
/**
* 点赞 或 取消点赞 操作
*/
btnlike (e) {
const _data = e.currentTarget.dataset
const quesid = _data.quesid
const ansid = _data.ansid
const tagid = _data.tagid
const sid = _data.sid
if (tagid) { // 取消 点赞操作
// wx.showLoading({ title: '操作中...', mask: true })
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Discuss.unlike(tagid).then(json => {
this.updateList()
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
} else { // 点赞操作
const param = {}
if (quesid) { param.question_id = quesid }
if (ansid) { param.answer_id = ansid }
if (sid) { param.semester_id = sid }
// wx.showLoading({ title: '操作中...', mask: true })
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Discuss.like(param).then(json => {
this.updateList()
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
}
},
/**
* 删除问题
*/
deleteDiscuss () {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Discuss.deleteDiscuss(this.id).then(json => {
this.$message({ type: 'success', message: this.$t('pages.learn.discussDetail.deleteSuccess') })
/* 返回上一级 菜单 */
setTimeout(() => {
// wx.navigateBack({ delta: 1 })
this.$router.go(-1)
}, 1000)
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
},
/**
* 点击回复 调起输入框
*/
callbackComment (e) {
// /* 如果,输入框中,本身已经存在输入内容,那么再点击其他回复时,不给提示,直接清空 */
this.inputStatus.input = '' // 这里如果想判断,不能使用 inputStatus.input这个 在blur时存储
const _data = e.currentTarget.dataset
const qid = _data.qid
const quesid = _data.quesid
const ansid = _data.ansid
const to = _data.to
const sid = _data.sid
const json = {}
if (qid) { json.questionId = qid }
if (to) { json.to = to }
if (quesid) { json.question_id = quesid }
if (ansid) { json.answer_id = ansid }
if (sid) { json.semester_id = sid }
this.call = json
if (to) {
this.inputStatus.placeholder = this.$t('pages.learn.discussDetail.reply') + to + ''
} else {
this.inputStatus.placeholder = `${this.$t('pages.learn.discussDetail.reply')}:`
}
this.inputStatus.canFocus = true
$('#editor').focus()
},
/**
* 取消 聚焦状态, 使用 ckeditor 进行 聚焦监听
*/
blurInput (e) {
this.inputStatus.canFocus = false
if ($('#editor').val()) {
// 输入框中存在内容则不做处理 - 这里不能做任何处理,否则会产生 输入框中内容遗留问题
} else {
this.inputStatus.placeholder = `${this.$t('pages.learn.discussDetail.answering')} ...`
this.inputStatus.input = ''
// 回答问题 方式
this.call = {
questionId: this.discussQues.qid,
semester_id: this.discussQues.sid,
contents: '',
question_id: this.discussQues.qid,
answer: true
}
// call: {}
}
},
/**
* 点击 键盘发送按钮时
*/
publishContent (e) {
const val = $('#editor').val()
if (this.call.to) {
this.call.comments = this.$t('pages.learn.discussDetail.reply') + this.call.to + '' + val
} else {
this.call.comments = val
created() {
this.paramId = {
sid: this.$route.params.sid,
cid: this.$route.params.cid,
id: this.$route.params.id
}
if (this.call.answer) { // 回答问题
// wx.showLoading({ title: '操作中...', mask: true })
this.call.contents = this.call.comments
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Discuss.answerQues(this.call).then(json => {
this.updateList()
// this.ckeditor.setData('')
$('#editor').val('')
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
} else { // 回复评论
// wx.showLoading({ title: '操作中...', mask: true })
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Discuss.callbackComment(this.call).then(json => {
this.updateList()
// this.ckeditor.setData('')
$('#editor').val('')
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
}
// mounted () {
// this.call = { questionId: this.id, semester_id: '', contents: '', question_id: this.id, answer: true }
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Discuss.getDiscussDetail(this.id).then(json => {
// this.discussQues = json.ques
// this.call.semester_id = this.discussQues.sid
// this.disQus.isShowComment = json.ques.isShowComment
// const answers = []
// for (let i = 0; i < json.answer.length; i++) {
// answers.push({
// aid: json.answer[i].aid,
// isShowComment: json.answer[i].isShowComment
// })
// }
// this.answersList = json.answer
// this.answers = answers
// }).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// // this.initckeditor()
// $('#editor').on('blur', () => {
// this.blurInput()
},
/* 刷新页面 全部状态 - 目前没有分页可以这么操作,如果存在分页,可能要重写 */
updateList () {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Discuss.getDiscussDetail(this.id).then(json => {
this.discussQues = json.ques
const _answers = this.answers
for (let i = 0; i < _answers.length; i++) {
for (let j = 0; j < json.answer.length; j++) {
if (_answers[i].aid === json.answer[j].aid) {
json.answer[j].isShowComment = _answers[i].isShowComment
break
}
}
}
const answers = []
for (let i = 0; i < json.answer.length; i++) {
answers.push({
aid: json.answer[i].aid,
isShowComment: json.answer[i].isShowComment
})
}
this.answersList = json.answer
this.answers = answers
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
},
/* 初始化 ckeditor */
initckeditor () {
!this.ckeditor && (this.ckeditor = CKEDITOR.replace('editor', {
height: 100,
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' ]" }
]
}))
}
}
// })
// },
// destroyed () {
// /* 清空 ckeditor 需要调用方法删除 并 在DOM结构中也移除 */
// this.ckeditor && this.ckeditor.destroy(true)
// this.ckeditor = null
// $('#editor').off('blur')
// },
// methods: {
// /**
// * 打开或关闭 讨论
// */
// openOrcloseDis (e) {
// const key = e.currentTarget.dataset.key
// if (key === 'disQus') {
// this.disQus.isShowComment = !this.disQus.isShowComment
// } else {
// const index = e.currentTarget.dataset.index
// this.answers[index].isShowComment = !this.answers[index].isShowComment
// }
// },
// /**
// * 删除评论
// */
// deleteComment (e) {
// const cid = e.currentTarget.dataset.cid
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Discuss.deleteComment(cid).then(json => {
// this.updateList()
// this.$message({ type: 'success', message: this.$t('pages.learn.discussDetail.deleteSuccess') })
// }).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// },
// /**
// * 删除回答
// */
// deleteAnswer (e) {
// const aid = e.currentTarget.dataset.aid
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Discuss.deleteAnswer(aid).then(json => {
// this.updateList()
// this.$message({ type: 'success', message: this.$t('pages.learn.discussDetail.deleteSuccess') })
// }).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// },
// /**
// * 点赞 或 取消点赞 操作
// */
// btnlike (e) {
// const _data = e.currentTarget.dataset
// const quesid = _data.quesid
// const ansid = _data.ansid
// const tagid = _data.tagid
// const sid = _data.sid
// if (tagid) { // 取消 点赞操作
// // wx.showLoading({ title: '操作中...', mask: true })
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Discuss.unlike(tagid).then(json => {
// this.updateList()
// }).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// } else { // 点赞操作
// const param = {}
// if (quesid) { param.question_id = quesid }
// if (ansid) { param.answer_id = ansid }
// if (sid) { param.semester_id = sid }
// // wx.showLoading({ title: '操作中...', mask: true })
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Discuss.like(param).then(json => {
// this.updateList()
// }).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// }
// },
// /**
// * 删除问题
// */
// deleteDiscuss () {
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Discuss.deleteDiscuss(this.id).then(json => {
// this.$message({ type: 'success', message: this.$t('pages.learn.discussDetail.deleteSuccess') })
// /* 返回上一级 菜单 */
// setTimeout(() => {
// // wx.navigateBack({ delta: 1 })
// this.$router.go(-1)
// }, 1000)
// }).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// },
// /**
// * 点击回复 调起输入框
// */
// callbackComment (e) {
// // /* 如果,输入框中,本身已经存在输入内容,那么再点击其他回复时,不给提示,直接清空 */
// this.inputStatus.input = '' // 这里如果想判断,不能使用 inputStatus.input这个 在blur时存储
// const _data = e.currentTarget.dataset
// const qid = _data.qid
// const quesid = _data.quesid
// const ansid = _data.ansid
// const to = _data.to
// const sid = _data.sid
// const json = {}
// if (qid) { json.questionId = qid }
// if (to) { json.to = to }
// if (quesid) { json.question_id = quesid }
// if (ansid) { json.answer_id = ansid }
// if (sid) { json.semester_id = sid }
// this.call = json
// if (to) {
// this.inputStatus.placeholder = this.$t('pages.learn.discussDetail.reply') + to + ''
// } else {
// this.inputStatus.placeholder = `${this.$t('pages.learn.discussDetail.reply')}:`
// }
// this.inputStatus.canFocus = true
// $('#editor').focus()
// },
// /**
// * 取消 聚焦状态, 使用 ckeditor 进行 聚焦监听
// */
// blurInput (e) {
// this.inputStatus.canFocus = false
// if ($('#editor').val()) {
// // 输入框中存在内容则不做处理 - 这里不能做任何处理,否则会产生 输入框中内容遗留问题
// } else {
// this.inputStatus.placeholder = `${this.$t('pages.learn.discussDetail.answering')} ...`
// this.inputStatus.input = ''
// // 回答问题 方式
// this.call = {
// questionId: this.discussQues.qid,
// semester_id: this.discussQues.sid,
// contents: '',
// question_id: this.discussQues.qid,
// answer: true
// }
// }
// },
// /**
// * 点击 键盘发送按钮时
// */
// publishContent (e) {
// const val = $('#editor').val()
// if (this.call.to) {
// this.call.comments = this.$t('pages.learn.discussDetail.reply') + this.call.to + '' + val
// } else {
// this.call.comments = val
// }
// if (this.call.answer) { // 回答问题
// // wx.showLoading({ title: '操作中...', mask: true })
// this.call.contents = this.call.comments
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Discuss.answerQues(this.call).then(json => {
// this.updateList()
// // this.ckeditor.setData('')
// $('#editor').val('')
// }).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// } else { // 回复评论
// // wx.showLoading({ title: '操作中...', mask: true })
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Discuss.callbackComment(this.call).then(json => {
// this.updateList()
// // this.ckeditor.setData('')
// $('#editor').val('')
// }).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// }
// // this.blurInput()
// },
// /* 刷新页面 全部状态 - 目前没有分页可以这么操作,如果存在分页,可能要重写 */
// updateList () {
// const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
// cAction.Discuss.getDiscussDetail(this.id).then(json => {
// this.discussQues = json.ques
// const _answers = this.answers
// for (let i = 0; i < _answers.length; i++) {
// for (let j = 0; j < json.answer.length; j++) {
// if (_answers[i].aid === json.answer[j].aid) {
// json.answer[j].isShowComment = _answers[i].isShowComment
// break
// }
// }
// }
// const answers = []
// for (let i = 0; i < json.answer.length; i++) {
// answers.push({
// aid: json.answer[i].aid,
// isShowComment: json.answer[i].isShowComment
// })
// }
// this.answersList = json.answer
// this.answers = answers
// }).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
// },
// /* 初始化 ckeditor */
// initckeditor () {
// !this.ckeditor && (this.ckeditor = CKEDITOR.replace('editor', {
// height: 100,
// 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' ]" }
// ]
// }))
// }
// }
}
</script>
......@@ -406,7 +413,7 @@ export default {
.discuss-detail-scroll .item-list .text { margin-top: 0.15rem; font-size: 0.16rem; color: #535353; }
.discuss-detail-scroll .item-list .text.on { color: #2263d9; }
.discuss-detail-scroll .no-data { padding: 1rem 0; font-size: 0.24rem; color: #c9c9c9; text-align: center; }
.input-publish { position: fixed; z-index: 2; height: 1.5rem; left: 200px; right: 15px; bottom: 0; padding: 0.2rem; background: #fff; box-sizing: border-box; }
.input-publish { position: fixed; z-index: 2; height: 1.5rem; left: 295px; right: 30px; bottom: 0; padding: 0.2rem; background: #fff; box-sizing: border-box; }
.input-publish #editor { width: 100%; height: 0.7rem; font-size: 18px; line-height: 1.5; outline: none; }
.input-publish .send { font-size: 14px; color: #ddd; margin-left: 10px; }
.input-publish .ask { position: relative; margin: 12px auto; width: 90%; height: 56px; border: 1px solid #dcdcdc; box-sizing: border-box; -webkit-box-sizing: border-box; }
......
......@@ -94,7 +94,13 @@ export default {
cAction.Player.getLiveList()
.then(response => {
if (response.status === 200) {
this.dataList = response.data
this.dataList = response.data.map(data => {
// 兼容老的数据结构
if (!Array.isArray(data.live)) {
data.live = [data.live]
}
return data
})
}
})
// 取消报错提醒
......@@ -124,7 +130,7 @@ export default {
onClick(data) {
const {
live_status: liveStatus,
live_type: liveType,
live_type: liveType = 'live',
start_time: liveTime
} = data
let message = this.calcTimeText(liveTime, liveStatus)
......
<template>
<div class="play-paper">
<div class="play-paper">
<div class="play-paper-body">
<div class="play-paper-title"><div><h3>{{chapterName}}</h3></div></div>
<div class="play-paper-title">
<div>
<h3>{{chapterName}}</h3>
</div>
</div>
<div class="play-paper-content">
<ul class="play-read-files">
<li><a :href="chapterRead.reading_attachment" target="_blank" v-html="chapterRead.reading_content"></a></li>
<li>
<a
:href="chapterRead.reading_attachment"
target="_blank"
v-html="chapterRead.reading_content"
></a>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
......@@ -19,3 +29,9 @@ export default {
}
}
</script>
<style scoped>
::v-deep p {
margin: 0;
}
</style>
<template>
<div class="play-paper">
<div class="play-paper">
<div class="play-paper-body">
<div class="play-paper-title"><div><h3>{{chapterName}}</h3></div></div>
<div class="play-paper-title">
<div>
<h3>{{chapterName}}</h3>
</div>
</div>
<div class="play-paper-content play-chapter-work">
<template v-if="chapterWork.questions && chapterWork.questions.length" >
<ul>
<template v-for="(item, index) in chapterWork.questions">
<li v-bind:key="index">
<li v-for="(item, index) in questions" :key="index">
<div class="work-number">{{index + 1}}.</div>
<div class="work-title">
<div class="edit_html" v-html="item.question_content"></div>
</div>
<textarea id="editor-chapterWork"></textarea>
<div style="height: 20px;"></div>
<!-- <el-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> } -->
<!-- 文本内容 -->
<v-editor v-model="item.descreption"></v-editor>
<!-- 上传附件 -->
<v-upload v-model="item.file_url">请上传对应的文件附件:</v-upload>
</li>
</template>
</ul>
</template>
<template v-else>
<!-- <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 v-if="deadLine">
<p style="color: red">请于截止日期 {{deadLine}} 前提交</p>
</template>
<div class="area-btns">
<el-button type="primary" @click="submitWork" :disabled="!!homeData.checker_time || deadLineFlag">{{homeData.checker_time ? '已批改' : '提交'}}</el-button>
<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>
<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>
......@@ -65,16 +51,18 @@
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import cAction from '@action'
import Base64 from 'Base64'
import CKEDITOR from 'CKEDITOR'
import VEditor from '@/components/editor.vue'
import VUpload from '@/components/upload.vue'
export default {
components: { VEditor, VUpload },
props: {
chapterId: { type: String, require: false },
chapterWork: { type: Object, require: false },
......@@ -83,150 +71,129 @@ export default {
cid: { type: String, require: false },
id: { type: String, require: false }
},
data () {
data() {
return {
ckeditor: null,
successFileUrl: '',
filesArr: [],
file: {
id: 'WU_FILE_0',
name: '',
type: '',
lastModifiedDate: '',
size: '',
file: ''
},
homeData: {},
/* 设置是否可以初始化 ckeditor */
setTime: null,
isInit: false,
deadLine: '',
deadLineFlag: false
deadLineFlag: false,
questions: []
}
},
/* 本组件 仅支持 单个 ckeditor 存在 */
mounted () {
watch: {
id: {
handler() {
this.loadAjax()
}
},
updated () {},
destroyed () {
/* 清空 ckeditor 需要调用方法删除 并 在DOM结构中也移除 */
this.ckeditor && this.ckeditor.destroy(true)
this.ckeditor = null
chapterWork: {
immediate: true,
handler(data) {
if (data.questions && data.questions.length) {
this.questions = data.questions.map(item => {
return Object.assign({}, item, { file_url: '', descreption: '' })
})
this.loadAjax()
}
}
}
},
methods: {
handleChange (file, filelist) {
this.file.name = file.raw.name
this.file.type = file.raw.type
this.file.lastModifiedDate = file.raw.lastModifiedDate
this.file.size = file.raw.size
this.file.file = file.raw
},
loadAjax () {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Player.getHomework(this.sid, this.cid, this.id).then(data => {
loadAjax() {
const loading = this.$loading({
lock: true,
text: '',
spinner: '',
background: 'rgba(255, 255, 255, 0.9)'
})
cAction.Player.getHomework(this.sid, this.cid, this.id)
.then(data => {
this.homeData = data
}).catch(e => { this.filesArr.pop(); this.$message.error(e.message) }).finally(() => {
this.setTime = setInterval(() => {
if (document.querySelector('#editor-chapterWork')) {
this.initckeditor()
if (this.homeData.work_contents) {
const json = JSON.parse(this.homeData.work_contents)
if (json[0].is_encoded) {
json[0].descreption = Base64.decode(json[0].descreption)
}
this.successFileUrl = json[0].file_url
this.ckeditor.setData(json[0].descreption)
const parseAnswers = JSON.parse(data.work_contents)
this.questions = this.questions.map(item => {
const found = parseAnswers.find(
answer => answer.question_id === item.id
)
if (found) {
return Object.assign({}, item, {
file_url: found.file_url,
descreption: Base64.decode(found.descreption)
})
} else {
this.successFileUrl = ''
this.ckeditor.setData('')
}
/* 滚动到头部 */
document.querySelector('.play-paper').scrollTop = 0
clearInterval(this.setTime)
return item
}
}, 50)
})
})
.catch(e => {
this.filesArr.pop()
this.$message.error(e.message)
})
.finally(() => {
loading.close()
})
setTimeout(() => {
cAction.Player.getHomeworkStopTime(this.sid, this.cid, this.chapterId).then(data => {
cAction.Player.getHomeworkStopTime(this.sid, this.cid, this.chapterId)
.then(data => {
this.deadLine = data.dead_line || ''
const deadLine = data.dead_line ? new Date(data.dead_line).getTime() : ''
// deadLine = new Date().getTime() - 100
this.deadLineFlag = ((new Date().getTime() > deadLine) && !!deadLine)
// console.log(this.deadLine)
}).catch(e => { this.$message.error(e.message) }).finally(() => {})
const deadLine = data.dead_line
? new Date(data.dead_line).getTime()
: ''
this.deadLineFlag = new Date().getTime() > deadLine && !!deadLine
})
.catch(e => {
this.$message.error(e.message)
})
}, 500)
},
submitWork () {
if (!this.ckeditor.getData()) {
submitWork() {
const emptyValue = this.questions.find(item => item.descreption === '')
if (emptyValue) {
this.$message.error('请填写内容')
return
}
/* 只能提交 单个问题 */
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,
descreption: Base64.encode(this.ckeditor.getData()),
file_url: this.successFileUrl,
const loading = this.$loading({
lock: true,
text: '',
spinner: '',
background: 'rgba(255, 255, 255, 0.9)'
})
// 组装提交的数据
const answers = this.questions.map(item => {
return {
question_id: item.id,
descreption: Base64.encode(item.descreption),
file_url: item.file_url,
is_encoded: 1
}])
}
})
cAction.Player.updateHomework({
semester_id: this.sid,
course_id: this.cid,
chapter_id: this.chapterId,
work_id: this.id,
work_contents: str,
work_contents: JSON.stringify(answers),
duration: 30 + Math.floor(Math.random() * 1000)
}).then(data => {
})
.then(data => {
if (data.status) {
this.$message({ type: 'success', message: '提交成功,等待批改' })
this.loadAjax()
}
}).catch(e => { this.filesArr.pop(); this.$message.error(e.message) }).finally(() => { loading.close() })
},
uploadFile () {
if (!/\.(docx)$/gi.test(this.file.name)) {
this.$message.error('文件格式不对,请重新上传')
this.filesArr.pop()
return
}
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
})
.catch(e => {
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()
}
this.$message.error(e.message)
})
.finally(() => {
loading.close()
})
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .upload {
margin: 10px 0 20px;
}
</style>
<template>
<div class="play-paper">
<div class="play-paper">
<div class="play-paper-body">
<div class="play-paper-title"><div><h3>{{exam.title}}</h3></div></div>
<template v-if="status.examinationStatus === '00'">
<div class="no-exam">暂无考试</div>
</template>
<template v-else-if="status.examinationStatus === '90'">
<div class="play-paper-title">
<div>
<h3>{{exam.title}}</h3>
</div>
</div>
<template v-if="status.type === 2 && status.isPublished === 1">
<template v-if="exam.score.total !== undefined">
<div
style="font-size: 18px;"
>得分:单选:{{exam.score.radio}}分,多选:{{exam.score.checkbox}}分,简答:{{exam.score.shortAnswer}}分,总分:{{exam.score.total}}</div>
</template>
<template v-if="exam.shortAnswerList.length">
<template v-for="(item, index) in exam.shortAnswerList">
<template v-if="item.check_comment">
<div style="font-size: 18px; margin-top: 10px;" :key="index">
<div>简答题{{index+1}}</div>
<div>
老师评语:
<div style="display: inline-block" v-html="item.check_comment || ''"></div>
</div>
</div>
</template>
</template>
</template>
</template>
<template v-else-if="status.type === 0">
<div class="no-exam">考试结束 - 尚未进行考试</div>
</template>
<template v-else>
<div class="no-exam">考试结束 - 试卷批改中,请耐心等待</div>
</template>
</template>
<template v-else>
<div class="play-paper-title">
<div>
<h3>{{exam.title}}</h3>
</div>
</div>
<template v-if="status.isStart">
<div class="play-paper-content play-chapter-exam">
<template v-if="exam.id">
<div class='exam'>
<div style='text-align: center;'>
<div class='topic'>
<div class="exam">
<!-- <div style='text-align: center;'> -->
<!-- <div class='topic'> -->
<!-- <div class='tit'>{{exam.title}}</div> -->
<template v-if='exam.score.total !== undefined'><div class='cur'>单选:{{exam.score.radio}}分,多选:{{exam.score.checkbox}}分,简答:{{exam.score.shortAnswer}}分,总分:{{exam.score.total}}</div></template>
<template v-if="exam.type === 2 && exam.isPublished === 1">
<template v-if="exam.score.total !== undefined">
<div
style="font-size: 18px;"
>得分:单选:{{exam.score.radio}}分,多选:{{exam.score.checkbox}}分,简答:{{exam.score.shortAnswer}}分,总分:{{exam.score.total}}</div>
</template>
<template v-if="exam.shortAnswerList.length">
<template v-for="(item, index) in exam.shortAnswerList">
<template v-if="item.check_comment">
<div style="font-size: 18px; margin-top: 10px;" :key="index">
<div>简答题{{index+1}}</div>
<div>
老师评语:
<div style="display: inline-block" v-html="item.check_comment || ''"></div>
</div>
</div>
</template>
</template>
</template>
</template>
<template v-else-if="exam.type === 1 || exam.type === 2">
<div class="no-exam">试卷批改中,请耐心等待</div>
</template>
<!-- </div> -->
<!-- </div> -->
<template v-if="(exam.type !== 1 && exam.type !== 2)">
<div style="text-align: center;">考试截止时间为:{{status.terminateTime}}</div>
<!-- 单选题 -->
<template v-if='exam.radioList.length'>
<template v-for='(item, index) in exam.radioList'>
<div v-bind:key="item.id" class='q-group' :data-index='index'>
<div class='q-num'>{{index+1}}.</div><div class='q-title' v-html='item.content'></div><div class='q-type'>(单选题)</div>
<el-radio-group class='radio-group' v-model="item.user_answer">
<template v-for='(item1, index1) in item.options'>
<el-radio v-bind:key="item1.id" :label='item1.id' :disabled='!!item.right_answer && !!exam.type' :class='["radio", ((item.right_answer && !!exam.type) ? (item1.id === item.right_answer ? "success" : "error") : "")]'>{{ index1 | getLetter() }}. {{item1.option}}</el-radio>
<template v-if="exam.radioList.length">
<template v-for="(item, index) in exam.radioList">
<div v-bind:key="item.id" class="q-group" :data-index="index">
<div class="q-num">{{index+1}}.</div>
<div class="q-title" v-html="item.content"></div>
<div class="q-type">(单选题)</div>
<el-radio-group class="radio-group" v-model="item.user_answer">
<template v-for="(item1, index1) in item.options">
<el-radio
v-bind:key="item1.id"
:label="item1.id"
:disabled="!!item.right_answer && !!exam.type"
:class="['radio', ((item.right_answer && !!exam.type) ? (item1.id === item.right_answer ? 'success' : 'error') : '')]"
>{{ index1 | getLetter() }}. {{item1.option}}</el-radio>
</template>
</el-radio-group>
<template v-if='item.right_answer && !!exam.type'><div class='result'>学生答案:<div :class='["stu", (item.right_answer === item.user_answer ? "success" : "error")]'>{{ item.user_answer | getRadioAnswer(item.options) }}</div>&nbsp;&nbsp;&nbsp;&nbsp;正确答案:{{ item.right_answer | getRadioAnswer(item.options) }}</div></template>
<template v-if="item.right_answer && !!exam.type">
<div class="result">
学生答案:
<div
:class="['stu', (item.right_answer === item.user_answer ? 'success' : 'error')]"
>{{ item.user_answer | getRadioAnswer(item.options) }}</div>
&nbsp;&nbsp;&nbsp;&nbsp;正确答案:{{ item.right_answer | getRadioAnswer(item.options) }}
</div>
</template>
</div>
</template>
</template>
<!-- 多选题 -->
<template v-if='exam.checkboxList.length'>
<template v-for='(item, index) in exam.checkboxList'>
<div v-bind:key="item.id" class='q-group' :data-index='index'>
<div class='q-num'>{{exam.radioList.length+index+1}}.</div><div class='q-title' v-html='item.content'></div><div class='q-type'>(多选题)</div>
<el-checkbox-group class='checkbox-group' v-model="item.user_answer">
<template v-for='(item1, index1) in item.options'>
<el-checkbox v-bind:key="item1.id" :label='item1.id' :disabled='!!item.right_answer.length && !!exam.type' :class='["checkbox", ((item.right_answer.length && !!exam.type) ? (isCheckboxChecked(item1.id, item.right_answer) ? "success" : "error") : "")]'>{{ index1 | getLetter() }}. {{item1.option}}</el-checkbox>
<template v-if="exam.checkboxList.length">
<template v-for="(item, index) in exam.checkboxList">
<div v-bind:key="item.id" class="q-group" :data-index="index">
<div class="q-num">{{exam.radioList.length+index+1}}.</div>
<div class="q-title" v-html="item.content"></div>
<div class="q-type">(多选题)</div>
<el-checkbox-group class="checkbox-group" v-model="item.user_answer">
<template v-for="(item1, index1) in item.options">
<el-checkbox
v-bind:key="item1.id"
:label="item1.id"
:disabled="!!item.right_answer.length && !!exam.type"
:class="['checkbox', ((item.right_answer.length && !!exam.type) ? (isCheckboxChecked(item1.id, item.right_answer) ? 'success' : 'error') : '')]"
>{{ index1 | getLetter() }}. {{item1.option}}</el-checkbox>
</template>
</el-checkbox-group>
<template v-if='item.right_answer.length && !!exam.type'><div class='result'>学生答案:<div :class='["stu", ((item.right_answer.length && isCheckboxRight(item.user_answer, item.right_answer)) ? "success" : "error")]'>{{ item.user_answer | getCheckboxAnswer(item.options) }}</div>&nbsp;&nbsp;&nbsp;&nbsp;正确答案:{{ item.right_answer | getCheckboxAnswer(item.options) }}</div></template>
<template v-if="item.right_answer.length && !!exam.type">
<div class="result">
学生答案:
<div
:class="['stu', ((item.right_answer.length && isCheckboxRight(item.user_answer, item.right_answer)) ? 'success' : 'error')]"
>{{ item.user_answer | getCheckboxAnswer(item.options) }}</div>
&nbsp;&nbsp;&nbsp;&nbsp;正确答案:{{ item.right_answer | getCheckboxAnswer(item.options) }}
</div>
</template>
</div>
</template>
</template>
<!-- 简答题 -->
<template v-if='exam.shortAnswerList.length'>
<template v-for='(item, index) in exam.shortAnswerList'>
<div v-bind:key="index" class='q-group'>
<div class='q-sa-title'>{{exam.radioList.length+exam.checkboxList.length+index+1}}.&nbsp;&nbsp;简答题</div>
<template v-if="exam.shortAnswerList.length">
<template v-for="(item, index) in exam.shortAnswerList">
<div class="q-group" :key="index">
<div
class="q-sa-title"
>{{exam.radioList.length+exam.checkboxList.length+index+1}}.&nbsp;&nbsp;简答题</div>
<div class="edit_html" v-html="item.content || ''"></div>
<textarea :id="('editor-exam' + index)" v-model="item.user_answer"></textarea>
<div style="height: 10px;"></div>
<component :is="item.upload.type" v-bind:key="item.upload.model" :item="item.upload" :formData="item" :isUpload="!exam.type"></component>
<!-- 利用key值自动更新组件 -->
<component
:is="item.upload.type"
v-bind:key="item.upload.id + new Date().getTime()"
:item="item.upload"
:formData="item"
:isUpload="!exam.type"
></component>
</div>
</template>
</template>
<div :class='["btn", (exam.type && "on")]' @click='submitExam' :data-submit='!!exam.type' @mousedown='_SubmitMouseLeftDown()'>{{exam.type ? "已提交" : "提交"}}</div>
<div class='care'>(注意:测试只有一次提交机会)</div>
<div
:class="['btn', (exam.type && 'on')]"
@click="submitExam"
:data-submit="!!exam.type"
@mousedown="_SubmitMouseLeftDown()"
>{{exam.type ? "已提交" : "提交"}}</div>
<div class="care">(注意:考试只有一次提交机会)</div>
<!-- <div :class='["btn"]' @click='repeatExam($event, true)' v-if="exam.work_contents">重做</div> -->
</template>
</div>
</template>
</div>
</template>
<template v-else>
<div class="exam">
<!-- <p>考试须知:</p> -->
<div style="text-align: left;">考试开始时间为:{{status.startTime}}</div>
<div style="text-align: left;">考试截止时间为:{{status.terminateTime}}</div>
<template
v-if="new Date(status.startTime).getTime() - new Date(status.serverTime).getTime() <= 0"
>
<div
style="width: 25%;"
:class="['btn']"
@click="beginExam(true)"
@mousedown="beginExam(true)"
>开始考试</div>
</template>
</div>
</template>
</template>
</div>
</div>
</div>
</template>
<script>
......@@ -68,21 +199,34 @@ import Base64 from 'Base64'
import CKEDITOR from 'CKEDITOR'
var getLetter = (val) => {
var getLetter = val => {
switch (val) {
case 0: return 'A'
case 1: return 'B'
case 2: return 'C'
case 3: return 'D'
case 4: return 'E'
case 5: return 'F'
case 6: return 'G'
case 7: return 'H'
case 8: return 'I'
case 9: return 'J'
case 10: return 'K'
case 11: return 'L'
case 12: return 'M'
case 0:
return 'A'
case 1:
return 'B'
case 2:
return 'C'
case 3:
return 'D'
case 4:
return 'E'
case 5:
return 'F'
case 6:
return 'G'
case 7:
return 'H'
case 8:
return 'I'
case 9:
return 'J'
case 10:
return 'K'
case 11:
return 'L'
case 12:
return 'M'
}
}
......@@ -115,13 +259,13 @@ export default {
return str.substr(0, str.length - 1)
}
},
data () {
data() {
return {
_time: null, // 定时器,自动化提交
exam: {
// id: '1',
// title: '标题',
// type: 0, // 0: 暂存,可以继续答题;1: 提交,不能再继续答题
// type: 0, // 0: 暂存,可以继续答题;1: 提交,不能再继续答题(等待批改) 2: 已批改(这时候有分数)
// radioList: [
// {
// id: '6622309081933676544',
......@@ -242,31 +386,35 @@ export default {
// }
// }
// ]
}
}
},
mounted () {
this.loadAjax()
if (this._time) {
clearInterval(this._time)
this._time = null
status: {
isStart: false,
startTime: '',
terminateTime: '',
serverTime: '',
examinationStatus: '',
type: 0,
isPublished: 0
}
this._time = setInterval(() => {
if (!this.exam.type) {
this.submitExam({ submitType: true }) // 暂存, submitType: true 暂存;其他或不填为提交
} else {
clearInterval(this._time)
this._time = null
}
}, 30000)
},
destroyed () {
mounted() {
this.init()
this.$emit('changeSideBar', '')
setTimeout(() => {
if (window.document.getElementById('switch-btn')) {
window.document.getElementById('switch-btn').style.display = 'none'
}
}, 500)
},
destroyed() {
if (this._time) {
console.log(11)
clearInterval(this._time)
this._time = null
}
if (window.document.getElementById('switch-btn')) {
window.document.getElementById('switch-btn').style.display = 'block'
}
},
methods: {
isCheckboxRight: (val, arr) => {
......@@ -279,7 +427,10 @@ export default {
break
}
}
if (j === val.length) { flag = false; break }
if (j === val.length) {
flag = false
break
}
}
return flag
},
......@@ -292,11 +443,15 @@ export default {
}
return false
},
initckeditor () {
if (!this.exam.shortAnswerList) { return }
initckeditor() {
if (!this.exam.shortAnswerList) {
return
}
/* 删除所有 ckeditor 实例 */
const instances = CKEDITOR.instances
for (const name in instances) { instances[name].destroy() }
for (const name in instances) {
instances[name].destroy()
}
for (let i = 0; i < this.exam.shortAnswerList.length; i++) {
if (!instances['editor-exam' + i]) {
CKEDITOR.replace('editor-exam' + i, {
......@@ -305,16 +460,51 @@ export default {
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: '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: '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: '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: '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'] }
]
......@@ -323,49 +513,169 @@ export default {
this.exam.shortAnswerList[i].ckeditor = instances['editor-exam' + i]
}
},
/**
* 生命周期函数--监听页面加载
*/
loadAjax () {
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
if (this.id !== '0') {
cAction.examAction.getExamAnswer(this.cid, this.sid, this.id).then(_data => {
if (_data.code === 8001) {
cAction.examAction.getExamInfo(this.cid, this.sid).then(_data => {
this.exam = _data
this.exam.id = this.id
}).catch(e => { this.$message.error(e.message) }).finally(() => {
/* 滚动到头部 */
document.querySelector('.play-paper').scrollTop = 0
loading.close()
this.initckeditor()
})
return
init() {
this.loadExamStatus()
this.loadExamInfo()
if (this._time) {
clearInterval(this._time)
this._time = null
}
this._time = setInterval(() => {
this.loadExamStatus()
if (!this.exam.type && this.status.isStart) {
// console.log(11, '暂存')
this.submitExam({ submitType: true }) // 暂存, submitType: true 暂存;其他或不填为提交
}
/* 到时间 自动提交 */
if (
!this.exam.type &&
this.status.isStart &&
new Date(this.status.terminateTime).getTime() -
new Date(this.status.serverTime).getTime() <=
5000
) {
this.submitExam({ submitType: false, currentTarget: { dataset: {} } })
}
}, 3000)
},
/* 定时调用 - 考试状态 */
loadExamStatus() {
cAction.Player.getExamStatus(this.cid, this.sid, this.id)
.then(_data => {
if (_data.status && _data.status === 200) {
this.status.startTime = this.setTime(_data.start_time)
this.status.terminateTime = _data.terminate_time
this.status.serverTime = this.setTime(_data.server_time)
this.status.examinationStatus = _data.examination_status
} else {
this.$message.error('数据异常,请联系管理员')
}
})
.catch(e => {
this.$message.error(e.message)
})
.finally(() => {})
},
/* 时间格式化(解决safari时间兼容问题); */
setTime(time) {
return time.replace(/-/g, '/')
},
/* 开始考试 */
beginExam(flag) {
this.status.isStart = true
this.loadAjax(flag)
},
/* 加载题库基本数据 */
loadExamInfo() {
const loading = this.$loading({
lock: true,
text: '',
spinner: '',
background: 'rgba(255, 255, 255, 0.9)'
})
cAction.Player.getExamInfo(this.cid, this.sid)
.then(_data => {
this.exam = _data
this.exam.id = this.id
}).catch(e => { this.$message.error(e.message) }).finally(() => {
/* 滚动到头部 */
document.querySelector('.play-paper').scrollTop = 0
})
.catch(e => {
this.$message.error(e.message)
})
.finally(() => {
loading.close()
this.initckeditor()
/* 正在考试,考试结束 */
if (
this.status.examinationStatus === '20' ||
this.status.examinationStatus === '90'
) {
this.beginExam()
}
})
},
/**
* 生命周期函数--监听页面加载
* @param flag 通过该字段 判别是点击进入考试 还是 自动调用
*/
loadAjax(flag) {
const loading = this.$loading({
lock: true,
text: '',
spinner: '',
background: 'rgba(255, 255, 255, 0.9)'
})
cAction.Player.getExamAnswer(this.cid, this.sid, this.id)
.then(_data => {
if (_data.code === 8001) {
console.log(
'没有考试内容,认为是第一次答题,并且在答题期间,所以显示考试开始页面'
)
if (!flag) {
this.status.isStart = false
}
} else {
cAction.examAction.getExamInfo(this.cid, this.sid).then(_data => {
this.exam = _data
this.exam.id = this.id
}).catch(e => { this.$message.error(e.message) }).finally(() => {
// this.exam.id = _data.id
this.exam.title = _data.title
this.exam.type = _data.type
this.exam.score = _data.score
this.exam.submitted_time = _data.submitted_time
this.exam.isPublished = _data.isPublished
this.status.type = _data.type
this.status.isPublished = _data.isPublished
for (let i = 0; i < this.exam.radioList.length; i++) {
for (let j = 0; j < _data.radioList.length; j++) {
if (_data.radioList[j].id === this.exam.radioList[i].id) {
for (const k in _data.radioList[j]) {
this.exam.radioList[i][k] = _data.radioList[j][k]
}
}
}
}
for (let i = 0; i < this.exam.checkboxList.length; i++) {
for (let j = 0; j < _data.checkboxList.length; j++) {
if (_data.checkboxList[j].id === this.exam.checkboxList[i].id) {
for (const k in _data.checkboxList[j]) {
this.exam.checkboxList[i][k] = _data.checkboxList[j][k]
}
}
}
}
for (let i = 0; i < this.exam.shortAnswerList.length; i++) {
for (let j = 0; j < _data.shortAnswerList.length; j++) {
if (
_data.shortAnswerList[j].id ===
this.exam.shortAnswerList[i].id
) {
for (const k in _data.shortAnswerList[j]) {
this.exam.shortAnswerList[i][k] =
_data.shortAnswerList[j][k]
}
}
}
}
}
})
.catch(e => {
this.$message.error(e.message)
})
.finally(() => {
// console.log(this.exam.type, this.exam.isPublished)
loading.close()
if (
this.status.isStart &&
this.exam.type !== 1 &&
this.exam.type !== 2
) {
/* 滚动到头部 */
document.querySelector('.play-paper').scrollTop = 0
loading.close()
this.initckeditor()
})
}
})
},
/**
* 提交试题
*/
submitExam (e) {
submitExam(e) {
if (!e.submitType && e.currentTarget.dataset.submit) {
this.$message.error('已做过,不能再提交')
return
......@@ -405,28 +715,49 @@ export default {
}
body.answers.shortAnswerList.push({
id: tmp.id,
user_answer: Base64.encode(tmp.user_answer, 'utf-8'),
user_answer: Base64.encode(tmp.user_answer),
attachments: tmp.attachments
})
}
body.answers = JSON.stringify(body.answers)
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.examAction.submitExam(this.cid, this.sid, this.exam.id, body).then(_res => {
let loading = null
if (!e.submitType) {
loading = this.$loading({
lock: true,
text: '',
spinner: '',
background: 'rgba(255, 255, 255, 0.9)'
})
}
cAction.Player.submitExam(this.cid, this.sid, this.exam.id, body)
.then(_res => {
if (e.submitType) {
this.$message.success('暂存成功')
// this.$message.success('暂存成功')
console.log('暂存成功')
return
}
if (_res.code === 200) {
this.loadAjax()
this.$message.success('考试答卷提交成功')
// this.exam.type = 1
this.init()
console.log(111, 'this.loadAjax()')
} else {
this.$message.error(_res.data.error)
}
}).catch(e => { this.$message.error(e.message) }).finally(() => { loading.close() })
})
.catch(e => {
this.$message.error(e.message)
})
.finally(() => {
if (!e.submitType) {
loading.close()
}
})
},
_SubmitMouseLeftDown () {
_SubmitMouseLeftDown() {
const _fn1 = this.repeatExam.bind(this, false)
document.addEventListener('keydown', _fn1, false)
const _fn3 = function () {
const _fn3 = function() {
document.removeEventListener('keydown', _fn1)
document.removeEventListener('mouseup', _fn3)
}
......@@ -435,25 +766,38 @@ export default {
/**
* 重做
*/
repeatExam (e, flag) {
repeatExam(e, flag) {
let _flag = flag
/* 字母 f */
if (e.keyCode === 70) {
_flag = true
}
if (!_flag) { return }
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
cAction.Player.getExamDetail(this.sid, this.cid, this.id).then(_data => {
if (!_flag) {
return
}
const loading = this.$loading({
lock: true,
text: '',
spinner: '',
background: 'rgba(255, 255, 255, 0.9)'
})
cAction.chapterAction
.getExamDetail(this.sid, this.cid, this.id)
.then(_data => {
this.exam = {}
}).catch(e => { this.$message.error(e.message) }).finally(() => {
})
.catch(e => {
this.$message.error(e.message)
})
.finally(() => {
loading.close()
})
}
},
watch: {
id: {
handler () {
this.loadAjax()
handler() {
this.init()
}
}
}
......@@ -462,29 +806,124 @@ export default {
<style lang="scss" scoped>
.play {
.exam { padding: 0; }
.exam .topic { display: inline-block; margin-bottom: 0.1rem; }
.exam .topic .tit { margin: 0 auto; padding: 0 0.2rem; text-align: center; font-size: 0.24rem; color: #313131; background: #fff; box-sizing: border-box; -webkit-box-sizing: border-box; }
.exam .topic .cur { text-align: center; font-size: 0.18rem; color: #313131; line-height: 0.4rem; }
.exam {
padding: 0;
}
.exam .topic {
display: inline-block;
margin-bottom: 0.1rem;
}
.exam .topic .tit {
margin: 0 auto;
padding: 0 0.2rem;
text-align: center;
font-size: 0.24rem;
color: #313131;
background: #fff;
box-sizing: border-box;
-webkit-box-sizing: border-box;
}
.exam .topic .cur {
text-align: center;
font-size: 0.18rem;
color: #313131;
line-height: 0.4rem;
}
/* 循环 所有选择题 */
.exam .q-group { padding: 0.1rem 0.1rem; border-bottom: 1px solid #c9c9c97a; overflow: hidden; }
.exam .q-group .q-num { float: left; margin-right: 0.1rem; font-size: 0.16rem; color: #676a6c; }
.exam .q-group .q-title { float: left; width: 90%; font-size: 0.16rem; color: #676a6c; text-align: justify; }
.exam .q-group .q-type { float: right; font-size: 0.16rem; color: #676a6c; }
.exam .q-group .radio-group { float: left; margin-top: 0.1rem; width: 100%; }
.exam .q-group .radio-group .radio { display: block; font-size: 0.18rem; color: #3f3b3a; line-height: 0.3rem; margin-bottom: 0.1rem; }
.exam .q-group .checkbox-group { float: left; margin-top: 0.1rem; width: 100%; }
.exam .q-group .checkbox-group .checkbox { display: block; font-size: 0.18rem; color: #3f3b3a; line-height: 0.3rem; margin-bottom: 0.1rem; }
.exam .q-group .radio-group .radio.error, .exam .q-group .checkbox-group .checkbox.error { color: #d80000; }
.exam .q-group .radio-group .radio.success, .exam .q-group .checkbox-group .checkbox.success { color: #090; }
.exam .q-group .result { float: right; font-size: 0.18rem; color: #3f3b3a; margin-right: 0; }
.exam .q-group .result .stu { display: inline-block; }
.exam .q-group .result .stu.error { color: #d80000; }
.exam .q-group .result .stu.success { color: #090; }
.exam .q-group:last-child { border-bottom: none; }
.exam .btn { margin: 0.2rem auto; width: 60%; height: 0.5rem; line-height: 0.5rem; font-size: 0.16rem; text-align: center; font-weight: 300; color: #fff; border-radius: 0.1rem; background: #b49441; cursor: pointer; }
.exam .btn.on { opacity: 0.5; }
.exam .care { font-size: 0.16rem; color: #d80000; text-align: center; }
.exam .q-group {
padding: 0.1rem 0.1rem;
border-bottom: 1px solid #c9c9c97a;
overflow: hidden;
}
.exam .q-group .q-num {
float: left;
margin-right: 0.1rem;
font-size: 0.16rem;
color: #676a6c;
}
.exam .q-group .q-title {
float: left;
width: 90%;
font-size: 0.16rem;
color: #676a6c;
text-align: justify;
}
.exam .q-group .q-type {
float: right;
font-size: 0.16rem;
color: #676a6c;
}
.exam .q-group .radio-group {
float: left;
margin-top: 0.1rem;
width: 100%;
}
.exam .q-group .radio-group .radio {
display: block;
font-size: 0.18rem;
color: #3f3b3a;
line-height: 0.3rem;
margin-bottom: 0.1rem;
}
.exam .q-group .checkbox-group {
float: left;
margin-top: 0.1rem;
width: 100%;
}
.exam .q-group .checkbox-group .checkbox {
display: block;
font-size: 0.18rem;
color: #3f3b3a;
line-height: 0.3rem;
margin-bottom: 0.1rem;
}
.exam .q-group .radio-group .radio.error,
.exam .q-group .checkbox-group .checkbox.error {
color: #d80000;
}
.exam .q-group .radio-group .radio.success,
.exam .q-group .checkbox-group .checkbox.success {
color: #090;
}
.exam .q-group .result {
float: right;
font-size: 0.18rem;
color: #3f3b3a;
margin-right: 0;
}
.exam .q-group .result .stu {
display: inline-block;
}
.exam .q-group .result .stu.error {
color: #d80000;
}
.exam .q-group .result .stu.success {
color: #090;
}
.exam .q-group:last-child {
border-bottom: none;
}
.exam .btn {
margin: 0.2rem auto;
width: 60%;
height: 0.5rem;
line-height: 0.5rem;
font-size: 0.16rem;
text-align: center;
font-weight: 300;
color: #fff;
border-radius: 0.1rem;
background: #b49441;
cursor: pointer;
}
.exam .btn.on {
opacity: 0.5;
}
.exam .care {
font-size: 0.16rem;
color: #d80000;
text-align: center;
}
.exam .q-sa-title {
float: left;
......@@ -494,4 +933,9 @@ export default {
text-align: justify;
}
}
.no-exam {
font-size: 0.24rem;
line-height: 19;
text-align: center;
}
</style>
// import viewerRoutes from '@/modules/viewer/routes.js'
export default [
{ path: '/', redirect: '/login/is-login' },
{
......@@ -237,5 +239,6 @@ export default [
// { path: '/survey-phone/*', redirect: '/learn-error/learn-error' },
/* 如果所有页面都没找到 - 指向 */
{ path: '*', component: () => import('@/components/errorPages/404.vue') }
// { path: '/viewer', component: () => import('@/modules/viewer/index.vue') }
// viewer module routes
// ...viewerRoutes
]
const fs = require('fs')
const conf = require('../config')
const com = require('@god/node-com')
const axios = require('axios')
const AdmZip = require('adm-zip')
/* 支持 finally函数 */
require('promise.prototype.finally').shim()
/* 通过API统一过拦截器,接口代理转发请求 */
const agentProcessor = () => {
/* 目录在服务器,启动时,直接创建号 */
let _totalDir = '001' // 默认表示,存储从第一学期开始
com.Tool.Directory.mkdir('upload_tmp/' + _totalDir)
return (req, res) => {
/* 获取 日志必须 返回 + 视频行为日志信息 */
const _logJson = com.Log.EzijingVideo.setLogVideo(req, {
_totalDir
})
let headers = req.headers
let options = {}
/* 删除一些 不必要属性 */
let pwdBase64 = headers['pmd5'] || ''
delete headers['pmd5'] // pwd base64
try {
options = {
timeout: 60 * 1000,
url: req.params[0], // .replace(/tenant\//gi, '')
baseURL: conf.agentApiUrl,
method: req.method,
data: req.body,
params: req.query
}
options.url = options.url.replace(/tenant\//gi, '')
headers['apikey'] = conf.apiKey
if (/user_center/gi.test(options.url)) {
headers['Host'] = 'sso.ezijing.com'
headers['host'] = 'sso.ezijing.com'
} else {
// headers['Host'] = 'lms-api.ezijing.com'
// headers['host'] = 'lms-api.ezijing.com'
headers['Host'] = conf.host
headers['host'] = conf.host
}
/* 测试 */
// if (/\/essay/gi.test(options.url) && /post/gi.test(req.method)) {
// console.time('essay')
// options.baseURL = 'http://192.168.10.199:8081/'
// options.url = options.url.replace(/tenant\//gi, '')
// console.log(options.baseURL + options.url)
// }
headers['accept'] = '*/*'
delete headers['accept-language']
com.Tool.ReqType.toUrlEncoded(req, options)
let _fileName = com.Tool.ReqType.toFormData(req, options)
/* 获取cookie,解析SUP,设置token */
headers['ticket'] = com.Tool.Cookie.getTicketByCAS('_SUP', req) // _SUP获取
headers['token'] = headers['ticket']
axios.defaults.headers = headers
axios.defaults.withCredentials = true
/* 处理特殊请求,在域名为e-learing2时生效 */
// let _stoken = com.Tool.Cookie.getCookie('_SUP', req)
// if (_stoken) {
// headers['stoken'] = _stoken
// }
/* 重新转发请求 */
let _reqTime = 0
let _status = 0
axios(options)
.then((data) => {
/* 记录 请求处理时长 + 请求状态 */
_reqTime =
new Date().getTime() - _logJson._nodeServerCurrentTime + 'ms'
_status = 200
// /* 兼容老版本,登录接口 清除_SUP; 退出登录 清除_SUP */
/* 重新修改,改为只能服务端 设置 cookie */
if (
/user_center\/login/gi.test(options.url) ||
/user_center\/code_login/gi.test(options.url)
) {
com.Tool.Cookie.setTicketByCAS(
'_SUP',
'.ezijing.com',
data.data.ticket,
res
)
com.Tool.Cookie.setCookie('_AUTH', '.ezijing.com', pwdBase64, res)
}
if (/user_center\/logout/gi.test(options.url)) {
res.clearCookie('_SUP', { path: '/', domain: '.ezijing.com' })
res.clearCookie('_AUTH', { path: '/', domain: '.ezijing.com' })
res.clearCookie('TGC', { path: '/', domain: '.ezijing.com' })
res.clearCookie('CAS_USER_ID', { path: '/', domain: '.ezijing.com' })
}
/* 如果是大作业提价,先docx解析,解析不出来则报错 */
if (
/util\/upload-file/gi.test(options.url) &&
req.body.special &&
req.body.special === 'course-work'
) {
let _strContent = ''
let contentXml = null
try {
let zip = new AdmZip(_fileName[0]) // filePath为文件路径
contentXml = zip.readAsText('word/document.xml') // 将document.xml读取为text内容;
_strContent = contentXml.replace(/<[/]{0,1}\w[^>]*>/gi, '')
// contentXml.match(/<w:t[^>]*?>[\s\S]*?<\/w:t>/ig).forEach((item) => {
// _strContent += item.slice(5, -6) // 不能换行,应该是 解析某些换行规则会比较慢
// })
data.data.dataStr = Buffer.from(_strContent, 'utf8').toString(
'base64'
)
} catch (e) {
res.status(500).json({
message:
'文档上传出错,错误原因:请不要直接修改后缀“.doc”为“.docx”或者正文内容必须大于400字',
errMsg: 'err',
code: 500
})
return
}
}
/* getInfo 时,则写入 用户基本信息 */
if (/user_center\/get_user_info/gi.test(options.url)) {
/* check-access接口 */
// headers['Host'] = 'lms-api.ezijing.com'
// headers['host'] = 'lms-api.ezijing.com'
headers['Host'] = conf.host
headers['host'] = conf.host
axios.defaults.headers = headers
axios({
timeout: 30 * 1000,
url: 'user/check-access',
baseURL: conf.agentApiUrl,
method: 'GET',
data: {},
params: ''
})
.then((_data) => {
if (_data.data.id) {
data.data['auth_key'] = headers['token']
data.data['id'] = headers['uid']
data.data.student_info = _data.data
let _name =
(data.data.student_info &&
data.data.student_info.personal_name) ||
data.data.nickname
let _email =
(data.data.student_info && data.data.student_info.email) ||
data.data.email
com.Log.EzijingVideo.writeBasicInfo(req, _logJson, {
_name, // 用于重新 定位学生目录
_email, // 用于重新 定位学生目录
_totalDir // 用于重新 定位学生目录
})
res.status(200).send(data.data)
} else {
res.status(403).send({
name: 'Unauthorized',
message: '非学习系统用户,请联系管理员',
code: 0,
status: 401
})
}
})
.catch((e) => {
try {
res.status(e.response.status).send(e.response.data)
} catch (e) {
res.status(500).send({
name: 'undefined',
message: '接口请求异常',
code: 0,
status: 500
})
}
})
} else {
res.status(200).send(data.data)
}
})
.catch((e) => {
/* 记录 请求处理时长 + 请求状态 */
_reqTime =
new Date().getTime() - _logJson._nodeServerCurrentTime + 'ms'
_status = (e.response && e.response.status) || 500
/* 未登录,则全部清空 _SUP */
if (e.response && e.response.status === 403) {
res.clearCookie('_SUP', { path: '/', domain: '.ezijing.com' })
res.clearCookie('_AUTH', { path: '/', domain: '.ezijing.com' })
}
/* 返回执行代码出错 或者 服务器请求错误 */
if (e.response && e.response.data) {
/* 如果未登录 强制 弹出到 登录页 */
// setPorxyHeader(e.response, res)
res.status(e.response.status).json(e.response.data)
console.error(
_logJson._uuid +
String.fromCharCode(0x001) +
JSON.stringify(options) +
String.fromCharCode(0x001) +
JSON.stringify(axios.defaults.headers) +
String.fromCharCode(0x001) +
JSON.stringify(e.response.data)
)
} else {
res.status(500).json({
message: '系统错误,请稍后重试或联系管理员',
errMsg: 'Error Proxy Request or BackData Excute Error',
code: 500
})
console.error(_logJson._uuid, e)
}
})
.finally(() => {
/* 请求结束,删除服务器端 缓存文件 */
if (_fileName.length) {
for (let i = 0; i < _fileName.length; i++) {
fs.unlinkSync(_fileName[i])
}
}
/* 视频行为日志 写入 */
com.Log.EzijingVideo.writeLogVideo(_logJson, {
_reqTime,
_status
})
})
} catch (e) {
res.status(500).json({
message: '系统错误,请稍后重试或联系管理员',
errMsg: 'Network Server Excute Error',
code: 500
})
console.error(_logJson._uuid, e)
}
}
}
module.exports = {
agentProcessor: agentProcessor
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论