Browse Source

feat: 二期

xiansin 2 năm trước cách đây
mục cha
commit
5452d1ac0d

+ 76 - 41
mini/components/Recharge/index.vue

xqd xqd xqd xqd xqd xqd
@@ -19,7 +19,7 @@
                 <view class="name">{{ episode.name }} 第{{ list.sort }}集</view>
                 <view class="sale-box main-left cross-center">
                   <view class="sale-price cross-center">专享价{{ list.sale_price }}金币</view>
-                  <view class="origin-price cross-center">原价{{ list.origin_price }}金币</view>
+                  <view v-if="list.origin_price" class="origin-price cross-center">原价{{ list.origin_price }}金币</view>
                 </view>
               </view>
               <view class="buy-num">{{ buyNum }}人购买</view>
@@ -38,19 +38,26 @@
           <view class="overage">账户余额:<text>{{ userInfo.info.integral }}金币</text></view>
         </template>
 
-        <view class="recharge-group dir-left-wrap">
-          <view
-            v-for="(combo,index) in combos"
-            :key="index"
-            class="recharge-item dir-top-wrap main-center cross-center"
-            :class="{active: rechargeActive === index}"
-            @click="rechargeActive = index"
-          >
-            <text class="price">{{ combo.price }}元</text>
-            <text class="gold">{{ combo.gold }}+{{ combo.gift }}金币</text>
-            <text class="gift">多送{{ combo.gift }}金币</text>
+        <scroll-view
+          class="recharge-group-view dir-left-wrap"
+          scroll-y
+          scroll-with-animation
+          :enable-flex="true"
+        >
+          <view class="recharge-group dir-left-wrap">
+            <view
+              v-for="(combo,index) in combos"
+              :key="index"
+              class="recharge-item dir-top-wrap main-center cross-center"
+              :class="{active: rechargeActive === index}"
+              @click="rechargeActive = index"
+            >
+              <text class="price">{{ combo.price }}元</text>
+              <text class="gold">{{ combo.gold }}+{{ combo.gift }}金币</text>
+              <text class="gift">多送{{ combo.gift }}金币</text>
+            </view>
           </view>
-        </view>
+        </scroll-view>
 
         <view class="btn" @click="handleRecharge">充值</view>
       </view>
@@ -156,6 +163,7 @@ export default {
           },
           fail: err => {
             console.log('-->data', err)
+            clearInterval(this.interval)
             // 调起收银台失败处理逻辑
           }
         })
@@ -173,10 +181,28 @@ export default {
           },
           fail: err => {
             console.log('-->data', err)
+            clearInterval(this.interval)
             // 调起收银台失败处理逻辑
           }
         })
         // #endif
+        // #ifdef MP-WEIXIN
+        uni.requestPayment({
+          ...res.data,
+          provider: 'wxpay',
+          success: res => {
+            console.log('success:' + JSON.stringify(res))
+            // _this.$u.toast("支付成功")
+            this.$loading('支付结果查询中...')
+            this.query()
+          },
+          fail(err) {
+            console.log('fail:' + JSON.stringify(err))
+            // _this.$u.toast("支付失败")
+            clearInterval(this.interval)
+          }
+        })
+        // #endif
         this.$hideLoading()
       }).catch(() => {
         this.$hideLoading()
@@ -212,6 +238,11 @@ export default {
         &.bottom{
           width: 750rpx;
           border: unset;
+          .recharge-group{
+            .recharge-item{
+              width: calc(#{670rpx} / 2) !important;
+            }
+          }
         }
         .static-text{
           font-size: 36rpx;
@@ -265,36 +296,40 @@ export default {
           }
         }
         // 充值套餐
-        .recharge-group{
-          margin-top: 30rpx;
-          .recharge-item{
-            border: 4rpx solid $primary-color;
-            width: calc(#{600rpx} / 2);
-            margin-right: 20rpx;
-            margin-bottom: 20rpx;
-            border-radius: 20rpx;
-            padding: 40rpx 20rpx;
-            transition: .3s;
-            &:nth-child(2n){
-              margin-right: 0;
-            }
-            &.active{
-              background: #1b1e32;
-              border: 4rpx solid #1b1e32;
+        .recharge-group-view{
+          .recharge-group{
+            margin-top: 30rpx;
+            height: 50vh;
+            display: flex;
+            .recharge-item{
+              border: 4rpx solid $primary-color;
+              width: calc(#{600rpx} / 2);
+              margin-right: 20rpx;
+              margin-bottom: 20rpx;
+              border-radius: 20rpx;
+              padding: 40rpx 20rpx;
+              transition: .3s;
+              &:nth-child(2n){
+                margin-right: 0;
+              }
+              &.active{
+                background: #1b1e32;
+                border: 4rpx solid #1b1e32;
+                .price{
+                  color: $default-color;
+                }
+              }
               .price{
-                color: $default-color;
+                margin-bottom: 40rpx;
+                font-size: 38rpx;
+              }
+              .gold{
+                color: $primary-color;
+                margin-bottom: 10rpx;
+              }
+              .gift{
+                color: $info-color;
               }
-            }
-            .price{
-              margin-bottom: 40rpx;
-              font-size: 38rpx;
-            }
-            .gold{
-              color: $primary-color;
-              margin-bottom: 10rpx;
-            }
-            .gift{
-              color: $info-color;
             }
           }
         }

+ 200 - 0
mini/pages/episode/components/EpisodeButtons.vue

xqd
@@ -0,0 +1,200 @@
+<template>
+  <view v-if="Object.keys(episode).length" class="episode-buttons">
+    <!--播放数据-->
+    <view class="view-num main-left cross-center">
+      <u-icon name="eye-fill" :color="isCollect?$colors.primaryColor:$colors.defaultColor" size="26rpx" />
+      <text>{{ episode.user_watch_record_count }}</text>
+    </view>
+    <!--按钮-->
+    <view class="status-bar dir-top-wrap main-center">
+      <!--喜欢-->
+      <view
+        class="item fav dir-top-wrap main-center cross-center"
+        :class="{active: isFav}"
+        @click="handleFavorite"
+      >
+        <u-icon name="heart-fill" size="58rpx" :color="isFav?$colors.primaryColor:$colors.defaultColor" />
+        <text>{{ episode.user_favorite_count }}</text>
+      </view>
+      <!--收藏-->
+      <view
+        class="item collect dir-top-wrap main-center cross-center"
+        :class="{active: isCollect}"
+        @click="handleCollect"
+      >
+        <u-icon name="star-fill" size="58rpx" :color="isCollect?$colors.primaryColor:$colors.defaultColor" />
+        <text>{{ episode.user_collect_count }}</text>
+      </view>
+      <!--分享-->
+      <view class="item share dir-top-wrap main-center cross-center">
+        <button open-type="share" />
+        <u-icon name="share-fill" size="58rpx" :color="$colors.defaultColor" />
+        <text>{{ episode.sahre_count }}</text>
+      </view>
+    </view>
+    <!--toast-->
+    <view
+      v-if="toast.show"
+      class="toast dir-top-wrap main-center cross-center"
+      :class="toast.status"
+    >
+      <u-icon
+        :name="toast.status === 'success' ? 'checkmark-circle' : 'close-circle'"
+        :color="toast.status === 'success' ? $colors.primaryColor : $colors.defaultColor"
+        size="80rpx"
+      />
+      <text>{{ toast.text }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'EpisodeButtons',
+  props: {
+    episode: {
+      type: Object,
+      required: true,
+      default() {
+        return {}
+      }
+    }
+  },
+  data() {
+    return {
+      isCollect: false,
+      isFav: false,
+      toast: { // 收藏/喜欢 toast
+        status: 'success',
+        text: '收藏成功',
+        show: false
+      }
+    }
+  },
+  watch: {
+    'toast.show'(val) {
+      if (val) {
+        setTimeout(() => {
+          this.toast.show = false
+        }, 1000)
+      }
+    }
+  },
+  created() {
+    if (Object.keys(this.episode).length) {
+      this.checkCollect()
+      this.checkFavorite()
+    }
+  },
+  methods: {
+    // 检查是否收藏当前剧集
+    checkCollect() {
+      this.$api.user.collect.check(this.episode.id).then(res => {
+        this.isCollect = res.data
+      })
+    },
+    // 收藏相关 处理
+    handleCollect() {
+      const method = this.isCollect ? 'destroy' : 'add'
+      const num = this.isCollect ? -1 : 1
+      this.$api.user.collect[method](this.episode.id).then(res => {
+        if (res.data) {
+          this.toast.show = true
+          this.toast.status = this.isCollect ? 'cancel' : 'success'
+          this.toast.text = this.isCollect ? '取消收藏' : '收藏成功'
+          this.isCollect = !this.isCollect
+          this.$emit('change', { type: 'collect', num: num })
+        }
+      })
+    },
+    // 检查是否喜欢当前短剧
+    checkFavorite() {
+      this.$api.user.favorite.check(this.episode.id).then(res => {
+        this.isFav = res.data
+      })
+    },
+    // 处理 喜欢剧集
+    handleFavorite() {
+      const method = this.isFav ? 'destroy' : 'add'
+      const num = this.isFav ? -1 : 1
+      this.$api.user.favorite[method](this.episode.id).then(res => {
+        if (res.data) {
+          this.toast.show = true
+          this.toast.status = this.isFav ? 'cancel' : 'success'
+          this.toast.text = this.isFav ? '取消喜欢' : '喜欢成功'
+          this.$emit('change', { type: 'fav', num: num })
+          this.isFav = !this.isFav
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.episode-buttons {
+  .view-num{
+    position: fixed;
+    right: 20rpx;
+    top: 40rpx;
+    color: #fff;
+    font-size: 26rpx;
+    z-index: 100;
+    text{
+      margin-left: 10rpx;
+    }
+  }
+
+  .status-bar{
+    position: fixed;
+    bottom: 300rpx;
+    right: 40rpx;
+    color: $default-color;
+    z-index: 999;
+    .item{
+      margin-bottom: 40rpx;
+      &.share{
+        position: relative;
+        button{
+          position: absolute;
+          background: transparent;
+          top: 0;
+          left: 0;
+          right: 0;
+          bottom: 0;
+          z-index: 1;
+          &:after{
+            content: unset;
+          }
+        }
+      }
+      &.active{
+        color: $primary-color;
+      }
+      text{
+        margin-top: 10rpx;
+      }
+    }
+  }
+
+  .toast{
+    position: fixed;
+    width: 60vw;
+    background: rgba(0,0,0,.5);
+    height: 300rpx;
+    top: 30%;
+    left: 50%;
+    transform: translate(-50%,-50%);
+    border-radius: 20rpx;
+    font-size: 36rpx;
+    color: $default-color;
+    z-index: 999;
+    &.success{
+      color: $primary-color;
+    }
+    text{
+      margin-top: 20rpx;
+    }
+  }
+}
+</style>

+ 250 - 0
mini/pages/episode/components/EpisodePart.vue

xqd
@@ -0,0 +1,250 @@
+<template>
+  <view v-if="Object.keys(episode).length" class="episode-part">
+    <view class="footer" :class="{episode: footerShow}">
+      <view class="bar main-between cross-center" @click="footerShow = !footerShow">
+        <view class="icon" />
+        <view class="name">
+          <u-text :text="episode.name" :color="$colors.infoColor" :lines="1" />
+        </view>
+        <view class="arrow">
+          <u-icon name="arrow-up" :color="$colors.infoColor" size="32rpx" />
+        </view>
+      </view>
+      <view class="episode-container dir-top-wrap">
+        <!--分集 横向滚动-->
+        <scroll-view
+          class="header-box dir-left-nowrap cross-center"
+          scroll-x
+          scroll-with-animation
+        >
+          <view
+            v-for="(item,index) in episodeTabData"
+            :key="index"
+            class="header-item"
+            :class="{active: episodesTabIndex === index}"
+            @click="episodesTabIndex = index"
+          >{{ item.title }}</view>
+        </scroll-view>
+        <!--几集选择-->
+        <view class="content dir-left-wrap main-left">
+          <view
+            v-for="(item, index) in episodeTabData[episodesTabIndex].lists"
+            :key="index"
+            class="episode-item"
+            @click="handleSelectEpisode(item.index)"
+          >
+            <image :src="episode.cover_img" />
+            <text>第{{ item.sort }}集</text>
+            <view v-if="currentEpisode.sort === item.sort && isPlaying" class="playing" />
+            <view
+              v-if="!item.is_free && buyRecord.indexOf(item.id) === -1 && !(episode.is_vip_watch && userInfo.info.is_vip)"
+              class="lock main-center cross-center"
+            >
+              <u-icon name="lock-fill" :color="$colors.defaultColor" size="46rpx" />
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+export default {
+  name: 'EpisodePart',
+  props: {
+    episode: {
+      type: Object,
+      required: true,
+      default() {
+        return {}
+      }
+    },
+    currentEpisode: {
+      type: Object,
+      required: true
+    },
+    isPlaying: {
+      type: Boolean,
+      required: true
+    },
+    buyRecord: {
+      type: Array,
+      required: true
+    }
+  },
+  data() {
+    return {
+      footerShow: false, // 底部是否弹出
+      episodesTabIndex: 0 // 分集 Tab 选中
+    }
+  },
+  computed: {
+    ...mapState({
+      userInfo: seate => seate.user.info
+    }),
+    episodeTabData() {
+      const list = []
+      if (Object.keys(this.episode).length) {
+        let temp = []
+        this.episode.lists.forEach((obj, index) => {
+          temp.push(obj)
+          if (temp.length === 6 || index === (this.episode.lists.length - 1)) {
+            const start = list.length ? (list.length * 6) + 1 : 1
+            const end = (start - 1) + temp.length
+            list.push({ title: `${start}集-${end}集`, lists: temp })
+            temp = []
+          }
+        })
+      }
+      return list
+    }
+  },
+  watch: {
+    currentEpisode() {
+      const index = this.episode.lists.findIndex(obj => {
+        return obj.sort === this.currentEpisode.sort
+      })
+      console.log('-->data', index)
+    }
+  },
+  methods: {
+    handleSelectEpisode(index) {
+      this.$emit('selectEpisode', index)
+      this.footerShow = false
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.episode-part {
+  .footer{
+    position: fixed;
+    width: 95vw;
+    height: 76rpx;
+    background: rgba(24, 28, 47, 0.8);
+    // #ifdef  MP-KUAISHOU
+    bottom: 100rpx;
+    // #endif
+    // #ifdef  MP-TOUTIAO | MP-WEIXIN
+    bottom: 100rpx;
+    // #endif
+    left: 50%;
+    transform: translateX(-50%);
+    font-size: 26rpx;
+    color: $info-color;
+    border-radius: 20rpx;
+    z-index: 999;
+    transition: .3s;
+    &.episode{
+      bottom: 700rpx;
+      margin-top: -4rpx;
+      border-bottom-left-radius: 0;
+      border-bottom-right-radius: 0;
+      .episode-container{
+        display: flex;
+        margin-top: -4rpx;
+        border-bottom-left-radius: 20px;
+        border-bottom-right-radius: 20px;
+      }
+      .bar{
+        .arrow{
+          transform: rotate(180deg);
+        }
+      }
+    }
+    .bar{
+      padding: 0 20rpx;
+      .icon{
+        background: url("/static/image/video.png") no-repeat center;
+        background-size: 70%;
+        width: 80rpx;
+        height: 80rpx;
+      }
+      .name{
+        text-align: left;
+        flex: 1;
+        padding: 0 30rpx;
+      }
+      .arrow{
+        transition: .3s;
+      }
+    }
+
+    .episode-container{
+      display: none;
+      background: inherit;
+      min-height: 620rpx;
+      .header-box{
+        white-space: nowrap;
+        margin: 20rpx 0;
+        .header-item{
+          margin-right: 20rpx;
+          border-radius: 20rpx;
+          display: inline-block;
+          width: 200rpx;
+          border: 1rpx solid $default-color;
+          text-align: center;
+          padding: 10rpx 0;
+          color: $default-color;
+          &.active{
+            border-color: $primary-color;
+            color: $primary-color;
+          }
+        }
+      }
+      .content{
+        margin-top: 20rpx;
+        .episode-item{
+          position: relative;
+          width: calc((100% - #{40rpx}) / 3);
+          margin-right: 20rpx;
+          margin-bottom: 20rpx;
+          overflow: hidden;
+          border-radius: 18rpx;
+          &:nth-child(3n){
+            margin-right: 0;
+          }
+          .playing{
+            position: absolute;
+            top: 0;
+            left: 0;
+            bottom: 0;
+            right: 0;
+            background: rgba(0,0,0,.5) url("/static/image/playing.png") no-repeat center;
+            background-size: 40rpx;
+            z-index: 2;
+          }
+          image{
+            width: 100%;
+            height: 260rpx;
+          }
+          text{
+            position: absolute;
+            left: 0;
+            bottom: 0;
+            right: 0;
+            color: $default-color;
+            padding: 20rpx 0;
+            text-align: center;
+            background: rgba(0,0,0,.3);
+            z-index: 1;
+          }
+          .lock{
+            position: absolute;
+            top: 0;
+            left: 0;
+            bottom: 0;
+            right: 0;
+            background: rgba(0,0,0,.5);
+            z-index: 2;
+          }
+        }
+      }
+    }
+
+  }
+}
+</style>

+ 745 - 0
mini/pages/episode/play.bak1.vue

xqd
@@ -0,0 +1,745 @@
+<template>
+  <view class="play-container">
+    <u-loading-page
+      :loading="loading"
+      :bg-color="$colors.bgColor"
+      :color="$colors.primaryColor"
+      :loading-color="$colors.primaryColor"
+    />
+    <template v-if="!loading">
+      <!--播放数据-->
+      <view class="view-num main-left cross-center">
+        <u-icon name="eye-fill" :color="isCollect?$colors.primaryColor:$colors.defaultColor" size="26rpx" />
+        <text>{{ episode.user_watch_record_count }}</text>
+      </view>
+      <!--按钮-->
+      <view class="status-bar dir-top-wrap main-center">
+        <!--喜欢-->
+        <view
+          class="item fav dir-top-wrap main-center cross-center"
+          :class="{active: isFav}"
+          @click="handleFavorite"
+        >
+          <u-icon name="heart-fill" size="58rpx" :color="isFav?$colors.primaryColor:$colors.defaultColor" />
+          <text>{{ episode.user_favorite_count }}</text>
+        </view>
+        <!--收藏-->
+        <view
+          class="item collect dir-top-wrap main-center cross-center"
+          :class="{active: isCollect}"
+          @click="handleCollect"
+        >
+          <u-icon name="star-fill" size="58rpx" :color="isCollect?$colors.primaryColor:$colors.defaultColor" />
+          <text>{{ episode.user_collect_count }}</text>
+        </view>
+        <!--分享-->
+        <view class="item share dir-top-wrap main-center cross-center">
+          <button open-type="share" />
+          <u-icon name="share-fill" size="58rpx" :color="$colors.defaultColor" />
+          <text>{{ episode.share_count }}</text>
+        </view>
+      </view>
+      <!--视频播放-->
+      <view class="video-box main-center cross-center" :style="{zIndex: isPlaying?0:998}">
+        <!--进度条-->
+        <!--        <view v-if="isPlaying" class="progress-container">-->
+        <!--          <view class="progress" :style="{width: progress+'%'}" />-->
+        <!--        </view>-->
+        <!--视频容器-->
+        <swiper
+          class="swiper"
+          circular
+          :vertical="true"
+          :current="swiperCurrent"
+          @change="handleSwiperChancge"
+        >
+          <swiper-item
+            v-for="(item,index) in episode.lists"
+            :key="index"
+          >
+            <!-- #ifdef  MP-TOUTIAO | MP-WEIXIN-->
+            <!-- 控制按钮 - 播放 -->
+            <view v-if="!item.isPlaying" class="play-layer main-center cross-center" @tap="handlePlay(item)">
+              <u-icon name="play-right-fill" size="100rpx" :color="$colors.defaultColor" />
+            </view>
+            <!-- 控制按钮 - 暂停 -->
+            <view v-if="item.isPlaying" class="pause-layer" @tap="handlePause(item)" />
+            <!-- #endif-->
+            <video
+              :id="`video${index}`"
+              :poster="episode.cover_img"
+              :src="item.url"
+              style="width: 100%;height: 100%;"
+              :show-play-btn="video.playBtn"
+              :show-fullscreen-btn="video.fullscreenBtn"
+              :controls="video.controls"
+              :show-progress="video.progress"
+              object-fit="contain"
+              @timeupdate="timeupdate"
+            />
+          </swiper-item>
+        </swiper>
+        <!--        <video-->
+        <!--          id="video"-->
+        <!--          :show-play-btn="video.playBtn"-->
+        <!--          :show-fullscreen-btn="video.fullscreenBtn"-->
+        <!--          :controls="video.controls"-->
+        <!--          object-fit="contain"-->
+        <!--          style="width: 100%;height: 100%;"-->
+        <!--          :poster="episode.cover_img"-->
+        <!--          :src="src"-->
+        <!--          @timeupdate="timeupdate"-->
+        <!--        />-->
+      </view>
+      <!--底部-->
+      <view class="footer" :class="{episode: footerShow}">
+        <view class="bar main-between cross-center" @click="footerShow = !footerShow">
+          <view class="icon" />
+          <view class="name">
+            <u-text :text="episode.name" :color="$colors.infoColor" :lines="1" />
+            <!--            <u-text text="321313" :color="$colors.infoColor" :lines="1" />-->
+          </view>
+          <view class="arrow">
+            <u-icon name="arrow-up" :color="$colors.infoColor" size="32rpx" />
+          </view>
+        </view>
+        <view class="episode-container dir-top-wrap">
+          <!--分集 横向滚动-->
+          <scroll-view
+            class="header-box dir-left-nowrap cross-center"
+            scroll-x
+            scroll-with-animation
+          >
+            <view
+              v-for="(item,index) in episodes"
+              :key="index"
+              class="header-item"
+              :class="{active: episodesIndex === index}"
+              @click="episodesIndex=index"
+            >{{ item.title }}</view>
+          </scroll-view>
+          <!--几集选择-->
+          <view class="content dir-left-wrap main-left">
+            <view
+              v-for="(item, index) in episodes[episodesIndex].lists"
+              :key="index"
+              class="episode-item"
+              @click="handleSelectEpisode(item.index)"
+            >
+              <image :src="episode.cover_img" />
+              <text>第{{ item.sort }}集</text>
+              <view v-if="currentEpisode.index === item.index && isPlaying" class="playing" />
+              <view v-if="!item.is_free && buyRecord.indexOf(item.id) === -1 && !(episode.is_vip_watch && userInfo.info.is_vip)" class="lock main-center cross-center">
+                <u-icon name="lock-fill" :color="$colors.defaultColor" size="46rpx" />
+              </view>
+            </view>
+          </view>
+        </view>
+      </view>
+      <!--toast-->
+      <view
+        v-if="toast.show"
+        class="toast dir-top-wrap main-center cross-center"
+        :class="toast.status"
+      >
+        <u-icon
+          :name="toast.status === 'success' ? 'checkmark-circle' : 'close-circle'"
+          :color="toast.status === 'success' ? $colors.primaryColor : $colors.defaultColor"
+          size="80rpx"
+        />
+        <text>{{ toast.text }}</text>
+      </view>
+      <!--购买弹窗-->
+      <u-modal
+        :show="modal.show"
+        :content="`确定购买【第${currentEpisode.sort}集】?`"
+        show-cancel-button
+        @confirm="handleBuy"
+        @cancel="modal.show = false"
+      />
+      <!--充值-->
+      <recharge
+        :show.sync="rechargeShow"
+        type="play"
+        mode="bottom"
+        :episode="episode"
+        :list="currentEpisode"
+      />
+    </template>
+  </view>
+</template>
+
+<script>
+import { mapState } from 'vuex'
+import Recharge from '../../components/Recharge/index'
+export default {
+  name: 'Play',
+  components: { Recharge },
+  data() {
+    return {
+      id: null,
+      listId: null,
+      isPlaying: false,
+      progress: 0,
+      episode: null,
+      loading: true,
+      isCollect: false,
+      isFav: false,
+      video: {
+        controls: true,
+        // #ifdef  MP-KUAISHOU |  MP-TOUTIAO
+        progress: false,
+        // #endif
+        // #ifdef  MP-TOUTIAO | MP-WEIXIN
+        progress: true,
+        // #endif
+        fullscreenBtn: false,
+        playBtn: false
+
+      },
+      toast: {
+        status: 'success',
+        text: '收藏成功',
+        show: false
+      },
+      footerShow: false,
+      episodesIndex: 0,
+      buyRecord: [],
+      modal: {
+        show: false
+      },
+      rechargeShow: false,
+      swiperCurrent: 0,
+      currentEpisode: null
+    }
+  },
+  computed: {
+    ...mapState({
+      userInfo: seate => seate.user.info
+    }),
+    src() {
+      if (!this.episode) return ''
+      return this.currentEpisode.url
+    },
+    episodes() {
+      const list = []
+      if (this.episode) {
+        let temp = []
+        this.episode.lists.forEach((obj, index) => {
+          temp.push(obj)
+          if (temp.length === 6 || index === (this.episode.lists.length - 1)) {
+            const start = list.length ? (list.length * 6) + 1 : 1
+            const end = (start - 1) + temp.length
+            list.push({ title: `${start}集-${end}集`, lists: temp })
+            temp = []
+          }
+        })
+      }
+      return list
+    },
+    videoContext() {
+      return uni.createVideoContext(`video${this.swiperCurrent}`, this)
+    }
+  },
+  watch: {
+    'toast.show'(val) {
+      if (val) {
+        setTimeout(() => {
+          this.toast.show = false
+        }, 1000)
+      }
+    },
+    progress(val) {
+      if (val >= 100) {
+        if ((this.swiperCurrent + 1) === this.episode.lists.length) {
+          this.$u.toast('已全部播放完成')
+          return 100
+        }
+        this.swiperCurrent += 1
+        this.currentEpisode = this.episode.lists[this.swiperCurrent]
+        this.$forceUpdate()
+        this.$loading('下一集加载中...')
+        setTimeout(() => {
+          this.$hideLoading()
+          this.handlePlay(this.currentEpisode)
+        }, 1000)
+      }
+    }
+  },
+  methods: {
+    // 播放进度
+    timeupdate({ detail }) {
+      // currentTime, duration
+      if (detail.duration) {
+        this.progress = (detail.currentTime / detail.duration * 100).toFixed(2)
+      }
+    },
+    // 播放
+    handlePlay(item) {
+      this.currentEpisode = item
+      // 检查是否购买
+      if (!this.checkBeforePlay(item)) {
+        // 余额是否购买
+        if (!this.checkOverage(item)) {
+          this.rechargeShow = true
+          return
+        }
+        // 余额足够 直接购买
+        this.handleBuy()
+        return
+      }
+      console.log('-->data', item)
+      item.isPlaying = true
+      this.isPlaying = true
+      this.videoContext.play()
+      this.watched(this.id, this.currentEpisode.id)
+    },
+    // 暂停
+    handlePause(item) {
+      if (!this.isPlaying) return
+      item.isPlaying = false
+      this.isPlaying = false
+      this.videoContext.pause()
+    },
+    // 选择剧集
+    handleSelectEpisode(index) {
+      // 暂停上一个
+      this.handlePause(this.currentEpisode)
+      // 切换播放
+      this.swiperCurrent = index
+
+      const item = this.episode.lists[index]
+      this.currentEpisode = item
+      // 检查是否购买
+      if (!this.checkBeforePlay(item)) {
+        // 余额是否购买
+        if (!this.checkOverage(item)) {
+          this.rechargeShow = true
+          return
+        }
+        // 余额足够 直接购买
+        this.handleBuy(index)
+        // this.modal.show = true
+        return
+      }
+      this.footerShow = false
+      this.$loading()
+      setTimeout(() => {
+        this.$hideLoading()
+        this.handlePlay(this.currentEpisode)
+        this.watched(this.id, this.currentEpisode.id)
+      }, 1000)
+    },
+    // 当前剧集购买记录
+    async getBuyRecord() {
+      await this.$api.user.episode.buyRecord(this.id).then(res => {
+        this.buyRecord = res.data
+      })
+    },
+    // 购买剧集
+    async handleBuy() {
+      // this.$loading('购买中...')
+      await this.$api.user.episode.buyHandle(this.id, this.currentEpisode.id).then(async res => {
+        this.$hideLoading()
+        if (typeof res.overage !== 'undefined') {
+          this.rechargeShow = true
+        } else {
+          this.$u.toast('购买成功')
+          this.modal.show = false
+          await this.getBuyRecord()
+          this.handlePlay(this.currentEpisode)
+          this.footerShow = false
+          this.$api.user.info().then(res => {
+            this.$store.dispatch('user/info', res.data)
+          })
+        }
+      }).catch(() => {
+        this.$hideLoading()
+      })
+    },
+    handleSwiperChancge({ detail }) {
+      // 暂停上一个
+      this.handlePause(this.currentEpisode)
+
+      console.log('-->data', detail)
+      this.swiperCurrent = detail.current
+      this.currentEpisode = this.episode.lists[this.swiperCurrent]
+      // 播放
+      this.handlePlay(this.currentEpisode)
+    },
+    // 播放前检查剧集是否购买/免费
+    checkBeforePlay(item) {
+      // 剧集免费 不免费已购买 VIP观看是VIP
+      if (item.is_free) {
+        return true
+      }
+
+      if (!item.is_free && this.buyRecord.indexOf(item.id) !== -1) {
+        return true
+      }
+
+      if (this.episode.is_vip_watch && this.userInfo.info.is_vip) {
+        return true
+      }
+
+      return false
+    },
+    // 检查余额是否足够支付
+    checkOverage(item) {
+      return this.userInfo.info.integral >= item.sale_price
+    },
+    // 检查是否收藏当前剧集
+    checkCollect() {
+      this.$api.user.collect.check(this.id).then(res => {
+        this.isCollect = res.data
+      })
+    },
+    // 收藏相关 处理
+    handleCollect() {
+      const method = this.isCollect ? 'destroy' : 'add'
+      const num = this.isCollect ? -1 : 1
+      this.$api.user.collect[method](this.id).then(res => {
+        if (res.data) {
+          this.toast.show = true
+          this.toast.status = this.isCollect ? 'cancel' : 'success'
+          this.toast.text = this.isCollect ? '取消收藏' : '收藏成功'
+          this.episode.user_collect_count += num
+          this.isCollect = !this.isCollect
+        }
+      })
+    },
+    // 检查是否喜欢当前短剧
+    checkFavorite() {
+      this.$api.user.favorite.check(this.id).then(res => {
+        this.isFav = res.data
+      })
+    },
+    // 处理 喜欢剧集
+    handleFavorite() {
+      const method = this.isFav ? 'destroy' : 'add'
+      const num = this.isFav ? -1 : 1
+      this.$api.user.favorite[method](this.id).then(res => {
+        if (res.data) {
+          this.toast.show = true
+          this.toast.status = this.isFav ? 'cancel' : 'success'
+          this.toast.text = this.isFav ? '取消喜欢' : '喜欢成功'
+          this.episode.user_favorite_count += num
+          this.isFav = !this.isFav
+        }
+      })
+    },
+    // 记录观看记录
+    watched(id, list_id) {
+      this.$api.user.episode.watched(id, list_id).then(res => {
+
+      })
+    },
+    // 分享
+    shared(id, list_id) {
+      this.$api.episode.shared(id, list_id).then(res => {
+        this.episode.share_count += 1
+      })
+    },
+    // 获取剧集详情
+    getEpisode() {
+      this.loading = true
+      this.$api.episode.detail(this.id).then(res => {
+        this.loading = false
+        this.episode = res.data
+        uni.setNavigationBarTitle({
+          title: this.episode.name + (this.episode.status === 0 ? ' | 更新中' : '已完结')
+        })
+        this.episode.lists.forEach((obj, index) => {
+          obj.isPlaying = false
+          obj.index = index
+        })
+        if (this.listId) {
+          this.episode.lists.forEach((obj, index) => {
+            if (parseInt(this.listId) === parseInt(obj.id)) {
+              this.currentEpisode = obj
+              this.swiperCurrent = index
+            }
+          })
+        } else {
+          this.currentEpisode = this.episode.lists[0]
+        }
+        this.$nextTick(() => {
+          this.handlePlay(this.currentEpisode)
+        })
+      })
+    }
+  },
+  async onLoad(options) {
+    this.id = options.id
+    this.listId = options?.list_id
+    await this.getBuyRecord()
+    this.getEpisode()
+    this.checkCollect()
+    this.checkFavorite()
+  },
+  // 分享
+  onShareAppMessage(res) {
+    if (res.from === 'button') { // 来自页面内分享按钮
+      console.log(res.target)
+    }
+    let options = {
+      title: '',
+      path: `/pages/episode/play?id=${this.id}`
+    }
+    if (this.episode) {
+      options = {
+        title: this.episode.name,
+        path: `/pages/episode/play?id=${this.id}`,
+        imageUrl: this.episode.cover_img,
+        desc: this.episode.name,
+        success: res => {
+          this.shared(this.id, this.currentEpisode.id)
+        }
+      }
+    }
+    return options
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+    .play-container {
+      font-size: 28rpx;
+      .video-box{
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        .play-layer{
+          position: fixed;
+          top: 0;
+          left: 0;
+          bottom: 0;
+          right: 0;
+          background: transparent;
+          z-index: 999;
+        }
+        .pause-layer{
+          position: absolute;
+          top: 0;
+          left: 0;
+          bottom: 0;
+          right: 0;
+          background: transparent;
+          z-index: 99;
+        }
+        .swiper {
+          width: 100%;
+          height: 100vh;
+          position: relative;
+          z-index: 99;
+          .swiper-item {
+            width: 100%;
+            height: 100%;
+          }
+        }
+        .progress-container{
+          width: 93vw;
+          background: #fff;
+          height: 10rpx;
+          position: fixed;
+          z-index: 100;
+          bottom: 160rpx;
+          .progress{
+            height: 10rpx;
+            background: linear-gradient(270deg, #6EEBE8 0%, #FF74B9 100%);
+          }
+        }
+      }
+      .view-num{
+        position: fixed;
+        right: 20rpx;
+        top: 40rpx;
+        color: #fff;
+        font-size: 26rpx;
+        z-index: 100;
+        text{
+          margin-left: 10rpx;
+        }
+      }
+
+      .status-bar{
+        position: fixed;
+        bottom: 300rpx;
+        right: 40rpx;
+        color: $default-color;
+        z-index: 999;
+        .item{
+          margin-bottom: 40rpx;
+          &.share{
+            position: relative;
+            button{
+              position: absolute;
+              background: transparent;
+              top: 0;
+              left: 0;
+              right: 0;
+              bottom: 0;
+              z-index: 1;
+              &:after{
+                content: unset;
+              }
+            }
+          }
+          &.active{
+            color: $primary-color;
+          }
+          text{
+            margin-top: 10rpx;
+          }
+        }
+      }
+
+      .footer{
+        position: fixed;
+        width: 95vw;
+        height: 76rpx;
+        background: rgba(24, 28, 47, 0.8);
+        // #ifdef  MP-KUAISHOU
+        bottom: 150rpx;
+        // #endif
+        // #ifdef  MP-TOUTIAO | MP-WEIXIN
+        bottom: 100rpx;
+        // #endif
+        left: 50%;
+        transform: translateX(-50%);
+        font-size: 26rpx;
+        color: $info-color;
+        border-radius: 20rpx;
+        z-index: 999;
+        transition: .3s;
+        &.episode{
+          bottom: 700rpx;
+          margin-top: -4rpx;
+          border-bottom-left-radius: 0;
+          border-bottom-right-radius: 0;
+          .episode-container{
+            display: flex;
+            margin-top: -4rpx;
+            border-bottom-left-radius: 20px;
+            border-bottom-right-radius: 20px;
+          }
+          .bar{
+            .arrow{
+              transform: rotate(180deg);
+            }
+          }
+        }
+        .bar{
+          padding: 0 20rpx;
+          .icon{
+            background: url("/static/image/video.png") no-repeat center;
+            background-size: 70%;
+            width: 80rpx;
+            height: 80rpx;
+          }
+          .name{
+            text-align: left;
+            flex: 1;
+            padding: 0 30rpx;
+          }
+          .arrow{
+            transition: .3s;
+          }
+        }
+
+        .episode-container{
+          display: none;
+          background: inherit;
+          min-height: 620rpx;
+          .header-box{
+            white-space: nowrap;
+            margin: 20rpx 0;
+            .header-item{
+              margin-right: 20rpx;
+              border-radius: 20rpx;
+              display: inline-block;
+              width: 200rpx;
+              border: 1rpx solid $default-color;
+              text-align: center;
+              padding: 10rpx 0;
+              color: $default-color;
+              &.active{
+                border-color: $primary-color;
+                color: $primary-color;
+              }
+            }
+          }
+          .content{
+            margin-top: 20rpx;
+            .episode-item{
+              position: relative;
+              width: calc((100% - #{40rpx}) / 3);
+              margin-right: 20rpx;
+              margin-bottom: 20rpx;
+              overflow: hidden;
+              border-radius: 18rpx;
+              &:nth-child(3n){
+                margin-right: 0;
+              }
+              .playing{
+                position: absolute;
+                top: 0;
+                left: 0;
+                bottom: 0;
+                right: 0;
+                background: rgba(0,0,0,.5) url("/static/image/playing.png") no-repeat center;
+                background-size: 40rpx;
+                z-index: 2;
+              }
+              image{
+                width: 100%;
+                height: 260rpx;
+              }
+              text{
+                position: absolute;
+                left: 0;
+                bottom: 0;
+                right: 0;
+                color: $default-color;
+                padding: 20rpx 0;
+                text-align: center;
+                background: rgba(0,0,0,.3);
+                z-index: 1;
+              }
+              .lock{
+                position: absolute;
+                top: 0;
+                left: 0;
+                bottom: 0;
+                right: 0;
+                background: rgba(0,0,0,.5);
+                z-index: 2;
+              }
+            }
+          }
+        }
+
+      }
+
+      .toast{
+        position: fixed;
+        width: 60vw;
+        background: rgba(0,0,0,.5);
+        height: 300rpx;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%,-50%);
+        border-radius: 20rpx;
+        font-size: 36rpx;
+        color: $default-color;
+        &.success{
+          color: $primary-color;
+        }
+        text{
+          margin-top: 20rpx;
+        }
+      }
+    }
+</style>

+ 161 - 466
mini/pages/episode/play.vue

xqd xqd xqd xqd xqd xqd xqd xqd xqd xqd xqd xqd xqd
@@ -7,44 +7,10 @@
       :loading-color="$colors.primaryColor"
     />
     <template v-if="!loading">
-      <!--播放数据-->
-      <view class="view-num main-left cross-center">
-        <u-icon name="eye-fill" :color="isCollect?$colors.primaryColor:$colors.defaultColor" size="26rpx" />
-        <text>{{ episode.user_watch_record_count }}</text>
-      </view>
-      <!--按钮-->
-      <view class="status-bar dir-top-wrap main-center">
-        <!--喜欢-->
-        <view
-          class="item fav dir-top-wrap main-center cross-center"
-          :class="{active: isFav}"
-          @click="handleFavorite"
-        >
-          <u-icon name="heart-fill" size="58rpx" :color="isFav?$colors.primaryColor:$colors.defaultColor" />
-          <text>{{ episode.user_favorite_count }}</text>
-        </view>
-        <!--收藏-->
-        <view
-          class="item collect dir-top-wrap main-center cross-center"
-          :class="{active: isCollect}"
-          @click="handleCollect"
-        >
-          <u-icon name="star-fill" size="58rpx" :color="isCollect?$colors.primaryColor:$colors.defaultColor" />
-          <text>{{ episode.user_collect_count }}</text>
-        </view>
-        <!--分享-->
-        <view class="item share dir-top-wrap main-center cross-center">
-          <button open-type="share" />
-          <u-icon name="share-fill" size="58rpx" :color="$colors.defaultColor" />
-          <text>{{ episode.sahre_count }}</text>
-        </view>
-      </view>
+      <!-- 剧集按钮-->
+      <episode-buttons :episode="episode" @change="handleCollectAndFavChange" />
       <!--视频播放-->
-      <view class="video-box main-center cross-center" :style="{zIndex: isPlaying?0:998}">
-        <!--进度条-->
-        <!--        <view v-if="isPlaying" class="progress-container">-->
-        <!--          <view class="progress" :style="{width: progress+'%'}" />-->
-        <!--        </view>-->
+      <view class="video-box main-center cross-center" :style="{zIndex: isPlaying ? 0 : 998}">
         <!--视频容器-->
         <swiper
           class="swiper"
@@ -54,7 +20,7 @@
           @change="handleSwiperChancge"
         >
           <swiper-item
-            v-for="(item,index) in episode.lists"
+            v-for="(item, index) in swiperEpisode"
             :key="index"
           >
             <!-- #ifdef  MP-TOUTIAO | MP-WEIXIN-->
@@ -63,9 +29,10 @@
               <u-icon name="play-right-fill" size="100rpx" :color="$colors.defaultColor" />
             </view>
             <!-- 控制按钮 - 暂停 -->
-            <view v-if="item.isPlaying" class="pause-layer" @tap="handlePause(item)" />
-            <!-- #endif-->
+            <view v-if="item.isPlaying" class="pause-layer" @tap="handlePause(item, true)" />
+            <!--   #endif-->
             <video
+              v-if="Object.keys(item).length"
               :id="`video${index}`"
               :poster="episode.cover_img"
               :src="item.url"
@@ -79,83 +46,14 @@
             />
           </swiper-item>
         </swiper>
-        <!--        <video-->
-        <!--          id="video"-->
-        <!--          :show-play-btn="video.playBtn"-->
-        <!--          :show-fullscreen-btn="video.fullscreenBtn"-->
-        <!--          :controls="video.controls"-->
-        <!--          object-fit="contain"-->
-        <!--          style="width: 100%;height: 100%;"-->
-        <!--          :poster="episode.cover_img"-->
-        <!--          :src="src"-->
-        <!--          @timeupdate="timeupdate"-->
-        <!--        />-->
       </view>
       <!--底部-->
-      <view class="footer" :class="{episode: footerShow}">
-        <view class="bar main-between cross-center" @click="footerShow = !footerShow">
-          <view class="icon" />
-          <view class="name">
-            <u-text :text="episode.name" :color="$colors.infoColor" :lines="1" />
-            <!--            <u-text text="321313" :color="$colors.infoColor" :lines="1" />-->
-          </view>
-          <view class="arrow">
-            <u-icon name="arrow-up" :color="$colors.infoColor" size="32rpx" />
-          </view>
-        </view>
-        <view class="episode-container dir-top-wrap">
-          <!--分集 横向滚动-->
-          <scroll-view
-            class="header-box dir-left-nowrap cross-center"
-            scroll-x
-            scroll-with-animation
-          >
-            <view
-              v-for="(item,index) in episodes"
-              :key="index"
-              class="header-item"
-              :class="{active: episodesIndex === index}"
-              @click="episodesIndex=index"
-            >{{ item.title }}</view>
-          </scroll-view>
-          <!--几集选择-->
-          <view class="content dir-left-wrap main-left">
-            <view
-              v-for="(item, index) in episodes[episodesIndex].lists"
-              :key="index"
-              class="episode-item"
-              @click="handleSelectEpisode(item.index)"
-            >
-              <image :src="episode.cover_img" />
-              <text>第{{ item.sort }}集</text>
-              <view v-if="currentEpisode.index === item.index && isPlaying" class="playing" />
-              <view v-if="!item.is_free && buyRecord.indexOf(item.id) === -1 && !(episode.is_vip_watch && userInfo.info.is_vip)" class="lock main-center cross-center">
-                <u-icon name="lock-fill" :color="$colors.defaultColor" size="46rpx" />
-              </view>
-            </view>
-          </view>
-        </view>
-      </view>
-      <!--toast-->
-      <view
-        v-if="toast.show"
-        class="toast dir-top-wrap main-center cross-center"
-        :class="toast.status"
-      >
-        <u-icon
-          :name="toast.status === 'success' ? 'checkmark-circle' : 'close-circle'"
-          :color="toast.status === 'success' ? $colors.primaryColor : $colors.defaultColor"
-          size="80rpx"
-        />
-        <text>{{ toast.text }}</text>
-      </view>
-      <!--购买弹窗-->
-      <u-modal
-        :show="modal.show"
-        :content="`确定购买【第${currentEpisode.sort}集】?`"
-        show-cancel-button
-        @confirm="handleBuy"
-        @cancel="modal.show = false"
+      <episode-part
+        :episode="episode"
+        :is-playing="isPlaying"
+        :buy-record="buyRecord"
+        :current-episode="currentEpisode"
+        @selectEpisode="handleSelectEpisode"
       />
       <!--充值-->
       <recharge
@@ -172,98 +70,112 @@
 <script>
 import { mapState } from 'vuex'
 import Recharge from '../../components/Recharge/index'
+import EpisodeButtons from './components/EpisodeButtons'
+import EpisodePart from './components/EpisodePart'
 export default {
   name: 'Play',
-  components: { Recharge },
+  components: { EpisodePart, EpisodeButtons, Recharge },
   data() {
     return {
-      id: null,
-      listId: null,
-      isPlaying: false,
-      progress: 0,
-      episode: null,
-      loading: true,
-      isCollect: false,
-      isFav: false,
-      video: {
+      id: null, // 短剧ID
+      listId: null, // 剧集ID
+      isPlaying: false, // 是否播放
+      progress: 0, // 进度条
+      episode: {}, // 短剧信息
+      lists: [], // 剧集信息
+      loading: false, // 数据加载
+      video: { // 视频配置
         controls: true,
-        // #ifdef  MP-KUAISHOU |  MP-TOUTIAO
-        progress: false,
-        // #endif
-        // #ifdef  MP-TOUTIAO | MP-WEIXIN
         progress: true,
-        // #endif
         fullscreenBtn: false,
         playBtn: false
-
-      },
-      toast: {
-        status: 'success',
-        text: '收藏成功',
-        show: false
       },
-      footerShow: false,
-      episodesIndex: 0,
-      buyRecord: [],
-      modal: {
-        show: false
-      },
-      rechargeShow: false,
-      swiperCurrent: 0,
-      currentEpisode: null
+      buyRecord: [], // 购买记录
+      rechargeShow: false, // 显示充值
+      swiperCurrent: 1, // 当前滚动
+      currentEpisode: {}, // 当前播放剧集
+      swiperEpisode: { // swiper 剧集
+        prev: {},
+        current: {},
+        next: {}
+      }
     }
   },
   computed: {
     ...mapState({
       userInfo: seate => seate.user.info
     }),
-    src() {
-      if (!this.episode) return ''
-      return this.currentEpisode.url
-    },
-    episodes() {
-      const list = []
-      if (this.episode) {
-        let temp = []
-        this.episode.lists.forEach((obj, index) => {
-          temp.push(obj)
-          if (temp.length === 6 || index === (this.episode.lists.length - 1)) {
-            const start = list.length ? (list.length * 6) + 1 : 1
-            const end = (start - 1) + temp.length
-            list.push({ title: `${start}集-${end}集`, lists: temp })
-            temp = []
-          }
-        })
-      }
-      return list
-    },
     videoContext() {
-      return uni.createVideoContext(`video${this.swiperCurrent}`, this)
+      const indexArr = ['prev', 'current', 'next']
+      const swiperKey = indexArr[this.swiperCurrent]
+      return uni.createVideoContext(`video${swiperKey}`, this)
     }
   },
   watch: {
-    'toast.show'(val) {
-      if (val) {
-        setTimeout(() => {
-          this.toast.show = false
-        }, 1000)
-      }
-    },
+    // 进度条
     progress(val) {
       if (val >= 100) {
-        if ((this.swiperCurrent + 1) === this.episode.lists.length) {
+        const len = this.lists.length
+        const indexArr = ['prev', 'current', 'next']
+        const swiperKey = indexArr[this.swiperCurrent]
+        if (this.swiperEpisode[swiperKey].sort === this.lists[len - 1].sort) {
           this.$u.toast('已全部播放完成')
           return 100
         }
-        this.swiperCurrent += 1
-        this.currentEpisode = this.episode.lists[this.swiperCurrent]
+        // 切换
+        switch (this.swiperCurrent) {
+          case 0:
+            this.swiperCurrent = 1
+            break
+          case 1:
+            this.swiperCurrent = 2
+            break
+          case 2:
+            this.swiperCurrent = 3
+            break
+        }
         this.$forceUpdate()
-        this.$loading('下一集加载中...')
-        setTimeout(() => {
-          this.$hideLoading()
-          this.handlePlay(this.currentEpisode)
-        }, 1000)
       }
+    },
+    swiperCurrent(val) {
+      const indexArr = ['prev', 'current', 'next']
+      const swiperKey = indexArr[val]
+      const dataIndex = this.lists.findIndex(val => {
+        return this.swiperEpisode[swiperKey].sort === val.sort
+      })
+      const len = this.lists.length
+      let prevIndex, currentIndex, nextIndex
+      switch (swiperKey) {
+        case 'prev':
+          currentIndex = dataIndex + 1 >= len ? 0 : dataIndex + 1
+          nextIndex = dataIndex - 1 < 0 ? len - 1 : dataIndex - 1
+          this.swiperEpisode.current = this.lists[currentIndex]
+          this.swiperEpisode.next = this.lists[nextIndex]
+          break
+        case 'current':
+          prevIndex = dataIndex - 1 < 0 ? len - 1 : dataIndex - 1
+          nextIndex = dataIndex + 1 >= len ? 0 : dataIndex + 1
+          this.swiperEpisode.prev = this.lists[prevIndex]
+          this.swiperEpisode.next = this.lists[nextIndex]
+          break
+        case 'next':
+          prevIndex = dataIndex + 1 >= len ? 0 : dataIndex + 1
+          currentIndex = dataIndex - 1 < 0 ? len - 1 : dataIndex - 1
+          this.swiperEpisode.current = this.lists[currentIndex]
+          this.swiperEpisode.prev = this.lists[prevIndex]
+          break
+      }
+
+      // 暂停其他
+      this.handlePause(this.currentEpisode, false)
+
+      // 当前播放剧集
+      this.currentEpisode = this.swiperEpisode[swiperKey]
+      // 播放
+      this.handlePlay(this.currentEpisode)
+    },
+    currentEpisode(val) {
+      console.log('-->data', val.sort)
     }
   },
   methods: {
@@ -288,47 +200,36 @@ export default {
         this.handleBuy()
         return
       }
-      console.log('-->data', item)
-      item.isPlaying = true
       this.isPlaying = true
+      item.isPlaying = true
       this.videoContext.play()
       this.watched(this.id, this.currentEpisode.id)
     },
     // 暂停
-    handlePause(item) {
+    handlePause(item, isAll) {
       if (!this.isPlaying) return
       item.isPlaying = false
       this.isPlaying = false
-      this.videoContext.pause()
+      // 展厅其他的
+      const indexArr = ['prev', 'current', 'next']
+      const swiperKey = indexArr[this.swiperCurrent]
+      indexArr.forEach(obj => {
+        if (swiperKey !== obj || isAll) {
+          const videoContext = uni.createVideoContext(`video${obj}`, this)
+          videoContext.pause()
+        }
+      })
     },
     // 选择剧集
     handleSelectEpisode(index) {
       // 暂停上一个
-      this.handlePause(this.currentEpisode)
-      // 切换播放
-      this.swiperCurrent = index
+      this.handlePause(this.currentEpisode, true)
 
-      const item = this.episode.lists[index]
-      this.currentEpisode = item
-      // 检查是否购买
-      if (!this.checkBeforePlay(item)) {
-        // 余额是否购买
-        if (!this.checkOverage(item)) {
-          this.rechargeShow = true
-          return
-        }
-        // 余额足够 直接购买
-        this.handleBuy(index)
-        // this.modal.show = true
-        return
-      }
-      this.footerShow = false
-      this.$loading()
-      setTimeout(() => {
-        this.$hideLoading()
-        this.handlePlay(this.currentEpisode)
-        this.watched(this.id, this.currentEpisode.id)
-      }, 1000)
+      const item = this.lists[index]
+
+      // 重置SwiperEpisode数据 切换播放
+      this.swiperCurrent = 1
+      this.initSwiperEpisode(item.id)
     },
     // 当前剧集购买记录
     async getBuyRecord() {
@@ -338,17 +239,14 @@ export default {
     },
     // 购买剧集
     async handleBuy() {
-      // this.$loading('购买中...')
       await this.$api.user.episode.buyHandle(this.id, this.currentEpisode.id).then(async res => {
         this.$hideLoading()
         if (typeof res.overage !== 'undefined') {
           this.rechargeShow = true
         } else {
           this.$u.toast('购买成功')
-          this.modal.show = false
           await this.getBuyRecord()
           this.handlePlay(this.currentEpisode)
-          this.footerShow = false
           this.$api.user.info().then(res => {
             this.$store.dispatch('user/info', res.data)
           })
@@ -357,15 +255,9 @@ export default {
         this.$hideLoading()
       })
     },
+    // 滚动 Swiper
     handleSwiperChancge({ detail }) {
-      // 暂停上一个
-      this.handlePause(this.currentEpisode)
-
-      console.log('-->data', detail)
       this.swiperCurrent = detail.current
-      this.currentEpisode = this.episode.lists[this.swiperCurrent]
-      // 播放
-      this.handlePlay(this.currentEpisode)
     },
     // 播放前检查剧集是否购买/免费
     checkBeforePlay(item) {
@@ -388,46 +280,6 @@ export default {
     checkOverage(item) {
       return this.userInfo.info.integral >= item.sale_price
     },
-    // 检查是否收藏当前剧集
-    checkCollect() {
-      this.$api.user.collect.check(this.id).then(res => {
-        this.isCollect = res.data
-      })
-    },
-    // 收藏相关 处理
-    handleCollect() {
-      const method = this.isCollect ? 'destroy' : 'add'
-      const num = this.isCollect ? -1 : 1
-      this.$api.user.collect[method](this.id).then(res => {
-        if (res.data) {
-          this.toast.show = true
-          this.toast.status = this.isCollect ? 'cancel' : 'success'
-          this.toast.text = this.isCollect ? '取消收藏' : '收藏成功'
-          this.episode.user_collect_count += num
-          this.isCollect = !this.isCollect
-        }
-      })
-    },
-    // 检查是否喜欢当前短剧
-    checkFavorite() {
-      this.$api.user.favorite.check(this.id).then(res => {
-        this.isFav = res.data
-      })
-    },
-    // 处理 喜欢剧集
-    handleFavorite() {
-      const method = this.isFav ? 'destroy' : 'add'
-      const num = this.isFav ? -1 : 1
-      this.$api.user.favorite[method](this.id).then(res => {
-        if (res.data) {
-          this.toast.show = true
-          this.toast.status = this.isFav ? 'cancel' : 'success'
-          this.toast.text = this.isFav ? '取消喜欢' : '喜欢成功'
-          this.episode.user_favorite_count += num
-          this.isFav = !this.isFav
-        }
-      })
-    },
     // 记录观看记录
     watched(id, list_id) {
       this.$api.user.episode.watched(id, list_id).then(res => {
@@ -435,44 +287,76 @@ export default {
       })
     },
     // 分享
-    shared(id, list_id) {
+    handleShared(id, list_id) {
       this.$api.episode.shared(id, list_id).then(res => {
         this.episode.share_count += 1
       })
     },
+    handleCollectAndFavChange(data) {
+      console.log('-->data', data)
+      if (data.type === 'collect') {
+        this.episode.user_collect_count += data.num
+      } else {
+        this.episode.user_favorite_count += data.num
+      }
+    },
+    // // 初始化 Swiper 剧集
+    initSwiperEpisode(listId) {
+      let currentIndex = 0
+      if (listId) {
+        currentIndex = this.lists.findIndex(obj => {
+          return parseInt(listId) === parseInt(obj.id)
+        })
+      }
+      let prevIndex = currentIndex - 1
+      let nextIndex = currentIndex + 1
+      const len = this.lists.length
+      if (parseInt(listId) === 0 || prevIndex < 0) {
+        prevIndex = len - 1
+      }
+      //
+      if (nextIndex >= len) {
+        nextIndex = 0
+      }
+
+      this.swiperEpisode = {
+        prev: this.lists[prevIndex],
+        current: this.lists[currentIndex],
+        next: this.lists[nextIndex]
+      }
+      console.log('-->data', JSON.stringify(this.swiperEpisode))
+
+      this.currentEpisode = this.lists[currentIndex]
+      this.$nextTick(() => {
+        this.handlePlay(this.currentEpisode)
+      })
+    },
     // 获取剧集详情
     getEpisode() {
       this.loading = true
       this.$api.episode.detail(this.id).then(res => {
         this.loading = false
         this.episode = res.data
+        uni.setNavigationBarTitle({
+          title: this.episode.name + (this.episode.status === 0 ? ' | 更新中' : '已完结')
+        })
         this.episode.lists.forEach((obj, index) => {
           obj.isPlaying = false
           obj.index = index
+          obj.progress = 0
         })
-        if (this.listId) {
-          this.episode.lists.forEach((obj, index) => {
-            if (parseInt(this.listId) === parseInt(obj.id)) {
-              this.currentEpisode = obj
-              this.swiperCurrent = index
-            }
-          })
-        } else {
-          this.currentEpisode = this.episode.lists[0]
-        }
-        this.$nextTick(() => {
-          this.handlePlay(this.currentEpisode)
-        })
+        this.lists = this.episode.lists
+        // 初始化 Swiper 剧集
+        this.initSwiperEpisode(this.listId)
       })
     }
   },
   async onLoad(options) {
     this.id = options.id
     this.listId = options?.list_id
+    this.listId = this.listId ? this.listId : 0
     await this.getBuyRecord()
     this.getEpisode()
-    this.checkCollect()
-    this.checkFavorite()
   },
   // 分享
   onShareAppMessage(res) {
@@ -490,7 +374,7 @@ export default {
         imageUrl: this.episode.cover_img,
         desc: this.episode.name,
         success: res => {
-          this.shared(this.id, this.currentEpisode.id)
+          this.handleShared(this.id, this.currentEpisode.id)
         }
       }
     }
@@ -533,7 +417,7 @@ export default {
           z-index: 99;
           .swiper-item {
             width: 100%;
-            height: 100%;
+            height: calc(100vh - #{150rpx});
           }
         }
         .progress-container{
@@ -549,194 +433,5 @@ export default {
           }
         }
       }
-      .view-num{
-        position: fixed;
-        right: 20rpx;
-        top: 40rpx;
-        color: #fff;
-        font-size: 26rpx;
-        z-index: 100;
-        text{
-          margin-left: 10rpx;
-        }
-      }
-
-      .status-bar{
-        position: fixed;
-        bottom: 300rpx;
-        right: 40rpx;
-        color: $default-color;
-        z-index: 999;
-        .item{
-          margin-bottom: 40rpx;
-          &.share{
-            position: relative;
-            button{
-              position: absolute;
-              background: transparent;
-              top: 0;
-              left: 0;
-              right: 0;
-              bottom: 0;
-              z-index: 1;
-              &:after{
-                content: unset;
-              }
-            }
-          }
-          &.active{
-            color: $primary-color;
-          }
-          text{
-            margin-top: 10rpx;
-          }
-        }
-      }
-
-      .footer{
-        position: fixed;
-        width: 95vw;
-        height: 76rpx;
-        background: rgba(24, 28, 47, 0.8);
-        // #ifdef  MP-KUAISHOU
-        bottom: 150rpx;
-        // #endif
-        // #ifdef  MP-TOUTIAO | MP-WEIXIN
-        bottom: 100rpx;
-        // #endif
-        left: 50%;
-        transform: translateX(-50%);
-        font-size: 26rpx;
-        color: $info-color;
-        border-radius: 20rpx;
-        z-index: 999;
-        transition: .3s;
-        &.episode{
-          bottom: 700rpx;
-          margin-top: -4rpx;
-          border-bottom-left-radius: 0;
-          border-bottom-right-radius: 0;
-          .episode-container{
-            display: flex;
-            margin-top: -4rpx;
-            border-bottom-left-radius: 20px;
-            border-bottom-right-radius: 20px;
-          }
-          .bar{
-            .arrow{
-              transform: rotate(180deg);
-            }
-          }
-        }
-        .bar{
-          padding: 0 20rpx;
-          .icon{
-            background: url("/static/image/video.png") no-repeat center;
-            background-size: 70%;
-            width: 80rpx;
-            height: 80rpx;
-          }
-          .name{
-            text-align: left;
-            flex: 1;
-            padding: 0 30rpx;
-          }
-          .arrow{
-            transition: .3s;
-          }
-        }
-
-        .episode-container{
-          display: none;
-          background: inherit;
-          min-height: 620rpx;
-          .header-box{
-            white-space: nowrap;
-            margin: 20rpx 0;
-            .header-item{
-              margin-right: 20rpx;
-              border-radius: 20rpx;
-              display: inline-block;
-              width: 200rpx;
-              border: 1rpx solid $default-color;
-              text-align: center;
-              padding: 10rpx 0;
-              color: $default-color;
-              &.active{
-                border-color: $primary-color;
-                color: $primary-color;
-              }
-            }
-          }
-          .content{
-            margin-top: 20rpx;
-            .episode-item{
-              position: relative;
-              width: calc((100% - #{40rpx}) / 3);
-              margin-right: 20rpx;
-              margin-bottom: 20rpx;
-              overflow: hidden;
-              border-radius: 18rpx;
-              &:nth-child(3n){
-                margin-right: 0;
-              }
-              .playing{
-                position: absolute;
-                top: 0;
-                left: 0;
-                bottom: 0;
-                right: 0;
-                background: rgba(0,0,0,.5) url("/static/image/playing.png") no-repeat center;
-                background-size: 40rpx;
-                z-index: 2;
-              }
-              image{
-                width: 100%;
-                height: 260rpx;
-              }
-              text{
-                position: absolute;
-                left: 0;
-                bottom: 0;
-                right: 0;
-                color: $default-color;
-                padding: 20rpx 0;
-                text-align: center;
-                background: rgba(0,0,0,.3);
-                z-index: 1;
-              }
-              .lock{
-                position: absolute;
-                top: 0;
-                left: 0;
-                bottom: 0;
-                right: 0;
-                background: rgba(0,0,0,.5);
-                z-index: 2;
-              }
-            }
-          }
-        }
-
-      }
-
-      .toast{
-        position: fixed;
-        width: 60vw;
-        background: rgba(0,0,0,.5);
-        height: 300rpx;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%,-50%);
-        border-radius: 20rpx;
-        font-size: 36rpx;
-        color: $default-color;
-        &.success{
-          color: $primary-color;
-        }
-        text{
-          margin-top: 20rpx;
-        }
-      }
     }
 </style>

+ 4 - 0
server/app/Helper/UniPlatform/BaseUniPlatform.php

xqd xqd
@@ -19,6 +19,8 @@ namespace App\Helper\UniPlatform;
 abstract class BaseUniPlatform
 {
     protected $appId = null;
+    protected $mchId = null;
+    protected $mchKey = null;
     protected $secret = null;
     protected $slat = null;
     protected $token = null;
@@ -43,6 +45,8 @@ abstract class BaseUniPlatform
         $this->secret = $config['app_secret'];
         $this->slat = isset($config['slat']) ? $config['slat'] : null;
         $this->token = isset($config['token']) ?  $config['token'] : null;
+        $this->mchId = isset($config['mch_id']) ?  $config['mch_id'] : null;
+        $this->mchKey = isset($config['mch_key']) ?  $config['mch_key'] : null;
 
         $this->setAccessFileDir();
         $this->setAccessFilePath();

+ 13 - 0
server/app/Helper/UniPlatform/Wechat/WechatAPI.php

xqd
@@ -0,0 +1,13 @@
+<?php
+namespace App\Helper\UniPlatform\Wechat;
+
+use App\Helper\UniPlatform\BaseAPI;
+
+
+final class WechatAPI extends BaseAPI
+{
+
+    const ACCESS_TOKEN = '';
+    const LOGIN = '';
+    const CREATE_ORDER = '';
+}

+ 95 - 0
server/app/Helper/Wechat.php

xqd
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Created by PhpStorm
+ * DateTime: 2022/10/28 22:50
+ *
+ * @description
+ */
+namespace App\Helper;
+use App\Helper\UniPlatform\BaseUniPlatform;
+use EasyWeChat\Factory;
+use EasyWeChat\MiniProgram\Application;
+
+class Wechat extends BaseUniPlatform
+{
+
+    public function mini(): Application
+    {
+        return Factory::miniProgram([
+            'app_id'        => $this->appId,
+            'secret'        => $this->secret,
+            'response_type' => 'array'
+        ]);
+    }
+
+    public function payment(): \EasyWeChat\Payment\Application
+    {
+        return Factory::payment([
+            'sandbox'            => false,
+            'app_id'             => $this->appId,
+            'mch_id'             => $this->mchId,
+            'key'                => $this->mchKey,
+            'cert_path'          => env('WECHAT_PAYMENT_CERT_PATH', 'path/to/cert/apiclient_cert.pem'),    // XXX: 绝对路径!!!!
+            'key_path'           => env('WECHAT_PAYMENT_KEY_PATH', 'path/to/cert/apiclient_key.pem'),      // XXX: 绝对路径!!!!
+            'notify_url'         => $this->noticeUrl
+        ]);
+    }
+
+    /**
+     * @param $outOrderNo
+     * @param $totalAmount
+     * @param $openId
+     * @return array
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
+     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
+     * @throws \GuzzleHttp\Exception\GuzzleException
+     * @throws \Exception
+     */
+    public function createOrder($outOrderNo, $totalAmount, $openId): array
+    {
+        $app = $this->payment();
+        $payment = $app->order->unify([
+            'body'         => '订单号:' . $outOrderNo,
+            'out_trade_no' => $outOrderNo,
+            'total_fee'    => $totalAmount * 100, // 分
+            'notify_url'   => $this->noticeUrl, // 支付结果通知网址,如果不设置则会使用配置里的默认地址
+            'trade_type'   => 'JSAPI', // 请对应换成你的支付方式对应的值类型
+            'openid'       => $openId,
+        ]);
+        if($payment['return_code'] != "SUCCESS" || $payment['result_code'] != 'SUCCESS'){
+            throw new \Exception($payment['return_msg']);
+        }
+        return [
+            'order_id' => $payment['prepay_id'],
+            'order_token' => ''
+        ];
+    }
+
+    protected function setNoticeUrl(): void
+    {
+        $this->noticeUrl = env('APP_URL').'/api/pay/wechat/notify';
+    }
+
+    protected function login($code): array
+    {
+    }
+
+    protected function post($uri = '', $data = []): array
+    {
+    }
+
+    protected function getAccessToken(): string
+    {
+    }
+
+
+
+    protected function setAccessFileDir(): void
+    {
+    }
+
+    protected function setAccessFilePath(): void
+    {
+    }
+
+}

+ 1 - 1
server/app/Http/Controllers/V1/AuthController.php

xqd
@@ -168,7 +168,7 @@ class AuthController extends Controller
         try {
             $code = $request->input('code');
             /* */
-            $app = $this->getUniFactory(3);
+            $app = $this->getUniFactory(3)->mini();
             $res = $app->auth->session($code);
 
             return $this->doLogin($res['openid'], '',3,  $res['session_key']);

+ 7 - 4
server/app/Http/Controllers/V1/Controller.php

xqd xqd xqd
@@ -6,6 +6,8 @@ use App\Helper\ByteDance;
 use App\Helper\Kuaishou;
 use App\Helper\UniPlatform\Bytedance\ByteDanceAPI;
 use App\Helper\UniPlatform\Kuaishou\KuaishouAPI;
+use App\Helper\UniPlatform\Wechat\WechatAPI;
+use App\Helper\Wechat;
 use App\Models\PayConfig;
 use Dingo\Api\Routing\Helpers;
 use EasyWeChat\Factory;
@@ -66,7 +68,7 @@ class Controller extends BaseController
 
     /**
      * @param int $platform
-     * @return ByteDance|Kuaishou|Application
+     * @return ByteDance|Kuaishou|Wechat
      */
     protected function getUniFactory(int $platform = 1)
     {
@@ -99,14 +101,15 @@ class Controller extends BaseController
         ]);
     }
 
-    protected function getWechatFactory(): Application
+    protected function getWechatFactory(): Wechat
     {
         $setting = PayConfig::first();
-        return Factory::miniProgram([
+        return (new Wechat(app(WechatAPI::class)))->factory([
             'app_id' => $setting->mini_app_id,
-            'secret' => $setting->mini_app_key,
+            'app_secret' => $setting->mini_app_key,
             'response_type' => 'array'
         ]);
+
     }
 
 }

+ 50 - 2
server/app/Http/Controllers/V1/PayNoticeController.php

xqd xqd xqd
@@ -12,11 +12,13 @@ use App\Models\UserInfo;
 use App\Models\UserRechargeRecord;
 use App\Models\UserVipRecord;
 use Carbon\Carbon;
+use Illuminate\Http\JsonResponse;
 use Illuminate\Support\Facades\Log;
+use Symfony\Component\HttpFoundation\Response;
 
 class PayNoticeController extends Controller
 {
-    public function bytedance()
+    public function bytedance(): JsonResponse
     {
         $factory = $this->getUniFactory();
         $app = new BytedancePayment($factory);
@@ -56,7 +58,7 @@ class PayNoticeController extends Controller
         });
     }
 
-    public function kuaishou()
+    public function kuaishou(): JsonResponse
     {
         $app = new KuaishouPayment($this->getUniFactory(2));
         return $app->payNotify(function ($data, $fail) {
@@ -97,6 +99,52 @@ class PayNoticeController extends Controller
         });
     }
 
+    /**
+     * @return Response
+     * @throws \EasyWeChat\Kernel\Exceptions\Exception
+     */
+    public function wechat(): Response
+    {
+        $factory = $this->getUniFactory(3)->payment();
+        return $factory->handlePaidNotify(function ($message, $fail){
+            \Log::info('微信支付回调==>'.json_encode($message,JSON_UNESCAPED_SLASHES));
+            //处理支付订单
+            try {
+                \DB::beginTransaction();
+                $payId = $message['out_order_no'];
+                $pay = Pay::find($payId);
+                if (!$pay) { // 如果订单不存在
+                    return true;
+                }
+                if ($message['return_code'] === 'SUCCESS') {
+                    if ($message['result_code'] === 'SUCCESS') {
+                        $pay->status = 1;
+                        $pay->serial_number = $message['transaction_id'];
+                    }elseif ($message['result_code'] === 'FAIL') {
+                        $pay->status = -1;
+                    }
+                } else {
+                    return $fail('通信失败,请稍后再通知我');
+                }
+                $pay->pay_dt = Carbon::now()->toDateTimeString(); // 更新支付时间为当前时间
+                $pay->save();
+                // 处理
+                if ($pay->source == Pay::SOURCE_RECHARGE) {
+                    $this->recharge($payId);
+                } else if ($pay->source == Pay::SOURCE_BUY_VIP) {
+                    $this->buyVip($payId);
+                }
+
+                \DB::commit();
+            } catch (\Exception $e) {
+                \DB::rollBack();
+                \Log::error('微信支付回调错误==>'.$e->getMessage());
+                return $fail('通信失败,请稍后再通知我');
+            }
+            return true;
+        });
+    }
+
 
     private function recharge($payId)
     {

+ 5 - 0
server/app/Http/Controllers/V1/User/RechargeController.php

xqd xqd
@@ -69,6 +69,7 @@ class RechargeController extends Controller
             }else{
                 $res = app(Pay::class)->create($app, $combo->price, Pay::SOURCE_RECHARGE);
 
+
                 $recharge = new UserRechargeRecord();
                 $recharge->user_id = \user()->id;
                 $recharge->combo_id = $combo->id;
@@ -78,6 +79,10 @@ class RechargeController extends Controller
                 $recharge->pay_id = $res['pay_id'];
                 $recharge->status = 0;
                 $recharge->save();
+                if(\user()->info->platform == 3){
+                    $pay = Pay::find($res['pay_id']);
+                    $res = $app->payment()->jssdk->bridgeConfig($pay->prepay_id);
+                }
 
             }
 

+ 5 - 0
server/app/Http/Controllers/V1/User/VipController.php

xqd
@@ -63,6 +63,11 @@ class VipController extends Controller
                 $recharge->pay_id = $res['pay_id'];
                 $recharge->status = 0;
                 $recharge->save();
+
+                if(\user()->info->platform == 3){
+                    $pay = Pay::find($res['pay_id']);
+                    $res = $app->payment()->jssdk->bridgeConfig($pay->prepay_id);
+                }
             }
             \DB::commit();
             return $this->success($res);

+ 6 - 5
server/app/Http/Controllers/V1/User/WatchRecordsController.php

xqd xqd xqd
@@ -2,8 +2,10 @@
 namespace App\Http\Controllers\V1\User;
 
 use App\Http\Controllers\V1\Controller;
+use App\Models\EpisodesList;
 use App\Models\User;
 use App\Models\UserWatchRecord;
+use Carbon\Carbon;
 use Dingo\Api\Http\Request;
 use Illuminate\Database\Eloquent\Builder;
 
@@ -45,8 +47,8 @@ class WatchRecordsController extends Controller
             ->first();
 
         if($info){
-            $count = $info->detail->episode->withCount('lists')->first()->toArray();
-            $info->detail->episode->total = $count['lists_count'];
+            $count = EpisodesList::where('episodes_id', $info->detail->episodes_id)->count('id');
+            $info->detail->episode->total = $count;
             $info->detail->episode->status_text = $info->detail->episode->status;
         }
 
@@ -58,15 +60,14 @@ class WatchRecordsController extends Controller
         $id = request()->input('id');
         $listId = request()->input('list_id');
         $res = UserWatchRecord::filterUser()->where('episode_id',$id)->first();
-        if($res){
-            $res->list_id = $listId;
-        }else{
+        if(!$res){
             $res = new UserWatchRecord();
             $res->user_id = \user()->id;
             $res->episode_id = $id;
         }
 
         $res->list_id = $listId;
+        $res->updated_at = Carbon::now()->toDateTimeString();
 
         return $this->success($res->save());
     }

+ 5 - 6
server/app/Models/Pay.php

xqd
@@ -74,17 +74,16 @@ class Pay extends Model
     }
 
     /**
-     * @param BaseUniPlatform $byteDance
-     * @param           $price
-     * @param int       $source
+     * @param BaseUniPlatform $uniPlatform
+     * @param float           $price
+     * @param int             $source
      * @return array|mixed
-     * @throws \Exception
      */
-    public function create(BaseUniPlatform $byteDance, float $price, int $source)
+    public function create(BaseUniPlatform $uniPlatform, float $price, int $source)
     {
         $payId = SerialNumber::createOrderId(\user()->id);
         // 字节跳动下单
-        $res = $byteDance->createOrder($payId, $price, \user()->open_id);
+        $res = $uniPlatform->createOrder($payId, $price, \user()->open_id);
         $res['pay_id'] = $payId;
 
         $pay = new Pay();

+ 2 - 1
server/config/global.php

xqd
@@ -26,7 +26,8 @@ return [
         0 =>'未付款',
         1 =>'已完成',
         2 =>'已取消',
-        3 => '已关闭'
+        3 => '已关闭',
+        -1 => '支付失败'
     ],
     // 签到设置
     'sign_config' => [

+ 1 - 0
server/routes/api.php

xqd
@@ -172,5 +172,6 @@ $api->version('v1', ['namespace' => 'App\Http\Controllers\V1'], function ($api)
     $api->get('pay/{pay_id}/query', 'PayController@query'); //字节跳动支付
     $api->any('pay/bytedance/notify', 'PayNoticeController@bytedance'); //字节跳动支付回调
     $api->any('pay/kuaishou/notify', 'PayNoticeController@kuaishou'); //快手支付回调
+    $api->any('pay/wechat/notify', 'PayNoticeController@wechat'); //微信支付回调
 
 });