Vant组件库封装可翻页日历组件

前言

我们在进行VUE开发的时候有的时候会使用到VantUI组件库:

https://vant-contrib.gitee.io/vant/v2/#/zh-CN/home#jie-shao

Vant 是一个轻量、可靠的移动端组件库,于 2017 年开源。

目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本,并由社区团队维护 React 版本和支付宝小程序版本。

但是近期在使用Vant组件库(下方所有Vant组件库均指代Vant2.x)中的日历时发现了一个bug:

BUG描述

需求是获取到当前日历的展示月份,然后使用按钮对日历进行增加或者减少操作,从而使得视图能跳转到增加或减小到的月份,如这样:

但是在调用官方api来获取当前进入视图的月份的信息的时候发现了一个致命问题:

就上图而言,可以看出,当用户首次打开日历(注意,是首次)的时候,往下滑动,确实会调用官方api,month-show进行传参显示(打印在控制台里面),但是回滑的时候却并不会触发month-show这个api。就比如用户自上而下从2月滑到了5月,其中2、3、4、5月份的信息都会调用month-show这个api,但是从5月往回滑却不会触发month-show这个api。

一句话,只有当月份首次进入视图的时候才会触发month-show这个api,非首次进入视图并不会触发month-show这个api。这显然和官方的描述是相悖的:

调用的demo的源码:

<template>
  <div class="main">
    <button @click="show = true">点击弹出Vant日历</button>
    <van-calendar v-model="show" @confirm="onConfirm" @month-show="dateShow"/>
  </div>
</template>
  
<script>
import Vue from "vue";
import { Calendar } from "vant";

Vue.use(Calendar);
export default {
  name: "demoPage",
  data() {
    return {
      date: "",
      show: false
    };
  },
  mounted() {},
  methods: {
    formatDate(date) {
      return `${date.getMonth() + 1}/${date.getDate()}`;
    },
    onConfirm(date) {
      this.show = false;
      this.date = this.formatDate(date);
    },
    dateShow(date){
      console.info(date)
    }
  }
};
</script>
  
<style scoped>
.main {
  width: 100%;
  height: 500px;
}
</style>

问题分析

到现在可以看出并不是开发者调用出了问题,而是Vant的日历组件的源码就有问题,算是个原生bug了。于是在不懈查找下,查到了vant中的日历对应month-show这个api的源码:

对应路径:\node_modules\vant\es\calendar\index.js

可以看出,在代码中的逻辑是遍历每个月份然后判断是否显隐,如果计算得知是应该展示的,则调用

this.$emit('month-show');

这个方法将满足要求的月份的信息和数据传输出去。

包括了代码的这一部分,总体上构成了对当前日历是否显隐的逻辑判断。

具体是哪里计算错误,就需求为导向而言就没必要深究了。只是知道对于Vue2.x(至少截止到2.12.54版本,bug依旧存在)而言,想修复这个bug只能对源码进行修改。

BUG解决思路

既然,使用视图判断不行,那就直接取日历的副标题,也就是subTitle,判断当其改变的时候,自动触发month-show方法实现类似于日历月份主体进入视图触发的效果。

但是从该图可以看出,日历的主体明明是6月份,但是副标题却显示的是5月份,这又是一个bug。。。不过此bug可以将传出的月份进行或增或减来校准一下。

当currentMonth(也就是subTitle对应的副标题)数据改变的时候,从month-show传出该数据。

if (currentMonth && (this.subtitle !== currentMonth.title)){
    this.subtitle = currentMonth.title;
    this.$emit('month-show', {
        date: currentMonth.date,
        title: currentMonth.title
    });
}

同时删除该部分代码:

注意: 修改node_modules源码后记得重新跑进程启动。

效果测试

可以看到修复效果良好,只不过可能得对输出的月份进行增减来进行校准。

需求实现全部代码:

calendar.vue:

<template>
  <div class="calendar">
    <div class="date" @click="handleCalendarShow">
      <span>{{displayDate[0]}}</span>
      <span>-</span>
      <span>{{displayDate[1]}}</span>
      <img :src="calIcon" alt="calendar" />
    </div>
    <van-calendar
      ref="calendar"
      :default-date="date"
      v-model="show"
      :show-subtitle="false"
      type="range"
      @confirm="onConfirm"
      @month-show="dateChange"
      :min-date="maxDateRange[0]"
      :max-date="maxDateRange[1]"
    >
      <template v-slot:title>
        <div class="timeSelect">
          <img :src="multiArrow" @click="scrollToDate('down','year')" class="left" />
          <img :src="signalArrow" @click="scrollToDate('down','month')" class="left" />
          <p>{{titleDate.title}}</p>
          <img :src="signalArrow" @click="scrollToDate('up','month')" />
          <img :src="multiArrow" @click="scrollToDate('up','year')" />
        </div>
      </template>
    </van-calendar>
  </div>
</template>
  
<script>
import Vue from "vue";
import moment from "moment";
import { Calendar, Toast } from "vant";
import { defaultDate, maxDateRange, dateFormat } from "./config";
// 日历图标
import calIcon from "./calIcon.png";
import signalArrow from "./signalArrow.png";
import multiArrow from "./multiArrow.png";

Vue.use(Calendar);
Vue.use(Toast);
export default {
  name: "demoPage",
  data() {
    return {
      // 日历副标题
      titleDate: {
        date: defaultDate[1],
        title: "2023年2月"
      },
      // 日期格式
      dateFormat,
      //默认时间范围
      defaultDate,
      //当前选择时间
      date: defaultDate,
      // 日期选择范围
      maxDateRange,
      // 日历弹窗是否展示
      show: false,
      calIcon,
      signalArrow,
      multiArrow
    };
  },
  computed: {
    //用于展示的时间
    displayDate() {
      return [
        moment(this.date[0]).format(dateFormat),
        moment(this.date[1]).format(dateFormat)
      ];
    }
  },
  mounted() {},
  methods: {
    //日历弹窗方法
    handleCalendarShow() {
      this.show = true;
      setTimeout(() => {
        //设置日历转到最新日期的展示界面
      }, 300);
      this.$refs.calendar.scrollToDate(new Date());
    },
    //选择日期触发方法
    onConfirm(date) {
      this.show = false;
      this.date = date;
    },
    //重置时间选择为默认时间
    reset() {
      this.date = this.defaultDate;
      this.$refs.calendar.reset(this.defaultDate);
    },
    //日历视图滚动时更新日历副标题数据
    dateChange(data) {
      let transDate = moment(data.date).add(1, "months");
      if (transDate <= maxDateRange[1] && transDate >= maxDateRange[0]) {
        if (moment(transDate).diff(moment(maxDateRange[0]), "months") < 1) {
          transDate = moment(data.date);
        }
        const yearNum = transDate.format("YYYY");
        const monthNum = parseInt(transDate.format("MM"));
        const newDate = {
          date: new Date(transDate.format()),
          title: yearNum + "年" + monthNum + "月"
        };
        this.titleDate = newDate;
      }
    },
    //视图滚动到指定日期的视图
    scrollToDate(type, dateType) {
      let transDate = "";
      if (type == "up") {
        transDate = moment(this.titleDate.date).add(1, dateType);
      }
      if (type == "down") {
        transDate = moment(this.titleDate.date).add(-1, dateType);
      }
      const leftDiffMonths = moment(transDate).diff(moment(maxDateRange[1]), "months");
      const rightDiffMonths = moment(maxDateRange[0]).diff(
        moment(transDate),
        "months"
      );
      // 控制翻页范围,超出就提示
      if (leftDiffMonths <= -13 || rightDiffMonths < -11) {
        Toast.fail("已超出最大可选范围");
        return null;
      }
      const yearNum = transDate.format("YYYY");
      const monthNum = parseInt(transDate.format("MM"));
      const newDate = {
        date: new Date(transDate.format()),
        title: yearNum + "年" + monthNum + "月"
      };
      this.titleDate = newDate;
      this.$refs.calendar.scrollToDate(new Date(newDate.date));
    }
  },
  watch: {
    date: {
      handler() {
        this.$emit("dateChange", this.displayDate);
      },
      immediate: true,
      deep: true
    }
  }
};
</script>
  
<style lang="less" scoped>
.calendar {
  width: 100%;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  box-sizing: border-box;
  padding: 10.4px 10.4px 10.4px 0;
  color: #989898;
  position: relative;
  .date {
    width: 100%;
    height: 28.6px;
    position: relative;
    border: 1px solid #e0e0e0;
    border-radius: 5px;
    display: flex;
    align-items: center;
    span {
      margin-left: 19.5px;
    }
    img {
      position: absolute;
      right: 5px;
      width: 23.4px;
      height: 23.4px;
    }
  }
  .timeSelect {
    width: 100%;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0 15px;
    img {
      width: 19.5px;
      height: 19.5px;
      touch-action: none;
    }
    .left {
      transform: rotateY(180deg);
    }
  }
  /deep/ .van-calendar__header-title {
    height: auto;
  }
  /deep/ .van-popup__close-icon {
    display: none;
  }
  /deep/.van__popup--bottom {
    height: 70%;
  }
}
</style>

config.js:

import moment from 'moment';
//时间格式
export const dateFormat ="YYYY-MM-DD";
//当前时间
const today = new Date();
//默认起始时间
const defaultStartDay = moment().add(-1, "years");
//时间筛选框默认时间
export const defaultDate = [new Date(defaultStartDay), today];
//日期选择限制
export const maxDateRange = [new Date(defaultStartDay), today];

父组件调用:

<template>
  <div class="main">
    <div class="calendarArea">
      <Calendar ref="calendar" @dateChange="dateChange" />
    </div>
  </div>
</template>
  
<script>
import Calendar from "./component/index.vue";

export default {
  name: "demoPage",
  components:{
    Calendar
  },
  data() {
    return {
    };
  },
  mounted() {},
  methods:{
    dateChange(dates){
      console.info(dates)
    },
    // 重置日期
    reset(){
      this.$refs.calendar.reset()
    }
  }
};
</script>
  
<style lang="less" scoped>
.main{
  width: 100%;
  height: 100%;
  .calendarArea{
    width:70%;
  }
}
</style>

最终效果:

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YOLO浪漫收藏家

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值