|
@@ -0,0 +1,846 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="pc-player-container">
|
|
|
|
|
+ <div class="pc-player-wrapper">
|
|
|
|
|
+ <div class="pc-player-left">
|
|
|
|
|
+ <div class="video-section">
|
|
|
|
|
+ <div class="video-title">{{ media.name || tpl.name }}</div>
|
|
|
|
|
+ <div class="video-player-wrap">
|
|
|
|
|
+ <video-player id="myVideo1" ref="videoPlayer" :playsinline="true" @pause="onPlayerPause($event)"
|
|
|
|
|
+ @timeupdate="onPlayerTimeupdate($event)" @play="onPlayerStart($event)" @ready="playerReadied"
|
|
|
|
|
+ @ended="onPlayerEnded($event)" @error="onPlayerError($event)"
|
|
|
|
|
+ :globalOptions="{controls:true}" :options="options">
|
|
|
|
|
+ </video-player>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="dialog-footer pt30">
|
|
|
|
|
+ <el-row class="media-footer">
|
|
|
|
|
+ <el-col :span="6" class="media-time">
|
|
|
|
|
+ <span>{{curTimes|useTime}}</span>
|
|
|
|
|
+ <strong>/</strong>
|
|
|
|
|
+ <span>{{media.duration|useTime}}</span>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="12" class="media-center">
|
|
|
|
|
+ <span class="volume-control">
|
|
|
|
|
+ <span class="vol-icon" @click="toggleMute">音量</span>
|
|
|
|
|
+ <el-slider class="volume-slider" :value="volume" :min="0" :max="1" :step="0.05"
|
|
|
|
|
+ @input="setVolume"></el-slider>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="6" class="media-select">
|
|
|
|
|
+ <div style="margin-top:0px;float: right;">
|
|
|
|
|
+ <el-select placeholder="流畅" :value="mediaType" class="media-el-select" @change="changeMedia">
|
|
|
|
|
+ <el-option label="流畅" value="ld"></el-option>
|
|
|
|
|
+ <el-option label="标清" value="hls"></el-option>
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="panel-toggle" @click="showPanel = !showPanel">
|
|
|
|
|
+ <i :class="showPanel ? 'el-icon-d-arrow-right' : 'el-icon-d-arrow-left'"></i>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="pc-player-right" v-show="showPanel">
|
|
|
|
|
+ <div class="playlist-header">
|
|
|
|
|
+ <div class="playlist-title">
|
|
|
|
|
+ <i class="el-icon-video-list"></i>
|
|
|
|
|
+ 课程列表
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="playlist-header-row">
|
|
|
|
|
+ <div class="playlist-count">
|
|
|
|
|
+ 共 {{ list.length }} 个视频
|
|
|
|
|
+ <span class="finished-count" v-if="finishedCount">,已完成 {{ finishedCount }} 个</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-switch
|
|
|
|
|
+ v-model="hideFinished"
|
|
|
|
|
+ active-text="隐藏已完成"
|
|
|
|
|
+ inactive-text=""
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ ></el-switch>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="playlist-body">
|
|
|
|
|
+ <div class="playlist-item" v-for="(item, index) in filteredList" :key="item.id" :class="{
|
|
|
|
|
+ 'is-active': media.id === item.id,
|
|
|
|
|
+ 'is-finished': item.isFinish == 1,
|
|
|
|
|
+ 'is-current-playing': media.id === item.id && onPlay
|
|
|
|
|
+ }" @click="loadMedia(item)">
|
|
|
|
|
+ <div class="item-index" :class="{ 'active-index': media.id === item.id }">
|
|
|
|
|
+ <span v-if="item.isFinish == 1" class="finish-badge">
|
|
|
|
|
+ <i class="el-icon-check"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span v-else>{{ index + 1 }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="item-info">
|
|
|
|
|
+ <div class="item-name">{{ item.name }}</div>
|
|
|
|
|
+ <div class="item-meta">
|
|
|
|
|
+ <span class="item-duration">学时:{{ item.xs / 10 }}</span>
|
|
|
|
|
+ <span class="item-progress">{{ (item.position * 100 / item.duration).toFixed(1) }}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="item-action">
|
|
|
|
|
+ <el-button v-if="media.id === item.id && onPlay" type="warning" size="mini" icon="el-icon-video-pause"
|
|
|
|
|
+ circle @click.stop="doPause"></el-button>
|
|
|
|
|
+ <el-button v-else-if="media.id === item.id" type="primary" size="mini" icon="el-icon-video-play" circle
|
|
|
|
|
+ @click.stop="doPlay"></el-button>
|
|
|
|
|
+ <el-button v-else type="text" size="mini" icon="el-icon-video-play"
|
|
|
|
|
+ @click.stop="loadMedia(item)">播放</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="playlist-empty" v-if="list.length === 0">
|
|
|
|
|
+ <i class="el-icon-document"></i>
|
|
|
|
|
+ <p>暂无视频</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+<script>
|
|
|
|
|
+ import { mapActions } from "vuex";
|
|
|
|
|
+ import {
|
|
|
|
|
+ getPercent
|
|
|
|
|
+ } from '@/utils/index.js'
|
|
|
|
|
+ import {
|
|
|
|
|
+ videoPlayer
|
|
|
|
|
+ } from 'vue-video-player';
|
|
|
|
|
+ import 'video.js/dist/video-js.css'
|
|
|
|
|
+ import {
|
|
|
|
|
+ httpServer
|
|
|
|
|
+ } from "@/components/httpServer/httpServer.js";
|
|
|
|
|
+ export default {
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ courseId: 0,
|
|
|
|
|
+ mediaId: 0,
|
|
|
|
|
+ media: {},
|
|
|
|
|
+ options: {
|
|
|
|
|
+ controls: true,
|
|
|
|
|
+ autoplay: true,
|
|
|
|
|
+ muted: true,
|
|
|
|
|
+ loop: false,
|
|
|
|
|
+ preload: "auto",
|
|
|
|
|
+ language: 'zh-CN',
|
|
|
|
|
+ aspectRatio: '16:9',
|
|
|
|
|
+ fluid: true,
|
|
|
|
|
+ sources: [],
|
|
|
|
|
+ poster: '',
|
|
|
|
|
+ notSupportedMessage: '无法播放媒体源',
|
|
|
|
|
+ playtimes: '',
|
|
|
|
|
+ controlBar: {
|
|
|
|
|
+ timeDivider: true,
|
|
|
|
|
+ durationDisplay: true,
|
|
|
|
|
+ remainingTimeDisplay: false,
|
|
|
|
|
+ fullscreenToggle: true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ curTimes: 0,
|
|
|
|
|
+ tpl: {},
|
|
|
|
|
+ info: {isBan:0},
|
|
|
|
|
+ list: [],
|
|
|
|
|
+ timer: false,
|
|
|
|
|
+ tickNum: 0,
|
|
|
|
|
+ prevTime: 0,
|
|
|
|
|
+ isReady: false,
|
|
|
|
|
+ onPlay: false,
|
|
|
|
|
+ volume: 0.5,
|
|
|
|
|
+ isMuted: false,
|
|
|
|
|
+ mediaType: 'hls',
|
|
|
|
|
+ hasSeeked: false,
|
|
|
|
|
+ hideFinished: false,
|
|
|
|
|
+ showPanel: true
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ components: {
|
|
|
|
|
+ videoPlayer
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ finishedCount() {
|
|
|
|
|
+ return this.list.filter(i => i.isFinish == 1).length;
|
|
|
|
|
+ },
|
|
|
|
|
+ filteredList() {
|
|
|
|
|
+ if (this.hideFinished) {
|
|
|
|
|
+ return this.list.filter(i => i.isFinish != 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ return this.list
|
|
|
|
|
+ },
|
|
|
|
|
+ playPercent() {
|
|
|
|
|
+ if (!this.media.duration) return 0;
|
|
|
|
|
+ return +((this.curTimes / this.media.duration) * 100).toFixed(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ filters: {
|
|
|
|
|
+ useTime(val) {
|
|
|
|
|
+ val = parseInt(val) || 0
|
|
|
|
|
+ let timestr = ""
|
|
|
|
|
+ let hour = parseInt(val / 3600);
|
|
|
|
|
+ let min = parseInt(val / 60 % 60);
|
|
|
|
|
+ let sec = parseInt(val % 60);
|
|
|
|
|
+ if (hour < 10) hour = "0" + hour;
|
|
|
|
|
+ if (min < 10) min = "0" + min;
|
|
|
|
|
+ if (sec < 10) sec = "0" + sec;
|
|
|
|
|
+ return hour + ":" + min + ":" + sec
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ getCourse() {
|
|
|
|
|
+ let param = {
|
|
|
|
|
+ courseId: this.courseId
|
|
|
|
|
+ }
|
|
|
|
|
+ httpServer("course.getCourse", param).then(res => {
|
|
|
|
|
+ if (res.code == 200) {
|
|
|
|
|
+ let {
|
|
|
|
|
+ info,
|
|
|
|
|
+ extra,
|
|
|
|
|
+ list,
|
|
|
|
|
+ tpl
|
|
|
|
|
+ } = res.data;
|
|
|
|
|
+ this.info = Object.assign(info, extra || {});
|
|
|
|
|
+ this.tpl = tpl || {};
|
|
|
|
|
+ this.list = list.map((item) => {
|
|
|
|
|
+ item.percent = getPercent(item) || 0;
|
|
|
|
|
+ return item;
|
|
|
|
|
+ })
|
|
|
|
|
+ let target = null
|
|
|
|
|
+ if (this.mediaId) {
|
|
|
|
|
+ target = this.list.find(i => i.id == this.mediaId || i.mediaId == this.mediaId)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!target) {
|
|
|
|
|
+ target = this.list.find(i => i.isFinish != 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!target && this.list.length) {
|
|
|
|
|
+ target = this.list[0]
|
|
|
|
|
+ }
|
|
|
|
|
+ if (target) {
|
|
|
|
|
+ this.loadMedia(target)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ setVolume(val) {
|
|
|
|
|
+ this.volume = val
|
|
|
|
|
+ this.isMuted = val === 0
|
|
|
|
|
+ let player = this.$refs.videoPlayer && this.$refs.videoPlayer.player
|
|
|
|
|
+ if (player && player.volume) {
|
|
|
|
|
+ player.volume(val)
|
|
|
|
|
+ player.muted(this.isMuted)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ changeMedia(val) {
|
|
|
|
|
+ if (!val || !this.mediaUrl) return;
|
|
|
|
|
+ let mediaUrl = this.mediaUrl;
|
|
|
|
|
+ if (val == 'ld') {
|
|
|
|
|
+ mediaUrl = mediaUrl.replace('/hls/', '/ld/');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ mediaUrl = mediaUrl.replace('/ld/', '/hls/');
|
|
|
|
|
+ }
|
|
|
|
|
+ this.mediaType = val;
|
|
|
|
|
+ this.stopTick()
|
|
|
|
|
+ this.onPlay = false
|
|
|
|
|
+ this.curTimes = 0
|
|
|
|
|
+ this.hasSeeked = false
|
|
|
|
|
+ this.options = {
|
|
|
|
|
+ ...this.options,
|
|
|
|
|
+ sources: [{ src: mediaUrl, type: "application/x-mpegURL" }],
|
|
|
|
|
+ playtimes: this.media.position || 0,
|
|
|
|
|
+ autoplay: true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ startTick() {
|
|
|
|
|
+ let tick = this.tryTick;
|
|
|
|
|
+ if (this.timer) clearInterval(this.timer);
|
|
|
|
|
+ this.timer = setInterval(tick, 5 * 1000);
|
|
|
|
|
+ },
|
|
|
|
|
+ stopTick() {
|
|
|
|
|
+ if (this.timer) clearInterval(this.timer);
|
|
|
|
|
+ },
|
|
|
|
|
+ tryTick() {
|
|
|
|
|
+ let that = this;
|
|
|
|
|
+ try {
|
|
|
|
|
+ that.tick()
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ that.reportErr("play", '' + err.message)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ playerReadied(audio) {
|
|
|
|
|
+ this.isReady = true
|
|
|
|
|
+ },
|
|
|
|
|
+ onPlayerError(event) {
|
|
|
|
|
+ console.error("Video error:", event)
|
|
|
|
|
+ },
|
|
|
|
|
+ onPlayerTimeupdate(player) {
|
|
|
|
|
+ let curTimes = player.currentTime();
|
|
|
|
|
+ this.curTimes = curTimes || 0
|
|
|
|
|
+ if (this.media.isFinish) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ safePlay(player) {
|
|
|
|
|
+ if (!player) return
|
|
|
|
|
+ try {
|
|
|
|
|
+ const p = player.play()
|
|
|
|
|
+ if (p && p.catch) p.catch(() => {})
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ },
|
|
|
|
|
+ setposition(position) {
|
|
|
|
|
+ if (position > this.media.duration) position = this.media.duration;
|
|
|
|
|
+ let player = this.$refs.videoPlayer.player;
|
|
|
|
|
+ player.currentTime(position);
|
|
|
|
|
+ this.safePlay(player)
|
|
|
|
|
+ if (this.media.isFinish) return;
|
|
|
|
|
+ if (this.media.position >= this.media.duration - 10 && !this.media.isFinish) {
|
|
|
|
|
+ this.tick(true)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onPlayerPause(event) {
|
|
|
|
|
+ this.reportErr("play", 'pause');
|
|
|
|
|
+ this.stopTick()
|
|
|
|
|
+ this.onPlay = false
|
|
|
|
|
+ },
|
|
|
|
|
+ onPlayerEnded(event) {
|
|
|
|
|
+ this.reportErr("play", 'end');
|
|
|
|
|
+ this.tick(true)
|
|
|
|
|
+ if (this.info.isBan > 1) {
|
|
|
|
|
+ this.playNext()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ playNext() {
|
|
|
|
|
+ let currentIndex = this.list.findIndex(i => i.id === this.media.id)
|
|
|
|
|
+ let nextItem = null
|
|
|
|
|
+ for (let i = currentIndex + 1; i < this.list.length; i++) {
|
|
|
|
|
+ if (this.list[i].isFinish != 1) {
|
|
|
|
|
+ nextItem = this.list[i]
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!nextItem) {
|
|
|
|
|
+ for (let i = 0; i < currentIndex; i++) {
|
|
|
|
|
+ if (this.list[i].isFinish != 1) {
|
|
|
|
|
+ nextItem = this.list[i]
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (nextItem) {
|
|
|
|
|
+ this.$message.success(`即将播放: ${nextItem.name}`)
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.loadMedia(nextItem)
|
|
|
|
|
+ }, 1000)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onClose() {
|
|
|
|
|
+ this.reportErr("play", 'close')
|
|
|
|
|
+ this.doPause()
|
|
|
|
|
+ },
|
|
|
|
|
+ doPause() {
|
|
|
|
|
+ this.stopTick()
|
|
|
|
|
+ this.onPlay = false
|
|
|
|
|
+ let myPlayer = this.$refs.videoPlayer.player;
|
|
|
|
|
+ myPlayer && myPlayer.pause()
|
|
|
|
|
+ },
|
|
|
|
|
+ doPlay() {
|
|
|
|
|
+ this.onPlay = true
|
|
|
|
|
+ this.startTick();
|
|
|
|
|
+ if (!this.$refs.videoPlayer || !this.$refs.videoPlayer.player) return;
|
|
|
|
|
+ this.safePlay(this.$refs.videoPlayer.player)
|
|
|
|
|
+ this.tickNum = 0
|
|
|
|
|
+ },
|
|
|
|
|
+ onPlayerStart() {
|
|
|
|
|
+ this.reportErr("play", 'start');
|
|
|
|
|
+ this.startTick()
|
|
|
|
|
+ this.onPlay = true
|
|
|
|
|
+ if (!this.hasSeeked && this.media.position > 5 && this.media.position < this.media.duration - 10) {
|
|
|
|
|
+ this.hasSeeked = true
|
|
|
|
|
+ let player = this.$refs.videoPlayer && this.$refs.videoPlayer.player
|
|
|
|
|
+ if (player && player.currentTime) {
|
|
|
|
|
+ player.currentTime(this.media.position)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ toggleMute() {
|
|
|
|
|
+ this.isMuted = !this.isMuted
|
|
|
|
|
+ let player = this.$refs.videoPlayer && this.$refs.videoPlayer.player
|
|
|
|
|
+ if (player && player.volume) {
|
|
|
|
|
+ player.muted(this.isMuted)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ reportErr(action, msg) {
|
|
|
|
|
+ httpServer("course.report", { action, msg }, true)
|
|
|
|
|
+ },
|
|
|
|
|
+ tick(force = false) {
|
|
|
|
|
+ let media = this.media;
|
|
|
|
|
+ this.tickNum++
|
|
|
|
|
+ if (this.media.isFinish) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ let myPlayer = this.$refs.videoPlayer.player;
|
|
|
|
|
+ if (!myPlayer) return;
|
|
|
|
|
+ let curTimes = parseInt(myPlayer.currentTime());
|
|
|
|
|
+ if (curTimes < 4) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.media.position > curTimes + 5 && !force) return;
|
|
|
|
|
+ let isFinish = force ? 1 : 0
|
|
|
|
|
+ if (curTimes >= media.duration) isFinish = 1;
|
|
|
|
|
+ if (!isFinish) {
|
|
|
|
|
+ if (!this.onPlay) return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (window.navigator.webdriver) {
|
|
|
|
|
+ this.reportErr("play", 'webdriver');
|
|
|
|
|
+ this.doPause()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ let param = {
|
|
|
|
|
+ id: media.id,
|
|
|
|
|
+ position: curTimes,
|
|
|
|
|
+ isFinish
|
|
|
|
|
+ };
|
|
|
|
|
+ httpServer("course.tick", param, true).then(res => {
|
|
|
|
|
+ if (res.code == 200) {
|
|
|
|
|
+ let {
|
|
|
|
|
+ skip,
|
|
|
|
|
+ position,
|
|
|
|
|
+ pause,
|
|
|
|
|
+ closed
|
|
|
|
|
+ } = res.data
|
|
|
|
|
+ if (pause || closed) {
|
|
|
|
|
+ this.doPause();
|
|
|
|
|
+ if (closed) {
|
|
|
|
|
+ this.$message.errorMsg("禁止多处同时学习", 5);
|
|
|
|
|
+ } else if (pause) {
|
|
|
|
|
+ this.$message.errorMsg("禁止多处同时学习", 5);
|
|
|
|
|
+ }
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!skip) {
|
|
|
|
|
+ let curTimes = parseInt(myPlayer.currentTime())
|
|
|
|
|
+ if (position < curTimes + 5) {
|
|
|
|
|
+ this.setposition(position)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Object.assign(this.media, res.data)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ },
|
|
|
|
|
+ loadMedia(item) {
|
|
|
|
|
+ this.media = item;
|
|
|
|
|
+ httpServer('course.GetMedia', {
|
|
|
|
|
+ id: item.id
|
|
|
|
|
+ }).then(res => {
|
|
|
|
|
+ if (res.code != 200) return;
|
|
|
|
|
+ let {
|
|
|
|
|
+ mediaUrl,
|
|
|
|
|
+ id,
|
|
|
|
|
+ position,
|
|
|
|
|
+ marks,
|
|
|
|
|
+ duration
|
|
|
|
|
+ } = res.data || {};
|
|
|
|
|
+ this.mediaUrl = mediaUrl;
|
|
|
|
|
+ this.media.position = position;
|
|
|
|
|
+ this.media.duration = duration;
|
|
|
|
|
+ this.media.id = id;
|
|
|
|
|
+ this.curTimes = 0
|
|
|
|
|
+ this.onPlay = false
|
|
|
|
|
+ this.hasSeeked = false
|
|
|
|
|
+ this.stopTick()
|
|
|
|
|
+ this.options = {
|
|
|
|
|
+ ...this.options,
|
|
|
|
|
+ sources: [{
|
|
|
|
|
+ src: mediaUrl,
|
|
|
|
|
+ type: "application/x-mpegURL"
|
|
|
|
|
+ }],
|
|
|
|
|
+ playtimes: position || 1,
|
|
|
|
|
+ autoplay: true
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ created() {
|
|
|
|
|
+ let { uid, token, courseId, mediaId } = this.$route.query
|
|
|
|
|
+ this.courseId = +courseId || +this.$route.params.courseId
|
|
|
|
|
+ this.mediaId = mediaId || this.$route.query.mediaId || 0
|
|
|
|
|
+ if (uid) localStorage.uid = uid
|
|
|
|
|
+ if (token) localStorage.token = token
|
|
|
|
|
+ if (this.courseId) {
|
|
|
|
|
+ this.getCourse()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ this.stopTick()
|
|
|
|
|
+ this.reportErr("play", 'destroy')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+</script>
|
|
|
|
|
+<style>
|
|
|
|
|
+ @import url("./play.css");
|
|
|
|
|
+
|
|
|
|
|
+ .pc-player-container {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+ background: #f0f2f5;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .pc-player-wrapper {
|
|
|
|
|
+ max-width: 1440px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ height: calc(100vh - 40px);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .pc-player-left {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-toggle {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ width: 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ transition: color 0.2s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .panel-toggle:hover {
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-section {
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-title {
|
|
|
|
|
+ padding: 16px 20px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1a1a2e;
|
|
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-player-wrap {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ background: #000;
|
|
|
|
|
+ touch-action: none;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-player-wrap .video-js {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-info-bar {
|
|
|
|
|
+ padding: 12px 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-time {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .time-current {
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .time-sep {
|
|
|
|
|
+ margin: 0 4px;
|
|
|
|
|
+ color: #ccc;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-progress {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 100px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-status {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .pc-player-right {
|
|
|
|
|
+ width: 380px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-header {
|
|
|
|
|
+ padding: 16px 20px 12px;
|
|
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-header-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-title {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #1a1a2e;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-title i {
|
|
|
|
|
+ margin-right: 6px;
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-count {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .finished-count {
|
|
|
|
|
+ color: #67C23A;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-body {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 8px 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-body::-webkit-scrollbar {
|
|
|
|
|
+ width: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-body::-webkit-scrollbar-thumb {
|
|
|
|
|
+ background: #ddd;
|
|
|
|
|
+ border-radius: 3px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-body::-webkit-scrollbar-track {
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 12px 20px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ border-left: 3px solid transparent;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-item:hover {
|
|
|
|
|
+ background: #f5f7fa;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-item.is-active {
|
|
|
|
|
+ background: #ecf5ff;
|
|
|
|
|
+ border-left-color: #409EFF;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-item.is-finished {
|
|
|
|
|
+ opacity: 0.65;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-item.is-current-playing {
|
|
|
|
|
+ background: #ecf5ff;
|
|
|
|
|
+ border-left-color: #409EFF;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .item-index {
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #f0f2f5;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ transition: all 0.2s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .item-index.active-index {
|
|
|
|
|
+ background: #409EFF;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .finish-badge {
|
|
|
|
|
+ color: #67C23A;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .item-info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .item-name {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ margin-bottom: 2px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .is-finished .item-name {
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ text-decoration: line-through;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .is-active .item-name {
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .item-meta {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #999;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .item-progress {
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .is-finished .item-progress {
|
|
|
|
|
+ color: #67C23A;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .item-action {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-empty {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 60px 20px;
|
|
|
|
|
+ color: #ccc;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-empty i {
|
|
|
|
|
+ font-size: 48px;
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .playlist-empty p {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .dialog-footer {
|
|
|
|
|
+ padding: 0 16px;
|
|
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
|
|
+ background: #fafafa;
|
|
|
|
|
+}
|
|
|
|
|
+.dialog-footer.pt30 {
|
|
|
|
|
+ padding: 8px 16px;
|
|
|
|
|
+}
|
|
|
|
|
+.media-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+.media-time {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ font-variant-numeric: tabular-nums;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.media-center {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+.bicon {
|
|
|
|
|
+ font-size: 22px !important;
|
|
|
|
|
+ padding: 4px !important;
|
|
|
|
|
+}
|
|
|
|
|
+.volume-control {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 6px;
|
|
|
|
|
+}
|
|
|
|
|
+.vol-icon {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ color: #606266;
|
|
|
|
|
+ user-select: none;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+.vol-icon:hover {
|
|
|
|
|
+ color: #409EFF;
|
|
|
|
|
+}
|
|
|
|
|
+.volume-slider {
|
|
|
|
|
+ width: 300px !important;
|
|
|
|
|
+}
|
|
|
|
|
+.volume-slider .el-slider__runway {
|
|
|
|
|
+ height: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.volume-slider .el-slider__bar {
|
|
|
|
|
+ height: 4px;
|
|
|
|
|
+}
|
|
|
|
|
+.volume-slider .el-slider__button {
|
|
|
|
|
+ width: 12px;
|
|
|
|
|
+ height: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+.media-select {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+}
|
|
|
|
|
+.media-el-select {
|
|
|
|
|
+ width: 80px;
|
|
|
|
|
+}
|
|
|
|
|
+.media-el-select .el-input__inner {
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ line-height: 28px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 900px) {
|
|
|
|
|
+ .pc-player-wrapper {
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .pc-player-right {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-height: 400px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .pc-player-container {
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+</style>
|