From 5b407566db61d2ff7e6512ca8f82f39111d8da0f Mon Sep 17 00:00:00 2001 From: chenjiahan Date: Mon, 14 Feb 2022 10:55:10 +0800 Subject: [PATCH] refactor: DatePicker Component --- packages/vant/src/date-picker/DatePicker.tsx | 191 +++++++++++++++ packages/vant/src/date-picker/README.md | 201 ++++++++++++++++ packages/vant/src/date-picker/README.zh-CN.md | 225 ++++++++++++++++++ packages/vant/src/date-picker/demo/index.vue | 99 ++++++++ packages/vant/src/date-picker/index.ts | 12 + packages/vant/src/time-picker/README.md | 8 +- packages/vant/src/time-picker/README.zh-CN.md | 8 +- packages/vant/vant.config.mjs | 8 +- 8 files changed, 740 insertions(+), 12 deletions(-) create mode 100644 packages/vant/src/date-picker/DatePicker.tsx create mode 100644 packages/vant/src/date-picker/README.md create mode 100644 packages/vant/src/date-picker/README.zh-CN.md create mode 100644 packages/vant/src/date-picker/demo/index.vue create mode 100644 packages/vant/src/date-picker/index.ts diff --git a/packages/vant/src/date-picker/DatePicker.tsx b/packages/vant/src/date-picker/DatePicker.tsx new file mode 100644 index 000000000..2c07fe780 --- /dev/null +++ b/packages/vant/src/date-picker/DatePicker.tsx @@ -0,0 +1,191 @@ +import { + ref, + watch, + computed, + nextTick, + defineComponent, + type PropType, + ExtractPropTypes, +} from 'vue'; + +// Utils +import { + pick, + clamp, + extend, + isDate, + padZero, + createNamespace, +} from '../utils'; +import { + times, + sharedProps, + getTrueValue, + getMonthEndDay, + pickerInheritKeys, +} from '../datetime-picker/utils'; + +// Components +import { Picker, PickerOption } from '../picker'; + +const currentYear = new Date().getFullYear(); +const [name] = createNamespace('date-picker'); + +export type DatePickerColumnType = 'year' | 'month' | 'day'; + +const datePickerProps = extend({}, sharedProps, { + modelValue: Date, + columnsType: { + type: Array as PropType, + default: () => ['year', 'month', 'day'], + }, + minDate: { + type: Date, + default: () => new Date(currentYear - 10, 0, 1), + validator: isDate, + }, + maxDate: { + type: Date, + default: () => new Date(currentYear + 10, 11, 31), + validator: isDate, + }, +}); + +export type DatePickerProps = ExtractPropTypes; + +export default defineComponent({ + name, + + props: datePickerProps, + + emits: ['confirm', 'cancel', 'change', 'update:modelValue'], + + setup(props, { emit, slots }) { + const formatValue = (value?: Date) => { + if (isDate(value)) { + const timestamp = clamp( + value.getTime(), + props.minDate.getTime(), + props.maxDate.getTime() + ); + return new Date(timestamp); + } + + return undefined; + }; + + const currentDate = ref(formatValue(props.modelValue)); + + const getBoundary = (type: 'max' | 'min', value: Date) => { + const boundary = props[`${type}Date` as const]; + const year = boundary.getFullYear(); + let month = 1; + let date = 1; + + if (type === 'max') { + month = 12; + date = getMonthEndDay(value.getFullYear(), value.getMonth() + 1); + } + + if (value.getFullYear() === year) { + month = boundary.getMonth() + 1; + + if (value.getMonth() + 1 === month) { + date = boundary.getDate(); + } + } + + return { + [`${type}Year`]: year, + [`${type}Month`]: month, + [`${type}Date`]: date, + }; + }; + + const ranges = computed(() => { + const { maxYear, maxDate, maxMonth } = getBoundary( + 'max', + currentDate.value || props.minDate + ); + const { minYear, minDate, minMonth } = getBoundary( + 'min', + currentDate.value || props.minDate + ); + + return props.columnsType.map((type) => { + switch (type) { + case 'year': + return { + type: 'year', + range: [minYear, maxYear], + }; + case 'month': + return { + type: 'month', + range: [minMonth, maxMonth], + }; + case 'day': + return { + type: 'day', + range: [minDate, maxDate], + }; + default: + throw new Error( + `[Vant] DatePicker: unsupported columns type: ${type}` + ); + } + }); + }); + + const columns = computed(() => + ranges.value.map(({ type, range }) => { + const options = times( + range[1] - range[0] + 1, + (index): PickerOption => { + const value = padZero(range[0] + index); + return props.formatter(type, { + text: value, + value, + }); + } + ); + + if (props.filter) { + return props.filter(type, options); + } + + return options; + }) + ); + + // watch(currentDate, (value, oldValue) => + // emit('update:modelValue', oldValue ? value : null) + // ); + + // watch( + // () => props.modelValue, + // (value) => { + // value = formatValue(value); + + // if (value && value.valueOf() !== currentDate.value?.valueOf()) { + // currentDate.value = value; + // } + // } + // ); + + const onChange = (...args: unknown[]) => emit('change', ...args); + const onCancel = (...args: unknown[]) => emit('cancel', ...args); + const onConfirm = (...args: unknown[]) => emit('confirm', ...args); + + return () => ( + + ); + }, +}); diff --git a/packages/vant/src/date-picker/README.md b/packages/vant/src/date-picker/README.md new file mode 100644 index 000000000..8ce322ac8 --- /dev/null +++ b/packages/vant/src/date-picker/README.md @@ -0,0 +1,201 @@ +# DatePicker + +### Intro + +Used to select date, usually used with the [Popup](#/en-US/popup) component. + +### Install + +Register component globally via `app.use`, refer to [Component Registration](#/en-US/advanced-usage#zu-jian-zhu-ce) for more registration ways. + +```js +import { createApp } from 'vue'; +import { DatePicker } from 'vant'; + +const app = createApp(); +app.use(DatePicker); +``` + +## Usage + +### Basic Usage + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const currentDate = ref(new Date(2021, 0, 1)); + return { + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 10, 1), + currentDate, + }; + }, +}; +``` + +### Columns Type + +Using `columns-type` prop to control the type of columns. + +For example: + +- Pass in `['year']` to select year. +- Pass in `['month']` to select month. +- Pass in `['year', 'month']` to select year and month. +- Pass in `['month', 'day']` to select month and day. + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const currentDate = ref(new Date(2021, 0, 1)); + return { + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 10, 1), + currentDate, + }; + }, +}; +``` + +### Options Formatter + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const currentDate = ref(new Date(2021, 0, 1)); + + const formatter = (type, val) => { + if (type === 'year') { + return `${val} Year`; + } + if (type === 'month') { + return `${val} Month`; + } + return val; + }; + + return { + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 10, 1), + formatter, + currentDate, + }; + }, +}; +``` + +### Options Filter + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const currentDate = ref(new Date(2021, 0, 1)); + const filter = (type, options) => { + if (type === 'month') { + return options.filter((option) => Number(option.value) % 6 === 0); + } + return options; + }; + + return { + filter, + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 10, 1), + currentTime, + }; + }, +}; +``` + +## API + +### Props + +| Attribute | Description | Type | Default | +| --- | --- | --- | --- | +| columns-type | Columns type | _string[]_ | `['year', 'month', 'day']` | +| min-date | Min date | _Date_ | Ten years ago on January 1 | +| max-date | Max date | _Date_ | Ten years later on December 31 | +| title | Toolbar title | _string_ | `''` | +| confirm-button-text | Text of confirm button | _string_ | `Confirm` | +| cancel-button-text | Text of cancel button | _string_ | `Cancel` | +| show-toolbar | Whether to show toolbar | _boolean_ | `true` | +| loading | Whether to show loading prompt | _boolean_ | `false` | +| readonly | Whether to be readonly | _boolean_ | `false` | +| filter | Option filter | _(type: string, options: PickerOption[]) => PickerOption[]_ | - | +| formatter | Option formatter | _(type: string, option: PickerOption) => PickerOption_ | - | +| option-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` | +| visible-option-num | Count of visible columns | _number \| string_ | `6` | +| swipe-duration | Duration of the momentum animation,unit `ms` | _number \| string_ | `1000` | + +### Events + +| Event | Description | Arguments | +| --- | --- | --- | +| confirm | Emitted when the confirm button is clicked | _{ selectedValues, selectedOptions }_ | +| cancel | Emitted when the cancel button is clicked | _{ selectedValues, selectedOptions }_ | +| change | Emitted when current option is changed | _{ selectedValues, selectedOptions, columnIndex }_ | + +### Slots + +| Name | Description | SlotProps | +| -------------- | ---------------------------- | ---------------------- | +| default | Custom toolbar content | - | +| title | Custom title | - | +| confirm | Custom confirm button text | - | +| cancel | Custom cancel button text | - | +| option | Custom option content | _option: PickerOption_ | +| columns-top | Custom content above columns | - | +| columns-bottom | Custom content below columns | - | + +### Types + +The component exports the following type definitions: + +```ts +import type { DatePickerProps } from 'vant'; +``` diff --git a/packages/vant/src/date-picker/README.zh-CN.md b/packages/vant/src/date-picker/README.zh-CN.md new file mode 100644 index 000000000..42c144b58 --- /dev/null +++ b/packages/vant/src/date-picker/README.zh-CN.md @@ -0,0 +1,225 @@ +# DatePicker 日期选择 + +### 介绍 + +日期选择器,用于选择年、月、日,通常与[弹出层](#/zh-CN/popup)组件配合使用。 + +### 引入 + +通过以下方式来全局注册组件,更多注册方式请参考[组件注册](#/zh-CN/advanced-usage#zu-jian-zhu-ce)。 + +```js +import { createApp } from 'vue'; +import { DatePicker } from 'vant'; + +const app = createApp(); +app.use(DatePicker); +``` + +## 代码演示 + +### 基础用法 + +通过 `v-model` 绑定当前选中的日期,通过 `min-date` 和 `max-date` 属性来设定可选的时间范围。 + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const currentDate = ref(new Date(2021, 0, 1)); + return { + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 10, 1), + currentDate, + }; + }, +}; +``` + +### 选项类型 + +通过 `columns-type` 属性可以控制选项的类型,支持以任意顺序对 `year`、`month` 和 `day` 进行排列组合。 + +比如: + +- 传入 `['year']` 来单独选择年份。 +- 传入 `['month']` 来单独选择月份。 +- 传入 `['year', 'month']` 来选择年份和月份。 +- 传入 `['month', 'day']` 来选择月份和日期。 + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const currentDate = ref(new Date(2021, 0, 1)); + return { + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 10, 1), + currentDate, + }; + }, +}; +``` + +### 格式化选项 + +通过传入 `formatter` 函数,可以对选项文字进行格式化处理。 + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const currentDate = ref(new Date(2021, 0, 1)); + + const formatter = (type, val) => { + if (type === 'year') { + return `${val}年`; + } + if (type === 'month') { + return `${val}月`; + } + return val; + }; + + return { + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 10, 1), + formatter, + currentDate, + }; + }, +}; +``` + +### 过滤选项 + +通过传入 `filter` 函数,可以对选项数组进行过滤,实现自定义选项间隔。 + +```html + +``` + +```js +import { ref } from 'vue'; + +export default { + setup() { + const currentDate = ref(new Date(2021, 0, 1)); + const filter = (type, options) => { + if (type === 'month') { + return options.filter((option) => Number(option.value) % 6 === 0); + } + return options; + }; + + return { + filter, + minDate: new Date(2020, 0, 1), + maxDate: new Date(2025, 10, 1), + currentTime, + }; + }, +}; +``` + +## API + +### Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| columns-type | 选项类型,由 `year`、`month` 和 `day` 组成的数组 | _string[]_ | `['year', 'month', 'day']` | +| min-date | 可选的最小时间,精确到日 | _Date_ | 十年前 | +| max-date | 可选的最大时间,精确到日 | _Date_ | 十年后 | +| title | 顶部栏标题 | _string_ | `''` | +| confirm-button-text | 确认按钮文字 | _string_ | `确认` | +| cancel-button-text | 取消按钮文字 | _string_ | `取消` | +| show-toolbar | 是否显示顶部栏 | _boolean_ | `true` | +| loading | 是否显示加载状态 | _boolean_ | `false` | +| readonly | 是否为只读状态,只读状态下无法切换选项 | _boolean_ | `false` | +| filter | 选项过滤函数 | _(type: string, options: PickerOption[]) => PickerOption[]_ | - | +| formatter | 选项格式化函数 | _(type: string, option: PickerOption) => PickerOption_ | - | +| option-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` | +| visible-option-num | 可见的选项个数 | _number \| string_ | `6` | +| swipe-duration | 快速滑动时惯性滚动的时长,单位 `ms` | _number \| string_ | `1000` | + +### Events + +| 事件名 | 说明 | 回调参数 | +| --- | --- | --- | +| confirm | 点击完成按钮时触发 | _{ selectedValues, selectedOptions }_ | +| cancel | 点击取消按钮时触发 | _{ selectedValues, selectedOptions }_ | +| change | 选项改变时触发 | _{ selectedValues, selectedOptions, columnIndex }_ | + +### Slots + +| 名称 | 说明 | 参数 | +| -------------- | ---------------------- | ---------------------- | +| default | 自定义整个顶部栏的内容 | - | +| title | 自定义标题内容 | - | +| confirm | 自定义确认按钮内容 | - | +| cancel | 自定义取消按钮内容 | - | +| option | 自定义选项内容 | _option: PickerOption_ | +| columns-top | 自定义选项上方内容 | - | +| columns-bottom | 自定义选项下方内容 | - | + +### 类型定义 + +组件导出以下类型定义: + +```ts +import type { DatePickerProps } from 'vant'; +``` + +## 常见问题 + +### 设置 min-date 或 max-date 后出现页面卡死的情况? + +请注意不要在模板中直接使用类似 `min-date="new Date()"` 的写法,这样会导致每次渲染组件时传入一个新的 Date 对象,而传入新的数据会触发下一次渲染,从而陷入死循环。 + +正确的做法是将 `min-date` 作为一个数据定义在 `data` 函数或 `setup` 中。 + +### 在 iOS 系统上初始化组件失败? + +如果你遇到了在 iOS 上无法渲染组件的问题,请确认在创建 Date 对象时没有使用 `new Date('2020-01-01')` 这样的写法,iOS 不支持以中划线分隔的日期格式,正确写法是 `new Date('2020/01/01')`。 + +对此问题的详细解释:[stackoverflow](https://stackoverflow.com/questions/13363673/javascript-date-is-invalid-on-ios)。 + +### 在桌面端无法操作组件? + +参见[桌面端适配](#/zh-CN/advanced-usage#zhuo-mian-duan-gua-pei)。 diff --git a/packages/vant/src/date-picker/demo/index.vue b/packages/vant/src/date-picker/demo/index.vue new file mode 100644 index 000000000..50e1c0250 --- /dev/null +++ b/packages/vant/src/date-picker/demo/index.vue @@ -0,0 +1,99 @@ + + + diff --git a/packages/vant/src/date-picker/index.ts b/packages/vant/src/date-picker/index.ts new file mode 100644 index 000000000..b0a9a1b25 --- /dev/null +++ b/packages/vant/src/date-picker/index.ts @@ -0,0 +1,12 @@ +import { withInstall } from '../utils'; +import _DatePicker, { DatePickerProps } from './DatePicker'; + +export const DatePicker = withInstall(_DatePicker); +export default DatePicker; +export type { DatePickerProps }; + +declare module 'vue' { + export interface GlobalComponents { + VanDatePicker: typeof DatePicker; + } +} diff --git a/packages/vant/src/time-picker/README.md b/packages/vant/src/time-picker/README.md index a0fdbdcb4..1006900d2 100644 --- a/packages/vant/src/time-picker/README.md +++ b/packages/vant/src/time-picker/README.md @@ -132,6 +132,10 @@ export default { | Attribute | Description | Type | Default | | --- | --- | --- | --- | | v-model | Current time | _string_ | - | +| min-hour | Min hour for `time` type | _number \| string_ | `0` | +| max-hour | Max hour for `time` type | _number \| string_ | `23` | +| min-minute | Max minute for `time` type | _number \| string_ | `0` | +| max-minute | Max minute for `time` type | _number \| string_ | `59` | | title | Toolbar title | _string_ | `''` | | confirm-button-text | Text of confirm button | _string_ | `Confirm` | | cancel-button-text | Text of cancel button | _string_ | `Cancel` | @@ -143,10 +147,6 @@ export default { | option-height | Option height, supports `px` `vw` `vh` `rem` unit, default `px` | _number \| string_ | `44` | | visible-option-num | Count of visible columns | _number \| string_ | `6` | | swipe-duration | Duration of the momentum animation,unit `ms` | _number \| string_ | `1000` | -| min-hour | Min hour for `time` type | _number \| string_ | `0` | -| max-hour | Max hour for `time` type | _number \| string_ | `23` | -| min-minute | Max minute for `time` type | _number \| string_ | `0` | -| max-minute | Max minute for `time` type | _number \| string_ | `59` | ### Events diff --git a/packages/vant/src/time-picker/README.zh-CN.md b/packages/vant/src/time-picker/README.zh-CN.md index aa904c10a..33e8f0db3 100644 --- a/packages/vant/src/time-picker/README.zh-CN.md +++ b/packages/vant/src/time-picker/README.zh-CN.md @@ -131,6 +131,10 @@ export default { | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | | v-model | 当前选中的时间 | _string_ | - | +| min-hour | 可选的最小小时 | _number \| string_ | `0` | +| max-hour | 可选的最大小时 | _number \| string_ | `23` | +| min-minute | 可选的最小分钟 | _number \| string_ | `0` | +| max-minute | 可选的最大分钟 | _number \| string_ | `59` | | title | 顶部栏标题 | _string_ | `''` | | confirm-button-text | 确认按钮文字 | _string_ | `确认` | | cancel-button-text | 取消按钮文字 | _string_ | `取消` | @@ -142,10 +146,6 @@ export default { | option-height | 选项高度,支持 `px` `vw` `vh` `rem` 单位,默认 `px` | _number \| string_ | `44` | | visible-option-num | 可见的选项个数 | _number \| string_ | `6` | | swipe-duration | 快速滑动时惯性滚动的时长,单位 `ms` | _number \| string_ | `1000` | -| min-hour | 可选的最小小时 | _number \| string_ | `0` | -| max-hour | 可选的最大小时 | _number \| string_ | `23` | -| min-minute | 可选的最小分钟 | _number \| string_ | `0` | -| max-minute | 可选的最大分钟 | _number \| string_ | `59` | ### Events diff --git a/packages/vant/vant.config.mjs b/packages/vant/vant.config.mjs index 4b334c1c2..b9cca6c35 100644 --- a/packages/vant/vant.config.mjs +++ b/packages/vant/vant.config.mjs @@ -169,8 +169,8 @@ export default { title: 'Checkbox 复选框', }, { - path: 'datetime-picker', - title: 'DatetimePicker 日期选择', + path: 'date-picker', + title: 'DatePicker 日期选择', }, { path: 'field', @@ -579,8 +579,8 @@ export default { title: 'Checkbox', }, { - path: 'datetime-picker', - title: 'DatetimePicker', + path: 'date-picker', + title: 'DatePicker', }, { path: 'field',