如何使用 Vue 实现日历组件?

1. 简介

随着移动端的普及以及网站的多样化需求,日历组件也越来越常见。Vue 作为当下较为流行的前端框架之一,其组件化的特性使得我们可以非常方便地实现一个自定义的日历组件,以满足各种业务需求。

2. 实现思路

我们将通过以下几个步骤来实现日历组件:

实现日历组件的基本 UI

绑定数据,实现日期的选择

实现切换月份的功能

实现日期的标记和特殊样式

3. 实现基本 UI

我们先来实现一个简单的日历组件。首先,我们需要定义一个组件的模板,包含日历的整体结构和基本样式。

// 日历组件的模板

<template>

<div class="calendar">

<div class="calendar-header">

<div class="calendar-prev"></div>

<div class="calendar-title"></div>

<div class="calendar-next"></div>

</div>

<div class="calendar-body">

<table>

<thead>

<tr>

<th>日</th>

<th>一</th>

<th>二</th>

<th>三</th>

<th>四</th>

<th>五</th>

<th>六</th>

</tr>

</thead>

<tbody>

<tr v-for="week in weeks" :key="week">

<td v-for="day in week" :key="day.date">{{ day.date }}</td>

</tr>

</tbody>

</table>

</div>

</div>

</template>

// 日历组件的样式

<style>

.calendar {

width: 300px;

border: 1px solid #ccc;

background-color: #fff;

font-size: 14px;

}

.calendar-header {

display: flex;

justify-content: space-between;

align-items: center;

padding: 10px;

}

.calendar-title {

flex: 1;

text-align: center;

}

.calendar-prev,

.calendar-next {

width: 20px;

height: 20px;

border: 1px solid #ccc;

cursor: pointer;

}

.calendar-body {

padding: 10px;

}

.calendar-body table {

width: 100%;

border-collapse: collapse;

}

.calendar-body td {

text-align: center;

height: 30px;

line-height: 30px;

}

.calendar-body td.today {

background-color: #eee;

}

.calendar-body td.selected {

background-color: #f00;

color: #fff;

}

</style>

代码中的日历组件模板包含了一个显示月份和年份的标题栏,一个表格用于显示日期,以及左右两个切换月份的箭头。我们给每个日期定义了一个唯一的 date 属性,以便后续绑定数据和实现选择日期的功能。同时,我们也为今天和已选择的日期定义了特殊样式,方便用户阅读和操作。

4. 绑定数据,实现日期的选择

4.1 数据绑定

接下来,我们需要将日期的数据动态地绑定到组件上。我们可以通过计算属性来实现。

// 日历组件的计算属性

export default {

name: 'Calendar',

data () {

return {

currentDate: new Date() // 当前日期

}

},

computed: {

// 计算当前月份的第一天

currentMonthFirstDay () {

const year = this.currentDate.getFullYear()

const month = this.currentDate.getMonth()

return new Date(year, month, 1)

},

// 计算当前月份的最后一天

currentMonthLastDay () {

const year = this.currentDate.getFullYear()

const month = this.currentDate.getMonth()

const lastDate = new Date(year, month + 1, 0)

return lastDate.getDate()

},

// 计算上一个月份的最后一天

prevMonthLastDay () {

const year = this.currentDate.getFullYear()

const month = this.currentDate.getMonth()

const lastDate = new Date(year, month, 0)

return lastDate.getDate()

},

// 计算当前展示的所有日期

weeks () {

const weeks = []

let start = 1

let end = 7 - this.currentMonthFirstDay.getDay()

// 添加上一个月份的日期

if (this.currentMonthFirstDay.getDay() !== 0) {

const prevMonth = this.currentDate.getMonth() === 0 ? 12 : this.currentDate.getMonth()

const prevMonthYear = this.currentDate.getMonth() === 0 ? this.currentDate.getFullYear() - 1 : this.currentDate.getFullYear()

const prevMonthLastDay = this.prevMonthLastDay

const prevMonthStart = prevMonthLastDay - this.currentMonthFirstDay.getDay() + 1

for (let i = prevMonthStart; i <= prevMonthLastDay; i++) {

weeks.push({ date: i, month: prevMonth, year: prevMonthYear, isCurrent: false })

}

}

// 添加当前月份的日期

for (let i = start; i <= this.currentMonthLastDay; i++) {

weeks.push({ date: i, month: this.currentDate.getMonth() + 1, year: this.currentDate.getFullYear(), isCurrent: true })

}

// 添加下一个月份的日期

for (let i = 1; i <= end; i++) {

weeks.push({ date: i, month: this.currentDate.getMonth() + 2, year: this.currentDate.getFullYear(), isCurrent: false })

}

return weeks

}

}

}

在计算属性中,我们使用了几个核心的时间函数来计算当前展示的日期。对于每个日期,我们都记录了其所在的月份和年份,以及是否是当前月份的日期(isCurrent 属性)。

4.2 实现日期选择

接下来,我们需要实现用户选择日期的功能。我们可以为日历表格中的每个单元格绑定点击事件,当用户点击一个日期时,我们将其记录为已选择的日期,并更新界面。

// 日历组件的方法

export default {

name: 'Calendar',

data () {

return {

currentDate: new Date(), // 当前日期

selectedDate: null // 已选择的日期

}

},

methods: {

// 选择日期的回调函数

selectDate (date) {

if (date.isCurrent) {

this.selectedDate = date

}

}

}

}

代码中的 selectDate 方法用于处理用户选择日期的操作。在选择日期之后,我们需要为已选择的日期添加特殊的样式,方便用户查看已经选择了哪个日期。

5. 实现切换月份的功能

接下来,我们需要实现左右箭头可以切换月份的功能。当用户点击箭头时,我们需要将当前日期加上或减去一个月,然后重新计算展示的日期,并更新界面。

// 日历组件的方法

export default {

name: 'Calendar',

data () {

return {

currentDate: new Date(), // 当前日期

selectedDate: null // 已选择的日期

}

},

methods: {

// 上一个月份的回调函数

prevMonth () {

const year = this.currentDate.getFullYear()

const month = this.currentDate.getMonth() - 1

this.currentDate = new Date(year, month, 1)

},

// 下一个月份的回调函数

nextMonth () {

const year = this.currentDate.getFullYear()

const month = this.currentDate.getMonth() + 1

this.currentDate = new Date(year, month, 1)

},

// 选择日期的回调函数

selectDate (date) {

if (date.isCurrent) {

this.selectedDate = date

}

}

}

}

代码中的 prevMonth 和 nextMonth 方法用于处理左右箭头的点击事件。当用户点击左箭头时,我们将当前日期减去一个月,并重新计算展示的日期;当用户点击右箭头时,我们将当前日期加上一个月,并重新计算展示的日期。我们还可以在标题栏的标题中显示当前展示的年份和月份,方便用户阅读。

6. 实现日期的标记和特殊样式

最后,我们需要实现为日期添加标记和特殊样式的功能。这在实际业务中非常常见,比如将用户的生日用特殊的标记标注出来,或者将节假日的日期用红色加粗字体标记出来。

我们可以添加一个 dates 数组,用于记录需要特殊标记的日期。然后,在渲染单元格时,我们可以根据当前日期是否被标记,在单元格上添加特定的类名或样式。

// 日历组件的模板

<template>

<div class="calendar">

<div class="calendar-header">

<div class="calendar-prev" @click="prevMonth"></div>

<div class="calendar-title">

{{ currentDate.getFullYear() }} 年 {{ currentDate.getMonth() + 1 }} 月

</div>

<div class="calendar-next" @click="nextMonth"></div>

</div>

<div class="calendar-body">

<table>

<thead>

<tr>

<th>日</th>

<th>一</th>

<th>二</th>

<th>三</th>

<th>四</th>

<th>五</th>

<th>六</th>

</tr>

</thead>

<tbody>

<tr v-for="week in weeks" :key="week">

<td v-for="day in week" :key="day.date"

:class="{ today: day.isToday, selected: day.isSelected, marked: day.isMarked }"

@click="selectDate(day)">

{{ day.date }}

<span v-if="day.isMarked" class="dot"></span>

</td>

</tr>

</tbody>

</table>

</div>

</div>

</template>

// 日历组件的样式

<style>

.calendar {

width: 300px;

border: 1px solid #ccc;

background-color: #fff;

font-size: 14px;

}

.calendar-header {

display: flex;

justify-content: space-between;

align-items: center;

padding: 10px;

}

.calendar-title {

flex: 1;

text-align: center;

}

.calendar-prev,

.calendar-next {

width: 20px;

height: 20px;

border: 1px solid #ccc;

cursor: pointer;

}

.calendar-body {

padding: 10px;

}

.calendar-body table {

width: 100%;

border-collapse: collapse;

}

.calendar-body td {

text-align: center;

height: 30px;

line-height: 30px;

}

.calendar-body td.today {

background-color: #eee;

}

.calendar-body td.selected {

background-color: #f00;

color: #fff;

}

.calendar-body td.marked {

position: relative;

color: #f00;

}

.calendar-body td.marked .dot {

position: absolute;

top: 50%;

left: 50%;

display: inline-block;

width: 6px;

height: 6px;

border-radius: 50%;

background-color: #f00;

transform: translate(-50%, -50%);

}

</style>

// 日历组件的计算属性

export default {

name: 'Calendar',

data () {

return {

currentDate: new Date(), // 当前日期

selectedDate: null, // 已选择的日期

dates: ['2022-02-01', '2022-02-05', '2022-02-10'] // 待标记的日期

}

},

computed: {

// 计算当前月份的第一天

currentMonthFirstDay () {

const year = this.currentDate.getFullYear()

const month = this.currentDate.getMonth()

return new Date(year, month, 1)

},

// 计算当前月份的最后一天

currentMonthLastDay () {

const year = this.currentDate.getFullYear()

const month = this.currentDate.getMonth()

const lastDate = new Date(year, month + 1, 0)

return lastDate.getDate()

},

// 计算上一个月份的最后一天

prevMonthLastDay () {

const year = this.currentDate.getFullYear()

const month = this.currentDate.getMonth()

const lastDate = new Date(year, month, 0)

return lastDate.getDate()

},

// 计算展示的日期数组

weeks () {

const weeks = []

let start = 1

let end = 7 - this.currentMonthFirstDay.getDay()

// 添加上一个月份的日期

if (this.currentMonthFirstDay.getDay() !== 0) {

const prevMonth = this.currentDate.getMonth() === 0 ? 12 : this.currentDate.getMonth()

const prevMonthYear = this.currentDate.getMonth() === 0 ? this.currentDate.getFullYear() - 1 : this.currentDate.getFullYear()

const prevMonthLastDay = this.prevMonthLastDay

const prevMonthStart = prevMonthLastDay - this.currentMonthFirstDay.getDay() + 1

for (let i = prevMonthStart; i <= prevMonthLastDay; i++) {

const date = `${prevMonthYear}-${prevMonth.toString().padStart(2, '0')}-${i.toString().padStart(2, '0')}`

weeks.push({ date: i, month: prevMonth, year: prevMonthYear, isCurrent: false, isToday: false, isSelected: false, isMarked: this.dates.includes(date) })

}

}

// 添加当前月份的日期

for (let i = start; i <= this.currentMonthLastDay; i++) {

const date = `${this.currentDate.getFullYear()}-${(this.currentDate.getMonth() + 1).toString().padStart(2, '0')}-${i.toString().padStart(2, '0')}`

weeks.push({ date: i, month: this.currentDate.getMonth() + 1, year: this.currentDate.getFullYear(), isCurrent: true, isToday: this.isToday(date), isSelected: this.isSelected(date), isMarked: this.dates.includes(date) })

}