From 732a251cca1fbb0af100b3df06cf8b822a5dad9a Mon Sep 17 00:00:00 2001 From: worldSaySorry Date: Fri, 24 Sep 2021 17:00:37 +0800 Subject: [PATCH] feat: datetimepicker (#16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 优化datetimepicker --- .../vantui-demo/src/pages/index/index.tsx | 17 + .../src/components/datetimePicker/index.tsx | 298 ++++++++++++++++++ .../src/components/datetimePicker/wxs.ts | 87 +++++ .../src/components/picker-column/index.tsx | 73 +++-- .../vantui/src/components/picker/index.tsx | 51 ++- packages/vantui/src/index.ts | 1 + packages/vantui/types/datetime-picker.d.ts | 25 ++ packages/vantui/types/index.d.ts | 1 + packages/vantui/types/picker.d.ts | 2 +- 9 files changed, 525 insertions(+), 30 deletions(-) create mode 100644 packages/vantui/src/components/datetimePicker/index.tsx create mode 100644 packages/vantui/src/components/datetimePicker/wxs.ts create mode 100644 packages/vantui/types/datetime-picker.d.ts diff --git a/packages/vantui-demo/src/pages/index/index.tsx b/packages/vantui-demo/src/pages/index/index.tsx index a6f6dcb75..cd6db0f65 100644 --- a/packages/vantui-demo/src/pages/index/index.tsx +++ b/packages/vantui-demo/src/pages/index/index.tsx @@ -34,6 +34,7 @@ import { GoodsAction, GoodsActionIcon, GoodsActionButton, + DatetimePicker, // NavBar, } from '@antmjs/vantui' @@ -50,14 +51,19 @@ const option2 = [ { text: '销量排序', value: 'c' }, ] const columns = ['杭州', '宁波', '温州', '绍兴', '湖州', '嘉兴', '金华', '衢州'] +const minDate = new Date().getTime() +const maxDate = new Date(2019, 10, 1).getTime() export default function Index() { const [rate, setRate] = useState(2.5) const NotifyInstance = useRef(null) const [serachValue] = useState('ff') + const [currentDate, setCurrentDate] = useState() + const value1 = 0 const value2 = 'a' useEffect(function () { + setCurrentDate(new Date().getTime()) console.info('index page load.') return function () { console.info('index page unload.') @@ -85,6 +91,10 @@ export default function Index() { console.info(a, 'picker onConfirm') }, []) + const onInput = function (e: any) { + console.info(e, 'DatetimePicker onInput') + } + // const x = useRef() return ( @@ -116,6 +126,13 @@ export default function Index() { + diff --git a/packages/vantui/src/components/datetimePicker/index.tsx b/packages/vantui/src/components/datetimePicker/index.tsx new file mode 100644 index 000000000..281204921 --- /dev/null +++ b/packages/vantui/src/components/datetimePicker/index.tsx @@ -0,0 +1,298 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useState, useCallback, useEffect, useRef } from 'react' +import VanPicker from '../picker/index' +import { DatetimePickerProps } from '../../../types/datetime-picker' +import * as utils from '../wxs/utils' +import { + defaultFormatter, + getMonthEndDay, + getTrueValue, + times, + padZero, + range, + isValidDate, + currentYear, + diff, +} from './wxs' + +export default function Index(props: DatetimePickerProps) { + const { + value = null, + filter, + type = 'datetime', + showToolbar = true, + formatter = defaultFormatter, + minDate = new Date(currentYear - 10, 0, 1).getTime(), + maxDate = new Date(currentYear + 10, 11, 31).getTime(), + minHour = 0, + maxHour = 23, + minMinute = 0, + maxMinute = 59, + title, + itemHeight, + visibleItemCount, + confirmButtonText, + cancelButtonText, + onInput, + onChange, + onCancel, + onConfirm, + ...others + } = props + + const PickRef = useRef(null) + const [innerValue, setInnerValue] = useState(Date.now()) + const [columns, setColumns] = useState([]) + const minHour_ = minHour + const maxHour_ = maxHour + const minMinute_ = minMinute + const maxMinute_ = maxMinute + + const getPicker = useCallback(function () { + if (PickRef.current) { + const { setColumnValues } = PickRef.current + PickRef.current.setColumnValues = (...args: any) => + setColumnValues.apply(PickRef.current, [...args, false]) + } + return PickRef.current + }, []) + + const updateColumns = function () { + const results = getOriginColumns().map((column: any) => ({ + values: column.values.map((value: any) => formatter(column.type, value)), + })) + if (!diff(results, columns)) { + return setColumns(results) + } + } + + const updateColumnValue = useCallback( + function (value: any) { + let values: Array = [] + const picker = getPicker() + if (type === 'time') { + const pair: any = value.split(':') + values = [formatter('hour', pair[0]), formatter('minute', pair[1])] + } else { + const date = new Date(value) + values = [ + formatter('year', `${date.getFullYear()}`), + formatter('month', padZero(date.getMonth() + 1)), + ] + if (type === 'date') { + values.push(formatter('day', padZero(date.getDate()))) + } + if (type === 'datetime') { + values.push( + formatter('day', padZero(date.getDate())), + formatter('hour', padZero(date.getHours())), + formatter('minute', padZero(date.getMinutes())), + ) + } + } + setInnerValue(value) + updateColumns() + + return new Promise((resolve) => { + setTimeout(() => { + picker.setValues(values) + resolve(value) + }, 16) + }) + }, + [formatter, getPicker, type, updateColumns], + ) + + const correctValue = function (value: any) { + // validate value + const isDateType = type !== 'time' + if (isDateType && !isValidDate(value)) { + value = minDate + } else if (!isDateType && !value) { + value = `${padZero(minHour)}:00` + } + // time type + if (!isDateType) { + let [hour, minute] = value.split(':') + hour = padZero(range(hour, minHour, maxHour)) + minute = padZero(range(minute, minMinute, maxMinute)) + return `${hour}:${minute}` + } + // date type + value = Math.max(value, minDate as number) + value = Math.min(value, maxDate as number) + return value + } + + useEffect( + function () { + const val = correctValue(value) + const isEqual = val === innerValue + if (!isEqual) { + updateColumnValue(val).then(() => { + if (onInput) onInput(val) + }) + } + }, + [value, correctValue, innerValue], + ) + + const getOriginColumns = function () { + const results = getRanges().map(({ type, range }: any) => { + let values = times(range[1] - range[0] + 1, (index: number) => { + const value = range[0] + index + return type === 'year' ? `${value}` : padZero(value) + }) + if (filter) { + values = filter(type, values) + } + + return { type, values } + }) + return results + } + + const getRanges = function (): any { + const res = [ + { + type: 'hour', + range: [minHour_, maxHour_], + }, + { + type: 'minute', + range: [minMinute_, maxMinute_], + }, + ] + if (type === 'time') { + return res + } + const { maxYear, maxDate, maxMonth, maxHour, maxMinute } = getBoundary( + 'max', + innerValue, + ) + const { minYear, minDate, minMonth, minHour, minMinute } = getBoundary( + 'min', + 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 (type === 'date') result.splice(3, 2) + if (type === 'year-month') result.splice(2, 3) + + return result + } + + const getBoundary = useCallback(function (type, innerValue) { + const value = new Date(innerValue) + // const boundary = new Date(data[`${type}Date`]) + const boundary = new Date() + let year = boundary.getFullYear() - 5 + let month = 1 + let date = 1 + let hour = 0 + let minute = 0 + if (type === 'max') { + month = 12 + date = getMonthEndDay(value.getFullYear(), value.getMonth() + 1) + hour = 23 + minute = 59 + year = year + 10 + } + // 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, + } + }, []) + + const onChange_ = function () { + let value: any + const picker = getPicker() + const originColumns = getOriginColumns() + if (type === 'time') { + const indexes = picker.getIndexes() + value = `${+originColumns[0].values[indexes[0]]}:${+originColumns[1] + .values[indexes[1]]}` + } else { + const indexes = picker.getIndexes() + const values = indexes.map( + (value: number, index: number) => originColumns[index].values[value], + ) + const year = getTrueValue(values[0]) + const month = getTrueValue(values[1]) + const maxDate = getMonthEndDay(year, month) + let date = getTrueValue(values[2]) + if (type === 'year-month') { + date = 1 + } + date = date > maxDate ? maxDate : date + let hour = 0 + let minute = 0 + if (type === 'datetime') { + hour = getTrueValue(values[3]) + minute = getTrueValue(values[4]) + } + value = new Date(year, month - 1, date, hour, minute) + } + value = correctValue(value) + + updateColumnValue(value).then(() => { + if (onInput) onInput(value) + if (onChange) onChange(picker) + }) + } + + return ( + + ) +} diff --git a/packages/vantui/src/components/datetimePicker/wxs.ts b/packages/vantui/src/components/datetimePicker/wxs.ts new file mode 100644 index 000000000..dd76cbcea --- /dev/null +++ b/packages/vantui/src/components/datetimePicker/wxs.ts @@ -0,0 +1,87 @@ +import { isDef } from '../common/validator.js' + +const currentYear = new Date().getFullYear() +function isValidDate(date: any) { + return isDef(date) && !isNaN(new Date(date).getTime()) +} + +function range(num: any, min: any, max: any) { + return Math.min(Math.max(num, min), max) +} + +function padZero(val: any) { + return `00${val}`.slice(-2) +} + +function times(n: number, iteratee: any) { + let index = -1 + const result = Array(n < 0 ? 0 : n) + while (++index < n) { + result[index] = iteratee(index) + } + return result +} + +function getTrueValue(formattedValue: any) { + if (formattedValue === undefined) { + formattedValue = '1' + } + while (isNaN(parseInt(formattedValue, 10))) { + formattedValue = formattedValue.slice(1) + } + return parseInt(formattedValue, 10) +} + +function getMonthEndDay(year: number, month: number): any { + return 32 - new Date(year, month - 1, 32).getDate() +} + +const defaultFormatter = (_type: any, value: any) => value + +function diff(obj1: any, obj2: any) { + const keys1 = Object.keys(obj1) + const keys2 = Object.keys(obj2) + + if (keys1.length !== keys2.length) { + return false + } else { + for (const key in obj1) { + if (!obj2.hasOwnProperty(key)) { + return false + } + //类型相同 + if (typeof obj1[key] === typeof obj2[key]) { + //同为引用类型 + if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') { + const equal = diff(obj1[key], obj2[key]) + if (!equal) { + return false + } + } + //同为基础数据类型 + if ( + typeof obj1[key] !== 'object' && + typeof obj2[key] !== 'object' && + obj1[key] !== obj2[key] + ) { + return false + } + } else { + return false + } + } + } + return true +} + +export { + defaultFormatter, + getMonthEndDay, + getTrueValue, + times, + padZero, + range, + isValidDate, + currentYear, + diff, +} diff --git a/packages/vantui/src/components/picker-column/index.tsx b/packages/vantui/src/components/picker-column/index.tsx index 5be505490..be8faeb2c 100644 --- a/packages/vantui/src/components/picker-column/index.tsx +++ b/packages/vantui/src/components/picker-column/index.tsx @@ -1,10 +1,10 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import { useEffect, useState, useCallback, useImperativeHandle, forwardRef, + memo, } from 'react' import { View } from '@tarojs/components' import * as utils from '../wxs/utils' @@ -35,6 +35,10 @@ function Index(props: PickerColumnProps, ref: any): JSX.Element { const [offset, setOffset] = useState(0) const [startOffset, setStartOffset] = useState(0) + const isDisabled = useCallback(function (option) { + return isObj(option) && option.disabled + }, []) + const adjustIndex = useCallback( function (index: number): any { const initialOptions_ = initialOptions as Array @@ -51,7 +55,7 @@ function Index(props: PickerColumnProps, ref: any): JSX.Element { } } }, - [initialOptions], + [initialOptions, isDisabled], ) const setIndex = useCallback( @@ -59,31 +63,29 @@ function Index(props: PickerColumnProps, ref: any): JSX.Element { index = adjustIndex(index) || 0 const offset = -index * Number(itemHeight) if (index !== currentIndex) { - setCurrentIndex(currentIndex) + setCurrentIndex(index) setOffset(offset) if (onChange && userAction) onChange(index) return } return setOffset(offset) }, - [adjustIndex, currentIndex, itemHeight], + [adjustIndex, currentIndex, itemHeight, onChange], ) useEffect( function () { - setCurrentIndex((defaultIndex as number) || 0) + if (defaultIndex && !currentIndex) setCurrentIndex(defaultIndex || 0) setOptions(initialOptions || []) setTimeout(() => { - setIndex((defaultIndex as number) || 0) + if (defaultIndex && !currentIndex) { + setIndex(defaultIndex || 0) + } }) }, - [defaultIndex, initialOptions, setIndex], + [currentIndex, initialOptions, setIndex, defaultIndex], ) - const isDisabled = useCallback(function (option) { - return isObj(option) && option.disabled - }, []) - const onTouchMove = useCallback( function (event) { event.preventDefault() @@ -96,12 +98,11 @@ function Index(props: PickerColumnProps, ref: any): JSX.Element { ), ) }, - [startOffset, itemHeight, options], + [startOffset, itemHeight, options, startY], ) const onTouchStart = useCallback( function (event) { - event.preventDefault() setStartY(event.touches[0].clientY) setStartOffset(offset) setDuration(0) @@ -121,17 +122,23 @@ function Index(props: PickerColumnProps, ref: any): JSX.Element { setIndex(index, true) } }, - [startOffset, offset, itemHeight], + [startOffset, offset, itemHeight, options.length, setIndex], ) - const onClickItem = useCallback(function (event) { - const { index } = event.currentTarget.dataset - setIndex(Number(index), true) - }, []) + const onClickItem = useCallback( + function (event) { + const { index } = event.currentTarget.dataset + setIndex(Number(index), true) + }, + [setIndex], + ) - const getCurrentIndex = useCallback(function () { - return currentIndex - }, []) + const getCurrentIndex = useCallback( + function () { + return currentIndex + }, + [currentIndex], + ) const getValue = useCallback( function () { @@ -140,10 +147,32 @@ function Index(props: PickerColumnProps, ref: any): JSX.Element { [options, currentIndex], ) + const getOptionText = useCallback( + function (option) { + return isObj(option) && valueKey && valueKey in option + ? option[valueKey] + : option + }, + [valueKey], + ) + + const setValue = useCallback( + function (value) { + for (let i = 0; i < options.length; i++) { + if (getOptionText(options[i]) === value) { + return setIndex(i) + } + } + return Promise.resolve() + }, + [setIndex, getOptionText, options], + ) + useImperativeHandle(ref, () => { return { getCurrentIndex, getValue, + setValue, } }) @@ -197,4 +226,4 @@ function Index(props: PickerColumnProps, ref: any): JSX.Element { ) } -export default forwardRef(Index) +export default memo(forwardRef(Index)) diff --git a/packages/vantui/src/components/picker/index.tsx b/packages/vantui/src/components/picker/index.tsx index cd089c13a..c9750521a 100644 --- a/packages/vantui/src/components/picker/index.tsx +++ b/packages/vantui/src/components/picker/index.tsx @@ -1,5 +1,12 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { useEffect, useState, useCallback, useRef } from 'react' +import { + useEffect, + useState, + useCallback, + useRef, + forwardRef, + useImperativeHandle, +} from 'react' import { View } from '@tarojs/components' import { PickerProps } from '../../../types/picker' import PickerColumn from '../picker-column/index' @@ -7,7 +14,10 @@ import * as utils from '../wxs/utils' import Loading from '../loading/index' import * as computed from './wxs' -export default function Index(props: PickerProps): JSX.Element { +export default forwardRef(function Index( + props: PickerProps, + ref: React.ForwardedRef, +): JSX.Element { const { valueKey, toolbarPosition = 'top', @@ -33,7 +43,7 @@ export default function Index(props: PickerProps): JSX.Element { useEffect( function () { - const simple = Boolean(columns.length && !columns[0].values) + const simple = Boolean(columns && columns.length && !columns[0].values) setSimple(simple) if (Array.isArray(children) && children.length) { setColumns().catch(() => {}) @@ -44,7 +54,7 @@ export default function Index(props: PickerProps): JSX.Element { const emit = useCallback(function (event: any) { const type = event?.currentTarget?.dataset?.type - if (typeof event === 'number') { + if (typeof event === 'number' || !type) { if (onChange) { onChange({ picker: children, @@ -81,7 +91,7 @@ export default function Index(props: PickerProps): JSX.Element { const setColumns = useCallback( function () { const columns_ = simple ? [{ values: columns }] : columns - const stack = columns_.map((column, index) => + const stack = (columns_ || []).map((column, index) => setColumnValues(index, column.values), ) return Promise.all(stack) @@ -123,11 +133,38 @@ export default function Index(props: PickerProps): JSX.Element { const onTouchMove = useCallback(function () {}, []) + useImperativeHandle(ref, () => { + return { + setColumnValues, + getIndexes, + getValues, + setColumns, + simple, + children, + setValues, + } + }) + + const setValues = function (values: any) { + const stack = values.map((value: any, index: number) => + setColumnValue(index, value), + ) + return Promise.all(stack) + } + + const setColumnValue = function (index: any, value: any) { + const column = children.current[index] || {} + if (column == null) { + return Promise.reject(new Error('setColumnValue: 对应列不存在')) + } + return column.setValue(value) + } + return ( {toolbarPosition === 'top' && showToolbar && ( @@ -226,4 +263,4 @@ export default function Index(props: PickerProps): JSX.Element { )} ) -} +}) diff --git a/packages/vantui/src/index.ts b/packages/vantui/src/index.ts index 1228b7d17..10ec0ae33 100644 --- a/packages/vantui/src/index.ts +++ b/packages/vantui/src/index.ts @@ -55,4 +55,5 @@ export { default as DropdownItem } from './components/dropdown-item' export { default as GoodsAction } from './components/goods-action' export { default as GoodsActionButton } from './components/goods-action-button' export { default as GoodsActionIcon } from './components/goods-action-icon' +export { default as DatetimePicker } from './components/datetimePicker' export { default as ShareSheet } from './components/share-sheet' diff --git a/packages/vantui/types/datetime-picker.d.ts b/packages/vantui/types/datetime-picker.d.ts new file mode 100644 index 000000000..c6d193628 --- /dev/null +++ b/packages/vantui/types/datetime-picker.d.ts @@ -0,0 +1,25 @@ +import { ComponentClass } from 'react' +import { StandardProps } from '@tarojs/components' +import { PickerProps } from './picker' + +export interface DatetimePickerProps extends PickerProps, StandardProps { + value?: string | number + filter?: (a?: any, b?: any) => any + type?: string + showToolbar?: boolean + formatter?: (a?: any) => any + minDate?: number | string + maxDate?: number | string + minHour?: number | string + maxHour?: number | string + minMinute?: number | string + maxMinute?: number | string + onInput?: (a?: any) => void + onChange?: (a?: any) => any + onConfirm?: (a?: any) => void + onCancel?: (a?: any) => void +} + +declare const DatetimePicker: ComponentClass + +export { DatetimePicker } diff --git a/packages/vantui/types/index.d.ts b/packages/vantui/types/index.d.ts index 06776f7bb..d68490f93 100644 --- a/packages/vantui/types/index.d.ts +++ b/packages/vantui/types/index.d.ts @@ -55,4 +55,5 @@ export { DropdownItem } from './dropdown-item' export { GoodsAction } from './goods-action.d' export { GoodsActionButton } from './goods-action-button.d' export { GoodsActionIcon } from './goods-action-icon.d' +export { DatetimePicker } from './datetime-picker' export { ShareSheet } from './share-sheet.d' diff --git a/packages/vantui/types/picker.d.ts b/packages/vantui/types/picker.d.ts index 654214ad3..c2ced2f71 100644 --- a/packages/vantui/types/picker.d.ts +++ b/packages/vantui/types/picker.d.ts @@ -5,7 +5,7 @@ export interface PickerProps extends StandardProps { valueKey?: string toolbarPosition?: string defaultIndex?: number - columns: Array + columns?: Array title?: string cancelButtonText?: string confirmButtonText?: string