关于自定义日历
工作需要,现有框架封装的日历无法满足需求,又找不到更好的插件的情况下,咋办??自己写个呗!
效果图和功能说明
先看看效果图
在这里插入图片描述
其实基本界面就这样了,和其他没啥区别。
但是既然要单独封装一个,那肯定有其他可扩展的地方,不然就没意义了
功能说明
1、基本日历功能
2、可以自定义标题头部
3、可自定义顶部时间以及月份切换部分
4、日历单元格样式可以自定义
5、星期一栏可以自定义
6、可以在日历里面插入数据:这一点是最重要的,也是必须要自己封装的重要因素
使用
封装为日历组件,直接引用即可。需要扩展其他功能,可以看组件里面的props哦,这是可以接收的参数。当然,如果用props不能满足的,那就看看里面的slot插槽部分咯。
这是props定义接收的参数,每个参数均有注释,这里不多做介绍。
props: { initDate:{ type:[String,Date,Number], default:()=>new Date() },//初始化日期 width:{ type:[String,Number], default:'100%' },//日历宽度 height:{ type:[String,Number], default:'100%' },//日历高度 calendarClass:String,//日历自定义样式类 titleClass:String,//年月标题自定义样式类 titleH:{ type:[String,Number], default:'35px' },//年月标题高度 titleBk:{ type:String, default:'#ffffff' },//年月标题颜色 bodyBk:{ type:String, default:'#ffffff' },//日历体背景 bodyClass:String,//日历体自定义样式 dateDefaultClass:String,//日期自定义默认类名 dateActivDateClass:String,//日期自定义选中类名 dateDisabledDateClass:String,//日期自定义不可见类名 titleDateConnector:String,//标题日期连接符 insertData:{ type:Array, default:()=>[] },//自定义拼接数据 weeks:{ type:Array, default:()=>['日','一','二','三','四','五','六'] },//周数据 dateProp:{ type:String, default:'date' },//自定义表示时间的字段 calenCellClass:String,//日历单元格自定义样式 firstRowCellClass:String,//日历第一行单元格自定义样式 firstColumCellClass:String,//日历第一列单元格自定义样式 cellBorder:Boolean,//是否有边框 cellTitleHeight:{ type:[Number,String], default:'40px' },//日历标题高度 cellTitleColor:{ type:String, default:'#333333' },//日历标题颜色 range:{ type:Array, default:()=>[] },//日期范围 },
更多个性化功能请看slot部分
在这里插入图片描述
插槽使用示例:
不熟悉插槽的伙伴可以去vue官网补补:https://cn.vuejs.org/v2/guide/components-slots.html
<calendar> <template #calendarTitle> <div class="rowStart calendarTitle"> <i class="el-icon-date yellow font20"></i> <strong class="font16" style="margin-left:5px;">日历标题</strong> </div> </template> <template #calendarTop="{currentYear,currentMonth,changeMonth}"> <div class="rowBtween canlendar-top-box"> <div class="rowStart year-back-box"> <strong class="font24">{{currentYear}}年{{currentMonth}}月</strong> <span class="blue back-today font14" @click="changeMonth(new Date())">返回今天</span> </div> <div class="change-month-box rowBtween"> <i class="el-icon-arrow-left el-icon blue" @click="changeMonth(0)"></i> <span></span> <i class="el-icon-arrow-right el-icon blue" @click="changeMonth(1)"></i> </div> </div> </template> </calendar>
其他不多说了,最后上代码。由于组件代码放在一个页面写重了点,因此分成了几个文件
所有data里面的变量,props,还有函数均有说明。
先看canlendar.vue
<template> <div class="custom-calendar" :style="{width:calendarWidth,height:calendarHeight}" :class="calendarClass"> <div class="calendar-topBox"> <slot name="calendarTitle"></slot> <slot name="calendarTop" :currentYear="currentYear" :currentMonth="currentMonth+1" :changeMonth="changeMonth"> <div class="calendar-title rowBtween" :style="{height:titleHeight,background:titleBk}" :class="titleClass"> <strong class="left">{{currentYear}}{{titleDateConnector || '年'}}{{currentMonth+1}}{{titleDateConnector ? '' : '月'}}</strong> <div class="right rowBtween"> <span @click="changeMonth(0)"><</span> <span @click="changeMonth(1)">></span> </div> </div> </slot> </div> <div class="calendar-body" :style="{background:bodyBk}" :class="bodyClass"> <div class="bodyTitleBox rowCenter"> <slot name="weeks"> <span class="body-title rowCenter" :ref="index ? '' : 'calenCellTitle'" :style="{height:cellTitleHeight,color:cellTitleColor}" v-for="(week,index) in weeks" :key="index">{{week}}</span> </slot> </div> <div class="calen-content rowCenter"> <div class="calen-cell" :id="'calen'+index" :ref="index ? '' : 'calenCell'" :class="[calenCellClass,index<7 ? 'firstRowCellClass': '',index%7===0 ? 'firstColumCellClass' : '']" :style="{...calenCellStyle,...cellBorderStyle(index)}" v-for="(day,index) in calendarList" :key="index" @click="choose(day)"> <slot name="day" :day="day"> <span class="dateSpan rowCenter" :class="day.dateClass">{{day.day}}</span> </slot> <slot name="haveDataTag" :hasData="day.hasData" :isThis="day.isThis"> <div v-show="day.hasData && day.isThis"></div> </slot> </div> </div> </div> </div></template><script> import {isValidDate,isNumber} from "./validate" import {getStartTimeEndTimeInfoFun,insertDataToCalendar} from './util' export default { name: "Calendar", props: { initDate:{ type:[String,Date,Number], default:()=>new Date() },//初始化日期 width:{ type:[String,Number], default:'100%' },//日历宽度 height:{ type:[String,Number], default:'100%' },//日历高度 calendarClass:String,//日历自定义样式类 titleClass:String,//年月标题自定义样式类 titleH:{ type:[String,Number], default:'35px' },//年月标题高度 titleBk:{ type:String, default:'#ffffff' },//年月标题颜色 bodyBk:{ type:String, default:'#ffffff' },//日历体背景 bodyClass:String,//日历体自定义样式 dateDefaultClass:String,//日期自定义默认类名 dateActivDateClass:String,//日期自定义选中类名 dateDisabledDateClass:String,//日期自定义不可见类名 titleDateConnector:String,//标题日期连接符 insertData:{ type:Array, default:()=>[] },//自定义拼接数据 weeks:{ type:Array, default:()=>['日','一','二','三','四','五','六'] },//周数据 dateProp:{ type:String, default:'date' },//自定义表示时间的字段 calenCellClass:String,//日历单元格自定义样式 firstRowCellClass:String,//日历第一行单元格自定义样式 firstColumCellClass:String,//日历第一列单元格自定义样式 cellBorder:Boolean,//是否有边框 cellTitleHeight:{ type:[Number,String], default:'40px' },//日历标题高度 cellTitleColor:{ type:String, default:'#333333' },//日历标题颜色 range:{ type:Array, default:()=>[] },//日期范围 }, data() { return { calendarList: [],//日历数据 activeDay: {},//选中日期信息 currentYear: '',//显示的年 currentMonth: '',//显示的月 chooseDate: '',//选择的日期 calenCellHeight:0,//日历单元格高度 calenBotyTitleCellStyle:{},//日历顶部单元格样式 calenCellStyle:{},//日历单元格样式 calendarWidth:'100%',//日历宽度 calendarHeight:'100%',//日历高度 titleHeight:'35px',//年月标题高度 dateCellDefaultClass:'',//日历单元格默认类名 dateCellActiClass:'',//日历单元格选中类名 dateCellDisabledClass:'',//日历不可见单元格类名 cellBorderStyle:(index)=>{return {}},//日历单元格边框样式 bodyCellTitleHeight:'40px' } }, async created(){ console.log(this.calenCellClass) let {width,height,titleH,dateDefaultClass,dateActivDateClass,dateDisabledDateClass,cellTitleHeight}=this this.calendarWidth=(typeof width==='number' || isNumber(width)) ? (width+'px') : width this.calendarHeight=(typeof height==='number' || isNumber(height)) ? (height+'px') : height this.titleHeight=(typeof cellTitleHeight==='number' || isNumber(cellTitleHeight)) ? (cellTitleHeight+'px') : cellTitleHeight this.bodyCellTitleHeight=(typeof titleH==='number' || isNumber(titleH)) ? (titleH+'px') : titleH this.dateCellDefaultClass=dateDefaultClass || 'dateDefaultCss' this.dateCellActiClass=dateActivDateClass || 'dateActiveCss' this.dateCellDisabledClass=dateDisabledDateClass || 'disableDateCss' let {dateTime,isValid}=isValidDate(this.initDate) if(!isValid) return let year=dateTime.getFullYear() let month=dateTime.getMonth() let date=dateTime.getDate() await this.init(year,month,date) insertDataToCalendar(this.insertData,this.calendarList,this.dateProp) }, methods: { //初始化日历 async init(year, month, date,config={}) { this.currentYear = year this.currentMonth = month let daysInMonth = new Date(year, month+1,0).getDate()//得到当前月份的天数 28,29,30,31 let firstDayInWeek = new Date(year, month,1).getDay()//获取当月的一号是星期几 let lastDayInWeek = new Date(year,month,daysInMonth).getDay()//获取当月最后一天是星期几 this.calendarList = await this.createcalendarList(year, month, date, firstDayInWeek, lastDayInWeek, daysInMonth,config) return this.calendarList }, //创建日历 createcalendarList(year, month, date, firstDayInWeek, lastDayInWeek, daysInMonth,config={}) { return new Promise((resolve)=>{ let thisYear=new Date().getFullYear() let thisMonth=new Date().getMonth() if(thisYear===year && thisMonth===month){ date=new Date().getDate() } let {isChangeMonth}=config let { calenStartDate,calenEndDate,calenStartYear,calenStartMonth,dateRangeCode}=getStartTimeEndTimeInfoFun(this.range[0],this.range[1]) // console.log(calenStartDate,calenEndDate,dateRangeCode,calenStartMonth) if(!isChangeMonth && dateRangeCode){ //日期范围有效 this.currentYear = calenStartYear this.currentMonth = calenStartMonth } let calendarList = [] //1号不在星期天,补全前面的日期信息 // console.log(firstDayInWeek) if (firstDayInWeek !== 0) { let prevMonthLastDate=new Date(year,month,0).getDate() for (let j = firstDayInWeek-1; j >= 0; j--) { calendarList.push({ dateClass: this.dateCellDisabledClass, year, month, day: prevMonthLastDate - j, week: new Date(year,month-1,prevMonthLastDate - j).getDay(), isThis: false }) } } // 添加日期信息 for (let i = 0; i < daysInMonth; i++) { let dateClass='' let isRange=true if(!dateRangeCode || isChangeMonth){ //日期范围值无效,已月份为范围 dateClass= (i + 1) === date ? this.dateCellActiClass : this.dateCellDefaultClass }else{ //日期范围值有效 if(((i + 1) === date) && (calenStartDate<=(i+1) && (i+1)<=calenEndDate)){ dateClass=this.dateCellActiClass }else if(calenStartDate<=(i+1) && (i+1)<=calenEndDate){ dateClass=this.dateCellDefaultClass }else{ dateClass=this.dateCellDisabledClass isRange=false } } let day = { dateClass, year, month:month+1, day: i + 1, week:new Date(year,month,i + 1).getDay(), isThis: true, isRange } ;((i + 1) === date) && (this.activeDay = day) calendarList.push(day) } //如果当月的最后一天不是星期六,后面补全日期信息 if (lastDayInWeek !== 6) { for (let i = 0; i < 6 - lastDayInWeek; i++) { calendarList.push({ dateClass: this.dateCellDisabledClass, year, month: month + 2, day: i + 1, week: new Date(year,month+1,i + 1).getDay(), isThis: false }) } } // 如果单元格有边框,执行边框函数 if(this.cellBorder){ this.cellBorderStyle=index=> { return { 'border-right-style':'solid', 'border-bottom-style':'solid', 'border-width':'1px', 'border-color':'#dddddd', 'border-top-style':index<7 ? 'solid' : '', 'border-left-style':index%7===0 ? 'solid' : '', } } } resolve(calendarList) }) }, //点击选择日期 choose(day) { console.log(day) let {isRange,isThis}=day if (isThis && isRange) { //本月 this.activeDay.dateClass = this.dateCellDefaultClass this.activeDay = day day.dateClass = this.dateCellActiClass let month=day.month <10 ? '0'+day.month : day.month let myDay=day.day <10 ? '0'+day.day : day.day this.chooseDate = day.year + '-' + month + '-' + myDay this.$emit('getChooseDate', this.chooseDate) } }, // 月份改变 async changeMonth(value) { let year=null let month=null let day=1 if(value===1 || value===0){ //翻页操作 if(value){ //向上翻 if (this.currentMonth === 11) { this.currentYear++ this.currentMonth = 0 } else { this.currentMonth++ } }else{ // 向下翻 if (this.currentMonth === 0) { this.currentYear-- this.currentMonth = 11 } else { this.currentMonth-- } } year=this.currentYear month=this.currentMonth }else{ //时间格式 if(typeof value !=='number' && typeof value !=='string' && !(value instanceof Date)){ console.error(`${value} 时间格式错误 `) return } if(typeof value ==='string'){ value=value.replace(/-/g,'/') } year=new Date(value).getFullYear() month=new Date(value).getMonth() day=new Date(value).getDate() } await this.init(year, month,day,{isChangeMonth:true})//等待日历先创建完成 insertDataToCalendar(this.insertData,this.calendarList,this.dateProp) this.$emit('monthChange') }, }, watch:{ //监听插入数据 insertData:{ deep:true, handler:function (value) { console.log(value) insertDataToCalendar(value,this.calendarList,this.dateProp) }, }, //监听日期范围变化 range:{ deep:true, handler:async function ([startTime,endTime]) { if(startTime && endTime){ await this.init(this.currentYear, this.currentMonth,1,)//等待日历先创建完成 insertDataToCalendar(this.insertData,this.calendarList,this.dateProp) } }, } } }</script><style lang="less" scoped> @import "./style"; @import "./flex";</style>
工具文件util.js
//获取开始日期结束日期最终信息export function getStartTimeEndTimeInfoFun(startDate,endDate){ let start=checkStartTimeEndTimeFun(startDate) let end=checkStartTimeEndTimeFun(endDate) let calenStartDateResult=start ? getNewDateFun(start) : false let calenStartYear=calenStartDateResult ? calenStartDateResult.calendarAddYear : '' let calenStartMonth=calenStartDateResult ? calenStartDateResult.calendarAddMonth : '' let calenStartDate=calenStartDateResult ? calenStartDateResult.calendarAddDate : '' let calenEndDateResult=end ? getNewDateFun(end) : false let calenEndYear=calenEndDateResult ? calenEndDateResult.calendarAddYear : '' let calenEndMonth=calenEndDateResult ? calenEndDateResult.calendarAddMonth : '' let calenEndDate=calenEndDateResult ? calenEndDateResult.calendarAddDate : '' if(calenStartYear!==calenEndYear || calenStartMonth!==calenEndMonth || !calenEndDate || !calenStartDate || (calenStartDate>calenEndDate)){ (start || end) && console.error(`日期范围仅支持本月范围选择或日期格式错误${startDate} ${endDate}`) return {dateRangeCode:0} } return{ calenStartYear, calenStartMonth:calenStartMonth-1, calenStartDate, calenEndYear, calenEndMonth:calenEndMonth-1, calenEndDate, dateRangeCode:1 }}//检查开始日期和结束日期export function checkStartTimeEndTimeFun(date){ if(typeof date!=='string' && (typeof date==='string' && !date) && typeof date!=='number' && !(date instanceof Date)){ //非时间可能的数据格式,或者明确时间没有值,跳过 return '' } if(typeof date==='number' && date.length<10){ console.error(`日期${date}格式错误,请使用正确的时间戳格式,例如:1593669468000`) return '' } if(typeof date==='string' && date){ //传递过来的时间格式时字符串,此处不可以用parseInt,因为parseInt会将类似2020-02-02解析为2020 if(!/-/.test(date) && !/\//.test(date && !Number(date))){ console.error(`日期${date}格式错误,请使用正确的字符串日期格式,例如:2020-06-06,或2020/06/06`) return '' } if(!/-/.test(date) && !/\//.test(date) && Number(date)){ date=Number(date) } } return date}//将传入的时间生成新的值并返回export function getNewDateFun(date){ let calendarAddYear=null let calendarAddMonth=null let calendarAddDate=null if(typeof date==='string'){ //字符串,可能时2020-03-04或2020/02/03或03-02或03/02 let dateArr=/-/.test(date) ? date.split('-') : date.split('/') if(dateArr.length===3){ //年月日 calendarAddYear=parseInt(dateArr[0]) calendarAddMonth=parseInt(dateArr[1]) calendarAddDate=parseInt(dateArr[2]) }else if(dateArr.length===2){ //月日,年默认为今年 calendarAddYear=new Date().getFullYear() calendarAddMonth=parseInt(dateArr[0]) calendarAddDate=parseInt(dateArr[1]) }else{ //其他未知情况,报错 console.error(`日期${date}格式错误,请使用正确的字符串日期格式,例如:2020-06-06,或2020/06/06`) return false } }else{ //时间戳和时间对象 if(typeof date==='number'){ //时间戳转为时间对象 date=new Date(date) } calendarAddYear=date.getFullYear() calendarAddMonth=date.getMonth()+1 calendarAddDate=date.getDate() } return{ calendarAddYear, calendarAddMonth, calendarAddDate }}//将数据插入日历export function insertDataToCalendar(value,calendarList,dateProp){ if(!value.length || !(value instanceof Array)){ return } for(let i=0;i<value.length;i++){ let date=value[i][dateProp] // console.log(date) if(typeof date!=='string' && (typeof date==='string' && !date) && typeof date!=='number' && !(date instanceof Date)){ //非时间可能的数据格式,或者明确时间没有值,跳过 continue } if(typeof date==='number' && date.length<10){ console.error(`日期${date}格式错误,请使用正确的时间戳格式,例如:1593669468000`) continue } if(typeof date==='string' && date){ //传递过来的时间格式时字符串,此处不可以用parseInt,因为parseInt会将类似2020-02-02解析为2020 if(!/-/.test(date) && !/\//.test(date && !Number(date))){ console.error(`日期${date}格式错误,请使用正确的字符串日期格式,例如:2020-06-06,或2020/06/06`) continue } if(!/-/.test(date) && !/\//.test(date) && Number(date)){ date=Number(date) } } let caldarTimeResult=getNewDateFun(date) if(!caldarTimeResult) return let {calendarAddYear,calendarAddMonth,calendarAddDate}=caldarTimeResult value[i]={...value[i],calendarAddYear,calendarAddMonth,calendarAddDate} } for(let i=0;i<calendarList.length;i++){ let filterCalendarList=value.filter(item=>item.calendarAddYear===calendarList[i].year)//先筛选出当前年的 filterCalendarList=filterCalendarList.filter(item=>item.calendarAddMonth===calendarList[i].month)//再筛选出当前月的 filterCalendarList=filterCalendarList.filter(item=>item.calendarAddDate===calendarList[i].day)//最后筛选出当前天的 // console.log(filterCalendarList) if(!filterCalendarList.length){ // 没有值,跳过 continue } // 将传过来的数据插入原日历数据 calendarList.splice(i,1,{...calendarList[i],...filterCalendarList[0],hasData:true}) }}
日期验证文件validate.js
/** * 判断日期格式是否正确,正确返回日期 */export function isValidDate(dateTime) { let yourDate=dateTime try { ;(typeof dateTime === 'string') && (dateTime=dateTime.replace(/-/g,'/')) dateTime=new Date(dateTime) if(dateTime instanceof Date && !isNaN(dateTime.getTime())){ return { dateTime, isValid:true, } }else{ console.error(`日期 ${yourDate} 格式错误`) return { isValid:false, } } }catch (err){ console.error(err) }}/** * 判断是否为数字,整数或者小数 */export function isNumber(val) { let regPos = /^\d+(\.\d+)?$/ //非负浮点数 let regNeg = /^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$/ //负浮点数 return regPos.test(val) || regNeg.test(val)}
最后是样式文件style.less和flex.less
style.less
html,body,ul,li,p,span,div{ margin:0; padding:0; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box;}.dateDefaultCss{ color: black; cursor: pointer}.dateActiveCss{ color: white; cursor: pointer; background:#317ef2;}.disableDateCss{ color: #bfbfbf; cursor: default}.custom-calendar { .calendar-title { padding:0 30px 0 10px; .left { font-size: 105%; } .right { width:70px; border:solid 1px #dddddd; padding:0 10px; span{ font-size: 20px; } span:hover { cursor: pointer; color: #3a82fa; } } margin-bottom: 5px; color: #000; } .calendar-body { padding-bottom:10px; .bodyTitleBox{ .body-title { width:14%; } } .calen-content{ flex-wrap:wrap; } .calen-cell { color: #000; display: table-cell; text-align: center; vertical-align: middle; width:14%; margin-bottom: 5px; .dateSpan{ padding: 3px; -webkit-border-radius: 50%; -moz-border-radius: 50%; border-radius: 50%; width:20px; height:20px; margin:0 auto; } div{ width: 3px; height: 3px; border-radius: 50%; background: #3a82fa; margin: 0 auto; } } }}
flex.less
/*flex布局样式 *//* flex布局 */.rowStart { display: flex; flex-direction: row; justify-content: flex-start; align-items: center;}.rowAround { display: flex; flex-direction: row; justify-content: space-around; align-items: center;}.rowBtween { display: flex; flex-direction: row; justify-content: space-between; align-items: center;}.rowCenter { display: flex; flex-direction: row; justify-content: center; align-items: center;}.rowEnd { display: flex; flex-direction: row; justify-content: flex-end; align-items: center;}.columnStart { display: flex; flex-direction: column; justify-content: flex-start; align-items: center;}.columnAround { display: flex; flex-direction: column; justify-content: space-around; align-items: center;}.columnBtween { display: flex; flex-direction: column; justify-content: space-between; align-items: center;}.columnCenter { display: flex; flex-direction: column; justify-content: center; align-items: center;}.columnEnd { display: flex; flex-direction: column; justify-content: flex-end; align-items: center;}