From 22399865dba162539d072eeb2708c65f30c0167b Mon Sep 17 00:00:00 2001 From: Jay Fong Date: Thu, 11 Apr 2019 16:50:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=BB=84=E4=BB=B6):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20TimePicker=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.tsx | 1 + src/components/TimePicker/index.tsx | 327 ++++++++++++++++++++++++++++ src/components/index.ts | 2 + src/pages/Home.tsx | 5 + src/pages/TimePicker.tsx | 77 +++++++ src/pages/index.ts | 1 + 6 files changed, 413 insertions(+) create mode 100644 src/components/TimePicker/index.tsx create mode 100644 src/pages/TimePicker.tsx diff --git a/src/app.tsx b/src/app.tsx index e728be7..504515e 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -13,6 +13,7 @@ class App extends Taro.Component { 'pages/Popup', 'pages/SinglePicker', 'pages/Sticky', + 'pages/TimePicker', 'pages/Transition', // @endindex ], diff --git a/src/components/TimePicker/index.tsx b/src/components/TimePicker/index.tsx new file mode 100644 index 0000000..c0773fa --- /dev/null +++ b/src/components/TimePicker/index.tsx @@ -0,0 +1,327 @@ +import MPicker from '../Picker' +import Taro from '@tarojs/taro' +import { CascadedData } from '../PickerView' +import { component, RequiredProp } from '../component' +import { formatTemplate, memoize, noop } from 'vtils' + +const formatHI = memoize( + formatTemplate, + { + createCache: () => new Map(), + serializer: (template, hi) => `${ + (hi.h && `${hi.h}h`) + || (hi.i && `${hi.i}i`) + }${template}`, + }, +) + +/** + * 时间选择器组件。 + * + * @example + * + * ```jsx + * this.setState({ selectedTime })}> + * 选择时间 + * + * ``` + */ +class MTimePicker extends component({ + props: { + /** + * 开始时间。 + * + * @default '00:00' + */ + startTime: '00:00' as string, + + /** + * 结束时间。 + * + * @default '23:59' + */ + endTime: `23:59` as string, + + /** + * 格式化小时。 + * + * @example + * + * 'h' // ==> '2' + * 'hh' // ==> '02' + * 'h时' // ==> '2时' + * 'hh点' // ==> '02点' + * + * @default 'h' + */ + formatHour: 'h' as string, + + /** + * 格式化分钟。 + * + * @example + * + * 'i' // ==> '5' + * 'ii' // ==> '05' + * 'i分' // ==> '5分' + * 'ii分' // ==> '05分' + * + * @default 'i' + */ + formatMinute: 'i' as string, + + /** + * 小时过滤器,调用 `reject()` 函数可跳过传入的小时。 + * + * @default () => {} + */ + onFilterHour: noop as (params: { + /** 时 */ + hour: number, + /** 跳过当前小时 */ + reject: () => void, + }) => void, + + /** + * 分钟过滤器,调用 `reject()` 函数可跳过传入的分钟。 + * + * @default () => {} + */ + onFilterMinute: noop as (params: { + /** 时 */ + hour: number, + /** 分 */ + minute: number, + /** 跳过当前分钟 */ + reject: () => void, + }) => void, + + /** + * 选中的时间。 + * + * @example + * + * [20, 5] // ==> 20 时 5 分 + * [0, 20] // ==> 0 时 20 分 + */ + selectedTime: [] as any as RequiredProp, + + /** + * 单个条目高度。 + * + * @default '2.5em' + */ + itemHeight: '2.5em' as string, + + /** + * 显示条目数量。 + * + * @default 5 + */ + visibleItemCount: 5 as number, + + /** + * 是否禁止选中 + * + * @default false + */ + disabled: false as boolean, + + /** + * 是否可点击遮罩关闭。 + * + * @default true + */ + maskClosable: true as boolean, + + /** + * 标题。 + * + * @default '' + */ + title: '' as string, + + /** + * 是否无取消按钮。 + * + * @default false + */ + noCancel: false as boolean, + + /** + * 取消文字。 + * + * @default '取消' + */ + cancelText: '取消' as string, + + /** + * 确定文字。 + * + * @default '确定' + */ + confirmText: '确定' as string, + + /** + * 点击取消事件。 + * + * @default () => {} + */ + onCancel: noop as () => void, + + /** + * 点击确定事件。 + * + * @default () => {} + */ + onConfirm: noop as any as RequiredProp<(selectedDate: number[]) => void>, + }, + state: { + localData: [] as CascadedData, + localSelectedIndexes: [] as number[], + }, +}) { + lastUpdateAt: number = 0 + + lastSelectedIndexes: number[] = [] + + componentDidMount() { + this.updateLocalState(this.props) + } + + componentWillReceiveProps(nextProps: MTimePicker['props']) { + // perf: 极短时间内的行为应是由子组件触发的父组件更新 + if (this.lastUpdateAt && (Date.now() - this.lastUpdateAt < 60)) { + this.setState({ + localSelectedIndexes: this.lastSelectedIndexes, + }) + } else { + this.updateLocalState(nextProps) + } + } + + updateLocalState(props: MTimePicker['props']) { + let pass = true + const reject = () => { + pass = false + } + + const startTime = props.startTime.split(':') + const startHour = parseInt(startTime[0]) || 0 + const startMinute = parseInt(startTime[1]) || 0 + + const endTime = props.endTime.split(':') + const endHour = parseInt(endTime[0]) || 23 + const endMinute = parseInt(endTime[1]) || 59 + + const useRawHourValue = props.formatHour === '' || props.formatHour === 'h' + const useRawMinuteValue = props.formatMinute === '' || props.formatMinute === 'i' + + const hourList: CascadedData = [] + const selectedIndexes: number[] = [] + for (let hour = startHour; hour <= endHour; hour++) { + this.props.onFilterHour({ + hour: hour, + reject: reject, + }) + if (pass) { + if (hour === props.selectedTime[0]) { + selectedIndexes[0] = hourList.length + } + const minuteList: CascadedData = [] + hourList.push({ + label: useRawHourValue ? hour.toString() : formatHI(props.formatHour, { h: hour }), + value: hour, + children: minuteList, + }) + const minutes = hour === endHour ? endMinute : 59 + for (let minute = (hour === startHour ? startMinute : 0); minute <= minutes; minute++) { + this.props.onFilterMinute({ + hour: hour, + minute: minute, + reject: reject, + }) + if (pass) { + if (minute === props.selectedTime[1]) { + selectedIndexes[1] = minuteList.length + } + minuteList.push({ + label: useRawMinuteValue ? minute.toString() : formatHI(props.formatMinute, { i: minute }), + value: minute, + }) + } else { + pass = true + } + } + selectedIndexes[1] = selectedIndexes[1] == null ? 0 : selectedIndexes[1] + } else { + pass = true + } + } + selectedIndexes[0] = selectedIndexes[0] == null ? 0 : selectedIndexes[0] + + this.setState({ + localData: hourList, + localSelectedIndexes: selectedIndexes, + }) + } + + handleCancel = () => { + this.props.onCancel() + } + + handleConfirm: MPicker['props']['onConfirm'] = selectedIndexes => { + const { localData } = this.state + const selectedDate: number[] = [] + let list = localData + const n = Math.min(selectedIndexes.length, 2) + for (let i = 0; i < n; i++) { + if (!list[selectedIndexes[i]]) break + selectedDate.push(list[selectedIndexes[i]].value) + list = list[selectedIndexes[i]].children + if (!list) break + } + this.lastUpdateAt = Date.now() + this.lastSelectedIndexes = selectedIndexes + this.props.onConfirm(selectedDate) + } + + render() { + const { + maskClosable, + itemHeight, + visibleItemCount, + noCancel, + cancelText, + confirmText, + title, + className, + } = this.props + const { + localData, + localSelectedIndexes, + } = this.state + return localData.length ? ( + + {this.props.children} + + ) : this.props.children + } +} + +export default MTimePicker diff --git a/src/components/index.ts b/src/components/index.ts index a52a9bd..76c72ea 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -5,6 +5,7 @@ export { default as MPickerView } from './PickerView' export { default as MPopup } from './Popup' export { default as MSinglePicker } from './SinglePicker' export { default as MSticky } from './Sticky' +export { default as MTimePicker } from './TimePicker' export { default as MTransition } from './Transition' // @endindex @@ -16,6 +17,7 @@ export type ComponentName = ( 'Popup' | 'SinglePicker' | 'Sticky' | + 'TimePicker' | 'Transition' // @endindex ) diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index d4119a3..86cabc5 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -49,6 +49,11 @@ const componentList: ComponentInfo[] = [ chineseName: '日期选择器', url: pageUrls.DatePicker, }, + { + name: 'TimePicker', + chineseName: '时间选择器', + url: pageUrls.TimePicker, + }, ] export default class Home extends component({ diff --git a/src/pages/TimePicker.tsx b/src/pages/TimePicker.tsx new file mode 100644 index 0000000..42cba01 --- /dev/null +++ b/src/pages/TimePicker.tsx @@ -0,0 +1,77 @@ +import Taro, { Config, ShareAppMessageReturn } from '@tarojs/taro' +import { component } from '../components/component' +import { Image, View } from '@tarojs/components' +import { MTimePicker } from '../components' +import { pageUrls } from '.' +import { XBackHome, XItem, XList, XTitle } from './components' + +const codeImg = 'https://ws1.sinaimg.cn/large/d9ddb3f8gy1g1y2gs4hmlj20hi16qwfk.jpg' + +export default class TimePicker extends component({ + state: { + selectedTime: [5, 20], + }, +}) { + config: Config = { + navigationBarTitleText: 'TimePicker', + } + + onShareAppMessage(): ShareAppMessageReturn { + return { + title: 'TimePicker', + path: pageUrls.TimePicker, + } + } + + render() { + const { selectedTime } = this.state + return ( + + 时间选择 + + { + if (payload.hour % 2 === 0) { + payload.reject() + } + }} + onFilterMinute={payload => { + if (payload.minute === 10) { + payload.reject() + } + }} + onConfirm={selectedTime => { + this.setState({ selectedTime }) + }}> + + + + { + Taro.previewImage({ + current: codeImg, + urls: [codeImg], + }) + }} + /> + + + + + ) + } +} diff --git a/src/pages/index.ts b/src/pages/index.ts index 56e8e52..3f24c5d 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -7,6 +7,7 @@ export const pageUrls = { Popup: '/pages/Popup' as '/pages/Popup', SinglePicker: '/pages/SinglePicker' as '/pages/SinglePicker', Sticky: '/pages/Sticky' as '/pages/Sticky', + TimePicker: '/pages/TimePicker' as '/pages/TimePicker', Transition: '/pages/Transition' as '/pages/Transition', // @endindex }