纯前端实现一个按月显示的月份日历效果demo,直拿!!!

发布于:2024-04-30 ⋅ 阅读:(29) ⋅ 点赞:(0)

截止目前为止,已经画过很多次的日历了,有从后端抓取日历信息渲染的,也有前端自己画的,不同的业务场景,采取不同的方案。如果只是一个日历,我们可以直接通过前端计算日期,如果有每天的信息,我们可以按天查询或直接从后端抓取。

思路

今天我们来看一下如果纯前端如何使用element-plus+vue实现一个简单的月份日历,思路其实很简单:

  • 查询当月天数;
  • 当月一号星期几,前面补空位,星期和日期对齐;
  • 当月多少天,后面补空位,使用布局更加整齐;
  • 当前日期年月日,在日历上面加判断,设置“今天”的标识

需求

  1. 我可以手动选择年月(默认显示年月,我点击时候变成日期选择框,支持选择);
  2. 我可以在每天上面添加自定义内容;
  3. 年月支持左右切换;

实现

这么看来,思路和需求都是比较简单的,那我们就需要考虑一下,在实现过程中,代码应该怎么去写。vue比较方便的就是提供了computed属性,我们可以直接使用它来计算今天的年份今天的月份等经常变化的信息。那么为什么我们不使用watch呢?其实经常使用vue的小伙伴都知道,计算和监听的应用场景是不同的,虽然watch也能实现computed的功能,但是性能上来说,执行了很多我们不需要的操作,因此我们这里就不用watch了。

那么我们怎么获取到我们需要的数据呢?

我们先定义一个周天到周六的数组,从今天星期几开始,找到数组中对应的下标,然后在天数前面补空位,达到日期与星期的对应;查询当月多少天,循环天数,通过grid布局限制每行显示数量,然后最后一天之后,计算剩余空位数量,再填充空位,这样就可以把月份的日历画出来了。如果希望在日历的每一天显示不同的自定义信息,可在下方代码的64-65行代码进行重写,直接提供一个slot进行重绘。

样式没有深入优化,可以按照自己的需求,自行调整一下

image.png

代码

<template>
  <div class="calendar-container">
    <!-- 年月控制 -->
    <div class="time-controls">
      <el-icon
        size="28px"
        color="#3e6ade"
        class="pointer"
        @click="handleMonthChange('minus')"
      >
        <DArrowLeft />
      </el-icon>
      <div>
        <el-date-picker
          ref="dateSelectorRef"
          class="pointer"
          @blur="handleMonthSelector(false)"
          @change="handleMonthSelector(false)"
          v-if="editMonth"
          v-model="currentTime"
          type="month"
        />
        <div
          v-else
          class="date-title pointer"
          @click="handleMonthSelector(true)"
        >
          {{ `${currentYear} 年 ${currentMonth} 月` }}
        </div>
      </div>
      <el-icon
        size="28px"
        color="#3e6ade"
        class="pointer"
        @click="handleMonthChange('add')"
      >
        <DArrowRight />
      </el-icon>
    </div>
    <!-- 日历显示区域 -->
    <div class="week-row-label">
      <div>周天</div>
      <div>周一</div>
      <div>周二</div>
      <div>周三</div>
      <div>周四</div>
      <div>周五</div>
      <div>周六</div>
    </div>
    <div class="calendar-area">
      <div class="week-container">
        <div
          class="week-row"
          v-for="(week, index) in monthJson"
          :key="`week_${index}`"
        >
          <div
            class="day-container"
            :class="day.day ? 'is-day' : ''"
            v-for="(day, day_index) in week"
            :key="`day_${day_index}`"
          >
            <!-- { "year": 2024, "month": 4, "day": 9 } -->
            <div v-if="isToday(day)" class="today-day">{{ day.day }}*</div>
            <div v-else-if="day.day">{{ day.day }}</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { DArrowLeft, DArrowRight } from "@element-plus/icons-vue";
export default {
  components: { DArrowLeft, DArrowRight },
  data() {
    return {
      currentTime: new Date(), // 默认当前时间(年月)
      editMonth: false, // 默认当前时间不可选择,点击时候再支持
      monthJson: [], // 是一个二位数组,按照周存储
    };
  },
  computed: {
    currentYear() {
      return this.currentTime.getFullYear();
    },
    currentMonth() {
      return this.currentTime.getMonth() + 1;
    },
  },
  mounted() {
    this.handleCalculateMonthJSON();
  },
  methods: {
    isToday(day) {
      // currentYear currentMonth
      const today = new Date();
      return (
        today.getFullYear() === this.currentYear &&
        today.getMonth() + 1 === this.currentMonth &&
        today.getDate() === day.day
      );
    },
    // 点击事件时候,控制显示并支持选择年月
    handleMonthSelector(status) {
      if (this.editMonth === status) return;
      this.editMonth = status;
      this.$nextTick(() => {
        this.$refs.dateSelectorRef?.focus();
      });
      if (!status) {
        // 选择器失焦之后,自动补货当前时间,计算月份
        this.handleCalculateMonthJSON();
      }
    },
    // 点击控制条时候,月份增减
    handleMonthChange(type) {
      if (type === "add") {
        const days = new Date(this.currentYear, this.currentMonth, 0).getDate(); // 获取当前年月有多少天
        const mockDate = `${this.currentYear}-${this.currentMonth}-${days} 23:59:59`;
        this.currentTime = new Date(new Date(mockDate) - -1000 * 60 * 60 * 24); // 加法会变成对象连接秒数字符串
      } else {
        const mockDate = `${this.currentYear}-${this.currentMonth}-1 00:00:00`;
        this.currentTime = new Date(new Date(mockDate) - 1000 * 60 * 60 * 24);
      }
      this.handleCalculateMonthJSON();
    },
    // 计算当前月份的日期天数和格式化json串
    handleCalculateMonthJSON() {
      this.monthJson = [];
      let monthDays = [];
      const weekDays = ["周天", "周一", "周二", "周三", "周四", "周五", "周六"];
      const dayCount = new Date(
        this.currentYear,
        this.currentMonth,
        0
      ).getDate(); // 当月共计多少天
      for (let i = 1; i <= dayCount; i++) {
        // 按照周json保存至monthJson的二维数组中(年月日,早中晚班类型,存储的数据)
        const date = `${this.currentYear}-${this.currentMonth}-${i} 00:00:00`;
        const day = weekDays[new Date(date).getDay() || 0]; // 当前日期周几
        // 计算一号周几,前面填充空白站位
        if (i === 1) {
          let notFund = true;
          weekDays.forEach((o) => {
            if (o !== day && notFund) {
              monthDays.push({}); // 空白站位
            } else if (o === day) {
              notFund = false; // 避免后面的日期重复添加站位
            }
          });
        }
        // 将当月的日期填入
        monthDays.push({
          year: this.currentYear,
          month: this.currentMonth,
          day: i,
          dayType: day,
          infos: {},
        });
      }
      // 格式化渲染的数据格式
      let weekDay = [];
      monthDays.forEach((o, i) => {
        if (i % 7 === 0) weekDay = [];
        weekDay.push(o);
        if (weekDay.length === 7 || i === monthDays.length - 1) {
          this.monthJson.push(weekDay);
        }
      });
      while (this.monthJson.length < 6) {
        this.monthJson.push([]);
      }
    },
  },
};
</script>

<style scoped lang="less">
.pointer {
  cursor: pointer;
}

.calendar-container {
  width: 100%;
  height: 100%;
  .time-controls {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 32px;
    height: 42px;

    .date-title {
      font-size: 24px;
    }
  }
  .week-row-label {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    align-content: space-between;
    justify-content: space-between;
    column-gap: 1px;
    row-gap: 8px;
    width: 100%;
    height: 48px;
    line-height: 48px;
    // background-color: #3e6ade;
    color: #fff;
    text-align: center;
    div {
      background-color: #3e6ade;
    }
  }

  .calendar-area {
    width: 100%;
    height: calc(100% - 132px);
    position: relative;
    display: flex;
    justify-content: space-between;
    margin-top: 12px;

    .week-container {
      width: 100%;
      height: 100%;
      display: grid;
      row-gap: 6px;
    }

    .week-row {
      display: grid;
      grid-template-columns: repeat(7, 1fr);
      grid-template-rows: 1fr;
      align-content: space-between;
      justify-content: space-between;
      row-gap: 6px;
      column-gap: 8px;
      width: 100%;

      .is-day {
        background-color: rgb(255, 251, 230);
      }

      .day-container {
        margin: 0;
        width: "100%";
        height: "100%";
        border-radius: 6px;
        overflow: hidden;
        div {
          width: 100%;
          height: 100%;
          display: flex;
          justify-content: center;
          align-items: center;
        }
        .today-day {
          background-color: #b9dcfc;
        }
      }
    }
  }
}
</style>