play.vue 21 KB

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