前端之vue 封装自定义日历

土豆片片 2024-07-17 08:33:01 阅读 65

关于自定义日历

工作需要,现有框架封装的日历无法满足需求,又找不到更好的插件的情况下,咋办??自己写个呗!

效果图和功能说明

先看看效果图

在这里插入图片描述

其实基本界面就这样了,和其他没啥区别。

但是既然要单独封装一个,那肯定有其他可扩展的地方,不然就没意义了

功能说明

1、基本日历功能

2、可以自定义标题头部

3、可自定义顶部时间以及月份切换部分

4、日历单元格样式可以自定义

5、星期一栏可以自定义

6、可以在日历里面插入数据:这一点是最重要的,也是必须要自己封装的重要因素

使用

封装为日历组件,直接引用即可。需要扩展其他功能,可以看组件里面的props哦,这是可以接收的参数。当然,如果用props不能满足的,那就看看里面的slot插槽部分咯。

这是props定义接收的参数,每个参数均有注释,这里不多做介绍。

<code> 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">code>

<i class="el-icon-date yellow font20"></i>code>

<strong class="font16" style="margin-left:5px;">日历标题</strong>code>

</div>

</template>

<template #calendarTop="{currentYear,currentMonth,changeMonth}">code>

<div class="rowBtween canlendar-top-box">code>

<div class="rowStart year-back-box">code>

<strong class="font24">{ {currentYear}}年{ {currentMonth}}月</strong>code>

<span class="blue back-today font14" @click="changeMonth(new Date())">返回今天</span>code>

</div>

<div class="change-month-box rowBtween">code>

<i class="el-icon-arrow-left el-icon blue" @click="changeMonth(0)"></i>code>

<span></span>

<i class="el-icon-arrow-right el-icon blue" @click="changeMonth(1)"></i>code>

</div>

</div>

</template>

</calendar>

其他不多说了,最后上代码。由于组件代码放在一个页面写重了点,因此分成了几个文件

所有data里面的变量,props,还有函数均有说明。

先看canlendar.vue

<template>

<div class="custom-calendar" :style="{width:calendarWidth,height:calendarHeight}" :class="calendarClass">code>

<div class="calendar-topBox">code>

<slot name="calendarTitle"></slot>code>

<slot name="calendarTop" :currentYear="currentYear" :currentMonth="currentMonth+1" :changeMonth="changeMonth">code>

<div class="calendar-title rowBtween" :style="{height:titleHeight,background:titleBk}" :class="titleClass">code>

<strong class="left">{ {currentYear}}{ {titleDateConnector || '年'}}{ {currentMonth+1}}{ {titleDateConnector ? '' : '月'}}</strong>code>

<div class="right rowBtween">code>

<span @click="changeMonth(0)">&lt;</span>code>

<span @click="changeMonth(1)">&gt;</span>code>

</div>

</div>

</slot>

</div>

<div class="calendar-body" :style="{background:bodyBk}" :class="bodyClass">code>

<div class="bodyTitleBox rowCenter">code>

<slot name="weeks">code>

<span class="body-title rowCenter" :ref="index ? '' : 'calenCellTitle'" :style="{height:cellTitleHeight,color:cellTitleColor}" v-for="(week,index) in weeks" :key="index">{ {week}}</span>code>

</slot>

</div>

<div class="calen-content rowCenter">code>

<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)">code>

<slot name="day" :day="day">code>

<span class="dateSpan rowCenter" :class="day.dateClass">{ {day.day}}</span>code>

</slot>

<slot name="haveDataTag" :hasData="day.hasData" :isThis="day.isThis">code>

<div v-show="day.hasData && day.isThis"></div>code>

</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=''code>

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>code>

@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;

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。