diff --git a/src/calendar/README.md b/src/calendar/README.md index 7a7fa10e2..597c09de9 100644 --- a/src/calendar/README.md +++ b/src/calendar/README.md @@ -44,6 +44,30 @@ export default { }; ``` +### Select Multiple Date + +```html + + +``` + +```js +export default { + data() { + return { + text: '', + show: false + }; + }, + methods: { + onConfirm(date) { + this.show = false; + this.text = `${date.length} dates selected`; + } + } +}; +``` + ### Select Date Range You can select a date range after setting `type` to` range`. In range mode, the date returned by the `confirm` event is an array, the first item in the array is the start time and the second item is the end time. @@ -212,7 +236,7 @@ Set `poppable` to `false`, the calendar will be displayed directly on the page i | Attribute | Description | Type | Default | |------|------|------|------| | v-model | Whether to show calendar | *boolean* | `false` | -| type | Type,can be set to `single` `range` | *string* | `single` | +| type `v2.5.4` | Type,can be set to `range` `multiple` | *string* | `single` | | title | Title of calendar | *string* | `Calendar` | | color | Color for the bottom button and selected date | *string* | `#ee0a24` | | min-date | Min date | *Date* | Today | diff --git a/src/calendar/README.zh-CN.md b/src/calendar/README.zh-CN.md index 0d536ba49..cf750bc2e 100644 --- a/src/calendar/README.zh-CN.md +++ b/src/calendar/README.zh-CN.md @@ -44,6 +44,32 @@ export default { }; ``` +### 选择多个日期 + +设置`type`为`multiple`后可以选择多个日期,此时`confirm`事件返回的 date 为数组结构,数组包含若干个选中的日期。 + +```html + + +``` + +```js +export default { + data() { + return { + text: '', + show: false + }; + }, + methods: { + onConfirm(date) { + this.show = false; + this.text = `选择了 ${date.length} 个日期`; + } + } +}; +``` + ### 选择日期区间 设置`type`为`range`后可以选择日期区间,此时`confirm`事件返回的 date 为数组结构,数组第一项为开始时间,第二项为结束时间。 @@ -212,7 +238,7 @@ export default { | 参数 | 说明 | 类型 | 默认值 | |------|------|------|------| | v-model | 是否显示日历弹窗 | *boolean* | `false` | -| type | 选择类型,`single`表示选择单个日期,
`range`表示选择日期区间 | *string* | `single` | +| type `v2.5.4` | 选择类型:
`single`表示选择单个日期,
`multiple`表示选择多个日期,
`range`表示选择日期区间 | *string* | `single` | | title | 日历标题 | *string* | `日期选择` | | color | 颜色,对底部按钮和选中日期生效 | *string* | `#ee0a24` | | min-date | 最小日期 | *Date* | 当前日期 | diff --git a/src/calendar/components/Month.js b/src/calendar/components/Month.js index e290df1a4..cf46a4987 100644 --- a/src/calendar/components/Month.js +++ b/src/calendar/components/Month.js @@ -1,5 +1,13 @@ import { createNamespace } from '../../utils'; -import { t, bem, compareDay, formatMonthTitle, ROW_HEIGHT } from '../utils'; +import { + t, + bem, + compareDay, + ROW_HEIGHT, + getPrevDay, + getNextDay, + formatMonthTitle, +} from '../utils'; import { getMonthEndDay } from '../../datetime-picker/utils'; const [createComponent] = createNamespace('calendar-month'); @@ -84,6 +92,56 @@ export default createComponent({ this.$refs.days.scrollIntoView(); }, + getMultipleDayType(day) { + const isSelected = date => + this.currentDate.some(item => compareDay(item, date) === 0); + + if (isSelected(day)) { + const prevDay = getPrevDay(day); + const nextDay = getNextDay(day); + const prevSelected = isSelected(prevDay); + const nextSelected = isSelected(nextDay); + + if (prevSelected && nextSelected) { + return 'multiple-middle'; + } + + if (prevSelected) { + return 'end'; + } + + return nextSelected ? 'start' : 'multiple-selected'; + } + + return ''; + }, + + getRangeDayType(day) { + const [startDay, endDay] = this.currentDate; + + if (!startDay) { + return; + } + + const compareToStart = compareDay(day, startDay); + if (compareToStart === 0) { + return 'start'; + } + + if (!endDay) { + return; + } + + const compareToEnd = compareDay(day, endDay); + if (compareToEnd === 0) { + return 'end'; + } + + if (compareToStart > 0 && compareToEnd < 0) { + return 'middle'; + } + }, + getDayType(day) { const { type, minDate, maxDate, currentDate } = this; @@ -95,41 +153,24 @@ export default createComponent({ return compareDay(day, currentDate) === 0 ? 'selected' : ''; } + if (type === 'multiple') { + return this.getMultipleDayType(day); + } + /* istanbul ignore else */ if (type === 'range') { - const [startDay, endDay] = this.currentDate; - - if (!startDay) { - return; - } - - const compareToStart = compareDay(day, startDay); - if (compareToStart === 0) { - return 'start'; - } - - if (!endDay) { - return; - } - - const compareToEnd = compareDay(day, endDay); - if (compareToEnd === 0) { - return 'end'; - } - - if (compareToStart > 0 && compareToEnd < 0) { - return 'middle'; - } + return this.getRangeDayType(day); } }, getBottomInfo(type) { - if (type === 'start') { - return t('start'); - } - - if (type === 'end') { - return t('end'); + if (this.type === 'range') { + if (type === 'start') { + return t('start'); + } + if (type === 'end') { + return t('end'); + } } }, diff --git a/src/calendar/demo/index.vue b/src/calendar/demo/index.vue index 8637f11c0..adbf52224 100644 --- a/src/calendar/demo/index.vue +++ b/src/calendar/demo/index.vue @@ -8,6 +8,13 @@ @click="show('single', 'selectSingle')" /> + + `选择了 ${count} 个日期`, selectSingle: '选择单个日期', + selectMultiple: '选择多个日期', selectRange: '选择日期区间', quickSelect: '快捷选择', confirmText: '完成', @@ -139,7 +148,9 @@ export default { youthDay: 'Youth Day', calendar: 'Calendar', maxRange: 'Max Range', + selectCount: count => `${count} dates selected`, selectSingle: 'Select Single Date', + selectMultiple: 'Select Multiple Date', selectRange: 'Select Date Range', quickSelect: 'Quick Select', confirmText: 'OK', @@ -160,6 +171,7 @@ export default { maxRange: [], selectSingle: null, selectRange: [], + selectMultiple: [], quickSelect1: null, quickSelect2: [], customColor: [], @@ -271,6 +283,12 @@ export default { } }, + formatMultiple(dates) { + if (dates.length) { + return this.$t('selectCount', dates.length); + } + }, + formatRange(dateRange) { if (dateRange.length) { const [start, end] = dateRange; diff --git a/src/calendar/index.js b/src/calendar/index.js index 7cb06c4d5..6363cd851 100644 --- a/src/calendar/index.js +++ b/src/calendar/index.js @@ -4,6 +4,7 @@ import { getScrollTop } from '../utils/dom/scroll'; import { t, bem, + copyDates, getNextDay, compareDay, compareMonth, @@ -94,10 +95,6 @@ export default createComponent({ }, computed: { - range() { - return this.type === 'range'; - }, - months() { const months = []; const cursor = new Date(this.minDate); @@ -113,11 +110,17 @@ export default createComponent({ }, buttonDisabled() { - if (this.range) { - return !this.currentDate[0] || !this.currentDate[1]; + const { type, currentDate } = this; + + if (type === 'range') { + return !currentDate[0] || !currentDate[1]; } - return !this.currentDate; + if (type === 'multiple') { + return !currentDate.length; + } + + return !currentDate; }, }, @@ -165,7 +168,8 @@ export default createComponent({ scrollIntoView() { this.$nextTick(() => { const { currentDate } = this; - const targetDate = this.range ? currentDate[0] : currentDate; + const targetDate = + this.type === 'single' ? currentDate : currentDate[0]; const displayed = this.value || !this.poppable; /* istanbul ignore if */ @@ -192,6 +196,10 @@ export default createComponent({ return [startDay || minDate, endDay || getNextDay(minDate)]; } + if (type === 'multiple') { + return [defaultDate || minDate]; + } + return defaultDate || minDate; }, @@ -232,9 +240,10 @@ export default createComponent({ onClickDay(item) { const { date } = item; + const { type, currentDate } = this; - if (this.range) { - const [startDay, endDay] = this.currentDate; + if (type === 'range') { + const [startDay, endDay] = currentDate; if (startDay && !endDay) { const compareToStart = compareDay(date, startDay); @@ -247,6 +256,22 @@ export default createComponent({ } else { this.select([date, null]); } + } else if (type === 'multiple') { + let selectedIndex; + + const selected = this.currentDate.some((dateItem, index) => { + const equal = compareDay(dateItem, date) === 0; + if (equal) { + selectedIndex = index; + } + return equal; + }); + + if (selected) { + currentDate.splice(selectedIndex, 1); + } else { + this.select([...currentDate, date]); + } } else { this.select(date, true); } @@ -258,9 +283,9 @@ export default createComponent({ select(date, complete) { this.currentDate = date; - this.$emit('select', this.currentDate); + this.$emit('select', copyDates(this.currentDate)); - if (complete && this.range) { + if (complete && this.type === 'range') { const valid = this.checkRange(); if (!valid) { @@ -285,11 +310,11 @@ export default createComponent({ }, onConfirm() { - if (this.range && !this.checkRange()) { + if (this.type === 'range' && !this.checkRange()) { return; } - this.$emit('confirm', this.currentDate); + this.$emit('confirm', copyDates(this.currentDate)); }, genMonth(date, index) { diff --git a/src/calendar/index.less b/src/calendar/index.less index 6b75c348e..e505670c5 100644 --- a/src/calendar/index.less +++ b/src/calendar/index.less @@ -94,7 +94,9 @@ cursor: pointer; &--end, - &--start { + &--start, + &--multiple-middle, + &--multiple-selected { color: @calendar-range-edge-color; background-color: @calendar-range-edge-background-color; } @@ -107,6 +109,10 @@ border-radius: 0 @border-radius-md @border-radius-md 0; } + &--multiple-selected { + border-radius: @border-radius-md; + } + &--middle { color: @calendar-range-middle-color; diff --git a/src/calendar/test/__snapshots__/demo.spec.js.snap b/src/calendar/test/__snapshots__/demo.spec.js.snap index 40e4ffea5..276d4abad 100644 --- a/src/calendar/test/__snapshots__/demo.spec.js.snap +++ b/src/calendar/test/__snapshots__/demo.spec.js.snap @@ -7,6 +7,10 @@ exports[`renders demo correctly 1`] = `
选择单个日期
+
+
选择多个日期
+ +
选择日期区间
diff --git a/src/calendar/test/index.spec.js b/src/calendar/test/index.spec.js index 0799e95e4..1fa7c6f8c 100644 --- a/src/calendar/test/index.spec.js +++ b/src/calendar/test/index.spec.js @@ -18,6 +18,10 @@ function formatRange([start, end]) { return `${formatDate(start)}-${formatDate(end)}`; } +function formatMultiple(dates) { + return dates.map(formatDate).join(','); +} + test('select event when type is single', async () => { const wrapper = mount(Calendar, { propsData: { @@ -63,6 +67,36 @@ test('select event when type is range', async () => { expect(formatRange(emittedSelect[3][0])).toEqual('2010/1/13-'); }); +test('select event when type is multiple', async () => { + const wrapper = mount(Calendar, { + propsData: { + type: 'multiple', + minDate, + maxDate, + poppable: false, + }, + }); + + await later(); + + const days = wrapper.findAll('.van-calendar__day'); + days.at(15).trigger('click'); + days.at(16).trigger('click'); + + await later(); + days.at(15).trigger('click'); + days.at(12).trigger('click'); + + const emittedSelect = wrapper.emitted('select'); + expect(formatMultiple(emittedSelect[0][0])).toEqual('2010/1/10,2010/1/16'); + expect(formatMultiple(emittedSelect[1][0])).toEqual( + '2010/1/10,2010/1/16,2010/1/17' + ); + expect(formatMultiple(emittedSelect[2][0])).toEqual( + '2010/1/10,2010/1/17,2010/1/13' + ); +}); + test('should not trigger select event when click disabled day', async () => { const wrapper = mount(Calendar, { propsData: { diff --git a/src/calendar/utils.ts b/src/calendar/utils.ts index 4cd606c25..3c469438a 100644 --- a/src/calendar/utils.ts +++ b/src/calendar/utils.ts @@ -36,15 +36,37 @@ export function compareDay(day1: Date, day2: Date) { return compareMonthResult; } -export function getNextDay(date: Date) { +function getDayByOffset(date: Date, offset: number) { date = new Date(date); - date.setDate(date.getDate() + 1); + date.setDate(date.getDate() + offset); return date; } +export function getPrevDay(date: Date) { + return getDayByOffset(date, -1); +} + +export function getNextDay(date: Date) { + return getDayByOffset(date, 1); +} + export function calcDateNum(date: [Date, Date]) { const day1 = date[0].getTime(); const day2 = date[1].getTime(); return (day2 - day1) / (1000 * 60 * 60 * 24) + 1; } + +export function copyDates(dates: Date | Date[]) { + if (Array.isArray(dates)) { + return dates.map(date => { + if (date === null) { + return date; + } + + return new Date(date); + }); + } + + return new Date(dates); +}