play.bak.vue 19 KB

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