play.vue 14 KB

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