记录-Vue移动端日历设计与实现

news2025/1/15 17:22:54

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

工作中遇到一个需求是根据日历查看某一天/某一周/某一月的睡眠报告,但是找了好多日历组件都不是很符合需求,只好自己手写一个日历组件,顺便记录一下。

先看看UI给的设计图和,需求是有数据的日期做标记,可以查看某一周/某一月的数据,周数据不用自定义,就按照日历上的周数据截取.

实现效果

1.规划dom部分区块划分

2.页面实现

选择月份和选择年份与日期做了条件渲染,切换方式是点击顶部时间切换选项

<template>
  <div class="calendar">
    <div class="date-top">
      <div class="left" @click="dateOperate('down')">
        <div></div>
      </div>
      <div class="time" @click="selectDate">{{ date.join("/") }}</div>
      <div class="right" @click="dateOperate('up')">
        <div></div>
      </div>
    </div>
    <!-- 日期列表 -->
    <div class="date-list" v-if="show === 'date'">
      <div class="date-content">
        <!-- 日历头 -->
        <div v-for="item in header" :key="item">
          {{ item }}
        </div>
        <!-- 日列表 -->
        <div
          v-for="(s, k) in dayList"
          :class="[
            'date-item',
            s.month !== date[1] ? 'other-day' : '',
            s.day === date[2] && s.month === date[1] ? 'today' : '',
          ]"
          :key="s + '-' + k"
          @click="selectDay(s)"
        >
          {{ s.day }}
          <div
            :class="[
              'check',
              haveList.includes(`${s.year}-${s.month}-${s.day}`) ? 'have' : '',
            ]"
          ></div>
        </div>
      </div>
      <!-- 操作栏 -->
      <div class="date-btn">
        <div
          class="btn-item"
          v-for="k in weeks + 1"
          :key="k"
          @click="weekReport(k)"
        >
          {{ k === 1 ? "" : "看周报" }}
        </div>
      </div>
    </div>
    <!-- 月份列表 -->
    <div class="month-list" v-else-if="show === 'month'">
      <div
        :class="date[1] == i ? 'month-item active' : 'month-item'"
        v-for="i in 12"
        :key="i"
        @click="selectMonth(i)"
      >
        {{ i }}月
      </div>
    </div>
    <!-- 年份列表 -->
    <div
      class="year-list"
      v-else
      @touchmove="touchMove"
      @touchstart="touchStart"
    >
      <div
        :class="date[0] === i ? 'month-item active' : 'month-item'"
        v-for="i in yearList"
        :key="i"
        @click="selectYear(i)"
      >
        {{ i }}
      </div>
    </div>
    <!-- 底部操作栏 -->
    <div class="date-bottom">
      <div class="b-left">
        <div class="tab"></div>
        <div class="totip">代表有睡眠报告</div>
      </div>
      <div class="b-right">
        <div class="cancel" @click="cancel">取消</div>
        <div class="m-report" @click="changeReport">看月报</div>
      </div>
    </div>
  </div>
</template>

css部分

<style lang="scss" scoped>
.calendar {
  width: 100%;
  background-color: #fff;

  .date-top {
    width: 100%;
    padding: 20px;
    display: flex;
    justify-content: space-around;
    align-items: center;
    .left,
    .right {
      width: 100px;
      height: 100%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      div {
        width: 20px;
        height: 20px;
        background-color: #00b7ae;
      }
    }
    .left > div {
      clip-path: polygon(0% 50%, 100% 0%, 100% 100%);
    }

    .right > div {
      clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
    }

    .time {
      font-size: 38px;
      font-weight: 500;
      color: #333333;
    }
  }
  .date-list,
  .year-list,
  .month-list {
    width: 100%;
    padding: 30px;
    height: 540px;
  }
  .month-list,
  .year-list {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-rows: auto;
    .month-item {
      text-align: center;
      display: flex;
      justify-content: center;
      align-items: center;
      font-size: 30px;
      height: 122px;
    }
    .month-item:active {
      background-color: #eee;
    }
    .active {
      background-color: #dcf4f3;
    }
  }
  .date-list {
    padding-top: 0;
    display: flex;
    .date-content {
      flex: 1;
      height: 100%;
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
      grid-template-rows: auto;
      grid-gap: 20px 20px;
      div {
        height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        border-radius: 10px;
      }
      .other-day {
        color: rgba($color: #363636, $alpha: 0.6) !important;
      }
      .today {
        background-color: #dcf4f3;
      }
      .date-item {
        font-size: 28px;
        font-weight: 400;
        color: #363636;
        .check {
          width: 10px;
          height: 10px;
          margin-top: 6px;
          border-radius: 50%;
          background-color: #00b7ae;
          opacity: 0;
        }
        .have {
          opacity: 1;
        }
      }
    }
    .date-btn {
      height: 100%;
      width: 80px;
      font-size: 22px;
      color: #4eb9f5;
      display: grid;
      grid-template-columns: 1fr;
      grid-template-rows: auto;
      grid-gap: 20px 20px;
      margin-left: 20px;
      .btn-item {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100%;
      }
    }
  }

  .date-bottom {
    width: calc(100% - 80px);
    display: flex;
    justify-content: space-between;
    align-content: center;
    padding: 20px;
    margin: 0 auto;
    border-top: 1px solid #eee;
    .b-left,
    .b-right {
      display: flex;
      align-items: center;
    }
    .b-left {
      .tab {
        width: 27px;
        height: 26px;
        background: #dcf4f3;
        border-radius: 13px;
      }
      .totip {
        font-size: 24px;
        font-weight: 400;
        color: #363636;
        margin-left: 20px;
      }
    }
    .b-right {
      .cancel {
        font-size: 26px;
        font-weight: 500;
        color: rgba($color: #000000, $alpha: 0.5);
      }
      .m-report {
        width: 195px;
        line-height: 70px;
        color: #fff;
        font-size: 26px;
        background: linear-gradient(196deg, #50dcdc, #18b6b7);
        border-radius: 20px;
        margin-left: 50px;
        text-align: center;
      }
    }
  }
}
</style>

3.接下来是逻辑处理部分

日数据的显示一共42条数据,先获取当前月的总天数,将每个月的天数保存在一个数组里,然后根据传入的参数返回相应的天数, 因为有闰年的存在,2月会是29天,所以做了闰年的判断.然后获取每周的第一天是周几,使用new Date().getDay()获取某一天是周几,返回的是0-7,这里为了方便使用将日历表头用数组保存起来返回的数字刚好是日里头对应的下标,然后根据第一天是周几计算出需要补上个月的几天数据,通过new Date(y,m,0)可以获取到上个月最后一天的,然后向日数据中添加上个月最后几天的数据,补充下个月开始的几天数据,直接使用42减去当月的天数和补充的上个月的天数得到的就是需要补充的下月天数.

月数据的切换显示,前后翻动切换年数据,每年固定都是12月所以就直接写固定值12然后v-for遍历生成dom

年数据的切换显示,每页显示12条数据,保存每页数据的第一条和最后一条用于前后翻页计算显示的数据+12或者-12.

校验选择的月份和已选择的日期是否匹配,因为选择日期后再切换月份有可能切换到的月份没有选择的日期如31日30日29日,所以需要验证是否正确,若是没有的话就当前月的最后一天.

手势操作没有写完整,只写了年份选择的滑动事件逻辑.

为了方便js部分的代码每行都有写详细的注释

自定月选择日期范围只需要修改日期点击事件的逻辑,新增一个参数判断是单日期选择还是选择一个日期范围,在事件处理里面记录点击的两个日期并计算中间的日期保存返回.

<script>
import { formatTime } from "@/utils/format";
export default {
  name: "calendar",
  props: {
    haveList: {
      type: Array,
      default: [],
    },
  },
  data() {
    return {
      // 切换日期选择
      show: "date",
      // 日历头
      header: ["日", "一", "二", "三", "四", "五", "六"],
      // 选择日期
      date: [],
      // 年列表
      yearList: [],
      // 天列表
      dayList: [],
      // 定时器
      timer: null,
      // 手势操作数据
      move: {
        pageX: 0,
        fNum: null,
        lNum: null,
      },
      // 第一天是周几
      weeks: 0,
    };
  },
  created() {},
  mounted() {
    let time = new Date();
    this.date.push(
      time.getFullYear(),
      formatTime(time.getMonth() + 1),
      formatTime(time.getDate())
    );
    this.countDay();
  },
  methods: {
    // 计算显示的天数据
    countDay() {
      console.log("chufa");
      let [y, m, d] = this.date;
      // 获取第一天是周几
      let week = new Date(`${y}/${m}/1`).getDay(),
        // 获取当前月的上个月多少天
        lastDays = this.getDays(y, m - 1),
        // 获取这个月有多少天
        days = this.getDays(y, m);
      // 计算这个月有多少周
      this.weeks = Math.ceil((days - (7 - week)) / 7) + 1;
      // 将当前月份的天数生成数组
      this.dayList = Array.from({ length: this.getDays(y, m) }, (v, k) => {
        return {
          day: formatTime(k + 1),
          month: m,
          year: y,
        };
      });
      // 将本月1日前的数据补齐
      for (let i = lastDays; i > lastDays - week; i--) {
        this.dayList.unshift({
          day: i,
          // 如果当前日期是1月补齐的是去年12月的数据
          month: +m - 1 === 0 ? 12 : formatTime(+m - 1),
          year: +m - 1 === 0 ? y - 1 : y,
        });
      }
      // 计算需要补齐多少天
      let length = this.weeks * 7 - this.dayList.length;
      console.log("length", week, lastDays, days, this.weeks);
      // 将本月最后一天的数据补齐
      for (let i = 1; i <= length; i++) {
        this.dayList.push({
          day: i,
          // 如果当前日期是12月补齐的是明年年1月的数据
          month: +m + 1 > 12 ? 1 : formatTime(+m + 1),
          year: +m + 1 > 12 ? y + 1 : y,
        });
      }

      console.log(this.dayList);
    },
    // 顶部时间点击事件
    selectDate() {
      let type = {
        month: "year",
        date: "month",
      };
      // 判断点击事件选择月份还是年份
      if (this.show !== "year") {
        this.show = type[this.show];
      }
      // 如果是月份就计算dateList数据
      if (this.show === "month") {
        // 清空每页显示的年份数据
        this.yearList.length = 0;
        // 计算页面显示的年份数据 每页显示12条数据
        for (let i = this.date[0] - 4; i <= this.date[0] + 7; i++) {
          this.yearList.push(i);
        }
      }
    },
    // 屏幕点击事件
    touchStart(val) {
      // 获取按下屏幕的x轴坐标
      this.move.pageX = val.touches[0].pageX;
    },
    // 左右滑动切换事件
    touchMove(val) {
      // 获取按下屏幕移动结束的x轴坐标
      let move = val.touches[0].pageX;
      clearTimeout(this.timer);
      // 判断往左滑动还是往右滑动
      // 滑动结束x轴坐标减去最初按下坐标为负数就是往左滑动,翻看当前日期以后的年份
      if (move - this.move.pageX < -20) {
        console.log("右滑", this.move.lNum);
        // 定时器防抖
        this.timer = setTimeout(this.changeYear("right"), 100);
      }
      // 滑动结束x轴坐标减去最初按下坐标为正数就是往右滑动,翻看当前日期以前的年份
      if (move - this.move.pageX > 20) {
        // 定时器防抖
        this.timer = setTimeout(this.changeYear("left"), 100);
      }
    },
    // 年份选择切换
    changeYear(type) {
      // 清空每页显示的年份数据
      this.yearList.length = 0;
      if (type === "right") {
        // 计算页面显示的年份数据 每页显示12条数据
        for (let i = this.move.lNum + 1; i < this.move.lNum + 13; i++) {
          this.yearList.push(i);
        }
      } else {
        for (let i = this.move.fNum - 12; i < this.move.fNum; i++) {
          this.yearList.push(i);
        }
      }
    },
    // 年份点击事件
    selectYear(val) {
      this.date[0] = val;
      this.show = "month";
    },
    // 月份点击事件
    selectMonth(val) {
      this.date[1] = val;
      this.show = "date";
      this.countDay();
      this.checkDay();
    },
    // 校验选择的月份和已选择的日期是否匹配
    checkDay() {
      // 获取选择的年月有多少天 防止这年不是闰年 就将日期跳转到28号,或者有的月份没有31号就跳到30号
      let num = this.getDays(this.date[0], this.date[1]);
      if (num < this.date[2]) {
        this.date.splice(2, 1, num);
      }
    },
    // 日期点击事件
    selectDay(val) {
      let oVal = this.date[1];
      this.date.splice(1, 2, val.month, val.day);
      if (val.month !== oVal) {
        this.countDay();
      }
      this.$emit("change", this.date.join("-"));
    },
    // 获取某个月有多少天
    getDays(year, month) {
      // 一年中每个月的天数
      let days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
      // 判断是不是闰年 2月29天
      if (year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)) {
        days[1] = 29;
      }
      return days[month - 1];
    },
    //左右按钮点击事件
    dateOperate(type) {
      let [y, m, d] = this.date;
      // 如果是向后翻
      if (type === "up") {
        // 日期向后翻 切换月份
        if (this.show === "date") {
          if (+m === 12) {
            this.date.splice(0, 1, y + 1);
            this.date.splice(1, 1, "01");
          } else {
            this.date.splice(1, 1, formatTime(+m + 1));
          }
          // 月份向后翻 切换年份
        } else if (this.show === "month") {
          this.date.splice(0, 1, y + 1);
          // 年份向后翻 重组数据
        } else {
          this.changeYear("right");
        }

        // 如果是前后翻
      } else {
        // 日期向前翻 切换月份
        if (this.show === "date") {
          if (+m === 1) {
            this.date.splice(0, 1, y - 1);
            this.date.splice(1, 1, 12);
          } else {
            this.date.splice(1, 1, formatTime(+m - 1));
          }
          // 月份向前翻 切换年份
        } else if (this.show === "month") {
          this.date.splice(0, 1, y - 1);
          // 年份向前翻 重组数据
        } else {
          this.changeYear("left");
        }
      }
      this.countDay();
      this.checkDay();
    },
    // 右侧按钮点击事件
    weekReport(i) {
      if (i === 1) return;
      let arr = [],
        // 选择一周的数据 开始
        s = 7 * (i - 1) - 7,
        // 结束
        e = 7 * (i - 1);
      // 遍历日数据 截取选择的周数据
      for (let k = s; k < e; k++) {
        arr.push(
          `${this.dayList[k].year}-${this.dayList[k].month}-${this.dayList[k].day}`
        );
      }
      this.$emit("weekReport", arr);
    },

    // 看月报事件
    changeReport() {
      let [y, m, d] = this.date;
      this.$emit("changeReport", `${y}-${m}`);
    },
    // 取消事件
    cancel() {
      this.$emit("cancel");
    },
  },
  computed: {},
  watch: {
    yearList(nVal, oVal) {
      // 记录每一页显示的数据第一位和最后一位 用于计算下一页或者上一页的数据
      this.move.fNum = nVal[0];
      this.move.lNum = nVal[11];
    },
    deep: true,
    immediate: true,
  },
};
</script>

formatTime是给月份和日期小于10的前面加0的方法

本文转载于:

https://juejin.cn/post/7218048201981853757

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/467960.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux文件编辑与编辑命令

文章目录 一、linux文件编辑与编辑命令总结 一、linux文件编辑与编辑命令 Linux mkdir命令:创建目录 Linux more命令:显示文本文件内容 Linux cat命令:连接文件并打印到标准输出设备上 Linux grep命令:检索文件内容 Linux rm命令:删除文件或目录 Linux touch命令:修改文件的时…

15款时间计划、任务管理APP/软件对比(团队\个人)

15 款不同类型的日程、任务管理应用&#xff1a;1.PingCode&#xff1b;2.Worktile&#xff1b;3.Todoist&#xff1b;4.Trello&#xff1b;5.Microsoft To Do&#xff1b;6.Asana&#xff1b;7.Google 任务&#xff1b;8.Notion&#xff1b;9.Monday.com&#xff1b;10.Teambi…

Matlab对日期变量和时间变量的管理

Matlab2012a内置了三个函数 datanumdatevecdatestr 靠这三个函数&#xff0c;可以基本实现日期变量和时间变量的管理。下面直接来看。 &#xff08;1&#xff09;datanum 这个函数用来将字符串&#xff0c;日期矢量转为通用日&#xff08;数值型&#xff09;。所谓的通用日…

js特殊对象 - RegExp对象(正则表达式)

1、概述 正则表达式用于定义一些字符串的规则&#xff0c;计算机可以根据正则表达式&#xff0c;来检查一个字符串是否符合规则&#xff0c;获取将字符串中符合规则的内容提取出来。 使用typeof检查正则对象&#xff0c;会返回object。 2、创建正则对象 2.1、使用对象创建 语法…

Java spring 注解 @PostConstruct 实战讲解

前言 在最近的学习中&#xff0c;发现了一个非常实用的注解 —— PostConstruct。通过学习了解&#xff0c;逐步发现它能帮助我更轻松的解决不少原本很复杂的问题。 下面&#xff0c;结合实例介绍 PostConstruct 注解的特性&#xff0c;因为PreDestroy基本用不到&#xff0c;所…

C++算法初级11——01背包问题(动态规划2)

C算法初级11——01背包问题&#xff08;动态规划2&#xff09; 文章目录 C算法初级11——01背包问题&#xff08;动态规划2&#xff09;问题引入0-1背包问题分析0-1背包问题的形式化分析优化 问题引入 辰辰采药 辰辰是个天资聪颖的孩子&#xff0c;他的梦想是成为世界上最伟大…

Ubuntu开机自启动一些东西

有三种方式做开机自启动 目录 1.免除sudo密码 2.Startup 2.desktop 3.service 1.免除sudo密码 做完这一步你的所有sudo命令都不会再让你输密码了 如果你的开机自启动的东西需要sudo&#xff0c;那么这一步就是必须的&#xff0c;如果不需要sudo&#xff0c;那么这一步可…

Linux安装kubectl

前言 以下所有命令基于CentOS7.9系统&#xff0c;官方参考文档&#xff1a;> 文章最后附有一键安装的脚本&#xff0c;可以直接运行脚本进行安装 下载安装文件 1. 下载最新发行版 curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/st…

C嘎嘎~~[类 中篇]

类 中篇 6.类的实例化7.类对象模型8.this指针8.1this指针是什么8.2this指针的特性 6.类的实例化 什么叫类的 实例化?? 首先, 我们应该关注这个"实" — 实际存在的, 它的反义词是 “虚” — 不存在的. > 类中的成员变量是虚的(相当于声明), 在类外面创建的对象是…

《程序员面试金典(第6版)》面试题 16.06. 最小差(双指针,pair数据结构)

题目描述 给定两个整数数组a和b&#xff0c;计算具有最小差绝对值的一对数值&#xff08;每个数组中取一个值&#xff09;&#xff0c;并返回该对数值的差 示例&#xff1a; 输入&#xff1a;{1, 3, 15, 11, 2}, {23, 127, 235, 19, 8}输出&#xff1a;3&#xff0c;即数值对(…

Power BI动态日期轴方法总结

趋势&#xff0c;应该是我们做可视化时最熟悉的一个词了&#xff0c;看趋势自然离不开日期&#xff0c;年度趋势&#xff0c;月趋势&#xff0c;周趋势等等。Power BI中我们可以借助于计算表&#xff0c;计算组&#xff0c;字段参数来实现动态实时轴的效果。 计算表实现动态日…

Node.js--基础

一、Node.js是什么 Node.js is a JavaScript runtime built on Chrome’s V8 JavaScript engine. 1、特性 Node.js 可以解析JS代码&#xff08;没有浏览器安全级别的限制&#xff09;提供很多系统级别的API&#xff0c;如&#xff1a; 文件的读写 (File System)进程的管理 …

每日一个小技巧:1分钟告诉你文字转图片的方法有哪些

在数字时代&#xff0c;信息传递快速便捷&#xff0c;但文字在传递中却显得单调乏味&#xff0c;难以吸引人们的眼球。为了解决这个问题&#xff0c;越来越多的人开始寻找方法将文字转化为图片。文字转图片不仅能够让文字更具视觉冲击力&#xff0c;还能够在社交媒体、广告宣传…

Nginx常见应用场景

文章目录 场景一&#xff1a;代理静态文件场景二&#xff1a;代理服务器 本教程讲述 Nginx 的常见应用场景。内容接上文&#xff1a;Nginx基本配置。 前提&#xff1a;假设我们已经安装好了 Nginx&#xff0c;并启动成功。 场景一&#xff1a;代理静态文件 Nginx 一个常用的功…

Hilt 和协程助力启动框架搭建:解决代码混乱和初始化策略问题

关于Hilt的使用&#xff0c;目前已经比较普及了&#xff0c;想必大家已经知道。今天说的是一个如何利用Hilt来做一个启动框架的故事。 是否经历过大型项目的启动优化&#xff0c;一遍过去无任何效果&#xff0c;第二遍过去好几处报错&#xff0c;第三遍过去启动不了&#xff0…

开放耳机有什么优缺点,列举出几款口碑不错的开放式耳机

开放式耳机是通过骨头振动传递声音&#xff0c;而不是通过耳道或鼓膜&#xff0c;因此它具有许多优势&#xff0c;比如可以在运动过程中保持对环境的感知&#xff0c;并避免对听力造成伤害。随着科技的进步和用户需求的增加&#xff0c;开放式耳机也在不断更新。目前市面上的开…

springboot+nodejs+vue众筹项目管理系统平台系统

筹资人&#xff08;企业&#xff09;&#xff1a; 1&#xff0c;可以后台注册并登录&#xff0c;发布项目情况&#xff0c;众筹项目需要管理员审核通过后才能展现在前台&#xff0c;没有审核或者审核不通过不会在前台展示&#xff1b; 众筹项目包括项目名称&#xff0c;项目分类…

盲目自学网络安全只会成为脚本小子?

前言&#xff1a;我们来看看怎么学才不会成为脚本小子 一&#xff0c;怎么入门&#xff1f; 1、Web 安全相关概念&#xff08;2 周&#xff09; 了解网络安全相关法律法规 熟悉基本概念&#xff08;SQL 注入、上传、XSS、CSRF、一句话木马等&#xff09;。 通过关键字&…

springboot整合flowable的简单使用

内容来自网络整理&#xff0c;文章最下有引用地址&#xff0c;可跳转至相关资源页面。若有侵权请联系删除 环境&#xff1a; mysql5.7.2 springboot 2.3.9.RELEASE flowable 6.7.2 采坑&#xff1a; 1.当前flowable sql需要与引用的pom依赖一致&#xff0c;否则会报library…

管理后台项目-07-菜单管理和动态展示菜单和按钮

目录 1-菜单管理 1.1-菜单管理列表 1.2-添加|修改功能 1.3-删除菜单 2-动态菜单按钮展示 2.1-路由文件的整理 2.2-动态展示不同的路由 1-菜单管理 当用户点击菜单管理的时候&#xff0c;会展示当前所有菜单&#xff0c;树型结构展示...并且可以对菜单进行新增编辑删除操…