一个Vue可滑动calendar日历组件

先看下效果图

组件概述

日历组件将支持以下功能:

  • 显示当前月份的日历。

  • 通过滑动切换到上一个月或下一个月

  • 点击特定日期可以触发事件。

使用方式

组件中使用moment和vant的swipe组件需要安装

npm i moment vant

main.js引入下

import { Swipe, SwipeItem } from 'vant';

Vue.use(Swipe);

Vue.use(SwipeItem);

组件也发布了npm包可直接使用

npm i vue-swipe-calendar

组件结构

ini 复制代码
<template>
  <div class="calendar">
    <div class="calendar-header">
      <div>{{ getFormatDate() }}</div>
      <div>
        <span class="btn" @click="handleMonth('pre')">上</span>
        <span class="btn" @click="handleMonth('current')">今</span>
        <span class="btn" @click="handleMonth('next')">下</span>
      </div>
    </div>
    <van-swipe ref="vanSwipe" :show-indicators="false" @change="onSwipeChange">
      <van-swipe-item v-for="dateArr in dateArrs" :key="dateArr.fullDate">
        <div class="calendar-week">
          <div
            class="calendar-week-item flex-c"
            v-for="(item, index) in weekArrs"
            :key="index"
          >
            {{ item }}
          </div>
        </div>
        <div class="calendar-date">
          <div
            class="calendar-date-item flex-c"
            v-for="item in dateArr"
            :key="item.fullDate"
            :class="item.type"
            @click="handleDate(item)"
          >
            {{ item.date }}
            <slot name="calendarItem" :item="item"></slot>
          </div>
        </div>
      </van-swipe-item>
    </van-swipe>
  </div>
</template>

<script>
import moment from "moment";
export default {
  name: "SwipeCalendar",
  data() {
    return {
      weekArrs: ["一", "二", "三", "四", "五", "六", "日"],
      dateArrs: [],
      today: moment().format("YYYY-MM-DD"),
      currentMonth: "",
      allCell: 42,
      currentSwipeIndex: 0, // 当前轮播的索引
    };
  },
  components: {},
  mounted() {
    this.initCalendar("current", 0);
  },
  methods: {
    onSwipeChange(index) {
      this.dateArrs = [[], [], []];
      // 判断滑动方向
      if (
        index - this.currentSwipeIndex === 1 ||
        index - this.currentSwipeIndex === -2
      ) {
        //console.log('向左滑动++');
        this.initCalendar("next", index);
      } else {
        //console.log('向右滑动--');
        this.initCalendar("pre", index);
      }

      this.currentSwipeIndex = index;
    },
    handleMonth(type) {
      if (type == "pre") {
        this.$refs.vanSwipe.prev();
      } else if (type == "next") {
        this.$refs.vanSwipe.next();
      } else {
        this.initCalendar("current");
      }
    },
    initCalendar(type, index) {
      if (type == "pre") {
        this.currentMonth = moment(this.currentMonth)
          .subtract(1, "month")
          .startOf("month");
      } else if (type == "next") {
        this.currentMonth = this.currentMonth.add(1, "month").startOf("month");
      } else {
        this.currentMonth = moment(this.today).startOf("month");
      }
      //获取当前上个月的第一天
      const preMonthFirstDay = this.currentMonth
        .clone()
        .subtract(1, "month")
        .startOf("month");
      //获取当前下个月的第一天
      const nextMonthFirstDay = this.currentMonth
        .clone()
        .add(1, "month")
        .startOf("month");
      const currentDateArrs = this.getMonthDateArrs(this.currentMonth.clone());
      const nextDateArrs = this.getMonthDateArrs(nextMonthFirstDay);
      const preDateArrs = this.getMonthDateArrs(preMonthFirstDay);
      if (index === 0) {
        // 初始化第0张图 当前月在第0张 月视图数据顺序为120(1为当前月)
        this.dateArrs = [currentDateArrs, nextDateArrs, preDateArrs];
      } else if (index === 1) {
        // 划到第1张图 当前月在第1张 月视图数据顺序为012(1为当前月)
        this.dateArrs = [preDateArrs, currentDateArrs, nextDateArrs];
      } else if (index === 2) {
        //划到第2张图 当前月在第2张 月视图数据顺序为201(1为当前月)
        this.dateArrs = [nextDateArrs, preDateArrs, currentDateArrs];
      }
    },
    getFormatDate() {
      if (this.currentMonth) {
        return this.currentMonth.format(`YYYY年M月`);
      } else {
        return moment(this.today).format(`YYYY年M月`);
      }
    },
    getMonthDateArrs(firstDay) {
      // 获取当前月一共多少天(占格子)
      const currentCell = this.getMonthDates(firstDay);
      // 获取当前月之前多少天(占格子)
      const preCell = this.getPreMonthDates(firstDay);
      // 获取当前月之后多少天(占格子)
      const nextCell = this.getNextMonthDates(firstDay, currentCell, preCell);
      return [...preCell, ...currentCell, ...nextCell];
    },
    getDateObj(day, diff, type) {
      const currentDate = moment(day).subtract(diff, "days");
      const fullDate = currentDate.format("YYYY-MM-DD"); // 完整日期格式
      const date = currentDate.date(); // 日期部分
      return { fullDate, date, type };
    },
    getMonthDates(firstDay) {
      // 计算当前月份补充的天数
      let daysInMonth = moment(firstDay).daysInMonth() - 1;
      const lastDay = moment(firstDay).endOf("month");
      const dates = [];
      // 遍历当前月的每一天
      while (daysInMonth >= 0) {
        const daysInfo = this.getDateObj(lastDay, daysInMonth, "current");
        dates.push(daysInfo);
        daysInMonth--; // 移动到下一天
      }
      return dates;
    },
    getPreMonthDates(firstDay) {
      //  计算前面需要补充的天数 获取当前月第一天是周几(0 表示周日,1 表示周一,...,6 表示周六)
      let preDaysInMonth = moment(firstDay).day() - 1;
      // 将周日0改为6
      if (preDaysInMonth === -1) {
        preDaysInMonth = 6;
      }
      const dates = [];
      while (preDaysInMonth > 0) {
        const daysInfo = this.getDateObj(firstDay, preDaysInMonth, "pre");
        dates.push(daysInfo);
        preDaysInMonth--;
      }
      return dates;
    },
    getNextMonthDates(firstDay, currentCell, preCell) {
      //计算当前月份之后补充的天数
      let nextDaysInMonth = this.allCell - currentCell.length - preCell.length;
      const lastDay = firstDay.endOf("month");
      const cellLastDay = lastDay.add(nextDaysInMonth, "days");
      const dates = [];
      while (nextDaysInMonth > 0) {
        const daysInfo = this.getDateObj(
          cellLastDay,
          nextDaysInMonth - 1,
          "next"
        );
        dates.push(daysInfo);
        nextDaysInMonth--;
      }
      return dates;
    },
    handleDate(item) {
      this.$emit("handleDate", item);
    },
  },
};
</script>
  <style lang="less" scoped>
  .calendar {
    padding: 20px;

    .flex-c {
      display: flex;
      justify-content: center;
      align-items: center;
    }

    &-header {
      padding: 0 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;

      .btn {
        margin-left: 10px;
      }
    }

    &-week {
      display: flex;

      &-item {
        flex: 1;
        height: 30px;
      }
    }

    /deep/.van-swipe {
      overflow: hidden;

      .van-swipe__track {
        display: flex;
      }
    }

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

      .calendar-date-item {
        width: calc(100% / 7);
        height: 40px;
        color: #333;

        &.pre,
        &.next {
          color: #999;
        }
      }
    }
  }
  </style>
相关推荐
风止何安啊4 分钟前
🚦 前端并发请求 “交通管制”:别让你的接口堵成早高峰
前端·javascript·面试
J船长8 分钟前
扫盲烂笔头:A 记录、CNAME 的作用, Nginx的用途,为啥叫反向代理
前端
妙码生花14 分钟前
从 PHP 到 AI + Golang,程序员自救转型手记(二十六):icon 组件封装
前端·vue.js·typescript
李明卫杭州14 分钟前
Vue2 & Vue3 中 Router `props` 配置详解:彻底摆脱 `$route` 依赖
前端·vue.js·前端框架
掘金一周15 分钟前
裁员了,公司很爽快,赔偿一分没少 | 沸点周刊 7.8
前端·人工智能·后端
熊猫钓鱼>_>19 分钟前
2026 前端的三场底层革命:当 Vue、React Native 和 Three.js 同时去掉中间层
前端·javascript·vue.js·react native·架构·webgl·three.js
名字还没想好☜29 分钟前
React useEffect 依赖数组踩坑:闭包陷阱与清理函数
前端·javascript·react.js·react·useeffect
宸翰1 小时前
uni-app 设置 Android 底部虚拟导航栏背景色
android·前端·uni-app
金色的暴发户2 小时前
一劳永逸解决Codex手机验证
前端·人工智能·开源
DanCheOo2 小时前
AI 应用的可观测性:你的 AI 系统在生产上到底表现怎么样
前端·openai