diff --git a/example/app.json b/example/app.json
index 121399bb..0232bc70 100644
--- a/example/app.json
+++ b/example/app.json
@@ -6,6 +6,7 @@
"pages/card/index",
"pages/cell/index",
"pages/col/index",
+ "pages/count-down/index",
"pages/dialog/index",
"pages/field/index",
"pages/icon/index",
@@ -61,6 +62,7 @@
"van-checkbox": "./dist/checkbox/index",
"van-checkbox-group": "./dist/checkbox-group/index",
"van-col": "./dist/col/index",
+ "van-count-down": "./dist/count-down/index",
"van-dialog": "./dist/dialog/index",
"van-divider": "./dist/divider/index",
"van-field": "./dist/field/index",
diff --git a/example/config.js b/example/config.js
index 951e7903..f650ef67 100644
--- a/example/config.js
+++ b/example/config.js
@@ -121,6 +121,10 @@ export default [
path: '/collapse',
title: 'Collapse 折叠面板'
},
+ {
+ path: '/count-down',
+ title: 'CountDown 倒计时'
+ },
{
path: '/notice-bar',
title: 'NoticeBar 通告栏'
diff --git a/example/pages/count-down/index.js b/example/pages/count-down/index.js
new file mode 100644
index 00000000..c992a982
--- /dev/null
+++ b/example/pages/count-down/index.js
@@ -0,0 +1,34 @@
+import Page from '../../common/page';
+import Toast from '../../dist/toast/toast';
+
+Page({
+ data: {
+ time: 30 * 60 * 60 * 1000,
+ timeData: {}
+ },
+
+ onChange(e) {
+ this.setData({
+ timeData: e.detail
+ });
+ },
+
+ start() {
+ const countDown = this.selectComponent('.control-count-down');
+ countDown.start();
+ },
+
+ pause() {
+ const countDown = this.selectComponent('.control-count-down');
+ countDown.pause();
+ },
+
+ reset() {
+ const countDown = this.selectComponent('.control-count-down');
+ countDown.reset();
+ },
+
+ finished() {
+ Toast('倒计时结束');
+ }
+});
diff --git a/example/pages/count-down/index.json b/example/pages/count-down/index.json
new file mode 100644
index 00000000..f4cd7449
--- /dev/null
+++ b/example/pages/count-down/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "CountDown 倒计时"
+}
diff --git a/example/pages/count-down/index.wxml b/example/pages/count-down/index.wxml
new file mode 100644
index 00000000..d980b3be
--- /dev/null
+++ b/example/pages/count-down/index.wxml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ timeData.hours }}
+ {{ timeData.minutes }}
+ {{ timeData.seconds }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/pages/count-down/index.wxss b/example/pages/count-down/index.wxss
new file mode 100644
index 00000000..ca7b0866
--- /dev/null
+++ b/example/pages/count-down/index.wxss
@@ -0,0 +1,15 @@
+.van-count-down {
+ margin: 0 16px 10px;
+}
+
+.item {
+ display: inline-block;
+ width: 22px;
+ margin-right: 5px;
+ color: #fff;
+ font-size: 12px;
+ text-align: center;
+ background-color: #1989fa;
+ border-radius: 2px;
+}
+
diff --git a/packages/common/style/var.less b/packages/common/style/var.less
index 8099a1a3..4ea26a99 100644
--- a/packages/common/style/var.less
+++ b/packages/common/style/var.less
@@ -88,6 +88,11 @@
@collapse-item-content-background-color: @white;
@collapse-item-title-disabled-color: @gray;
+// CountDown
+@count-down-text-color: @text-color;
+@count-down-font-size: @font-size-md;
+@count-down-line-height: 20px;
+
// Info
@info-size: 16px;
@info-color: @white;
diff --git a/packages/count-down/README.md b/packages/count-down/README.md
new file mode 100644
index 00000000..8566b1da
--- /dev/null
+++ b/packages/count-down/README.md
@@ -0,0 +1,180 @@
+# CountDown 倒计时
+
+### 引入
+
+在`app.json`或`index.json`中引入组件,详细介绍见[快速上手](#/quickstart#yin-ru-zu-jian)
+
+```json
+"usingComponents": {
+ "van-count-down": "path/to/vant-weapp/dist/count-down/index"
+}
+```
+
+## 代码演示
+
+### 基本用法
+
+`time`属性表示倒计时总时长,单位为毫秒
+
+```html
+
+```
+
+```js
+Page({
+ data: {
+ time: 30 * 60 * 60 * 1000
+ }
+});
+```
+
+### 自定义格式
+
+通过`format`属性设置倒计时文本的内容
+
+```html
+
+```
+
+### 毫秒级渲染
+
+倒计时默认每秒渲染一次,设置`millisecond`属性可以开启毫秒级渲染
+
+```html
+
+```
+
+### 自定义样式
+
+通过`bind:change`事件获取`timeData`对象,格式见下方表格
+
+```html
+
+ {{ timeData.hours }}
+ {{ timeData.minutes }}
+ {{ timeData.seconds }}
+
+```
+
+```js
+
+Page({
+ data: {
+ time: 30 * 60 * 60 * 1000,
+ timeData: {}
+ },
+
+ onChange(e) {
+ this.setData({
+ timeData: e.detail
+ });
+ }
+});
+```
+
+```css
+.item {
+ display: inline-block;
+ width: 22px;
+ margin-right: 5px;
+ color: #fff;
+ font-size: 12px;
+ text-align: center;
+ background-color: #1989fa;
+ border-radius: 2px;
+}
+```
+
+### 手动控制
+
+通过 `selectComponent` 选择器获取到组件实例后,可以调用`start`、`pause`、`reset`方法
+
+```html
+
+
+
+
+
+
+
+```
+
+```js
+Page({
+ start() {
+ const countDown = this.selectComponent('.control-count-down');
+ countDown.start();
+ },
+
+ pause() {
+ const countDown = this.selectComponent('.control-count-down');
+ countDown.pause();
+ },
+
+ reset() {
+ const countDown = this.selectComponent('.control-count-down');
+ countDown.reset();
+ },
+
+ finished() {
+ Toast('倒计时结束');
+ }
+});
+```
+
+## API
+
+### Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+|------|------|------|------|------|
+| time | 倒计时时长,单位毫秒 | *number* | - | - |
+| format | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 | *string* | `HH:mm:ss` | - |
+| auto-start | 是否自动开始倒计时 | *boolean* | `true` | - |
+| millisecond | 是否开启毫秒级渲染 | *boolean* | `false` | - |
+| useCustom | 是否自定义样式 | *boolean* | `false` | - |
+
+### Events
+
+| 事件名 | 说明 | 回调参数 |
+|------|------|------|
+| change | 时间变化时触发 | timeData |
+| finish | 倒计时结束时触发 | - |
+
+### timeData 格式
+
+| 名称 | 说明 | 类型 |
+|------|------|------|
+| days | 剩余天数 | *number* |
+| hours | 剩余小时 | *number* |
+| minutes | 剩余分钟 | *number* |
+| seconds | 剩余秒数 | *number* |
+| milliseconds | 剩余毫秒 | *number* |
+
+### 方法
+
+通过 selectComponent 可以获取到 CountDown 实例并调用实例方法
+
+| 方法名 | 参数 | 返回值 | 介绍 |
+|------|------|------|------|
+| start | - | - | 开始倒计时 |
+| pause | - | - | 暂停倒计时 |
+| reset | - | - | 重设倒计时,若`auto-start`为`true`,重设后会自动开始倒计时 |
diff --git a/packages/count-down/index.json b/packages/count-down/index.json
new file mode 100644
index 00000000..467ce294
--- /dev/null
+++ b/packages/count-down/index.json
@@ -0,0 +1,3 @@
+{
+ "component": true
+}
diff --git a/packages/count-down/index.less b/packages/count-down/index.less
new file mode 100644
index 00000000..8e49c01b
--- /dev/null
+++ b/packages/count-down/index.less
@@ -0,0 +1,7 @@
+@import '../common/style/var.less';
+
+.van-count-down {
+ color: @count-down-text-color;
+ font-size: @count-down-font-size;
+ line-height: @count-down-line-height;
+}
diff --git a/packages/count-down/index.ts b/packages/count-down/index.ts
new file mode 100644
index 00000000..11a7638a
--- /dev/null
+++ b/packages/count-down/index.ts
@@ -0,0 +1,112 @@
+import { VantComponent } from '../common/component';
+import { isSameSecond, parseFormat, parseTimeData } from './utils';
+
+VantComponent({
+ props: {
+ time: {
+ type: Number,
+ observer: 'reset',
+ },
+ format: {
+ type: String,
+ value: 'HH:mm:ss'
+ },
+ useCustom: {
+ type: Boolean,
+ value: false,
+ },
+ autoStart: {
+ type: Boolean,
+ value: true,
+ },
+ millisecond: {
+ type: Boolean,
+ value: false,
+ },
+ },
+
+ data: {
+ timeData: parseTimeData(0),
+ formattedTime: '0'
+ },
+
+ methods: {
+ // 开始
+ start() {
+ if (this.counting) {
+ return;
+ }
+
+ this.counting = true;
+ this.endTime = Date.now() + this.remain;
+ this.tick();
+ },
+
+ // 暂停
+ pause() {
+ this.counting = false;
+ clearTimeout(this.tid);
+ },
+
+ // 重置
+ reset() {
+ this.pause();
+ this.remain = this.data.time;
+ this.setRemain(this.remain);
+
+ if (this.data.autoStart) {
+ this.start();
+ }
+ },
+
+ tick() {
+ if (this.data.millisecond) {
+ this.microTick();
+ } else {
+ this.macroTick();
+ }
+ },
+
+ microTick() {
+ this.tid = setTimeout(() => {
+ this.setRemain(this.getRemain());
+
+ if (this.remain !== 0) {
+ this.microTick();
+ }
+ }, 100);
+ },
+
+ macroTick() {
+ this.tid = setTimeout(() => {
+ const remain = this.getRemain();
+
+ if (!isSameSecond(remain, this.remain) || remain === 0) {
+ this.setRemain(remain);
+ }
+
+ if (this.remain !== 0) {
+ this.macroTick();
+ }
+ }, 1000);
+ },
+
+ getRemain() {
+ return Math.max(this.endTime - Date.now(), 0);
+ },
+
+ setRemain(remain) {
+ this.remain = remain;
+ const timeData = parseTimeData(remain);
+ this.$emit('change', timeData);
+ this.setData({
+ formattedTime: parseFormat(this.data.format, timeData)
+ });
+
+ if (remain === 0) {
+ this.pause();
+ this.$emit('finish');
+ }
+ }
+ }
+});
diff --git a/packages/count-down/index.wxml b/packages/count-down/index.wxml
new file mode 100644
index 00000000..ab6461d9
--- /dev/null
+++ b/packages/count-down/index.wxml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ {{ formattedTime }}
+
diff --git a/packages/count-down/utils.ts b/packages/count-down/utils.ts
new file mode 100644
index 00000000..bf1ced1b
--- /dev/null
+++ b/packages/count-down/utils.ts
@@ -0,0 +1,73 @@
+function padZero(num: number | string, targetLength = 2): string {
+ let str = num + '';
+
+ while (str.length < targetLength) {
+ str = '0' + str;
+ }
+
+ return str;
+}
+
+export type TimeData = {
+ days: number;
+ hours: number;
+ minutes: number;
+ seconds: number;
+ milliseconds: number;
+};
+
+const SECOND = 1000;
+const MINUTE = 60 * SECOND;
+const HOUR = 60 * MINUTE;
+const DAY = 24 * HOUR;
+
+export function parseTimeData(time: number): TimeData {
+ const days = Math.floor(time / DAY);
+ const hours = Math.floor((time % DAY) / HOUR);
+ const minutes = Math.floor((time % HOUR) / MINUTE);
+ const seconds = Math.floor((time % MINUTE) / SECOND);
+ const milliseconds = Math.floor(time % SECOND);
+
+ return {
+ days,
+ hours,
+ minutes,
+ seconds,
+ milliseconds
+ };
+}
+
+export function parseFormat(format: string, timeData: TimeData): string {
+ const { days } = timeData;
+ let { hours, minutes, seconds, milliseconds } = timeData;
+
+ if (format.indexOf('DD') === -1) {
+ hours += days * 24;
+ } else {
+ format = format.replace('DD', padZero(days));
+ }
+
+ if (format.indexOf('HH') === -1) {
+ minutes += hours * 60;
+ } else {
+ format = format.replace('HH', padZero(hours));
+ }
+
+ if (format.indexOf('mm') === -1) {
+ seconds += minutes * 60;
+ } else {
+ format = format.replace('mm', padZero(minutes));
+ }
+
+ if (format.indexOf('ss') === -1) {
+ milliseconds += seconds * 1000;
+ } else {
+ format = format.replace('ss', padZero(seconds));
+ }
+
+ return format.replace('SSS', padZero(milliseconds, 3));
+}
+
+export function isSameSecond(time1: number, time2: number): boolean {
+ return Math.floor(time1 / 1000) === Math.floor(time2 / 1000);
+}