当前位置:

vue 编写时间选择器

访客 2024-04-23 787 0

最近研究了DatePicker的实现原理后做了一个vue的DatePicker组件,今天带大家一步一步实现DatePicker的vue组件。

原理

DatePicker的原理是——计算日历面板中当月或选中月份的总天数及前后月份相近的日子,根据点击事件计算日历面板显示内容,以及将所选值赋值给<input/>标签。

实现
  • CSS代码于文章末尾处
1.构思页面结构

DatePicker组件由输入框和日历面板组成,写好页面主体结构。

<divclass="date-picker"><inputclass="input"v-model="dateValue"@click="openPanel"/><transitionname="fadeDownBig"><divclass="date-panel"v-show="panelState"></div></transiton></div>

输入框<input>点击显示或隐藏日历面板,openPanel()方法改变panelState布尔值控制日历面板的显示隐藏。

日历面板由顶部条和面板两部分组成,而面板则由年份选择面板,月份选择面板,日期选择面板所组成,结构如下:

<divclass="date-panel"v-show="panelState"><!--顶部按钮及年月显示条--><divclass="topbar"><span@click="leftBig">&lt;&lt;</span><span@click="left">&lt;</span><spanclass="year"@click="panelType='year'">{{tmpYear}}</span><spanclass="month"@click="panelType='month'">{{changeTmpMonth}}</span><span@click="right">&gt;</span><span@click="rightBig">&gt;&gt;</span></div><!--年面板--><divclass="type-year"v-show="panelType==='year'"><ulclass="year-list"><liv-for="(item,index)inyearList":key="index"@click="selectYear(item)"><span:class="{selected:item===tmpYear}">{{item}}</span></li></ul></div><!--月面板--><divclass="type-year"v-show="panelType==='month'"><ulclass="year-list"><liv-for="(item,index)inmonthList":key="index"@click="selectMonth(item)"><span:class="{selected:item.value===tmpMonth}">{{item.label}}</span></li></ul></div><!--日期面板--><divclass="date-group"v-show="panelType==='date'"><spanv-for="(item,index)inweekList":key="index"class="weekday">{{item.label}}</span><ulclass="date-list"><liv-for="(item,index)indateList"v-text="item.value":class="{preMonth:item.previousMonth,nextMonth:item.nextMonth,selected:date===item.value&&month===tmpMonth&&item.currentMonth,invalid:validateDate(item)}":key="index"@click="selectDate(item)"></li></ul></div></div>2.页面数据实现

DatePicker所对应的data代码

data(){return{dateValue:"",//输入框显示日期date:newDate().getDate(),//当前日期panelState:false,//初始值,默认panel关闭tmpMonth:newDate().getMonth(),//临时月份,可修改month:newDate().getMonth(),tmpYear:newDate().getFullYear(),//临时年份,可修改weekList:[{label:"Sun",value:0},{label:"Mon",value:1},{label:"Tue",value:2},{label:"Wed",value:3},{label:"Thu",value:4},{label:"Fri",value:5},{label:"Sat",value:6}],//周monthList:[{label:"Jan",value:0},{label:"Feb",value:1},{label:"Mar",value:2},{label:"Apr",value:3},{label:"May",value:4},{label:"Jun",value:5},{label:"Jul",value:6},{label:"Aug",value:7},{label:"Sept",value:8},{label:"Oct",value:9},{label:"Nov",value:10},{label:"Dec",value:11}],//月nowValue:0,//当前选中日期值panelType:"date"//面板状态};},

DatePicker的核心在于日期面板的数据。我们知道,一个月最多31天,最少28天。面板按周日至周六设计,最极端的情况如下:

最多的极端情况:

******1
2345678
9101112131415
16171819202122
23242526272829
303112345

最少的极端情况:

1234567
891011121314
15161718192021
22232425262728
1234567
891011121314

根据上表我们可以得知一个月最多占六个星期,最少四个星期,所以日历面板必须设计为6行,剩余的用下个月的日期补上,最多补14天。因此日期数组可以这么设计:

computed:{dateList(){//获取当月的天数letcurrentMonthLength=newDate(this.tmpYear,this.tmpMonth1,0).getDate();//先将当月的日期塞入dateListletdateList=Array.from({length:currentMonthLength},(val,index)=>{return{currentMonth:true,value:index1};});//获取当月1号的星期是为了确定在1号前需要插多少天letstartDay=newDate(this.tmpYear,this.tmpMonth,1).getDay();//确认上个月一共多少天letpreviousMongthLength=newDate(this.tmpYear,this.tmpMonth,0).getDate();//在1号前插入上个月日期for(leti=0,len=startDay;i<len;i){dateList=[{previousMonth:true,value:previousMongthLength-i}].concat(dateList);}//补全剩余位置,至少14天,则i<15for(leti=1,item=1;i<15;i,item){dateList[dateList.length]={nextMonth:true,value:i};}returndateList;},}

changeTmpMonth为选择月份后显示的文案,yearList为年份列表,为了与月份数量保持一致,我们也设长度为12.

computed:{changeTmpMonth(){returnthis.monthList[this.tmpMonth].label;},//通过改变this.tmpYear则可以改变年份数组yearList(){returnArray.from({length:12},(value,index)=>this.tmpYearindex);}}3.实现页面功能

(1)面板切换功能

  • 点击输入框,除了打开日历面板,同时也默认为日期面板
openPanel(){this.panelState=!this.panelState;this.panelType="date";},
  • 点击2018年份进入年份面板,点击相对应年份显示该年份并进入月份选择面板
<spanclass="year"@click="panelType='year'">{{tmpYear}}</span>selectYear(item){this.tmpYear=item;this.panelType="month";},
  • 点击Aug月份进入月份面板,点击相对应月份显示该月份并进入日期选择面板
<spanclass="month"@click="panelType='month'">{{changeTmpMonth}}</span>selectMonth(item){this.tmpMonth=item.value;this.panelType="date";},

点击日期选择日期,关闭面板同时赋值给输入框

//methodsselectDate(item){//赋值当前nowValue,用于控制样式突出显示当前月份日期this.nowValue=item.value;//选择了上个月if(item.previousMonth)this.tmpMonth--;//选择了下个月if(item.nextMonth)this.tmpMonth;//获取选中日期的dateletselectDay=newDate(this.tmpYear,this.tmpMonth,this.nowValue);//格式日期为字符串后,赋值给inputthis.dateValue=this.formatDate(selectDay.getTime());//关闭面板this.panelState=!this.panelState;},//日期格式方法formatDate(date,fmt=this.format){if(date===null||date==="null"){return"--";}date=newDate(Number(date));varo={"M":date.getMonth()1,//月份"d":date.getDate(),//日"h":date.getHours(),//小时"m":date.getMinutes(),//分"s":date.getSeconds(),//秒"q":Math.floor((date.getMonth()3)/3),//季度S:date.getMilliseconds()//毫秒};if(/(y)/.test(fmt))fmt=fmt.replace(RegExp.$1,(date.getFullYear()"").substr(4-RegExp.$1.length));for(varkino){if(newRegExp("("k")").test(fmt))fmt=fmt.replace(RegExp.$1,RegExp.$1.length===1?o[k]:("00"o[k]).substr((""o[k]).length));}returnfmt;},//确认是否为当前月份validateDate(item){if(this.nowValue===item.value&&item.currentMonth)returntrue;},

(2)topbar中左右箭头功能,具体详看下面方法

//<left(){if(this.panelType==="year")this.tmpYear--;else{if(this.tmpMonth===0){this.tmpYear--;this.tmpMonth=11;}elsethis.tmpMonth--;}},//<<leftBig(){if(this.panelType==="year")this.tmpYear-=12;elsethis.tmpYear--;},//>right(){if(this.panelType==="year")this.tmpYear;else{if(this.tmpMonth===11){this.tmpYear;this.tmpMonth=0;}elsethis.tmpMonth;}},//>>rightBig(){if(this.panelType==="year")this.tmpYear=12;elsethis.tmpYear;},

(3)实现输入框的双向绑定及格式规定

props

props:{value:{type:[Date,String],default:""},format:{type:String,default:"yyyy-MM-dd"}},

其中value支持日期格式和字符串,当设置了props时,则需在monted钩子函数中初始化input值。format默认值为"yyyy-MM-dd",当然你也可以设置为"dd-MM-yyyy"等。

mounted(){if(this.value){this.dateValue=this.formatDate(newDate(this.value).getTime());}},

双向绑定父组件赋值props为value,子组件传递的事件为input,因此需在selectDate方法中emit事件及数据给父组件

selectDate(item){...this.$emit("input",selectDay);},

这样,父组件便可以进行双向绑定了

<Datepickerv-model="time"format="dd-MM-yyyy"/>

(4)点击页面其他位置收起日历面板

原理

监听页面的点击事件,检测到有点击事件时关闭面板,但点击组件内容时也会触发点击事件,因此需要在组件内部阻止冒泡。同时,当组件销毁时,也要及时清除该监听器。

组件最外层阻止冒泡

<divclass="date-picker"@click.stop></div>

页面创建设置监听

mounted(){...window.addEventListener("click",this.eventListener);}

页面销毁清除监听

destroyed(){window.removeEventListener("click",this.eventListener);}

公共方法

eventListener(){this.panelState=false;},

项目Demo

项目源码

有用就点个赞呗~

最后,贴上CSS代码...
  • fadeDownBig后面的样式为vue<transiton>的动画特效.
.topbar{padding-top:8px;}.topbarspan{display:inline-block;width:20px;height:30px;line-height:30px;color:#515a6e;cursor:pointer;}.topbarspan:hover{color:#2d8cf0;}.topbar.year,.topbar.month{width:60px;}.year-list{height:200px;width:210px;}.year-list.selected{background:#2d8cf0;border-radius:4px;color:#fff;}.year-listli{display:inline-block;width:70px;height:50px;line-height:50px;border-radius:10px;cursor:pointer;}.year-listspan{display:inline-block;line-height:16px;padding:8px;}.year-listspan:hover{background:#e1f0fe;}.weekday{display:inline-block;font-size:13px;width:30px;color:#c5c8ce;text-align:center;}.date-picker{width:210px;text-align:center;font-family:"Avenir",Helvetica,Arial,sans-serif;}.date-panel{width:210px;box-shadow:008px#ccc;background:#fff;}ul{list-style:none;padding:0;margin:0;}.date-list{width:210px;text-align:left;height:180px;overflow:hidden;margin-top:4px;}.date-listli{display:inline-block;width:28px;height:28px;line-height:30px;text-align:center;cursor:pointer;color:#000;border:1pxsolid#fff;border-radius:4px;}.date-list.selected{border:1pxsolid#2d8cf0;}.date-list.invalid{background:#2d8cf0;color:#fff;}.date-list.preMonth,.date-list.nextMonth{color:#c5c8ce;}.date-listli:hover{background:#e1f0fe;}input{display:inline-block;box-sizing:border-box;width:100%;height:32px;line-height:1.5;padding:4px7px;font-size:12px;border:1pxsolid#dcdee2;border-radius:4px;color:#515a6e;background-color:#fff;background-image:none;position:relative;cursor:text;transition:border0.2sease-in-out,background0.2sease-in-out,box-shadow0.2sease-in-out;margin-bottom:6px;}.fadeDownBig-enter-active,.fadeDownBig-leave-active,.fadeInDownBig{-webkit-animation-duration:0.5s;animation-duration:0.5s;-webkit-animation-fill-mode:both;animation-fill-mode:both;}.fadeDownBig-enter-active{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig;}.fadeDownBig-leave-active{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig;}@-webkit-keyframesfadeInDownBig{from{opacity:0.8;-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0);}to{opacity:1;-webkit-transform:none;transform:none;}}@keyframesfadeInDownBig{from{opacity:0.8;-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0);}to{opacity:1;-webkit-transform:none;transform:none;}}@-webkit-keyframesfadeOutDownBig{from{opacity:1;}to{opacity:0.8;-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0);}}@keyframesfadeOutDownBig{from{opacity:1;}to{opacity:0;}}

发表评论

  • 评论列表
还没有人评论,快来抢沙发吧~