play.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  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. <episode-buttons
  12. :episode="episode"
  13. :current-episode="currentEpisode"
  14. @change="handleCollectAndFavChange"
  15. />
  16. <!--视频播放-->
  17. <view class="video-box main-center cross-center" :style="{zIndex: isPlaying ? 0 : 998}">
  18. <!--视频容器-->
  19. <swiper
  20. class="swiper"
  21. circular
  22. :vertical="true"
  23. :current="swiperCurrent"
  24. @change="handleSwiperChancge"
  25. @animationfinish="handleSwiperAnimationFinish"
  26. >
  27. <swiper-item
  28. v-for="(item, index) in swiperEpisode"
  29. :key="index"
  30. class="swiper-item"
  31. >
  32. <!-- #ifdef MP-TOUTIAO | MP-WEIXIN-->
  33. <!-- 控制按钮 - 播放 -->
  34. <view v-if="!item.isPlaying" class="play-layer main-center cross-center" @tap="handlePlay(item)">
  35. <view class="icon"> <u-icon name="play-right-fill" size="100rpx" :color="$colors.defaultColor" /></view>
  36. </view>
  37. <!-- 控制按钮 - 暂停 -->
  38. <view v-if="item.isPlaying" class="pause-layer" @tap="handlePause(item, true)" />
  39. <!-- #endif-->
  40. <video
  41. v-if="Object.keys(item).length"
  42. :id="`video${index}`"
  43. :poster="episode.cover_img"
  44. :src="item.url"
  45. :style="{
  46. width:'100%',
  47. height: 'calc(100vh - 130rpx)',
  48. zIndex: 0
  49. }"
  50. :show-play-btn="video.playBtn"
  51. :show-center-play-btn="video.playBtn"
  52. :show-fullscreen-btn="video.fullscreenBtn"
  53. :controls="video.controls"
  54. :show-progress="video.progress"
  55. object-fit="contain"
  56. @timeupdate="timeupdate"
  57. @ended="ended"
  58. @play="play($event,item)"
  59. />
  60. </swiper-item>
  61. </swiper>
  62. </view>
  63. <!--底部-->
  64. <episode-part
  65. :episode="episode"
  66. :is-playing="isPlaying"
  67. :buy-record="buyRecord"
  68. :current-episode="currentEpisode"
  69. @selectEpisode="handleSelectEpisode"
  70. />
  71. <!--充值-->
  72. <recharge
  73. :show.sync="rechargeShow"
  74. type="play"
  75. mode="bottom"
  76. :episode="episode"
  77. :list="currentEpisode"
  78. />
  79. </template>
  80. </view>
  81. </template>
  82. <script>
  83. import { mapState } from 'vuex'
  84. import Recharge from '../../components/Recharge/index'
  85. import EpisodeButtons from './components/EpisodeButtons'
  86. import EpisodePart from './components/EpisodePart'
  87. export default {
  88. name: 'Play',
  89. components: { EpisodePart, EpisodeButtons, Recharge },
  90. data() {
  91. return {
  92. id: null, // 短剧ID
  93. listId: null, // 剧集ID
  94. isPlaying: false, // 是否播放
  95. progress: 0, // 进度条
  96. episode: {}, // 短剧信息
  97. lists: [], // 剧集信息
  98. loading: false, // 数据加载
  99. video: { // 视频配置
  100. controls: true,
  101. progress: true,
  102. fullscreenBtn: false,
  103. playBtn: false,
  104. // #ifdef MP-KUAISHOU | MP-TOUTIAO
  105. duration: 450,
  106. // #endif
  107. // #ifdef MP-WEIXIN
  108. duration: 500
  109. // #endif
  110. },
  111. buyRecord: [], // 购买记录
  112. rechargeShow: false, // 显示充值
  113. swiperCurrent: 1, // 当前滚动
  114. currentEpisode: {}, // 当前播放剧集
  115. swiperEpisode: { // swiper 剧集
  116. prev: {},
  117. current: {},
  118. next: {}
  119. },
  120. indexArr: {
  121. 'prev': 0,
  122. 'current': 1,
  123. 'next': 2
  124. }
  125. }
  126. },
  127. computed: {
  128. ...mapState({
  129. userInfo: seate => seate.user.info
  130. }),
  131. videoContext() {
  132. const indexArr = ['prev', 'current', 'next']
  133. const swiperKey = indexArr[this.swiperCurrent]
  134. return uni.createVideoContext(`video${swiperKey}`, this)
  135. }
  136. },
  137. watch: {
  138. // 进度条
  139. progress(val) {
  140. if (val >= 100) {
  141. const indexArr = ['prev', 'current', 'next']
  142. const len = this.lists.length
  143. const swiperKey = indexArr[this.swiperCurrent]
  144. if (this.swiperEpisode[swiperKey].sort === this.lists[len - 1].sort) {
  145. this.$u.toast('已全部播放完成')
  146. return 100
  147. }
  148. // 切换
  149. switch (this.swiperCurrent) {
  150. case 0:
  151. this.swiperCurrent = 1
  152. break
  153. case 1:
  154. this.swiperCurrent = 2
  155. break
  156. case 2:
  157. this.swiperCurrent = 0
  158. break
  159. }
  160. this.$forceUpdate()
  161. }
  162. },
  163. swiperCurrent(val) {
  164. const indexArr = ['prev', 'current', 'next']
  165. const swiperKey = indexArr[val]
  166. const dataIndex = this.lists.findIndex(val => {
  167. return this.swiperEpisode[swiperKey].sort === val.sort
  168. })
  169. const len = this.lists.length
  170. let prevIndex, currentIndex, nextIndex
  171. switch (swiperKey) {
  172. case 'prev':
  173. currentIndex = dataIndex + 1 >= len ? 0 : dataIndex + 1
  174. nextIndex = dataIndex - 1 < 0 ? len - 1 : dataIndex - 1
  175. this.swiperEpisode.current = this.lists[currentIndex]
  176. this.swiperEpisode.next = this.lists[nextIndex]
  177. break
  178. case 'current':
  179. prevIndex = dataIndex - 1 < 0 ? len - 1 : dataIndex - 1
  180. nextIndex = dataIndex + 1 >= len ? 0 : dataIndex + 1
  181. this.swiperEpisode.prev = this.lists[prevIndex]
  182. this.swiperEpisode.next = this.lists[nextIndex]
  183. break
  184. case 'next':
  185. prevIndex = dataIndex + 1 >= len ? 0 : dataIndex + 1
  186. currentIndex = dataIndex - 1 < 0 ? len - 1 : dataIndex - 1
  187. this.swiperEpisode.current = this.lists[currentIndex]
  188. this.swiperEpisode.prev = this.lists[prevIndex]
  189. break
  190. }
  191. // 暂停其他
  192. this.handlePause(this.currentEpisode, false)
  193. // 当前播放剧集
  194. this.currentEpisode = this.swiperEpisode[swiperKey]
  195. // 播放
  196. this.handlePlay(this.currentEpisode)
  197. },
  198. currentEpisode(val) {
  199. console.log('-->当前剧集', val.sort)
  200. }
  201. },
  202. methods: {
  203. // 播放进度
  204. timeupdate({ detail }) {
  205. // currentTime, duration
  206. if (detail.duration) {
  207. this.progress = (detail.currentTime / detail.duration * 100).toFixed(2)
  208. }
  209. },
  210. ended() {
  211. // #ifdef MP-KUAISHOU
  212. this.progress = 100
  213. // #endif
  214. },
  215. play(e, item) {
  216. console.log('-->data', e, item)
  217. // #ifdef MP-KUAISHOU
  218. this.handlePlay(item)
  219. // #endif
  220. },
  221. // 播放
  222. handlePlay(item) {
  223. this.currentEpisode = item
  224. // 检查是否购买
  225. if (!this.checkBeforePlay(item)) {
  226. // 余额是否购买
  227. if (!this.checkOverage(item)) {
  228. // #ifdef MP-KUAISHOU
  229. // 如果没有购买,那把视频链接设置为空 停止播放,使用 pause 和 stop 在真机上不生效 只能采用这种方式
  230. item.url = ''
  231. // #endif
  232. this.rechargeShow = true
  233. return
  234. }
  235. // 余额足够 直接购买
  236. this.handleBuy()
  237. return
  238. }
  239. this.isPlaying = true
  240. item.isPlaying = true
  241. // #ifdef MP-KUAISHOU
  242. item.url = item.src
  243. this.$forceUpdate()
  244. setTimeout(() => {
  245. this.videoContext.play()
  246. }, 1000)
  247. // #endif
  248. // #ifdef MP-TOUTIAO | MP-WEIXIN
  249. this.videoContext.play()
  250. // #endif
  251. this.progress = 0
  252. this.watched(this.id, this.currentEpisode.id)
  253. },
  254. // 暂停
  255. handlePause(item, isAll = false) {
  256. if (!this.isPlaying) return
  257. item.isPlaying = false
  258. this.isPlaying = false
  259. // 展厅其他的
  260. const indexArr = ['prev', 'current', 'next']
  261. const swiperKey = indexArr[this.swiperCurrent]
  262. indexArr.forEach(obj => {
  263. if (swiperKey !== obj || isAll) {
  264. const videoContext = uni.createVideoContext(`video${obj}`, this)
  265. videoContext.pause()
  266. }
  267. })
  268. },
  269. // 选择剧集
  270. handleSelectEpisode(index) {
  271. // 暂停上一个
  272. this.handlePause(this.currentEpisode, true)
  273. const item = this.lists[index]
  274. // 重置SwiperEpisode数据 切换播放
  275. this.swiperCurrent = 1
  276. this.initSwiperEpisode(item.id)
  277. },
  278. // 当前剧集购买记录
  279. async getBuyRecord() {
  280. await this.$api.user.episode.buyRecord(this.id).then(res => {
  281. this.buyRecord = res.data
  282. })
  283. },
  284. // 购买剧集
  285. async handleBuy() {
  286. await this.$api.user.episode.buyHandle(this.id, this.currentEpisode.id).then(async res => {
  287. this.$hideLoading()
  288. if (typeof res.data.overage !== 'undefined') {
  289. this.rechargeShow = true
  290. } else {
  291. this.$u.toast('购买成功')
  292. await this.getBuyRecord()
  293. this.handlePlay(this.currentEpisode)
  294. this.$api.user.info().then(res => {
  295. this.$store.dispatch('user/info', res.data)
  296. })
  297. }
  298. }).catch(() => {
  299. this.$hideLoading()
  300. })
  301. },
  302. // 滚动 Swiper
  303. handleSwiperChancge({ detail }) {
  304. // this.swiperCurrent = detail.current
  305. },
  306. handleSwiperAnimationFinish({ detail }) {
  307. this.swiperCurrent = detail.current
  308. },
  309. // 播放前检查剧集是否购买/免费
  310. checkBeforePlay(item) {
  311. // 剧集免费 不免费已购买 VIP观看是VIP
  312. if (item.is_free) {
  313. return true
  314. }
  315. if (!item.is_free && this.buyRecord.indexOf(item.id) !== -1) {
  316. return true
  317. }
  318. if (this.episode.is_vip_watch && this.userInfo.info.is_vip) {
  319. return true
  320. }
  321. return false
  322. },
  323. // 检查余额是否足够支付
  324. checkOverage(item) {
  325. return this.userInfo.info.integral >= item.sale_price
  326. },
  327. // 记录观看记录
  328. watched(id, list_id) {
  329. this.$api.user.episode.watched(id, list_id).then(res => {
  330. })
  331. },
  332. // 分享
  333. handleShared(id, list_id) {
  334. this.$api.episode.shared(id, list_id).then(res => {
  335. this.episode.share_count += 1
  336. })
  337. },
  338. handleCollectAndFavChange(data) {
  339. if (data.type === 'collect') {
  340. this.episode.user_collect_count += data.num
  341. } else {
  342. // this.episode.user_favorite_count += data.num
  343. }
  344. },
  345. // // 初始化 Swiper 剧集
  346. initSwiperEpisode(listId) {
  347. let currentIndex = 0
  348. if (listId) {
  349. currentIndex = this.lists.findIndex(obj => {
  350. return parseInt(listId) === parseInt(obj.id)
  351. })
  352. }
  353. let prevIndex = currentIndex - 1
  354. let nextIndex = currentIndex + 1
  355. const len = this.lists.length
  356. if (parseInt(listId) === 0 || prevIndex < 0) {
  357. prevIndex = len - 1
  358. }
  359. //
  360. if (nextIndex >= len) {
  361. nextIndex = 0
  362. }
  363. this.swiperEpisode = {
  364. prev: this.lists[prevIndex],
  365. current: this.lists[currentIndex],
  366. next: this.lists[nextIndex]
  367. }
  368. console.log('-->swiper data', JSON.stringify(this.swiperEpisode))
  369. this.currentEpisode = this.lists[currentIndex]
  370. console.log('-->currentEpisode', this.currentEpisode)
  371. this.$nextTick(() => {
  372. this.handlePlay(this.currentEpisode)
  373. })
  374. },
  375. // 获取剧集详情
  376. getEpisode() {
  377. this.loading = true
  378. this.$api.episode.detail(this.id).then(res => {
  379. this.loading = false
  380. this.episode = res.data
  381. uni.setNavigationBarTitle({
  382. title: this.episode.name + (this.episode.status === 0 ? ' | 更新中' : '已完结')
  383. })
  384. this.episode.lists.forEach((obj, index) => {
  385. obj.isPlaying = false
  386. obj.index = index
  387. obj.progress = 0
  388. obj.src = obj.url
  389. })
  390. this.lists = this.episode.lists
  391. // 初始化 Swiper 剧集
  392. this.initSwiperEpisode(this.listId)
  393. })
  394. }
  395. },
  396. async onLoad(options) {
  397. this.id = options.id
  398. this.listId = options?.list_id
  399. this.listId = this.listId ? this.listId : 0
  400. await this.getBuyRecord()
  401. this.getEpisode()
  402. },
  403. // 分享
  404. onShareAppMessage(res) {
  405. if (res.from === 'button') { // 来自页面内分享按钮
  406. console.log(res.target)
  407. }
  408. let options = {
  409. title: '',
  410. path: `/pages/episode/play?id=${this.id}`
  411. }
  412. if (this.episode) {
  413. options = {
  414. title: this.episode.name,
  415. path: `/pages/episode/play?id=${this.id}`,
  416. imageUrl: this.episode.cover_img,
  417. desc: this.episode.name,
  418. success: res => {
  419. this.handleShared(this.id, this.currentEpisode.id)
  420. }
  421. }
  422. }
  423. return options
  424. }
  425. }
  426. </script>
  427. <style lang="scss" scoped>
  428. .play-container {
  429. font-size: 28rpx;
  430. .video-box{
  431. position: fixed;
  432. top: 0;
  433. left: 0;
  434. right: 0;
  435. bottom: 0;
  436. .play-layer{
  437. position: fixed;
  438. top: 0;
  439. left: 0;
  440. // #ifdef MP-KUAISHOU | MP-WEIXIN
  441. bottom: 240rpx;
  442. // #endif
  443. // #ifdef MP-TOUTIAO
  444. bottom: 360rpx;
  445. // #endif
  446. right: 0;
  447. background: transparent;
  448. z-index: 999;
  449. .icon{
  450. position: absolute;
  451. top: 50%;
  452. transform: translate(2px,63%);
  453. }
  454. }
  455. .pause-layer{
  456. position: absolute;
  457. top: 0;
  458. left: 0;
  459. // #ifdef MP-KUAISHOU | MP-WEIXIN
  460. bottom: 240rpx;
  461. // #endif
  462. // #ifdef MP-TOUTIAO
  463. bottom: 360rpx;
  464. // #endif
  465. right: 0;
  466. background: transparent;
  467. z-index: 99;
  468. }
  469. .swiper {
  470. width: 100%;
  471. height: 100vh;
  472. position: relative;
  473. z-index: 99;
  474. .swiper-item {
  475. width: 100%;
  476. height: 100vh !important;
  477. }
  478. }
  479. .progress-container{
  480. width: 93vw;
  481. background: #fff;
  482. height: 10rpx;
  483. position: fixed;
  484. z-index: 100;
  485. bottom: 160rpx;
  486. .progress{
  487. height: 10rpx;
  488. background: linear-gradient(270deg, #6EEBE8 0%, #FF74B9 100%);
  489. }
  490. }
  491. }
  492. }
  493. </style>