src/main.vue
<template> <div class="el-calendar"> <div class="el-calendar__header"> <div class="el-calendar__title"> {{ i18nDate }} </div> <div class="el-calendar__button-group" v-if="validatedRange.length === 0"> <el-button-group> <el-button type="plain" size="mini" @click="selectDate('prev-month')"> {{ t('el.datepicker.prevMonth') }} </el-button> <el-button type="plain" size="mini" @click="selectDate('today')"> {{ t('el.datepicker.today') }} </el-button> <el-button type="plain" size="mini" @click="selectDate('next-month')"> {{ t('el.datepicker.nextMonth') }} </el-button> </el-button-group> </div> </div> <div class="el-calendar__body" v-if="validatedRange.length === 0" key="no-range"> <date-table :date="date" :selected-day="realSelectedDay" @pick="pickDay" /> </div> <div v-else class="el-calendar__body" key="has-range"> <date-table v-for="(range, index) in validatedRange" :key="index" :date="range[0]" :selected-day="realSelectedDay" :range="range" :hide-header="index !== 0" @pick="pickDay" /> </div> </div> </template> <script> import Locale from 'element-ui/src/mixins/locale'; import fecha from 'element-ui/src/utils/date'; import DateTable from './date-table'; import { validateRangeInOneMonth } from 'element-ui/src/utils/date-util'; const validTypes = ['prev-month', 'today', 'next-month']; const oneDay = 86400000; export default { name: 'ElCalendar', mixins: [Locale], components: { DateTable }, props: { value: [Date, String, Number], range: { type: Array, validator(range) { if (Array.isArray(range)) { return range.length === 2 && range.every( item => typeof item === 'string' || typeof item === 'number' || item instanceof Date); } else { return true; } } } }, provide() { return { elCalendar: this }; }, methods: { // 接收子组件选中的日期 pickDay(day) { // 设置为选中日 this.realSelectedDay = day; }, /** 点击上月今天下月 */ selectDate(type) { if (validTypes.indexOf(type) === -1) { throw new Error(`invalid type ${type}`); } let day = ''; if (type === 'prev-month') { // 上月第一天 day = `${this.prevMonthDatePrefix}-01`; } else if (type === 'next-month') { // 下月第一天 day = `${this.nextMonthDatePrefix}-01`; } else { // 今天 day = this.formatedToday; } if (day === this.formatedDate) return; this.pickDay(day); }, toDate(val) { if (!val) { throw new Error('invalid val'); } return val instanceof Date ? val : new Date(val); } }, computed: { prevMonthDatePrefix() { const temp = new Date(this.date.getTime()); // 获取上个月最后一天 temp.setDate(0); // 返回上个月年月 return fecha.format(temp, 'yyyy-MM'); }, curMonthDatePrefix() { return fecha.format(this.date, 'yyyy-MM'); }, nextMonthDatePrefix() { const temp = new Date(this.date.getFullYear(), this.date.getMonth() + 1, 1); return fecha.format(temp, 'yyyy-MM'); }, formatedDate() { return fecha.format(this.date, 'yyyy-MM-dd'); }, i18nDate() { const year = this.formatedDate.slice(0, 4); const month = this.formatedDate.slice(5, 7).replace('0', ''); return `${year} ${this.t('el.datepicker.year')} ${this.t('el.datepicker.month' + month)}`; }, formatedToday() { return fecha.format(this.now, 'yyyy-MM-dd'); }, // 动态计算选中日,重写get,set方法 realSelectedDay: { get() { if (!this.value) return this.selectedDay; return this.formatedDate; }, set(val) { this.selectedDay = val; const date = new Date(val); // 此处双向绑定,相当于v-model this.$emit('input', date); } }, // 没有range时,把date传给子组件date-table date() { if (!this.value) { if (this.realSelectedDay) { return new Date(this.selectedDay); } else if (this.validatedRange.length) { return this.validatedRange[0][0]; } return this.now; } else { return this.toDate(this.value); } }, // if range is valid, we get a two-digit array validatedRange() { let range = this.range; if (!range) return []; const expetedMap = { 0: { value: 1, message: 'start of range should be Monday.' }, 1: { value: 0, message: 'end of range should be Sunday.' } }; range = range.reduce((prev, val, index) => { const date = this.toDate(val); if (date.getDay() !== expetedMap[index].value) { console.warn('[ElementCalendar]', expetedMap[index].message, ' invalid range will be ignored'); } else { prev = prev.concat(date); } return prev; }, []); if (range.length === 2) { const [start, end] = range; if (start > end) { console.warn('[ElementCalendar]end time should be greater than start time'); return []; } // start time and end time in one month if (validateRangeInOneMonth(start, end)) { return [ [start, end] ]; } const data = []; let startDay = new Date(start.getFullYear(), start.getMonth() + 1, 1); const lastDay = this.toDate(startDay.getTime() - oneDay); if (!validateRangeInOneMonth(startDay, end)) { console.warn('[ElementCalendar]start time and end time interval must not exceed two months'); return []; } data.push([ start, lastDay ]); let interval = startDay.getDay(); interval = interval <= 1 ? Math.abs(interval - 1) : (8 - interval); startDay = this.toDate(startDay.getTime() + interval * oneDay); if (startDay.getDate() < end.getDate()) { data.push([ startDay, end ]); } return data; } return []; } }, data() { return { selectedDay: '', now: new Date() }; } }; </script>
src/date-table.vue
<script> import fecha from 'element-ui/src/utils/date'; import { range as rangeArr, getFirstDayOfMonth, getPrevMonthLastDays, getMonthDays, getI18nSettings, validateRangeInOneMonth } from 'element-ui/src/utils/date-util'; export default { props: { selectedDay: String, // formated date yyyy-MM-dd range: { type: Array, validator(val) { if (!(val && val.length)) return true; const [start, end] = val; return validateRangeInOneMonth(start, end); } }, date: Date, hideHeader: Boolean }, inject: ['elCalendar'], methods: { // 本月分割成7个数组,赋给rows,动态渲染 toNestedArr(days) { return rangeArr(days.length / 7).map((_, index) => { const start = index * 7; return days.slice(start, start + 7); }); }, getFormateDate(day, type) { if (!day || ['prev', 'current', 'next'].indexOf(type) === -1) { throw new Error('invalid day or type'); } let prefix = this.curMonthDatePrefix; if (type === 'prev') { prefix = this.prevMonthDatePrefix; } else if (type === 'next') { prefix = this.nextMonthDatePrefix; } day = `00${day}`.slice(-2); return `${prefix}-${day}`; }, getCellClass({ text, type}) { const classes = [type]; if (type === 'current') { const date = this.getFormateDate(text, type); if (date === this.selectedDay) { classes.push('is-selected'); } if (date === this.formatedToday) { classes.push('is-today'); } } return classes; }, // 点击哪天事件往上传递,日期传出去,父组件接收pick事件 pickDay({ text, type }) { const date = this.getFormateDate(text, type); this.$emit('pick', date); }, cellRenderProxy({ text, type }) { let render = this.elCalendar.$scopedSlots.dateCell; if (!render) return <span>{ text }</span>; const day = this.getFormateDate(text, type); const date = new Date(day); const data = { isSelected: this.selectedDay === day, type: `${type}-month`, day }; return render({ date, data }); } }, computed: { prevMonthDatePrefix() { const temp = new Date(this.date.getTime()); temp.setDate(0); return fecha.format(temp, 'yyyy-MM'); }, curMonthDatePrefix() { return fecha.format(this.date, 'yyyy-MM'); }, nextMonthDatePrefix() { const temp = new Date(this.date.getFullYear(), this.date.getMonth() + 1, 1); return fecha.format(temp, 'yyyy-MM'); }, formatedToday() { return this.elCalendar.formatedToday; }, isInRange() { return this.range && this.range.length; }, /* 动态计算rows根据接收的date */ rows() { let days = []; // if range exists, should render days in range. if (this.isInRange) { const [start, end] = this.range; const currentMonthRange = rangeArr(end.getDate() - start.getDate() + 1).map((_, index) => ({ text: start.getDate() + index, type: 'current' })); let remaining = currentMonthRange.length % 7; remaining = remaining === 0 ? 0 : 7 - remaining; const nextMonthRange = rangeArr(remaining).map((_, index) => ({ text: index + 1, type: 'next' })); days = currentMonthRange.concat(nextMonthRange); } else { const date = this.date; // 获取当月第一天 const firstDay = getFirstDayOfMonth(date); // 根据当月第一天计算当月中上个月显示几天 const prevMonthDays = getPrevMonthLastDays(date, firstDay - 1).map(day => ({ text: day, type: 'prev' })); // 获取本月多少天 const currentMonthDays = getMonthDays(date).map(day => ({ text: day, type: 'current' })); days = [...prevMonthDays, ...currentMonthDays]; // 日历一共6行每周7天共42天,42-上个月-本月=下月天数 const nextMonthDays = rangeArr(42 - days.length).map((_, index) => ({ text: index + 1, type: 'next' })); // 连起来共多少天 days = days.concat(nextMonthDays); } return this.toNestedArr(days); } }, data() { const dayNames = getI18nSettings().dayNames; return { DAYS: dayNames.slice(1).concat(dayNames[0]) }; }, render() { const thead = this.hideHeader ? null : (<thead> { this.DAYS.map(day => <th key={day}>{ day }</th>) } </thead>); return ( <table class={{ 'el-calendar-table': true, 'is-range': this.isInRange }} cellspacing="0" cellpadding="0"> { thead } <tbody> { this.rows.map((row, index) => <tr class={{ 'el-calendar-table__row': true, 'el-calendar-table__row--hide-border': index === 0 && this.hideHeader }} key={index}> { row.map((cell, key) => <td key={key} class={ this.getCellClass(cell) } onClick={this.pickDay.bind(this, cell)}> <div class="el-calendar-day"> { this.cellRenderProxy(cell) } </div> </td>) } </tr>) } </tbody> </table>); } }; </script>