转自公众号:前端帮
先看最终效果
前置知识
实现这个日历组件,只需要知道以下几个问题:
- 现在的年份
- 现在的月份
- 今天是几号
- 当月共有多少天
- 这个月1号是星期几
前三个问题很好理解,直接调api,就不多说了。
第四个问题有个小技巧,当Date构造函数的第三个传0的时候,表示上个月的最后一天的日期,所以我们如果想知道某个月共有多少天,就是要知道最后一天是几号。第二个参数大家都知道,月份是从0开始的,现在是5月,想知道这个月共有多少天,就是要知道6月的前一个月的最后一天的日期,所以第二个参数传5,new Date(2024, 5, 0)就表示5月31号,再调用getDate方法就得到了31。
第五个问题更简单,先得到本月1号的Date实例,再调用getDay方法拿到星期几。
开始编码
和我们手机上的日历一样,每周是把周日作为第一天,定义一个数组用于遍历周几。上面我们已经知道了一个月有多少天,也直接遍历数字。
<template>
<div class="calendar">
<div class="item" v-for="item in weeks" :key="item">{{ '周' + item }}</div>
<div
class="item"
v-for="item in days"
:key="item"
>
{{ item }}
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const weeks = ['日', '一', '二', '三', '四', '五', '六']
const now = new Date()
// 本月的总天数
const days = computed(() => {
return new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()
})
</script>
<style lang="scss">
.calendar {
width: 500px;
display: flex;
flex-wrap: wrap;
.item {
width: calc(100% / 7);
height: 80px;
}
}
</style>
很明显这个不对,因为今年的5月1号是周三。
从前置知识我们已经知道了每月1号是周几,而每周又是从周日开始的,直接在1号前面加上对应的几天占位的dom元素。
<template>
<div class="calendar">
<div class="item" v-for="item in weeks" :key="item">{{ '周' + item }}</div>
<div
class="item"
v-for="item in firstDateDay"
:key="item"
></div>
<div
class="item"
v-for="item in days"
:key="item"
>
{{ item }}
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const weeks = ['日', '一', '二', '三', '四', '五', '六']
const now = new Date()
// 本月1号是周几
const firstDateDay = computed(() => {
return new Date(now.getFullYear(), now.getMonth(), 1).getDay()
})
// 本月的总天数
const days = computed(() => {
return new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()
})
</script>
<style lang="scss">
.calendar {
width: 500px;
display: flex;
flex-wrap: wrap;
.item {
width: calc(100% / 7);
height: 80px;
}
}
</style>
这样就对了。
不过这还远远不够,还要在加个切换上下月、高亮显示今天的日期,这两个功能,这个日历组件才算是基本完善了。
切换上下月
头部显示的年月是随着切换变化的,所以用computed来计算。
<div class="header">
<div class="title">{{ nowYear }}年{{ nowMonth }}月</div>
<div class="btns">
<button @click="toPrevMonth"> < </button>
<button @click="toToday"> 今天 </button>
<button @click="toNextMonth"> > </button>
</div>
</div>
// 实时计算当前页面显示的年份
const nowYear = computed(() => {
return now.getFullYear()
})
// 实时计算当前页面显示的月份
const nowMonth = computed(() => {
return now.getMonth() + 1
})
// 切换到上个月
const toPrevMonth = () => {
}
// 切换到下个月
const toNextMonth = () => {
}
// 回到今天
const toToday = () => {
}
重点就在切换上下月,这里又有小技巧。
const toPrevMonth = () => {
now = new Date(now.getFullYear(), now.getMonth() - 1, 1)
}
const toNextMonth = () => {
now = new Date(now.getFullYear(), now.getMonth() + 1, 1)
}
-
切换月份,只需要改变new Date()的第二个参数。
-
月份-1,即切换到上个月,如果当前是1月,month就是0,再减1,就是-1,此时年份也会自动减1,自动会变成上一年的12月,month就是11了。
-
改变月份直接用
setMonth方法会有问题,例如以下情况,4月没有31号,就返回了5月1号的日期,这不是我们想要的。再例如1月和3月都有30、31号,而2月最多只有29天,就不举例了。 -
new Date()的第三个参数为什么传1,是因为每个月总天数不一样,就像以上情况,如果传直接传当前日期,当今天是某个月的31号时,切换到上个月或下个月,就不对了(七八月份除外,因为都有31号)。
-
而我们这里只需要切换月份,拿到切换后的年和月,不关心几号,所以传1就好了,毕竟每个月都有1号。
回到今天,直接赋值今天
const toToday = () => {
now = new Date()
}
这时运行才发现,我们上面的代码有问题,点击时页面并不会变化,是因为我们定义的now,不是一个响应式变量,我们点击修改的时候,computed不会感知到,也就不会重新计算,改一下代码用ref定义,now都换成now.value。
到此时代码如下:
<template>
<div class="container">
<div class="header">
<div class="title">{{ nowYear }}年{{ nowMonth }}月</div>
<div class="btns">
<button @click="toPrevMonth"> < </button>
<button @click="toToday"> 今天 </button>
<button @click="toNextMonth"> > </button>
</div>
</div>
<div class="calendar">
<div class="item" v-for="item in weeks" :key="item">{{ '周' + item }}</div>
<div
class="item"
v-for="item in firstDateDay"
:key="item"
></div>
<div
class="item"
:class="{today: realDate.year === nowYear && realDate.month === nowMonth && realDate.date === item}"
v-for="item in days"
:key="item"
>
{{ item }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const weeks = ['日', '一', '二', '三', '四', '五', '六']
const now = ref(new Date())
// 本月1号是周几
const firstDateDay = computed(() => {
return new Date(now.value.getFullYear(), now.value.getMonth(), 1).getDay()
})
// 本月的总天数
const days = computed(() => {
return new Date(now.value.getFullYear(), now.value.getMonth() + 1, 0).getDate()
})
// 今天的年月日
const realDate = {
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
date: new Date().getDate()
}
// 实时计算当前页面显示的年份
const nowYear = computed(() => {
return now.value.getFullYear()
})
// 实时计算当前页面显示的月份
const nowMonth = computed(() => {
return now.value.getMonth() + 1
})
const toPrevMonth = () => {
now.value = new Date(now.value.getFullYear(), now.value.getMonth() - 1, 1)
}
const toNextMonth = () => {
now.value = new Date(now.value.getFullYear(), now.value.getMonth() + 1, 1)
}
// 回到今天
const toToday = () => {
now.value = new Date()
}
</script>
<style lang="scss">
.container {
width: 500px;
.header {
height: 60px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
.title {
font-size: 26px;
font-weight: 600;
}
.btns {
button {
height: 26px;
border-radius: 0;
margin: 0 2px;
cursor: pointer;
border-width: 1px;
}
}
}
.calendar {
display: flex;
flex-wrap: wrap;
.item {
width: calc(100% / 7);
height: 80px;
text-align: center;
}
.today {
color: blue;
font-weight: 600;
}
}
}
</style>
高亮显示今日
高亮显示今天的日期,这个是不会随着用户的切换动作而改变的,所以直接定义写死,和实时切换后的年月日对比,如果是同一天,就加上一个类样式。
<div
class="item"
:class="{today: realDate.year === nowYear && realDate.month === nowMonth && realDate.date === item}"
v-for="item in days"
:key="item"
>
{{ item }}
</div>
// 今天的年月日,这个是一天变一次的,不会随着用户切换而变化
const realDate = {
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
date: new Date().getDate()
}
.today {
color: blue;
font-weight: 600;
}
到这里这个日历主要部分也算是基本实现了,还可以加上农历,再美化一下样式,给组件加一些props供外部修改
半夜了,写不动了。
公历和农历互转的js,网上开源的一大堆,感兴趣的自己去搜。