|
|
@@ -1,51 +1,48 @@
|
|
|
<template>
|
|
|
- <el-row v-if="video !== null" class="movie-list">
|
|
|
- <el-col :md="12">
|
|
|
- <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
|
|
|
- <el-card class="box-card">
|
|
|
- <div slot="header" class="clearfix">
|
|
|
- <el-row>
|
|
|
- <el-button style="float: right; padding: 3px 0" type="text" @click="nextVideo">下一个</el-button>
|
|
|
- </el-row>
|
|
|
- <el-row>
|
|
|
- <h3 v-html="video.title" />
|
|
|
- </el-row>
|
|
|
- <el-row style="color: #999;font-size: 16px;padding-top: 0px;">
|
|
|
- <span><i class="el-icon-video-play">{{ video.view }}</i></span>
|
|
|
- <span v-html="' '" />
|
|
|
- <span><i class="el-icon-s-comment">{{ video.comment }}</i></span>
|
|
|
- <span v-html="' '" />
|
|
|
- <span><i class="el-icon-watch">{{ video.pubDate }}</i></span>
|
|
|
- </el-row>
|
|
|
- </div>
|
|
|
- <div class="text item">
|
|
|
- <div id="dplayer" ref="dplayer" style="height: 480px;" />
|
|
|
- </div>
|
|
|
- </el-card>
|
|
|
- </el-row>
|
|
|
- <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
|
|
|
- <user-avatar-card v-if="user !== null" :user-avatar="user" />
|
|
|
- </el-row>
|
|
|
- </el-col>
|
|
|
- </el-row>
|
|
|
+ <div class="short-video-container" v-if="video !== null">
|
|
|
+ <div class="video-overlay-top">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="medium"
|
|
|
+ round
|
|
|
+ icon="el-icon-refresh-right"
|
|
|
+ class="next-btn"
|
|
|
+ @click="nextVideo"
|
|
|
+ >
|
|
|
+ 下一个
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="video-wrapper">
|
|
|
+ <div id="dplayer" ref="dplayer" class="player-instance" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="video-overlay-info">
|
|
|
+ <h3 class="video-title" v-html="video.title" />
|
|
|
+ <div class="video-meta">
|
|
|
+ <span class="meta-item"><i class="el-icon-video-play"></i> {{ video.view }}</span>
|
|
|
+ <span class="meta-item"><i class="el-icon-s-comment"></i> {{ video.comment }}</span>
|
|
|
+ <span class="meta-item"><i class="el-icon-time"></i> {{ video.pubDate }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="user" class="author-info">
|
|
|
+ <el-avatar :size="30" :src="user.avatar" icon="el-icon-user"></el-avatar>
|
|
|
+ <span class="username">{{ user.screenName }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
-import UserAvatarCard from '@/components/card/UserAvatarCard'
|
|
|
-
|
|
|
import DPlayer from 'dplayer'
|
|
|
-import { videoUrl } from '@/api/video'
|
|
|
-import { getShortVideo } from '@/api/video'
|
|
|
+import { videoUrl, getShortVideo } from '@/api/video'
|
|
|
import { getUserInfo } from '@/api/user'
|
|
|
import { getAccessToken } from '@/utils/auth'
|
|
|
|
|
|
export default {
|
|
|
name: 'ShortVideo',
|
|
|
- components: { UserAvatarCard },
|
|
|
data() {
|
|
|
return {
|
|
|
- // 屏幕宽度, 为了控制分页条的大小
|
|
|
- screenWidth: document.body.clientWidth,
|
|
|
+ dp: null, // 存放播放器实例
|
|
|
video: null,
|
|
|
user: null,
|
|
|
userToken: null,
|
|
|
@@ -58,10 +55,9 @@ export default {
|
|
|
this.userToken = getAccessToken()
|
|
|
this.getShortVideoWrapper()
|
|
|
},
|
|
|
- mounted() {
|
|
|
- const header = this.$refs.header
|
|
|
- if (header !== undefined && header !== null) {
|
|
|
- this.wrapStyle = `height: calc(100vh - ${header.clientHeight + 20}px)`
|
|
|
+ beforeDestroy() {
|
|
|
+ if (this.dp) {
|
|
|
+ this.dp.destroy()
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
@@ -71,135 +67,63 @@ export default {
|
|
|
this.video = resp.data
|
|
|
document.title = resp.data.title
|
|
|
this.getVideoUrl(this.video.videoId)
|
|
|
- this.userId = resp.data.userId
|
|
|
- getUserInfo(this.userId).then(resp => {
|
|
|
- if (resp.code === 0) {
|
|
|
- this.user = resp.data
|
|
|
- } else {
|
|
|
- this.$notify.error({
|
|
|
- message: '用户数据获取失败',
|
|
|
- type: 'warning',
|
|
|
- duration: 3000
|
|
|
- })
|
|
|
- }
|
|
|
- })
|
|
|
+ this.fetchUserInfo(resp.data.userId)
|
|
|
+ }
|
|
|
+ }).catch(err => {
|
|
|
+ this.$message.error(err.message)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ fetchUserInfo(userId) {
|
|
|
+ getUserInfo(userId).then(resp => {
|
|
|
+ if (resp.code === 0) {
|
|
|
+ this.user = resp.data
|
|
|
}
|
|
|
- }).catch(error => {
|
|
|
- this.$notify.error({
|
|
|
- message: error.message,
|
|
|
- type: 'warning',
|
|
|
- duration: 3000
|
|
|
- })
|
|
|
})
|
|
|
},
|
|
|
async getVideoUrl(videoId) {
|
|
|
videoUrl(videoId).then(res => {
|
|
|
- if (res.code === 0) {
|
|
|
- var sendEvent = false
|
|
|
- const urlType = res.data.type
|
|
|
- if (urlType === 'mp4') {
|
|
|
- const urls = res.data.urls
|
|
|
- for (const url of urls) {
|
|
|
- url.type = 'normal'
|
|
|
- }
|
|
|
- this.initMp4Player(this.userId, videoId, this.video.coverUrl, urls, res.data.currentTime, sendEvent)
|
|
|
- } else {
|
|
|
- this.$notify.error({
|
|
|
- message: '视频 url 类型不合法',
|
|
|
- type: 'warning',
|
|
|
- duration: 3000
|
|
|
- })
|
|
|
- }
|
|
|
- } else {
|
|
|
- this.$notify.error({
|
|
|
- message: '视频 url 获取失败',
|
|
|
- type: 'warning',
|
|
|
- duration: 3000
|
|
|
- })
|
|
|
+ if (res.code === 0 && res.data.type === 'mp4') {
|
|
|
+ const urls = res.data.urls.map(u => ({ ...u, type: 'normal' }))
|
|
|
+ this.initMp4Player(videoId, this.video.coverUrl, urls, res.data.currentTime)
|
|
|
}
|
|
|
- }).catch(error => {
|
|
|
- this.$notify.error({
|
|
|
- message: error.message,
|
|
|
- type: 'error',
|
|
|
- duration: 3000
|
|
|
- })
|
|
|
})
|
|
|
},
|
|
|
- initMp4Player(userId, videoId, coverUrl, urls, pos, sendEvent) {
|
|
|
- const player = new DPlayer({
|
|
|
+ initMp4Player(videoId, coverUrl, urls, pos) {
|
|
|
+ // 如果已经存在实例,先销毁切换
|
|
|
+ if (this.dp) {
|
|
|
+ this.dp.switchVideo({
|
|
|
+ url: urls[0].url,
|
|
|
+ pic: coverUrl,
|
|
|
+ thumbnails: coverUrl
|
|
|
+ })
|
|
|
+ this.dp.seek(pos)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.dp = new DPlayer({
|
|
|
container: document.querySelector('#dplayer'),
|
|
|
lang: 'zh-cn',
|
|
|
screenshot: true,
|
|
|
- autoplay: false,
|
|
|
- volume: 0.1,
|
|
|
- mutex: true,
|
|
|
+ autoplay: true, // 沉浸式体验建议自动播放
|
|
|
+ volume: 0.2,
|
|
|
+ loop: true,
|
|
|
video: {
|
|
|
pic: coverUrl,
|
|
|
defaultQuality: 0,
|
|
|
- quality: urls,
|
|
|
- hotkey: true
|
|
|
+ quality: urls
|
|
|
},
|
|
|
danmaku: {
|
|
|
id: videoId,
|
|
|
- maximum: 10000,
|
|
|
api: this.danmaku.api,
|
|
|
token: this.userToken,
|
|
|
- bottom: '15%',
|
|
|
- unlimited: true
|
|
|
+ maximum: 1000,
|
|
|
+ bottom: '15%'
|
|
|
}
|
|
|
})
|
|
|
|
|
|
- // 设置音量
|
|
|
- // player.volume(0.1, true, false)
|
|
|
- // 跳转到上次看到的位置
|
|
|
- player.seek(pos)
|
|
|
-
|
|
|
- var ended = false
|
|
|
- /* 事件绑定 */
|
|
|
- const that = this
|
|
|
- player.on('play', function() {
|
|
|
- if (sendEvent) {
|
|
|
- clearInterval(that.intervalEvent)
|
|
|
- that.intervalEvent = setInterval(() => {
|
|
|
- if (!ended) {
|
|
|
- const payload = {}
|
|
|
- payload.mediaId = videoId
|
|
|
- payload.mediaType = 1
|
|
|
- payload.currentTime = player.video.currentTime
|
|
|
- payload.ended = ended
|
|
|
-
|
|
|
- const jsonData = {}
|
|
|
- jsonData.event = 'media_progress'
|
|
|
- jsonData.data = JSON.stringify(payload)
|
|
|
-
|
|
|
- that.wsClient.send(jsonData)
|
|
|
- }
|
|
|
- }, 5000)
|
|
|
- }
|
|
|
- })
|
|
|
- player.on('ended', () => {
|
|
|
- clearInterval(that.intervalEvent)
|
|
|
- ended = true
|
|
|
- if (sendEvent) {
|
|
|
- const payload = {}
|
|
|
- payload.mediaId = videoId
|
|
|
- payload.mediaType = 1
|
|
|
- payload.currentTime = player.video.currentTime
|
|
|
- payload.ended = ended
|
|
|
-
|
|
|
- const jsonData = {}
|
|
|
- jsonData.event = 'media_progress'
|
|
|
- jsonData.data = JSON.stringify(payload)
|
|
|
-
|
|
|
- that.wsClient.send(jsonData)
|
|
|
- }
|
|
|
- })
|
|
|
-
|
|
|
- player.on('volumechange', () => {
|
|
|
- console.log('声音改变')
|
|
|
- })
|
|
|
+ this.dp.seek(pos)
|
|
|
},
|
|
|
- async nextVideo() {
|
|
|
+ nextVideo() {
|
|
|
this.getShortVideoWrapper()
|
|
|
}
|
|
|
}
|
|
|
@@ -207,28 +131,115 @@ export default {
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-/*处于手机屏幕时*/
|
|
|
+/* 容器全屏化 */
|
|
|
+.short-video-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: calc(100vh - 60px); /* 减去顶部导航栏高度,如果没有导航栏请设为 100vh */
|
|
|
+ background-color: #000;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 播放器容器 */
|
|
|
+.video-wrapper {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.player-instance {
|
|
|
+ width: 100% !important;
|
|
|
+ height: 100% !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 顶部操作遮罩 */
|
|
|
+.video-overlay-top {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ right: 20px;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.next-btn {
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.4);
|
|
|
+ color: #fff;
|
|
|
+ backdrop-filter: blur(5px);
|
|
|
+}
|
|
|
+
|
|
|
+.next-btn:hover {
|
|
|
+ background: rgba(255, 255, 255, 0.4);
|
|
|
+}
|
|
|
+
|
|
|
+/* 底部信息遮罩 */
|
|
|
+.video-overlay-info {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 40px;
|
|
|
+ left: 20px;
|
|
|
+ right: 80px; /* 避开播放器右侧可能的控制按钮 */
|
|
|
+ z-index: 10;
|
|
|
+ color: #fff;
|
|
|
+ pointer-events: none; /* 防止点击遮罩挡住视频操作 */
|
|
|
+}
|
|
|
+
|
|
|
+.video-title {
|
|
|
+ font-size: 1.2rem;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ text-shadow: 0 1px 2px rgba(0,0,0,0.8);
|
|
|
+ pointer-events: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.video-meta {
|
|
|
+ font-size: 14px;
|
|
|
+ opacity: 0.9;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.meta-item {
|
|
|
+ margin-right: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.author-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ pointer-events: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.username {
|
|
|
+ margin-left: 10px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+/* 移动端适配 */
|
|
|
@media screen and (max-width: 768px) {
|
|
|
- .movie-list {
|
|
|
- padding-top: 8px;
|
|
|
- padding-left: 0.5%;
|
|
|
- padding-right: 0.5%;
|
|
|
+ .short-video-container {
|
|
|
+ height: calc(100vh - 50px); /* 移动端高度调整 */
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-overlay-info {
|
|
|
+ bottom: 60px; /* 避开底部控制条 */
|
|
|
+ left: 15px;
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-.movie-list {
|
|
|
- padding-top: 15px;
|
|
|
- padding-left: 5%;
|
|
|
- padding-right: 5%;
|
|
|
+ .video-title {
|
|
|
+ font-size: 1rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .video-overlay-top {
|
|
|
+ top: 15px;
|
|
|
+ right: 15px;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.clearfix:before,
|
|
|
-.clearfix:after {
|
|
|
- display: table;
|
|
|
- content: "";
|
|
|
+/* 强制修改 DPlayer 样式,适配全屏 */
|
|
|
+::v-deep .dplayer-video-wrap {
|
|
|
+ background-color: #000;
|
|
|
}
|
|
|
|
|
|
-.clearfix:after {
|
|
|
- clear: both;
|
|
|
+::v-deep .dplayer-video {
|
|
|
+ object-fit: contain; /* 确保长宽比不一致时,视频完整显示不拉伸 */
|
|
|
}
|
|
|
</style>
|