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 }} + + +