mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-04-06 03:57:59 +08:00
feat(Calendar): support select date range
This commit is contained in:
parent
c447152be9
commit
0db1a03996
@ -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
74
src/calendar/Month.js
Normal 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>
|
||||
);
|
||||
}
|
||||
});
|
@ -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 | 自定义标题 |
|
||||
|
||||
### 方法
|
||||
|
||||
|
@ -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]
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -12,6 +12,10 @@ export default {
|
||||
confirmDelete: '确定要删除么',
|
||||
telInvalid: '请填写正确的电话',
|
||||
vanCalendar: {
|
||||
end: '结束',
|
||||
start: '开始',
|
||||
title: '日期选择',
|
||||
confirm: '确定',
|
||||
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
|
||||
monthTitle: (year: number, month: number) => `${year}年${month}月`
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user