前言
我们在进行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>
最终效果: