play.vue 15 KB

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