Explorar o código

接入秒杀活动列表

owen hai 1 ano
pai
achega
9323f1ce64

+ 270 - 114
pages/activity/seckill/list.vue

@@ -1,29 +1,58 @@
 <template>
   <s-layout navbar="inner" :bgStyle="{ color: 'rgb(245,28,19)' }">
+    <!--顶部背景图-->
     <view
-      class="page-bg"
-      :style="[{ marginTop: '-' + Number(statusBarHeight + 88) + 'rpx' }]"
+        class="page-bg"
+        :style="[{ marginTop: '-' + Number(statusBarHeight + 88) + 'rpx' }]"
     ></view>
+    <!-- 时间段轮播图 -->
+    <view class="header" v-if="activeTimeConfig?.sliderPicUrls?.length > 0">
+      <swiper indicator-dots="true" autoplay="true" :circular="true" interval="3000" duration="1500"
+              indicator-color="rgba(255,255,255,0.6)" indicator-active-color="#fff">
+        <block v-for="(picUrl, index) in activeTimeConfig.sliderPicUrls" :key="index">
+          <swiper-item class="borRadius14">
+            <image :src="picUrl" class="slide-image borRadius14" lazy-load />
+          </swiper-item>
+        </block>
+      </swiper>
+    </view>
+    <!-- 时间段列表 -->
+    <view class="flex align-center justify-between ss-p-25">
+      <!-- 左侧图标 -->
+      <view class="time-icon">
+        <image class="ss-w-100 ss-h-100" src="http://mall.yudao.iocoder.cn/static/images/priceTag.png"></image>
+      </view>
+      <scroll-view class="time-list" :scroll-into-view="activeTimeElId" scroll-x scroll-with-animation>
+        <view v-for="(config, index) in timeConfigList" :key="index"
+              :class="['item', { active: activeTimeIndex === index}]"
+              :id="`timeItem${index}`"
+              @tap="handleChangeTimeConfig(index)">
+          <!-- 活动起始时间 -->
+          <view class="time">{{ config.startTime }}</view>
+          <!-- 活动状态 -->
+          <view class="status">{{ config.status }}</view>
+        </view>
+      </scroll-view>
+    </view>
+    <!-- 内容区 -->
     <view class="list-content">
+      <!-- 活动倒计时 -->
       <view class="content-header ss-flex-col ss-col-center ss-row-center">
-        <view class="content-header-title ss-m-b-22 ss-flex ss-row-center">
-          <view>{{ state.activityInfo.title }}</view>
-          <!-- <view class="more">更多</view> -->
-        </view>
         <view class="content-header-box ss-flex ss-row-center">
-          <view class="countdown-box ss-flex" v-if="endTime?.ms > 0 && state.activityInfo">
+          <view class="countdown-box ss-flex" v-if="activeTimeConfig?.status === TimeStatusEnum.STARTED">
             <view class="countdown-title ss-m-r-12">距结束</view>
             <view class="ss-flex countdown-time">
-              <view class="ss-flex countdown-h">{{ endTime.h }}</view>
+              <view class="ss-flex countdown-h">{{ countDown.h }}</view>
               <view class="ss-m-x-4">:</view>
-              <view class="countdown-num ss-flex ss-row-center">{{ endTime.m }}</view>
+              <view class="countdown-num ss-flex ss-row-center">{{ countDown.m }}</view>
               <view class="ss-m-x-4">:</view>
-              <view class="countdown-num ss-flex ss-row-center">{{ endTime.s }}</view>
+              <view class="countdown-num ss-flex ss-row-center">{{ countDown.s }}</view>
             </view>
           </view>
-          <view class="" v-if="endTime?.ms < 0 && state.activityInfo"> 活动已结束 </view>
+          <view v-else> {{ activeTimeConfig?.status }} </view>
         </view>
       </view>
+      <!-- 活动列表 -->
       <scroll-view
         class="scroll-box"
         :style="{ height: pageHeight + 'rpx' }"
@@ -31,125 +60,265 @@
         :scroll-with-animation="false"
         :enable-back-to-top="true"
       >
-        <view class="goods-box ss-m-b-20" v-for="item in state.pagination.data" :key="item.id">
+        <view class="goods-box ss-m-b-20" v-for="activity in activityList" :key="activity.id">
           <s-goods-column
-            class=""
             size="lg"
-            :data="item"
+            :data="{ ...activity, price: activity.seckillPrice }"
+            :goodsFields="goodsFields"
             :seckillTag="true"
             @click="
               sheep.$router.go('/pages/goods/seckill', {
-                id: item.id,
-                activity_id: state.activityId,
+                id: activity.spuId,
+                activityId: activity.id
               })
             "
           >
-            <template v-slot:cart>
-              <button class="ss-reset-button cart-btn">去抢购</button>
+            <!-- 抢购进度 -->
+            <template #activity>
+              <view class="limit">限量 <text class="ss-m-l-5">{{ activity.stock}} {{activity.unitName}}</text></view>
+              <su-progress :percentage="activity.percent" strokeWidth="10" textInside isAnimate />
+            </template>
+            <!-- 抢购按钮 -->
+            <template #cart>
+              <button :class="['ss-reset-button cart-btn', { disabled: activeTimeConfig.status === TimeStatusEnum.END }]">
+                <span v-if="activeTimeConfig?.status === TimeStatusEnum.WAIT_START">未开始</span>
+                <span v-else-if="activeTimeConfig?.status === TimeStatusEnum.STARTED">马上抢</span>
+                <span v-else>已结束</span>
+              </button>
             </template>
           </s-goods-column>
         </view>
         <uni-load-more
-          v-if="state.pagination.total > 0"
-          :status="state.loadStatus"
+          v-if="activityTotal > 0"
+          :status="loadStatus"
           :content-text="{
             contentdown: '上拉加载更多',
           }"
-          @tap="loadmore"
+          @tap="loadMore"
         />
       </scroll-view>
     </view>
   </s-layout>
 </template>
 <script setup>
-  import { reactive, computed } from 'vue';
+  import {reactive, computed, ref, nextTick} from 'vue';
   import { onLoad, onReachBottom } from '@dcloudio/uni-app';
   import sheep from '@/sheep';
-  import _ from 'lodash';
   import { useDurationTime } from '@/sheep/hooks/useGoods';
+  import SeckillApi from "@/sheep/api/promotion/seckill";
+  import dayjs from "dayjs";
+
+  // 时间段的状态枚举
+  const TimeStatusEnum = {
+    WAIT_START: '即将开始',
+    STARTED: '进行中',
+    END: '已结束',
+  }
 
-  const { screenHeight, safeAreaInsets, screenWidth, safeArea } = sheep.$platform.device;
-  const sys_navBar = sheep.$platform.navbar;
+  // 计算页面高度
+  const { safeAreaInsets, safeArea } = sheep.$platform.device;
   const statusBarHeight = sheep.$platform.device.statusBarHeight * 2;
-  const pageHeight =
-    (safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sys_navBar - 350;
+  const pageHeight = (safeArea.height + safeAreaInsets.bottom) * 2 + statusBarHeight - sheep.$platform.navbar - 350;
   const headerBg = sheep.$url.css('/static/img/shop/goods/seckill-header.png');
 
-  const state = reactive({
-    activityId: 0,
-    pagination: {
-      data: [],
-      current_page: 1,
-      total: 1,
-      last_page: 1,
-    },
-    loadStatus: '',
-    activityInfo: {},
-  });
-  // 倒计时
-  const endTime = computed(() => {
-    if (state.activityInfo.end_time) {
-      return useDurationTime(state.activityInfo.end_time);
-    }
-  });
+  // 商品控件显示的字段(不显示库存、销量。改为显示自定义的进度条)
+  const goodsFields = {
+    // 商品名称
+    name: { show: true },
+    // 商品介绍
+    introduction: { show: true },
+    // 商品价格
+    price: { show: true },
+    // 市场价
+    marketPrice: { show: true },
+  };
 
-  async function getList(activityId, page = 1, list_rows = 4) {
-    state.loadStatus = 'loading';
-    const res = await sheep.$api.goods.activityList({
-      list_rows,
-      activity_id: activityId,
-      page,
-    });
-    if (res.error === 0) {
-        let couponList = _.concat(state.pagination.data, res.data.data);
-        state.pagination = {
-          ...res.data,
-          data: couponList,
-        };
-      if (state.pagination.current_page < state.pagination.last_page) {
-        state.loadStatus = 'more';
+  //#region 时间段
+  // 时间段列表
+  const timeConfigList = ref([])
+  // 查询时间段
+  const getSeckillConfigList = async () => {
+    const { data } = await SeckillApi.getSeckillConfigList()
+    const now = dayjs();
+    const today = now.format('YYYY-MM-DD')
+    // 判断时间段的状态
+    data.forEach((config, index) => {
+      const startTime = dayjs(`${today} ${config.startTime}`)
+      const endTime = dayjs(`${today} ${config.endTime}`)
+      if (now.isBefore(startTime)) {
+        config.status = TimeStatusEnum.WAIT_START;
+      } else if (now.isAfter(endTime)) {
+        config.status = TimeStatusEnum.END;
       } else {
-        state.loadStatus = 'noMore';
+        config.status = TimeStatusEnum.STARTED;
+        activeTimeIndex.value = index;
       }
+    })
+    timeConfigList.value = data
+    // 默认选中进行中的活动
+    handleChangeTimeConfig(activeTimeIndex.value);
+    // 滚动到进行中的时间段
+    scrollToTimeConfig(activeTimeIndex.value)
+  }
+
+  // 当前选中的时间段的元素ID
+  const activeTimeElId = ref('')
+  // 滚动到指定时间段
+  const scrollToTimeConfig = (index) => {
+    nextTick(() => activeTimeElId.value = `timeItem${index}`)
+  }
+
+  // 当前选中的时间段的索引
+  const activeTimeIndex = ref(0)
+  // 当前选中的时间段
+  const activeTimeConfig = computed(() => timeConfigList.value[activeTimeIndex.value])
+  // 切换时间段
+  const handleChangeTimeConfig = (index) => {
+    activeTimeIndex.value = index
+
+    // 查询活动列表
+    activityPageParams.pageNo = 1
+    activityList.value = []
+    getActivityList();
+  }
+
+  // 倒计时
+  const countDown = computed(() => {
+    const endTime = activeTimeConfig.value?.endTime
+    if (endTime) {
+      return useDurationTime(`${dayjs().format('YYYY-MM-DD')} ${endTime}`);
     }
+  });
+
+  //#endregion
+
+  //#region 分页查询活动列表
+
+  // 活动分页查询参数
+  const activityPageParams = reactive({
+    // 时间段ID
+    id: 0,
+    // 页码
+    pageNo: 1,
+    // 每页数量
+    pageSize: 5,
+  })
+  // 活动总数
+  const activityTotal = ref(0)
+  // 活动列表
+  const activityList = ref([])
+  // 页面加载状态
+  const loadStatus = ref('')
+  // 查询活动列表
+  async function getActivityList() {
+    loadStatus.value = 'loading';
+    const { data } = await SeckillApi.getSeckillActivityPage(activityPageParams)
+    data.list.forEach(activity => {
+      // 计算抢购进度
+      activity.percent = parseInt(100 * (activity.totalStock - activity.stock) / activity.totalStock);
+    })
+    activityList.value = activityList.value.concat(...data.list);
+    activityTotal.value = data.total;
+
+    loadStatus.value = activityList.value.length < activityTotal.value ? 'more' : 'noMore';
   }
-  async function getActivity(id) {}
+
   // 加载更多
-  function loadmore() {
-    if (state.loadStatus !== 'noMore') {
-      getList(state.activityId, state.pagination.current_page + 1);
+  function loadMore() {
+    if (loadStatus.value !== 'noMore') {
+      activityPageParams.pageNo += 1
+      getActivityList();
     }
   }
   // 上拉加载更多
-  onReachBottom(() => {
-    loadmore();
-  });
-  onLoad(async (options) => {
-    if (!options.id) {
-      state.activityInfo = null;
-      return;
-    }
-    state.activityId = options.id;
-    getList(state.activityId);
-    const { error, data } = await sheep.$api.activity.activity(options.id);
-    if (error === 0) {
-      state.activityInfo = data;
-    } else {
-      state.activityInfo = null;
-    }
+  onReachBottom(() => loadMore());
+  //#endregion
+
+  // 页面初始化
+  onLoad(async () => {
+    await getSeckillConfigList()
   });
 </script>
 <style lang="scss" scoped>
+  // 顶部背景图
   .page-bg {
     width: 100%;
     height: 458rpx;
     background: v-bind(headerBg) no-repeat;
     background-size: 100% 100%;
   }
+
+  // 时间段轮播图
+  .header {
+    width: 710rpx;
+    height: 330rpx;
+    margin: -276rpx auto 0 auto;
+    border-radius: 14rpx;
+    overflow: hidden;
+    swiper{
+      height: 330rpx !important;
+      border-radius: 14rpx;
+      overflow: hidden;
+    }
+
+    image {
+      width: 100%;
+      height: 100%;
+      border-radius: 14rpx;
+      overflow: hidden;
+      img{
+        border-radius: 14rpx;
+      }
+    }
+  }
+
+  // 时间段列表:左侧图标
+  .time-icon {
+    width: 75rpx;
+    height: 70rpx;
+  }
+  // 时间段列表
+  .time-list {
+    width: 596rpx;
+    white-space: nowrap;
+    // 时间段
+    .item {
+      display: inline-block;
+      font-size: 20rpx;
+      color: #666;
+      text-align: center;
+      box-sizing: border-box;
+      margin-right: 30rpx;
+      width: 130rpx;
+      // 开始时间
+      .time {
+        font-size: 36rpx;
+        font-weight: 600;
+        color: #333;
+      }
+      // 选中的时间段
+      &.active {
+        .time {
+          color: var(--ui-BG-Main);
+        }
+        // 状态
+        .status {
+          height: 30rpx;
+          line-height: 30rpx;
+          border-radius: 15rpx;
+          width: 128rpx;
+          background: linear-gradient(90deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%);
+          color: #fff;
+        }
+      }
+    }
+  }
+
+  // 内容区
   .list-content {
     position: relative;
     z-index: 3;
-    margin: -190rpx 20rpx 0 20rpx;
+    margin: 0 20rpx 0 20rpx;
     background: #fff;
     border-radius: 20rpx 20rpx 0 0;
     .content-header {
@@ -157,52 +326,24 @@
       border-radius: 20rpx 20rpx 0 0;
       height: 150rpx;
       background: linear-gradient(180deg, #fff4f7, #ffe6ec);
-      .content-header-title {
-        width: 100%;
-        font-size: 30rpx;
-        font-weight: 500;
-        color: #ff2923;
-        line-height: 30rpx;
-        position: relative;
-        .more {
-          position: absolute;
-          right: 30rpx;
-          top: 0;
-          font-size: 24rpx;
-          font-weight: 400;
-          color: #999999;
-          line-height: 30rpx;
-        }
-      }
+
       .content-header-box {
         width: 678rpx;
         height: 64rpx;
         background: rgba($color: #fff, $alpha: 0.66);
         border-radius: 32px;
-        .num {
-          font-size: 24rpx;
-          font-family: OPPOSANS;
-          font-weight: 500;
-          color: #f51c11;
-          line-height: 30rpx;
-        }
-        .title {
-          font-size: 24rpx;
-          font-weight: 400;
-          font-family: OPPOSANS;
-          color: #333;
-          line-height: 30rpx;
-        }
+        // 场次倒计时内容
         .countdown-title {
           font-size: 28rpx;
           font-weight: 500;
           color: #333333;
           line-height: 28rpx;
         }
-
+        // 场次倒计时
         .countdown-time {
           font-size: 28rpx;
           color: rgba(#ed3c30, 0.23);
+          // 场次倒计时:小时部分
           .countdown-h {
             font-size: 24rpx;
             font-family: OPPOSANS;
@@ -213,6 +354,7 @@
             background: rgba(#ed3c30, 0.23);
             border-radius: 6rpx;
           }
+          // 场次倒计时:分钟、秒
           .countdown-num {
             font-size: 24rpx;
             font-family: OPPOSANS;
@@ -226,22 +368,36 @@
         }
       }
     }
+    // 活动列表
     .scroll-box {
       height: 900rpx;
+      // 活动
       .goods-box {
         position: relative;
+        // 抢购按钮
         .cart-btn {
           position: absolute;
           bottom: 10rpx;
           right: 20rpx;
           z-index: 11;
-          height: 50rpx;
+          height: 44rpx;
           line-height: 50rpx;
           padding: 0 20rpx;
           border-radius: 25rpx;
           font-size: 24rpx;
           color: #fff;
           background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
+
+          &.disabled {
+            background: $gray-b;
+            color: #fff;
+          }
+        }
+        // 秒杀限量商品数
+        .limit {
+          font-size: 22rpx;
+          color: $dark-9;
+          margin-bottom: 5rpx;
         }
       }
     }

+ 2 - 2
sheep/api/promotion/seckill.js

@@ -12,8 +12,8 @@ const SeckillApi = {
   },
 
   // 获得秒杀活动分页
-  getSeckillActivityPage: () => {
-    return request2({ url: 'promotion/seckill-activity/page', method: 'GET' });
+  getSeckillActivityPage: (params) => {
+    return request2({ url: 'promotion/seckill-activity/page', method: 'GET', params });
   },
 
   /**

+ 5 - 2
sheep/ui/su-progress/su-progress.vue

@@ -25,7 +25,7 @@
           v-if="isAnimate"
         >
           <view class="textInside flex a-center j-center" v-if="textInside && !noData">
-            <view>{{ percentage }}%</view>
+            <view class="text">{{ percentage }}%</view>
           </view>
         </view>
         <view
@@ -80,7 +80,7 @@
       // 背景颜色
       bgColor: {
         type: String,
-        default: '#409eff',
+        default: 'linear-gradient(90deg, var(--ui-BG-Main) 0%, var(--ui-BG-Main-gradient) 100%)',
       },
       // 是否不显示数据
       noData: {
@@ -168,6 +168,9 @@
 
   .text {
     margin-left: 10rpx;
+    font-size: 16rpx;
+    width: 100rpx;
+    color: #FFB9B9;
   }
 
   .percentage {