【无标题】

在日常 Vue 项目开发中,我们经常会遇到需要展示当月日历并支持日期选中的场景,比如预约打卡、日期筛选等。如果不需要复杂的年月切换功能,大可不必引入庞大的第三方日历库,自己实现一个轻量极简的日历组件既高效又灵活。今天就来手把手教大家实现这样一个无年月切换、支持点击选中的 Vue 日历组件。

组件效果预览

这个组件最终实现的功能如下:

  1. 自动渲染当前月份的完整日历,包含周日至周六的星期头部
  2. 自动标记当天日期,区分当月有效日期和月初空白占位
  3. 支持点击当月日期进行选中,选中状态高亮展示
  4. 选中日期变化时向外派发事件,传递完整的日期信息
  5. 样式简洁美观,支持自适应布局,适配移动端(使用 rpx 单位,兼容 uni-app/Vue 移动端项目)

组件实现思路拆解

一个日历组件的核心逻辑其实是日期的计算与格式化,再配合 Vue 的模板渲染和样式美化,就能快速成型。整体实现分为三个核心步骤:

  1. 计算当月核心日期信息(当月第一天、最后一天、总天数、第一天是星期几)
  2. 生成日历渲染所需的日期数组,包含日期状态(是否当月、是否当天、日期字符串等)
  3. 模板渲染与样式美化,绑定点击事件实现选中功能

完整代码与分步解析

下面我们就结合完整代码,一步步拆解组件的实现细节,本文以 Vue 3 为例(兼容 uni-app,样式使用 SCSS)。

第一步:模板结构搭建(template 部分)

模板部分主要分为星期头部日期主体两部分,结构清晰简洁,利用 Vue 的指令实现数据驱动渲染。

vue

复制代码
<template>
  <view class="calendar-container">
    <!-- 日历头部:星期标题 -->
    <view class="calendar-week-header">
      <view class="week-item" v-for="(week, index) in weekList" :key="index">
        {{ week }}
      </view>
    </view>

    <!-- 日历主体:日期格子 -->
    <view class="calendar-date-content">
      <view
        class="date-item"
        v-for="(date, index) in calendarDateList"
        :key="index"
        :class="{
          'empty-item': !date.isCurrentMonth, // 非当月空白占位
          'today-item': date.isToday, // 当天日期样式
          'selected-item': date.dateStr === selectedDateStr, // 选中日期样式
        }"
        @click="handleDateClick(date)" // 日期点击事件
      >
        {{ date.day || "" }}
      </view>
    </view>
  </view>
</template>
模板关键说明:
  1. 星期头部通过v-for遍历weekList数组,快速渲染 "日、一、二、三、四、五、六"
  2. 日期主体同样通过v-for遍历核心数据calendarDateList,每个元素对应一个日历格子
  3. 动态 class 绑定三个状态样式,实现不同状态的视觉区分,提升用户体验
  4. 点击事件绑定handleDateClick,并传递当前日期对象,方便后续处理
  5. 日期展示使用date.day || "",确保非当月占位格子不显示无效内容

第二步:核心逻辑实现(script 部分)

脚本部分是日历组件的灵魂,负责日期计算、数据生成和事件处理,我们按模块逐一解析。

1. 组件基础配置与数据定义

javascript

运行

复制代码
export default {
  name: "CurrentMonthCalendar",
  data() {
    return {
      weekList: ["日", "一", "二", "三", "四", "五", "六"], // 星期头部数据
      calendarDateList: [], // 日历核心渲染数组
      selectedDateStr: "", // 选中日期的格式化字符串(用于状态判断)
    };
  },
  // 组件创建后立即执行,初始化日历数据
  created() {
    this.initCurrentMonthCalendar();
  },
};
2. 核心方法:日历初始化(initCurrentMonthCalendar)

这个方法是整个组件的核心,负责计算当月所有日期信息,并生成calendarDateList数组,步骤拆解如下:

javascript

运行

复制代码
initCurrentMonthCalendar() {
  // 1. 获取当前日期的核心信息
  const now = new Date();
  const currentYear = now.getFullYear();
  const currentMonth = now.getMonth(); // 注意:JavaScript中月份是0-11
  const currentDay = now.getDate();

  // 2. 计算当月关键日期:第一天、最后一天、第一天是星期几、当月总天数
  const firstDayOfMonth = new Date(currentYear, currentMonth, 1); // 当月第一天
  const lastDayOfMonth = new Date(currentYear, currentMonth + 1, 0); // 当月最后一天
  const firstDayWeek = firstDayOfMonth.getDay(); // 当月第一天是星期几(0=周日,6=周六)
  const currentMonthDayCount = lastDayOfMonth.getDate(); // 当月总天数

  // 3. 计算日历所需的总格子数(月初空白占位 + 当月总天数)
  const actualGridCount = firstDayWeek + currentMonthDayCount;
  // 可选优化:凑满整行(避免最后一行缺列):Math.ceil((firstDayWeek + currentMonthDayCount) / 7) * 7;

  // 4. 循环生成日历渲染数组
  this.calendarDateList = [];
  for (let i = 0; i < actualGridCount; i++) {
    // 计算当前格子对应的相对日期(相对于当月第一天)
    const relativeDay = i - firstDayWeek + 1;
    let dateItem = {};

    // 5. 区分当月有效日期和月初空白占位
    if (relativeDay >= 1 && relativeDay <= currentMonthDayCount) {
      // 当月有效日期:组装完整日期信息
      const currentDate = new Date(currentYear, currentMonth, relativeDay);
      const dateStr = this.formatDateStr(currentDate);
      dateItem = {
        year: currentYear,
        month: currentMonth + 1, // 转换为1-12的月份格式,符合日常使用习惯
        day: relativeDay,
        dateStr: dateStr, // 格式化日期字符串,用于选中状态判断
        isCurrentMonth: true, // 标记为当月日期
        isToday: relativeDay === currentDay, // 标记是否为当天
      };
    } else {
      // 月初空白占位:仅保留基础结构,不显示有效内容
      dateItem = {
        year: currentYear,
        month: currentMonth + 1,
        day: "",
        dateStr: "",
        isCurrentMonth: false,
        isToday: false,
      };
    }
    this.calendarDateList.push(dateItem);
  }

  // 6. 初始化选中日期为当天
  this.selectedDateStr = this.formatDateStr(now);
}
3. 辅助方法:日期格式化与补零

javascript

运行

复制代码
// 格式化日期为"YYYY-MM-DD"格式
formatDateStr(date) {
  const year = date.getFullYear();
  const month = this.padZero(date.getMonth() + 1);
  const day = this.padZero(date.getDate());
  return `${year}-${month}-${day}`;
},

// 数字补零(确保月份、日期都是两位数格式)
padZero(num) {
  return num < 10 ? `0${num}` : `${num}`;
}
4. 事件方法:日期点击处理

javascript

运行

复制代码
handleDateClick(date) {
  // 过滤非当月日期,禁止选中空白占位
  if (!date.isCurrentMonth) return;
  // 更新选中日期状态
  this.selectedDateStr = date.dateStr;
  // 向外派发日期变化事件,传递完整日期对象,方便父组件接收
  this.$emit("date-change", date);
}
脚本关键说明:
  1. 利用 JavaScript 的Date对象精准计算当月关键日期,这是日历渲染的基础
  2. 区分 "当月有效日期" 和 "月初空白占位",确保日历布局规整且无无效内容
  3. 日期格式化统一为YYYY-MM-DD格式,避免选中状态判断出现歧义
  4. 点击事件做了非当月日期过滤,提升交互的合理性
  5. 组件初始化时默认选中当天,符合用户使用习惯
  6. 向外派发date-change事件,实现组件与父组件的通信,提升组件复用性

第三步:样式美化(style 部分)

样式使用 SCSS 编写,开启scoped确保样式隔离,不污染全局,同时兼顾美观和移动端适配。

scss

复制代码
<style scoped lang="scss">
.calendar-container {
  width: 100%;
  padding: 20rpx;
  box-sizing: border-box;
  background: #fff;
}

.calendar-week-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 20rpx;

  .week-item {
    width: 14.28%; // 1/7,确保7个元素均匀分布
    text-align: center;
    font-size: 28rpx;
    color: #666;
  }
}

.calendar-date-content {
  display: flex;
  flex-wrap: wrap; // 自动换行,实现日历网格布局

  .date-item {
    width: 14.28%;
    height: 80rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28rpx;
    color: #333;
    box-sizing: border-box;
    margin-bottom: 10rpx;

    // 非当月占位样式(透明化)
    &.empty-item {
      color: transparent;
    }

    // 当天日期样式(浅蓝背景+蓝色文字)
    &.today-item {
      background: #e6f7ff;
      color: #1890ff;
      font-weight: bold;
    }

    // 选中日期样式(深蓝背景+白色文字)
    &.selected-item {
      background: #1890ff;
      color: #fff;
      font-weight: bold;
    }
  }
}
</style>
样式关键说明:
  1. 采用flex布局实现星期头部和日期网格的均匀分布,14.28%对应 1/7,确保 7 列布局规整
  2. 日期格子使用flex居中对齐,提升视觉美观度
  3. 不同状态使用差异化的背景色和文字色,视觉区分明显,用户体验佳
  4. 使用rpx单位适配移动端不同设备屏幕,兼容性更好
  5. scoped属性确保样式仅作用于当前组件,避免样式冲突

组件使用方法

这个组件实现完成后,可直接在父组件中引入使用,步骤如下:

  1. 引入并注册组件

javascript

运行

复制代码
<script setup>
// 引入日历组件
import CurrentMonthCalendar from "../../components/CurrentMonthCalendar/CurrentMonthCalendar.vue";
// 补充组件抛出事件的处理方法
const handleCalendarDateChange = (date) => {
  console.log("选中的日期:", date);
  // 后续业务逻辑可在此扩展
};
</script>
  1. 在模板中使用并监听日期变化事件

vue

复制代码
<template>
  <view class="page-container">
    <!-- 引入日历组件 -->
    <CurrentMonthCalendar @date-change="handleCalendarDateChange" />
  </view>
</template>
  1. 父组件接收选中日期

javascript

运行

复制代码
methods: {
  handleCalendarDateChange(selectedDate) {
    console.log("选中的日期信息:", selectedDate);
    // 后续可进行预约、筛选等业务逻辑处理
  }
}

组件优化与扩展方向

这个极简日历组件满足了基础需求,我们还可以根据业务场景进行优化和扩展:

  1. 样式优化:添加圆角、阴影效果,提升视觉层次感;支持自定义主题色
  2. 功能扩展:增加年月切换按钮、支持日期范围选择、禁用指定日期
  3. 兼容性优化:适配 Vue 3,支持 Composition API;兼容 PC 端(替换 rpx 为 px/rem)
  4. 性能优化:缓存当月日期数据,避免组件重复渲染时重复计算

希望这篇文章能帮助大家理解日历组件的实现逻辑,在实际项目中快速落地相关功能!

完整源码

复制代码
<template>
  <view class="calendar-container">
    <!-- 日历头部:星期标题 -->
    <view class="calendar-week-header">
      <view class="week-item" v-for="(week, index) in weekList" :key="index">
        {{ week }}
      </view>
    </view>

    <!-- 日历主体:日期格子 -->
    <view class="calendar-date-content">
      <view
        class="date-item"
        v-for="(date, index) in calendarDateList"
        :key="index"
        :class="{
          'empty-item': !date.isCurrentMonth,
          'today-item': date.isToday,
          'selected-item': date.dateStr === selectedDateStr,
        }"
        @click="handleDateClick(date)"
      >
        {{ date.day || "" }}
      </view>
    </view>
  </view>
</template>

<script>
export default {
  name: "CurrentMonthCalendar",
  data() {
    return {
      weekList: ["日", "一", "二", "三", "四", "五", "六"],
      calendarDateList: [],
      selectedDateStr: "",
    };
  },
  // 关键:组件创建后立即执行(必触发)
  created() {
    this.initCurrentMonthCalendar();
  },
  methods: {
    initCurrentMonthCalendar() {
      const now = new Date();
      const currentYear = now.getFullYear();
      const currentMonth = now.getMonth();
      const currentDay = now.getDate();

      const firstDayOfMonth = new Date(currentYear, currentMonth, 1);
      const lastDayOfMonth = new Date(currentYear, currentMonth + 1, 0);
      const firstDayWeek = firstDayOfMonth.getDay(); // 当月第一天是星期几(0=周日)
      const currentMonthDayCount = lastDayOfMonth.getDate(); // 当月总天数

      // 关键修改1:计算实际需要的格子数(不再固定42个)
      // 公式:月初空白格子数 + 当月总天数
      // 月初空白格子数 = firstDayWeek(周日开头)
      const actualGridCount = firstDayWeek + currentMonthDayCount;
      // 可选:若想凑满整行(避免最后一行缺列),可计算到最近的7的倍数(注释掉则直接截止到最后一天)
      // const actualGridCount = Math.ceil((firstDayWeek + currentMonthDayCount) / 7) * 7;

      this.calendarDateList = [];
      for (let i = 0; i < actualGridCount; i++) {
        // 关键修改2:循环到实际格子数,而非42
        const relativeDay = i - firstDayWeek + 1;
        let dateItem = {};

        if (relativeDay >= 1 && relativeDay <= currentMonthDayCount) {
          // 当月有效日期
          const currentDate = new Date(currentYear, currentMonth, relativeDay);
          const dateStr = this.formatDateStr(currentDate);
          dateItem = {
            year: currentYear,
            month: currentMonth + 1,
            day: relativeDay,
            dateStr: dateStr,
            isCurrentMonth: true,
            isToday: relativeDay === currentDay,
          };
        } else {
          // 月初空白占位(仅保留上方,无下方占位)
          dateItem = {
            year: currentYear,
            month: currentMonth + 1,
            day: "",
            dateStr: "",
            isCurrentMonth: false,
            isToday: false,
          };
        }
        this.calendarDateList.push(dateItem);
      }
      this.selectedDateStr = this.formatDateStr(now);
    },
    formatDateStr(date) {
      const year = date.getFullYear();
      const month = this.padZero(date.getMonth() + 1);
      const day = this.padZero(date.getDate());
      return `${year}-${month}-${day}`;
    },
    padZero(num) {
      return num < 10 ? `0${num}` : `${num}`;
    },
    handleDateClick(date) {
      if (!date.isCurrentMonth) return;
      this.selectedDateStr = date.dateStr;
      this.$emit("date-change", date);
    },
  },
};
</script>

<style scoped lang="scss">
.calendar-container {
  width: 100%;
  padding: 20rpx;
  box-sizing: border-box;
  background: #fff;
}

.calendar-week-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 20rpx;

  .week-item {
    width: 14.28%;
    text-align: center;
    font-size: 28rpx;
    color: #666;
  }
}

.calendar-date-content {
  display: flex;
  flex-wrap: wrap;

  .date-item {
    width: 14.28%;
    height: 80rpx;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28rpx;
    color: #333;
    box-sizing: border-box;
    margin-bottom: 10rpx;

    &.empty-item {
      color: transparent;
    }

    &.today-item {
      background: #e6f7ff;
      color: #1890ff;
      font-weight: bold;
    }

    &.selected-item {
      background: #1890ff;
      color: #fff;
      font-weight: bold;
    }
  }
}
</style>
相关推荐
前端小L5 小时前
双指针专题(四):像毛毛虫一样伸缩——「长度最小的子数组」
javascript·算法·双指针与滑动窗口
沛沛rh455 小时前
React入门:从一个简单的Hello World开始
前端·react.js·前端框架
谢尔登5 小时前
Vue3 应用实例创建及页面渲染底层原理
javascript·vue.js·ecmascript
小笔学长5 小时前
XMLHttpRequest 对象:传统的网络请求方式
javascript·xmlhttprequest·前端开发·网络请求实战·跨域问题解决
IT_陈寒6 小时前
SpringBoot性能翻倍秘籍:5个被低估的配置项让我QPS提升200%
前端·人工智能·后端
破晓之翼6 小时前
EASDEP 自动单据生成DEMO
javascript
阿珊和她的猫6 小时前
Webpack 常用插件深度解析
前端·webpack·node.js
kylezhao20196 小时前
第三节、C# 上位机面向对象编程详解(工控硬件封装实战版)
开发语言·前端·c#
行思理6 小时前
css 样式新手教程
前端·css·html5