vue手写日历组件

  • 需求场景
  • 简要逻辑分析 (一堆废话 不想看直接跳到代码部分)
  • 存放日期的表格/二维数组
  • 月份年份选择
  • 代码呈现


需求场景

阅读此文章默认读者已经了解vue基本语法/指令

在一些查询类的表单中,经常会有选择日期/时间一类的需求,参考ElementUI/ElementPlus如下图所示的日历控件,以此为模板,在vue3的框架下手写类似的迷你组件用于满足查询类表单对于日期选择的需求。

vue日志监控 vue 日志组件_elementui

一小段废话:
也许会有人会问为什么要重复造轮子,这无疑是一个问题,一是所在公司的业务要求(可以不使用第三方库就不使用,考虑到商业版权/项目大小/兼容以及二般情况下等等),二来我不是一个喜欢伸手就来的人,二话不说就拉个框架来用固然很爽爽爽但是失去了很多思考的过程,学习成本小收益也小。刚刚进到公司的时候,单单是写页面就很懊恼许多组件什么的都没有现成的要自己一个一个写,到后来写了几个慢慢习惯了,反而开始享受这个过程,从中获益良多。所以我更注重的是造轮子的过程,怎么样顺畅地去造轮子或者说造轮子过程整个思考的逻辑是如何跑通的,不是为了造轮子而造轮子。

以上内容仅限于使用UI框架上,因为还没有学习了解得非常深入,所以构建打包项目等等工具还是得用现成的哈哈哈哈!

简要逻辑分析 (一堆废话 不想看直接跳到代码部分)

存放日期的表格/二维数组

首先是具体日期的选择,随意翻看日历,可以发现如下规律:有的月份需要 7x6 的表格去进行摆放,而有的月份只需要 7x5 的表格即可,所以我们需要一个至多为 7x6 的二维数组作为存储每个月具体日期的数据格式
dayArr: [[], [], [], [], [], []], 接下来我们只要知道每个月的一号是星期X,然后从数组[0][X]的位置开始遍历28/29/30/31天,即可形成每个月对应日期的表格。随意翻看一个日历可知,每个月1号的是星期几都是不固定的,但是我们可以通过JavaScript中Date()内置对象获取即可:
new Date().getMonth()方法获取的月份是从0开始的,例如new Date("2022/01/01").getMonth()返回的是 0 月,因此如果需要获取对应month的信息则需要输入month-1 ,即
new Date(year, month - 1).getDay() 然后是数组的遍历次数,对应每个月的天数,而每个月的天数可能不一样,有的月份三十天、有的月份三十一天、还有二十八天和二十九天的,这时候就需要对month进行三次判断,一次是判断年份(闰年二月有29天其余28天),一次是判断月份是否为2月 ,还有一次判断该月是三十天或者三十一天。

//		以下仅为简单逻辑,非正式运行代码
//		首先判断月份是否为二月
if(month == 2){
	//		判断年份是否为闰年
	if(year / 4 ){
		maxday = 28;
		return;
	}
	else {
	maxday = 29;
	return
	}
}
//		判断月份为三十天或者三十一天
if(month == 4 || month == 6 || month == 9 || month == 11 ){
	maxday = 30
}
else maxday = 31

来点高级写法( Array.includes() + &&运算符 + 三目运算符 ):
let maxday = [4, 6, 9, 11].includes(month) ? 30 : 31;month == 2 && (maxday = year % 4 ? 28 : 29); 两行代码即可解决上述的三次判断!具体意义或者单独用法可以另外查一下

月份年份选择

接下来是月份与年份的选择,这里的难度相比于上述编写对应月份的日期表格就算是小菜一碟了;
对于月份和年份的选择,根据部分实例,我打算以 按钮+下拉框 的形式进行构建,按钮中的逻辑比较简单直接选择上一个月或者下一个月(月末或者月初情况下需要改变年份和月份),而下拉框中则是可以选择年份以及一年之中任意十二个月
此处逻辑较为简单此处则不做单独展示,统一在最后展示。
由于对应业务的需求,我的日历控件是需要可以选取两个日期,即起始日期以及截止日期,那么此处也应进行对应的逻辑判断
完成以上的逻辑分析,基本上一个简单的日历控件就完成了。

代码呈现

由于业务需求以及对功能逻辑的思考比较深入,忽略了对html结构的优化,导致html代码比较冗余。对于下拉框这个小组件,已经进行了另外的封装,后续再分享出来!

同时该日历组件只满足基本功能,后续实际细节逻辑功能有待补全

HTML部分

<template>
  <div>
    <!-- 展示框:起始yy-mm-dd / 截止yy-mm-dd -->
    <div class="date-div" @click.stop="is_display = !is_display">
      {{
        DisplayDate0.year +
        "-" +
        (DisplayDate0.month < 10
          ? "0" + DisplayDate0.month
          : DisplayDate0.month) +
        "-" +
        DisplayDate0.day
      }}
      -
      {{
        DisplayDate1.year +
        "-" +
        (DisplayDate1.month < 10
          ? "0" + DisplayDate1.month
          : DisplayDate1.month) +
        "-" +
        DisplayDate1.day
      }}
    </div>
    <!-- 组件框 -->
    <div
      id="date-container"
      class="date-container"
      v-show="is_display"
      @click.stop
    >
      <div class="calendar-container">
        <div class="calendar">
          <!-- 按钮+标题栏+下拉框:年份月份的选择 -->
          <div class="yy-mm-chose">
            <span @click="changeMonth('left', 'dec')"> < </span>
            <!--下拉框模块 -->
            <section class="select-container">
              <!-- 内容:年份 -->
              <div
                class="selection-header"
                @click.stop="is_Year0 = !is_Year0"
                :style="is_Year0 ? 'background-color: #d2ecfd;' : ''"
              >
                {{ Date0.year }}
              </div>
              <!-- 列表:年份 -->
              <ul :class="is_Year0 ? '' : 'selection-hidden'">
                <li
                  v-for="item in selection_year"
                  :key="item"
                  @click="chooseYY_MM(item, 'is_Year0')"
                >
                  <div>
                    <span>{{ item }}</span>
                  </div>
                </li>
              </ul>
            </section>
            <!-- 下拉框模块 -->
            <section class="select-container">
              <!-- 内容:月份 -->
              <div
                class="selection-header"
                @click.stop="is_Month0 = !is_Month0"
                :style="is_Month0 ? 'background-color: #d2ecfd;' : ''"
              >
                {{ Date0.month < 10 ? "0" + Date0.month : Date0.month }}
              </div>
              <!-- 列表:月份 -->
              <ul :class="is_Month0 ? '' : 'selection-hidden'">
                <li
                  v-for="item in 12"
                  :key="item"
                  @click="chooseYY_MM(item, 'is_Month0')"
                >
                  <div>
                    <span>{{ item < 10 ? "0" + item : item }}</span>
                  </div>
                </li>
              </ul>
            </section>
            <span @click="changeMonth('left', 'inc')">">></span>
          </div>
          <!-- 具体日期选择 -->
          <table>
            <tbody>
              <tr>
                <th v-for="it in week_day">
                  {{ it }}
                </th>
              </tr>
              <tr v-for="it in day_arr0">
                <td
                  @click="getDay(item, 'from')"
                  v-for="item in it"
                  :class="Date0.day == item ? 'chose' : ''"
                  v-show="item != ''"
                >
                  {{ item }}
                </td>
              </tr>
            </tbody>
          </table>
        </div>
        <div class="calendar">
          <!-- 按钮+标题栏+下拉框:年份月份的选择 -->
          <div class="yy-mm-chose">
            <span @click="changeMonth('right', 'dec')"><</span>
            <!-- 下拉框模块 -->
            <section class="select-container">
              <!-- 内容:年份 -->
              <div
                class="selection-header"
                @click.stop="is_Year1 = !is_Year1"
                :style="is_Year1 ? 'background-color: #d2ecfd;' : ''"
              >
                {{ Date1.year }}
              </div>
              <!-- 列表:年份 -->
              <ul :class="is_Year1 ? '' : 'selection-hidden'">
                <li
                  v-for="item in selection_year"
                  :key="item"
                  @click="chooseYY_MM(item, 'is_Year1')"
                >
                  <div>
                    <span>{{ item }}</span>
                  </div>
                </li>
              </ul>
            </section>
            <!--  下拉框模块 -->
            <section class="select-container">
              <!-- 内容:月份 -->
              <div
                class="selection-header"
                @click.stop="is_Month1 = !is_Month1"
                :style="is_Month1 ? 'background-color: #d2ecfd;' : ''"
              >
                {{ Date1.month < 10 ? "0" + Date1.month : Date1.month }}
              </div>
              <!-- 列表:月份 -->
              <ul :class="is_Month1 ? '' : 'selection-hidden'">
                <li
                  v-for="item in 12"
                  :key="item"
                  @click="chooseYY_MM(item, 'is_Month1')"
                >
                  <div>
                    <span>{{ item < 10 ? "0" + item : item }}</span>
                  </div>
                </li>
              </ul>
            </section>
            <span @click="changeMonth('right', 'inc')">">></span>
          </div>
          <!-- 具体日期选择 -->
          <table>
            <tbody>
              <tr>
                <th v-for="it in week_day">
                  {{ it }}
                </th>
              </tr>
              <tr v-for="it in day_arr1">
                <td
                  @click="getDay(item, 'to')"
                  v-for="item in it"
                  :class="Date1.day == item ? 'chose' : ''"
                >
                  {{ item }}
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
      <!-- 提交表单 -->
      <div class="submit-Btn">
        <input type="button" value="确认" @click="commit" />
        <input type="button" value="取消" @click="cancel" />
      </div>
    </div>
  </div>
</template>

Css部分

::-webkit-scrollbar {
  display: none;
}
//    具体日期展示框
.date-div {
  cursor: pointer;
  line-height: 28px;
  font-size: 14px;
  padding: 0 10px;
  height: 28px;
  width: 200px;
  color: #ffffff;
  background-color: #99d6ff;
}
//    日历组件盒子
.date-container {
  position: relative;
  z-index: 100;
  width: 320px;
  padding: 10px;
  box-shadow: 1px 1px 8px rgb(0 0 0 / 20%);
  background-color: #fff;
  //    日历容器
  .calendar-container {
    display: flex;
    justify-content: space-around;
    // padding-top: 0.8rem;
    text-align: center;
    //    年份月份选择_下拉框
    .yy-mm-chose {
      width: 100%;
      display: flex;
      justify-content: space-between;
      > span {
        cursor: pointer;
        line-height: 18px;
        padding: 0 3px;
        font-weight: 1000;
        color: #fff;
        background: #108cee;
      }
      .select-container {
        cursor: pointer;
        height: 18px;
        color: #108cee;
        background-color: #eaf6fe;

        .selection-header {
          line-height: 18px;
          padding: 0 5px;
        }

        ul {
          position: absolute;
          background-color: #fff;
          // width: 100%;
          height: 50px;
          overflow-y: scroll;
          box-shadow: 2px 5px 8px rgb(0 0 0 / 20%);
          transition: all 0.3s;

          li {
            line-height: 18px;
            color: #333;
            padding: 0 5px;
            cursor: pointer;
          }

          li:hover {
            color: #108cee;
            background-color: #eaf6fe;
          }
        }

        .selection-hidden {
          opacity: 0;
          height: 0;
        }
      }
    }
    //    日历
    .calendar {
      font-size: 12px;
      display: flex;
      flex-direction: column;
      align-items: center;

      table {
        tr {
          * {
            padding: 2px;
          }
          th {
            color: #999;
          }
        }
      }
      table tr {
        line-height: 16px;
        td:hover {
          cursor: pointer;
          color: #108cee;
          background-color: #eaf6fe;
        }
        td.chose {
          color: #fff;
          background: #108cee;
        }
      }
    }
  }
  //    按钮
  .submit-Btn {
    margin-left: 180px;
    input {
      line-height: 20px;
      font-size: 12px;
      margin-left: 0.5rem;
      padding: 0 0.3rem;
      color: #fff;
      background-color: #108cee;
      border-radius: 2px;
    }
  }
}

JavaScript部分

<script>
import { reactive, toRefs, watch } from "vue";
export default {
  setup(props, content) {
    const state = reactive({
      //    变量名称0表示起始部分 / 变量名称0表示截止部分
      //    组件展示框中起始日期
      DisplayDate0: { year: 2021, month: 3, day: "12" },
      //    组件展示框中截止日期
      DisplayDate1: { year: 2022, month: 6, day: "21" },
      //    组件中选择的起始年份月份日期
      Date0: { year: 2021, month: 3, day: "12" },
      //    组件中选择的截止年份月份日期表单
      Date1: { year: 2022, month: 6, day: "21" },
      //    日历组件容器的激活状态
      is_display: false,
      //    日历组件中下拉框激活状态:起始年份、月份,截止年份、月份
      is_Year0: false,
      is_Month0: false,
      is_Year1: false,
      is_Month1: false,
      //    下拉框可选列表
      selection_year: [2018, 2019, 2020, 2021, 2022],
      //    日历表格的表头
      week_day: ["一", "二", "三", "四", "五", "六", "日"],
      //    用于模板中渲染日历表格的二维数组
      day_arr0: [[], [], [], [], [], []],
      day_arr1: [[], [], [], [], [], []],
    });
    //    从日历表格中获取具体日期
    //    当日期天数小于10时候,展示为 01/02 模板
    const getDay = (value, type) => {
      if (value == undefined) return;
      type == "from" && (state.Date0.day = value < 10 ? "0" + value : value);
      type == "to" && (state.Date1.day = value < 10 ? "0" + value : value);
    };
    //    下拉框选择年份&&月份
    //    value为下拉框选中的值,position为起/止 年份/月份的判断
    const chooseYY_MM = (value, position) => {
      //    type用于当多个下拉框同时激活时,区分对应赋值
      position == "is_Year0" && (state.Date0.year = value);
      position == "is_Month0" && (state.Date0.month = value);
      position == "is_Year1" && (state.Date1.year = value);
      position == "is_Month1" && (state.Date1.month = value);
    };
    //    单击按钮改变月份:添加或者减少
    const changeMonth = (position, type) => {
      //    position == "left" 为起始日期
      if (position == "left") {
        //    起始月份减少
        if (type == "dec") {
          //    当月份为一月时候,年份进行减一
          if (state.Date0.month == 1) {
            --state.Date0.year;
            state.Date0.month = 12;
            return;
          }
          //    起始月份逐一减少
          state.Date0.month > 1 && --state.Date0.month;
        }
        //    起始月份增加
        if (type == "inc") {
          //    当月份为十二月时候,年份进行加一
          if (state.Date0.month == 12) {
            ++state.Date0.year;
            state.Date0.month = 1;
            return;
          }
          //    起始月份逐一增加
          state.Date0.month < 12 && ++state.Date0.month;
        }
      }

      //    position == "right" 为截止日期
      if (position == "right") {
        if (type == "dec") {
          if (state.Date1.month == 1) {
            --state.Date1.year;
            state.Date1.month = 12;
            return;
          }
          state.Date1.month > 1 && --state.Date1.month;
        }
        if (type == "inc") {
          if (state.Date1.month == 12) {
            ++state.Date1.year;
            state.Date1.month = 1;
            return;
          }
          state.Date1.month < 12 && ++state.Date1.month;
        }
      }
    };
    //    计算对应月份的天数形成日历数组
    const cal_calendar_arr = (year, month) => {
      //    渲染日历表格的起点
      let day = 1;
      //    渲染日历表格终点:需要判断当前月份的天数
      let max_day = [4, 6, 9, 11].includes(month) ? 30 : 31;
      //    内置对象new Date()获取当前月份第一天为星期几
      let op = new Date(year, month - 1).getDay();
      //    日历数组对象
      let dayArr = [[], [], [], [], [], []];
      //    内置对象new Date().getDay()获取星期日的值为0
      op = op == 0 ? 7 : op;
      //    闰年二月天数的判断
      month == 2 && (max_day = year % 4 ? 28 : 29);
      //    渲染日期到日历数组对应的星期几上
      for (let i = 0; i < 6; i++) {
        for (let j = i == 0 ? op - 1 : 0; j < 7; j++) {
          dayArr[i][j] = day;
          day++;
          if (day > max_day) return dayArr;
        }
      }
    };
    //    监听&&初始化 起始yy-mm的日期
    watch(
      [() => state.Date0.year, () => state.Date0.month],
      () => {
        state.day_arr0 = cal_calendar_arr(state.Date0.year, state.Date0.month);
      },
      { immediate: true }
    );
    //    监听&&初始化 截止yy-mm的日期
    watch(
      [() => state.Date1.year, () => state.Date1.month],
      () => {
        state.day_arr1 = cal_calendar_arr(state.Date1.year, state.Date1.month);
      },
      { immediate: true }
    );
    //    取消按钮
    const cancel = () => {
      //    还原原本的日期
      state.Date0 = { ...state.DisplayDate0 };
      state.Date1 = { ...state.DisplayDate1 };
      state.is_display = false;
    };
    //    返回日期选择错误提示
    const errorTip = () => {
      alert("请选择正确的年份月份");
      cancel();
      state.is_display = true;
    };
    //    确认按钮
    const commit = () => {
      const yearF = state.Date0.year,
        monthF = state.Date0.month,
        dayF = state.Date0.day,
        yearT = state.Date1.year,
        monthT = state.Date1.month,
        dayT = state.Date1.day;
      //    判断起始年份月份 <= 截止年份月份
      if (
        yearF > yearT ||
        (yearF == yearT && monthF > monthT) ||
        (yearF == yearT && monthF == monthT && dayF > dayT)
      ) {
        errorTip();
        return;
      }
      //    获取改变的日期
      state.DisplayDate0 = { ...state.Date0 };
      state.DisplayDate1 = { ...state.Date1 };
      //    将确定的日期传给父组件
      content.emit("dateEmit", {
        Date0: state.DisplayDate0,
        Date1: state.DisplayDate1,
      });
      state.is_display = false;
    };

    //    下拉框激活时 监听页面点击事件在对应时候关闭下拉框
    const year_monthListener = () => {
      state.is_Month0 && (state.is_Month0 = false);
      state.is_Month1 && (state.is_Month1 = false);
      state.is_Year0 && (state.is_Year0 = false);
      state.is_Year1 && (state.is_Year1 = false);
    };
    watch(
      [
        () => state.is_Month0,
        () => state.is_Month1,
        () => state.is_Year0,
        () => state.is_Year1,
      ],
      (val) => {
        let targetDom = document.getElementById("date-container");
        val.includes(true) &&
          targetDom.addEventListener("click", year_monthListener, {
            once: true,
          });
      }
    );
    //    监听日历选择框的展开/关闭事件
    watch(
      () => state.is_display,
      () => {
        state.is_display &&
          window.addEventListener(
            "click",
            () => {
              state.is_display = false;
            },
            { once: true }
          );
        //    更新日历组件中下拉框的状态
        year_monthListener();
      }
    );
    return {
      ...toRefs(state),
      chooseYY_MM,
      getDay,
      changeMonth,
      commit,
      cancel,
    };
  },
};
</script>
<style scoped src="datepicker.less"></style>