VideoPage.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. <template>
  2. <el-row v-if="!permissionDenied">
  3. <el-row v-if="video !== null" class="movie-list">
  4. <el-col :md="15">
  5. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  6. <el-card class="box-card">
  7. <div slot="header" class="clearfix">
  8. <el-row>
  9. <h3 v-html="video.title" />
  10. </el-row>
  11. <el-row style="color: #999;font-size: 16px;padding-top: 0px;">
  12. <span><i class="el-icon-video-play">{{ video.view }}</i></span>
  13. <span v-html="'&nbsp;&nbsp;&nbsp;&nbsp;'" />
  14. <span><i class="el-icon-s-comment">{{ video.comment }}</i></span>
  15. <span v-html="'&nbsp;&nbsp;&nbsp;&nbsp;'" />
  16. <span><i class="el-icon-watch">{{ video.pubDate }}</i></span>
  17. <span v-html="'&nbsp;&nbsp;&nbsp;&nbsp;'" />
  18. <span v-if="videoId !== null && videoId.includes('BV')"><i class="el-icon-apple">
  19. <a target="_blank" :href="`https://bilibili.com/` + videoId">bili</a>
  20. </i></span>
  21. </el-row>
  22. </div>
  23. <div class="text item">
  24. <video-player :video-prop="video" />
  25. </div>
  26. </el-card>
  27. </el-row>
  28. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  29. <el-card class="box-card">
  30. <div slot="header" class="clearfix">
  31. <div class="video-data-row">
  32. <el-button
  33. type="danger"
  34. size="mini"
  35. icon="el-icon-collection"
  36. :disabled="isCollected"
  37. @click="collection(video.videoId)"
  38. >
  39. <span>收藏 {{ video.favorite }}</span>
  40. </el-button>
  41. <el-button
  42. type="danger"
  43. size="mini"
  44. icon="el-icon-thumb"
  45. :disabled="isCollected"
  46. @click="collection(video.videoId)"
  47. >
  48. <span>喜欢 {{ video.thumbUp }}</span>
  49. </el-button>
  50. <el-button
  51. type="danger"
  52. size="mini"
  53. icon="el-icon-share"
  54. :disabled="isCollected"
  55. @click="getShareUrl(video.videoId)"
  56. >
  57. <span>分享 {{ video.share }}</span>
  58. </el-button>
  59. <el-button
  60. type="danger"
  61. size="mini"
  62. icon="el-icon-download"
  63. @click="getDownloadUrl(video.videoId)"
  64. >
  65. <span>下载</span>
  66. </el-button>
  67. <el-button
  68. type="danger"
  69. size="mini"
  70. icon="el-icon-help"
  71. @click="displayErrorReportDialog"
  72. >
  73. <span>报错</span>
  74. </el-button>
  75. </div>
  76. </div>
  77. <div class="text item">
  78. <!--视频描述行-->
  79. <span class="description" v-html="video.description" />
  80. <el-divider />
  81. <!--视频标签行-->
  82. <div class="v-tag">
  83. <el-tag
  84. v-for="(tag,index) in video.tags"
  85. :key="index"
  86. class="tag"
  87. size="medium"
  88. effect="plain"
  89. >
  90. <router-link target="_blank" :to="`/video/tag/` + tag">
  91. {{ tag }}
  92. </router-link>
  93. </el-tag>
  94. </div>
  95. </div>
  96. </el-card>
  97. </el-row>
  98. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  99. <el-card class="box-card">
  100. <div slot="header" class="clearfix">
  101. <el-row>
  102. <h3>视频评论</h3>
  103. </el-row>
  104. </div>
  105. <div class="text item">
  106. <div ref="comment" :style="wrapStyle" class="comment-wrap">
  107. <comment
  108. v-model="dataList"
  109. :user="currentUser"
  110. :props="props"
  111. :before-submit="submit"
  112. :before-like="like"
  113. :before-delete="deleteComment"
  114. :upload-img="uploadImg"
  115. />
  116. <el-pagination
  117. :small="screenWidth <= 768"
  118. hide-on-single-page
  119. layout="prev, pager, next"
  120. :page-size="pageSize"
  121. :current-page="currentPage"
  122. :total="totalSize"
  123. @current-change="handleCurrentChange"
  124. />
  125. </div>
  126. </div>
  127. </el-card>
  128. </el-row>
  129. </el-col>
  130. <el-col :md="9">
  131. <el-row>
  132. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  133. <user-avatar-card v-if="user !== null" :user-avatar="user" />
  134. </el-row>
  135. </el-row>
  136. <el-row style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  137. <el-card class="box-card">
  138. <div slot="header" class="clearfix">
  139. <el-row>
  140. <h3>接下来播放</h3>
  141. </el-row>
  142. <el-row>
  143. <span>自动播放 <el-switch v-model="autoPlay" /></span>
  144. </el-row>
  145. </div>
  146. </el-card>
  147. </el-row>
  148. <el-row v-for="(item,index) in similarVideos" :key="index" style="padding-right: 5px; padding-left: 5px; padding-bottom: 5px">
  149. <side-video-card :video="item" />
  150. </el-row>
  151. </el-col>
  152. <!-- 视频访问码对话框 -->
  153. <el-dialog
  154. append-to-body
  155. :visible.sync="showAccessCodeDialog"
  156. width="30%"
  157. center
  158. >
  159. <el-card class="box-card">
  160. <div slot="header" class="clearfix">
  161. <span>输入视频访问码</span>
  162. <el-button style="float: right; padding: 3px 0" type="text" @click="submitAccessCodeWrapper">提交</el-button>
  163. </div>
  164. <div class="text item">
  165. <el-form ref="form" :model="accessCodeForm" label-width="80px">
  166. <el-form-item label="访问码">
  167. <el-input v-model="accessCodeForm.accessCode" style="width: 70%; padding-right: 2px" />
  168. </el-form-item>
  169. </el-form>
  170. </div>
  171. </el-card>
  172. </el-dialog>
  173. <!-- 视频报错对话框 -->
  174. <el-dialog
  175. append-to-body
  176. :visible.sync="showErrorReportDialog"
  177. width="30%"
  178. center
  179. >
  180. <el-card class="box-card">
  181. <div slot="header" class="clearfix">
  182. <span>视频报错</span>
  183. <el-button style="float: right; padding: 3px 0" type="text" @click="submitErrorReport">提交错误</el-button>
  184. </div>
  185. <div class="text item">
  186. <el-form ref="form" :model="errorReportForm" label-width="80px">
  187. <el-form-item label="错误类型">
  188. <el-select v-model="errorReportForm.errorCode" placeholder="选择视频错误类型">
  189. <el-option label="视频无封面" value="8" />
  190. <el-option label="视频无资源" value="9" />
  191. <el-option label="视频有广告" value="10" />
  192. </el-select>
  193. </el-form-item>
  194. </el-form>
  195. </div>
  196. </el-card>
  197. </el-dialog>
  198. </el-row>
  199. </el-row>
  200. <el-row v-else>
  201. <permission-denied-card :text-object="textObject" />
  202. </el-row>
  203. </template>
  204. <script>
  205. import PermissionDeniedCard from '@/components/card/PermissionDeniedCard'
  206. import VideoPlayer from 'components/VideoPlayer'
  207. import SideVideoCard from 'components/card/SideVideoCard'
  208. import UserAvatarCard from '@/components/card/UserAvatarCard'
  209. import comment from '@/components/comment'
  210. import { similarVideo, videoInfo, videoErrorReport, downloadVideo, getShortUrl } from '@/api/video'
  211. import { collectItem } from '@/api/collect'
  212. import { getUserInfo } from '@/api/user'
  213. import { submitAccessCode } from '@/api/content'
  214. import { publishComment, getComment } from '@/api/comment'
  215. import { getAuthedUser } from '@/utils/auth'
  216. export default {
  217. name: 'VideoPage',
  218. components: { SideVideoCard, VideoPlayer, UserAvatarCard, PermissionDeniedCard, comment },
  219. data() {
  220. return {
  221. // 屏幕宽度, 为了控制分页条的大小
  222. screenWidth: document.body.clientWidth,
  223. currentPage: 1,
  224. pageSize: 20,
  225. totalSize: 0,
  226. dataList: [],
  227. // ********************************************************************/
  228. wrapStyle: '',
  229. videoComments: [
  230. {
  231. commentId: 114511,
  232. content: 'this is comment content',
  233. imageUrl: '',
  234. children: [],
  235. likes: 0,
  236. liked: false,
  237. reply: null,
  238. createAt: 1700271326393,
  239. user: {
  240. userId: 10001,
  241. name: '西瓜',
  242. avatar: ''
  243. }
  244. }
  245. ],
  246. currentUser: {
  247. userId: 9999,
  248. name: '芒果',
  249. avatar: '//picx.zhimg.com/v2-a2c89378a6332cbfed3e28b5ab84feb7.jpg',
  250. author: true
  251. },
  252. // 自定义组件中 comment 对象的字段名
  253. props: {
  254. id: 'commentId',
  255. content: 'content',
  256. imgSrc: 'imageUrl',
  257. children: 'children',
  258. likes: 'likes',
  259. liked: 'liked',
  260. reply: 'reply',
  261. createAt: 'createAt',
  262. total: 'total',
  263. user: 'user'
  264. },
  265. // ********************************************************************/
  266. videoId: null,
  267. video: null,
  268. user: null,
  269. similarVideos: [],
  270. isCollected: false,
  271. showAccessCodeDialog: false,
  272. accessCodeForm: {
  273. contentId: null,
  274. contentType: 1002,
  275. accessCode: null
  276. },
  277. showErrorReportDialog: false,
  278. errorReportForm: {
  279. videoId: null,
  280. errorCode: null
  281. },
  282. permissionDenied: false,
  283. textObject: {
  284. content: '视频',
  285. route: '/video'
  286. },
  287. autoPlay: false
  288. }
  289. },
  290. watch: {
  291. // 地址栏 url 发生变化时重新加载本页面
  292. $route() {
  293. this.$router.go()
  294. }
  295. },
  296. created() {
  297. const loginUser = getAuthedUser()
  298. if (loginUser != null) {
  299. this.currentUser = {
  300. userId: loginUser.userId,
  301. name: loginUser.screenName,
  302. avatar: loginUser.avatarUrl,
  303. author: true
  304. }
  305. }
  306. this.videoId = this.$route.params.id
  307. this.accessCodeForm.contentId = this.videoId
  308. this.getVideoInfo(this.videoId)
  309. this.getSimilarVideos(this.videoId)
  310. this.getCommentWrapper(this.currentPage)
  311. },
  312. mounted() {
  313. const header = this.$refs.header
  314. if (header !== undefined && header !== null) {
  315. this.wrapStyle = `height: calc(100vh - ${header.clientHeight + 20}px)`
  316. }
  317. },
  318. methods: {
  319. // ****************************************************************************************************************
  320. handleCurrentChange(currentPage) {
  321. this.currentPage = currentPage
  322. this.getCommentWrapper(currentPage)
  323. // 回到顶部
  324. scrollTo(0, 0)
  325. },
  326. getCommentWrapper(pageNumber) {
  327. getComment(this.videoId, pageNumber).then(resp => {
  328. if (resp.code === 0) {
  329. const respData = resp.data
  330. this.dataList = respData.list
  331. this.totalSize = respData.totalSize
  332. } else {
  333. this.$notify({
  334. title: '提示',
  335. message: resp.msg,
  336. type: 'error',
  337. duration: 3000
  338. })
  339. }
  340. }).catch(error => {
  341. this.$notify({
  342. title: '提示',
  343. message: error.message,
  344. type: 'warning',
  345. duration: 3000
  346. })
  347. })
  348. },
  349. // ****************************************************************************************************************
  350. // 获取视频的详细信息
  351. getVideoInfo(videoId) {
  352. videoInfo(videoId).then(resp => {
  353. if (resp.code === 0) {
  354. // this.showAccessCodeDialog = true
  355. this.video = resp.data
  356. document.title = resp.data.title
  357. this.userId = resp.data.userId
  358. getUserInfo(this.userId).then(resp => {
  359. if (resp.code === 0) {
  360. this.user = resp.data
  361. } else {
  362. this.$notify.error({
  363. message: '用户数据获取失败',
  364. type: 'warning',
  365. duration: 3000
  366. })
  367. }
  368. })
  369. } else if (resp.code === 2) {
  370. this.$router.push('/404')
  371. } else {
  372. this.permissionDenied = true
  373. }
  374. }).catch(error => {
  375. this.$notify.error({
  376. message: error.message,
  377. type: 'warning',
  378. duration: 3000
  379. })
  380. })
  381. },
  382. // 获取和当前视频类似的其他视频
  383. getSimilarVideos(videoId) {
  384. similarVideo(videoId).then(resp => {
  385. if (resp.code === 0) {
  386. this.similarVideos = resp.data
  387. } else {
  388. this.$notify.error({
  389. message: '推荐视频数据获取失败',
  390. type: 'warning',
  391. duration: 3000
  392. })
  393. }
  394. }).catch(error => {
  395. this.$notify.error({
  396. message: error.message,
  397. type: 'warning',
  398. duration: 3000
  399. })
  400. })
  401. },
  402. // 换一换
  403. refreshSimilar() {
  404. console.log('刷新相关推荐')
  405. },
  406. // 用户点击收藏
  407. collection(videoId) {
  408. const jsonData = {}
  409. jsonData.contentType = 1002
  410. jsonData.contentId = videoId
  411. jsonData.collected = true
  412. collectItem(jsonData).then(resp => {
  413. if (resp.code === 0) {
  414. this.$notify.success({
  415. title: '视频已收藏',
  416. type: 'success',
  417. duration: 3000
  418. })
  419. } else {
  420. this.$notify.warning({
  421. title: '视频收藏失败',
  422. type: 'warning',
  423. duration: 3000
  424. })
  425. }
  426. })
  427. },
  428. getShareUrl(videoId) {
  429. getShortUrl(videoId).then(resp => {
  430. if (resp.code === 0) {
  431. console.log(resp.data)
  432. this.video.share += 1
  433. }
  434. })
  435. },
  436. getDownloadUrl(videoId) {
  437. // let filename
  438. downloadVideo(videoId).then(resp => {
  439. if (resp.code === 0) {
  440. const downloadUrl = resp.data.url
  441. window.open(downloadUrl, '_blank')
  442. /* fetch(downloadUrl.url, {
  443. headers: {
  444. Authorization: 'Bearer ' + downloadUrl.token
  445. },
  446. method: 'GET',
  447. credentials: 'include'
  448. }).then(resp => {
  449. /!*
  450. 遍历 formdata
  451. for (const key of resp.headers.keys()) {
  452. console.log(key + ' : ' + resp.headers.get(key))
  453. }*!/
  454. const header = resp.headers.get('Content-Disposition')
  455. const parts = header.split(';')
  456. const encodeFilename = parts[1].split('=')[1]
  457. filename = decodeURI(encodeFilename)
  458. return resp.blob()
  459. }).then(data => {
  460. const blobUrl = window.URL.createObjectURL(data)
  461. const a = document.createElement('a')
  462. a.download = filename
  463. a.href = blobUrl
  464. a.click()
  465. }).catch(e => {
  466. this.$notify({
  467. title: '提示',
  468. message: '视频下载失败',
  469. type: 'warning',
  470. duration: 3000
  471. })
  472. })*/
  473. } else {
  474. this.$notify({
  475. title: '提示',
  476. message: resp.msg,
  477. type: 'warning',
  478. duration: 3000
  479. })
  480. }
  481. }).catch(error => {
  482. this.$notify({
  483. title: '提示',
  484. message: error.message,
  485. type: 'error',
  486. duration: 3000
  487. })
  488. })
  489. },
  490. submitAccessCodeWrapper() {
  491. submitAccessCode(this.accessCodeForm).then(resp => {
  492. if (resp.code === 0) {
  493. this.video = resp.data
  494. } else {
  495. this.$notify({
  496. message: resp.msg,
  497. type: 'warning',
  498. duration: 3000
  499. })
  500. }
  501. }).catch(error => {
  502. this.$notify({
  503. title: '提示',
  504. message: error.message,
  505. type: 'warning',
  506. duration: 3000
  507. })
  508. })
  509. },
  510. displayErrorReportDialog() {
  511. this.errorReportForm.videoId = this.video.videoId
  512. this.showErrorReportDialog = true
  513. },
  514. submitErrorReport() {
  515. this.showErrorReportDialog = false
  516. videoErrorReport(this.errorReportForm).then(resp => {
  517. if (resp.code === 0) {
  518. this.errorReportForm.errorCode = null
  519. this.$notify({
  520. title: '提示',
  521. message: '视频错误已提交',
  522. type: 'warning',
  523. duration: 3000
  524. })
  525. }
  526. }).catch(error => {
  527. this.$notify({
  528. title: '提示',
  529. message: error.message,
  530. type: 'warning',
  531. duration: 3000
  532. })
  533. })
  534. },
  535. // ****************************************************************************************************************
  536. // 评论
  537. async submit(newComment, parent, add) {
  538. const res = await new Promise((resolve) => {
  539. setTimeout(() => {
  540. resolve({ newComment, parent })
  541. }, 300)
  542. })
  543. add(Object.assign(res.newComment, { postId: this.video.videoId }))
  544. if (res.parent !== null) {
  545. // console.log('parent: ', res.parent)
  546. } else {
  547. this.totalSize += 1
  548. }
  549. // console.log('addComment: ', res)
  550. publishComment(res).then(resp => {
  551. if (resp.code === 0) {
  552. this.$notify.success({
  553. message: '评论已发布',
  554. duration: 3000
  555. })
  556. } else {
  557. this.$notify.warning({
  558. message: '评论发布失败',
  559. duration: 3000
  560. })
  561. }
  562. })
  563. },
  564. async like(comment) {
  565. const res = await new Promise((resolve) => {
  566. setTimeout(() => {
  567. resolve(comment)
  568. }, 0)
  569. })
  570. console.log('likeComment: ', res)
  571. },
  572. async uploadImg({ file, callback }) {
  573. const res = await new Promise((resolve, reject) => {
  574. const reader = new FileReader()
  575. reader.readAsDataURL(file)
  576. reader.onload = () => {
  577. resolve(reader.result)
  578. }
  579. reader.onerror = () => {
  580. reject(reader.error)
  581. }
  582. })
  583. callback(res)
  584. console.log('uploadImg: ', res)
  585. },
  586. async deleteComment(comment, parent) {
  587. const res = await new Promise((resolve) => {
  588. setTimeout(() => {
  589. resolve({ comment, parent })
  590. }, 300)
  591. })
  592. console.log('deleteComment: ', res)
  593. }
  594. // ****************************************************************************************************************
  595. }
  596. }
  597. </script>
  598. <style scoped>
  599. /*处于手机屏幕时*/
  600. @media screen and (max-width: 768px) {
  601. .movie-list {
  602. padding-top: 8px;
  603. padding-left: 0.5%;
  604. padding-right: 0.5%;
  605. }
  606. }
  607. .movie-list {
  608. padding-top: 15px;
  609. padding-left: 5%;
  610. padding-right: 5%;
  611. }
  612. .clearfix:before,
  613. .clearfix:after {
  614. display: table;
  615. content: "";
  616. }
  617. .clearfix:after {
  618. clear: both;
  619. }
  620. .v-tag {
  621. padding-top: 10px;
  622. }
  623. .tag{
  624. margin-right: 3px;
  625. }
  626. </style>