date-table
<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: {
// 选中的日期,格式为 yyyy-MM-dd
selectedDay: String,
// 日期范围,数组格式 [start, end]
range: {
type: Array,
// 验证器:确保范围在同一个月内
validator(val) {
if (!(val && val.length)) return true;
const [start, end] = val;
return validateRangeInOneMonth(start, end);
}
},
// 当前显示的月份日期
date: Date,
// 是否隐藏表头(星期几)
hideHeader: Boolean,
// 每周的第一天(0-6,0 表示周日)
firstDayOfWeek: Number
},
// 注入父组件 elCalendar 实例
inject: ['elCalendar'],
methods: {
// 将天数数组转换为嵌套数组(每行 7 天)
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;
}
// 将日期补齐为两位数,如 1 -> 01
day = `00${day}`.slice(-2);
return `${prefix}-${day}`;
},
// 获取日期单元格的类名
getCellClass({ text, type}) {
const classes = [type];
if (type === 'current') {
const date = this.getFormateDate(text, type);
// 如果是选中的日期,添加 is-selected 类
if (date === this.selectedDay) {
classes.push('is-selected');
}
// 如果是今天,添加 is-today 类
if (date === this.formatedToday) {
classes.push('is-today');
}
}
return classes;
},
// 选择日期
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: {
// 获取国际化设置的星期名称
WEEK_DAYS() {
return getI18nSettings().dayNames;
},
// 上个月的日期前缀(yyyy-MM)
prevMonthDatePrefix() {
const temp = new Date(this.date.getTime());
temp.setDate(0);
return fecha.format(temp, 'yyyy-MM');
},
// 当前月的日期前缀(yyyy-MM)
curMonthDatePrefix() {
return fecha.format(this.date, 'yyyy-MM');
},
// 下个月的日期前缀(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() {
let days = [];
// 如果存在范围,渲染范围内的日期
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;
// 获取当月第一天是星期几
let firstDay = getFirstDayOfMonth(date);
firstDay = firstDay === 0 ? 7 : firstDay;
// 获取每周的第一天(默认为 1,即周一)
const firstDayOfWeek = typeof this.firstDayOfWeek === 'number' ? this.firstDayOfWeek : 1;
// 计算偏移量
const offset = (7 + firstDay - firstDayOfWeek) % 7;
// 上个月的日期
const prevMonthDays = getPrevMonthLastDays(date, offset).map(day => ({
text: day,
type: 'prev'
}));
// 当前月的日期
const currentMonthDays = getMonthDays(date).map(day => ({
text: day,
type: 'current'
}));
days = [...prevMonthDays, ...currentMonthDays];
// 下个月的日期,补齐到 42 天(6 行)
const nextMonthDays = rangeArr(42 - days.length).map((_, index) => ({
text: index + 1,
type: 'next'
}));
days = days.concat(nextMonthDays);
}
return this.toNestedArr(days);
},
// 计算星期几的显示顺序
weekDays() {
const start = this.firstDayOfWeek;
const { WEEK_DAYS } = this;
if (typeof start !== 'number' || start === 0) {
return WEEK_DAYS.slice();
} else {
// 根据每周的第一天调整星期顺序
return WEEK_DAYS.slice(start).concat(WEEK_DAYS.slice(0, start));
}
}
},
// 渲染函数
render() {
// 如果隐藏表头,不渲染 thead
const thead = this.hideHeader ? null : (<thead>
{
this.weekDays.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>
Calendar
<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"
:first-day-of-week="realFirstDayOfWeek"
@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"
:first-day-of-week="realFirstDayOfWeek"
@pick="pickDay" />
</div>
</div>
</template>
<script>
import Locale from 'element-ui/src/mixins/locale';
import fecha from 'element-ui/src/utils/date';
import ElButton from 'element-ui/packages/button';
import ElButtonGroup from 'element-ui/packages/button-group';
import DateTable from './date-table';
import { validateRangeInOneMonth } from 'element-ui/src/utils/date-util';
// 有效的日期选择类型
const validTypes = ['prev-month', 'today', 'next-month'];
// 星期几的英文名称
const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
// 一天的毫秒数
const oneDay = 86400000;
export default {
name: 'ElCalendar',
// 混入国际化 mixin
mixins: [Locale],
// 注册子组件
components: {
DateTable,
ElButton,
ElButtonGroup
},
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;
}
}
},
// 每周的第一天(0-6,0 表示周日)
firstDayOfWeek: {
type: Number,
default: 1
}
},
// 向子组件提供当前组件实例
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);
},
// 将值转换为 Date 对象
toDate(val) {
if (!val) {
throw new Error('invalid val');
}
return val instanceof Date ? val : new Date(val);
},
// 验证范围的开始或结束日期
rangeValidator(date, isStart) {
const firstDayOfWeek = this.realFirstDayOfWeek;
// 期望的星期几
const expected = isStart ? firstDayOfWeek : (firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1);
const message = `${isStart ? 'start' : 'end'} of range should be ${weekDays[expected]}.`;
if (date.getDay() !== expected) {
console.warn('[ElementCalendar]', message, 'Invalid range will be ignored.');
return false;
}
return true;
}
},
computed: {
// 上个月的日期前缀(yyyy-MM)
prevMonthDatePrefix() {
const temp = new Date(this.date.getTime());
temp.setDate(0);
return fecha.format(temp, 'yyyy-MM');
},
// 当前月的日期前缀(yyyy-MM)
curMonthDatePrefix() {
return fecha.format(this.date, 'yyyy-MM');
},
// 下个月的日期前缀(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.date.getFullYear();
const month = this.date.getMonth() + 1;
return `${year} ${this.t('el.datepicker.year')} ${this.t('el.datepicker.month' + month)}`;
},
// 格式化的今天日期
formatedToday() {
return fecha.format(this.now, 'yyyy-MM-dd');
},
// 实际选中的日期(计算属性)
realSelectedDay: {
get() {
// 如果没有 value,使用 selectedDay
if (!this.value) return this.selectedDay;
// 否则使用格式化的当前日期
return this.formatedDate;
},
set(val) {
// 更新 selectedDay
this.selectedDay = val;
// 将字符串转换为 Date 对象并触发 input 事件
const date = new Date(val);
this.$emit('input', date);
}
},
// 当前显示的日期
date() {
if (!this.value) {
// 如果没有 value,使用选中的日期
if (this.realSelectedDay) {
const d = this.selectedDay.split('-');
return new Date(d[0], d[1] - 1, d[2]);
} else if (this.validatedRange.length) {
// 如果有范围,使用范围的开始日期
return this.validatedRange[0][0];
}
// 否则使用今天的日期
return this.now;
} else {
// 将 value 转换为 Date 对象
return this.toDate(this.value);
}
},
// 验证并计算日期范围
validatedRange() {
let range = this.range;
if (!range) return [];
// 验证范围的开始和结束日期
range = range.reduce((prev, val, index) => {
const date = this.toDate(val);
if (this.rangeValidator(date, index === 0)) {
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 [];
}
// 开始和结束时间在同一个月
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
]);
// 下一月的时间范围,需要计算一下该月的第一个周起始日
const firstDayOfWeek = this.realFirstDayOfWeek;
const nextMontFirstDay = startDay.getDay();
let interval = 0;
// 计算间隔天数
if (nextMontFirstDay !== firstDayOfWeek) {
if (firstDayOfWeek === 0) {
interval = 7 - nextMontFirstDay;
} else {
interval = firstDayOfWeek - nextMontFirstDay;
interval = interval > 0 ? interval : 7 + interval;
}
}
// 调整下个月的开始日期
startDay = this.toDate(startDay.getTime() + interval * oneDay);
// 如果还有剩余天数,添加第二个月的范围
if (startDay.getDate() < end.getDate()) {
data.push([
startDay,
end
]);
}
return data;
}
return [];
},
// 实际的每周第一天
realFirstDayOfWeek() {
// 验证 firstDayOfWeek 的值是否在 0-6 之间
if (this.firstDayOfWeek < 1 || this.firstDayOfWeek > 6) {
return 0;
}
return Math.floor(this.firstDayOfWeek);
}
},
data() {
return {
// 内部状态:选中的日期
selectedDay: '',
// 当前时间
now: new Date()
};
}
};
</script>
ElCalendar 组件教学文档
组件概述
ElCalendar 是一个日历组件,用于显示和选择日期。它由两个子组件组成:
- ElCalendar:日历容器组件,负责整体布局、日期选择和范围管理
- DateTable:日期表格组件,负责渲染日历的日期单元格
该组件支持单日期选择、日期范围选择、自定义每周第一天、国际化等功能,常用于日程管理、日期选择、范围预订等场景。
一、DateTable 组件详解
1.1 Props 属性
selectedDay
selectedDay: String
作用:选中的日期。
格式:yyyy-MM-dd
示例:'2024-01-15'
range
range: {
type: Array,
validator(val) {
if (!(val && val.length)) return true;
const [start, end] = val;
return validateRangeInOneMonth(start, end);
}
}
作用:日期范围。
格式:[start, end]
验证器:
- 确保范围在同一个月内
- 使用
validateRangeInOneMonth函数验证
date
date: Date
作用:当前显示的月份日期。
类型:Date 对象
hideHeader
hideHeader: Boolean
作用:是否隐藏表头(星期几)。
类型:布尔值
默认值:false
firstDayOfWeek
firstDayOfWeek: Number
作用:每周的第一天。
类型:数字
范围:0-6(0 表示周日,1 表示周一,以此类推)
1.2 Inject 注入
inject: ['elCalendar']
作用:注入父组件 elCalendar 实例。
说明:
- 获取父组件的实例
- 访问父组件的属性和方法
- 用于获取国际化设置和自定义插槽
1.3 Methods 方法
toNestedArr
toNestedArr(days) {
return rangeArr(days.length / 7).map((_, index) => {
const start = index * 7;
return days.slice(start, start + 7);
});
}
作用:将天数数组转换为嵌套数组(每行 7 天)。
说明:
- 将一维数组转换为二维数组
- 每个子数组包含 7 天(一周)
- 用于渲染日历表格的行
示例:
// 输入:[1, 2, 3, ..., 42]
// 输出:[[1, 2, 3, 4, 5, 6, 7], [8, 9, ..., 14], ...]
getFormateDate
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}`;
}
作用:格式化日期字符串。
参数:
day:日期数字(1-31)type:日期类型('prev'、'current'、'next')
返回值:yyyy-MM-dd 格式的日期字符串
逻辑:
- 验证参数有效性
- 根据类型选择月份前缀
- 将日期补齐为两位数(如 1 -> 01)
- 拼接成完整日期字符串
示例:
getFormateDate(15, 'current'); // 返回:'2024-01-15'
getFormateDate(28, 'prev'); // 返回:'2023-12-28'
getFormateDate(1, 'next'); // 返回:'2024-02-01'
getCellClass
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;
}
作用:获取日期单元格的类名。
参数:
text:日期数字type:日期类型
返回值:类名数组
逻辑:
- 添加基础类型类名('prev'、'current'、'next')
- 如果是当前月:
- 如果是选中的日期,添加 'is-selected'
- 如果是今天,添加 'is-today'
示例:
getCellClass({ text: 15, type: 'current' });
// 返回:['current', 'is-selected', 'is-today']
getCellClass({ text: 28, type: 'prev' });
// 返回:['prev']
pickDay
pickDay({ text, type }) {
const date = this.getFormateDate(text, type);
this.$emit('pick', date);
}
作用:选择日期。
逻辑:
- 格式化日期字符串
- 触发 'pick' 事件,传递日期
cellRenderProxy
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 });
}
作用:日期单元格渲染代理。
逻辑:
- 检查是否有自定义的日期单元格插槽
- 如果没有,返回默认的日期文本
- 如果有,调用自定义渲染函数
自定义渲染:
<el-calendar>
<template #dateCell="{ date, data }">
<div>{{ date.getDate() }}</div>
<div v-if="data.isSelected">已选择</div>
</template>
</el-calendar>
1.4 Computed 计算属性
WEEK_DAYS
WEEK_DAYS() {
return getI18nSettings().dayNames;
}
作用:获取国际化设置的星期名称。
返回值:星期名称数组
示例:
// 中文:['周日', '周一', '周二', '周三', '周四', '周五', '周六']
// 英文:['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
prevMonthDatePrefix
prevMonthDatePrefix() {
const temp = new Date(this.date.getTime());
temp.setDate(0);
return fecha.format(temp, 'yyyy-MM');
}
作用:获取上个月的日期前缀。
逻辑:
- 复制当前日期
- 设置日期为 0(上个月的最后一天)
- 格式化为
yyyy-MM
示例:
// 当前日期:2024-01-15
// 返回:'2023-12'
curMonthDatePrefix
curMonthDatePrefix() {
return fecha.format(this.date, 'yyyy-MM');
}
作用:获取当前月的日期前缀。
返回值:yyyy-MM 格式的字符串
nextMonthDatePrefix
nextMonthDatePrefix() {
const temp = new Date(this.date.getFullYear(), this.date.getMonth() + 1, 1);
return fecha.format(temp, 'yyyy-MM');
}
作用:获取下个月的日期前缀。
逻辑:
- 创建下个月的第一天
- 格式化为
yyyy-MM
formatedToday
formatedToday() {
return this.elCalendar.formatedToday;
}
作用:获取格式化的今天日期。
说明:从父组件获取,避免重复计算。
isInRange
isInRange() {
return this.range && this.range.length;
}
作用:判断是否在范围内。
返回值:布尔值
rows
rows() {
let days = [];
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;
let firstDay = getFirstDayOfMonth(date);
firstDay = firstDay === 0 ? 7 : firstDay;
const firstDayOfWeek = typeof this.firstDayOfWeek === 'number' ? this.firstDayOfWeek : 1;
const offset = (7 + firstDay - firstDayOfWeek) % 7;
const prevMonthDays = getPrevMonthLastDays(date, offset).map(day => ({
text: day,
type: 'prev'
}));
const currentMonthDays = getMonthDays(date).map(day => ({
text: day,
type: 'current'
}));
days = [...prevMonthDays, ...currentMonthDays];
const nextMonthDays = rangeArr(42 - days.length).map((_, index) => ({
text: index + 1,
type: 'next'
}));
days = days.concat(nextMonthDays);
}
return this.toNestedArr(days);
}
作用:计算日历的行数据。
逻辑:
范围模式:
- 计算当前月的日期范围
- 补齐到整行(7 天)
- 添加下个月的日期
正常模式:
- 获取当月第一天是星期几
- 计算偏移量
- 添加上个月的日期
- 添加当前月的日期
- 补齐到 42 天(6 行)
- 转换为嵌套数组
weekDays
weekDays() {
const start = this.firstDayOfWeek;
const { WEEK_DAYS } = this;
if (typeof start !== 'number' || start === 0) {
return WEEK_DAYS.slice();
} else {
return WEEK_DAYS.slice(start).concat(WEEK_DAYS.slice(0, start));
}
}
作用:计算星期几的显示顺序。
逻辑:
- 如果
firstDayOfWeek为 0 或不是数字,保持原顺序 - 否则,根据每周的第一天调整顺序
示例:
// firstDayOfWeek = 1(周一)
// 返回:['周一', '周二', '周三', '周四', '周五', '周六', '周日']
// firstDayOfWeek = 0(周日)
// 返回:['周日', '周一', '周二', '周三', '周四', '周五', '周六']
1.5 Render 渲染函数
render() {
const thead = this.hideHeader ? null : (<thead>
{
this.weekDays.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>);
}
作用:渲染日历表格。
结构:
<table>:表格容器<thead>:表头(星期几)<tbody>:表格主体(日期行)<tr>:表格行<td>:表格单元格
说明:
- 使用 JSX 语法渲染
- 根据条件渲染表头
- 动态绑定类名
- 绑定点击事件
二、ElCalendar 组件详解
2.1 模板部分(Template)
2.1.1 日历头部
<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>
作用:显示当前年月和导航按钮。
组成:
- 标题:显示当前年月(国际化)
- 按钮组:上个月、今天、下个月
条件:
- 只在非范围模式下显示按钮组
2.1.2 日历主体(非范围模式)
<div class="el-calendar__body" v-if="validatedRange.length === 0" key="no-range">
<date-table
:date="date"
:selected-day="realSelectedDay"
:first-day-of-week="realFirstDayOfWeek"
@pick="pickDay" />
</div>
作用:渲染单个日期表格。
条件:
- 非范围模式
传递的属性:
date:当前显示的月份selected-day:选中的日期first-day-of-week:每周的第一天
2.1.3 日历主体(范围模式)
<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"
:first-day-of-week="realFirstDayOfWeek"
@pick="pickDay" />
</div>
作用:渲染多个日期表格(范围模式)。
条件:
- 范围模式
传递的属性:
date:范围的开始日期range:日期范围hide-header:第一个表格显示表头,后续表格隐藏
2.2 脚本部分(Script)
2.2.1 常量定义
const validTypes = ['prev-month', 'today', 'next-month'];
const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const oneDay = 86400000;
作用:定义常量。
说明:
validTypes:有效的日期选择类型weekDays:星期几的英文名称oneDay:一天的毫秒数(24 * 60 * 60 * 1000)
2.2.2 Mixins 混入
mixins: [Locale]
作用:混入国际化 mixin。
说明:
- 提供
t()方法用于翻译 - 自动处理语言切换
2.2.3 Props 属性
value
value: [Date, String, Number]
作用:当前选中的日期。
类型:
- Date 对象
- 字符串(日期字符串)
- 数字(时间戳)
range
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;
}
}
}
作用:日期范围。
格式:[start, end]
验证器:
- 确保数组长度为 2
- 确保每个元素是有效的日期类型
firstDayOfWeek
firstDayOfWeek: {
type: Number,
default: 1
}
作用:每周的第一天。
类型:数字
默认值:1(周一)
范围:0-6
2.2.4 Provide 提供
provide() {
return {
elCalendar: this
};
}
作用:向子组件提供当前组件实例。
说明:
- 子组件可以通过 inject 访问
- 用于获取国际化设置和自定义插槽
2.2.5 Methods 方法
pickDay
pickDay(day) {
this.realSelectedDay = day;
}
作用:选择日期。
逻辑:
- 更新
realSelectedDay计算属性 - 触发
input事件
selectDate
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);
}
作用:选择日期类型(上个月、今天、下个月)。
参数:
type:日期类型('prev-month'、'today'、'next-month')
逻辑:
- 验证类型有效性
- 根据类型计算目标日期
- 如果目标日期与当前日期相同,不处理
- 否则,选择目标日期
toDate
toDate(val) {
if (!val) {
throw new Error('invalid val');
}
return val instanceof Date ? val : new Date(val);
}
作用:将值转换为 Date 对象。
逻辑:
- 如果已经是 Date 对象,直接返回
- 否则,转换为 Date 对象
rangeValidator
rangeValidator(date, isStart) {
const firstDayOfWeek = this.realFirstDayOfWeek;
const expected = isStart ? firstDayOfWeek : (firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1);
const message = `${isStart ? 'start' : 'end'} of range should be ${weekDays[expected]}.`;
if (date.getDay() !== expected) {
console.warn('[ElementCalendar]', message, 'Invalid range will be ignored.');
return false;
}
return true;
}
作用:验证范围的开始或结束日期。
参数:
date:要验证的日期isStart:是否是开始日期
逻辑:
- 计算期望的星期几
- 检查日期的星期几是否匹配
- 如果不匹配,输出警告并返回 false
- 否则,返回 true
2.2.6 Computed 计算属性
prevMonthDatePrefix
prevMonthDatePrefix() {
const temp = new Date(this.date.getTime());
temp.setDate(0);
return fecha.format(temp, 'yyyy-MM');
}
作用:获取上个月的日期前缀。
curMonthDatePrefix
curMonthDatePrefix() {
return fecha.format(this.date, 'yyyy-MM');
}
作用:获取当前月的日期前缀。
nextMonthDatePrefix
nextMonthDatePrefix() {
const temp = new Date(this.date.getFullYear(), this.date.getMonth() + 1, 1);
return fecha.format(temp, 'yyyy-MM');
}
作用:获取下个月的日期前缀。
formatedDate
formatedDate() {
return fecha.format(this.date, 'yyyy-MM-dd');
}
作用:获取格式化的当前日期。
i18nDate
i18nDate() {
const year = this.date.getFullYear();
const month = this.date.getMonth() + 1;
return `${year} ${this.t('el.datepicker.year')} ${this.t('el.datepicker.month' + month)}`;
}
作用:获取国际化的日期显示(年月)。
示例:
// 中文:'2024 年 1 月'
// 英文:'2024 January'
formatedToday
formatedToday() {
return fecha.format(this.now, 'yyyy-MM-dd');
}
作用:获取格式化的今天日期。
realSelectedDay
realSelectedDay: {
get() {
if (!this.value) return this.selectedDay;
return this.formatedDate;
},
set(val) {
this.selectedDay = val;
const date = new Date(val);
this.$emit('input', date);
}
}
作用:实际选中的日期(计算属性)。
getter:
- 如果没有
value,使用selectedDay - 否则,使用格式化的当前日期
setter:
- 更新
selectedDay - 将字符串转换为 Date 对象
- 触发
input事件
date
date() {
if (!this.value) {
if (this.realSelectedDay) {
const d = this.selectedDay.split('-');
return new Date(d[0], d[1] - 1, d[2]);
} else if (this.validatedRange.length) {
return this.validatedRange[0][0];
}
return this.now;
} else {
return this.toDate(this.value);
}
}
作用:当前显示的日期。
逻辑:
- 如果没有
value:- 如果有选中的日期,使用选中的日期
- 如果有范围,使用范围的开始日期
- 否则,使用今天的日期
- 否则,将
value转换为 Date 对象
validatedRange
validatedRange() {
let range = this.range;
if (!range) return [];
range = range.reduce((prev, val, index) => {
const date = this.toDate(val);
if (this.rangeValidator(date, index === 0)) {
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 [];
}
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]);
const firstDayOfWeek = this.realFirstDayOfWeek;
const nextMontFirstDay = startDay.getDay();
let interval = 0;
if (nextMontFirstDay !== firstDayOfWeek) {
if (firstDayOfWeek === 0) {
interval = 7 - nextMontFirstDay;
} else {
interval = firstDayOfWeek - nextMontFirstDay;
interval = interval > 0 ? interval : 7 + interval;
}
}
startDay = this.toDate(startDay.getTime() + interval * oneDay);
if (startDay.getDate() < end.getDate()) {
data.push([startDay, end]);
}
return data;
}
return [];
}
作用:验证并计算日期范围。
逻辑:
- 验证范围的开始和结束日期
- 检查结束日期是否大于开始日期
- 如果在同一个月,返回单个范围
- 如果跨月,计算两个月的范围
- 确保不超过两个月
realFirstDayOfWeek
realFirstDayOfWeek() {
if (this.firstDayOfWeek < 1 || this.firstDayOfWeek > 6) {
return 0;
}
return Math.floor(this.firstDayOfWeek);
}
作用:实际的每周第一天。
逻辑:
- 验证
firstDayOfWeek的值是否在 0-6 之间 - 如果不在范围内,返回 0(周日)
- 否则,返回向下取整的值
2.2.7 Data 数据
data() {
return {
selectedDay: '',
now: new Date()
};
}
作用:定义组件的响应式数据。
字段:
selectedDay:内部状态,选中的日期now:当前时间
三、组件使用示例
3.1 基础用法
<template>
<el-calendar v-model="value"></el-calendar>
</template>
<script>
export default {
data() {
return {
value: new Date()
}
}
}
</script>
说明:最简单的用法,绑定当前日期。
3.2 自定义每周第一天
<template>
<el-calendar v-model="value" :first-day-of-week="0"></el-calendar>
</template>
说明:设置每周的第一天为周日。
3.3 日期范围
<template>
<el-calendar :range="['2024-01-01', '2024-01-15']"></el-calendar>
</template>
说明:显示日期范围内的日历。
3.4 自定义日期单元格
<template>
<el-calendar v-model="value">
<template #dateCell="{ date, data }">
<div>{{ date.getDate() }}</div>
<div v-if="data.isSelected" class="selected">已选择</div>
</template>
</el-calendar>
</template>
说明:使用作用域插槽自定义日期单元格的渲染。
3.5 监听日期选择
<template>
<el-calendar v-model="value" @input="handleDateChange"></el-calendar>
</template>
<script>
export default {
data() {
return {
value: new Date()
}
},
methods: {
handleDateChange(date) {
console.log('选择的日期:', date);
}
}
}
</script>
说明:监听日期变化事件。
3.6 国际化
<template>
<el-calendar v-model="value"></el-calendar>
</template>
<script>
import ElementUI from 'element-ui';
import locale from 'element-ui/lib/locale/lang/en';
Vue.use(ElementUI, { locale });
</script>
说明:切换语言为英文。
四、核心知识点总结
4.1 日期处理
-
日期格式化:
- 使用
fecha.format()格式化日期 - 支持多种格式('yyyy-MM-dd'、'yyyy-MM' 等)
- 使用
-
日期计算:
- 使用 Date 对象进行日期计算
- 设置日期、月份、年份
-
日期验证:
- 验证日期范围
- 检查星期几
4.2 组件通信
-
Provide/Inject:
- 父组件提供实例
- 子组件注入实例
- 跨层级通信
-
Props/Events:
- Props:父组件向子组件传递数据
- Events:子组件向父组件传递数据
4.3 计算属性
-
复杂计算:
- 封装复杂的计算逻辑
- 响应式更新
-
Getter/Setter:
realSelectedDay使用 getter/setter- 实现双向绑定
4.4 渲染函数
-
JSX 语法:
- 使用 JSX 渲染复杂的结构
- 动态生成 DOM
-
条件渲染:
- 根据条件渲染不同的内容
- 使用三元运算符
4.5 国际化
-
Mixin 混入:
- 混入 Locale mixin
- 提供
t()方法
-
动态翻译:
- 根据语言切换显示
- 支持多语言
五、常见问题
5.1 如何设置日历的初始日期?
方法:使用 v-model 绑定日期。
<template>
<el-calendar v-model="value"></el-calendar>
</template>
<script>
export default {
data() {
return {
value: new Date('2024-01-15')
}
}
}
</script>
5.2 如何自定义日期单元格的样式?
方法:使用作用域插槽。
<template>
<el-calendar v-model="value">
<template #dateCell="{ date, data }">
<div :class="{ 'is-today': isToday(date) }">
{{ date.getDate() }}
</div>
</template>
</el-calendar>
</template>
<script>
export default {
methods: {
isToday(date) {
const today = new Date();
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
}
}
}
</script>
5.3 如何实现日期范围选择?
方法:使用 range 属性。
<template>
<el-calendar :range="range"></el-calendar>
</template>
<script>
export default {
data() {
return {
range: ['2024-01-01', '2024-01-15']
}
}
}
</script>
5.4 如何设置每周的第一天?
方法:使用 first-day-of-week 属性。
<template>
<el-calendar v-model="value" :first-day-of-week="1"></el-calendar>
</template>
可选值:
- 0:周日
- 1:周一
- 2:周二
- ...
- 6:周六
5.5 如何监听日期变化?
方法:监听 input 事件。
<template>
<el-calendar v-model="value" @input="handleDateChange"></el-calendar>
</template>
<script>
export default {
methods: {
handleDateChange(date) {
console.log('日期变化:', date);
}
}
}
</script>
六、扩展建议
6.1 功能扩展
- 多选日期:支持选择多个日期
- 禁用日期:添加
disabled-date属性 - 快捷选项:添加快捷日期选择
- 日期标记:在特定日期上显示标记
- 拖拽选择:支持拖拽选择日期范围
6.2 样式扩展
- 主题定制:支持多种主题颜色
- 自定义样式:允许覆盖默认样式
- 动画效果:添加切换动画
- 响应式设计:适配移动端
6.3 交互扩展
- 悬停提示:添加 Tooltip
- 右键菜单:添加右键菜单功能
- 键盘导航:支持键盘快捷键
- 触摸手势:支持移动端手势
七、总结
ElCalendar 是一个功能完善的日历组件,由两个子组件组成。通过学习这个组件,我们掌握了:
- 日期处理:日期格式化、计算、验证
- 组件通信:Provide/Inject、Props/Events
- 计算属性:复杂计算、Getter/Setter
- 渲染函数:JSX 语法、条件渲染
- 国际化:Mixin 混入、动态翻译
这个组件的设计思路和实现方式展示了 Vue 组件开发的最佳实践,特别是:
- 使用 Provide/Inject 实现跨层级通信
- 通过计算属性实现双向绑定
- 使用渲染函数处理复杂的 DOM 结构
- 注重日期计算和验证