play.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. <template>
  2. <view class="play-container">
  3. <u-loading-page
  4. :loading="loading"
  5. :bg-color="$colors.bgColor"
  6. :color="$colors.primaryColor"
  7. :loading-color="$colors.primaryColor"
  8. />
  9. <template v-if="!loading">
  10. <!--播放数据-->
  11. <view class="view-num main-left cross-center">
  12. <u-icon name="eye-fill" :color="isCollect?$colors.primaryColor:$colors.defaultColor" size="26rpx" />
  13. <text>{{ episode.user_watch_record_count }}</text>
  14. </view>
  15. <!--按钮-->
  16. <view class="status-bar dir-top-wrap main-center">
  17. <!--喜欢-->
  18. <view
  19. class="item fav dir-top-wrap main-center cross-center"
  20. :class="{active: isFav}"
  21. @click="handleFavorite"
  22. >
  23. <u-icon name="heart-fill" size="58rpx" :color="isFav?$colors.primaryColor:$colors.defaultColor" />
  24. <text>{{ episode.user_favorite_count }}</text>
  25. </view>
  26. <!--收藏-->
  27. <view
  28. class="item collect dir-top-wrap main-center cross-center"
  29. :class="{active: isCollect}"
  30. @click="handleCollect"
  31. >
  32. <u-icon name="star-fill" size="58rpx" :color="isCollect?$colors.primaryColor:$colors.defaultColor" />
  33. <text>{{ episode.user_collect_count }}</text>
  34. </view>
  35. <!--分享-->
  36. <view class="item share dir-top-wrap main-center cross-center">
  37. <button open-type="share" />
  38. <u-icon name="share-fill" size="58rpx" :color="$colors.defaultColor" />
  39. <text>{{ episode.sahre_count }}</text>
  40. </view>
  41. </view>
  42. <!--视频播放-->
  43. <view class="video-box main-center cross-center" :style="{zIndex: isPlaying?0:998}">
  44. <!-- 控制按钮 - 播放 -->
  45. <view v-if="!isPlaying" class="play-layer main-center cross-center" @tap="handlePlay">
  46. <u-icon name="play-right-fill" size="100rpx" :color="$colors.defaultColor" />
  47. </view>
  48. <!-- 控制按钮 - 暂停 -->
  49. <view v-if="isPlaying" class="pause-layer" @tap="handlePause" />
  50. <!--进度条-->
  51. <view v-if="isPlaying" class="progress-container">
  52. <view class="progress" :style="{width: progress+'%'}" />
  53. </view>
  54. <!--视频容器-->
  55. <video
  56. id="video"
  57. :show-play-btn="video.playBtn"
  58. :show-fullscreen-btn="video.fullscreenBtn"
  59. :controls="video.controls"
  60. object-fit="contain"
  61. style="width: 100%;height: 100%;"
  62. :poster="episode.cover_img"
  63. :src="src"
  64. @timeupdate="timeupdate"
  65. />
  66. </view>
  67. <!--底部-->
  68. <view class="footer" :class="{episode: footerShow}">
  69. <view class="bar main-between cross-center" @click="footerShow = !footerShow">
  70. <view class="icon" />
  71. <view class="name">
  72. <u-text :text="episode.name" :color="$colors.infoColor" :lines="1" />
  73. <!-- <u-text text="321313" :color="$colors.infoColor" :lines="1" />-->
  74. </view>
  75. <view class="arrow">
  76. <u-icon name="arrow-up" :color="$colors.infoColor" size="32rpx" />
  77. </view>
  78. </view>
  79. <view class="episode-container dir-top-wrap">
  80. <!--分集 横向滚动-->
  81. <scroll-view
  82. class="header-box dir-left-nowrap cross-center"
  83. scroll-x
  84. scroll-with-animation
  85. >
  86. <view
  87. v-for="(item,index) in episodes"
  88. :key="index"
  89. class="header-item"
  90. :class="{active: episodesIndex === index}"
  91. @click="episodesIndex=index"
  92. >{{ item.title }}</view>
  93. </scroll-view>
  94. <!--几集选择-->
  95. <view class="content dir-left-wrap main-left">
  96. <view
  97. v-for="(item, index) in episodes[episodesIndex].lists"
  98. :key="index"
  99. class="episode-item"
  100. @click="handleSelectEpisode(item.index)"
  101. >
  102. <image :src="episode.cover_img" />
  103. <text>第{{ item.sort }}集</text>
  104. <view v-if="activeIndex === item.index && isPlaying" class="playing" />
  105. <view v-if="!item.is_free && buyRecord.indexOf(item.id) === -1" class="lock main-center cross-center">
  106. <u-icon name="lock-fill" :color="$colors.defaultColor" size="46rpx" />
  107. </view>
  108. </view>
  109. </view>
  110. </view>
  111. </view>
  112. <!--toast-->
  113. <view
  114. v-if="toast.show"
  115. class="toast dir-top-wrap main-center cross-center"
  116. :class="toast.status"
  117. >
  118. <u-icon
  119. :name="toast.status === 'success' ? 'checkmark-circle' : 'close-circle'"
  120. :color="toast.status === 'success' ? $colors.primaryColor : $colors.defaultColor"
  121. size="80rpx"
  122. />
  123. <text>{{ toast.text }}</text>
  124. </view>
  125. <!--购买弹窗-->
  126. <u-modal
  127. :show="modal.show"
  128. :content="`确定购买【第${modal.item.sort}集】?`"
  129. show-cancel-button
  130. @confirm="handleBuy"
  131. @cancel="modal.show = false"
  132. />
  133. <!--充值-->
  134. <recharge
  135. :show.sync="rechargeShow"
  136. type="play"
  137. mode="bottom"
  138. :episode="episode"
  139. :list="modal.item"
  140. />
  141. </template>
  142. </view>
  143. </template>
  144. <script>
  145. import { mapState } from 'vuex'
  146. import Recharge from '../../components/Recharge/index'
  147. export default {
  148. name: 'Play',
  149. components: { Recharge },
  150. data() {
  151. return {
  152. id: null,
  153. listId: null,
  154. isPlaying: false,
  155. videoContext: null,
  156. progress: 0,
  157. episode: null,
  158. activeIndex: 0,
  159. loading: true,
  160. isCollect: false,
  161. isFav: false,
  162. video: {
  163. controls: false,
  164. fullscreenBtn: false,
  165. playBtn: false
  166. },
  167. toast: {
  168. status: 'success',
  169. text: '收藏成功',
  170. show: false
  171. },
  172. footerShow: false,
  173. episodesIndex: 0,
  174. buyRecord: [],
  175. modal: {
  176. show: false,
  177. item: {}
  178. },
  179. rechargeShow: false
  180. }
  181. },
  182. computed: {
  183. ...mapState({
  184. userInfo: seate => seate.user.info
  185. }),
  186. src() {
  187. if (!this.episode) return ''
  188. return this.episode.lists[this.activeIndex].url
  189. },
  190. episodes() {
  191. const list = []
  192. if (this.episode) {
  193. let temp = []
  194. this.episode.lists.forEach((obj, index) => {
  195. obj.index = index
  196. temp.push(obj)
  197. if (temp.length === 6 || index === (this.episode.lists.length - 1)) {
  198. const start = list.length ? (list.length * 6) + 1 : 1
  199. const end = (start - 1) + temp.length
  200. list.push({ title: `${start}集-${end}集`, lists: temp })
  201. temp = []
  202. }
  203. })
  204. }
  205. return list
  206. }
  207. },
  208. watch: {
  209. 'toast.show'(val) {
  210. if (val) {
  211. setTimeout(() => {
  212. this.toast.show = false
  213. }, 1000)
  214. }
  215. }
  216. },
  217. methods: {
  218. // 播放进度
  219. timeupdate({ detail }) {
  220. // currentTime, duration
  221. if (detail.duration) {
  222. this.progress = (detail.currentTime / detail.duration * 100).toFixed(2)
  223. }
  224. },
  225. // 播放
  226. handlePlay() {
  227. const item = this.episode.lists[this.activeIndex]
  228. this.modal.item = item
  229. // 检查是否购买
  230. if (!this.checkBeforePlay(item)) {
  231. // 余额是否购买
  232. if (!this.checkOverage(item)) {
  233. this.rechargeShow = true
  234. return
  235. }
  236. this.modal.show = true
  237. this.footerShow = true
  238. return
  239. }
  240. this.isPlaying = true
  241. this.videoContext.play()
  242. this.watched(this.id, this.listId)
  243. },
  244. // 暂停
  245. handlePause() {
  246. if (!this.isPlaying) return
  247. this.isPlaying = false
  248. this.videoContext.pause()
  249. },
  250. // 选择剧集
  251. handleSelectEpisode(index) {
  252. const item = this.episode.lists[index]
  253. this.modal.item = item
  254. // 检查是否购买
  255. if (!this.checkBeforePlay(item)) {
  256. // 余额是否购买
  257. if (!this.checkOverage(item)) {
  258. this.rechargeShow = true
  259. return
  260. }
  261. this.modal.show = true
  262. return
  263. }
  264. this.activeIndex = index
  265. this.footerShow = false
  266. this.$loading()
  267. setTimeout(() => {
  268. this.$hideLoading()
  269. this.isPlaying = true
  270. this.videoContext.play()
  271. this.watched(this.id, this.episode.lists[this.activeIndex].id)
  272. }, 1000)
  273. },
  274. // 获取剧集详情
  275. getEpisode() {
  276. this.loading = true
  277. this.$api.episode.detail(this.id).then(res => {
  278. this.loading = false
  279. this.episode = res.data
  280. if (this.listId) {
  281. this.episode.lists.forEach((obj, index) => {
  282. if (parseInt(this.listId) === parseInt(obj.id)) {
  283. this.activeIndex = index
  284. }
  285. })
  286. } else {
  287. this.listId = this.episode.lists[0].id
  288. }
  289. })
  290. },
  291. // 检查是否收藏当前剧集
  292. checkCollect() {
  293. this.$api.user.collect.check(this.id).then(res => {
  294. this.isCollect = res.data
  295. })
  296. },
  297. // 收藏相关 处理
  298. handleCollect() {
  299. const method = this.isCollect ? 'destroy' : 'add'
  300. const num = this.isCollect ? -1 : 1
  301. this.$api.user.collect[method](this.id).then(res => {
  302. if (res.data) {
  303. this.toast.show = true
  304. this.toast.status = this.isCollect ? 'cancel' : 'success'
  305. this.toast.text = this.isCollect ? '取消收藏' : '收藏成功'
  306. this.episode.user_collect_count += num
  307. this.isCollect = !this.isCollect
  308. }
  309. })
  310. },
  311. // 检查是否喜欢当前短剧
  312. checkFavorite() {
  313. this.$api.user.favorite.check(this.id).then(res => {
  314. this.isFav = res.data
  315. })
  316. },
  317. // 喜欢剧集 处理
  318. handleFavorite() {
  319. const method = this.isFav ? 'destroy' : 'add'
  320. const num = this.isFav ? -1 : 1
  321. this.$api.user.favorite[method](this.id).then(res => {
  322. if (res.data) {
  323. this.toast.show = true
  324. this.toast.status = this.isFav ? 'cancel' : 'success'
  325. this.toast.text = this.isFav ? '取消喜欢' : '喜欢成功'
  326. this.episode.user_favorite_count += num
  327. this.isFav = !this.isFav
  328. }
  329. })
  330. },
  331. // 观看记录
  332. watched(id, list_id) {
  333. this.$api.user.episode.watched(id, list_id).then(res => {
  334. })
  335. },
  336. // 分享
  337. shared(id, list_id) {
  338. this.$api.episode.shared(id, list_id).then(res => {
  339. this.episode.share_count += 1
  340. })
  341. },
  342. // 当前剧集购买记录
  343. async getBuyRecord() {
  344. await this.$api.user.episode.buyRecord(this.id).then(res => {
  345. this.buyRecord = res.data
  346. })
  347. },
  348. // 购买
  349. handleBuy() {
  350. this.$loading('购买中...')
  351. this.$api.user.episode.buyHandle(this.id, this.modal.item.id).then(res => {
  352. this.$hideLoading()
  353. if (typeof res.overage !== 'undefined') {
  354. this.rechargeShow = true
  355. } else {
  356. this.$u.toast('购买成功')
  357. this.modal.show = false
  358. this.getBuyRecord()
  359. this.$api.user.info().then(res => {
  360. this.$store.dispatch('user/info', res.data)
  361. })
  362. }
  363. }).catch(() => {
  364. this.$hideLoading()
  365. })
  366. },
  367. // 播放前检查是否购买/免费
  368. checkBeforePlay(item) {
  369. return item.is_free || (!item.is_free && this.buyRecord.indexOf(item.id) !== -1)
  370. },
  371. // 检查余额是否足够支付
  372. checkOverage(item) {
  373. return this.userInfo.info.integral >= item.sale_price
  374. }
  375. },
  376. async onLoad(options) {
  377. this.videoContext = uni.createVideoContext('video', this)
  378. this.id = options.id
  379. this.listId = options?.list_id
  380. await this.getBuyRecord()
  381. this.getEpisode()
  382. this.checkCollect()
  383. this.checkFavorite()
  384. },
  385. // 分享
  386. onShareAppMessage(res) {
  387. if (res.from === 'button') { // 来自页面内分享按钮
  388. console.log(res.target)
  389. }
  390. let options = {
  391. title: '',
  392. path: `/pages/episode/play?id=${this.id}`
  393. }
  394. if (this.episode) {
  395. options = {
  396. title: this.episode.name,
  397. path: `/pages/episode/play?id=${this.id}`,
  398. imageUrl: this.episode.cover_img,
  399. desc: this.episode.name,
  400. success: res => {
  401. this.shared(this.id, this.listId)
  402. }
  403. }
  404. }
  405. return options
  406. }
  407. }
  408. </script>
  409. <style lang="scss" scoped>
  410. .play-container {
  411. font-size: 28rpx;
  412. .video-box{
  413. position: fixed;
  414. top: 0;
  415. left: 0;
  416. right: 0;
  417. bottom: 0;
  418. .play-layer{
  419. position: fixed;
  420. top: 0;
  421. left: 0;
  422. bottom: 0;
  423. right: 0;
  424. background: transparent;
  425. z-index: 999;
  426. }
  427. .pause-layer{
  428. position: absolute;
  429. top: 0;
  430. left: 0;
  431. bottom: 0;
  432. right: 0;
  433. background: transparent;
  434. z-index: 99;
  435. }
  436. video{
  437. position: absolute;
  438. z-index: 98;
  439. }
  440. .progress-container{
  441. width: 93vw;
  442. background: #fff;
  443. height: 10rpx;
  444. position: fixed;
  445. z-index: 100;
  446. bottom: 160rpx;
  447. .progress{
  448. height: 10rpx;
  449. background: linear-gradient(270deg, #6EEBE8 0%, #FF74B9 100%);
  450. }
  451. }
  452. }
  453. .view-num{
  454. position: fixed;
  455. right: 20rpx;
  456. top: 40rpx;
  457. color: #fff;
  458. font-size: 26rpx;
  459. z-index: 100;
  460. text{
  461. margin-left: 10rpx;
  462. }
  463. }
  464. .status-bar{
  465. position: fixed;
  466. bottom: 300rpx;
  467. right: 40rpx;
  468. color: $default-color;
  469. z-index: 100;
  470. .item{
  471. margin-bottom: 40rpx;
  472. &.share{
  473. position: relative;
  474. button{
  475. position: absolute;
  476. background: transparent;
  477. top: 0;
  478. left: 0;
  479. right: 0;
  480. bottom: 0;
  481. z-index: 1;
  482. &:after{
  483. content: unset;
  484. }
  485. }
  486. }
  487. &.active{
  488. color: $primary-color;
  489. }
  490. text{
  491. margin-top: 10rpx;
  492. }
  493. }
  494. }
  495. .footer{
  496. position: fixed;
  497. width: 95vw;
  498. height: 76rpx;
  499. background: rgba(24, 28, 47, 0.8);
  500. bottom: 50rpx;
  501. left: 50%;
  502. transform: translateX(-50%);
  503. font-size: 26rpx;
  504. color: $info-color;
  505. border-radius: 20rpx;
  506. z-index: 999;
  507. transition: .3s;
  508. &.episode{
  509. bottom: 700rpx;
  510. margin-top: -4rpx;
  511. border-bottom-left-radius: 0;
  512. border-bottom-right-radius: 0;
  513. .episode-container{
  514. display: flex;
  515. margin-top: -4rpx;
  516. border-bottom-left-radius: 20px;
  517. border-bottom-right-radius: 20px;
  518. }
  519. .bar{
  520. .arrow{
  521. transform: rotate(180deg);
  522. }
  523. }
  524. }
  525. .bar{
  526. padding: 0 20rpx;
  527. .icon{
  528. background: url("/static/image/video.png") no-repeat center;
  529. background-size: 70%;
  530. width: 80rpx;
  531. height: 80rpx;
  532. }
  533. .name{
  534. text-align: left;
  535. flex: 1;
  536. padding: 0 30rpx;
  537. }
  538. .arrow{
  539. transition: .3s;
  540. }
  541. }
  542. .episode-container{
  543. display: none;
  544. background: inherit;
  545. .header-box{
  546. white-space: nowrap;
  547. margin: 20rpx 0;
  548. .header-item{
  549. margin-right: 20rpx;
  550. border-radius: 20rpx;
  551. display: inline-block;
  552. width: 200rpx;
  553. border: 1rpx solid $default-color;
  554. text-align: center;
  555. padding: 10rpx 0;
  556. color: $default-color;
  557. &.active{
  558. border-color: $primary-color;
  559. color: $primary-color;
  560. }
  561. }
  562. }
  563. .content{
  564. margin-top: 20rpx;
  565. .episode-item{
  566. position: relative;
  567. width: calc((100% - #{40rpx}) / 3);
  568. margin-right: 20rpx;
  569. margin-bottom: 20rpx;
  570. overflow: hidden;
  571. border-radius: 18rpx;
  572. &:nth-child(3n){
  573. margin-right: 0;
  574. }
  575. .playing{
  576. position: absolute;
  577. top: 0;
  578. left: 0;
  579. bottom: 0;
  580. right: 0;
  581. background: rgba(0,0,0,.5) url("/static/image/playing.png") no-repeat center;
  582. background-size: 40rpx;
  583. z-index: 2;
  584. }
  585. image{
  586. width: 100%;
  587. height: 260rpx;
  588. }
  589. text{
  590. position: absolute;
  591. left: 0;
  592. bottom: 0;
  593. right: 0;
  594. color: $default-color;
  595. padding: 20rpx 0;
  596. text-align: center;
  597. background: rgba(0,0,0,.3);
  598. z-index: 1;
  599. }
  600. .lock{
  601. position: absolute;
  602. top: 0;
  603. left: 0;
  604. bottom: 0;
  605. right: 0;
  606. background: rgba(0,0,0,.5);
  607. z-index: 2;
  608. }
  609. }
  610. }
  611. }
  612. }
  613. .toast{
  614. position: fixed;
  615. width: 60vw;
  616. background: rgba(0,0,0,.5);
  617. height: 300rpx;
  618. top: 50%;
  619. left: 50%;
  620. transform: translate(-50%,-50%);
  621. border-radius: 20rpx;
  622. font-size: 36rpx;
  623. color: $default-color;
  624. &.success{
  625. color: $primary-color;
  626. }
  627. text{
  628. margin-top: 20rpx;
  629. }
  630. }
  631. }
  632. </style>