From 9fcc9086f77c96d76eaa184d9546ab8aed08cbfc Mon Sep 17 00:00:00 2001 From: neverland Date: Wed, 10 Jul 2019 20:20:59 +0800 Subject: [PATCH] [new feature] add CountDown component (#3805) --- docs/site/doc.config.js | 8 + src/checkbox/README.zh-CN.md | 2 +- src/count-down/README.md | 114 +++++++++++++ src/count-down/README.zh-CN.md | 160 ++++++++++++++++++ src/count-down/demo/index.vue | 148 ++++++++++++++++ src/count-down/index.js | 127 ++++++++++++++ src/count-down/index.less | 7 + .../test/__snapshots__/demo.spec.js.snap | 35 ++++ src/count-down/test/demo.spec.js | 4 + src/count-down/utils.ts | 65 +++++++ src/index.less | 1 + src/index.ts | 3 + src/radio/README.zh-CN.md | 2 +- src/style/var.less | 5 + src/utils/format/string.ts | 10 +- types/index.d.ts | 1 + 16 files changed, 688 insertions(+), 4 deletions(-) create mode 100644 src/count-down/README.md create mode 100644 src/count-down/README.zh-CN.md create mode 100644 src/count-down/demo/index.vue create mode 100644 src/count-down/index.js create mode 100644 src/count-down/index.less create mode 100644 src/count-down/test/__snapshots__/demo.spec.js.snap create mode 100644 src/count-down/test/demo.spec.js create mode 100644 src/count-down/utils.ts diff --git a/docs/site/doc.config.js b/docs/site/doc.config.js index b2d4abe78..1135945d0 100644 --- a/docs/site/doc.config.js +++ b/docs/site/doc.config.js @@ -227,6 +227,10 @@ export default { path: '/collapse', title: 'Collapse 折叠面板' }, + { + path: '/count-down', + title: 'CountDown 倒计时' + }, { path: '/divider', title: 'Divider 分割线' @@ -561,6 +565,10 @@ export default { path: '/collapse', title: 'Collapse' }, + { + path: '/count-down', + title: 'CountDown' + }, { path: '/divider', title: 'Divider' diff --git a/src/checkbox/README.zh-CN.md b/src/checkbox/README.zh-CN.md index ed47033fb..90f7d4d83 100644 --- a/src/checkbox/README.zh-CN.md +++ b/src/checkbox/README.zh-CN.md @@ -179,7 +179,7 @@ export default { ### Checkbox Slots -| 名称 | 说明 | slot-scope | +| 名称 | 说明 | slot-scope 参数 | |------|------|------| | default | 自定义文本 | - | | icon | 自定义图标 | checked: 是否为选中状态 | diff --git a/src/count-down/README.md b/src/count-down/README.md new file mode 100644 index 000000000..9d7440746 --- /dev/null +++ b/src/count-down/README.md @@ -0,0 +1,114 @@ +# CountDown + +### Install + +``` javascript +import { CountDown } from 'vant'; + +Vue.use(CountDown); +``` + +## Usage + +### Basic Usage + +```html + +``` + +```js +export default { + data() { + return { + time: 30 * 60 * 60 * 1000 + }; + } +} +``` + +### Custom Format + +```html + +``` + +### Millisecond + +```html + +``` + +### Custom Style + +```html + + + + + +``` + +### Manual Control + +```html + + + + + + +``` + +```js +export default { + methods: { + start() { + this.$refs.countDown.start(); + }, + pause() { + this.$refs.countDown.pause(); + }, + reset() { + this.$refs.countDown.reset(); + }, + finish() { + this.$toast('Finished'); + } + } +} +``` + +## API + +### Props + +| Attribute | Description | Type | Default | +|------|------|------|------| diff --git a/src/count-down/README.zh-CN.md b/src/count-down/README.zh-CN.md new file mode 100644 index 000000000..316f94d13 --- /dev/null +++ b/src/count-down/README.zh-CN.md @@ -0,0 +1,160 @@ +# CountDown 倒计时 + +### 引入 + +``` javascript +import { CountDown } from 'vant'; + +Vue.use(CountDown); +``` + +## 代码演示 + +### 基本用法 + +`time`属性表示倒计时总时长,单位为毫秒 + +```html + +``` + +```js +export default { + data() { + return { + time: 30 * 60 * 60 * 1000 + }; + } +} +``` + +### 自定义格式 + +通过`format`属性设置倒计时文本的内容 + +```html + +``` + +### 毫秒级渲染 + +倒计时默认每秒渲染一次,设置`millisecond`属性可以开启毫秒级渲染 + +```html + +``` + +### 自定义样式 + +通过插槽自定义倒计时的样式,`timeData`对象格式见下方表格 + +```html + + + + + +``` + +### 手动控制 + +通过 ref 获取到组件实例后,可以调用`start`、`pause`、`reset`方法 + +```html + + + + + + +``` + +```js +export default { + methods: { + start() { + this.$refs.countDown.start(); + }, + pause() { + this.$refs.countDown.pause(); + }, + reset() { + this.$refs.countDown.reset(); + }, + finish() { + this.$toast('倒计时结束'); + } + } +} +``` + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +|------|------|------|------|------| +| time | 倒计时时长,单位毫秒 | `Number` | - | - | +| format | 时间格式,DD-日,HH-时,mm-分,ss-秒,SSS-毫秒 | `String` | `HH:mm:ss` | - | +| auto-start | 是否自动开始倒计时 | `Boolean` | `true` | - | +| millisecond | 是否开启毫秒级渲染 | `Boolean` | `false` | - | + +### Events + +| 事件名 | 说明 | 回调参数 | +|------|------|------| +| finish | 倒计时结束时触发 | - | + +### Slots + +| 名称 | 说明 | slot-scope 参数 | +|------|------|------| +| default | 自定义内容 | timeData | + +### timeData 格式 + +| 名称 | 说明 | 类型 | +|------|------|------| +| days | 剩余天数 | `number` | +| hours | 剩余小时 | `number` | +| minutes | 剩余分钟 | `number` | +| seconds | 剩余秒数 | `number` | +| milliseconds | 剩余毫秒 | `number` | + +### 方法 + +通过 ref 可以获取到 CountDown 实例并调用实例方法 + +| 方法名 | 参数 | 返回值 | 介绍 | +|------|------|------|------| +| start | - | - | 开始倒计时 | +| pause | - | - | 暂停倒计时 | +| reset | - | - | 重设倒计时,若`auto-start`为`true`,重设后会自动开始倒计时 | diff --git a/src/count-down/demo/index.vue b/src/count-down/demo/index.vue new file mode 100644 index 000000000..ba3be3bcd --- /dev/null +++ b/src/count-down/demo/index.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/src/count-down/index.js b/src/count-down/index.js new file mode 100644 index 000000000..928f4298a --- /dev/null +++ b/src/count-down/index.js @@ -0,0 +1,127 @@ +import { createNamespace } from '../utils'; +import { raf, cancelRaf } from '../utils/dom/raf'; +import { isSameSecond, parseTimeData, parseFormat } from './utils'; + +const [createComponent, bem] = createNamespace('count-down'); + +export default createComponent({ + props: { + millisecond: Boolean, + time: { + type: Number, + default: 0 + }, + format: { + type: String, + default: 'HH:mm:ss' + }, + autoStart: { + type: Boolean, + default: true + } + }, + + data() { + return { + remain: 0 + }; + }, + + computed: { + timeData() { + return parseTimeData(this.remain); + }, + + formattedTime() { + return parseFormat(this.format, this.timeData); + } + }, + + watch: { + time: { + immediate: true, + handler() { + this.reset(); + } + } + }, + + methods: { + start() { + if (this.counting) { + return; + } + + this.counting = true; + this.endTime = Date.now() + this.remain; + this.tick(); + }, + + pause() { + this.counting = false; + cancelRaf(this.rafId); + }, + + reset() { + this.pause(); + this.remain = this.time; + + if (this.autoStart) { + this.start(); + } + }, + + tick() { + if (this.millisecond) { + this.microTick(); + } else { + this.macroTick(); + } + }, + + microTick() { + this.rafId = raf(() => { + this.setRemain(this.getRemain()); + + if (this.remain !== 0) { + this.microTick(); + } + }); + }, + + macroTick() { + this.rafId = raf(() => { + const remain = this.getRemain(); + + if (!isSameSecond(remain, this.remain) || remain === 0) { + this.setRemain(remain); + } + + if (this.remain !== 0) { + this.macroTick(); + } + }); + }, + + getRemain() { + return Math.max(this.endTime - Date.now(), 0); + }, + + setRemain(remain) { + this.remain = remain; + + if (remain === 0) { + this.pause(); + this.$emit('finish'); + } + } + }, + + render(h) { + return ( +
+ {this.slots('default', this.timeData) || this.formattedTime} +
+ ); + } +}); diff --git a/src/count-down/index.less b/src/count-down/index.less new file mode 100644 index 000000000..5a6206a0b --- /dev/null +++ b/src/count-down/index.less @@ -0,0 +1,7 @@ +@import '../style/var'; + +.van-count-down { + color: @count-down-text-color; + font-size: @count-down-font-size; + line-height: @count-down-line-height; +} diff --git a/src/count-down/test/__snapshots__/demo.spec.js.snap b/src/count-down/test/__snapshots__/demo.spec.js.snap new file mode 100644 index 000000000..237f2ee09 --- /dev/null +++ b/src/count-down/test/__snapshots__/demo.spec.js.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders demo correctly 1`] = ` +
+
+
30:00:00
+
+
+
01 天 06 时 00 分 00 秒
+
+
+
30:00:00:000
+
+
+
6 0 0
+
+
+
03:000
+
+
+
+ 开始
+
+
+
+ 暂停
+
+
+
+ 重置
+
+
+
+
+`; diff --git a/src/count-down/test/demo.spec.js b/src/count-down/test/demo.spec.js new file mode 100644 index 000000000..d647cfabc --- /dev/null +++ b/src/count-down/test/demo.spec.js @@ -0,0 +1,4 @@ +import Demo from '../demo'; +import demoTest from '../../../test/demo-test'; + +demoTest(Demo); diff --git a/src/count-down/utils.ts b/src/count-down/utils.ts new file mode 100644 index 000000000..bed42a80e --- /dev/null +++ b/src/count-down/utils.ts @@ -0,0 +1,65 @@ +import { padZero } from '../utils/format/string'; + +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); +} diff --git a/src/index.less b/src/index.less index 4b58c1138..6a18e94d0 100644 --- a/src/index.less +++ b/src/index.less @@ -13,6 +13,7 @@ @import './image/index'; @import './circle/index'; @import './collapse-item/index'; +@import './count-down/index'; @import './divider/index'; @import './list/index'; @import './nav-bar/index'; diff --git a/src/index.ts b/src/index.ts index 505a7e413..6aeed1a10 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import CollapseItem from './collapse-item'; import ContactCard from './contact-card'; import ContactEdit from './contact-edit'; import ContactList from './contact-list'; +import CountDown from './count-down'; import Coupon from './coupon'; import CouponCell from './coupon-cell'; import CouponList from './coupon-list'; @@ -107,6 +108,7 @@ const components = [ ContactCard, ContactEdit, ContactList, + CountDown, Coupon, CouponCell, CouponList, @@ -201,6 +203,7 @@ export { ContactCard, ContactEdit, ContactList, + CountDown, Coupon, CouponCell, CouponList, diff --git a/src/radio/README.zh-CN.md b/src/radio/README.zh-CN.md index a38f468dc..add74ada4 100644 --- a/src/radio/README.zh-CN.md +++ b/src/radio/README.zh-CN.md @@ -143,7 +143,7 @@ export default { ### Radio Slots -| 名称 | 说明 | slot-scope | +| 名称 | 说明 | slot-scope 参数 | |------|------|------| | default | 自定义文本 | - | | icon | 自定义图标 | checked: 是否为选中状态 | diff --git a/src/style/var.less b/src/style/var.less index 4d5c49efe..7690ca4cd 100644 --- a/src/style/var.less +++ b/src/style/var.less @@ -166,6 +166,11 @@ @contact-list-add-button-z-index: 9999; @contact-list-item-padding: 15px; +// CountDown +@count-down-text-color: @text-color; +@count-down-font-size: 14px; +@count-down-line-height: 20px; + // Coupon @coupon-margin: 0 15px 15px; @coupon-content-height: 100px; diff --git a/src/utils/format/string.ts b/src/utils/format/string.ts index 9f39001f9..f801d910b 100644 --- a/src/utils/format/string.ts +++ b/src/utils/format/string.ts @@ -4,6 +4,12 @@ export function camelize(str: string): string { return str.replace(camelizeRE, (_, c) => c.toUpperCase()); } -export function padZero(num: number | string): string { - return (num < 10 ? '0' : '') + num; +export function padZero(num: number | string, targetLength = 2): string { + let str = num + ''; + + while (str.length < targetLength) { + str = '0' + str; + } + + return str; } diff --git a/types/index.d.ts b/types/index.d.ts index 83929b247..27095ce85 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -17,6 +17,7 @@ export class Button extends VanComponent {} export class Card extends VanComponent {} export class Cell extends VanComponent {} export class CellGroup extends VanComponent {} +export class CountDown extends VanComponent {} export class Divider extends VanComponent {} export class SwipeCell extends VanComponent {} export class Checkbox extends VanComponent {}