play.vue 15 KB

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