浏览代码

feat: CountDown 新增重置、毫秒级展示、自定义格式等功能 (#1227)

* feat: 冲突解决 (#2)

* fix(uploader): taro h5 env upload error #1096

* fix: taro 3.4.6 sass calc error #1200

* fix(uploader): taro env formdata type error

* chore: add international components signature

* chore: add international components ecard

* chore: add international components timeselect

* chore: add international components sku

* fix: demo textarea 自适应 (#1201)

* feat: add translations chinese traditional

* docs: open international

* fix: input show-word-limit default false (#1203)

* refactor(circleprogress): add features (#1204)

* fix(popup): 修复popup在Taro转H5下的渲染问题

* feat: avatar能力补齐、input doc文档更新 (#1195)

* fix: 修改input单元测试

* fix: 增加avatar group

* fix: 更新input md

* fix: avatar能力补齐

* fix: avatar能力补齐、适配taro

* feat: 迁移 AddressList、Category 组件,适配taro

* fix: 修改input md

Co-authored-by: richard1015 <51844712@qq.com>

* chore: add international components addresslist

Co-authored-by: richard1015 <51844712@qq.com>
Co-authored-by: Ymm <being0008@163.com>
Co-authored-by: Lei Wang <wlchn@qq.com>
Co-authored-by: Drjingfubo <50685476+Drjingfubo@users.noreply.github.com>
Co-authored-by: suzigang <1039168735@qq.com>
Co-authored-by: ailululu <912429321@qq.com>

* feat: picker与DatePicker新增slot插槽

* fix: datepicker 问题修改

* fix: infiniteloading issue 问题修改(#1189)

* fix: countdown 问题修改

* feat: 冲突解决

* feat: 初版

* feat: countdown 单元测试

* feat: 文档补齐

Co-authored-by: richard1015 <51844712@qq.com>
Co-authored-by: Ymm <being0008@163.com>
Co-authored-by: Lei Wang <wlchn@qq.com>
Co-authored-by: Drjingfubo <50685476+Drjingfubo@users.noreply.github.com>
Co-authored-by: suzigang <1039168735@qq.com>
Co-authored-by: ailululu <912429321@qq.com>
yangxiaolu1993 3 年之前
父节点
当前提交
c7d33a3f4e

+ 6 - 4
src/packages/__VUE/countdown/__tests__/countdown.spec.ts

@@ -24,15 +24,17 @@ test('endTime props', async () => {
   expect(wrapper.emitted('on-end')).toBeTruthy();
 });
 
-test('show days props', async () => {
+test('format props', async () => {
   const wrapper = mount(CountDown, {
     props: {
-      showDays: true
+      endTime: Date.now() + 1 * 1000,
+      format: 'DD天HH时mm分ss秒'
     }
   });
-  const prevSnapShot = wrapper.find('.nut-cd-dot');
   await nextTick();
-  expect(prevSnapShot.text() == '天').toBe(true);
+  expect(wrapper.find('.nut-cd-block').exists()).toBeTruthy();
+  const prevSnapShot = wrapper.find('.nut-cd-block');
+  expect(prevSnapShot.text() == '00天00时00分00秒').toBe(true);
 });
 
 test('paused props', async () => {

+ 40 - 16
src/packages/__VUE/countdown/demo.vue

@@ -4,28 +4,27 @@
     <nut-cell>
       <nut-countdown :endTime="end" @on-end="onend"></nut-countdown>
     </nut-cell>
-    <h2>显示天</h2>
-
+    <h2>自定义格式</h2>
     <nut-cell>
-      <nut-countdown :endTime="end" showDays />
+      <nut-countdown :endTime="end" format="DD 天 HH 时 mm 分 ss 秒" />
     </nut-cell>
 
-    <h2>以服务端的时间为准</h2>
+    <h2>毫秒级渲染</h2>
 
     <nut-cell>
-      <nut-countdown :startTime="serverTime" :endTime="end" />
+      <nut-countdown :endTime="end" millisecond format="HH:mm:ss:SS" />
     </nut-cell>
 
-    <h2>显示为 天时分秒</h2>
+    <h2>以服务端的时间为准</h2>
 
     <nut-cell>
-      <nut-countdown showDays showPlainText :endTime="end" />
+      <nut-countdown :startTime="serverTime" :endTime="end" />
     </nut-cell>
 
     <h2>异步更新结束时间</h2>
 
     <nut-cell>
-      <nut-countdown showPlainText :endTime="asyncEnd" />
+      <nut-countdown :endTime="asyncEnd" />
     </nut-cell>
 
     <h2>控制开始和暂停的倒计时</h2>
@@ -37,7 +36,7 @@
       </div>
     </nut-cell>
 
-    <h2>自定义展示</h2>
+    <h2>自定义展示样式</h2>
 
     <nut-cell>
       <span>
@@ -54,24 +53,31 @@
       </span>
     </nut-cell>
 
-    <h2>自定义显示</h2>
-
+    <h2>手动控制</h2>
     <nut-cell>
-      <span>可调用该组件提供的 restTime 方法获取 '天时分秒' 自定义显示</span>
+      <nut-countdown time="20000" ref="CountDown" :autoStart="false" format="ss:SS" />
     </nut-cell>
+
+    <nut-grid :column-num="3">
+      <nut-grid-item><nut-button type="primary" @click="start">开始</nut-button></nut-grid-item>
+      <nut-grid-item><nut-button type="primary" @click="pause">暂停</nut-button></nut-grid-item>
+      <nut-grid-item><nut-button type="primary" @click="reset">重置</nut-button></nut-grid-item>
+    </nut-grid>
   </div>
 </template>
 
 <script lang="ts">
-import { toRefs, onMounted, onUnmounted, reactive } from 'vue';
+import { toRefs, onMounted, ref, reactive } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 const { createDemo } = createComponent('countdown');
 export default createDemo({
   props: {},
   setup() {
+    const CountDown = ref(null);
     const state = reactive({
-      serverTime: Date.now() - 30 * 1000,
-      end: Date.now() + 50 * 1000,
+      serverTime: Date.now() - 20 * 1000,
+      end: Date.now() + 60 * 1000,
+      starttime: Date.now(),
       asyncEnd: 0,
       paused: false,
       resetTime: {
@@ -94,6 +100,20 @@ export default createDemo({
     const onrestart = (v) => {
       console.log('restart: ', v);
     };
+    const start = () => {
+      CountDown.value.start();
+    };
+
+    const pause = () => {
+      CountDown.value.pause();
+    };
+
+    const reset = () => {
+      CountDown.value.reset();
+    };
+    onMounted(() => {
+      console.log(CountDown.value);
+    });
 
     setTimeout(() => {
       state.asyncEnd = Date.now() + 30 * 1000;
@@ -104,7 +124,11 @@ export default createDemo({
       toggle,
       onend,
       onpaused,
-      onrestart
+      onrestart,
+      CountDown,
+      start,
+      pause,
+      reset
     };
   }
 });

+ 96 - 25
src/packages/__VUE/countdown/doc.md

@@ -32,7 +32,7 @@ app.use(CountDown);
   export default {
     setup(props) {
       const state = reactive({
-        end: Date.now() + 50 * 1000,
+        end: Date.now() + 60 * 1000,
       });
       return {
         ...toRefs(state)
@@ -44,13 +44,15 @@ app.use(CountDown);
 ```
 :::
 
-### 显示天
+### 自定义格式
+
+通过设置 format 属性可实现不同的倒计时展示文本
 
 :::demo
 ```html
 <template>
   <nut-cell>
-    <nut-countdown :end-time="end" show-days ></nut-countdown>
+    <nut-countdown :endTime="end" format="DD 天 HH 时 mm 分 ss 秒" />
   </nut-cell>
 </template>
 <script>
@@ -58,7 +60,7 @@ app.use(CountDown);
   export default {
     setup(props) {
       const state = reactive({
-        end: Date.now() + 50 * 1000,
+        end: Date.now() + 60 * 1000,
       });
       return {
         ...toRefs(state)
@@ -69,27 +71,22 @@ app.use(CountDown);
 ```
 :::
 
-
-
-### 以服务端的时间为准
+### 毫秒级渲染
 
 :::demo
 ```html
 <template>
   <nut-cell>
-    <nut-countdown  :start-time="serverTime" :end-time="end" show-days ></nut-countdown>
+    <nut-countdown :endTime="end" millisecond format="HH:mm:ss:SS" />
   </nut-cell>
 </template>
-
-
 <script>
   import { ref,reactive,toRefs } from 'vue';
   export default {
     setup(props) {
       const state = reactive({
-          serverTime: Date.now() - 30 * 1000,
-          end: Date.now() + 50 * 1000,
-        });
+        end: Date.now() + 60 * 1000,
+      });
       return {
         ...toRefs(state)
       };
@@ -98,13 +95,14 @@ app.use(CountDown);
 </script>
 ```
 :::
-### 显示为 天时分秒
+
+### 以服务端的时间为准
 
 :::demo
 ```html
 <template>
   <nut-cell>
-    <nut-countdown show-days show-plain-text  :end-time="end"></nut-countdown>
+    <nut-countdown  :startTime="serverTime" :endTime="end" ></nut-countdown>
   </nut-cell>
 </template>
 
@@ -113,7 +111,8 @@ app.use(CountDown);
   export default {
     setup(props) {
       const state = reactive({
-          end: Date.now() + 50 * 1000,
+          serverTime: Date.now() - 20 * 1000,
+          end: Date.now() + 60 * 1000,
         });
       return {
         ...toRefs(state)
@@ -130,7 +129,7 @@ app.use(CountDown);
 ```html
 <template>
   <nut-cell>
-    <nut-countdown  show-days show-plain-text  :end-time="asyncEnd" ></nut-countdown>
+    <nut-countdown :end-time="asyncEnd" ></nut-countdown>
   </nut-cell>
 </template>
 
@@ -140,7 +139,11 @@ app.use(CountDown);
     setup(props) {
       const state = reactive({
           asyncEnd: 0,
-        });
+      });
+      // 模拟异步时间
+      setTimeout(() => {
+        state.asyncEnd = Date.now() + 30 * 1000;
+      }, 3000);
       return {
         ...toRefs(state)
       };
@@ -150,7 +153,9 @@ app.use(CountDown);
 ```
 :::
 
-### 控制开始和暂停的倒计时
+### 控制开始和暂停倒计时
+
+通过 paused 属性实现倒计时的暂停和重启
 
 :::demo
 ```html
@@ -168,7 +173,7 @@ app.use(CountDown);
     setup(props) {
       const state = reactive({
         paused: false,
-        end: Date.now() + 50 * 1000,
+        end: Date.now() + 60 * 1000,
       });
 
       const toggle = ()=> {
@@ -193,7 +198,7 @@ app.use(CountDown);
 ```
 :::
 
-### 自定义展示
+### 自定义展示样式
 
 :::demo
 ```html
@@ -216,7 +221,7 @@ app.use(CountDown);
   export default {
     setup(props) {
       const state = reactive({
-          end: Date.now() + 50 * 1000,
+          end: Date.now() + 60 * 1000,
           resetTime: {
             d: '1',
             h: '00',
@@ -257,7 +262,48 @@ app.use(CountDown);
 ```
 :::
 
+### 手动控制
+
+通过 ref 获取到组件实例后,可以调用 start、pause、reset 方法。在使用手动控制时,通过 time 属性实现倒计时总时长,单位为毫秒。startTime、endTime 属性失效
 
+:::demo
+```html
+<template>
+  <nut-cell>
+      <nut-countdown time="20000" ref="CountDown" :autoStart="false" format="ss:SS"/>
+  </nut-cell>
+  <nut-grid :column-num="3">
+    <nut-grid-item><nut-button type="primary" @click="start">开始</nut-button></nut-grid-item>
+    <nut-grid-item><nut-button type="primary" @click="pause">暂停</nut-button></nut-grid-item>
+    <nut-grid-item><nut-button type="primary" @click="reset">重置</nut-button></nut-grid-item>
+  </nut-grid>
+</template>
+<script>
+  import { ref,reactive,toRefs } from 'vue';
+  export default {
+    setup(props) {
+      const CountDown = ref(null);
+      const start = () => {
+        CountDown.value.start();
+      };
+      const pause = () => {
+        CountDown.value.pause();
+      };
+      const reset = () => {
+        CountDown.value.reset();
+      };
+      return {
+          toggle,
+          onpaused,
+          onrestart,
+        ...toRefs(state)
+      };
+    }
+  }
+</script>
+
+```
+:::
 ### API
 
 ### Props
@@ -267,10 +313,25 @@ app.use(CountDown);
 | v-model | 当前时间,自定义展示内容时生效 | Object | {}
 | start-time | 开始时间 | String, Number | Date.now()
 | end-time | 结束时间 | String, Number | Date.now()
-| show-days | 是否显示天 | Boolean | false
-| show-plain-text | 显示为纯文本 | Boolean | false
+| format |  时间格式 | String | HH:mm:ss
+| millisecond |  是否开启毫秒级渲染 | Boolean | false
+| auto-start |  是否自动开始倒计时 | Boolean | true
+| time | 倒计时显示时间,单位是毫秒。autoStart 为 false 时生效 | String,Number | 0
 | paused | 是否暂停 | Boolean | false
+| show-days | 是否显示天`(废弃)` | Boolean | false
+| show-plain-text | 显示为纯文本`(废弃)` | Boolean | false
+
+### format 格式
 
+| 格式 | 说明 | 
+| ----- | ----- | 
+| DD | 天数 | 
+| HH | 小时 | 
+| mm | 分钟 | 
+| ss | 秒数 | 
+| S | 毫秒(1位) | 
+| SS | 毫秒(2位) | 
+| SSS | 毫秒(3位) | 
 
 ### Event
 
@@ -278,4 +339,14 @@ app.use(CountDown);
 | ----- | ----- | ----- 
 | on-end | 倒计时结束时 | 剩余时间戳
 | on-paused | 暂停时 | 剩余时间戳
-| on-restart | 暂停时 | 剩余时间戳
+| on-restart | 暂停时 | 剩余时间戳
+
+### 方法
+
+通过 ref 可以获取到 CountDown 实例并调用实例方法。
+
+| 方法明 | 说明 |
+| ----- | ----- | 
+| start | 开始倒计时 | 
+| pause | 暂停倒计时 | 
+| reset | 重设倒计时,若 auto-start 为 true,重设后会自动开始倒计时 | 

+ 185 - 150
src/packages/__VUE/countdown/index.taro.vue

@@ -1,37 +1,17 @@
 <template>
-  <view :class="classes" @click="handleClick">
+  <view :class="classes">
     <template v-if="slots.default">
       <slot></slot>
     </template>
-    <template v-else-if="showPlainText">
-      <view class="nut-cd-block">{{ plainText }}</view>
-    </template>
     <template v-else>
-      <template v-if="resttime.d >= 0 && showDays">
-        <view class="nut-cd-block">{{ resttime.d }}</view>
-        <view class="nut-cd-dot">{{ translate('day') }}</view>
-      </template>
-      <view class="nut-cd-block">{{ resttime.h }}</view
-      ><view class="nut-cd-dot">:</view><view class="nut-cd-block">{{ resttime.m }}</view
-      ><view class="nut-cd-dot">:</view><view class="nut-cd-block">{{ resttime.s }}</view>
+      <view class="nut-cd-block" v-html="renderTime"></view>
     </template>
   </view>
 </template>
 <script lang="ts">
-import {
-  toRefs,
-  onMounted,
-  onUnmounted,
-  reactive,
-  computed,
-  CSSProperties,
-  onActivated,
-  onDeactivated,
-  ref,
-  watch,
-  vModelText
-} from 'vue';
+import { toRefs, computed, watch, reactive, onBeforeMount, onMounted } from 'vue';
 import { createComponent } from '@/packages/utils/create';
+import { padZero, getTimeStamp } from './util';
 const { componentName, create, translate } = createComponent('countdown');
 
 export default create({
@@ -46,14 +26,7 @@ export default create({
       default: false,
       type: Boolean
     },
-    showDays: {
-      default: false,
-      type: Boolean
-    },
-    showPlainText: {
-      default: false,
-      type: Boolean
-    },
+
     startTime: {
       // 可以是服务器当前时间
       type: [Number, String],
@@ -68,48 +41,186 @@ export default create({
         const dateStr = new Date(v).toString().toLowerCase();
         return dateStr !== 'invalid date';
       }
+    },
+    // 是否开启毫秒
+    millisecond: {
+      default: false,
+      type: Boolean
+    },
+    // 时间格式化
+    format: {
+      type: String,
+      default: 'HH:mm:ss'
+    },
+    autoStart: {
+      type: Boolean,
+      default: true
+    },
+
+    // 倒计时时长,单位毫秒
+    time: {
+      type: [Number, String],
+      default: 0
     }
   },
-  components: {},
   emits: ['input', 'on-end', 'on-restart', 'on-paused', 'update:modelValue'],
 
-  setup(props, { emit, slots }) {
-    // console.log('componentName', componentName);
-
+  setup(props: any, { emit, slots }) {
     const state = reactive({
-      restTime: 0,
-      p: 0,
-      _curr: 0,
-      timer: null
+      restTime: 0, // 倒计时剩余时间时间
+      timer: null,
+      counting: !props.paused && props.autoStart, // 是否处于倒计时中
+      handleEndTime: Date.now(), // 最终截止时间
+      diffTime: 0 // 设置了 startTime 时,与 date.now() 的差异
     });
 
-    const resttime = computed(() => {
-      const rest = restTime(state.restTime);
-      const { d, h, m, s } = rest;
-      if (!props.showDays && d > 0) {
-        rest.h = fill2(Number(rest.h) + d * 24);
-        rest.d = 0;
-      }
-      return rest;
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
     });
 
-    const plainText = computed(() => {
-      const { d, h, m, s } = resttime.value;
-
-      return `${d > 0 && props.showDays ? d + translate('day') + h : h}${translate('hour')}${m}${translate(
-        'minute'
-      )}${s}${translate('second')}`;
+    const renderTime = computed(() => {
+      return formatRemainTime(state.restTime);
     });
 
-    watch(
-      () => props.value,
-      (value) => {}
-    );
+    // 倒计时 interval
+    const initTime = () => {
+      state.handleEndTime = props.endTime;
+      state.diffTime = Date.now() - getTimeStamp(props.startTime); // 时间差
+      if (!state.counting) state.counting = true;
+      tick();
+    };
+
+    const tick = () => {
+      if (window !== undefined) {
+        (state.timer as any) = requestAnimationFrame(() => {
+          if (state.counting) {
+            const currentTime = Date.now() - state.diffTime;
+            const remainTime = Math.max(state.handleEndTime - currentTime, 0);
+
+            state.restTime = remainTime;
+
+            if (!remainTime) {
+              state.counting = false;
+              pause();
+              emit('on-end');
+            }
+
+            if (remainTime > 0) {
+              tick();
+            }
+          }
+        });
+      }
+    };
+
+    // 将倒计时剩余时间格式化   参数: t  时间戳  type custom 自定义类型
+    const formatRemainTime = (t: number, type?: string) => {
+      const ts = t;
+      let rest = {
+        d: 0,
+        h: 0,
+        m: 0,
+        s: 0,
+        ms: 0
+      };
+
+      const SECOND = 1000;
+      const MINUTE = 60 * SECOND;
+      const HOUR = 60 * MINUTE;
+      const DAY = 24 * HOUR;
+
+      if (ts > 0) {
+        rest.d = ts >= SECOND ? Math.floor(ts / DAY) : 0;
+        rest.h = Math.floor((ts % DAY) / HOUR);
+        rest.m = Math.floor((ts % HOUR) / MINUTE);
+        rest.s = Math.floor((ts % MINUTE) / SECOND);
+        rest.ms = Math.floor(ts % SECOND);
+      }
+
+      return type == 'custom' ? rest : parseFormat({ ...rest });
+    };
+
+    const parseFormat = (time: { d: number; h: number; m: number; s: number; ms: number }) => {
+      let { d, h, m, s, ms } = time;
+      let format = props.format;
+
+      if (format.includes('DD')) {
+        format = format.replace('DD', padZero(d));
+      } else {
+        h += Number(d) * 24;
+      }
+
+      if (format.includes('HH')) {
+        format = format.replace('HH', padZero(h));
+      } else {
+        m += Number(h) * 60;
+      }
+
+      if (format.includes('mm')) {
+        format = format.replace('mm', padZero(m));
+      } else {
+        s += Number(m) * 60;
+      }
+
+      if (format.includes('ss')) {
+        format = format.replace('ss', padZero(s));
+      } else {
+        ms += Number(s) * 1000;
+      }
+
+      if (format.includes('S')) {
+        const msC = padZero(ms, 3).toString();
+
+        if (format.includes('SSS')) {
+          format = format.replace('SSS', msC);
+        } else if (format.includes('SS')) {
+          format = format.replace('SS', msC.slice(0, 2));
+        } else if (format.includes('S')) {
+          format = format.replace('SS', msC.slice(0, 1));
+        }
+      }
+      return format;
+    };
+
+    // 开始
+    const start = () => {
+      if (!state.counting && !props.autoStart) {
+        state.counting = true;
+        state.handleEndTime = Date.now() + Number(state.restTime);
+        tick();
+        emit('on-restart', state.restTime);
+      }
+    };
+    // 暂定
+    const pause = () => {
+      cancelAnimationFrame(state.timer as any);
+      state.counting = false;
+      emit('on-paused', state.restTime);
+    };
+
+    //重置
+    const reset = () => {
+      if (!props.autoStart) {
+        pause();
+        state.restTime = props.time;
+      }
+    };
+
+    onBeforeMount(() => {
+      if (props.autoStart) {
+        initTime();
+      } else {
+        state.restTime = props.time;
+      }
+    });
 
     watch(
       () => state.restTime,
       (value) => {
-        let tranTime = restTime(value);
+        let tranTime = formatRemainTime(value, 'custom');
         emit('update:modelValue', tranTime);
         emit('input', tranTime);
       }
@@ -119,10 +230,15 @@ export default create({
       () => props.paused,
       (v, ov) => {
         if (!ov) {
-          state._curr = getTimeStamp();
-          emit('on-paused', state.restTime);
+          if (state.counting) {
+            pause();
+          }
         } else {
-          state.p += getTimeStamp() - state._curr;
+          if (!state.counting) {
+            state.counting = true;
+            state.handleEndTime = Date.now() + Number(state.restTime);
+            tick();
+          }
           emit('on-restart', state.restTime);
         }
       }
@@ -131,108 +247,27 @@ export default create({
     watch(
       () => props.endTime,
       (value) => {
-        initTimer();
+        initTime();
       }
     );
 
     watch(
       () => props.startTime,
       (value) => {
-        initTimer();
+        initTime();
       }
     );
 
-    const classes = computed(() => {
-      const prefixCls = componentName;
-      return {
-        [prefixCls]: true
-      };
-    });
-
-    const getTimeStamp = (timeStr?: string | number) => {
-      if (!timeStr) return Date.now();
-      let t = timeStr;
-      t = t > 0 ? +t : t.toString().replace(/\-/g, '/');
-      return new Date(t).getTime();
-    };
-
-    const initTimer = () => {
-      const delay = 1000;
-      const curr = Date.now();
-      const start = getTimeStamp(props.startTime || curr);
-      const end = getTimeStamp(props.endTime || curr);
-      const diffTime = curr - start;
-
-      state.restTime = end - (start + diffTime);
-      clearInterval(state.timer);
-      state.timer = null;
-      (state.timer as any) = setInterval(() => {
-        if (!props.paused) {
-          let restTime = end - (Date.now() - state.p + diffTime);
-          state.restTime = restTime;
-          if (restTime < delay) {
-            state.restTime = 0;
-            emit('on-end');
-            clearInterval(state.timer as any);
-          }
-        } else {
-          // 暂停
-        }
-      }, delay);
-    };
-
-    const fill2 = (v: any) => {
-      v += '';
-      while (v.length < 2) {
-        v = '0' + v;
-      }
-      return v;
-    };
-    const restTime = (t: any) => {
-      const ts = t;
-      let rest = {
-        d: '-',
-        h: '--',
-        m: '--',
-        s: '--'
-      };
-      if (ts === 0) {
-        rest = {
-          d: '0',
-          h: '00',
-          m: '00',
-          s: '00'
-        };
-      }
-      if (ts) {
-        const ds = 24 * 60 * 60 * 1000;
-        const hs = 60 * 60 * 1000;
-        const ms = 60 * 1000;
-
-        const d = ts >= ds ? parseInt(ts / ds) : 0;
-        const h = ts - d * ds >= hs ? parseInt((ts - d * ds) / hs) : 0;
-        const m = ts - d * ds - h * hs >= ms ? parseInt((ts - d * ds - h * hs) / ms) : 0;
-        const s = Math.round((ts - d * ds - h * hs - m * ms) / 1000);
-
-        if (d >= 0) rest.d = d + '';
-        if (h >= 0) rest.h = fill2(h);
-        if (m >= 0) rest.m = fill2(m);
-        if (s >= 0) rest.s = fill2(s);
-      }
-      return rest;
-    };
-
-    initTimer();
-
     return {
       ...toRefs(props),
       slots,
       classes,
       getTimeStamp,
-      initTimer,
-      resttime,
-      plainText,
-      translate
+      start,
+      pause,
+      renderTime,
+      translate,
+      reset
     };
   }
 });

+ 185 - 150
src/packages/__VUE/countdown/index.vue

@@ -1,39 +1,18 @@
 <template>
-  <view :class="classes" @click="handleClick">
+  <view :class="classes">
     <template v-if="slots.default">
       <slot></slot>
     </template>
-    <template v-else-if="showPlainText">
-      <view class="nut-cd-block">{{ plainText }}</view>
-    </template>
     <template v-else>
-      <template v-if="resttime.d >= 0 && showDays">
-        <view class="nut-cd-block">{{ resttime.d }}</view>
-        <view class="nut-cd-dot">{{ translate('day') }}</view>
-      </template>
-      <view class="nut-cd-block">{{ resttime.h }}</view
-      ><view class="nut-cd-dot">:</view><view class="nut-cd-block">{{ resttime.m }}</view
-      ><view class="nut-cd-dot">:</view><view class="nut-cd-block">{{ resttime.s }}</view>
+      <view class="nut-cd-block" v-html="renderTime"></view>
     </template>
   </view>
 </template>
 <script lang="ts">
-import {
-  toRefs,
-  onMounted,
-  onUnmounted,
-  reactive,
-  computed,
-  CSSProperties,
-  onActivated,
-  onDeactivated,
-  ref,
-  watch,
-  vModelText
-} from 'vue';
+import { toRefs, computed, watch, reactive, onBeforeMount, onMounted } from 'vue';
 import { createComponent } from '@/packages/utils/create';
+import { padZero, getTimeStamp } from './util';
 const { componentName, create, translate } = createComponent('countdown');
-
 export default create({
   props: {
     modelValue: {
@@ -46,14 +25,7 @@ export default create({
       default: false,
       type: Boolean
     },
-    showDays: {
-      default: false,
-      type: Boolean
-    },
-    showPlainText: {
-      default: false,
-      type: Boolean
-    },
+
     startTime: {
       // 可以是服务器当前时间
       type: [Number, String],
@@ -68,46 +40,186 @@ export default create({
         const dateStr = new Date(v).toString().toLowerCase();
         return dateStr !== 'invalid date';
       }
+    },
+    // 是否开启毫秒
+    millisecond: {
+      default: false,
+      type: Boolean
+    },
+    // 时间格式化
+    format: {
+      type: String,
+      default: 'HH:mm:ss'
+    },
+    autoStart: {
+      type: Boolean,
+      default: true
+    },
+
+    // 倒计时时长,单位毫秒
+    time: {
+      type: [Number, String],
+      default: 0
     }
   },
-  components: {},
   emits: ['input', 'on-end', 'on-restart', 'on-paused', 'update:modelValue'],
 
-  setup(props, { emit, slots }) {
+  setup(props: any, { emit, slots }) {
     const state = reactive({
-      restTime: 0,
-      p: 0,
-      _curr: 0,
-      timer: null
+      restTime: 0, // 倒计时剩余时间时间
+      timer: null,
+      counting: !props.paused && props.autoStart, // 是否处于倒计时中
+      handleEndTime: Date.now(), // 最终截止时间
+      diffTime: 0 // 设置了 startTime 时,与 date.now() 的差异
     });
 
-    const resttime = computed(() => {
-      const rest = restTime(state.restTime);
-      const { d, h, m, s } = rest;
-      if (!props.showDays && d > 0) {
-        rest.h = fill2(Number(rest.h) + d * 24);
-        rest.d = 0;
-      }
-      return rest;
+    const classes = computed(() => {
+      const prefixCls = componentName;
+      return {
+        [prefixCls]: true
+      };
     });
 
-    const plainText = computed(() => {
-      const { d, h, m, s } = resttime.value;
-
-      return `${d > 0 && props.showDays ? d + translate('day') + h : h}${translate('hour')}${m}${translate(
-        'minute'
-      )}${s}${translate('second')}`;
+    const renderTime = computed(() => {
+      return formatRemainTime(state.restTime);
     });
 
-    watch(
-      () => props.value,
-      (value) => {}
-    );
+    // 倒计时 interval
+    const initTime = () => {
+      state.handleEndTime = props.endTime;
+      state.diffTime = Date.now() - getTimeStamp(props.startTime); // 时间差
+      if (!state.counting) state.counting = true;
+      tick();
+    };
+
+    const tick = () => {
+      if (window !== undefined) {
+        (state.timer as any) = requestAnimationFrame(() => {
+          if (state.counting) {
+            const currentTime = Date.now() - state.diffTime;
+            const remainTime = Math.max(state.handleEndTime - currentTime, 0);
+
+            state.restTime = remainTime;
+
+            if (!remainTime) {
+              state.counting = false;
+              pause();
+              emit('on-end');
+            }
+
+            if (remainTime > 0) {
+              tick();
+            }
+          }
+        });
+      }
+    };
+
+    // 将倒计时剩余时间格式化   参数: t  时间戳  type custom 自定义类型
+    const formatRemainTime = (t: number, type?: string) => {
+      const ts = t;
+      let rest = {
+        d: 0,
+        h: 0,
+        m: 0,
+        s: 0,
+        ms: 0
+      };
+
+      const SECOND = 1000;
+      const MINUTE = 60 * SECOND;
+      const HOUR = 60 * MINUTE;
+      const DAY = 24 * HOUR;
+
+      if (ts > 0) {
+        rest.d = ts >= SECOND ? Math.floor(ts / DAY) : 0;
+        rest.h = Math.floor((ts % DAY) / HOUR);
+        rest.m = Math.floor((ts % HOUR) / MINUTE);
+        rest.s = Math.floor((ts % MINUTE) / SECOND);
+        rest.ms = Math.floor(ts % SECOND);
+      }
+
+      return type == 'custom' ? rest : parseFormat({ ...rest });
+    };
+
+    const parseFormat = (time: { d: number; h: number; m: number; s: number; ms: number }) => {
+      let { d, h, m, s, ms } = time;
+      let format = props.format;
+
+      if (format.includes('DD')) {
+        format = format.replace('DD', padZero(d));
+      } else {
+        h += Number(d) * 24;
+      }
+
+      if (format.includes('HH')) {
+        format = format.replace('HH', padZero(h));
+      } else {
+        m += Number(h) * 60;
+      }
+
+      if (format.includes('mm')) {
+        format = format.replace('mm', padZero(m));
+      } else {
+        s += Number(m) * 60;
+      }
+
+      if (format.includes('ss')) {
+        format = format.replace('ss', padZero(s));
+      } else {
+        ms += Number(s) * 1000;
+      }
+
+      if (format.includes('S')) {
+        const msC = padZero(ms, 3).toString();
+
+        if (format.includes('SSS')) {
+          format = format.replace('SSS', msC);
+        } else if (format.includes('SS')) {
+          format = format.replace('SS', msC.slice(0, 2));
+        } else if (format.includes('S')) {
+          format = format.replace('SS', msC.slice(0, 1));
+        }
+      }
+      return format;
+    };
+
+    // 开始
+    const start = () => {
+      if (!state.counting && !props.autoStart) {
+        state.counting = true;
+        state.handleEndTime = Date.now() + Number(state.restTime);
+        tick();
+        emit('on-restart', state.restTime);
+      }
+    };
+    // 暂定
+    const pause = () => {
+      cancelAnimationFrame(state.timer as any);
+      state.counting = false;
+      emit('on-paused', state.restTime);
+    };
+
+    //重置
+    const reset = () => {
+      if (!props.autoStart) {
+        pause();
+        state.restTime = props.time;
+      }
+    };
+
+    onBeforeMount(() => {
+      if (props.autoStart) {
+        initTime();
+      } else {
+        state.restTime = props.time;
+      }
+    });
 
     watch(
       () => state.restTime,
       (value) => {
-        let tranTime = restTime(value);
+        let tranTime = formatRemainTime(value, 'custom');
         emit('update:modelValue', tranTime);
         emit('input', tranTime);
       }
@@ -117,10 +229,15 @@ export default create({
       () => props.paused,
       (v, ov) => {
         if (!ov) {
-          state._curr = getTimeStamp();
-          emit('on-paused', state.restTime);
+          if (state.counting) {
+            pause();
+          }
         } else {
-          state.p += getTimeStamp() - state._curr;
+          if (!state.counting) {
+            state.counting = true;
+            state.handleEndTime = Date.now() + Number(state.restTime);
+            tick();
+          }
           emit('on-restart', state.restTime);
         }
       }
@@ -129,109 +246,27 @@ export default create({
     watch(
       () => props.endTime,
       (value) => {
-        initTimer();
+        initTime();
       }
     );
 
     watch(
       () => props.startTime,
       (value) => {
-        initTimer();
+        initTime();
       }
     );
 
-    const classes = computed(() => {
-      const prefixCls = componentName;
-      return {
-        [prefixCls]: true
-      };
-    });
-
-    const getTimeStamp = (timeStr?: string | number) => {
-      if (!timeStr) return Date.now();
-      let t = timeStr;
-      t = t > 0 ? +t : t.toString().replace(/\-/g, '/');
-      return new Date(t).getTime();
-    };
-
-    const initTimer = () => {
-      const delay = 1000;
-      const curr = Date.now();
-      const start = getTimeStamp(props.startTime || curr);
-      const end = getTimeStamp(props.endTime || curr);
-      const diffTime = curr - start;
-
-      state.restTime = end - (start + diffTime);
-      clearInterval(state.timer);
-      state.timer = null;
-      (state.timer as any) = setInterval(() => {
-        if (!props.paused) {
-          let restTime = end - (Date.now() - state.p + diffTime);
-          state.restTime = restTime;
-
-          if (restTime < 0) {
-            state.restTime = 0;
-            emit('on-end');
-            clearInterval(state.timer as any);
-          }
-        } else {
-          // 暂停
-        }
-      }, delay);
-    };
-
-    const fill2 = (v: any) => {
-      v += '';
-      while (v.length < 2) {
-        v = '0' + v;
-      }
-      return v;
-    };
-    const restTime = (t: any) => {
-      const ts = t;
-      let rest = {
-        d: '-',
-        h: '--',
-        m: '--',
-        s: '--'
-      };
-      if (ts === 0) {
-        rest = {
-          d: '0',
-          h: '00',
-          m: '00',
-          s: '00'
-        };
-      }
-      if (ts) {
-        const ds = 24 * 60 * 60 * 1000;
-        const hs = 60 * 60 * 1000;
-        const ms = 60 * 1000;
-
-        const d = ts >= ds ? parseInt(ts / ds) : 0;
-        const h = ts - d * ds >= hs ? parseInt((ts - d * ds) / hs) : 0;
-        const m = ts - d * ds - h * hs >= ms ? parseInt((ts - d * ds - h * hs) / ms) : 0;
-        const s = Math.round((ts - d * ds - h * hs - m * ms) / 1000);
-
-        if (d >= 0) rest.d = d + '';
-        if (h >= 0) rest.h = fill2(h);
-        if (m >= 0) rest.m = fill2(m);
-        if (s >= 0) rest.s = fill2(s);
-      }
-      return rest;
-    };
-
-    initTimer();
-
     return {
       ...toRefs(props),
       slots,
       classes,
       getTimeStamp,
-      initTimer,
-      resttime,
-      plainText,
-      translate
+      start,
+      pause,
+      renderTime,
+      translate,
+      reset
     };
   }
 });

+ 15 - 0
src/packages/__VUE/countdown/util.ts

@@ -0,0 +1,15 @@
+export const padZero = (num: number | string, length = 2) => {
+  num += '';
+  while ((num as string).length < length) {
+    num = '0' + num;
+  }
+  return num.toString();
+};
+
+// 时间戳转换 或 获取当前时间的时间戳
+export const getTimeStamp = (timeStr?: string | number) => {
+  if (!timeStr) return Date.now();
+  let t = timeStr;
+  t = t > 0 ? +t : t.toString().replace(/\-/g, '/');
+  return new Date(t).getTime();
+};

+ 4 - 4
src/packages/__VUE/infiniteloading/index.vue

@@ -225,10 +225,10 @@ export default create({
       }
     };
 
-    // 滚动监听对象
-    const getParentElement = (el: HTMLElement) => {
-      return !!props.containerId ? document.querySelector(`#${props.containerId}`) : el && el.parentNode;
-    };
+    // // 滚动监听对象
+    // const getParentElement = (el: HTMLElement) => {
+    //   return !!props.containerId ? document.querySelector(`#${props.containerId}`) : el && el.parentNode;
+    // };
 
     onMounted(() => {
       const parentElement = getParentElement(state.scroller as HTMLElement) as Node & ParentNode;

+ 2 - 2
src/sites/mobile-taro/vue/project.private.config.json

@@ -25,8 +25,8 @@
           "scene": null
         },
         {
-          "name": "dentry/pages/pickers/index",
-          "pathName": "dentry/pages/picker/index",
+          "name": "exhibition/pages/countdown/index",
+          "pathName": "exhibition/pages/countdown/index",
           "query": "",
           "scene": null
         },

+ 40 - 28
src/sites/mobile-taro/vue/src/exhibition/pages/countdown/index.vue

@@ -4,28 +4,27 @@
     <nut-cell>
       <nut-countdown :endTime="end" @on-end="onend"></nut-countdown>
     </nut-cell>
-    <h2>显示天</h2>
-
+    <h2>自定义格式</h2>
     <nut-cell>
-      <nut-countdown :endTime="end" showDays />
+      <nut-countdown :endTime="end" format="DD 天 HH 时 mm 分 ss 秒" />
     </nut-cell>
 
-    <h2>以服务端的时间为准</h2>
+    <h2>毫秒级渲染</h2>
 
     <nut-cell>
-      <nut-countdown :startTime="serverTime" :endTime="end" />
+      <nut-countdown :endTime="end" millisecond format="HH:mm:ss:SS" />
     </nut-cell>
 
-    <h2>显示为 天时分秒</h2>
+    <h2>以服务端的时间为准</h2>
 
     <nut-cell>
-      <nut-countdown showDays showPlainText :endTime="end" />
+      <nut-countdown :startTime="serverTime" :endTime="end" />
     </nut-cell>
 
     <h2>异步更新结束时间</h2>
 
     <nut-cell>
-      <nut-countdown showPlainText :endTime="asyncEnd" />
+      <nut-countdown :endTime="asyncEnd" />
     </nut-cell>
 
     <h2>控制开始和暂停的倒计时</h2>
@@ -37,7 +36,7 @@
       </div>
     </nut-cell>
 
-    <h2>自定义展示</h2>
+    <h2>自定义展示样式</h2>
 
     <nut-cell>
       <span>
@@ -54,35 +53,30 @@
       </span>
     </nut-cell>
 
-    <h2>自定义显示</h2>
-
+    <h2>手动控制</h2>
     <nut-cell>
-      <span>可调用该组件提供的 restTime 方法获取 '天时分秒' 自定义显示</span>
+      <nut-countdown time="20000" ref="CountDown" :autoStart="false" format="ss:SS" />
     </nut-cell>
+
+    <nut-grid :column-num="3">
+      <nut-grid-item><nut-button type="primary" @click="start">开始</nut-button></nut-grid-item>
+      <nut-grid-item><nut-button type="primary" @click="pause">暂停</nut-button></nut-grid-item>
+      <nut-grid-item><nut-button type="primary" @click="reset">重置</nut-button></nut-grid-item>
+    </nut-grid>
   </div>
 </template>
 
 <script lang="ts">
-import {
-  toRefs,
-  onMounted,
-  onUnmounted,
-  reactive,
-  computed,
-  CSSProperties,
-  onActivated,
-  onDeactivated,
-  ref,
-  watch,
-  h
-} from 'vue';
+import { toRefs, onMounted, ref, reactive } from 'vue';
 
 export default {
   props: {},
   setup() {
+    const CountDown = ref(null);
     const state = reactive({
-      serverTime: Date.now() - 30 * 1000,
-      end: Date.now() + 50 * 1000,
+      serverTime: Date.now() - 20 * 1000,
+      end: Date.now() + 60 * 1000,
+      starttime: Date.now(),
       asyncEnd: 0,
       paused: false,
       resetTime: {
@@ -105,6 +99,20 @@ export default {
     const onrestart = (v) => {
       console.log('restart: ', v);
     };
+    const start = () => {
+      CountDown.value.start();
+    };
+
+    const pause = () => {
+      CountDown.value.pause();
+    };
+
+    const reset = () => {
+      CountDown.value.reset();
+    };
+    onMounted(() => {
+      console.log(CountDown.value);
+    });
 
     setTimeout(() => {
       state.asyncEnd = Date.now() + 30 * 1000;
@@ -115,7 +123,11 @@ export default {
       toggle,
       onend,
       onpaused,
-      onrestart
+      onrestart,
+      CountDown,
+      start,
+      pause,
+      reset
     };
   }
 };