PlaylistView.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. <template>
  2. <div class="playlist-view-container">
  3. <el-row v-if="!permissionDenied" :gutter="10" class="main-content">
  4. <el-col v-if="video !== null" :md="16" :sm="24" :xs="24" class="video-left-section">
  5. <el-card class="video-card" shadow="never">
  6. <div slot="header" class="video-header">
  7. <div class="title-row">
  8. <h1 class="video-title" v-html="video.title" />
  9. <router-link target="_blank" :to="`/video/${video.videoId}`">
  10. <el-link type="primary" :underline="false" class="origin-link">原视频 <i class="el-icon-right" /></el-link>
  11. </router-link>
  12. </div>
  13. <div class="video-stats">
  14. <span><i class="el-icon-video-play" /> {{ video.view }}</span>
  15. <span><i class="el-icon-s-comment" /> {{ video.comment }}</span>
  16. <span><i class="el-icon-time" /> {{ video.pubDate }}</span>
  17. </div>
  18. </div>
  19. <div class="player-wrapper">
  20. <div id="dplayer" ref="dplayer" class="dplayer-instance" />
  21. </div>
  22. <div class="video-actions">
  23. <el-button
  24. type="danger"
  25. size="small"
  26. round
  27. icon="el-icon-collection"
  28. :disabled="isCollected"
  29. @click="collection(video.videoId)"
  30. >
  31. 收藏 {{ video.favorite }}
  32. </el-button>
  33. <el-button
  34. type="info"
  35. size="small"
  36. round
  37. plain
  38. icon="el-icon-warning-outline"
  39. @click="deleteVideo(video)"
  40. >
  41. 报错/删除
  42. </el-button>
  43. </div>
  44. </el-card>
  45. <el-card class="description-card" shadow="never">
  46. <div class="description-content">
  47. <p class="desc-text" v-html="video.description || '暂无简介'" />
  48. </div>
  49. <el-divider />
  50. <div class="tag-group">
  51. <el-tag
  52. v-for="(tag, index) in video.tags"
  53. :key="index"
  54. class="video-tag"
  55. size="small"
  56. effect="plain"
  57. >
  58. <router-link :to="`/video/tag/${tag}`">{{ tag }}</router-link>
  59. </el-tag>
  60. </div>
  61. </el-card>
  62. </el-col>
  63. <el-col :md="8" :sm="24" :xs="24" class="playlist-right-section">
  64. <el-card class="playlist-card" shadow="never">
  65. <div slot="header" class="playlist-header">
  66. <div class="flex-between">
  67. <h3 class="m0">播放列表</h3>
  68. <div class="auto-play-switch">
  69. <span class="label-text">自动播放</span>
  70. <el-switch v-model="autoPlay" active-color="#13ce66" />
  71. </div>
  72. </div>
  73. </div>
  74. <div class="playlist-body">
  75. <el-table
  76. :data="playList.list"
  77. :show-header="false"
  78. highlight-current-row
  79. :row-class-name="tableRowClassName"
  80. class="playlist-table"
  81. @row-click="playItem"
  82. >
  83. <el-table-column width="40" type="index" align="center" />
  84. <el-table-column width="100">
  85. <template slot-scope="scope">
  86. <div class="playlist-cover-container">
  87. <el-image
  88. lazy
  89. fit="cover"
  90. class="playlist-cover-img"
  91. :src="scope.row.coverUrl"
  92. />
  93. <span class="duration-badge">{{ scope.row.duration }}</span>
  94. </div>
  95. </template>
  96. </el-table-column>
  97. <el-table-column>
  98. <template slot-scope="scope">
  99. <span class="playlist-item-title">{{ scope.row.title }}</span>
  100. </template>
  101. </el-table-column>
  102. </el-table>
  103. </div>
  104. </el-card>
  105. </el-col>
  106. </el-row>
  107. <el-row v-else>
  108. <permission-denied-card :text-object="textObject" />
  109. </el-row>
  110. </div>
  111. </template>
  112. <script>
  113. import PermissionDeniedCard from '@/components/card/PermissionDeniedCard'
  114. import DPlayer from 'dplayer'
  115. import { videoUrl, videoInfo } from '@/api/video'
  116. import { getPlaylistItems } from '@/api/collect'
  117. export default {
  118. name: 'PlaylistView',
  119. components: { PermissionDeniedCard },
  120. data() {
  121. return {
  122. video: null,
  123. user: null,
  124. isCollected: false,
  125. permissionDenied: false,
  126. textObject: { content: '视频', route: '/video' },
  127. autoPlay: false,
  128. playList: { current: 0, list: [] },
  129. danmaku: {
  130. api: process.env.VUE_APP_SERVER_URL + '/api/comment/danmaku/',
  131. token: 'tnbapp'
  132. },
  133. player: null
  134. }
  135. },
  136. watch: {
  137. '$route.params.albumId': function() {
  138. this.initPage()
  139. }
  140. },
  141. created() {
  142. this.initPage()
  143. },
  144. methods: {
  145. initPage() {
  146. const albumId = this.$route.params.albumId
  147. getPlaylistItems(albumId).then(resp => {
  148. if (resp.code === 0) {
  149. this.playList.list = resp.data.pageList.list
  150. document.title = resp.data.albumInfo.albumName
  151. if (this.playList.list.length > 0) {
  152. this.getVideoInfo(this.playList.list[0].videoId)
  153. }
  154. }
  155. })
  156. },
  157. getVideoInfo(videoId) {
  158. videoInfo(videoId).then(resp => {
  159. if (resp.code === 0) {
  160. this.video = resp.data
  161. this.getVideoUrl(videoId)
  162. } else {
  163. this.permissionDenied = true
  164. }
  165. })
  166. },
  167. getVideoUrl(videoId) {
  168. videoUrl(videoId).then(res => {
  169. if (res.code === 0 && res.data.type === 'mp4') {
  170. this.initPlayer(res.data.urls, res.data.currentTime)
  171. }
  172. })
  173. },
  174. initPlayer(urls, pos) {
  175. if (this.player) this.player.destroy()
  176. this.player = new DPlayer({
  177. container: this.$refs.dplayer,
  178. autoplay: this.autoPlay,
  179. theme: '#409EFF',
  180. volume: 0.7,
  181. video: {
  182. pic: this.video.coverUrl,
  183. quality: urls.map(u => ({ name: u.label || '标清', url: u.url, type: 'normal' })),
  184. defaultQuality: 0
  185. },
  186. danmaku: {
  187. id: this.video.videoId,
  188. api: this.danmaku.api,
  189. token: this.danmaku.token,
  190. user: this.video.userId
  191. }
  192. })
  193. this.player.seek(pos)
  194. this.player.on('ended', () => {
  195. if (this.autoPlay) this.playNext()
  196. })
  197. },
  198. playItem(row) {
  199. this.getVideoInfo(row.videoId)
  200. // 移动端点击后滚动到顶部播放器
  201. if (window.innerWidth < 768) {
  202. window.scrollTo({ top: 0, behavior: 'smooth' })
  203. }
  204. },
  205. playNext() {
  206. const currentIndex = this.playList.list.findIndex(i => i.videoId === this.video.videoId)
  207. if (currentIndex < this.playList.list.length - 1) {
  208. this.playItem(this.playList.list[currentIndex + 1])
  209. }
  210. },
  211. tableRowClassName({ row }) {
  212. if (this.video && row.videoId === this.video.videoId) {
  213. return 'current-playing-row'
  214. }
  215. return ''
  216. }
  217. }
  218. }
  219. </script>
  220. <style scoped>
  221. .playlist-view-container {
  222. max-width: 1400px;
  223. margin: 0 auto;
  224. padding: 15px;
  225. background-color: #f4f4f5;
  226. min-height: 100vh;
  227. }
  228. .m0 { margin: 0; }
  229. .flex-between { display: flex; justify-content: space-between; align-items: center; }
  230. /* 视频标题与信息 */
  231. .video-header { padding: 10px 0; }
  232. .title-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
  233. .video-title { font-size: 1.2rem; line-height: 1.4; margin: 0 0 10px 0; color: #1f1f1f; flex: 1; }
  234. .origin-link { font-size: 14px; white-space: nowrap; }
  235. .video-stats { color: #909399; font-size: 13px; display: flex; gap: 15px; flex-wrap: wrap; }
  236. /* 播放器适配 */
  237. .player-wrapper {
  238. background: #000;
  239. border-radius: 4px;
  240. overflow: hidden;
  241. position: relative;
  242. width: 100%;
  243. }
  244. /* PC端高度,移动端通过媒体查询调整 */
  245. .dplayer-instance { height: 500px; width: 100%; }
  246. .video-actions { padding: 15px 0 5px; display: flex; gap: 10px; }
  247. /* 简介与标签 */
  248. .description-card { margin-top: 10px; border: none; }
  249. .desc-text { font-size: 14px; color: #606266; line-height: 1.6; white-space: pre-wrap; margin: 0; }
  250. .tag-group { display: flex; flex-wrap: wrap; gap: 8px; }
  251. .video-tag a { text-decoration: none; color: inherit; }
  252. /* 播放列表 */
  253. .playlist-header { padding: 5px 0; }
  254. .auto-play-switch { display: flex; align-items: center; gap: 8px; }
  255. .label-text { font-size: 13px; color: #606266; }
  256. .playlist-body { max-height: 600px; overflow-y: auto; }
  257. .playlist-table { cursor: pointer; }
  258. .playlist-cover-container { position: relative; width: 90px; height: 56px; border-radius: 4px; overflow: hidden; }
  259. .playlist-cover-img { width: 100%; height: 100%; }
  260. .duration-badge {
  261. position: absolute; bottom: 2px; right: 2px;
  262. background: rgba(0,0,0,0.7); color: #fff;
  263. font-size: 10px; padding: 0 4px; border-radius: 2px;
  264. }
  265. .playlist-item-title {
  266. font-size: 13px; color: #303133;
  267. display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
  268. }
  269. /* 移动端深度适配 */
  270. @media screen and (max-width: 768px) {
  271. .playlist-view-container { padding: 0; background-color: #fff; }
  272. .video-left-section, .playlist-right-section { padding: 0 !important; }
  273. /* 移动端播放器强制 16:9 */
  274. .dplayer-instance { height: 56.25vw !important; }
  275. .video-card { border: none; border-radius: 0; }
  276. .video-card ::v-deep .el-card__header { padding: 10px 15px; }
  277. .video-card ::v-deep .el-card__body { padding: 0; }
  278. .video-title { font-size: 1rem; }
  279. .video-actions { padding: 10px 15px; }
  280. .description-card { border-radius: 0; }
  281. .playlist-card { border: none; border-top: 8px solid #f4f4f5; border-radius: 0; }
  282. .playlist-body { max-height: none; }
  283. }
  284. /* 高亮当前播放行 */
  285. ::v-deep .current-playing-row {
  286. background-color: #ecf5ff !important;
  287. }
  288. ::v-deep .current-playing-row .playlist-item-title {
  289. color: #409EFF;
  290. font-weight: bold;
  291. }
  292. </style>