play.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  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. progress(val) {
  217. if (val >= 100) {
  218. this.activeIndex = this.activeIndex + 1
  219. if (this.activeIndex === this.episode.lists.length) {
  220. this.$u.toast('已全部播放完成')
  221. } else {
  222. this.$forceUpdate()
  223. this.$loading('下一集加载中...')
  224. setTimeout(() => {
  225. this.$hideLoading()
  226. this.handlePlay()
  227. }, 1000)
  228. }
  229. }
  230. }
  231. },
  232. methods: {
  233. // 播放进度
  234. timeupdate({ detail }) {
  235. // currentTime, duration
  236. if (detail.duration) {
  237. this.progress = (detail.currentTime / detail.duration * 100).toFixed(2)
  238. }
  239. },
  240. // 播放
  241. handlePlay() {
  242. const item = this.episode.lists[this.activeIndex]
  243. this.modal.item = item
  244. // 检查是否购买
  245. if (!this.checkBeforePlay(item)) {
  246. // 余额是否购买
  247. if (!this.checkOverage(item)) {
  248. this.rechargeShow = true
  249. return
  250. }
  251. // 余额足够 直接购买
  252. this.handleBuy()
  253. // this.modal.show = true
  254. // this.footerShow = true
  255. return
  256. }
  257. console.log('-->data', item)
  258. this.isPlaying = true
  259. this.videoContext.play()
  260. // this.videoContext?.seek(125)
  261. this.watched(this.id, this.listId)
  262. },
  263. // 暂停
  264. handlePause() {
  265. if (!this.isPlaying) return
  266. this.isPlaying = false
  267. this.videoContext.pause()
  268. },
  269. // 选择剧集
  270. handleSelectEpisode(index) {
  271. const item = this.episode.lists[index]
  272. this.activeIndex = index
  273. this.modal.item = item
  274. // 检查是否购买
  275. if (!this.checkBeforePlay(item)) {
  276. // 余额是否购买
  277. if (!this.checkOverage(item)) {
  278. this.rechargeShow = true
  279. return
  280. }
  281. // 余额足够 直接购买
  282. this.handleBuy(index)
  283. // this.modal.show = true
  284. return
  285. }
  286. this.footerShow = false
  287. this.$loading()
  288. setTimeout(() => {
  289. this.$hideLoading()
  290. this.isPlaying = true
  291. this.videoContext.play()
  292. this.watched(this.id, this.episode.lists[this.activeIndex].id)
  293. }, 1000)
  294. },
  295. // 获取剧集详情
  296. getEpisode() {
  297. this.loading = true
  298. this.$api.episode.detail(this.id).then(res => {
  299. this.loading = false
  300. this.episode = res.data
  301. if (this.listId) {
  302. this.episode.lists.forEach((obj, index) => {
  303. if (parseInt(this.listId) === parseInt(obj.id)) {
  304. this.activeIndex = index
  305. }
  306. })
  307. } else {
  308. this.listId = this.episode.lists[0].id
  309. }
  310. })
  311. },
  312. // 检查是否收藏当前剧集
  313. checkCollect() {
  314. this.$api.user.collect.check(this.id).then(res => {
  315. this.isCollect = res.data
  316. })
  317. },
  318. // 收藏相关 处理
  319. handleCollect() {
  320. const method = this.isCollect ? 'destroy' : 'add'
  321. const num = this.isCollect ? -1 : 1
  322. this.$api.user.collect[method](this.id).then(res => {
  323. if (res.data) {
  324. this.toast.show = true
  325. this.toast.status = this.isCollect ? 'cancel' : 'success'
  326. this.toast.text = this.isCollect ? '取消收藏' : '收藏成功'
  327. this.episode.user_collect_count += num
  328. this.isCollect = !this.isCollect
  329. }
  330. })
  331. },
  332. // 检查是否喜欢当前短剧
  333. checkFavorite() {
  334. this.$api.user.favorite.check(this.id).then(res => {
  335. this.isFav = res.data
  336. })
  337. },
  338. // 喜欢剧集 处理
  339. handleFavorite() {
  340. const method = this.isFav ? 'destroy' : 'add'
  341. const num = this.isFav ? -1 : 1
  342. this.$api.user.favorite[method](this.id).then(res => {
  343. if (res.data) {
  344. this.toast.show = true
  345. this.toast.status = this.isFav ? 'cancel' : 'success'
  346. this.toast.text = this.isFav ? '取消喜欢' : '喜欢成功'
  347. this.episode.user_favorite_count += num
  348. this.isFav = !this.isFav
  349. }
  350. })
  351. },
  352. // 观看记录
  353. watched(id, list_id) {
  354. this.$api.user.episode.watched(id, list_id).then(res => {
  355. })
  356. },
  357. // 分享
  358. shared(id, list_id) {
  359. this.$api.episode.shared(id, list_id).then(res => {
  360. this.episode.share_count += 1
  361. })
  362. },
  363. // 当前剧集购买记录
  364. async getBuyRecord() {
  365. await this.$api.user.episode.buyRecord(this.id).then(res => {
  366. this.buyRecord = res.data
  367. })
  368. },
  369. // 购买
  370. async handleBuy() {
  371. // this.$loading('购买中...')
  372. await this.$api.user.episode.buyHandle(this.id, this.modal.item.id).then(async res => {
  373. this.$hideLoading()
  374. if (typeof res.overage !== 'undefined') {
  375. this.rechargeShow = true
  376. } else {
  377. this.$u.toast('购买成功')
  378. this.modal.show = false
  379. await this.getBuyRecord()
  380. this.handlePlay()
  381. this.footerShow = false
  382. this.$api.user.info().then(res => {
  383. this.$store.dispatch('user/info', res.data)
  384. })
  385. }
  386. }).catch(() => {
  387. this.$hideLoading()
  388. })
  389. },
  390. // 播放前检查是否购买/免费
  391. checkBeforePlay(item) {
  392. return item.is_free || (!item.is_free && this.buyRecord.indexOf(item.id) !== -1)
  393. },
  394. // 检查余额是否足够支付
  395. checkOverage(item) {
  396. return this.userInfo.info.integral >= item.sale_price
  397. }
  398. },
  399. async onLoad(options) {
  400. this.videoContext = uni.createVideoContext('video', this)
  401. this.id = options.id
  402. this.listId = options?.list_id
  403. await this.getBuyRecord()
  404. this.getEpisode()
  405. this.checkCollect()
  406. this.checkFavorite()
  407. },
  408. // 分享
  409. onShareAppMessage(res) {
  410. if (res.from === 'button') { // 来自页面内分享按钮
  411. console.log(res.target)
  412. }
  413. let options = {
  414. title: '',
  415. path: `/pages/episode/play?id=${this.id}`
  416. }
  417. if (this.episode) {
  418. options = {
  419. title: this.episode.name,
  420. path: `/pages/episode/play?id=${this.id}`,
  421. imageUrl: this.episode.cover_img,
  422. desc: this.episode.name,
  423. success: res => {
  424. this.shared(this.id, this.listId)
  425. }
  426. }
  427. }
  428. return options
  429. }
  430. }
  431. </script>
  432. <style lang="scss" scoped>
  433. .play-container {
  434. font-size: 28rpx;
  435. .video-box{
  436. position: fixed;
  437. top: 0;
  438. left: 0;
  439. right: 0;
  440. bottom: 0;
  441. .play-layer{
  442. position: fixed;
  443. top: 0;
  444. left: 0;
  445. bottom: 0;
  446. right: 0;
  447. background: transparent;
  448. z-index: 999;
  449. }
  450. .pause-layer{
  451. position: absolute;
  452. top: 0;
  453. left: 0;
  454. bottom: 0;
  455. right: 0;
  456. background: transparent;
  457. z-index: 99;
  458. }
  459. video{
  460. position: absolute;
  461. z-index: 98;
  462. }
  463. .progress-container{
  464. width: 93vw;
  465. background: #fff;
  466. height: 10rpx;
  467. position: fixed;
  468. z-index: 100;
  469. bottom: 160rpx;
  470. .progress{
  471. height: 10rpx;
  472. background: linear-gradient(270deg, #6EEBE8 0%, #FF74B9 100%);
  473. }
  474. }
  475. }
  476. .view-num{
  477. position: fixed;
  478. right: 20rpx;
  479. top: 40rpx;
  480. color: #fff;
  481. font-size: 26rpx;
  482. z-index: 100;
  483. text{
  484. margin-left: 10rpx;
  485. }
  486. }
  487. .status-bar{
  488. position: fixed;
  489. bottom: 300rpx;
  490. right: 40rpx;
  491. color: $default-color;
  492. z-index: 100;
  493. .item{
  494. margin-bottom: 40rpx;
  495. &.share{
  496. position: relative;
  497. button{
  498. position: absolute;
  499. background: transparent;
  500. top: 0;
  501. left: 0;
  502. right: 0;
  503. bottom: 0;
  504. z-index: 1;
  505. &:after{
  506. content: unset;
  507. }
  508. }
  509. }
  510. &.active{
  511. color: $primary-color;
  512. }
  513. text{
  514. margin-top: 10rpx;
  515. }
  516. }
  517. }
  518. .footer{
  519. position: fixed;
  520. width: 95vw;
  521. height: 76rpx;
  522. background: rgba(24, 28, 47, 0.8);
  523. bottom: 50rpx;
  524. left: 50%;
  525. transform: translateX(-50%);
  526. font-size: 26rpx;
  527. color: $info-color;
  528. border-radius: 20rpx;
  529. z-index: 999;
  530. transition: .3s;
  531. &.episode{
  532. bottom: 700rpx;
  533. margin-top: -4rpx;
  534. border-bottom-left-radius: 0;
  535. border-bottom-right-radius: 0;
  536. .episode-container{
  537. display: flex;
  538. margin-top: -4rpx;
  539. border-bottom-left-radius: 20px;
  540. border-bottom-right-radius: 20px;
  541. }
  542. .bar{
  543. .arrow{
  544. transform: rotate(180deg);
  545. }
  546. }
  547. }
  548. .bar{
  549. padding: 0 20rpx;
  550. .icon{
  551. background: url("/static/image/video.png") no-repeat center;
  552. background-size: 70%;
  553. width: 80rpx;
  554. height: 80rpx;
  555. }
  556. .name{
  557. text-align: left;
  558. flex: 1;
  559. padding: 0 30rpx;
  560. }
  561. .arrow{
  562. transition: .3s;
  563. }
  564. }
  565. .episode-container{
  566. display: none;
  567. background: inherit;
  568. min-height: 620rpx;
  569. .header-box{
  570. white-space: nowrap;
  571. margin: 20rpx 0;
  572. .header-item{
  573. margin-right: 20rpx;
  574. border-radius: 20rpx;
  575. display: inline-block;
  576. width: 200rpx;
  577. border: 1rpx solid $default-color;
  578. text-align: center;
  579. padding: 10rpx 0;
  580. color: $default-color;
  581. &.active{
  582. border-color: $primary-color;
  583. color: $primary-color;
  584. }
  585. }
  586. }
  587. .content{
  588. margin-top: 20rpx;
  589. .episode-item{
  590. position: relative;
  591. width: calc((100% - #{40rpx}) / 3);
  592. margin-right: 20rpx;
  593. margin-bottom: 20rpx;
  594. overflow: hidden;
  595. border-radius: 18rpx;
  596. &:nth-child(3n){
  597. margin-right: 0;
  598. }
  599. .playing{
  600. position: absolute;
  601. top: 0;
  602. left: 0;
  603. bottom: 0;
  604. right: 0;
  605. background: rgba(0,0,0,.5) url("/static/image/playing.png") no-repeat center;
  606. background-size: 40rpx;
  607. z-index: 2;
  608. }
  609. image{
  610. width: 100%;
  611. height: 260rpx;
  612. }
  613. text{
  614. position: absolute;
  615. left: 0;
  616. bottom: 0;
  617. right: 0;
  618. color: $default-color;
  619. padding: 20rpx 0;
  620. text-align: center;
  621. background: rgba(0,0,0,.3);
  622. z-index: 1;
  623. }
  624. .lock{
  625. position: absolute;
  626. top: 0;
  627. left: 0;
  628. bottom: 0;
  629. right: 0;
  630. background: rgba(0,0,0,.5);
  631. z-index: 2;
  632. }
  633. }
  634. }
  635. }
  636. }
  637. .toast{
  638. position: fixed;
  639. width: 60vw;
  640. background: rgba(0,0,0,.5);
  641. height: 300rpx;
  642. top: 50%;
  643. left: 50%;
  644. transform: translate(-50%,-50%);
  645. border-radius: 20rpx;
  646. font-size: 36rpx;
  647. color: $default-color;
  648. &.success{
  649. color: $primary-color;
  650. }
  651. text{
  652. margin-top: 20rpx;
  653. }
  654. }
  655. }
  656. </style>