Browse Source

feat:限时活动banner组件及活动倒计时组件

yangfeng 3 years ago
parent
commit
8b015b7971

+ 1 - 0
src/api/modules/index.js

@@ -12,3 +12,4 @@ export * from './svip'
 export * from './file'
 export * from './customer'
 export * from './dataExport'
+export * from './marketing'

+ 33 - 0
src/api/modules/marketing.js

@@ -0,0 +1,33 @@
+import request from '@/api'
+/**
+ * 轻量化营销服务
+ */
+
+// 获取服务端当前时间戳
+export function getServerInitTime () {
+  return request({
+    baseURL: '/marketing',
+    url: '/time/now?t=' + Date.now(),
+    method: 'post'
+  })
+}
+
+// 预约
+export function appointmentAdd (data) {
+  return request({
+    baseURL: '/marketing',
+    url: '/appointment/add',
+    method: 'post',
+    data
+  })
+}
+
+// 预热信息
+export function getAppointmentInfo (data) {
+  return request({
+    baseURL: '/marketing',
+    url: '/appointment/info',
+    method: 'post',
+    data
+  })
+}

BIN
src/assets/images/limited-already.png


BIN
src/assets/images/limited-bg.png


BIN
src/assets/images/limited-empty.png


BIN
src/assets/images/subscribe-btn.png


+ 104 - 0
src/components/limited-banner/index.vue

@@ -0,0 +1,104 @@
+<template>
+  <div class="limited-discount">
+    <div class="limited-bg">
+      <div class="limited-badge">{{ badge }}</div>
+      <div class="limited-content">{{ content }}</div>
+      <div class="limited-countdown">
+        <LimitedCountDown :endTime="1659427812000"></LimitedCountDown>
+      </div>
+      <div class="limited-handel">
+        <div class="cycle-running" v-if="status === 0" @click.stop="$emit('subscribe')"></div>
+        <div class="cycle-already" v-if="status === 1"></div>
+        <div class="cycle-empty" v-if="status === 2"></div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import LimitedCountDown from '@/components/limited-countdown'
+export default {
+  name: 'LimitedDiscount',
+  components: {
+    LimitedCountDown
+  },
+  props: {
+    badge: {
+      type: String,
+      default: '限时特惠'
+    },
+    content: {
+      type: String,
+      default: '夏季福利季月度计划一口价30元'
+    },
+    status: {
+      type: Number,
+      default: 1
+    },
+    endTime: {
+      type: Number,
+      default: 0
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.limited-discount{
+  width: 100%;
+  background: linear-gradient(173deg, #FF7C32 0%, #F33838 100%);
+  .limited-bg{
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 1120px;
+    height: 80px;
+    background: url('~@/assets/images/limited-bg.png') no-repeat center;
+    background-size: contain;
+    margin: 0 auto;
+    color: #fff;
+  }
+  .limited-badge{
+    padding: 7px 16px;
+    background: linear-gradient(163deg, #CB1313 0%, #F82424 100%);
+    border-radius: 16px;
+    font-size: 16px;
+    color: #fff;
+    line-height: 18px;
+    text-align: center;
+  }
+  .limited-content{
+    padding: 0 16px;
+    line-height: 28px;
+    font-size: 18px;
+  }
+  .limited-countdown{
+    padding: 7px 16px;
+    background: linear-gradient(163deg, #CB1313 0%, rgba(248,36,36,0) 100%);
+    border-radius: 16px;
+    line-height: 18px;
+  }
+  .cycle-running{
+    width: 112px;
+    height: 40px;
+    background: url('~@/assets/images/subscribe-btn.png') no-repeat center;
+    background-size: contain;
+    cursor: pointer;
+  }
+  .cycle-item{
+    width: 112px;
+    height: 76px;
+    background-repeat: no-repeat;
+    background-position: center;
+    background-size: contain;
+  }
+  .cycle-empty{
+    @extend .cycle-item;
+    background-image: url('~@/assets/images/limited-empty.png')
+  }
+  .cycle-already{
+    @extend .cycle-item;
+    background-image: url('~@/assets/images/limited-already.png')
+  }
+}
+</style>

+ 134 - 0
src/components/limited-countdown/index.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="countdown">
+    <span v-if="showStartText">{{startText}}</span>
+    <span>{{content}}</span>
+  </div>
+</template>
+
+<script>
+import { getServerInitTime } from '@/api/modules/'
+export default {
+  name: 'LimitedCountDown',
+  props: {
+    /**
+     * 活动开始文案
+     */
+    startText: {
+      type: String,
+      default: '开始倒计时:'
+    },
+    /**
+     * 开始时间-时间戳
+     */
+    startTime: {
+      type: Number,
+      default: 0
+    },
+    /**
+     * 到期时间-时间戳
+     */
+    endTime: {
+      type: Number,
+      default: 0
+    },
+    /**
+     * 倒计时结束显示文案
+     */
+    endText: {
+      type: String,
+      default: '活动结束'
+    },
+    /**
+     * 时间单位为空是否展示占位
+     */
+    placeholder: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data () {
+    return {
+      showStartText: false,
+      content: '',
+      serverInitTime: null,
+      localInitTime: null
+    }
+  },
+  watch: {
+    endTime (val) {
+      this.countdown(val)
+    }
+  },
+  mounted () {
+    this.countdown(this.endTime)
+    document.addEventListener('visibilitychange', () => {
+      if (document.visibilityState === 'visible') {
+        console.log('切出当前页')
+        this.initAdjustTime()
+      }
+    })
+  },
+  methods: {
+    async initAdjustTime () {
+      // 接口响应时服务端的本地时间
+      const { data } = await getServerInitTime()
+      if (data) {
+        this.serverInitTime = data?.time.toString().length === 10 ? data?.time * 1000 : data?.time
+      } else {
+        this.serverInitTime = Date.now()
+      }
+      /**
+       * 浏览器提供了一个performance.timeOrigin,可以大致理解为整个页面加载的初始时间
+       * performance.now接口返回值表示从timeOrigin之后到当前调用时经过的时间,主要用来测试某个函数的执行时间等
+       */
+      this.localInitTime = performance.now()
+    },
+    getCurrentTime () {
+      if (!this.serverInitTime) {
+        console.error('日期校验暂未初始化')
+        return
+      }
+      const localCurrentTime = performance.now()
+      return Math.floor(this.serverInitTime + (localCurrentTime - this.localInitTime))
+    },
+    async countdown (timestamp) {
+      await this.initAdjustTime()
+      console.log(this.serverInitTime, timestamp)
+      const timer = setInterval(() => {
+        const nowTime = this.getCurrentTime()
+        const stopTime = timestamp.toString().length === 10 ? timestamp * 1000 : timestamp
+        const t = stopTime - nowTime
+        if (t > 0) {
+          this.showStartText = true
+          const day = Math.floor(t / 86400000)
+          let hour = Math.floor((t / 3600000) % 24)
+          let min = Math.floor((t / 60000) % 60)
+          let sec = Math.floor((t / 1000) % 60)
+          hour = hour < 10 ? '0' + hour : hour
+          min = min < 10 ? '0' + min : min
+          sec = sec < 10 ? '0' + sec : sec
+          let format = ''
+          if (this.placeholder) {
+            format = `${day}天${hour}小时${min}分${sec}秒`
+          } else {
+            if (day > 0) {
+              format = `${day}天${hour}小时${min}分${sec}秒`
+            }
+            if (day <= 0 && hour > 0) {
+              format = `${hour}小时${min}分${sec}秒`
+            }
+            if (day <= 0 && hour <= 0) {
+              format = `${min}分${sec}秒`
+            }
+          }
+          this.content = format
+        } else {
+          clearInterval(timer)
+          this.showStartText = false
+          this.content = this.endText
+        }
+      }, 1000)
+    }
+  }
+}
+</script>

+ 85 - 80
src/views/vipsubscribe/Buy.vue

@@ -1,84 +1,87 @@
 <template>
-  <Layout class="vip-subscribe-buy" contentWithState="full" :needAd="false">
-    <div class="vip-subscribe-title">{{ buyTypeText }}超级订阅</div>
-    <div class="vip-subscribe-content">
-      <div class="vip-sub-list">
-        <AreaSelector
-          ref="areaSelector"
-          class="vip-sub-list-item"
-          :class="{ 'pd-b0': upgradeTipShow }"
-          :showSearchInput="false"
-          :onlyProvince="true"
-          :showSelectResult="true"
-          selectorType="line"
-          @onChange="onAreaChange">
-          <div slot="header" class="vip-sub-item-title">购买区域</div>
-        </AreaSelector>
-        <SelectorCard
-          class="vip-sub-tip"
-          :cardType="conf.selectorType">
-          <div slot="header" class="vip-sub-item-title"></div>
-          <div class="tip-content font-red" v-show="upgradeTipShow">
-            请增加购买区域进行升级,当前选择省份数量未高于原套餐数,无法升级
-          </div>
-        </SelectorCard>
-        <SelectorCard
-          class="vip-sub-list-item"
-          v-if="moduleShow.specList"
-          :cardType="conf.selectorType">
-          <div slot="header" class="vip-sub-item-title">选择购买周期</div>
-          <div class="vip-sub-item-content">
-            <SpecList :list="specList" v-model="specIdActive" @change="specChange" />
-          </div>
-        </SelectorCard>
-        <SelectorCard
-          class="vip-sub-list-item"
-          v-show="moduleShow.coupon"
-          :cardType="conf.selectorType">
-          <div slot="header" class="vip-sub-item-title">选择优惠券</div>
-          <div class="vip-sub-item-content">
-            <CouponCardList
-              ref="couponRef"
-              :productionId="specActiveItem.productionId"
-              :price="computedPrice.total"
-              @loaded="couponCardLoaded"
-              @change="couponCardChange" />
-          </div>
-        </SelectorCard>
-        <SelectorCard
-          class="vip-sub-list-item"
-          v-show="moduleShow.gift"
-          :cardType="conf.selectorType">
-          <div slot="header" class="vip-sub-item-title">赠品</div>
-          <div class="vip-sub-item-content">
-            <CouponGiftList
-              :productionId="specActiveItem.productionId"
-              @loaded="giftListLoaded" />
-          </div>
-        </SelectorCard>
-        <SelectorCard
-          class="vip-sub-list-item"
-          :cardType="conf.selectorType">
-          <div slot="header" class="vip-sub-item-title">手机号码</div>
-          <div class="vip-sub-item-content">
-            <CheckPhone v-model="userInfo.phone" :pass.sync="phoneRegPass" />
-          </div>
-        </SelectorCard>
+  <div>
+    <LimitedBanner></LimitedBanner>
+    <Layout class="vip-subscribe-buy" contentWithState="full" :needAd="false">
+      <div class="vip-subscribe-title">{{ buyTypeText }}超级订阅</div>
+      <div class="vip-subscribe-content">
+        <div class="vip-sub-list">
+          <AreaSelector
+            ref="areaSelector"
+            class="vip-sub-list-item"
+            :class="{ 'pd-b0': upgradeTipShow }"
+            :showSearchInput="false"
+            :onlyProvince="true"
+            :showSelectResult="true"
+            selectorType="line"
+            @onChange="onAreaChange">
+            <div slot="header" class="vip-sub-item-title">购买区域</div>
+          </AreaSelector>
+          <SelectorCard
+            class="vip-sub-tip"
+            :cardType="conf.selectorType">
+            <div slot="header" class="vip-sub-item-title"></div>
+            <div class="tip-content font-red" v-show="upgradeTipShow">
+              请增加购买区域进行升级,当前选择省份数量未高于原套餐数,无法升级
+            </div>
+          </SelectorCard>
+          <SelectorCard
+            class="vip-sub-list-item"
+            v-if="moduleShow.specList"
+            :cardType="conf.selectorType">
+            <div slot="header" class="vip-sub-item-title">选择购买周期</div>
+            <div class="vip-sub-item-content">
+              <SpecList :list="specList" v-model="specIdActive" @change="specChange" />
+            </div>
+          </SelectorCard>
+          <SelectorCard
+            class="vip-sub-list-item"
+            v-show="moduleShow.coupon"
+            :cardType="conf.selectorType">
+            <div slot="header" class="vip-sub-item-title">选择优惠券</div>
+            <div class="vip-sub-item-content">
+              <CouponCardList
+                ref="couponRef"
+                :productionId="specActiveItem.productionId"
+                :price="computedPrice.total"
+                @loaded="couponCardLoaded"
+                @change="couponCardChange" />
+            </div>
+          </SelectorCard>
+          <SelectorCard
+            class="vip-sub-list-item"
+            v-show="moduleShow.gift"
+            :cardType="conf.selectorType">
+            <div slot="header" class="vip-sub-item-title">赠品</div>
+            <div class="vip-sub-item-content">
+              <CouponGiftList
+                :productionId="specActiveItem.productionId"
+                @loaded="giftListLoaded" />
+            </div>
+          </SelectorCard>
+          <SelectorCard
+            class="vip-sub-list-item"
+            :cardType="conf.selectorType">
+            <div slot="header" class="vip-sub-item-title">手机号码</div>
+            <div class="vip-sub-item-content">
+              <CheckPhone v-model="userInfo.phone" :pass.sync="phoneRegPass" />
+            </div>
+          </SelectorCard>
+        </div>
+        <BuySubmit
+          :pass="allPass"
+          :productionTotal="computedPrice.total / 100"
+          :productionDiscount="computedPrice.discount / 100"
+          :productionPay="computedPrice.pay / 100"
+          @submit="submit"
+        ></BuySubmit>
       </div>
-      <BuySubmit
-        :pass="allPass"
-        :productionTotal="computedPrice.total / 100"
-        :productionDiscount="computedPrice.discount / 100"
-        :productionPay="computedPrice.pay / 100"
-        @submit="submit"
-      ></BuySubmit>
-    </div>
-    <div class="vip-subscribe-desc">
-      <Contrast></Contrast>
-    </div>
-    <!-- <div style="height: 800px;background: green"></div>
-    <div class="j-bottom" style="height: 300px;background: red"></div> -->
-  </Layout>
+      <div class="vip-subscribe-desc">
+        <Contrast></Contrast>
+      </div>
+      <!-- <div style="height: 800px;background: green"></div>
+      <div class="j-bottom" style="height: 300px;background: red"></div> -->
+    </Layout>
+  </div>
 </template>
 
 <script>
@@ -91,6 +94,7 @@ import CouponGiftList from '@/components/coupon/CouponGiftList.vue'
 import CheckPhone from '@/components/coupon/CheckPhone.vue'
 import BuySubmit from '@/components/coupon/BuySubmit.vue'
 import Contrast from '@/views/vipsubscribe/components/Contrast.vue'
+import LimitedBanner from '@/components/limited-banner/index.vue'
 import { Dialog } from 'element-ui'
 
 import {
@@ -114,7 +118,8 @@ export default {
     CheckPhone,
     BuySubmit,
     Contrast,
-    [Dialog.name]: Dialog
+    [Dialog.name]: Dialog,
+    LimitedBanner
   },
   data () {
     return {

+ 10 - 0
vue.config.js

@@ -71,6 +71,16 @@ module.exports = {
         // target: 'http://127.0.0.1:829',
         changeOrigin: true,
         logLevel: 'debug'
+      },
+      '^/jyCoupon': {
+        target: 'https://jybx2-webtest.jydev.jianyu360.com',
+        changeOrigin: true,
+        logLevel: 'debug'
+      },
+      '^/marketing': {
+        target: 'http://192.168.3.240:709',
+        changeOrigin: true,
+        logLevel: 'debug'
       }
     },
     headers: {

+ 0 - 10
yarn.lock

@@ -7604,11 +7604,6 @@ simple-swizzle@^0.2.2:
   dependencies:
     is-arrayish "^0.3.1"
 
-single-spa-vue@^1.5.4:
-  version "1.10.1"
-  resolved "https://registry.npm.taobao.org/single-spa-vue/download/single-spa-vue-1.10.1.tgz#80cbb603dc8eaec4148577b3570fa04b2d45ee16"
-  integrity sha1-gMu2A9yOrsQUhXezVw+gSy1F7hY=
-
 sirv@^1.0.7:
   version "1.0.11"
   resolved "https://registry.npm.taobao.org/sirv/download/sirv-1.0.11.tgz#81c19a29202048507d6ec0d8ba8910fda52eb5a4"
@@ -8057,11 +8052,6 @@ svgo@^1.0.0:
     unquote "~1.1.1"
     util.promisify "~1.0.0"
 
-systemjs-webpack-interop@^1.1.2:
-  version "1.2.1"
-  resolved "https://registry.npm.taobao.org/systemjs-webpack-interop/download/systemjs-webpack-interop-1.2.1.tgz#70cce64a93ff646f52344eb560288d83362c9695"
-  integrity sha1-cMzmSpP/ZG9SNE61YCiNgzYslpU=
-
 table@^5.2.3:
   version "5.4.6"
   resolved "https://registry.npm.taobao.org/table/download/table-5.4.6.tgz?cache=0&sync_timestamp=1609732718890&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftable%2Fdownload%2Ftable-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e"