| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849 |
- <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|percent}}%</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;
- if(this.media.isFinish ==1) return 100;
- return +((this.curTimes / this.media.duration) * 100).toFixed(1);
- }
- },
- filters: {
- percent(item){
- if( item.isFinish == 1) return 100;
- return (item.position * 100 / item.duration).toFixed(1)
- },
- 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)
- 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>
|