Browse Source

feat: 倒计时功能、Noticebar 纵向 (#533)

* feat: 扩展倒计时功能

* feat: 扩展倒计时组件

* feat: 扩展 noticebar 功能

Co-authored-by: yangxiaolu3 <yangxiaolu3@jd.com>
yangxiaolu1993 4 years ago
parent
commit
712691991d

+ 15 - 1
src/packages/countdown/countdown.vue

@@ -1,6 +1,9 @@
 <template>
   <span class="nut-cd-timer">
-    <template v-if="showPlainText">
+    <template v-if="$slots.default">
+      <slot></slot>
+    </template>
+    <template v-else-if="showPlainText">
       <span class="nut-cd-block">{{ plainText }}</span>
     </template>
     <template v-else>
@@ -66,6 +69,12 @@ const countdownTimer = {
     };
   },
   props: {
+    value: {
+      type: Object,
+      default: () => {
+        return {};
+      }
+    },
     paused: {
       default: false,
       type: Boolean
@@ -111,6 +120,11 @@ const countdownTimer = {
     }
   },
   watch: {
+    value(newVal, oldVal) {},
+    restTime(n, o) {
+      let tranTime = restTime(n);
+      this.$emit('input', tranTime);
+    },
     paused(v, ov) {
       if (!ov) {
         this._curr = this.getTimeStamp();

+ 47 - 2
src/packages/countdown/demo.vue

@@ -41,6 +41,23 @@
         </div>
       </nut-cell>
     </div>
+    <h4>自定义展示</h4>
+    <div>
+      <nut-cell>
+        <span slot="title">
+          <nut-countdown v-model="resetTime" :endTime="end">
+            <div class="countdown-part-box">
+              <div class="part-item-symbol">{{ resetTime.d }}天</div>
+              <div class="part-item h">{{ resetTime.h }}</div>
+              <span class="part-item-symbol">:</span>
+              <div class="part-item m">{{ resetTime.m }}</div>
+              <span class="part-item-symbol">:</span>
+              <div class="part-item s">{{ resetTime.s }}</div>
+            </div>
+          </nut-countdown>
+        </span>
+      </nut-cell>
+    </div>
     <h4>自定义显示</h4>
     <div>
       <nut-cell>
@@ -57,7 +74,13 @@ export default {
       serverTime: Date.now() - 30 * 1000,
       end: Date.now() + 50 * 1000,
       asyncEnd: 0,
-      paused: false
+      paused: false,
+      resetTime: {
+        d: '1',
+        h: '00',
+        m: '00',
+        s: '00'
+      }
     };
   },
   methods: {
@@ -82,4 +105,26 @@ export default {
 };
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+.countdown-part-box {
+  display: flex;
+  align-items: center;
+
+  .part-item {
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 20px;
+    height: 25px;
+    background: #e8220e;
+    color: #fff;
+    font-size: 14px;
+    border-radius: 6px;
+  }
+
+  .part-item-symbol {
+    margin: 0 5px;
+  }
+}
+</style>

+ 55 - 0
src/packages/countdown/doc.md

@@ -62,10 +62,65 @@
 </nut-countdown>
 ```
 
+自定义展示
+
+```html
+<nut-countdown v-model="resetTime" :endTime="end">
+    <div class="countdown-part-box">
+        <div class="part-item-symbol">{{ resetTime.d }}天</div>
+        <div class="part-item h">{{ resetTime.h }}</div>
+        <span class="part-item-symbol">:</span>
+        <div class="part-item m">{{ resetTime.m }}</div>
+        <span class="part-item-symbol">:</span>
+        <div class="part-item s">{{ resetTime.s }}</div>
+    </div>
+</nut-countdown>
+
+<style lang="scss" scoped>
+.countdown-part-box {
+  display: flex;
+  align-items: center;
+
+  .part-item {
+    flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 20px;
+    height: 25px;
+    background: #e8220e;
+    color: #fff;
+    font-size: 14px;
+    border-radius: 6px;
+  }
+
+  .part-item-symbol {
+    margin: 0 5px;
+  }
+}
+</style>
+```
+```javascript
+export default {
+  data() {
+    return {
+        end: Date.now() + 50 * 1000,
+        resetTime: {
+            d: '1',
+            h: '00',
+            m: '00',
+            s: '00'
+        }
+    };
+  }
+};
+```
+
 ## Prop
 
 | 字段 | 说明 | 类型 | 默认值
 | ----- | ----- | ----- | -----
+| v-model | 当前时间,自定义展示内容时生效 | Object | {}
 | start-time | 开始时间 | String, Number | Date.now()
 | end-time | 结束时间 | String, Number | Date.now()
 | show-days | 是否显示天 | Boolean | false

+ 37 - 1
src/packages/noticebar/demo.vue

@@ -15,17 +15,53 @@
     <nut-noticebar left-icon="https://img13.360buyimg.com/imagetools/jfs/t1/72082/2/3006/1197/5d130c8dE1c71bcd6/e48a3b60804c9775.png">
       <a href="https://www.jd.com">京东商城</a>
     </nut-noticebar>
+
+    <h4>纵向滚动</h4>
+    <div class="interstroll-list">
+      <nut-noticebar direction="vertical" :list="horseLamp1" :speed="10" :standTime="1000" @click="go" :closeMode="true"></nut-noticebar>
+    </div>
+
+    <h4>纵向复杂滚动动画</h4>
+    <nut-noticebar direction="vertical" :list="horseLamp2" :speed="10" :standTime="2000" :complexAm="true"></nut-noticebar>
+
+    <h4>纵向自定义滚动内容</h4>
+    <nut-noticebar direction="vertical" :height="50" :speed="10" :standTime="1000" :list="[]" @close="go">
+      <div class="custom-item" :data-index="index" v-for="(item, index) in horseLamp3" :key="index">{{ item }}</div>
+    </nut-noticebar>
+
+    <h4>纵向自定义右侧图标</h4>
+    <nut-noticebar direction="vertical" :list="horseLamp1" :speed="10" :standTime="1000">
+      <template v-slot:rightIcon>
+        <nut-icon type="trolley" color="#f0250f"> </nut-icon>
+      </template>
+    </nut-noticebar>
   </div>
 </template>
 
 <script>
 export default {
+  data() {
+    return {
+      horseLamp1: ['惊喜红包免费领', '爆款准点秒', '买超值优惠', '赢百万京豆'],
+      horseLamp2: ['惊喜红包免费领', '爆款准点秒', '买超值优惠', '赢百万京豆'],
+      horseLamp3: ['惊喜红包免费领', '爆款准点秒', '买超值优惠', '赢百万京豆']
+    };
+  },
   methods: {
     hello() {
       console.log('hello world');
+    },
+    go(item) {
+      console.log(item);
     }
   }
 };
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss">
+.custom-item {
+  display: flex;
+  align-items: center;
+  height: 50px;
+}
+</style>

+ 83 - 0
src/packages/noticebar/doc.md

@@ -32,12 +32,84 @@
     </nut-noticebar>
 ```
 
+### 纵向滚动
+
+```html
+<nut-noticebar direction='vertical' :list="horseLamp1" :speed='10' :standTime='1000'  @click='go' :closeMode="true"></nut-noticebar>
+```
+```javascript
+data() {
+    return {
+        horseLamp1: ['惊喜红包免费领', '爆款准点秒', '买超值优惠', '赢百万京豆'],
+    };
+},
+
+methods:{
+    go(item){
+        console.log(item)
+    }
+}
+```
+
+
+### 复杂滚动动画
+
+```html
+<nut-noticebar direction='vertical' :list="horseLamp2" :speed='10' :standTime='2000' :complexAm='true'></nut-noticebar>
+```
+```javascript
+data() {
+    return {
+        horseLamp2: ['惊喜红包免费领', '爆款准点秒', '买超值优惠', '赢百万京豆'],
+    };
+}
+```
+
+### 自定义滚动内容
+
+```html
+<nut-noticebar direction='vertical' :height='50' :speed='10' :standTime='1000' :list="[]"  @close='go'>
+  <div class="custom-item" :data-index='index' v-for="(item,index) in horseLamp3" :key="index">{{item}}</div>
+</nut-noticebar>
+```
+```javascript
+data() {
+    return {
+        horseLamp3: ['惊喜红包免费领', '爆款准点秒', '买超值优惠', '赢百万京豆'],
+    };
+}
+```
+
+
+### 纵向自定义右侧图标
+
+```html
+<nut-noticebar direction='vertical' :list="horseLamp1" :speed='10' :standTime='1000' >
+  <template v-slot:rightIcon>
+    <nut-icon 
+      type="trolley" 
+      color="#f0250f"
+    >
+    </nut-icon>
+
+  </template>
+</nut-noticebar>
+```
+```javascript
+data() {
+    return {
+        horseLamp1: ['惊喜红包免费领', '爆款准点秒', '买超值优惠', '赢百万京豆'],
+    };
+}
+```
+
 
 
 ## Prop
 
 | 字段       | 说明                                                       | 类型          | 默认值 |
 | ---------- | ---------------------------------------------------------- | ------------- | ------ |
+| direction       | 滚动的方向,可选 across、vertical                         | String        | across     |
 | text       | 提示的信息                                                 | String        | 空     |
 | closeMode  | 是否启用关闭模式                                           | Boolean       | false  |
 | leftIcon   | close为没有左边icon,其他为自定义的图片链接,没有为默认图片 | String        | 空     |
@@ -47,6 +119,17 @@
 | scrollable | 是否可以滚动                                               | Boolean       | true   |
 | speed      | 滚动速率 (px/s)                                            | Number        | 50     |
 
+### Prop(direction=vertical)
+
+| 参数         | 说明                             | 类型   | 默认值           |
+|--------------|----------------------------------|--------|------------------|
+| list         | 纵向滚动数据列表               | Array | []               |
+| speed        | 滚动的速度                         | Number | 50               |
+| standTime         | 停留时间(毫秒) | Number | 1000                |
+| complexAm | 稍复杂的动画,耗能会高     | Boolean | false |
+| height          | 每一个滚动列的高度(px),注意:在使用 slot 插槽定义滚动单元时,按照实际高度修改此值                 | Number | 40              |
+| closeMode  | 是否启用右侧关闭图标,可以通过slot[name=rightIcon]自定义图标                                   | Boolean       | false  |
+
 ## Event
 
 | 字段  | 说明             | 回调参数     |

+ 13 - 0
src/packages/noticebar/item.js

@@ -0,0 +1,13 @@
+export default {
+  name: 'ScrollItem',
+  functional: true,
+  props: {
+    item: null
+  },
+  render: (createElement, context) => {
+    // console.log(context.props.item)
+    // console.log(context.slots().default)
+    const { tag, children, data } = context.props.item;
+    return createElement(tag, data, children);
+  }
+};

+ 41 - 16
src/packages/noticebar/noticebar.scss

@@ -9,19 +9,18 @@
   color: $text-color;
   background-color: rgba(251, 248, 220, 1);
   color: rgba(211, 125, 066, 1);
-  &.wrapable{
+  &.wrapable {
     height: auto;
     padding: 8px 16px;
-    .wrap{
+    .wrap {
       height: auto;
-      .content{
+      .content {
         position: relative;
         white-space: normal;
         word-wrap: break-word;
       }
     }
-
-  }    
+  }
   a {
     text-decoration: none;
     color: rgba(211, 125, 066, 1);
@@ -51,7 +50,6 @@
     height: 24px;
     overflow: hidden;
     position: relative;
-    
   }
   .content {
     position: absolute;
@@ -60,7 +58,7 @@
       max-width: 100%;
       white-space: nowrap;
       overflow: hidden;
-      text-overflow:ellipsis;
+      text-overflow: ellipsis;
     }
   }
   // 只跑一次
@@ -76,19 +74,46 @@
 }
 
 @keyframes nut-notice-bar-play {
-	to {
-		transform: translate3d(-100%, 0, 0);
-	}
+  to {
+    transform: translate3d(-100%, 0, 0);
+  }
 }
 
 @keyframes nut-notice-bar-play-infinite {
-	to {
-		transform: translate3d(-100%, 0, 0);
-	}
+  to {
+    transform: translate3d(-100%, 0, 0);
+  }
 }
 // 垂直方向的滚动
 @keyframes nut-notice-bar-play-vertical {
-	to {
-		transform: translateY(40px);
-	}
+  to {
+    transform: translateY(40px);
+  }
+}
+
+// 纵向
+.nut-noticebar-vertical {
+  position: relative;
+  display: flex;
+  justify-content: space-between;
+  height: 40px;
+  padding: 0 16px;
+  border-radius: 4px;
+  box-sizing: border-box;
+  overflow: hidden;
+
+  .horseLamp_list {
+    display: block;
+    flex: 1;
+    .horseLamp_list_item {
+      display: flex;
+      align-items: center;
+      height: 40px;
+      font-size: 16px;
+    }
+  }
+
+  .go {
+    align-self: center;
+  }
 }

+ 186 - 24
src/packages/noticebar/noticebar.vue

@@ -1,36 +1,88 @@
 <template>
-  <div
-    v-show="showNoticeBar"
-    class="nut-noticebar"
-    :class="{ withicon: closeMode, close: closeMode, wrapable: wrapable }"
-    :style="barStyle"
-    @click="handleClick"
-  >
-    <div class="left-icon" v-if="iconShow" :style="{ 'background-image': `url(${iconBg})` }">
-      <nut-icon type="notice" :color="color" size="16px" v-if="!iconBg"></nut-icon>
-    </div>
-    <div ref="wrap" class="wrap">
-      <div
-        ref="content"
-        class="content"
-        :class="[animationClass, { 'nut-ellipsis': !scrollable && !wrapable }]"
-        :style="contentStyle"
-        @animationend="onAnimationEnd"
-        @webkitAnimationEnd="onAnimationEnd"
-      >
-        <slot>{{ text }}</slot>
+  <div>
+    <div
+      v-show="showNoticeBar"
+      class="nut-noticebar"
+      :class="{ withicon: closeMode, close: closeMode, wrapable: wrapable }"
+      :style="barStyle"
+      @click="handleClick"
+      v-if="direction == 'across'"
+    >
+      <div class="left-icon" v-if="iconShow" :style="{ 'background-image': `url(${iconBg})` }">
+        <nut-icon type="notice" :color="color" size="16px" v-if="!iconBg"></nut-icon>
+      </div>
+      <div ref="wrap" class="wrap">
+        <div
+          ref="content"
+          class="content"
+          :class="[animationClass, { 'nut-ellipsis': !scrollable && !wrapable }]"
+          :style="contentStyle"
+          @animationend="onAnimationEnd"
+          @webkitAnimationEnd="onAnimationEnd"
+        >
+          <slot>{{ text }}</slot>
+        </div>
+      </div>
+      <div v-if="closeMode" class="right-icon" @click.stop="onClickIcon">
+        <nut-icon type="cross" :color="color" size="11px"></nut-icon>
       </div>
     </div>
-    <div v-if="closeMode" class="right-icon" @click.stop="onClickIcon">
-      <nut-icon type="cross" :color="color" size="11px"></nut-icon>
+
+    <div class="nut-noticebar-vertical" v-if="scrollList.length > 0 && direction == 'vertical'" :style="barStyle">
+      <template v-if="$slots.default">
+        <div class="horseLamp_list" :style="horseLampStyle">
+          <ScrollItem v-for="(item, index) in scrollList" v-bind:key="index" :style="{ height: height }" :item="item"></ScrollItem>
+        </div>
+      </template>
+
+      <template v-else>
+        <ul class="horseLamp_list" :style="horseLampStyle">
+          <li class="horseLamp_list_item" v-for="(item, index) in scrollList" :key="index" :style="{ height: height }" @click="go(item)">
+            {{ item }}
+          </li>
+        </ul>
+      </template>
+
+      <div class="go" @click="!$slots.rightIcon && handleClickIcon()">
+        <template v-if="$slots.rightIcon">
+          <slot name="rightIcon"></slot>
+        </template>
+        <template v-else-if="closeMode">
+          <nut-icon type="cross" :color="color" size="11px"></nut-icon>
+        </template>
+      </div>
     </div>
   </div>
 </template>
 
 <script>
+import ScrollItem from './item';
 export default {
   name: 'nut-noticebar',
   props: {
+    // 滚动方向  across 横向 vertical 纵向
+    direction: {
+      type: String,
+      default: 'across'
+    },
+    list: {
+      type: Array,
+      default: () => {
+        return [];
+      }
+    },
+    standTime: {
+      type: Number,
+      default: 1000
+    },
+    complexAm: {
+      type: Boolean,
+      default: false
+    },
+    height: {
+      type: Number,
+      default: 40
+    },
     text: {
       type: String,
       default: ''
@@ -72,9 +124,18 @@ export default {
       duration: 0,
       offsetWidth: 0,
       showNoticeBar: true,
-      animationClass: ''
+      animationClass: '',
+
+      animate: false,
+      scrollList: [],
+      distance: 0,
+      timer: null,
+      keepAlive: false
     };
   },
+  components: {
+    ScrollItem: ScrollItem
+  },
   computed: {
     iconShow() {
       if (this.leftIcon == 'close') {
@@ -84,10 +145,15 @@ export default {
       }
     },
     barStyle() {
-      return {
+      let style = {
         color: this.color,
         background: this.background
       };
+
+      if (this.direction == 'vertical') {
+        style.height = `${this.height}px`;
+      }
+      return style;
     },
     contentStyle() {
       return {
@@ -102,6 +168,22 @@ export default {
         iconBg = this.leftIcon;
       }
       return iconBg;
+    },
+    horseLampStyle() {
+      let styles = {};
+      if (this.complexAm) {
+        styles = {
+          transform: `translateY(${this.distance}px)`
+        };
+      } else {
+        if (this.animate) {
+          styles = {
+            transition: `all ${~~(this.height / this.speed / 4)}s`,
+            'margin-top': `-${this.height}px`
+          };
+        }
+      }
+      return styles;
     }
   },
   watch: {
@@ -135,6 +217,23 @@ export default {
         });
       },
       immediate: true
+    },
+    list(newValue, oldValue) {
+      this.scrollList = [].concat(newValue);
+    }
+  },
+  mounted() {
+    console.log(this.direction);
+    if (this.direction == 'vertical') {
+      if (this.$slots.default) {
+        this.scrollList = [].concat(this.$slots.default);
+      } else {
+        this.scrollList = [].concat(this.list);
+      }
+
+      setTimeout(() => {
+        this.complexAm ? this.startRoll() : this.startRollEasy();
+      }, this.standTime);
     }
   },
   methods: {
@@ -151,7 +250,70 @@ export default {
         this.duration = (this.offsetWidth + this.wrapWidth) / this.speed;
         this.animationClass = 'play-infinite';
       });
+    },
+    /**
+     * 利益点滚动方式一
+     */
+    startRollEasy() {
+      this.showhorseLamp();
+      this.timer = setInterval(this.showhorseLamp, ~~(this.height / this.speed / 4) * 1000 + this.standTime);
+    },
+    showhorseLamp() {
+      this.animate = true;
+      setTimeout(() => {
+        this.scrollList.push(this.scrollList[0]);
+        this.scrollList.shift();
+        this.animate = false;
+      }, ~~(this.height / this.speed / 4) * 1000);
+    },
+
+    startRoll() {
+      this.timer = setInterval(() => {
+        let chunk = 100;
+        for (let i = 0; i < chunk; i++) {
+          this.scroll(i, i < chunk - 1 ? false : true);
+        }
+      }, this.standTime + 100 * this.speed);
+    },
+    scroll(n, last) {
+      setTimeout(() => {
+        this.distance -= this.height / 100;
+        if (last) {
+          this.scrollList.push(this.scrollList[0]);
+          this.scrollList.shift();
+          this.distance = 0;
+        }
+      }, n * this.speed);
+    },
+
+    /**
+     * 点击滚动单元
+     */
+    go(item) {
+      this.$emit('click', item);
+    },
+
+    handleClickIcon() {
+      this.$emit('close', this.scrollList[0]);
+    },
+
+    activated() {
+      if (this.keepAlive) {
+        this.keepAlive = false;
+      }
+    },
+
+    deactivated() {
+      this.keepAlive = true;
+      clearInterval(this.timer);
+    },
+
+    destroyed() {
+      clearInterval(this.timer);
     }
   }
 };
 </script>
+<style lang="scss">
+@import 'noticebar.scss';
+</style>