手写一个vue3日历组件

1,622 阅读5分钟

转自公众号:前端帮

原文链接:mp.weixin.qq.com/s/N_nDmoH1R…

先看最终效果

前置知识

实现这个日历组件,只需要知道以下几个问题:

  1. 现在的年份
  2. 现在的月份
  3. 今天是几号
  4. 当月共有多少天
  5. 这个月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元素。 image

<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>

image 这样就对了。

不过这还远远不够,还要在加个切换上下月高亮显示今天的日期,这两个功能,这个日历组件才算是基本完善了。 image

切换上下月

头部显示的年月是随着切换变化的,所以用computed来计算。

<div class="header">
  <div class="title">{{ nowYear }}年{{ nowMonth }}月</div>
  <div class="btns">
    <button @click="toPrevMonth"> &lt; </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"> &lt; </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,网上开源的一大堆,感兴趣的自己去搜。