VideoList.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. <template>
  2. <el-row v-if="!permissionDenied" class="movie-list">
  3. <el-col v-if="video !== null" :md="15">
  4. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  5. <el-card class="box-card">
  6. <div slot="header" class="clearfix">
  7. <el-row>
  8. <h3 v-html="video.title" />
  9. </el-row>
  10. <el-row style="color: #999;font-size: 16px;padding-top: 0px;">
  11. <span><i class="el-icon-video-play">{{ video.view }}</i></span>
  12. <span v-html="'&nbsp;&nbsp;&nbsp;&nbsp;'" />
  13. <span><i class="el-icon-s-comment">{{ video.comment }}</i></span>
  14. <span v-html="'&nbsp;&nbsp;&nbsp;&nbsp;'" />
  15. <span><i class="el-icon-watch">{{ video.pubDate }}</i></span>
  16. </el-row>
  17. </div>
  18. <div class="text item">
  19. <div id="dplayer" ref="dplayer" style="height: 480px;" />
  20. </div>
  21. </el-card>
  22. </el-row>
  23. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  24. <el-card class="box-card">
  25. <div slot="header" class="clearfix">
  26. <div class="video-data-row">
  27. <el-button
  28. type="danger"
  29. size="mini"
  30. icon="el-icon-collection"
  31. :disabled="isCollected"
  32. @click="collection(video.videoId)"
  33. >
  34. <span>收藏 {{ video.favorite }}</span>
  35. </el-button>
  36. <el-button
  37. type="danger"
  38. size="mini"
  39. icon="el-icon-thumb"
  40. :disabled="isCollected"
  41. @click="collection(video.videoId)"
  42. >
  43. <span>喜欢 {{ video.thumbUp }}</span>
  44. </el-button>
  45. <el-button
  46. type="danger"
  47. size="mini"
  48. icon="el-icon-share"
  49. :disabled="isCollected"
  50. @click="collection(video.videoId)"
  51. >
  52. <span>分享 {{ video.share }}</span>
  53. </el-button>
  54. <el-button
  55. type="danger"
  56. size="mini"
  57. icon="el-icon-download"
  58. @click="getDownloadUrl(video.videoId)"
  59. >
  60. <span>下载</span>
  61. </el-button>
  62. <el-button
  63. v-if="video.cache != null"
  64. type="danger"
  65. size="mini"
  66. icon="el-icon-download"
  67. @click="cacheBiliVideo(video.videoId)"
  68. >
  69. <span>{{ video.cache.msg }}</span>
  70. </el-button>
  71. <el-button
  72. type="danger"
  73. size="mini"
  74. icon="el-icon-help"
  75. @click="displayErrorReportDialog"
  76. >
  77. <span>报错</span>
  78. </el-button>
  79. </div>
  80. </div>
  81. <div class="text item">
  82. <!--视频描述行-->
  83. <span class="description" v-html="video.description" />
  84. <el-divider />
  85. <!--视频标签行-->
  86. <div class="v-tag">
  87. <el-tag
  88. v-for="(tag,index) in video.tags"
  89. :key="index"
  90. class="tag"
  91. size="medium"
  92. effect="plain"
  93. >
  94. <router-link target="_blank" :to="`/video/tag/` + tag">
  95. {{ tag }}
  96. </router-link>
  97. </el-tag>
  98. </div>
  99. </div>
  100. </el-card>
  101. </el-row>
  102. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  103. <el-card class="box-card">
  104. <div slot="header" class="clearfix">
  105. <el-row>
  106. <h3>视频评论</h3>
  107. </el-row>
  108. </div>
  109. <div class="text item">
  110. <div ref="comment" :style="wrapStyle" class="comment-wrap">
  111. <comment
  112. v-model="videoComments"
  113. :user="currentUser"
  114. :props="props"
  115. :before-submit="submit"
  116. :before-like="like"
  117. :before-delete="deleteComment"
  118. :upload-img="uploadImg"
  119. />
  120. </div>
  121. </div>
  122. </el-card>
  123. </el-row>
  124. </el-col>
  125. <el-col :md="9">
  126. <el-row>
  127. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  128. <user-avatar-card v-if="user !== null" :user-avatar="user" />
  129. </el-row>
  130. <el-row v-if="showPlaylist" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  131. <el-card class="box-card">
  132. <div slot="header" class="clearfix">
  133. <el-row>
  134. <h3>播放列表</h3>
  135. </el-row>
  136. <el-row>
  137. <span>自动播放 <el-switch v-model="autoPlay" /></span>
  138. </el-row>
  139. </div>
  140. <div class="text item">
  141. <el-table
  142. :data="playList.list"
  143. :show-header="false"
  144. height="480"
  145. style="width: 100%"
  146. >
  147. <el-table-column
  148. prop="title"
  149. >
  150. <template slot-scope="scope">
  151. <router-link :to="`/vidlist/${scope.row.videoId}`">
  152. <span>{{ scope.row.title | ellipsis }}</span>
  153. </router-link>
  154. </template>
  155. </el-table-column>
  156. <el-table-column
  157. prop="duration"
  158. >
  159. <template slot-scope="scope">
  160. <span>{{ scope.row.duration }}</span>
  161. </template>
  162. </el-table-column>
  163. </el-table>
  164. </div>
  165. </el-card>
  166. </el-row>
  167. </el-row>
  168. </el-col>
  169. <!-- 视频访问码对话框 -->
  170. <el-dialog
  171. append-to-body
  172. :visible.sync="showAccessCodeDialog"
  173. width="30%"
  174. center
  175. >
  176. <el-card class="box-card">
  177. <div slot="header" class="clearfix">
  178. <span>输入视频访问码</span>
  179. <el-button style="float: right; padding: 3px 0" type="text" @click="submitAccessCodeWrapper">提交</el-button>
  180. </div>
  181. <div class="text item">
  182. <el-form ref="form" :model="accessCodeForm" label-width="80px">
  183. <el-form-item label="访问码">
  184. <el-input v-model="accessCodeForm.accessCode" style="width: 70%; padding-right: 2px" />
  185. </el-form-item>
  186. </el-form>
  187. </div>
  188. </el-card>
  189. </el-dialog>
  190. <!-- 视频报错对话框 -->
  191. <el-dialog
  192. append-to-body
  193. :visible.sync="showErrorReportDialog"
  194. width="30%"
  195. center
  196. >
  197. <el-card class="box-card">
  198. <div slot="header" class="clearfix">
  199. <span>视频报错</span>
  200. <el-button style="float: right; padding: 3px 0" type="text" @click="submitErrorReport">提交错误</el-button>
  201. </div>
  202. <div class="text item">
  203. <el-form ref="form" :model="errorReportForm" label-width="80px">
  204. <el-form-item label="错误类型">
  205. <el-select v-model="errorReportForm.errorCode" placeholder="选择视频错误类型">
  206. <el-option label="视频无封面" value="1" />
  207. <el-option label="视频无声音" value="2" />
  208. <el-option label="视频无画面" value="3" />
  209. <el-option label="视频无资源" value="4" />
  210. <el-option label="视频待删除" value="5" />
  211. </el-select>
  212. </el-form-item>
  213. </el-form>
  214. </div>
  215. </el-card>
  216. </el-dialog>
  217. </el-row>
  218. <el-row v-else>
  219. <permission-denied-card :text-object="textObject" />
  220. </el-row>
  221. </template>
  222. <script>
  223. import PermissionDeniedCard from '@/components/card/PermissionDeniedCard'
  224. import UserAvatarCard from '@/components/card/UserAvatarCard'
  225. import comment from '@/components/comment'
  226. import SocketInstance from '@/utils/ws/socket-instance'
  227. import flvjs from 'flv.js'
  228. import DPlayer from 'dplayer'
  229. import { videoUrl, similarVideo, videoInfo, videoErrorReport, downloadVideo, cacheBiliVideo } from '@/api/video'
  230. import { collectItem } from '@/api/collect'
  231. import { getUserInfo } from '@/api/user'
  232. import { submitAccessCode } from '@/api/content'
  233. import { getAccessToken } from '@/utils/auth'
  234. export default {
  235. name: 'VideoList',
  236. components: { UserAvatarCard, PermissionDeniedCard, comment },
  237. filters: {
  238. ellipsis(value) {
  239. if (!value) return ''
  240. const max = 20
  241. if (value.length > max) {
  242. return value.slice(0, max) + '...'
  243. }
  244. return value
  245. }
  246. },
  247. data() {
  248. return {
  249. /** ********************************************************************/
  250. wrapStyle: '',
  251. videoComments: [],
  252. currentUser: {
  253. name: '草莓',
  254. avatar: '//picx.zhimg.com/v2-a2c89378a6332cbfed3e28b5ab84feb7.jpg'
  255. },
  256. props: {
  257. id: 114511,
  258. content: 'this is comment content',
  259. imgSrc: '',
  260. children: [],
  261. likes: 0,
  262. liked: false,
  263. reply: null,
  264. createAt: 'createAt',
  265. user: {
  266. name: '芒果',
  267. avatar: ''
  268. }
  269. },
  270. // **********************************************************************/
  271. video: null,
  272. user: null,
  273. isCollected: false,
  274. showAccessCodeDialog: false,
  275. accessCodeForm: {
  276. contentId: null,
  277. contentType: 1002,
  278. accessCode: null
  279. },
  280. showErrorReportDialog: false,
  281. errorReportForm: {
  282. videoId: null,
  283. errorCode: null
  284. },
  285. permissionDenied: false,
  286. textObject: {
  287. content: '视频',
  288. route: '/video'
  289. },
  290. showPlaylist: true,
  291. autoPlay: false,
  292. playList: {
  293. current: 0,
  294. list: [
  295. 'eyNXaDnmN3',
  296. 'WkYNYzDePp',
  297. 'a8Vx9EGDbA',
  298. 'a8V3D88NJK',
  299. '4m7qMXapp1'
  300. ]
  301. },
  302. // **********************************************************************/
  303. flvjs,
  304. DPlayer,
  305. danmaku: {
  306. api: process.env.VUE_APP_SERVER_URL + '/api/comment/danmaku/',
  307. token: 'tnbapp'
  308. },
  309. getUrl: true
  310. }
  311. },
  312. watch: {
  313. // 地址栏 url 发生变化时重新加载本页面
  314. $route() {
  315. this.$router.go()
  316. }
  317. },
  318. created() {
  319. const videoId = this.$route.params.id
  320. this.accessCodeForm.contentId = videoId
  321. this.getVideoInfo(videoId)
  322. const key = 'myplaylist-' + videoId
  323. const value = localStorage.getItem(key)
  324. if (value != null) {
  325. this.playList = JSON.parse(value)
  326. this.calculateCurrent(videoId)
  327. } else {
  328. this.getSimilarVideos(videoId)
  329. }
  330. },
  331. mounted() {
  332. window.addEventListener('beforeunload', this.handleBeforeUnload)
  333. const header = this.$refs.header
  334. if (header !== undefined && header !== null) {
  335. console.log('header -> ' + header)
  336. this.wrapStyle = `height: calc(100vh - ${header.clientHeight + 20}px)`
  337. }
  338. },
  339. methods: {
  340. handleBeforeUnload(event) {
  341. const key = 'myplaylist-' + this.video.videoId
  342. localStorage.removeItem(key)
  343. // event.preventDefault()
  344. },
  345. // 获取视频的详细信息
  346. getVideoInfo(videoId) {
  347. videoInfo(videoId).then(resp => {
  348. if (resp.code === 0) {
  349. // this.showAccessCodeDialog = true
  350. this.video = resp.data
  351. document.title = resp.data.title
  352. this.getVideoUrl(videoId)
  353. this.userId = resp.data.userId
  354. getUserInfo(this.userId).then(resp => {
  355. if (resp.code === 0) {
  356. this.user = resp.data
  357. } else {
  358. this.$notify.error({
  359. message: '用户数据获取失败',
  360. type: 'warning',
  361. duration: 3000
  362. })
  363. }
  364. })
  365. } else if (resp.code === 2) {
  366. this.$router.push('/404')
  367. } else {
  368. this.permissionDenied = true
  369. }
  370. }).catch(error => {
  371. this.$notify.error({
  372. message: error.message,
  373. type: 'warning',
  374. duration: 3000
  375. })
  376. })
  377. },
  378. // 获取和当前视频类似的其他视频
  379. getSimilarVideos(videoId) {
  380. similarVideo(videoId).then(resp => {
  381. if (resp.code === 0) {
  382. this.playList.list = resp.data
  383. this.calculateCurrent(videoId)
  384. } else {
  385. this.$notify.error({
  386. message: '推荐视频数据获取失败',
  387. type: 'warning',
  388. duration: 3000
  389. })
  390. }
  391. }).catch(error => {
  392. this.$notify.error({
  393. message: error.message,
  394. type: 'warning',
  395. duration: 3000
  396. })
  397. })
  398. },
  399. // 换一换
  400. refreshSimilar() {
  401. console.log('刷新相关推荐')
  402. },
  403. // 用户点击收藏
  404. collection(videoId) {
  405. const jsonData = {}
  406. jsonData.contentType = 1002
  407. jsonData.contentId = videoId
  408. jsonData.collected = true
  409. collectItem(jsonData).then(resp => {
  410. if (resp.code !== 0) {
  411. this.$notify.success({
  412. title: '视频收藏失败',
  413. type: 'warning',
  414. duration: 3000
  415. })
  416. }
  417. })
  418. },
  419. getDownloadUrl(videoId) {
  420. // let filename
  421. downloadVideo(videoId).then(resp => {
  422. if (resp.code === 0) {
  423. const downloadUrl = resp.data.url
  424. window.open(downloadUrl, '_blank')
  425. } else {
  426. this.$notify({
  427. title: '提示',
  428. message: resp.msg,
  429. type: 'warning',
  430. duration: 3000
  431. })
  432. }
  433. }).catch(error => {
  434. this.$notify({
  435. title: '提示',
  436. message: error.message,
  437. type: 'error',
  438. duration: 3000
  439. })
  440. })
  441. },
  442. cacheBiliVideo(bvId) {
  443. cacheBiliVideo(bvId).then(resp => {
  444. if (resp.code === 0) {
  445. const resData = resp.data
  446. this.$notify({
  447. title: '提示',
  448. message: resData.msg,
  449. type: 'warning',
  450. duration: 3000
  451. })
  452. }
  453. })
  454. },
  455. submitAccessCodeWrapper() {
  456. submitAccessCode(this.accessCodeForm).then(resp => {
  457. if (resp.code === 0) {
  458. this.video = resp.data
  459. } else {
  460. this.$notify({
  461. message: resp.msg,
  462. type: 'warning',
  463. duration: 3000
  464. })
  465. }
  466. }).catch(error => {
  467. this.$notify({
  468. title: '提示',
  469. message: error.message,
  470. type: 'warning',
  471. duration: 3000
  472. })
  473. })
  474. },
  475. displayErrorReportDialog() {
  476. this.errorReportForm.videoId = this.video.videoId
  477. this.showErrorReportDialog = true
  478. },
  479. submitErrorReport() {
  480. this.showErrorReportDialog = false
  481. videoErrorReport(this.errorReportForm).then(resp => {
  482. if (resp.code === 0) {
  483. this.$notify({
  484. title: '提示',
  485. message: '视频错误已提交',
  486. type: 'warning',
  487. duration: 3000
  488. })
  489. }
  490. }).catch(error => {
  491. this.$notify({
  492. title: '提示',
  493. message: error.message,
  494. type: 'warning',
  495. duration: 3000
  496. })
  497. })
  498. },
  499. // ****************************************************************************************************************
  500. getVideoUrl(videoId) {
  501. videoUrl(videoId).then(res => {
  502. if (res.code === 0) {
  503. const token = getAccessToken()
  504. if (token != null) {
  505. SocketInstance.connect()
  506. }
  507. const urlType = res.data.type
  508. if (urlType === 'mp4') {
  509. const urls = res.data.urls
  510. for (const url of urls) {
  511. url.type = 'normal'
  512. }
  513. this.initMp4Player(this.video.userId, videoId, this.video.coverUrl, urls, res.data.currentTime)
  514. } else {
  515. this.$notify.error({
  516. message: '视频 url 类型不合法',
  517. type: 'warning',
  518. duration: 3000
  519. })
  520. }
  521. } else {
  522. this.$notify.error({
  523. message: '视频 url 获取失败',
  524. type: 'warning',
  525. duration: 3000
  526. })
  527. }
  528. }).catch(error => {
  529. this.$notify.error({
  530. message: error.message,
  531. type: 'error',
  532. duration: 3000
  533. })
  534. })
  535. },
  536. initMp4Player(userId, videoId, coverUrl, urls, pos) {
  537. const player = new DPlayer({
  538. container: document.querySelector('#dplayer'),
  539. lang: 'zh-cn',
  540. screenshot: false,
  541. autoplay: false,
  542. volume: 0.1,
  543. mutex: true,
  544. video: {
  545. pic: coverUrl,
  546. defaultQuality: 0,
  547. quality: urls,
  548. hotkey: true
  549. },
  550. danmaku: {
  551. id: videoId,
  552. maximum: 10000,
  553. api: this.danmaku.api,
  554. token: this.danmaku.token,
  555. user: userId,
  556. bottom: '15%',
  557. unlimited: true
  558. }
  559. })
  560. // 设置音量
  561. // player.volume(0.1, true, false)
  562. // 跳转到上次看到的位置
  563. player.seek(pos)
  564. /* 事件绑定 */
  565. player.on('progress', function() {
  566. const jsonData = {}
  567. jsonData.videoId = videoId
  568. jsonData.currentTime = player.video.currentTime
  569. SocketInstance.send(jsonData)
  570. })
  571. player.on('ended', () => {
  572. const jsonData = {}
  573. jsonData.videoId = videoId
  574. jsonData.currentTime = player.video.currentTime
  575. SocketInstance.send(jsonData)
  576. this.getNextPath(videoId)
  577. })
  578. player.on('volumechange', () => {
  579. console.log('声音改变')
  580. })
  581. },
  582. calculateCurrent(videoId) {
  583. for (var i = 0; i < this.playList.list.length; i++) {
  584. if (videoId === this.playList.list[i].videoId) {
  585. this.playList.current = i
  586. const key = 'myplaylist-' + this.video.videoId
  587. localStorage.setItem(key, JSON.stringify(this.playList))
  588. }
  589. }
  590. },
  591. setCurrent(current) {
  592. this.playList.current = current
  593. const key = 'myplaylist-' + this.video.videoId
  594. localStorage.setItem(key, JSON.stringify(this.playList))
  595. },
  596. getNextPath(current) {
  597. this.calculateCurrent(current)
  598. const next = this.playList.current + 1
  599. if (next < this.playList.list.length) {
  600. this.setCurrent(next)
  601. const videoId = this.playList.list[next].videoId
  602. const path = '/vidlist/' + videoId
  603. if (path !== this.$route.path) {
  604. this.$router.push(path)
  605. } else {
  606. console.log(this.playList)
  607. this.$notify.info({
  608. message: '视频列表播放完成',
  609. duration: 3000
  610. })
  611. }
  612. }
  613. },
  614. // ****************************************************************************************************************
  615. async submit(newComment, parent, add) {
  616. const res = await new Promise((resolve) => {
  617. setTimeout(() => {
  618. resolve({ newComment, parent })
  619. }, 300)
  620. })
  621. add(Object.assign(res.newComment, { _id: new Date().getTime() }))
  622. console.log('addComment: ', res)
  623. },
  624. async like(comment) {
  625. const res = await new Promise((resolve) => {
  626. setTimeout(() => {
  627. resolve(comment)
  628. }, 0)
  629. })
  630. console.log('likeComment: ', res)
  631. },
  632. async uploadImg({ file, callback }) {
  633. const res = await new Promise((resolve, reject) => {
  634. const reader = new FileReader()
  635. reader.readAsDataURL(file)
  636. reader.onload = () => {
  637. resolve(reader.result)
  638. }
  639. reader.onerror = () => {
  640. reject(reader.error)
  641. }
  642. })
  643. callback(res)
  644. console.log('uploadImg: ', res)
  645. },
  646. async deleteComment(comment, parent) {
  647. const res = await new Promise((resolve) => {
  648. setTimeout(() => {
  649. resolve({ comment, parent })
  650. }, 300)
  651. })
  652. console.log('deleteComment: ', res)
  653. }
  654. }
  655. }
  656. </script>
  657. <style scoped>
  658. /*处于手机屏幕时*/
  659. @media screen and (max-width: 768px) {
  660. .movie-list {
  661. padding-top: 8px;
  662. padding-left: 0.5%;
  663. padding-right: 0.5%;
  664. }
  665. }
  666. .movie-list {
  667. padding-top: 15px;
  668. padding-left: 5%;
  669. padding-right: 5%;
  670. }
  671. .clearfix:before,
  672. .clearfix:after {
  673. display: table;
  674. content: "";
  675. }
  676. .clearfix:after {
  677. clear: both;
  678. }
  679. .v-tag {
  680. padding-top: 10px;
  681. }
  682. .tag{
  683. margin-right: 3px;
  684. }
  685. </style>