From 0db1a03996e734f691aa31727c08b7e6cdf5fc24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=98=89=E6=B6=B5?= Date: Wed, 25 Dec 2019 10:56:53 +0800 Subject: [PATCH] feat(Calendar): support select date range --- src/calendar/Header.js | 14 ++- src/calendar/Month.js | 74 ++++++++++++ src/calendar/README.zh-CN.md | 13 ++- src/calendar/demo/index.vue | 68 ++++++++--- src/calendar/index.js | 213 +++++++++++++++++++++++------------ src/calendar/index.less | 84 ++++++++++---- src/calendar/utils.ts | 20 ++++ src/locale/lang/zh-CN.ts | 4 + 8 files changed, 365 insertions(+), 125 deletions(-) create mode 100644 src/calendar/Month.js diff --git a/src/calendar/Header.js b/src/calendar/Header.js index 17306245c..db590abed 100644 --- a/src/calendar/Header.js +++ b/src/calendar/Header.js @@ -1,7 +1,7 @@ import { createNamespace } from '../utils'; -import { t, formatMonthTitle } from './utils'; +import { t, bem, formatMonthTitle } from './utils'; -const [createComponent, bem] = createNamespace('calendar-header'); +const [createComponent] = createNamespace('calendar-header'); export default createComponent({ props: { @@ -11,14 +11,16 @@ export default createComponent({ methods: { genTitle() { - if (this.title) { - return
{this.title}
; + const title = this.slots('title') || this.title || t('title'); + + if (title) { + return
{title}
; } }, genMonth() { return ( -
+
{formatMonthTitle(this.currentMonth)}
); @@ -39,7 +41,7 @@ export default createComponent({ render() { return ( -
+
{this.genTitle()} {this.genMonth()} {this.genWeekDays()} diff --git a/src/calendar/Month.js b/src/calendar/Month.js new file mode 100644 index 000000000..290d74481 --- /dev/null +++ b/src/calendar/Month.js @@ -0,0 +1,74 @@ +import { createNamespace } from '../utils'; +import { t, bem } from './utils'; + +const [createComponent] = createNamespace('calendar-month'); + +export default createComponent({ + props: { + date: Date, + days: Array, + title: String + }, + + mounted() { + this.height = this.$el.getBoundingClientRect().height; + }, + + methods: { + getLabel(item) { + if (item.type === 'start') { + return t('start'); + } + + if (item.type === 'end') { + return t('end'); + } + }, + + genTitle() { + if (this.title) { + return
{this.title}
; + } + }, + + genDay(item) { + const { type } = item; + + const onClick = () => { + if (type !== 'disabled') { + this.$emit('click', item); + } + }; + + if (type === 'selected') { + return ( +
+
{item.day}
+
+ ); + } + + const label = this.getLabel(item); + const Label = label &&
{label}
; + + return ( +
+ {item.day} + {Label} +
+ ); + } + }, + + render() { + return ( +
+ {this.genTitle()} +
+
{this.date.getMonth() + 1}
+ {this.days.map(this.genDay)} +
+
+ ); + } +}); diff --git a/src/calendar/README.zh-CN.md b/src/calendar/README.zh-CN.md index 756060a72..735da40e7 100644 --- a/src/calendar/README.zh-CN.md +++ b/src/calendar/README.zh-CN.md @@ -1,5 +1,9 @@ # Calendar 日历 +### 介绍 + +日历组件可以用于选择日期或日期区间,通常与 [弹出层](#/zh-CN/popup) 组件配合使用 + ### 引入 ``` javascript @@ -11,7 +15,7 @@ Vue.use(Calendar); ## 代码演示 -### 基础用法 +### 选择单个日期 ```html @@ -26,8 +30,10 @@ Vue.use(Calendar); | v-model | 选中的日期 | `Date` | - | - | | type | 选择类型,`single`为选择单日,`range`为选择日期区间 | `string` | `single` | - | | title | 日历标题 | `string` | - | - | -| min-date | 最小日期 | `Date` | 当前时间 | - | -| max-date | 最大日期 | `Date` | 当前时间的六个月后 | - | +| min-date | 最小日期 | `Date` | 当前日期 | - | +| max-date | 最大日期 | `Date` | 当前日期的六个月后 | - | +| confirm-text | 选择日期区间时,确认按钮的文字 | `string` | `确定` | - | +| confirm-disabled-text | 选择日期区间时,确认按钮处于禁用状态时的文字 | `string` | `确定` | - | ### Events @@ -39,6 +45,7 @@ Vue.use(Calendar); | 名称 | 说明 | |------|------| +| title | 自定义标题 | ### 方法 diff --git a/src/calendar/demo/index.vue b/src/calendar/demo/index.vue index abae5324c..b767fbfc3 100644 --- a/src/calendar/demo/index.vue +++ b/src/calendar/demo/index.vue @@ -2,20 +2,46 @@ - - - + + + + - + + + + + @@ -23,22 +49,24 @@ export default { i18n: { 'zh-CN': { - title: '日期选择', - selectDate: '选择日期' + selectSingleDate: '选择单个日期', + selectDateRange: '选择日期区间' }, 'en-US': { - title: 'Select Date', - selectDate: 'Select Date' + selectSingleDate: 'Select Single Date', + selectDateRange: 'Select Date Range' } }, data() { return { date: { - selectDate: null + selectSingleDate: null, + selectDateRange: [] }, show: { - selectDate: false + selectSingleDate: false, + selectDateRange: false } }; }, @@ -52,6 +80,14 @@ export default { if (date) { return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; } + }, + + formatDateRange(dateRange) { + if (dateRange.length) { + return `${this.formatDate(dateRange[0])} - ${this.formatDate( + dateRange[1] + )}`; + } } } }; diff --git a/src/calendar/index.js b/src/calendar/index.js index 740961899..b55b78718 100644 --- a/src/calendar/index.js +++ b/src/calendar/index.js @@ -1,12 +1,24 @@ import { isDate } from '../utils/validate/date'; import { getScrollTop } from '../utils/dom/scroll'; -import { createComponent, bem, compareMonth, formatMonthTitle } from './utils'; +import { + t, + bem, + getNextDay, + compareDay, + compareMonth, + createComponent, + formatMonthTitle +} from './utils'; +import Month from './Month'; import Header from './Header'; +import Button from '../button'; export default createComponent({ props: { - value: Date, title: String, + value: [Date, Array], + confirmText: String, + confirmDisabledText: String, type: { type: String, default: 'single' @@ -28,7 +40,8 @@ export default createComponent({ data() { return { - currentMonth: this.minDate + currentMonth: this.minDate, + currentValue: this.getDefaultValue() }; }, @@ -54,42 +67,64 @@ export default createComponent({ } }, - mounted() { - this.initRects(); + watch: { + value(val) { + this.currentValue = val; + } }, methods: { - initRects() { - this.monthsHeight = this.$refs.month.map( - month => month.getBoundingClientRect().height - ); + getDefaultValue() { + const { type, value, minDate } = this; + + if (type === 'single') { + return value || minDate; + } + + if (type === 'range') { + const range = value || []; + return [range[0] || minDate, range[1] || getNextDay(minDate)]; + } + }, + + getDayType(day) { + const { type, minDate, maxDate, currentValue } = this; + + if (compareDay(day, minDate) < 0 || compareDay(day, maxDate) > 0) { + return 'disabled'; + } + + if (type === 'single') { + return compareDay(day, currentValue) === 0 ? 'selected' : ''; + } + + if (type === 'range') { + if (!currentValue[0]) { + return; + } + + const compareWithStart = compareDay(day, currentValue[0]); + if (compareWithStart === 0) { + return 'start'; + } + + if (!currentValue[1]) { + return; + } + + const compareWithEnd = compareDay(day, currentValue[1]); + if (compareWithEnd === 0) { + return 'end'; + } + + if (compareWithStart > 0 && compareWithEnd < 0) { + return 'middle'; + } + } }, getDays(date) { const days = []; - const { minDate, maxDate } = this; - const checkMinDate = compareMonth(date, minDate) === 0; - const checkMaxDate = compareMonth(date, maxDate) === 0; - const checkSelected = - this.value && - this.type === 'single' && - compareMonth(date, this.value) === 0; - - const isDisabled = date => { - if (checkMaxDate && date.getDate() > maxDate.getDate()) { - return true; - } - - if (checkMinDate && date.getDate() < minDate.getDate()) { - return true; - } - - return false; - }; - - const isSelected = date => - checkSelected && date.getDate() === this.value.getDate(); - const placeholderCount = date.getDay() === 0 ? 6 : date.getDay() - 1; for (let i = 1; i <= placeholderCount; i++) { @@ -102,8 +137,7 @@ export default createComponent({ days.push({ day: cursor.getDate(), date: new Date(cursor), - disabled: isDisabled(cursor), - selected: isSelected(cursor) + type: this.getDayType(cursor) }); cursor.setDate(cursor.getDate() + 1); @@ -113,46 +147,21 @@ export default createComponent({ }, genMonth(month, index) { - const Title = index !== 0 && ( -
{month.title}
- ); - - const Days = month.days.map(item => { - const onClick = () => { - this.onClickDay(item); - }; - - if (item.selected) { - return ( -
-
{item.day}
-
- ); - } - - return ( -
- {item.day} -
- ); - }); - return ( -
- {Title} -
-
{month.date.getMonth() + 1}
- {Days} -
-
+ ); }, onScroll() { const scrollTop = getScrollTop(this.$refs.body); + const monthsHeight = this.$refs.month.map(item => item.height); let height = 0; for (let i = 0; i < this.months.length; i++) { @@ -161,18 +170,65 @@ export default createComponent({ return; } - height += this.monthsHeight[i]; + height += monthsHeight[i]; } }, onClickDay(item) { - if (item.disabled) { - return; - } + const { date } = item; if (this.type === 'single') { - this.$emit('input', item.date); - this.$emit('select', item.date); + this.$emit('input', date); + this.$emit('select', date); + } + + if (this.type === 'range') { + const startDay = this.currentValue[0]; + const endDay = this.currentValue[1]; + + if (startDay && endDay) { + this.$emit('input', [date, null]); + return; + } + + if (startDay) { + const compareWithStart = compareDay(date, startDay); + + if (compareWithStart === 1) { + this.$emit('input', [startDay, date]); + } + + if (compareWithStart === -1) { + this.$emit('input', [date, null]); + } + } + } + }, + + onConfirmRange() { + this.$emit('input', this.currentValue); + this.$emit('select', this.currentValue); + }, + + genFooter() { + if (this.type === 'range') { + const disabled = !this.currentValue[1]; + const text = disabled ? this.confirmDisabledText : this.confirmText; + + return ( +
+ +
+ ); } } }, @@ -180,10 +236,17 @@ export default createComponent({ render() { return (
-
+
this.slots('title') + }} + />
{this.months.map(this.genMonth)}
+ {this.genFooter()}
); } diff --git a/src/calendar/index.less b/src/calendar/index.less index 7c1cdc33a..b540cd98b 100644 --- a/src/calendar/index.less +++ b/src/calendar/index.less @@ -3,10 +3,14 @@ .van-calendar { display: flex; flex-direction: column; - height: 80vh; + height: 100%; - &-header__title, - &-header__month, + &__header { + flex-shrink: 0; + box-shadow: 0 2px 10px rgba(125, 126, 128, 0.16); + } + + &__header-title, &__month-title { height: 44px; font-weight: @font-weight-bold; @@ -14,34 +18,25 @@ text-align: center; } - &-header { - flex-shrink: 0; - box-shadow: 0 2px 10px rgba(125, 126, 128, 0.16); - - &__title { - font-size: @font-size-lg; - } - - &__month { - font-size: @font-size-md; - } - - &__weekdays { - display: flex; - } - - &__weekday { - flex: 1; - font-size: @font-size-sm; - line-height: 30px; - text-align: center; - } + &__header-title { + font-size: @font-size-lg; } &__month-title { font-size: @font-size-md; } + &__weekdays { + display: flex; + } + + &__weekday { + flex: 1; + font-size: @font-size-sm; + line-height: 30px; + text-align: center; + } + &__body { flex: 1; overflow: auto; @@ -73,17 +68,47 @@ } &__day { + position: relative; width: 14.285%; height: 64px; font-size: @font-size-lg; cursor: pointer; + &--end, + &--start { + color: @white; + background-color: @red; + } + + &--start { + border-radius: @border-radius-md 0 0 @border-radius-md; + } + + &--end { + border-radius: 0 @border-radius-md @border-radius-md 0; + } + + &--middle { + color: @red; + background-color: fade(@red, 10%); + } + &--disabled { color: @gray-5; cursor: default; } } + &__day-label { + position: absolute; + right: 0; + bottom: 6px; + left: 0; + font-size: @font-size-xs; + line-height: 14px; + text-align: center; + } + &__selected-day { width: 54px; height: 54px; @@ -91,4 +116,13 @@ background: @red; border-radius: @border-radius-md; } + + &__footer { + padding: 7px @padding-md; + } + + &__confirm { + height: 36px; + line-height: 34px; + } } diff --git a/src/calendar/utils.ts b/src/calendar/utils.ts index 8b1fd52ba..ff2af104c 100644 --- a/src/calendar/utils.ts +++ b/src/calendar/utils.ts @@ -21,3 +21,23 @@ export function compareMonth(date1: Date, date2: Date) { return year1 > year2 ? 1 : -1; } + +export function compareDay(day1: Date, day2: Date) { + const compareMonthResult = compareMonth(day1, day2); + + if (compareMonthResult === 0) { + const date1 = day1.getDate(); + const date2 = day2.getDate(); + + return date1 === date2 ? 0 : date1 > date2 ? 1 : -1; + } + + return compareMonthResult; +} + +export function getNextDay(date: Date) { + date = new Date(date); + date.setDate(date.getDate() + 1); + + return date; +} diff --git a/src/locale/lang/zh-CN.ts b/src/locale/lang/zh-CN.ts index 9722f2149..fac901e63 100644 --- a/src/locale/lang/zh-CN.ts +++ b/src/locale/lang/zh-CN.ts @@ -12,6 +12,10 @@ export default { confirmDelete: '确定要删除么', telInvalid: '请填写正确的电话', vanCalendar: { + end: '结束', + start: '开始', + title: '日期选择', + confirm: '确定', weekdays: ['日', '一', '二', '三', '四', '五', '六'], monthTitle: (year: number, month: number) => `${year}年${month}月` },