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

update

上级 f63bcf66
......@@ -8,6 +8,10 @@ export function login(data) {
export function bindWechat(data) {
return httpRequest.post('/api/passport/rest/wechat/bind-unionid', data)
}
// 修改密码
export function updatePassword(data) {
return httpRequest.post('/api/usercenter/user/change-pwd-by-cookie', data)
}
// 重置密码
export function resetPassword(data) {
return httpRequest.post('/api/usercenter/user/update-pwd', data)
......
import httpRequest from '@/utils/axios'
/**
* 获取免费课程列表
*/
export function getFreeCourseList() {
return httpRequest.get('/api/zy/v2/education/freecourse')
}
/**
* 获取课程列表
*/
......@@ -42,31 +36,3 @@ export function getCourseTagList(courseId) {
export function getCourseTag(tagId) {
return httpRequest.get(`/api/zy/v2/education/tag/${tagId}`)
}
/**
* 获取搜索记录
*/
export function getSearchTips() {
return httpRequest.get('/api/zy/v2/education/search/tips')
}
/**
* 知识点搜索
*/
export function getSearchTagList(data) {
return httpRequest.post('/api/zy/v2/education/search/tag', data)
}
/**
* 视频课程搜索
*/
export function getSearchCourseVideoList(data) {
return httpRequest.post('/api/zy/v2/education/search/chapter1', data)
}
/**
* 课程搜索
*/
export function getSearchCourseList(data) {
return httpRequest.post('/api/zy/v2/education/search/chapter2', data)
}
......@@ -46,14 +46,12 @@ export function cacheQuestion(data) {
/* 意见反馈 */
export function submitFeedback(data) {
return httpRequest.post('/api/zy/v2/feedback/commit', data, {
return httpRequest.post('/api/zy/v2/feedback/commit', data)
}
/* 获取考试状态 */
export function getExamStatus(data) {
return httpRequest.get('/api/zy/v2/examination/examination-papers-status', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
// /* 获取考试状态 */
// export function getExamStatus(data) {
// return httpRequest.get('/api/zy/v2/examination/examination-papers-status', data, {
// headers: { 'Content-Type': 'multipart/form-data' }
// })
// }
import httpRequest from '@/utils/axios'
/**
* 获取商品详情
*/
export function getGoodsDetails(id) {
return httpRequest.get(`/api/zy/v2/mall/product/${id}`)
}
/**
* 获取我的购买订单
*/
export function getMyOrder() {
return httpRequest.get('/api/zy/v2/mall/order/my')
}
/* 获取openid */
export function getOpenid(data) {
return httpRequest.get('/usercenter/user/get-user-openid', data)
}
/* 监听支付状态 */
export function getOrderStatus() {
return httpRequest.get('/api/zy/v2/mall/order/status')
}
<template>
<div class="course-list" v-loading="!loaded">
<div class="course-list" element-loading-text="加载中..." v-loading="!loaded">
<template v-if="list.length">
<course-list-item v-for="item in list" :data="item" :key="item.id" v-bind="$attrs" v-on="$listeners" />
</template>
......
......@@ -5,9 +5,9 @@
<div class="course-item-content">
<div class="course-item__title">{{ data.title }}</div>
<div class="course-item__tools">
<div class="course-item__text course-item__text__freevideo">{{ data.free_video_num }}个免费视频</div>
<div class="course-item__text course-item__text__video">{{ data.video_num }}节视频课</div>
<div class="course-item__text course-item__text__course">{{ data.course_num }}节课</div>
<div class="course-item__text course-item__text__video">{{ data.video_num }}节视频课</div>
<div class="course-item__text course-item__text__freevideo">{{ data.free_video_num }}个免费视频</div>
</div>
</div>
</div>
......@@ -105,6 +105,9 @@ export default {
padding: 20px 0;
border-bottom: 1px solid #eee;
cursor: pointer;
&:hover {
color: #c01540;
}
.name {
flex: 1;
font-size: 18px;
......
<template>
<el-collapse v-model="activeNames">
<el-collapse-item :title="item.name" :name="item.id" v-for="item in data" :key="item.id">
<template #right-icon>
<van-icon name="arrow" />
<van-icon name="arrow-down" />
</template>
<ul>
<li v-for="subItem in item.children" :key="subItem.id" @click="$emit('on-click', subItem)">
<div class="name">
{{ subItem.name }}
<template v-if="subItem.free">(免费)</template>
</div>
<div class="progress" v-if="showProgress">{{ progressText(subItem.video_progress, subItem.free) }}</div>
</li>
</ul>
</el-collapse-item>
</el-collapse>
</template>
<script>
export default {
props: {
courseId: { type: String },
data: { type: Array, required: true, default: () => [] },
showProgress: { type: Boolean, default: false }
},
data() {
return {
activeNames: []
}
},
computed: {
isVip() {
return this.$store.state.isVip
}
},
methods: {
progressText(value, isFree) {
value = parseInt(value)
if (value === 0) {
return isFree || this.isVip ? '未开始' : '未开通'
}
if (value === 100) {
return '已学完'
}
return `已学${value}%`
}
}
}
</script>
<style lang="scss" scoped>
::v-deep .van-cell {
padding-left: 0;
padding-right: 0;
}
::v-deep .el-collapse-item__content {
padding: 0;
}
::v-deep .el-collapse-item--border::after {
left: 0;
right: 0;
}
::v-deep .el-collapse-item__title--expanded::after {
display: none;
}
li {
display: flex;
padding: 10px 0;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 20px;
}
.name {
flex: 1;
font-size: 13px;
overflow: hidden;
}
.progress {
margin-left: 20px;
font-size: 12px;
color: #999;
}
}
</style>
......@@ -30,7 +30,7 @@ export default {
{ title: '错题集合', icon: '', path: '/exam' },
{ title: '收藏试题', icon: '', path: '/exam' },
{ title: '必考考点', icon: '', path: '/exam' },
{ title: '考证课程', icon: '', path: '/course/learn' },
{ title: '考证课程', icon: '', path: '/course' },
{ title: '意见反馈', icon: '', path: '/feedback' },
{ title: '联系客服', icon: '', path: '/contact' }
]
......@@ -85,7 +85,7 @@ export default {
img {
width: 100%;
height: 100%;
object-fit: contain;
object-fit: cover;
}
}
.user-tools {
......
......@@ -2,7 +2,7 @@
<div class="app-layout">
<app-header />
<div class="app-layout-bd">
<app-aside v-bind="$attrs" />
<app-aside v-bind="$attrs" v-if="showAside" />
<app-main />
</div>
</div>
......@@ -13,7 +13,8 @@ import AppHeader from './header'
import AppAside from './aside'
import AppMain from './main'
export default {
components: { AppHeader, AppAside, AppMain }
components: { AppHeader, AppAside, AppMain },
props: { showAside: { type: Boolean, default: true } }
}
</script>
......
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(
`/api/lms/v2/education/courses/${semesterId}/${courseId}`
)
}
/**
* 获取章节资源详情
* @param {string} vid 资源ID
*/
export function getChapterVideo(vid) {
return httpRequest.post(
'/api/lms/v2/education/video-streaming',
{ vid },
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取章节资源详情
* @param {string} vid 章节的资源ID
*/
export function getChapterVideoAliyun(vid) {
return httpRequest.post(
'/api/lms/v2/education/aliyun-video-streaming',
{ vid },
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取章节视频播放进度
* @param {string} semesterId 学期ID
* @param {string} resourseId 章节的资源ID
* @param {Object} params
*/
export function getChapterVideoProgress(semesterId, resourseId, params) {
return httpRequest.get(
`/api/lms/v2/education/video/${semesterId}/${resourseId}/device`,
params
)
}
/**
* 更新章节视频播放进度
* @param {Object} params
*/
export function updateChapterVideoProgress(params) {
return httpRequest.get('/api/lms/v2/analytics/upload-video', params)
}
/**
* 获取章节作业
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} resourseId 章节的资源ID
*/
export function getChapterHomework(semesterId, courseId, resourseId) {
return httpRequest.get(
`/api/lms/v2/education/homeworks/${semesterId}/${courseId}/${resourseId}`
)
}
/**
* 获取提交作业截止时间
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} chapterId 章节ID
*/
export function getChapterHomeworkDeadline(semesterId, courseId, chapterId) {
return httpRequest.get(
`/api/lms/v2/education/homeworks/${semesterId}/${courseId}/${chapterId}/deadline`
)
}
/**
* 提交考试
*/
export function sbumitChapterHomework(params) {
return httpRequest.post('/api/lms/v2/education/homeworks', params, {
headers: { 'Content-Type': 'application/json' }
})
}
/**
* 上传文件
*/
export function uploadFile(data) {
return httpRequest.post('/api/lms/util/upload-file', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* 获取课程大作业详情
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function getCourseWork(semesterId, courseId) {
return httpRequest.get(
`/api/lms/v2/education/courses/${semesterId}/${courseId}/essay`
)
}
/**
* 提交课程大作业
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function updateCourseWork(semesterId, courseId, data) {
return httpRequest.post(
`/api/lms/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(
`/api/lms/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(
`/api/lms/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(
`/api/lms/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, params) {
return httpRequest.get(
`/api/lms/v2/education/${semesterId}/${courseId}/examination/${examId}/sheet`,
params
)
}
<template>
<ul class="chapter-list">
<li class="chapter-item" v-for="item in chapters" :key="item.id">
<h4>{{ item.name }}</h4>
<ul class="chapter-item-list">
<li
v-for="subItem in item.children"
:key="subItem.id"
@click="onClick(subItem)"
:class="{ 'is-active': subItem.id === (active ? active.id : '') }"
>
<span class="chapter-item-list__name">{{ subItem.name | showName(subItem) }}</span>
<i class="el-icon" :class="genIconClass(subItem.type)"></i>
</li>
</ul>
</li>
</ul>
</template>
<script>
export default {
props: {
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
},
chapters: { type: Array, default: () => [] },
// 当前选中的章节
active: {
type: Object,
default() {
return {}
}
}
},
data() {
return {}
},
filters: {
showName(name, data) {
if ([5, 8].includes(data.type) && data.live) {
return `${name}(${data.live.start_time})`
}
return name
}
},
methods: {
genIconClass(type) {
const map = {
2: 'el-icon-self-iconset0481',
3: 'el-icon-edit-outline',
4: 'el-icon-self-cc-book'
}
return map[type] || 'el-icon-self-cc-book'
},
onClick(data) {
if (data.type === 1) {
return
}
// zoom直播
if (data.type === 8) {
const live = data.live
const hasRecordUrl = live.enable_record && live.record_url
if ([3, 5].includes(live.live_status) && !hasRecordUrl) {
this.$message.error('直播结束')
return
}
window.open(live.record_url || live.join_url)
return
}
// 课程大作业
if (data.id === 'course_work' && !this.data.survey) {
this.$message('请先填写教学评估,然后完成大作业。')
return
}
// 教学评估
if (data.id === 'teach_evaluation') {
const { sid, cid } = this.$route.params
this.$router.push({ name: 'survey', params: { sid, cid } })
return
}
this.$router.push({ name: 'viewerCourseChapter', params: { id: data.id } })
}
}
}
</script>
<style lang="scss" scoped>
/* 章列表样式 */
.chapter-list {
margin: 0;
padding: 0;
line-height: 1.6;
overflow: hidden;
.chapter-item {
h4 {
padding: 10px 22px;
margin: 0;
font-size: 15px;
color: #b0b0b0;
background-color: #2f2f2f;
}
/* 节列表样式 */
.chapter-item-list {
margin: 0;
padding: 0;
line-height: 1.6;
overflow: hidden;
li {
position: relative;
&.is-active {
background: #3c3c3c;
.chapter-item-list__name {
color: #b49441;
}
}
&:hover {
background: #3c3c3c;
}
&:before {
display: block;
content: '';
position: absolute;
left: 13px;
top: 16px;
z-index: 10;
width: 18px;
height: 18px;
background: #5b5b5b;
border: 2px solid #5b5b5b;
border-radius: 50%;
}
&:after {
display: block;
content: '';
position: absolute;
left: 22px;
top: 0;
z-index: 5;
width: 1px;
height: 100px;
background: #616161;
}
}
.chapter-item-list__name {
display: block;
padding: 15px 35px 15px 40px;
font-size: 14px;
color: #909090;
text-decoration: none;
cursor: pointer;
}
}
/* 章节后面小图标的样式 */
.el-icon {
position: absolute;
font-size: 16px;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #a0a0a0;
}
}
}
</style>
<template>
<aside class="course-viewer-aside">
<el-tabs v-model="activeName">
<el-tab-pane label="章节" name="0">
<div class="tab-pane">
<aside-chapter :data="data" :chapters="chapters" :active="active"></aside-chapter>
</div>
</el-tab-pane>
<el-tab-pane label="讲义" name="1" v-if="active && active.type === 2">
<div class="tab-pane">
<aside-lecture :ppts="ppts" :pptIndex="pptIndex" v-on="$listeners"></aside-lecture>
</div>
</el-tab-pane>
</el-tabs>
</aside>
</template>
<script>
import AsideChapter from './chapter.vue'
import AsideLecture from './lecture.vue'
export default {
props: {
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
},
// 章节
chapters: { type: Array, default: () => [] },
// 讲义
ppts: { type: Array, default: () => [] },
// 当前选中的章节
active: {
type: Object,
default() {
return {}
}
},
// 当前选择的PPT
pptIndex: { type: Number, default: 0 }
},
components: { AsideChapter, AsideLecture },
data() {
return {
activeName: '0'
}
}
}
</script>
<style lang="scss" scoped>
.course-viewer-aside {
width: 350px;
min-height: 100vh;
background-color: #232323;
}
.tab-pane {
height: calc(100vh - 56px);
overflow-y: auto;
}
::v-deep .el-tabs__header {
margin: 0;
}
::v-deep .el-tabs__nav {
float: none;
display: flex;
}
::v-deep .el-tabs__item {
flex: 1;
height: 56px;
font-size: 16px;
line-height: 56px;
color: #909090;
text-align: center;
&.is-active {
color: #b49441;
}
}
::v-deep .el-tabs__active-bar,
::v-deep .el-tabs__nav-wrap::after {
display: none;
}
</style>
<template>
<ul class="lecture-list">
<li
v-for="(item, index) in ppts"
:key="item.id"
@click="onClick(index)"
:class="{'is-active': index === activeIndex}"
>
<img :src="item.ppt_url" />
</li>
</ul>
</template>
<script>
export default {
props: {
// 当前选择的PPT
pptIndex: { type: Number, default: 0 },
ppts: { type: Array, default: () => [] }
},
data() {
return {
activeIndex: this.pptIndex
}
},
watch: {
pptIndex(index) {
this.activeIndex = index
}
},
methods: {
// 点击PPT
onClick(index) {
this.activeIndex = index
this.$emit('change-ppt', index)
}
}
}
</script>
<style lang="scss" scoped>
.lecture-list {
padding: 0 16px;
li {
padding: 8px 16px;
cursor: pointer;
&.is-active {
background: #888;
}
img {
width: 100%;
}
}
}
</style>
<template>
<div class="course-viewer-content">
<div class="course-viewer-content-hd">
<slot name="header">
<h3 class="course-viewer-content-hd__title">
<slot name="title">{{title}}</slot>
</h3>
<div class="course-viewer-content-hd__aside">
<slot name="header-aside"></slot>
</div>
</slot>
</div>
<div class="course-viewer-content-bd">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'Continaer',
props: { title: String }
}
</script>
<template>
<div class="editor">
<textarea name="editor" :id="textareaElementId" :disabled="disabled"></textarea>
</div>
</template>
<script>
import { uniqueId } from 'lodash'
export default {
name: 'VEditor',
props: {
value: { type: String },
disabled: { type: Boolean, default: false }
},
data() {
return {
textareaElementId: uniqueId('editor_'),
ckEditor: null
}
},
watch: {
value(val) {
if (this.ckEditor && this.ckEditor.getData() !== val) {
this.ckEditor.setData(val)
}
},
disabled(val) {
if (this.ckEditor && this.ckEditor.instanceReady) {
this.ckEditor.setReadOnly(val)
}
}
},
methods: {
createEditor() {
const config = {
height: 400,
uiColor: '#eeeeee',
filebrowserImageUploadUrl: '/api/ck/form/ckeditor-upload',
fileTools_requestHeaders: { tenant: 'sofia' },
// resize_enabled: typeof this.props.resizable === 'boolean' ? this.props.resizable : true,
toolbar: [
// { name: 'document', items: ['Source', '-', 'Save', 'NewPage', 'Preview'] },
{ name: 'styles', items: ['Styles', 'Format', 'Font', 'FontSize'] },
{ name: 'colors', items: ['TextColor', 'BGColor'] },
{ name: 'tools', items: ['Maximize', 'ShowBlocks'] },
// { name: 'clipboard', items: ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo'] },
{ name: 'editing', items: ['Find', 'Replace'] },
// { name: 'forms', items: ['Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', 'Button', 'ImageButton', 'HiddenField'] },
'/',
{
name: 'basicstyles',
items: ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat']
},
{
name: 'paragraph',
items: [
'NumberedList',
'BulletedList',
'-',
'Outdent',
'Indent',
'-',
'Blockquote',
'CreateDiv',
'-',
'JustifyLeft',
'JustifyCenter',
'JustifyRight',
'JustifyBlock',
'-',
'BidiLtr',
'BidiRtl'
]
},
{ name: 'links', items: ['Link', 'Unlink', 'Anchor'] },
{ name: 'insert', items: ['Image', 'Table', 'HorizontalRule'] }
]
}
// if (this.disabled !== null) {
// console.log(this.disabled)
// config.readOnly = this.disabled
// }
const editor = (this.ckEditor = CKEDITOR.replace(this.textareaElementId, config))
editor.on('instanceReady', () => {
const data = this.value
editor.fire('lockSnapshot')
editor.setData(data, {
callback: () => {
this.bindEvent()
const newData = editor.getData()
// Locking the snapshot prevents the 'change' event.
// Trigger it manually to update the bound data.
if (data !== newData) {
this.$once('input', () => {
this.$emit('ready', editor)
})
this.$emit('input', newData)
} else {
this.$emit('ready', editor)
}
editor.fire('unlockSnapshot')
}
})
editor.setReadOnly(this.disabled)
})
},
bindEvent() {
const editor = this.ckEditor
editor.on('change', evt => {
const data = editor.getData()
if (this.value !== data) {
this.$emit('input', data, evt, editor)
}
})
editor.on('focus', evt => {
this.$emit('focus', evt, editor)
})
editor.on('blur', evt => {
this.$emit('blur', evt, editor)
})
}
},
mounted() {
this.createEditor()
},
beforeDestroy() {
this.ckEditor && this.ckEditor.destroy()
this.ckEditor = null
}
}
</script>
<style lang="scss" scoped>
* {
margin: 0;
padding: 0;
}
</style>
<template>
<div class="upload">
<el-upload action :disabled="disabled" :show-file-list="false" :http-request="httpRequest">
<slot></slot>
<el-button type="text" icon="el-icon-upload">点击上传</el-button>
<template v-slot:tip>
<div class="el-upload__tips">
<slot name="tip"></slot>
</div>
</template>
</el-upload>
<div class="file-list" v-if="fileList.length">
<div class="file-list-item" v-for="(item, index) in fileList" :key="index">
<a :href="item.url" :download="item.name" target="_blank">
<i class="el-icon-document"></i>
{{ item.name }}
</a>
<div>
<a href="javascript:;" @click="handleRemove(index)" style="margin-right: 10px" v-if="!disabled">
<el-tooltip effect="dark" content="删除">
<i class="el-icon-delete"></i>
</el-tooltip>
</a>
<a :href="item.url" :download="item.name" target="_blank">
<el-tooltip effect="dark" content="下载">
<i class="el-icon-download"></i>
</el-tooltip>
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import * as api from '../../api'
export default {
name: 'VUpload',
props: {
value: { type: [String, Array] },
disabled: { type: Boolean, default: false }
},
data() {
return {
fileList: []
}
},
watch: {
value: {
immediate: true,
handler(value) {
if (!value) {
return
}
let fileList = []
if (Array.isArray(value)) {
fileList = value.map(item => {
return { name: item.name || item, url: item.url || item }
})
} else {
fileList.push({ name: '附件下载', url: value })
}
this.fileList = fileList
}
}
},
methods: {
httpRequest(xhr) {
api
.uploadFile({ file: xhr.file })
.then(response => {
if (response.success) {
if (Array.isArray(this.value)) {
this.fileList.push({ name: xhr.file.name, url: response.url })
this.$emit('input', this.fileList)
} else {
this.fileList = [response.url]
this.$emit('input', response.url)
}
}
})
.catch(error => {
this.$message.error(error.message)
})
},
handleRemove(index) {
this.fileList.splice(index, 1)
this.$emit('input', Array.isArray(this.value) ? this.fileList : '')
}
}
}
</script>
<style lang="scss" scoped>
.file-list-item {
display: flex;
margin-bottom: 10px;
padding: 0 10px;
justify-content: space-between;
line-height: 30px;
background-color: #fff;
border-radius: 4px;
a {
text-decoration: none;
color: #333;
&:hover {
color: #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 ChapterExam from './work/chapterExam.vue' // 章节考试
import ChapterRead from './read/chapterRead.vue' // 章节资料
import ChapterLive from './live/chapterLive.vue' // 章节直播
import CourseWork from './work/courseWork.vue' // 课程大作业
import CourseRead from './read/courseRead.vue' // 课程资料
import CourseExam from './work/courseExam.vue' // 课程考试
export default {
name: 'ViewerLayout',
components: {
ChapterPlayer,
ChapterWork,
ChapterRead,
ChapterExam,
ChapterLive,
CourseWork,
CourseRead,
CourseExam
},
props: {
chapter: {
type: Object,
default() {
return {}
}
}
},
computed: {
currentCompoent() {
const componentNames = {
2: 'ChapterPlayer', // 视频
3: 'ChapterWork', // 作业
4: 'ChapterRead', // 资料
5: 'ChapterLive', // CC直播
8: 'ChapterLive', // CC直播
9: 'ChapterExam', // 考试
99: 'CourseWork', // 课程大作业
100: 'CourseRead', // 课程资料
101: 'CourseExam' // 课程考试
}
return this.chapter ? componentNames[this.chapter.type] || '' : ''
},
pid() {
return this.$route.params.id
}
}
}
</script>
<template>
<div style="width: 100%; height: 100%">
<div class="course-viewer-content" v-if="isLiveEnd && !hasRecord">
<div class="empty">直播已结束</div>
</div>
<iframe
:src="iframeUrl"
frameborder="0"
width="100%"
height="100%"
allow="autoplay;geolocation;microphone;camera;midi;encrypted-media;"
v-else
></iframe>
</div>
</template>
<script>
// 章节视频
export default {
name: 'ChapterLive',
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
user() {
return window.G.UserInfo ? window.G.UserInfo.student_info : {}
},
nickName() {
return this.user.personal_name || '匿名'
},
live() {
const live = this.chapter.live || {}
live.live_status = parseInt(live.live_status)
return live
},
// 是否直播结束
isLiveEnd() {
return [3, 5].includes(this.live.live_status)
},
// 是否有回放
hasRecord() {
// enable_record 0:不启用回放 1:开启回放
return this.live.enable_record === 1 && this.live.record_url
},
iframeUrl() {
if (this.live.type === 5) {
return this.ccUrl
}
if (this.live.type === 8) {
return this.zoomUrl
}
},
// cc直播
ccUrl() {
const live = this.live
if (this.isLiveEnd && this.hasRecord) {
// 查看回放
return live.record_url.replace(/^http:|^https:/, '')
} else {
// 直播
live.user_name = live.user_name || this.nickName
return `https://view.csslcloud.net/api/view/index?roomid=${live.room_id}&userid=${live.account_id}&autoLogin=true&viewername=${live.user_name}&viewertoken=${live.play_pass}`
}
},
// zoom直播
zoomUrl() {
return this.live.record_url || this.live.join_url
}
}
}
</script>
<style scoped>
.empty {
padding: 100px;
font-size: 30px;
text-align: center;
}
</style>
<template>
<div class="player" v-if="chatperResources">
<div class="player-main">
<div class="player-column" v-show="videoVisible">
<!-- 视频 -->
<video-player
:isSkip="isSkip"
:skipTime="skipTime"
:video="chatperResources.video"
@timeupdate="onTimeupdate"
@ready="onReady"
ref="videoPlayer"
></video-player>
</div>
<div class="player-column" v-if="pptVisible">
<!-- ppt -->
<ppt-player
:index="pptIndex"
:ppts="chatperResources.ppts"
@close="onPPTClose"
@fullscreen="onPPTFullscreen"
@videoSyncTime="onVideoSyncTime"
></ppt-player>
</div>
</div>
<div class="player-footer">
<em class="player-button player-button-download" v-if="chapter.pdf">
<a :href="chapter.pdf" download target="_blank">下载PPT</a>
</em>
<em :class="pptClass" @click="togglePPTVisible" v-if="chatperResources.ppts.length">同步显示PPT</em>
<em :class="skipClass" @click="toggleSkip">始终跳过片头</em>
</div>
</div>
</template>
<script>
import Cookies from 'js-cookie'
import { throttle } from 'lodash'
// api
import * as api from '../../api'
// components
import videoPlayer from './videoPlayer.vue'
import pptPlayer from './pptPlayer.vue'
export default {
name: 'ChapterPlayer',
components: { videoPlayer, pptPlayer },
props: {
// 当前章节
chapter: { type: Object },
// 是否是PPT播放跳转
isSeek: { type: Boolean, default: false },
// PPT当前选中的索引
pptIndex: { type: Number, default: 0 }
},
data() {
// 是否跳过片头
const isSkip = window.localStorage.getItem('isSkip') === 'true'
return {
videoVisible: true,
pptVisible: false,
isSkip,
skipTime: 6,
chatperResources: null,
throttled: null,
throttleWait: 5, // 秒
progress: {
cpt: 0, // 当前播放时间
mpt: 0, // 当前播放最大时间
progress: 0, // 进度
pt: 0 // 累计观看时间
},
player: null,
watchedTime: 0,
watchedTimePoint: [] // 视频观看的时间点
}
},
watch: {
pptIndex(index) {
this.isSeek && this.updateVideoCurrentTime(index)
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 视频资源ID
resourceId() {
return this.chapter.resource_id
},
/**
* 视频提供者
* @return 1是CC加密; 2是非加密; 3是阿里云
*/
videoProvider() {
const video = this.chapter.video || {}
return video.video_provider || 3
},
pptClass() {
return {
'player-button': true,
'player-button-ppt': !this.pptVisible,
'player-button-ppt__active': this.pptVisible
}
},
skipClass() {
return {
'player-button': true,
'player-button-skip': !this.isSkip,
'player-button-skip__active': this.isSkip
}
}
},
methods: {
// 同步显示PPT
togglePPTVisible() {
this.videoVisible = true
this.pptVisible = !this.pptVisible
},
// 始终跳过片头
toggleSkip() {
this.isSkip = !this.isSkip
window.localStorage.setItem('isSkip', this.isSkip)
},
// 关闭PPT
onPPTClose() {
this.pptVisible = false
this.videoVisible = true
},
// PPT全屏
onPPTFullscreen(value) {
this.videoVisible = !value
},
// 设置视频时间为当前PPT时间
onVideoSyncTime(time) {
this.player.seek(time)
},
// 播放器ready
onReady(player) {
this.player = player
// 跳转播放进度
if (this.progress.cpt) {
this.player.seek(this.progress.cpt)
}
},
// 当前播放时间更新
onTimeupdate(time) {
time = Math.floor(time)
const ppts = this.chatperResources.ppts || []
let index = this.chatperResources.ppts.findIndex(item => item.ppt_point > time)
index = index !== -1 ? index - 1 : ppts.length - 1
this.$emit('change-ppt', index)
const durations = this.player.getDuration()
// 更新当前播放时间
this.progress.cpt = time
// 观看的最大点
this.progress.mpt = Math.max(time, this.progress.mpt)
const hasTimePoint = this.watchedTimePoint.includes(this.progress.cpt)
if (!hasTimePoint) {
this.watchedTimePoint.push(this.progress.cpt)
}
// 更新视频观看总时长
this.updateWatchTime(time)
// 更新视频进度,10秒更新一次
if (this.throttled) {
this.throttled(time, durations)
} else {
this.throttled = throttle(this.updateChapterVideoProgress, this.throttleWait * 1000, { leading: false })
}
},
// 更新视频当前播放时间
updateVideoCurrentTime() {
const ppt = this.chatperResources.ppts[this.pptIndex]
ppt && this.player.seek(ppt.ppt_point) // 增加2秒
},
// 获取章节视频详情
getChapterVideo() {
// 视频播放类型 1是CC加密; 2是非加密; 3是阿里云
if (this.videoProvider === 3) {
api.getChapterVideoAliyun(this.resourceId).then(response => {
this.chatperResources = response
Array.isArray(response.ppts) && this.$emit('pptupdate', response.ppts)
})
} else {
api.getChapterVideo(this.resourceId).then(response => {
let { video, audio, ppts } = response
video = video.reduce(
(result, item) => {
if (item.quality === '10') {
result.LD = item.playurl
}
if (item.quality === '20') {
result.SD = item.playurl
}
return result
},
{ LD: '', SD: '' }
)
this.chatperResources = { video, audio, ppts }
Array.isArray(ppts) && this.$emit('pptupdate', ppts)
})
}
},
// 获取章节视频进度
getChapterVideoProgress() {
api
.getChapterVideoProgress(this.sid, this.resourceId, {
device_id: Cookies.get('_idt')
})
.then(response => {
this.progress = response
// 跳转播放进度
if (this.player && response.cpt) {
this.player.seek(response.cpt)
}
})
},
// 更新章节视频进度
updateChapterVideoProgress(time, durations) {
// 登录用户信息
const user = window.G.UserInfo
const params = {
sid: user.student_info.id,
uid: user.id,
d: Cookies.get('_idt'),
i: Cookies.get('_idt'),
c: this.cid, // 课程ID
s: this.sid, // 学期ID
v: this.resourceId, // 视频资源ID
_p: this.progress.pt, // 累计时间
_m: this.progress.mpt, // 当前播放最大时间
_c: this.progress.cpt, // 当前播放位置
ps: this.watchedTimePoint.join(',') // 播放时,统计帧
}
api.updateChapterVideoProgress(params)
// 清空已经上传过的观看时间点
this.watchedTimePoint = []
},
// 更新观看总时长
updateWatchTime(time) {
if (time === this.watchedTime) {
return
}
this.watchedTime = time
// 增加跳过片头时间
if (this.isSkip && !this.progress.pt) {
this.progress.pt = this.skipTime + 20
}
// 默认增加时间
this.progress.pt = this.progress.pt || 20
this.progress.pt++
}
},
beforeMount() {
// 获取视频
this.getChapterVideo()
// 获取视频进度
this.getChapterVideoProgress()
}
}
</script>
<style lang="scss" scoped>
.player {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: #3f3f3f;
}
.player-main {
display: flex;
flex: 1;
overflow: hidden;
}
.player-column {
flex: 1;
height: 100%;
}
.player-footer {
display: flex;
align-items: center;
height: 54px;
padding: 0 20px;
font-size: 14px;
color: #a0a0a0;
a {
color: #a0a0a0;
text-decoration: none;
}
em {
margin-right: 40px;
cursor: pointer;
}
}
.player-button {
display: inline-block;
color: #a0a0a0;
padding-left: 25px;
font-size: 14px;
line-height: 18px;
margin: 0 20px;
background: url(../../assets/play-icons.png) no-repeat 0 0;
cursor: pointer;
}
.player-button-download {
background-position: 0 -240px;
}
.player-button-ppt {
background-position: 0 -240px;
}
.player-button-ppt__active {
background-position: 0 -280px;
color: #b19241;
}
.player-button-skip {
background-position: 0 -160px;
}
.player-button-skip__active {
background-position: 0 -200px;
color: #b19241;
}
</style>
<template>
<div class="ppt-player">
<template v-if="ppts.length">
<div class="ppt-player-preview">
<img :src="pptUrl" v-if="pptUrl" />
</div>
<div class="ppt-player-controls">
<div class="ppt-player-controls__page">
<template v-if="currentIndex >= 0">
<i class="el-icon-arrow-left" @click="prev"></i>
</template>
<template v-if="currentIndex + 1 < ppts.length">
<i class="el-icon-arrow-right" @click="next"></i>
</template>
</div>
<div class="ppt-player-controls__pages">
<span class="is-active">{{currentIndex + 1}}</span>
/
<span>{{ppts.length}}</span>
</div>
<div class="ppt-player-controls__tools">
<el-tooltip content="PPT同步视频播放">
<i :class="['el-icon-self-xuexiao', (isSync ? 'active' : '')]" @click="onToggleSync"></i>
</el-tooltip>
<el-tooltip content="放大PPT">
<i class="el-icon-self-quanping" @click="fullscreen"></i>
</el-tooltip>
<el-tooltip content="切换视频到当前PPT页">
<i class="el-icon-self-shipin" @click="setVideoTime"></i>
</el-tooltip>
<el-tooltip content="关闭PPT">
<i class="el-icon-self-guanbi" @click="$emit('close')"></i>
</el-tooltip>
</div>
</div>
</template>
</div>
</template>
<script>
export default {
name: 'ppt-player',
props: {
ppts: { type: Array },
index: { type: Number, default: 0 }
},
data() {
return {
currentIndex: this.index,
isSync: true,
isFullscreen: false
}
},
watch: {
index: {
handler(value) {
if (this.isSync) {
this.currentIndex = value
}
}
}
},
computed: {
pptUrl() {
return this.ppts[this.currentIndex]
? this.ppts[this.currentIndex].ppt_url
: ''
}
},
methods: {
gotoIndex(index) {
this.currentIndex = index
},
getIndex(index) {
return Math.min(this.ppts.length - 1, Math.max(0, index))
},
prev() {
this.currentIndex = this.getIndex(this.currentIndex - 1)
this.isSync = false
},
next(e) {
this.currentIndex = this.getIndex(this.currentIndex + 1)
this.isSync = false
},
onToggleSync(e) {
this.isSync = !this.isSync
},
setVideoTime(e) {
this.isSync = true
this.$emit('videoSyncTime', this.ppts[this.currentIndex].ppt_point)
},
// 全屏
fullscreen() {
this.isFullscreen = !this.isFullscreen
this.$emit('fullscreen', this.isFullscreen)
}
}
}
</script>
<style lang="scss" scoped>
.ppt-player {
position: relative;
width: 100%;
height: 100%;
background-color: #000;
}
.ppt-player-preview {
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.ppt-player-controls {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 44px;
line-height: 44px;
padding: 0 14px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
}
.ppt-player-controls__page {
width: 90px;
color: #fff;
i {
padding: 0 10px;
font-size: 18px;
cursor: pointer;
}
}
.ppt-player-controls__pages {
flex: 1;
color: #fff;
text-align: center;
}
.ppt-player-controls__pages .is-active {
color: #d29f29;
}
.ppt-player-controls__tools {
float: right;
}
.ppt-player-controls__tools i {
color: #fff;
margin: 0 10px;
cursor: pointer;
}
.ppt-player-controls__tools i.active,
.ppt-player-controls__tools i:hover {
color: #d29f29;
}
.ppt-player-controls__tools .icon-rotate {
font-size: 1.125em;
}
</style>
<template>
<div class="video-player" id="player"></div>
</template>
<script>
export default {
name: 'VideoPlayer',
props: {
isSkip: Boolean,
video: Object,
autoplay: { type: Boolean, default: false }
},
data() {
return { player: null }
},
methods: {
createPlayer() {
const _this = this
const { FD, LD, SD } = this.video
/*
"OD" : "原画"
"FD" : "流畅"
"LD" : "标清"
"SD" : "高清"
"HD" : "超清"
"2K" : "2K"
"4K" : "4K"
*/
this.player = new Aliplayer(
{
id: 'player',
source: JSON.stringify({ FD, LD, SD }),
width: '100%',
height: '100%',
autoplay: this.autoplay,
isLive: false,
controlBarVisibility: 'always',
definition: 'FD,LD,SD',
defaultDefinition: 'LD',
useHlsPluginForSafari: true
},
function(player) {
player.on('ready', function() {
// 跳过片头
_this.isSkip && player.seek(6)
_this.$emit('ready', player)
})
player.on('timeupdate', function(event) {
_this.$emit('timeupdate', player.getCurrentTime())
})
player.on('error', function(event) {
console.log(event)
})
}
)
}
},
mounted() {
this.createPlayer()
},
beforeDestroy() {
this.player && this.player.dispose()
}
}
</script>
<style lang="scss" scoped>
.video-player {
width: 100%;
height: 100%;
}
</style>
<template>
<container :title="chapter.name">
<file-list :files="files"></file-list>
</container>
</template>
<script>
// components
import Container from '../common/container.vue'
import FileList from './fileList.vue'
// 章节阅读资料
export default {
name: 'ChapterRead',
components: { Container, FileList },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
files() {
const reading = this.chapter.reading
const file = {
file_name: reading.reading_content,
file_url: reading.reading_attachment
}
return [file]
}
}
}
</script>
<template>
<container :title="chapter.name">
<file-list :files="files"></file-list>
</container>
</template>
<script>
// components
import Container from '../common/container.vue'
import FileList from './fileList.vue'
// 课程阅读资料
export default {
name: 'CourseRead',
components: { Container, FileList },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
files() {
return this.data.files || []
}
}
}
</script>
<template>
<div>
<ul class="file-list" v-if="files.length">
<li class="file-list-item" v-for="file in files" :key="file.id">
<a :href="file.file_url" target="_blank">
<i class="el-icon-document"></i>
<div class="file-list-item__inner" v-html="file.file_name"></div>
</a>
<!-- <span v-if="file.file_size">{{ file.file_size }}</span> -->
<a :href="file.file_url" :download="file.file_name" target="_blank">
<el-tooltip effect="dark" content="下载">
<i class="el-icon-download"></i>
</el-tooltip>
</a>
</li>
</ul>
<div class="empty" v-else>
<slot name="empty">暂无课程资料</slot>
</div>
</div>
</template>
<script>
export default {
name: 'FilePanel',
props: {
// 标题
title: { type: String, default: '课程资料' },
// 文件列表
files: { type: Array, default: () => [] }
}
}
</script>
<style lang="scss" scoped>
.file-list {
padding: 0;
}
.file-list-item {
display: flex;
font-size: 16px;
padding: 20px 30px;
margin-bottom: 10px;
background-color: #fff;
list-style: none;
border-radius: 32px;
justify-content: space-between;
a {
display: flex;
align-items: center;
text-decoration: none;
color: #333;
&:hover {
color: #b49441;
}
::v-deep * {
margin: 0;
padding: 0;
}
}
}
.empty {
font-size: 18px;
line-height: 80px;
background-color: #fff;
text-align: center;
border-radius: 40px;
}
.file-list-item__inner {
margin: 0 10px !important;
}
</style>
<template>
<container :title="detail.paper_title" v-loading="loading">
<template v-slot:header-aside v-if="isExamComplete">分数:{{exam.score.total}}</template>
<div class="exam">
<template v-if="isSubmited && !isExamComplete">
<div class="no-exam">试卷批改中,请耐心等待</div>
</template>
<template v-else>
<!-- 考试期间,未开始考试 -->
<div class="exam-welcome" v-if="!isStartExam">
<div v-if="detail.paper_deadline">考试截止时间:{{detail.paper_deadline}}</div>
<el-button
type="primary"
:disabled="!isExamTime"
@click="onStartExam"
>{{startExamButtonText}}</el-button>
</div>
<!-- 考试试题 -->
<div class="exam-form" v-if="isStartExam">
<el-form :disabled="isSubmited">
<template v-for="items in questions">
<exam-item
v-for="(item, index) in items"
:index="index"
:type="item.type"
:data="item"
:value="item.formModel"
:disabled="isSubmited"
:key="item.id"
></exam-item>
</template>
<div class="exam-buttons">
<el-tooltip effect="dark" content="提交之后就不能修改了哦" placement="right">
<el-button type="primary" :loading="submitLoading" @click="onSubmit">{{submitText}}</el-button>
</el-tooltip>
</div>
</el-form>
</div>
</template>
</div>
</container>
</template>
<script>
import Base64 from 'Base64'
// components
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节测试题
export default {
name: 'ChapterExam',
components: { Container, ExamItem },
props: {
// 当前选中的章节
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
detail: {},
questions: [],
messageInstance: null,
exam: {},
isStartExam: false, // 是否开始考试
autoSubmitTimer: null, // 自动提交定时器
submitLoading: false
}
},
watch: {
chapter: {
immediate: true,
handler(data) {
this.detail = data.paper
this.questions = data.paper
? this.genQuestions(data.paper.examination)
: []
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 是否是考试时间
isExamTime() {
if (!this.detail.paper_deadline) {
return true
}
// 大于开始时间,小于结束时间
const endTime = +new Date(this.exam.paper_deadline)
const currentTime = new Date().getTime()
return currentTime < endTime
},
// 考试按钮
startExamButtonText() {
return this.isExamTime ? '开始考试' : '考试结束'
},
// 考试完成
isExamComplete() {
// 考试完成,批改完成并且公布成绩
return this.exam.is_published === 1 && this.exam.type === 2
},
// 是否提交
isSubmited() {
return this.exam.type === 1 || this.exam.type === 2
},
// 提交按钮文本
submitText() {
return this.isSubmited ? '已提交' : '提交'
}
},
methods: {
// 开始考试
onStartExam() {
this.isStartExam = true
// 自动提交答题
this.autoSubmit()
},
// 组装问题数据
genQuestions(list) {
if (!list) {
return []
}
return list.map(data => {
let { radioList, checkboxList, shortAnswerList } = data
// 单选
radioList = radioList.map(item => {
const temp = {
type: 1,
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
return Object.assign({}, item, temp)
})
// 多选
checkboxList = checkboxList.map(item => {
const temp = {
type: 2,
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
return Object.assign({}, item, temp)
})
// 问答
shortAnswerList = shortAnswerList.map(item => {
const temp = {
type: 3,
formModel: {
id: item.id,
user_answer: item.user_answer
? Base64.decode(item.user_answer.replace(/ /gi, '+'))
: '',
attachments: item.attachments || []
}
}
return Object.assign({}, item, temp)
})
return [...radioList, ...checkboxList, ...shortAnswerList]
})
},
// 获取考试结果
getExamResult() {
api
.getCourseExamResult(this.sid, this.cid, this.pid, { paper_type: 0 })
.then(response => {
// 设置问题列表数据
if (response.code !== 8001) {
this.isStartExam = true
this.exam = response
this.questions = this.genQuestions(response.sheet)
// 自动提交
if (this.isStartExam && !this.isSubmited && !this.isExamComplete) {
this.autoSubmit()
}
}
})
},
// 提交校验
checkSubmit() {
for (let i = 0; i < this.questions.length; i++) {
const questions = this.questions[i]
for (let k = 0; k < questions.length; k++) {
const value = questions[k].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
return false
}
}
}
return true
},
// 提交
onSubmit() {
// 校验
if (!this.checkSubmit()) {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error('还有题目未做,不能提交')
return
}
// 提交的答案数据
const answers = this.handleSubmitData()
// 提交参数
const params = { answers: JSON.stringify(answers), type: 1 }
// 请求接口
this.submitLoading = true
this.handleSubmitRequest(params)
},
// 自动提交
autoSubmit() {
// 10秒提交一次
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.autoSubmitTimer = setInterval(() => {
// 提交的答案数据
const answers = this.handleSubmitData()
const params = { answers: JSON.stringify(answers), type: 0 }
// 请求接口
this.handleSubmitRequest(params)
}, 3000)
},
// 处理请求接口答案数据
handleSubmitData() {
return this.questions.map(questions => {
return questions.reduce(
(result, item) => {
// 单选题
if (item.type === 1) {
result.radioList.push(item.formModel)
}
// 多选题
if (item.type === 2) {
result.checkboxList.push(item.formModel)
}
// 简答题
if (item.type === 3) {
const formModel = Object.assign({}, item.formModel, {
user_answer: Base64.encode(item.formModel.user_answer)
})
result.shortAnswerList.push(formModel)
}
return result
},
{ radioList: [], checkboxList: [], shortAnswerList: [] }
)
})
},
// 请求提交接口
handleSubmitRequest(params) {
params.paper_type = 0
api
.submitCourseExam(this.sid, this.cid, this.pid, params)
.then(response => {
if (params.type === 0) {
console.log('暂存成功')
return
}
if (response.code === 200) {
this.$message.success('考试答卷提交成功')
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.getExamResult()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
}
},
beforeMount() {
// 获取考试结果
this.getExamResult()
},
destroyed() {
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
width: 240px;
margin: 40px auto;
}
}
.no-exam {
padding: 100px;
font-size: 30px;
text-align: center;
}
.exam-welcome {
padding: 40px;
line-height: 30px;
text-align: center;
::v-deep .el-button {
margin-top: 30px;
}
}
</style>
<template>
<container :title="chapter.name" v-loading="loading">
<template v-slot:header-aside v-if="isSubmited">正确率:{{ detail.score }}%</template>
<div class="exam">
<div class="exam-form">
<el-form :disabled="isSubmited">
<exam-item
v-for="(item, index) in questions"
:index="index"
:type="item.question_type"
:data="item"
:value="item.formModel"
:disabled="isSubmited"
:key="item.id"
></exam-item>
<div class="exam-buttons">
<el-tooltip effect="dark" content="提交之后就不能修改了哦" placement="right">
<el-button type="primary" :loading="submitLoading" @click="onSubmit">{{ submitText }}</el-button>
</el-tooltip>
</div>
</el-form>
</div>
</div>
</container>
</template>
<script>
// libs
import { shuffle } from 'lodash'
// components
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节测试题
export default {
name: 'ChapterTest',
components: { Container, ExamItem },
props: {
// 当前选中的章节
chapter: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
detail: null,
questions: [], // 问题列表
startTime: new Date().getTime(), // 进入时间
messageInstance: null,
submitLoading: false
}
},
watch: {
chapter: {
immediate: true,
handler(data) {
this.questions = data.homework ? this.genQuenstions(data.homework.questions) : []
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 资源ID
resourceId() {
return this.chapter.resource_id
},
// 打乱顺序的问题列表
unorderedQuestions() {
const ids = this.questions.map(item => item.id)
const sortIds = shuffle(ids)
return sortIds.map(id => this.questions.find(item => item.id === id))
},
// 是否提交
isSubmited() {
return this.detail ? !!this.detail.work_contents : false
},
// 提交按钮文本
submitText() {
return this.isSubmited ? '已提交' : '提交'
}
},
methods: {
// 获取测试答题详情
getDetail() {
this.loading = true
api
.getChapterHomework(this.sid, this.cid, this.resourceId)
.then(response => {
this.detail = Array.isArray(response) ? null : response
if (this.detail) {
const parseAnswers = JSON.parse(this.detail.work_contents)
// 设置答案
this.questions = this.questions.map(item => {
const found = parseAnswers.find(answer => answer.question_id === item.id)
if (found) {
const selectedIds = found.options.reduce((result, item) => {
item.selected && result.push(item.id)
return result
}, [])
item.user_answer = item.question_type === 2 ? selectedIds : selectedIds[0]
}
return item
})
this.questions = this.genQuenstions(this.questions)
}
})
.finally(() => {
this.loading = false
})
},
// 组装问题数据
genQuenstions(list) {
if (!list) {
return []
}
return list.map(item => {
let temp = null
if (item.question_type === 1) {
// 单选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
} else if (item.question_type === 2) {
// 多选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
} else if (item.question_type === 3) {
// 简答
temp = {
formModel: {
id: item.id,
user_answer: item.user_answer ? Base64.decode(item.user_answer) : '',
attachments: item.attachments || ''
}
}
}
return Object.assign(
{},
item,
{
content: item.question_content,
options: item.question_options ? JSON.parse(item.question_options) : []
},
temp
)
})
},
// 提交校验
checkSubmit() {
const quenstions = this.questions
for (let i = 0; i < quenstions.length; i++) {
const value = quenstions[i].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
return false
}
}
return true
},
// 提交
onSubmit() {
// 校验
if (!this.checkSubmit()) {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error('还有题目未做,不能提交')
return
}
// 计算答题时间
const duration = Math.floor((new Date().getTime() - this.startTime) / 1000)
// 答案数据
const data = this.handleSubmitData()
// 计算分数
const score = data.reduce((result, item) => {
item.is_correct && result++
return result
}, 0)
const total = this.questions.length
const params = {
semester_id: this.sid,
course_id: this.cid,
chapter_id: this.pid,
work_id: this.resourceId,
work_contents: JSON.stringify(data),
duration,
score: ((score / total) * 100).toFixed(1)
}
// 请求接口
this.handleSubmitRequest(params)
},
// 提交的答案数据
handleSubmitData() {
const result = this.questions.map(item => {
// 设置提交选中状态
let isCorrect = true
const options = item.options.map(option => {
// 选择的项
const answers = item.formModel.user_answer
// 是否选中该项
const selected = Array.isArray(answers) ? answers.includes(option.id) : option.id === answers
// 是否选择正确
if (option.checked !== selected && isCorrect) {
isCorrect = false
}
return {
id: option.id,
checked: option.checked,
option: option.option,
selected
}
})
return {
question_id: item.id,
is_correct: isCorrect ? 1 : 0,
options
}
})
return result
},
// 请求提交接口
handleSubmitRequest(params) {
this.submitLoading = true
api
.sbumitChapterHomework(params)
.then(response => {
if (response.status) {
this.getDetail()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
}
},
beforeMount() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
width: 240px;
margin: 40px auto;
}
}
</style>
<template>
<container :title="chapter.name" v-loading="loading">
<div class="exam-form">
<el-form :disabled="disabled || !isWorkTime">
<exam-item
v-for="(item, index) in questions"
:index="index"
:type="item.question_type"
:data="item"
:value="item.formModel"
:disabled="disabled || !isWorkTime"
:key="item.id"
></exam-item>
</el-form>
</div>
<p style="color:red;" v-if="deadline">请于截止日期 {{ deadline }} 前提交</p>
<!-- 驳回状态 -->
<template v-if="detail && detail.status === 1">
<div class="work-bottom">
<div class="info">
<div class="paper-check">
<h4>作业被驳回,点击“重新编辑”按钮重新编辑内容再次提交</h4>
<div class="paper-check-item">
<b>驳回时间:</b>
{{ detail.checker_time }}
</div>
<div class="paper-check-item">
<b>驳回说明:</b>
<div class="edit_html" v-html="detail.check_comments"></div>
</div>
</div>
</div>
</div>
<div class="buttons">
<el-button type="primary" @click="onReEdit" :disabled="!isWorkTime">重新编辑</el-button>
</div>
</template>
<!-- 正常状态 -->
<template v-else>
<div class="work-bottom" v-if="detail">
<div class="info">
<template v-if="isRevised">
<div class="paper-check">
<p>批改时间:{{ detail.checker_time }}</p>
<div class="paper-check-item">
<b>评分:</b>
{{ detail.score }}
</div>
<div class="paper-check-item">
<b>评语:</b>
<div class="edit_html" v-html="detail.check_comments"></div>
</div>
</div>
</template>
<template v-else-if="detail.created_time">
<p class="help">已于 {{ detail.created_time }} 提交,等待老师批改中。</p>
<template
v-if="
detail.updated_time &&
detail.updated_time !== detail.created_time
"
>
<p class="help">最近提交时间: {{ detail.updated_time }}</p>
</template>
</template>
</div>
</div>
<div class="buttons">
<el-tooltip content="在获老师批改之前,可以多次提交,将以最后一次提交为准" placement="right">
<el-button
type="primary"
:disabled="disabled || !isWorkTime"
:loading="submitLoading"
@click="onSubmit"
>{{ submitText }}</el-button>
</el-tooltip>
</div>
</template>
</container>
</template>
<script>
import Base64 from 'Base64'
// componets
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节作业
export default {
name: 'ChapterWork',
components: { Container, ExamItem },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
detail: null,
questions: [], // 问题列表
startTime: new Date().getTime(), // 进入时间
messageInstance: null,
deadline: '', // 截止时间
disabled: false,
submitLoading: false
}
},
watch: {
chapter: {
immediate: true,
handler(data) {
this.questions = data.homework
? this.genQuenstions(data.homework.questions)
: []
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 资源ID
resourceId() {
return this.chapter.resource_id
},
// 是否批改
isRevised() {
return this.detail ? this.detail.status === 0 : false
},
// 提交按钮文本
submitText() {
return this.isRevised ? '已批改' : '提交'
},
// 是否是提交作业时间
isWorkTime() {
if (!this.deadline) {
return true
}
// 大于开始时间,小于结束时间
const endTime = +new Date(this.deadline)
const currentTime = new Date().getTime()
return currentTime < endTime
}
},
methods: {
// 获取作业截止时间
getDeadline() {
api
.getChapterHomeworkDeadline(this.sid, this.cid, this.pid)
.then(response => {
this.deadline = response.dead_line
})
},
// 获取详情
getDetail() {
this.loading = true
api
.getChapterHomework(this.sid, this.cid, this.resourceId)
.then(response => {
this.detail = Array.isArray(response) ? null : response
if (this.detail) {
// -1未处理 0已处理 1驳回
this.disabled = [0, 1].includes(this.detail.status)
const parseAnswers = JSON.parse(this.detail.work_contents)
// 设置答案
this.questions = this.questions.map(item => {
const found = parseAnswers.find(
answer => answer.question_id === item.id
)
if (found) {
item.user_answer = found.descreption
item.attachments = found.file_url
}
return item
})
this.questions = this.genQuenstions(this.questions)
}
})
.finally(() => {
this.loading = false
})
},
// 组装问题数据
genQuenstions(list) {
if (!list) {
return []
}
return list.map(item => {
let temp = null
if (item.question_type === 1) {
// 单选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
} else if (item.question_type === 2) {
// 多选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
} else if (item.question_type === 3) {
// 简答
temp = {
formModel: {
id: item.id,
user_answer: item.user_answer
? Base64.decode(item.user_answer)
: '',
attachments: item.attachments || ''
}
}
}
return Object.assign(
{},
item,
{
content: item.question_content,
options: item.question_options
? JSON.parse(item.question_options)
: []
},
temp
)
})
},
// 提交校验
checkSubmit() {
const quenstions = this.questions
for (let i = 0; i < quenstions.length; i++) {
const value = quenstions[i].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
return false
}
}
return true
},
// 提交
onSubmit() {
// 校验
if (!this.checkSubmit()) {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error(
'答题内容不能为空,请检查并输入内容'
)
return
}
// 计算答题时间
const duration = Math.floor(
(new Date().getTime() - this.startTime) / 1000
)
// 提交的答案数据
const answers = this.questions.map(item => {
return {
question_id: item.id,
descreption:
item.question_type === 3
? Base64.encode(item.formModel.user_answer)
: item.formModel.user_answer,
file_url: item.formModel.attachments,
is_encoded: 1
}
})
// 提交参数
const params = {
semester_id: this.sid,
course_id: this.cid,
chapter_id: this.pid,
work_id: this.resourceId,
work_contents: JSON.stringify(answers),
duration
}
// 请求接口
this.handleSubmitRequest(params)
},
// 请求提交接口
handleSubmitRequest(params) {
this.submitLoading = true
api
.sbumitChapterHomework(params)
.then(response => {
if (response.status) {
this.$message.success('提交成功,等待批改')
this.getDetail()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
},
// 重新编辑
onReEdit() {
this.disabled = false
this.detail.status = -1
}
},
beforeMount() {
this.getDetail()
this.getDeadline()
}
}
</script>
<style lang="scss" scoped>
.work-bottom {
margin-top: 20px;
.info {
color: #999;
line-height: 28px;
}
}
.buttons {
padding: 20px 0;
::v-deep .el-button {
width: 120px;
}
}
.paper-check {
padding: 10px;
color: #000;
border: 1px solid #dedede;
h4 {
margin: 0 0 10px;
}
}
.paper-check-item {
display: flex;
b {
white-space: nowrap;
}
}
</style>
<template>
<container :title="chapter.name" v-loading="loading">
<el-steps direction="vertical" v-if="data.curriculum">
<el-step title="阅读大作业要求" status="process">
<template v-slot:description>
<div v-html="data.curriculum.curriculum_essay"></div>
<p>截止日期:{{data.essay_date}}</p>
</template>
</el-step>
<el-step title="填写作业主题、正文,上传附件(点击“提交”保存)" status="process">
<template v-slot:description>
<el-form
:model="ruleForm"
:rules="rules"
:hide-required-asterisk="true"
:disabled="isRevised"
label-position="top"
ref="ruleForm"
>
<el-form-item label="主题" prop="essay_name">
<el-input v-model="ruleForm.essay_name" placeholder="主题" maxlength="50"></el-input>
</el-form-item>
<el-form-item label="正文" prop="essay_description">
<!-- 编辑器 -->
<v-editor :disabled="isRevised" v-model="ruleForm.essay_description"></v-editor>
</el-form-item>
<el-form-item prop="url">
<!-- 文件上传 -->
<v-upload v-model="ruleForm.url">
请上传对应的文件附件:
<!-- <template v-slot:tip>只支持docx格式的文件,文件小于10M</template> -->
</v-upload>
</el-form-item>
</el-form>
</template>
</el-step>
<el-step title="截止日期前提交" status="process">
<template v-slot:description>
<div class="work-bottom" v-if="detail">
<div class="info">
<template v-if="isRevised">
<div class="paper-check">
<p>批改时间:{{detail.check_date}}</p>
<div class="paper-check-item">
<b>评分:</b>
{{detail.score}}
</div>
<div class="paper-check-item">
<b>评语:</b>
<div class="edit_html" v-html="detail.check_comments"></div>
</div>
</div>
</template>
<template v-else-if="detail.created_time">
<p class="help">已于 {{detail.created_time}} 提交,等待老师批改中。</p>
<template v-if="detail.updated_time && detail.updated_time !== detail.created_time">
<p class="help">最近提交时间: {{detail.updated_time}}</p>
</template>
</template>
</div>
</div>
<div class="buttons">
<el-tooltip content="在获老师批改之前,可以多次提交,将以最后一次提交为准" placement="right">
<el-button
type="primary"
:disabled="isRevised"
:loading="submitLoading"
@click="onSubmit"
>{{submitText}}</el-button>
</el-tooltip>
</div>
</template>
</el-step>
</el-steps>
</container>
</template>
<script>
// componets
import Container from '../common/container.vue'
import VEditor from '../common/editor.vue'
import VUpload from '../common/upload.vue'
// api
import * as api from '../../api'
// 课程大作业
export default {
name: 'CourseWork',
components: { Container, VEditor, VUpload },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
ruleForm: {
essay_name: '',
essay_description: '',
url: ''
},
rules: {
essay_name: [
{ required: true, message: '请输入主题', trigger: 'blur' }
],
essay_description: [
{ required: true, message: '请输入正文', trigger: 'change' }
],
url: [{ required: true, message: '请上传附件', trigger: 'change' }]
},
detail: null,
loading: false,
messageInstance: null,
submitLoading: false
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 是否批改
isRevised() {
return this.detail ? !!this.detail.check_date : false
},
// 提交按钮文本
submitText() {
return this.isRevised ? '已批改' : '提交'
}
},
methods: {
// 获取大作业详情
getDetail() {
this.loading = true
api
.getCourseWork(this.sid, this.cid)
.then(response => {
this.detail = Array.isArray(response) ? null : response
if (this.detail) {
this.ruleForm.essay_name = this.detail.essay_name
this.ruleForm.essay_description = this.detail.essay_description
this.ruleForm.url = this.detail.file_url
}
})
.finally(() => {
this.loading = false
})
},
// 提交
onSubmit() {
this.messageInstance && this.messageInstance.close()
if (!this.ruleForm.essay_name) {
this.messageInstance = this.$message.error('请输入主题')
return
}
if (!this.ruleForm.essay_description) {
this.messageInstance = this.$message.error('请输入正文')
return
}
if (!this.ruleForm.url) {
this.messageInstance = this.$message.error('请上传附件')
return
}
const params = Object.assign(this.ruleForm, {
semester_id: this.sid,
course_id: this.cid
})
this.handleSubmitRequest(params)
},
// 请求提交接口
handleSubmitRequest(params) {
this.submitLoading = true
api
.updateCourseWork(this.sid, this.cid, params)
.then(response => {
if (response.status) {
this.$message.success('提交成功,等待批改')
this.getDetail()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
}
},
beforeMount() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
p {
margin: 0;
}
::v-deep .el-step__title {
border-bottom: 1px dashed #cecece;
}
::v-deep .el-step__description {
padding: 20px 0 30px;
font-size: 14px;
}
::v-deep .el-form-item__label {
font-weight: bold;
line-height: 24px;
padding: 0 0 5px;
}
.work-bottom {
.info {
color: #999;
line-height: 28px;
}
}
.buttons {
padding: 20px 0;
::v-deep .el-button {
width: 120px;
}
}
.paper-check {
padding: 10px;
color: #000;
border: 1px solid #dedede;
}
.paper-check-item {
display: flex;
b {
white-space: nowrap;
}
}
</style>
<template>
<div class="q-item">
<div class="q-item-hd">
<div class="q-item-num">{{ index + 1 }}.</div>
<div class="q-item-title" v-html="data.content"></div>
<div class="q-item-aside">
<template v-if="typeText">({{ typeText }})</template>
<template v-if="data.hasOwnProperty('score')">({{ data.score }}分)</template>
</div>
</div>
<div class="q-item-bd">
<!-- 单选 -->
<el-radio-group v-model="currentValue.user_answer" v-if="type === 1">
<div class="q-option-item" v-for="item in currentOptions" :key="item.id">
<el-radio :class="genClass(item)" :label="item.id">
<div class="q-option-item__answer" v-html="item.abc_option"></div>
</el-radio>
</div>
</el-radio-group>
<!-- 多选 -->
<el-checkbox-group v-model="currentValue.user_answer" v-if="type === 2">
<div class="q-option-item" v-for="item in currentOptions" :key="item.id">
<el-checkbox :class="genClass(item)" :label="item.id">
<div class="q-option-item__answer" v-html="item.abc_option"></div>
</el-checkbox>
</div>
</el-checkbox-group>
<!-- 简答题 -->
<template v-if="type === 3">
<v-editor v-model="currentValue.user_answer" :disabled="disabled"></v-editor>
<v-upload :disabled="disabled" v-model="currentValue.attachments">请上传对应的文件附件:</v-upload>
</template>
</div>
<div class="q-item-ft" v-if="disabled && showResult">
<template v-if="type === 3">
<p v-if="data.check_comment">
<span>评语:</span>
<span>{{ data.check_comment }}</span>
</p>
</template>
<template v-else>
<div class="result">
<p>
<span>学生答案:</span>
<span :class="isCorrect ? 'is-success' : 'is-error'">{{ submitAnswerText }}</span>
</p>
<p>
<span>正确答案:</span>
<span>{{ correctAnswerText }}</span>
</p>
</div>
</template>
<p v-if="data.hasOwnProperty('get_score')">
<span>评分:</span>
<span>{{ data.get_score }}分</span>
</p>
<div class="analyze" v-if="data.analysis">
<span>解析:</span>
<div class="analyze-main">
<span style="color: blue; cursor: pointer" @click="showAnalyze = !showAnalyze">查看解析</span>
<div v-html="data.analysis" v-if="data.analysis" v-show="showAnalyze" class="analyze-content"></div>
</div>
</div>
</div>
</div>
</template>
<script>
// components
import VEditor from '../common/editor.vue'
import VUpload from '../common/upload.vue'
export default {
name: 'ExamItem',
components: { VEditor, VUpload },
props: {
// 索引
index: { type: Number },
// 问题类型
type: { type: Number },
// 单条数据
data: {
type: Object,
default() {
return {}
}
},
// 提交的答案
value: {
type: Object,
default() {
return {}
}
},
// 是否禁用,提交过的是禁用状态
disabled: { type: Boolean, default: false },
showResult: { type: Boolean, default: true }
},
data() {
return {
currentValue: {},
showAnalyze: false
}
},
watch: {
value: {
immediate: true,
handler(value) {
this.currentValue = value
}
}
},
computed: {
// 26个英文字母
A_Z() {
const result = []
for (let i = 0; i < 26; i++) {
result.push(String.fromCharCode(65 + i))
}
return result
},
// 选项类型
typeText() {
const map = { 1: '单选题', 2: '多选题' }
return map[this.type]
},
// 处理后的options数据
currentOptions() {
if (!this.data.options) {
return []
}
return this.data.options.map((item, index) => {
// 英文字母 + 名称
item.abc = this.A_Z[index]
item.abc_option = `${this.A_Z[index]}. ${item.option}`
// 提交时的选中状态
const value = this.value.user_answer || ''
item.selected = Array.isArray(value) ? value.includes(item.id) : value === item.id
// 处理正确的选中状态
const hasChecked = Object.prototype.hasOwnProperty.call(item, 'checked')
const rightAnswer = this.data.right_answer || ''
if (!hasChecked && rightAnswer) {
item.checked = Array.isArray(rightAnswer) ? rightAnswer.includes(item.id) : rightAnswer === item.id
}
return item
})
},
// 正确答案显示的英文字母
correctAnswerText() {
const result = this.currentOptions.reduce((result, item) => {
item.checked && result.push(item.abc)
return result
}, [])
return result.join('、')
},
// 提交答案显示的英文字母
submitAnswerText() {
const result = this.currentOptions.reduce((result, item) => {
item.selected && result.push(item.abc)
return result
}, [])
return result.join('、')
},
// 是否回答正确
isCorrect() {
const options = this.currentOptions
for (let i = 0; i < options.length; i++) {
if (options[i].checked !== !!options[i].selected) {
return false
}
}
return true
}
},
methods: {
// 生成class
genClass(item) {
if (!this.disabled || !this.showResult) {
return null
}
return {
'is-error': !this.isCorrect && item.selected,
'is-success': this.isCorrect && item.selected
}
}
}
}
</script>
<style lang="scss" scoped>
.q-item {
font-size: 16px;
padding: 10px 0;
border-bottom: 1px solid #c9c9c97a;
.upload {
font-size: 14px;
}
}
.q-item-hd {
display: flex;
padding: 10px 0 20px;
::v-deep p {
margin: 0;
padding: 0;
}
::v-deep ul {
margin: 0;
padding: 0;
list-style: none;
}
}
.q-item-num {
width: 20px;
text-align: center;
}
.q-item-title {
flex: 1;
::v-deep img {
max-width: 100%;
}
}
.q-item-aside {
padding-left: 20px;
// align-self: flex-end;
}
.q-option-item {
padding-left: 20px;
margin-bottom: 14px;
}
.q-option-item__answer {
display: inline;
::v-deep * {
display: inline;
}
}
.is-success {
color: #090;
}
.is-error {
color: #d80000;
}
::v-deep .el-radio {
&.is-disabled .el-radio__label {
color: #3c3c3c;
}
&.is-error .el-radio__label {
color: #d80000;
}
&.is-success .el-radio__label {
color: #090;
}
}
::v-deep .el-checkbox {
&.is-disabled .el-checkbox__label {
color: #3c3c3c;
}
&.is-error .el-checkbox__label {
color: #d80000;
}
&.is-success .el-checkbox__label {
color: #090;
}
}
.q-item-ft {
padding: 10px 0;
p {
font-size: 14px;
margin: 0 0 10px 0;
}
.result {
display: flex;
justify-content: flex-end;
p {
padding-left: 20px;
}
}
.analyze {
display: flex;
font-size: 14px;
}
.analyze-main {
flex: 1;
overflow: hidden;
}
.analyze-content {
margin-top: 10px;
background-color: #c9c9c97a;
border: 1px solid #c9c9c97a;
padding: 10px;
::v-deep * {
margin: 0;
padding: 0;
max-width: 100%;
}
}
}
</style>
<template>
<component :is="currentCompoent" :chapter="chapter" :data="data" v-bind="$attrs" v-on="$listeners" v-if="chapter" />
</template>
<script>
// componets
import ChapterWork from './chapterWork.vue'
import ChapterTest from './chapterTest.vue'
export default {
name: 'ViewerWork',
components: { ChapterWork, ChapterTest },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
currentCompoent() {
const componentNames = {
1: 'ChapterTest', // 课后测验
2: 'ChapterWork' // 作业
}
const homework = this.chapter.homework
return homework ? componentNames[homework.work_type] : ''
}
}
}
</script>
<template>
<div class="course-viewer">
<div class="course-viewer-main">
<!-- 顶部区域 -->
<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-main-hd__title">{{ detail.course_name }}</h1>
<!-- 直播的时候显示帮助按钮 -->
<template v-if="isLive">
<router-link to="/app/feedback/feedback-create" target="_blank">
<el-tooltip effect="light" content="意见反馈">
<i class="el-icon-self-fankuiyijian"></i>
</el-tooltip>
</router-link>
<router-link to="/mobile/help/student" target="_blank">
<el-tooltip effect="light" content="帮助">
<i class="el-icon-self-icon-test"></i>
</el-tooltip>
</router-link>
</template>
<div class="course-menu" @click="menuVisible = !menuVisible">
<i class="el-icon-s-unfold" v-if="menuVisible"></i>
<i class="el-icon-s-fold" v-else></i>
</div>
</div>
<!-- 主体区域 -->
<div class="course-viewer-main-bd">
<router-view
:data="detail"
:chapter="activeChapter"
:pptIndex="pptIndex"
:isSeek="isSeek"
:key="pid"
@pptupdate="handlePPTupdate"
@change-ppt="handleChangePPT(...arguments, false)"
@update="getCourse"
/>
</div>
</div>
<!-- 侧边栏 -->
<v-aside
:data="detail"
:chapters="chapters"
:active="activeChapter"
:ppts="ppts"
:pptIndex="pptIndex"
@change-ppt="handleChangePPT(...arguments, true)"
v-if="detail.chapters"
v-show="menuVisible"
></v-aside>
</div>
</template>
<script>
// api
import * as api from './api'
// components
import VAside from './components/aside/index.vue'
export default {
name: 'CourseViewer',
components: { VAside },
data() {
return {
detail: {},
ppts: [],
pptIndex: 0,
isSeek: false,
menuVisible: true
}
},
watch: {
activeChapter() {
this.ppts = []
this.pptIndex = 0
},
isLive(value) {
if (value) {
this.menuVisible = false
}
},
isCourseExam(value) {
if (value) {
this.menuVisible = false
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 章节列表
chapters() {
const chapters = this.detail.chapters || []
if (!chapters.length) {
return []
}
const customeChapter = {
name: '大作业及资料',
children: [
{ name: '课程大作业', id: 'course_work', type: 99 },
{ name: '课程资料', id: 'course_info', type: 100 },
{ name: '教学评估', id: 'teach_evaluation', type: 102 }
]
}
// 课程考试
if (this.detail.course_examination) {
customeChapter.children.push({
name: '课程考试',
id: 'course_exam',
type: 101
})
}
chapters.push(customeChapter)
return chapters
},
// 当前选中的章节
activeChapter() {
const id = this.pid
const list = this.chapters
return this.findChapter(id, list)
},
// 直播
isLive() {
return this.activeChapter ? [5, 8].includes(this.activeChapter.type) : false
},
// 课程考试
isCourseExam() {
return this.activeChapter ? this.activeChapter.type === 101 : false
}
},
methods: {
// 查找当前章节
findChapter(id, list) {
for (const item of list) {
if (item.id === id) {
return item
}
if (item.children && item.children.length) {
const found = this.findChapter(id, item.children)
if (found) {
return found
}
}
}
return null
},
// 获取课程详情
getCourse() {
api.getCourse(this.sid, this.cid).then(response => {
this.detail = response
})
},
// PPT列表更新
handlePPTupdate(list) {
this.ppts = list
},
// 右侧菜单选中的PPT修改
handleChangePPT(index, isSeek) {
this.pptIndex = index
this.isSeek = isSeek
}
},
beforeMount() {
this.getCourse()
}
}
</script>
<style lang="scss">
.course-viewer {
display: flex;
height: 100vh;
overflow: hidden;
}
.course-viewer-main {
flex: 1;
display: flex;
flex-direction: column;
}
.course-viewer-main-hd {
display: flex;
align-items: center;
background-color: #3f3f3f;
height: 56px;
a {
color: #fff;
padding: 10px;
}
i {
font-size: 24px;
}
}
.course-viewer-main-hd__title {
flex: 1;
font-size: 1.5em;
// text-align: center;
color: #a0a0a0;
}
.course-viewer-main-bd {
flex: 1;
height: calc(100vh - 56px);
overflow-y: auto;
}
.course-viewer-content {
// min-height: 50%;
max-width: 900px;
padding: 40px 120px 80px;
margin: 40px auto;
background-color: #f2f2f2;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.05);
}
.course-viewer-content-hd {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40px 0;
// text-align: center;
}
.course-viewer-content-hd__title {
position: relative;
display: inline-block;
margin: 0 0 0 20px;
padding: 0 0 5px;
font-size: 20px;
border-bottom: 3px solid #707070;
&::before {
content: '·';
position: absolute;
left: -30px;
top: 50%;
font-size: 30px;
transform: translateY(-50%);
}
&::after {
content: '';
position: absolute;
left: 0;
bottom: -8px;
width: 100%;
height: 1px;
background-color: #707070;
}
}
.course-viewer-content-hd__aside {
font-size: 18px;
// border-bottom: 3px solid #707070;
}
.course-menu {
width: 24px;
height: 24px;
padding: 12px;
margin-right: 10px;
color: #fff;
text-align: center;
border-radius: 50%;
cursor: pointer;
&:hover {
background-color: rgba(255, 255, 255, 0.08);
}
}
</style>
export default [
{
path: '/viewer/:sid/:cid',
component: () => import('./index.vue'),
children: [
{
name: 'viewerCourseChapter',
path: ':id',
component: () => import('./components/layout.vue')
}
]
}
]
<template>
<app-container title="修改密码">
<el-form :model="ruleForm" :rules="rules" label-width="90px" ref="ruleForm" class="form">
<el-form :model="ruleForm" :rules="rules" label-width="100px" ref="ruleForm" class="form">
<el-form-item label="旧密码" prop="old_password">
<el-input type="password" v-model="ruleForm.old_password" placeholder="请输入密码"></el-input>
</el-form-item>
......@@ -11,7 +11,7 @@
<el-input type="password" v-model="ruleForm.passwordR" placeholder="请重复输入新密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmit">保存</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
</el-form-item>
</el-form>
</app-container>
......@@ -19,16 +19,38 @@
<script>
import AppContainer from '@/components/AppContainer'
import * as api from '@/api/account'
export default {
components: { AppContainer },
data() {
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.ruleForm.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
}
return {
ruleForm: {
old_password: '',
password: '',
passwordR: ''
},
rules: {}
rules: {
old_password: { required: true, message: '请输入登录密码', trigger: 'blur' },
password: [
{ required: true, message: '请输入新的登录密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度为6-20个字符', trigger: 'blur' }
],
passwordR: [
{ required: true, message: '请再次输入新的登录密码', trigger: 'blur' },
{ validator: validatePass, trigger: 'blur' }
]
},
submitLoading: false
}
},
methods: {
......@@ -36,7 +58,17 @@ export default {
this.$refs.ruleForm.validate().then(this.handleSubmitRequest)
},
handleSubmitRequest() {
console.log(this.ruleForm)
this.submitLoading = true
api
.updatePassword(this.ruleForm)
.then(response => {
this.$message({ message: '密码修改成功', type: 'success' })
// 重置表单
this.$refs.ruleForm.resetFields()
})
.finally(() => {
this.submitLoading = false
})
}
}
}
......
<template>
<div class="main-container" v-loading="!loaded">
<div class="main-container" element-loading-text="加载中..." v-loading="!loaded">
<div class="course-top" v-if="detail.curriculum">
<div class="course-top-hd">
<div class="course-top__title">{{ detail.curriculum.curriculum_name }}</div>
......@@ -10,10 +10,10 @@
</div>
</div>
<el-tabs v-model="tabActive">
<el-tab-pane label="按章节学习">
<el-tab-pane lazy label="按章节学习">
<course-chapter :courseId="courseId" :data="detail.chapters" @on-click="handleClick"></course-chapter>
</el-tab-pane>
<el-tab-pane label="按考点学习">
<el-tab-pane lazy label="按考点学习">
<course-tag :courseId="courseId"></course-tag>
</el-tab-pane>
</el-tabs>
......
import BaseAPI from '@/api/base_api'
const httpRequest = new BaseAPI(webConf)
/**
* 获取课程列表
*/
export function getCourseList() {
return httpRequest.get('/api/zy/v2/education/mokuai')
}
<template>
<div class="course-tag-message">
<div class="course-tag-message-hd">
<div class="course-tag-message__title">{{data.name}}</div>
<div class="course-tag-message__more" @click="viewMore" v-if="hasMore">
<span>更多</span>
<van-icon name="arrow" />
</div>
</div>
<div class="course-tag-message-bd" v-if="dataList && dataList.length">
<ul class="message-tag-list" ref="content">
<template v-for="item in dataList">
<li class="course-tag-item" :key="item.id" @click="onClick(item)">
<p>{{item.title}}{{item.free? '(免费)': ''}}</p>
</li>
</template>
</ul>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'CourseTagMessage',
props: {
courseId: { type: String },
isTest: { type: Boolean, default: false },
data: { type: Object }
},
data() {
return {
maxCount: 8
}
},
computed: {
...mapState(['isWeapp', 'isAndroid', 'isIos', 'isVip', 'isLogin']),
hasMore() {
return this.data.tag ? this.data.tag.length > this.maxCount : false
},
// 最多显示8条
dataList() {
let list = this.data.tag || []
if (!this.isVip) {
// 免费的在前
list = [...list.filter(item => item.free), ...list.filter(item => !item.free)]
}
return list.slice(0, this.maxCount)
},
moreText() {
if (this.isVip) {
return '更多'
}
if (this.isIos) {
return '更多请开通'
}
return '更多请购买'
}
},
methods: {
onClick(data) {
// 未登录
if (!data.free && !this.isLogin) {
this.isWeapp ? wx.miniProgram.navigateTo({ url: '/pages/login/index' }) : this.$router.push({ name: 'login' })
return
}
// 未开通
if (!data.free && !this.isVip) {
this.isWeapp
? wx.miniProgram.navigateTo({ url: `/pages/web/index?src=${window.location.origin}/pay` })
: this.$router.push({ name: 'pay' })
return
}
if (this.isTest) {
// 知识点测试
const path = `/exam/courseNodeExam?tag_id=${data.id}`
if (this.isWeapp) {
const src = encodeURIComponent(`${window.location.origin}${path}`)
wx.miniProgram.navigateTo({ url: `/pages/web/index?src=${src}` })
} else {
this.$router.push({ path })
}
} else {
this.$emit('change', data)
}
},
viewMore() {
const path = `/course/learn/${this.courseId}/tag/${this.data.id}?is_test=${this.isTest ? '1' : '0'}`
if (this.isWeapp) {
const src = encodeURIComponent(`${window.location.origin}${path}`)
wx.miniProgram.navigateTo({
url: `/pages/web/index?src=${src}`
})
} else {
this.$router.push({ path })
}
}
}
}
</script>
<style lang="scss" scoped>
.course-tag-message {
padding: 10px;
background-color: #fff;
border-radius: 6px;
}
.course-tag-message-hd {
display: flex;
}
.course-tag-message__title {
flex: 1;
font-size: 15px;
font-weight: bold;
color: #222;
}
.course-tag-message__more {
display: flex;
align-items: center;
margin-left: 20px;
font-size: 13px;
color: #f47885;
}
.message-tag-list {
margin-top: 20px;
}
.course-tag-item {
display: inline-block;
max-width: 100%;
margin: 0 10px 10px 0;
padding: 0 15px;
background: #f47885;
border-radius: 12px;
cursor: pointer;
p {
font-size: 13px;
color: #fff;
line-height: 24px;
}
}
.more {
padding-top: 10px;
border-top: 1px solid #eee;
font-size: 13px;
color: #222;
text-align: center;
cursor: pointer;
}
</style>
<template>
<div class="message-card" :class="classes">
<div class="message-card-content">
<div class="message-arrow"></div>
<slot :data="data.payload">
<div class="message-card-text" v-if="data.type === 0">{{data.payload.text}}</div>
<course-tag-message
:courseId="courseId"
:isTest="isTest"
:data="data.payload"
v-on="$listeners"
v-if="data.type === 1"
></course-tag-message>
<search-tag-message
:courseId="courseId"
:isTest="isTest"
:data="data.payload"
v-on="$listeners"
v-if="data.type === 2"
></search-tag-message>
<tag-message
:courseId="courseId"
:data="data.payload"
v-on="$listeners"
v-if="data.type === 3"
></tag-message>
</slot>
</div>
</div>
</template>
<script>
import CourseTagMessage from './courseTagMessage.vue'
import SearchTagMessage from './searchTagMessage.vue'
import TagMessage from './tagMessage.vue'
export default {
name: 'MessageCard',
props: {
courseId: { type: String },
isTest: { type: Boolean, default: false },
data: { type: Object }
},
components: { CourseTagMessage, SearchTagMessage, TagMessage },
computed: {
isMyPublish() {
return this.data.from === 'user'
},
classes() {
return {
'is-my': this.isMyPublish,
'is-system': !this.isMyPublish
}
}
}
}
</script>
<style lang="scss" scoped>
.message-card {
clear: both;
margin-bottom: 20px;
}
.message-card-text {
display: inline-block;
padding: 10px;
background-color: #fff;
border-radius: 6px;
}
.is-my {
.message-arrow {
position: absolute;
right: -5px;
top: 16px;
width: 0;
height: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 5px solid #f47885;
}
.message-card-content {
text-align: right;
}
.message-card-text {
text-align: left;
color: #fff;
background-color: #f47885;
}
}
.message-card-content {
position: relative;
}
.is-system {
.message-arrow {
position: absolute;
left: -5px;
top: 16px;
width: 0;
height: 0;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-right: 5px solid #fff;
}
}
</style>
<template>
<div class="search-tag-message">
<p class="tips">交小通猜你想查:</p>
<ul class="search-tag-list">
<template v-for="(item,index) in dataList">
<li class="search-tag-item" :key="item.id" @click="onClick(item)">
<span class="num">{{index+1}}:</span>
<span class="text">{{item.title}}{{item.free ? '(免费)': ''}}</span>
</li>
</template>
</ul>
<div class="more" @click="toggleMore" v-if="hasMore">
<template v-if="!showMore">
<span>更多</span>
<van-icon name="arrow-down"></van-icon>
</template>
<template v-else>
<span>收起</span>
<van-icon name="arrow-up"></van-icon>
</template>
</div>
</div>
</template>
<script>
export default {
name: 'SearchTagMessage',
props: {
courseId: { type: String },
isTest: { type: Boolean, default: false },
data: {
type: Array,
default() {
return []
}
}
},
data() {
return { showMore: false, maxCount: 7 }
},
computed: {
isWeapp() {
return this.$store.state.isWeapp
},
isVip() {
return this.$store.state.isVip
},
isLogin() {
return this.$store.state.isLogin
},
hasMore() {
return this.data.length > this.maxCount
},
dataList() {
if (this.hasMore && !this.showMore) {
return this.data.filter((item, index) => index < this.maxCount)
}
return this.data
}
},
methods: {
onClick(data) {
// 未登录
if (!data.free && !this.isLogin) {
this.isWeapp ? wx.miniProgram.navigateTo({ url: '/pages/login/index' }) : this.$router.push({ name: 'login' })
return
}
// 未开通
if (!data.free && !this.isVip) {
this.isWeapp
? wx.miniProgram.navigateTo({ url: `/pages/web/index?src=${window.location.origin}/pay` })
: this.$router.push({ name: 'pay' })
return
}
if (this.isTest) {
// 知识点测试
const path = `/exam/courseNodeExam?tag_id=${data.id}`
if (this.isWeapp) {
const src = encodeURIComponent(`${window.location.origin}${path}`)
wx.miniProgram.navigateTo({ url: `/pages/web/index?src=${src}` })
} else {
this.$router.push({ path })
}
} else {
this.$emit('change', data)
}
},
toggleMore() {
this.showMore = !this.showMore
}
}
}
</script>
<style lang="scss" scoped>
.search-tag-message {
padding: 10px;
background-color: #fff;
border-radius: 6px;
}
.search-tag-item {
display: flex;
margin: 10px 0;
.num {
padding-right: 5px;
}
.text {
border-bottom: 1px solid #c62245;
cursor: pointer;
}
}
.more {
padding-top: 10px;
border-top: 1px solid #eee;
font-size: 13px;
color: #222;
text-align: center;
cursor: pointer;
}
</style>
<template>
<div>
<div class="tag-message" :class="classes" @click="showMore = true">
<div class="tag-message-bd">
<div class="tag-message-content" ref="content" v-html="html"></div>
</div>
<div class="more" @click.stop="toggleMore" v-if="hasMore">
<template v-if="!showMore">
<span>更多</span>
<van-icon name="arrow-down"></van-icon>
</template>
<template v-else>
<span>收起</span>
<van-icon name="arrow-up"></van-icon>
</template>
</div>
</div>
<div class="tools">
<ul>
<li @click="toExamPage" v-if="data.has_kaoshi">相关试题</li>
<li @click="toCourseVideo" v-if="data.has_video">相关视频</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'TagMessage',
props: {
courseId: { type: String },
data: { type: Object }
},
data() {
return {
showMore: false,
maxHeight: 90,
contentHeight: 0
}
},
computed: {
hasMore() {
return this.contentHeight > this.maxHeight
},
classes() {
return {
'has-more': this.hasMore && !this.showMore
}
},
html() {
let contents = this.data.contents || ''
const hasHtmlTag = contents.includes('</p>')
contents = contents
.replace(/---------- start ----------[\n]{0,1}/gi, '')
.replace(/---------- end ----------/gi, '')
return hasHtmlTag ? contents : contents.replace(/\n/g, '<br/>')
},
isWeapp() {
return this.$store.state.isWeapp
}
},
methods: {
toggleMore() {
this.showMore = !this.showMore
},
viewMore() {
if (this.isWeapp) {
wx.miniProgram.navigateTo({
url: `/pages/web/index?src=${window.location.origin}/course/tag/${this.data.id}`
})
} else {
this.$router.push({
name: 'courseTagItem',
params: { id: this.data.id }
})
}
},
// 去知识点考试页面
toExamPage() {
const path = `/exam/courseNodeExam?tag_id=${this.data.id}`
if (this.isWeapp) {
const src = encodeURIComponent(`${window.location.origin}${path}`)
wx.miniProgram.navigateTo({ url: `/pages/web/index?src=${src}` })
} else {
this.$router.push({ path })
}
},
// 去课程视频页面
toCourseVideo() {
if (this.isWeapp) {
wx.miniProgram.navigateTo({
url: `/pages/course/item?id=${this.data.course_id}&chapter_id=${this.data.section_id}`
})
} else {
window.alert('请在微信小程序中打开')
}
}
},
mounted() {
this.contentHeight = this.$refs.content.offsetHeight
}
}
</script>
<style lang="scss" scoped>
.tag-message {
padding: 10px;
background-color: #fff;
border-radius: 6px;
}
.has-more .tag-message-bd {
height: 90px;
overflow: hidden;
}
.tag-message-content {
font-size: 13px;
line-height: 30px;
::v-deep img {
max-width: 100%;
}
::v-deep b {
font-weight: bold;
}
}
.more {
padding-top: 10px;
border-top: 1px solid #eee;
font-size: 13px;
color: #222;
text-align: center;
cursor: pointer;
}
.tools {
margin-top: 5px;
li {
display: inline-block;
height: 24px;
margin: 0 10px 10px 0;
padding: 0 15px;
font-size: 13px;
color: #fff;
line-height: 24px;
background: #f47885;
border-radius: 12px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
}
}
</style>
<template>
<div class="course-tag">
<div class="messages" ref="messages">
<template v-for="item in messageList">
<message-card
:courseId="courseId"
:isTest="isTest"
:data="item"
:key="item.id"
@search="onSearchTag"
@change="onChangeTag"
></message-card>
</template>
</div>
<div class="send">
<div class="inner">
<form action="/">
<van-search
v-model="searchValue"
left-icon
show-action
background="#f7f7f7"
:maxlength="40"
placeholder="全国道路运输安全生产管理培训"
@search="onSearch"
>
<template #action>
<div @click="onSearch">搜索</div>
</template>
</van-search>
</form>
</div>
</div>
<div class="backtop" @click="scrollTop" v-show="showBacktop"></div>
</div>
<el-collapse v-model="activeNames">
<el-collapse-item :title="item.name" :name="item.id" v-for="item in detail.chapters" :key="item.id">
<ul>
<li v-for="subItem in item.tag" :key="subItem.id" @click="handleClick(subItem)">
<div class="name">{{ subItem.title }}</div>
</li>
</ul>
</el-collapse-item>
</el-collapse>
</template>
<script>
import MessageCard from './components/messageCard.vue'
import * as api from '@/api/course.js'
export default {
props: {
......@@ -48,164 +22,56 @@ export default {
},
isTest: { type: Boolean, default: false }
},
components: { MessageCard },
data() {
return {
detail: { chapters: [] },
messageList: [], // {id:'', type: 1, payload: {}}
searchValue: '',
showBacktop: false
}
},
watch: {
messageList(list) {
if (list.length > this.detail.chapters.length) {
this.scrollBottom()
}
this.$emit('message', list)
activeNames: []
}
},
methods: {
// 获取知识点列表
getCourseTagList() {
api.getCourseTagList(this.courseId).then(response => {
this.$emit('ready', response)
this.detail = response
this.messageList = response.chapters.map((item, index) => {
return {
id: this.genId(index),
type: 1,
from: 'system',
payload: item
}
})
})
},
// 输入搜索
onSearch() {
if (!this.searchValue.trim()) {
return
}
this.messageList.push({
id: this.genId(),
type: 0,
from: 'user',
payload: { text: this.searchValue }
})
this.searchTag(this.searchValue)
this.searchValue = ''
},
// 点击标签
onSearchTag(data) {
this.messageList.push({
id: this.genId(),
type: 0,
from: 'user',
payload: { text: data.name }
})
this.searchTag(data.name)
},
searchTag(keywords) {
api.getSearchTagList({ keywords, course_id: this.courseId }).then(response => {
if (response.length) {
this.messageList.push({
id: this.genId(),
type: 2,
from: 'system',
payload: response
})
} else {
this.messageList.push({
id: this.genId(),
type: 0,
from: 'system',
payload: { text: '找不到相关内容' }
})
}
})
},
onChangeTag(data) {
this.messageList.push({
id: this.genId(),
type: 0,
from: 'user',
payload: { text: data.title }
})
this.getCourseTag(data.id)
},
// 获取知识点详情
getCourseTag(tagId) {
api.getCourseTag(tagId).then(response => {
this.messageList.push({
id: this.genId(),
type: 3,
from: 'system',
payload: response
})
})
},
// 生成消息ID
genId(index) {
index = index || this.messageList.length
return `message_${index}`
},
// 滚动到底部
scrollBottom() {
this.$nextTick(() => {
window.scrollTo(0, document.body.scrollHeight)
})
},
// 滚到到顶部
scrollTop() {
window.scrollTo(0, 0)
},
handleScroll() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop
this.showBacktop = scrollTop >= 10
handleClick(data) {
this.$router.push({ name: 'courseTagItem', params: { id: data.id } })
}
},
beforeMount() {
this.getCourseTagList()
},
mounted() {
window.addEventListener('scroll', this.handleScroll)
},
destroyed() {
window.removeEventListener('scroll', this.handleScroll)
}
}
</script>
<style lang="scss" scoped>
.messages {
background: #eee;
.el-collapse {
border: 0;
}
.send {
height: 50px;
padding-bottom: env(safe-area-inset-bottom);
.inner {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: rgba(247, 247, 247, 1);
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.05);
z-index: 999;
padding-bottom: env(safe-area-inset-bottom);
}
::v-deep .van-search__content {
background-color: #fff;
}
::v-deep .el-collapse-item__header {
font-size: 14px;
font-weight: 600;
color: #222;
}
::v-deep .el-collapse-item__content {
padding-bottom: 10px;
}
.backtop {
position: fixed;
right: 15px;
bottom: 64px;
width: 46px;
height: 46px;
background: url('../../../assets/images/icon_backtop.png') no-repeat;
background-size: contain;
li {
display: flex;
padding: 5px 0;
cursor: pointer;
margin-bottom: env(safe-area-inset-bottom);
color: #666;
&:hover {
color: #c01540;
}
.name {
flex: 1;
overflow: hidden;
}
.progress {
margin-left: 20px;
color: #999;
}
}
</style>
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论