关于自定义日历
工作需要,现有框架封装的日历无法满足需求,又找不到更好的插件的情况下,咋办??自己写个呗!
效果图和功能说明
先看看效果图
在这里插入图片描述
其实基本界面就这样了,和其他没啥区别。
但是既然要单独封装一个,那肯定有其他可扩展的地方,不然就没意义了
功能说明
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;
}