feat(Calendar): support select date range

This commit is contained in:
陈嘉涵 2019-12-25 10:56:53 +08:00 committed by neverland
parent c447152be9
commit 0db1a03996
8 changed files with 365 additions and 125 deletions

View File

@ -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 <div class={bem('title')}>{this.title}</div>;
const title = this.slots('title') || this.title || t('title');
if (title) {
return <div class={bem('header-title')}>{title}</div>;
}
},
genMonth() {
return (
<div class={bem('month')}>
<div class={bem('month-title')}>
{formatMonthTitle(this.currentMonth)}
</div>
);
@ -39,7 +41,7 @@ export default createComponent({
render() {
return (
<div class={bem()}>
<div class={bem('header')}>
{this.genTitle()}
{this.genMonth()}
{this.genWeekDays()}

74
src/calendar/Month.js Normal file
View File

@ -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 <div class={bem('month-title')}>{this.title}</div>;
}
},
genDay(item) {
const { type } = item;
const onClick = () => {
if (type !== 'disabled') {
this.$emit('click', item);
}
};
if (type === 'selected') {
return (
<div class={bem('day')} onClick={onClick}>
<div class={bem('selected-day')}>{item.day}</div>
</div>
);
}
const label = this.getLabel(item);
const Label = label && <div class={bem('day-label')}>{label}</div>;
return (
<div class={bem('day', [type])} onClick={onClick}>
{item.day}
{Label}
</div>
);
}
},
render() {
return (
<div class={bem('month')}>
{this.genTitle()}
<div class={bem('days')}>
<div class={bem('month-mark')}>{this.date.getMonth() + 1}</div>
{this.days.map(this.genDay)}
</div>
</div>
);
}
});

View File

@ -1,5 +1,9 @@
# Calendar 日历
### 介绍
日历组件可以用于选择日期或日期区间,通常与 [弹出层](#/zh-CN/popup) 组件配合使用
### 引入
``` javascript
@ -11,7 +15,7 @@ Vue.use(Calendar);
## 代码演示
### 基础用法
### 选择单个日期
```html
<van-calendar />
@ -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 | 自定义标题 |
### 方法

View File

@ -2,20 +2,46 @@
<demo-section>
<demo-block :title="$t('basicUsage')">
<van-cell
:title="$t('selectDate')"
:value="formatDate(date.selectDate)"
is-link
@click="toggle('selectDate', true)"
:title="$t('selectSingleDate')"
:value="formatDate(date.selectSingleDate)"
@click="toggle('selectSingleDate', true)"
/>
</demo-block>
<van-popup v-model="show.selectDate" round closeable position="bottom">
<van-calendar
v-model="date.selectDate"
:title="$t('title')"
@select="show.selectDate = false"
<van-popup
v-model="show.selectSingleDate"
round
closeable
position="bottom"
style="height: 80vh;"
>
<van-calendar
v-model="date.selectSingleDate"
@select="show.selectSingleDate = false"
/>
</van-popup>
<van-cell
is-link
:title="$t('selectDateRange')"
:value="formatDateRange(date.selectDateRange)"
@click="toggle('selectDateRange', true)"
/>
</van-popup>
<van-popup
v-model="show.selectDateRange"
round
closeable
position="bottom"
style="height: 80vh;"
>
<van-calendar
v-model="date.selectDateRange"
type="range"
@select="show.selectDateRange = false"
/>
</van-popup>
</demo-block>
</demo-section>
</template>
@ -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]
)}`;
}
}
}
};

View File

@ -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 && (
<div class={bem('month-title')}>{month.title}</div>
);
const Days = month.days.map(item => {
const onClick = () => {
this.onClickDay(item);
};
if (item.selected) {
return (
<div class={bem('day')} onClick={onClick}>
<div class={bem('selected-day')}>{item.day}</div>
</div>
);
}
return (
<div
class={bem('day', { disabled: item.disabled })}
onClick={onClick}
>
{item.day}
</div>
);
});
return (
<div class={bem('month')} ref="month" refInFor>
{Title}
<div class={bem('days')}>
<div class={bem('month-mark')}>{month.date.getMonth() + 1}</div>
{Days}
</div>
</div>
<Month
ref="month"
refInFor
days={month.days}
date={month.date}
title={index !== 0 ? month.title : ''}
onClick={this.onClickDay}
/>
);
},
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 (
<div class={bem('footer')}>
<Button
round
block
type="danger"
disabled={disabled}
class={bem('confirm')}
onClick={this.onConfirmRange}
>
{text || t('confirm')}
</Button>
</div>
);
}
}
},
@ -180,10 +236,17 @@ export default createComponent({
render() {
return (
<div class={bem()}>
<Header title={this.title} currentMonth={this.currentMonth} />
<Header
title={this.title}
currentMonth={this.currentMonth}
scopedSlots={{
title: () => this.slots('title')
}}
/>
<div ref="body" class={bem('body')} onScroll={this.onScroll}>
{this.months.map(this.genMonth)}
</div>
{this.genFooter()}
</div>
);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -12,6 +12,10 @@ export default {
confirmDelete: '确定要删除么',
telInvalid: '请填写正确的电话',
vanCalendar: {
end: '结束',
start: '开始',
title: '日期选择',
confirm: '确定',
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
monthTitle: (year: number, month: number) => `${year}${month}`
},