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

add viewer module

上级 2e07d544
......@@ -10,7 +10,8 @@
},
"rules": {
"no-new": "off",
"no-debugger": "off"
"no-debugger": "off",
"space-before-function-paren": "off"
},
"globals": {
"CKEDITOR": false,
......
......@@ -7,7 +7,7 @@
<title></title>
<meta name="viewport" id="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, shrink-to-fit=no">
<!-- 直接引入aliyun播放插件 CSS -->
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.8.2/skins/default/aliplayer-min.css" />
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.8.8/skins/default/aliplayer-min.css" />
</head>
<body>
<div id="app"></div>
......@@ -23,7 +23,7 @@
<script type="text/javascript" src="https://zws-imgs-pub.ezijing.com/static/build/learn-mba/static/common/jQuery-2.1.4.min.js"></script>
<script type="text/javascript" src="https://zws-imgs-pub.ezijing.com/static/build/learn-mba/static/common/runtime.js"></script>
<!-- 直接引入aliyun播放插件 JS -->
<script type="text/javascript" charset="utf-8" src="https://g.alicdn.com/de/prismplayer/2.8.2/aliplayer-min.js"></script>
<script type="text/javascript" charset="utf-8" src="https://g.alicdn.com/de/prismplayer/2.8.8/aliplayer-min.js"></script>
<script type="text/javascript" charset="utf-8" src="https://player.alicdn.com/aliplayer/presentation/js/aliplayercomponents.min.js"></script>
<!-- 解决iframe嵌套,CC视频在safri中打开免登陆兼容问题 -->
<script src="//view.csslcloud.net/js/_fix_.js"></script>
......
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>
<div class="course-viewer-aside">
<el-tabs v-model="activeName">
<el-tab-pane label="章节" name="0">
<div class="tab-pane">
<aside-chapter :data="chapters"></aside-chapter>
</div>
</el-tab-pane>
<el-tab-pane label="讲义" name="1">
<div class="tab-pane">
<aside-lecture :data="ppts"></aside-lecture>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import AsideChapter from './asideChapter.vue'
import AsideLecture from './asideLecture.vue'
export default {
props: {
// 章节
chapters: {
type: Array,
default() {
return []
}
},
// 讲义
ppts: {
type: Array,
default() {
return []
}
}
},
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="chapter-list">
<li class="chapter-item" v-for="item in list" :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>
</li>
</ul>
</li>
</ul>
</template>
<script>
export default {
props: {
data: {
type: Array,
default() {
return []
}
}
},
data() {
return {
otherList: [
{
name: '大作业及资料',
children: [
{ name: '课程大作业', id: 'course_work' },
{ name: '课程资料', id: 'course_info' },
{ name: '教学评估', id: 'teach_evaluation' }
]
}
]
}
},
computed: {
list() {
return this.data.concat(this.otherList)
}
},
filters: {
showName(name, type) {
return name
}
},
methods: {
onClick(data) {
console.log(data)
}
}
}
</script>
<style lang="scss" scoped>
/* 章列表样式 */
.chapter-list {
margin: 0;
padding: 0;
line-height: 1.6;
overflow: hidden;
.chapter-item {
h4 {
padding: 10px 32px;
margin: 0;
font-size: 15px;
color: #b0b0b0;
background-color: #2f2f2f;
}
/* 节列表样式 */
.knot-list {
margin: 0;
padding: 0;
line-height: 1.6;
overflow: hidden;
li {
position: relative;
&.on {
background: #3c3c3c;
a {
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;
}
}
.knot-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%);
}
}
}
</style>
<template>
<ul class="lecture-list">
<li v-for="item in data" :key="item.id" @click="onClick(item)">
<img :src="item.ppt_url" />
</li>
</ul>
</template>
<script>
export default {
props: {
data: {
type: Array,
default() {
return []
}
}
},
data() {
return {
activeIndex: 0
}
},
methods: {
// 点击PPT
onClick(data) {
this.activeIndex = data.id
this.$emit('clickPPT', data)
}
}
}
</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="player">
<div class="player-main">
<div class="player-column" v-show="videoVisible">
<!-- 视频 -->
<video-player :video="video"></video-player>
</div>
<div class="player-column" v-if="pptVisible">
<!-- ppt -->
<ppt-player :ppts="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>
<em :class="pptClass" @click="togglePPTVisible" v-if="ppts.length">同步显示PPT</em>
<em :class="skipClass" @click="toggleSkip">始终跳过片头</em>
</div>
</div>
</template>
<script>
import videoPlayer from './videoPlayer.vue'
import pptPlayer from './pptPlayer.vue'
export default {
name: 'Player',
components: { videoPlayer, pptPlayer },
props: {
video: { type: Object },
pdf: { type: String },
ppts: {
type: Array,
default() {
return []
}
}
},
data() {
return {
videoVisible: true,
pptVisible: false,
isSkip: false
}
},
computed: {
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
},
// PPT全屏
onPPTFullscreen(value) {
this.videoVisible = !value
}
}
}
</script>
<style lang="scss" scoped>
.player {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.player-main {
display: flex;
flex: 1;
}
.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">
<i :class="['el-icon-self-xuexiao', (currentSync ? 'active' : '')]" @click="onToggleSync"></i>
<i class="el-icon-self-quanping" @click="fullscreen"></i>
<i class="el-icon-self-shipin" @click="setVideoTime"></i>
<i class="el-icon-self-guanbi" @click="$emit('close')"></i>
</div>
</div>
</template>
</div>
</template>
<script>
export default {
name: 'ppt-player',
props: {
ppts: { type: Array },
index: { type: Number, default: 0 },
isSync: { type: Boolean, default: false }
},
data() {
return {
currentIndex: this.index,
currentSync: this.isSync,
isFullscreen: false
}
},
watch: {
index: {
handler(value) {
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.currentSync = false
},
next(e) {
this.currentIndex = this.getIndex(this.currentIndex + 1)
this.currentSync = false
},
onToggleSync(e) {
this.currentSync = !this.currentSync
this.currentIndex = this.currentSync
? this.currentIndex
: this.currentIndex
},
setVideoTime(e) {
this.$emit('onVideoSyncTime', 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: { video: Object },
data() {
return { player: null }
},
methods: {
createPlayer() {
const { FD, LD, SD } = this.video
this.player = new Aliplayer(
{
id: 'player',
source: JSON.stringify({ FD, LD, SD }),
width: '100%',
height: '100%',
autoplay: true,
isLive: false,
controlBarVisibility: 'always',
components: [
{
name: 'QualityComponent',
type: AliPlayerComponent.QualityComponent
}
]
},
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
player
.getComponent('QualityComponent')
.setCurrentQuality(desc, definition)
})
}
)
}
},
mounted() {
this.createPlayer()
},
beforeDestroy() {
this.player && this.player.dispose()
}
}
</script>
<style lang="scss" scoped>
.video-player {
width: 100%;
height: 100%;
}
</style>
<template>
<div class="course-viewer">
<div class="course-viewer-main">
<!-- 顶部区域 -->
<div class="course-viewer-hd">
<router-link to="/mobile/help/student">
<i class="el-icon-arrow-left"></i>
</router-link>
<h1 class="course-viewer-hd__title">{{detail.course_name}}</h1>
<router-link to="/app/account/feedbackCreate" 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>
</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>
</div>
<!-- 侧边栏 -->
<v-aside :chapters="detail.chapters" :ppts="chatperResources.ppts"></v-aside>
</div>
</template>
<script>
// api
import * as api from './api/index'
// components
import VAside from './components/aside/aside.vue'
import Player from './components/player/player.vue'
export default {
name: 'CourseViewer',
components: { VAside, Player },
data() {
return {
detail: {},
chatperResources: {}
}
},
computed: {
// 当前章节
activeChapter() {
return {
resource_id: '6414747439944695808'
}
},
// 视频资源ID
resourceId() {
return this.activeChapter.resource_id
},
/**
* 视频提供者
* @return 1是CC加密; 2是非加密; 3是阿里云
*/
videoProvider() {
const video = this.activeChapter.video || {}
return video.video_provider || 3
}
},
methods: {
// 获取课程详情
getCourse() {
api
.getCourse('6437296642994470912', '6437335122927681536')
.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
})
}
}
},
beforeMount() {
this.getCourse()
this.getChapterVideo()
}
}
</script>
<style lang="scss" scoped>
.course-viewer {
display: flex;
background-color: #3f3f3f;
}
.course-viewer-main {
flex: 1;
display: flex;
flex-direction: column;
}
.course-viewer-hd {
display: flex;
align-items: center;
height: 56px;
a {
color: #fff;
padding: 10px;
}
i {
font-size: 24px;
}
}
.course-viewer-hd__title {
flex: 1;
font-size: 1.5em;
text-align: center;
color: #a0a0a0;
}
.course-viewer-bd {
flex: 1;
}
</style>
......@@ -385,10 +385,10 @@ export default {
}).catch(e => { this.$message.error(e.message); loading.close() }).finally(() => { })
window.addEventListener('resize', this.resizeRoot.bind(this), false)
/* 实时刷新数据 */
if (this.timeHeart) { clearInterval(this.timeHeart); this.timeHeart = null }
this.timeHeart = setInterval(() => {
this.updatePages()
}, 3000)
// if (this.timeHeart) { clearInterval(this.timeHeart); this.timeHeart = null }
// this.timeHeart = setInterval(() => {
// this.updatePages()
// }, 3000)
},
destroyed () {
window.removeEventListener('resize', this.resizeRoot.bind(this), false)
......
......@@ -155,7 +155,7 @@ export default {
},
beforeRouteUpdate (to, from, next) {
/* 只有 视频时 才有PPT */
if (to.name === 'chapterVideo') {
if (to.name === 'video') {
this.state.isChapterVideo = true
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
const str = to.params.videoType !== '3' ? 'getCurrentChapterDetail' : 'getCurrentChapterDetailAliyun'
......@@ -198,7 +198,7 @@ export default {
},
mounted () {
/* 只有 视频时 才有PPT */
if (this.$route.name === 'chapterVideo') {
if (this.$route.name === 'video') {
this.state.isChapterVideo = true
const loading = this.$loading({ lock: true, text: '', spinner: '', background: 'rgba(255, 255, 255, 0.9)' })
const str = this.videoType !== '3' ? 'getCurrentChapterDetail' : 'getCurrentChapterDetailAliyun'
......
......@@ -192,5 +192,6 @@ export default [
// /* survey-phone 内未找到页面时 - 指向 */
// { path: '/survey-phone/*', redirect: '/learn-error/learn-error' },
/* 如果所有页面都没找到 - 指向 */
{ path: '*', component: () => import('@/components/errorPages/404.vue') }
{ path: '*', component: () => import('@/components/errorPages/404.vue') },
{ path: '/viewer', component: () => import('@/modules/viewer/index.vue') }
]
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论