diff --git a/example/app.json b/example/app.json
index 2d48a667..10bd3994 100644
--- a/example/app.json
+++ b/example/app.json
@@ -34,7 +34,8 @@
"pages/radio/index",
"pages/checkbox/index",
"pages/goods-action/index",
- "pages/swipe-cell/index"
+ "pages/swipe-cell/index",
+ "pages/datetime-picker/index"
],
"window": {
"navigationBarBackgroundColor": "#f8f8f8",
@@ -87,6 +88,7 @@
"van-tag": "../../dist/tag/index",
"van-toast": "../../dist/toast/index",
"van-transition": "../../dist/transition/index",
- "van-tree-select": "../../dist/tree-select/index"
+ "van-tree-select": "../../dist/tree-select/index",
+ "van-datetime-picker": "../../dist/datetime-picker/index"
}
}
diff --git a/example/config.js b/example/config.js
index 457968e5..aee7aea6 100644
--- a/example/config.js
+++ b/example/config.js
@@ -72,6 +72,10 @@ export default [
path: '/action-sheet',
title: 'ActionSheet 上拉菜单'
},
+ {
+ path: '/datetime-picker',
+ title: 'DatetimePicker 时间选择'
+ },
{
path: '/dialog',
title: 'Dialog 弹出框'
diff --git a/example/pages/datetime-picker/index.js b/example/pages/datetime-picker/index.js
new file mode 100644
index 00000000..0ff84142
--- /dev/null
+++ b/example/pages/datetime-picker/index.js
@@ -0,0 +1,20 @@
+import Page from '../../common/page';
+
+Page({
+ data: {
+ minHour: 10,
+ maxHour: 20,
+ minDate: new Date(2018, 0, 1).getTime(),
+ maxDate: new Date(2019, 10, 1).getTime(),
+ currentDate1: new Date(2018, 2, 1).getTime(),
+ currentDate2: null,
+ currentDate3: new Date(2018, 0, 1),
+ currentDate4: '12:00',
+ loading: false
+ },
+
+ onChange(event) {
+ const picker = event.detail;
+ console.log(event);
+ }
+});
diff --git a/example/pages/datetime-picker/index.json b/example/pages/datetime-picker/index.json
new file mode 100644
index 00000000..7e046cf2
--- /dev/null
+++ b/example/pages/datetime-picker/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "DatetimePicker 时间选择"
+}
diff --git a/example/pages/datetime-picker/index.wxml b/example/pages/datetime-picker/index.wxml
new file mode 100644
index 00000000..b10f7f09
--- /dev/null
+++ b/example/pages/datetime-picker/index.wxml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/pages/datetime-picker/index.wxss b/example/pages/datetime-picker/index.wxss
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/example/pages/datetime-picker/index.wxss
@@ -0,0 +1 @@
+
diff --git a/packages/datetime-picker/README.md b/packages/datetime-picker/README.md
new file mode 100644
index 00000000..9b3f43f0
--- /dev/null
+++ b/packages/datetime-picker/README.md
@@ -0,0 +1,159 @@
+## DatetimePicker 时间选择
+时间选择组件通常与 [弹出层](#/popup) 组件配合使用
+
+### 使用指南
+在 app.json 或 index.json 中引入组件
+```json
+"usingComponents": {
+ "van-datetime-picker": "path/to/vant-weapp/dist/datetime-picker/index"
+}
+```
+
+### 代码演示
+
+#### 选择完整时间
+
+```html
+
+```
+
+```javascript
+Page({
+ data: {
+ minHour: 10,
+ maxHour: 20,
+ minDate: new Date(),
+ maxDate: new Date(2019, 10, 1),
+ currentDate: new Date()
+ },
+ onChange(event) {
+ this.setData({
+ currentDate: event.detail.value
+ });
+ }
+});
+```
+
+#### 选择日期(年月日)
+
+```html
+
+```
+
+```js
+Page({
+ data: {
+ currentDate: new Date(),
+ minDate: new Date()
+ },
+ onChange(event) {
+ this.setData({
+ currentDate: event.detail.value
+ });
+ }
+});
+```
+
+#### 选择日期(年月)
+
+```html
+
+```
+
+```js
+Page({
+ data: {
+ currentDate: new Date(),
+ minDate: new Date()
+ },
+ onChange(event) {
+ this.setData({
+ currentDate: event.detail.value
+ });
+ }
+});
+```
+
+#### 选择时间
+
+```html
+
+```
+
+```js
+Page({
+ data: {
+ currentDate: '12:00',
+ minHour: 9,
+ maxHour: 23
+ },
+ onChange(event) {
+ this.setData({
+ currentDate: event.detail.value
+ });
+ }
+});
+```
+
+### API
+
+| 参数 | 说明 | 类型 | 默认值 |
+|------|------|------|------|------|
+| type | 类型,可选值为 `date`
`time` `year-month` | `String` | `datetime` |
+| min-date | 可选的最小时间,精确到分钟 | `Date` | 十年前 |
+| max-date | 可选的最大时间,精确到分钟 | `Date` | 十年后 |
+| min-hour | 可选的最小小时,针对 time 类型 | `Number` | `0` |
+| max-hour | 可选的最大小时,针对 time 类型 | `Number` | `23` |
+| min-minute | 可选的最小分钟,针对 time 类型 | `Number` | `0` |
+| max-minute | 可选的最大分钟,针对 time 类型 | `Number` | `59` |
+| title | 顶部栏标题 | `String` | `''` |
+| show-toolbar | 是否显示顶部栏 | `Boolean` | `false` |
+| loading | 是否显示加载状态 | `Boolean` | `false` |
+| item-height | 选项高度 | `Number` | `44` |
+| confirm-button-text | 确认按钮文字 | `String` | `确认` |
+| cancel-button-text | 取消按钮文字 | `String` | `取消` |
+| visible-item-count | 可见的选项个数 | `Number` | `5` |
+
+### Event
+
+| 事件名称 | 说明 | 回调参数 |
+|------|------|------|
+| input | 当值变化时触发的事件 | 当前 value |
+| change | 当值变化时触发的事件 | 组件实例 |
+| confirm | 点击完成按钮时触发的事件 | 当前 value |
+| cancel | 点击取消按钮时触发的事件 | - |
+
+### change事件
+
+在`change`事件中,可以获取到组件实例,对组件进行相应的更新等操作:
+
+| 函数 | 说明 |
+|------|------|
+| getColumnValue(index) | 获取对应列中选中的值 |
+| setColumnValue(index, value) | 设置对应列中选中的值 |
+| getColumnValues(index) | 获取对应列中所有的备选值 |
+| setColumnValues(index, values) | 设置对应列中所有的备选值 |
+| getValues() | 获取所有列中被选中的值,返回一个数组 |
+| setValues(values) | `values`为一个数组,设置所有列中被选中的值 |
diff --git a/packages/datetime-picker/index.json b/packages/datetime-picker/index.json
new file mode 100644
index 00000000..01077f5d
--- /dev/null
+++ b/packages/datetime-picker/index.json
@@ -0,0 +1,6 @@
+{
+ "component": true,
+ "usingComponents": {
+ "van-loading": "../loading/index"
+ }
+}
diff --git a/packages/datetime-picker/index.less b/packages/datetime-picker/index.less
new file mode 100644
index 00000000..72b95115
--- /dev/null
+++ b/packages/datetime-picker/index.less
@@ -0,0 +1,72 @@
+@import '../common/style/var.less';
+
+.van-picker {
+ -webkit-text-size-adjust: 100%; /* avoid iOS text size adjust */
+ position: relative;
+ overflow: hidden;
+ background-color: @white;
+ user-select: none;
+
+ &__toolbar {
+ display: flex;
+ justify-content: space-between;
+ height: 44px;
+ line-height: 44px;
+ }
+
+ &__cancel,
+ &__confirm {
+ color: @blue;
+ padding: 0 15px;
+ font-size: 14px;
+
+ &:active {
+ background-color: @active-color;
+ }
+ }
+
+ &__title {
+ max-width: 50%;
+ font-size: 16px;
+ font-weight: 500;
+ text-align: center;
+ }
+
+ &__columns {
+ position: relative;
+ }
+
+ &__loading {
+ display: flex;
+ z-index: 4;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, .9);
+ }
+
+ &-column {
+ flex: 1;
+ overflow: hidden;
+ font-size: 16px;
+ text-align: center;
+
+ &__item {
+ padding: 0 5px;
+ color: @gray-dark;
+
+ &--selected {
+ font-weight: 500;
+ color: @text-color;
+ }
+
+ &--disabled {
+ opacity: .3;
+ }
+ }
+ }
+}
diff --git a/packages/datetime-picker/index.ts b/packages/datetime-picker/index.ts
new file mode 100644
index 00000000..3e6a7668
--- /dev/null
+++ b/packages/datetime-picker/index.ts
@@ -0,0 +1,356 @@
+import { VantComponent } from '../common/component';
+
+const currentYear = new Date().getFullYear();
+const isValidDate = date => !isNaN(new Date(date).getTime());
+
+function range(num, min, max) {
+ return Math.min(Math.max(num, min), max);
+}
+
+VantComponent({
+ props: {
+ value: null,
+ title: String,
+ loading: Boolean,
+ itemHeight: {
+ type: Number,
+ value: 44
+ },
+ visibleItemCount: {
+ type: Number,
+ value: 5
+ },
+ confirmButtonText: {
+ type: String,
+ value: '确认'
+ },
+ cancelButtonText: {
+ type: String,
+ value: '取消'
+ },
+ type: {
+ type: String,
+ value: 'datetime'
+ },
+ showToolbar: {
+ type: Boolean,
+ value: true
+ },
+ minDate: {
+ type: Number,
+ value: new Date(currentYear - 10, 0, 1).getTime()
+ },
+ maxDate: {
+ type: Number,
+ value: new Date(currentYear + 10, 11, 31).getTime()
+ },
+ minHour: {
+ type: Number,
+ value: 0
+ },
+ maxHour: {
+ type: Number,
+ value: 23
+ },
+ minMinute: {
+ type: Number,
+ value: 0
+ },
+ maxMinute: {
+ type: Number,
+ value: 59
+ }
+ },
+
+ data: {
+ pickerValue: [],
+ innerValue: Date.now()
+ },
+
+ computed: {
+ columns() {
+ const results = this.getRanges().map(({ type, range }) => {
+ const values = this.times(range[1] - range[0] + 1, index => {
+ let value = range[0] + index;
+ value = type === 'year' ? `${value}` : this.pad(value);
+ return value;
+ });
+
+ return values;
+ });
+
+ return results;
+ }
+ },
+
+ watch: {
+ value(val) {
+ const { data } = this;
+ val = this.correctValue(val);
+ const isEqual = val === data.innerValue;
+ if (!isEqual) {
+ this.setData({ innerValue: val }, () => {
+ this.updateColumnValue(val);
+ this.$emit('input', val);
+ });
+ }
+ }
+ },
+
+ methods: {
+ getRanges(): object[] {
+ const { data } = this;
+ if (data.type === 'time') {
+ return [
+ {
+ type: 'hour',
+ range: [data.minHour, data.maxHour]
+ },
+ {
+ type: 'minute',
+ range: [data.minMinute, data.maxMinute]
+ }
+ ];
+ }
+
+ const { maxYear, maxDate, maxMonth, maxHour, maxMinute } = this.getBoundary('max', data.innerValue);
+ const { minYear, minDate, minMonth, minHour, minMinute } = this.getBoundary('min', data.innerValue);
+
+ const result = [
+ {
+ type: 'year',
+ range: [minYear, maxYear]
+ },
+ {
+ type: 'month',
+ range: [minMonth, maxMonth]
+ },
+ {
+ type: 'day',
+ range: [minDate, maxDate]
+ },
+ {
+ type: 'hour',
+ range: [minHour, maxHour]
+ },
+ {
+ type: 'minute',
+ range: [minMinute, maxMinute]
+ }
+ ];
+
+ if (data.type === 'date') result.splice(3, 2);
+ if (data.type === 'year-month') result.splice(2, 3);
+ return result;
+ },
+
+ pad(val: string | number): string {
+ return `00${val}`.slice(-2);
+ },
+
+ correctValue(value) {
+ const { data, pad } = this;
+ // validate value
+ const isDateType = data.type !== 'time';
+ if (isDateType && !isValidDate(value)) {
+ value = data.minDate;
+ } else if (!isDateType && !value) {
+ const { minHour } = data;
+ value = `${pad(minHour)}:00`;
+ }
+
+ // time type
+ if (!isDateType) {
+ let [hour, minute] = value.split(':');
+ hour = pad(range(hour, data.minHour, data.maxHour));
+ minute = pad(range(minute, data.minMinute, data.maxMinute));
+
+ return `${hour}:${minute}`;
+ }
+
+ // date type
+ const { maxYear, maxDate, maxMonth, maxHour, maxMinute } = this.getBoundary('max', value);
+ const { minYear, minDate, minMonth, minHour, minMinute } = this.getBoundary('min', value);
+ const minDay = new Date(minYear, minMonth - 1, minDate, minHour, minMinute);
+ const maxDay = new Date(maxYear, maxMonth - 1, maxDate, maxHour, maxMinute);
+ value = Math.max(value, minDay.getTime());
+ value = Math.min(value, maxDay.getTime());
+
+ return value;
+ },
+
+ times(n: number, iteratee: (number) => string): string[] {
+ let index = -1;
+ const result = Array(n);
+
+ while (++index < n) {
+ result[index] = iteratee(index);
+ }
+ return result;
+ },
+
+ getBoundary(type: string, innerValue: number): object {
+ const value = new Date(innerValue);
+ const boundary = new Date(this.data[`${type}Date`]);
+ const year = boundary.getFullYear();
+ let month = 1;
+ let date = 1;
+ let hour = 0;
+ let minute = 0;
+
+ if (type === 'max') {
+ month = 12;
+ date = this.getMonthEndDay(value.getFullYear(), value.getMonth() + 1);
+ hour = 23;
+ minute = 59;
+ }
+
+ if (value.getFullYear() === year) {
+ month = boundary.getMonth() + 1;
+ if (value.getMonth() + 1 === month) {
+ date = boundary.getDate();
+ if (value.getDate() === date) {
+ hour = boundary.getHours();
+ if (value.getHours() === hour) {
+ minute = boundary.getMinutes();
+ }
+ }
+ }
+ }
+
+ return {
+ [`${type}Year`]: year,
+ [`${type}Month`]: month,
+ [`${type}Date`]: date,
+ [`${type}Hour`]: hour,
+ [`${type}Minute`]: minute
+ };
+ },
+
+ getTrueValue(formattedValue: string): number {
+ if (!formattedValue) return;
+ while (isNaN(parseInt(formattedValue, 10))) {
+ formattedValue = formattedValue.slice(1);
+ }
+ return parseInt(formattedValue, 10);
+ },
+
+ getMonthEndDay(year, month): number {
+ return 32 - new Date(year, month - 1, 32).getDate();
+ },
+
+ onCancel() {
+ this.$emit('cancel');
+ },
+
+ onConfirm(): void {
+ this.$emit('confirm', this.data.innerValue);
+ },
+
+ onChange(event: Weapp.Event): void {
+ const { data } = this;
+ const pickerValue = event.detail.value;
+ const values = pickerValue.map((value, index) => data.columns[index][value]);
+ let value;
+
+ if (data.type === 'time') {
+ value = values.join(':');
+ } else {
+ const year = this.getTrueValue(values[0]);
+ const month = this.getTrueValue(values[1]);
+ const maxDate = this.getMonthEndDay(year, month);
+ let date = this.getTrueValue(values[2]);
+ if (data.type === 'year-month') {
+ date = 1;
+ }
+ date = date > maxDate ? maxDate : date;
+ let hour = 0;
+ let minute = 0;
+ if (data.type === 'datetime') {
+ hour = this.getTrueValue(values[3]);
+ minute = this.getTrueValue(values[4]);
+ }
+ value = new Date(year, month - 1, date, hour, minute);
+ }
+ value = this.correctValue(value);
+
+ this.setData({ innerValue: value }, () => {
+ this.updateColumnValue(value);
+ this.$emit('input', value);
+ this.$emit('change', this);
+ });
+ },
+
+ getColumnValue(index) {
+ return this.getValues()[index];
+ },
+
+ setColumnValue(index, value) {
+ const { pickerValue, columns } = this.data;
+ pickerValue[index] = columns[index].indexOf(value);
+ this.setData({ pickerValue });
+ },
+
+ getColumnValues(index) {
+ return this.data.columns[index];
+ },
+
+ setColumnValues(index, values) {
+ const { columns } = this.data;
+ columns[index] = values;
+ this.setData({ columns });
+ },
+
+ getValues() {
+ const { pickerValue, columns } = this.data;
+ return pickerValue.map((value, index) => columns[index][value])
+ },
+
+ setValues(values) {
+ const { columns } = this.data;
+ this.setData({
+ pickerValue: values.map((value, index) => columns[index].indexOf(value))
+ });
+ },
+
+ updateColumnValue(value): void {
+ let values = [];
+ const { pad, data } = this;
+ const { columns } = data;
+
+ if (data.type === 'time') {
+ const currentValue = value.split(':');
+ values = [
+ columns[0].indexOf(currentValue[0]),
+ columns[1].indexOf(currentValue[1])
+ ];
+ } else {
+ const date = new Date(value);
+ values = [
+ columns[0].indexOf(`${date.getFullYear()}`),
+ columns[1].indexOf(pad(date.getMonth() + 1))
+ ];
+ if (data.type === 'date') {
+ values.push(columns[2].indexOf(pad(date.getDate())));
+ }
+ if (data.type === 'datetime') {
+ values.push(
+ columns[2].indexOf(pad(date.getDate())),
+ columns[3].indexOf(pad(date.getHours())),
+ columns[4].indexOf(pad(date.getMinutes()))
+ );
+ }
+ }
+
+ this.setData({ pickerValue: values });
+ }
+ },
+
+ created() {
+ const innerValue = this.correctValue(this.data.value);
+ this.setData({ innerValue }, () => {
+ this.updateColumnValue(innerValue);
+ this.$emit('input', innerValue);
+ });
+ }
+});
diff --git a/packages/datetime-picker/index.wxml b/packages/datetime-picker/index.wxml
new file mode 100644
index 00000000..fef4bec7
--- /dev/null
+++ b/packages/datetime-picker/index.wxml
@@ -0,0 +1,34 @@
+
+
+ {{ cancelButtonText }}
+ {{ title }}
+ {{ confirmButtonText }}
+
+
+
+
+
+
+
+
+ {{ item }}
+
+
+