最近工作中经常用到日历这个组件, 以前大多是用moment.js或者dayjs去做的, 其实使用原生js也并不复杂.
1.首先我们来认识几个这个组件中使用的new Date常用的方法
this.lastDay = new Date(y, m + 1, 0).getDate() // 获取指定月的最后一天也即每月多少天
this.lastMonthDay = new Date(y, m, 0).getDate() // 获取上个月多少天
this.firstDayisWhat = new Date(y, m, 1).getDay() // 第一天星期几0-6(星期日到星期六)
this.lastDayisWhat = new Date(y, m + 1, 0).getDay() // 最后一天星期几0-6(星期日到星期六)
date.getFullYear() // 指定日期的年
date.getMonth() // 指定日期的月
date.getDate() // 指定日期的天
new Date().getFullYear() + 1 // 指定年加1年
这几个方法就可以写出我们需要的组件. 其中需要注意的是 星期是 0 - 6 星期日到星期六, 月份是 0 - 11 一月到十二月.
2.需求整理
依照ElementUI的日历, 我们需要可动态渲染的slot, 默认的选中, 上月下月翻页, 除此之外,我又加了动态的起始周
3.开始代码
props: {
activeDay: {
type: Array,
default: () => []
}, // 首先我们接收activeDay 默认选中的日期 后面我们将点击事件暴露给父节点, 在父节点中动态修改选中的日期
startingweek: {
type: [Number, String],
default: () => 0
}, // 起始周 默认为 0 即周日开始 参数 0 - 6 , 星期日到星期六
filtWeekTab: {
type: Function
}, // 渲染week的方法, 这个方法暴露出三个参数 当前的星期, 当前年, 当前月
activeCurrentDay: {
type: Boolean,
default: () => true
}, // 是否默认选中当前天
width: {
type: [Number, String],
default: () => 400
} // 宽度
},
核心方法,因为动态起始周的关系,所以前后填充的逻辑比较绕
(7 - this.startingweek + this.firstDayisWhat) % 7
(6 - this.lastDayisWhat + this.startingweek) % 7
能看懂这俩为什么这么写,基本上算是无压力写这个组件了
此处拿到所有需要展示的日期
dateInit (year = new Date().getFullYear(), month = new Date().getMonth()) {
const y = year // 获取传入的年 默认 new Date().getFullYear()
const m = month // 获取传入的月 默认 new Date().getMonth()
this.panelYear = year // 全局展示的年
this.panelMonth = month // 全局展示的月
this.lastDay = new Date(y, m + 1, 0).getDate() // 每月最后一天也即每月多少天
this.lastMonthDay = new Date(y, m, 0).getDate() // 获取上个月多少天
this.firstDayisWhat = new Date(y, m, 1).getDay() // 第一天星期几0-6(星期日到星期六)
this.lastDayisWhat = new Date(y, m + 1, 0).getDay() // 最后一天星期几0-6(星期日到星期六)
let beginTmp = [] // 初始化头部填充 非当前月的数据都是day开头
// 头部拿到 firstDayisWhat 当月第一天是周几 去循环 用lastMonthDay上个月的总天数
for (let i = 0; i < (7 - this.startingweek + this.firstDayisWhat) % 7; i++) {
beginTmp.push(`upday-${this.lastMonthDay - ((7 - this.startingweek + this.firstDayisWhat) % 7 - i - 1)}`)
}
// 初始化完整的月的每天格式为年月日
const lastFullTmp = []
// 拿到lastDay本月的天数循环拼接完整年月日 panelYear panelMonth i addPreZero方法补零
for (let i = 1; i <= this.lastDay; i++) {
lastFullTmp.push(`${this.panelYear}-${this.addPreZero(this.panelMonth + 1)}-${this.addPreZero(i)}`)
}
// 尾部填充
let lastinTmp = []
// 获取最后一天是周几
for (let i = 0; i < (6 - this.lastDayisWhat + this.startingweek) % 7; i++) {
lastinTmp.push(`downday-0${i + 1}`)
}
// 拼接数组拿到当前月完整的数据
this.dayFullList = [...beginTmp, ...lastFullTmp, ...lastinTmp]
},
此处做选中, 如果activeCurrentDay为false则不选中当前天, 很简单的逻辑控制
// 设置选中的日期
getActiveDay (day) {
if (this.activeCurrentDay && this.formatDate().includes(day)) {
return true
}
return this.activeDay.includes(day)
},
此处对应的 filtWeekTab以及起始周的控制,filtWeekTab接收一个方法, 暴露出三个参数, 如下图
Math.abs(value - 1 + this.startingweek) % 7 // 这个是关键
getStartingDay (value) {
let week
let key
key = Math.abs(value - 1 + this.startingweek) % 7
switch (key) {
case 0:
week = '日'
break;
case 1:
week = '一'
break;
case 2:
week = '二'
break;
case 3:
week = '三'
break;
case 4:
week = '四'
break;
case 5:
week = '五'
break;
case 6:
week = '六'
break;
}
if (typeof this.filtWeekTab === 'function') {
return this.filtWeekTab(week, this.panelYear, this.panelMonth)
}
return week
},
使用方法
filtWeekTab (value, year, month) {
console.log(value, year, month);
return value
},
最关键的翻页逻辑及slot插槽处理 // 属于内置方法,
updateDays (year = this.panelYear, month = this.panelMonth) {
this.dateInit(year, month)
this.panelYear = year
this.panelMonth = month
}
<div class="date-tools" :style="`width: ${width}px`">
<div class="date-years">
<span>{{ panelYear }}年{{ panelMonth + 1 }}月</span>
<slot name="buttons"></slot>
</div>
<div class="date-weeks">
<span v-for="i in 7" :key="i">{{ getStartingDay(i) }}</span>
</div>
<div class="date-days">
<template v-for="(day, index) in dayFullList">
<div @click="getClickDay(day)" v-if="day.includes('day-')" :key="index" class="dayButton">
<slot :days="day">
<span class="othermonths">{{ filterDay(day) }}</span>
</slot>
</div>
<div @click="getClickDay(day)" v-else :key="index" :class="{ active: getActiveDay(day) }" class="dayButton">
<slot :days="day">
<span class="currentmonth">{{ filterDay(day) }}</span>
</slot>
</div>
</template>
</div>
</div>
这个组件总共设置了两个插槽一个具名插槽buttons默认为空, 因为觉的翻页按钮每个人放的位置都不一样,而且翻页按钮有些定制化的需求如翻到指定年月日选中日期等,所以通过具名插槽及updateDays方法可以动态设置翻页.使用案列如下
<demo ref="demos" :activeDay="activeDay" :activeCurrentDay="true" :startingweek="0" :filtWeekTab="filtWeekTab" @getClickDay="getClickDay">
<template slot="buttons">
<div>
<el-button @click="down">上个月</el-button>
<el-button @click="up">下个月</el-button>
</div>
</template>
<template v-slot="{ days }">
<span v-if="days.includes('day-')" class="othermonths">{{ getDays(days) }}</span>
<span v-if="!days.includes('day-')" class="currentmonth">{{ getDays(days) }} {{ activeDay.includes(days) ? '✔️' : '' }}</span>
</template>
</demo>
up () {
let t = this.$refs.demos.getCurrentYearMonth()
let year = t[0]
let month
if (Number(t[1]) >= 11) {
month = 0
year = new Date(`${t[0]}-01-01`).getFullYear() + 1
} else {
month = Number(t[1]) + 1
}
this.$refs.demos.updateDays(year, month)
},
down () {
let t = this.$refs.demos.getCurrentYearMonth()
let year = t[0]
let month
if (Number(t[1]) <= 0) {
month = 11
year = new Date(`${t[0]}-01-01`).getFullYear() - 1
} else {
month = Number(t[1]) - 1
}
this.$refs.demos.updateDays(year, month)
}
说一下第二个匿名插槽, 匿名插槽暴露出来的参数是days及当前月的日期 可以通过判断条件渲染不同的样式及页面, 和element的日历是一致的
最后补充上完整代码和使用案列, 使用上有什么问题欢迎和我说
<template>
<div class="date-tools" :style="`width: ${width}px`">
<div class="date-years">
<span>{{ panelYear }}年{{ panelMonth + 1 }}月</span>
<slot name="buttons"></slot>
</div>
<div class="date-weeks">
<span v-for="i in 7" :key="i">{{ getStartingDay(i) }}</span>
</div>
<div class="date-days">
<template v-for="(day, index) in dayFullList">
<div @click="getClickDay(day)" v-if="day.includes('day-')" :key="index" class="dayButton">
<slot :days="day">
<span class="othermonths">{{ filterDay(day) }}</span>
</slot>
</div>
<div @click="getClickDay(day)" v-else :key="index" :class="{ active: getActiveDay(day) }" class="dayButton">
<slot :days="day">
<span class="currentmonth">{{ filterDay(day) }}</span>
</slot>
</div>
</template>
</div>
</div>
</template>
<script>
export default {
props: {
activeDay: {
type: Array,
default: () => []
},
startingweek: {
type: [Number, String],
default: () => 0
},
filtWeekTab: {
type: Function
},
activeCurrentDay: {
type: Boolean,
default: () => true
},
width: {
type: [Number, String],
default: () => 400
}
},
data () {
return {
dayFullList: [], // 所有的天数列表,前面空位补0
panelYear: '', // 全局展示的年
panelMonth: '', // 全局展示的月(从0开始)
lastDay: '', // 每月最后一天也即每月多少天
lastMonthDay: '', // 获取上个月多少天
firstDayisWhat: '', // 第一天星期几0-6(星期日到星期六)
lastDayisWhat: '' // 最后一天星期几0-6(星期日到星期六)
}
},
methods: {
dateInit (year = new Date().getFullYear(), month = new Date().getMonth()) {
const y = year // 获取传入的年 默认 new Date().getFullYear()
const m = month // 获取传入的月 默认 new Date().getMonth()
this.panelYear = year // 全局展示的年
this.panelMonth = month // 全局展示的月
this.lastDay = new Date(y, m + 1, 0).getDate() // 每月最后一天也即每月多少天
this.lastMonthDay = new Date(y, m, 0).getDate() // 获取上个月多少天
this.firstDayisWhat = new Date(y, m, 1).getDay() // 第一天星期几0-6(星期日到星期六)
this.lastDayisWhat = new Date(y, m + 1, 0).getDay() // 最后一天星期几0-6(星期日到星期六)
let beginTmp = [] // 初始化头部填充 非当前月的数据都是day开头
// 头部拿到 firstDayisWhat 当月第一天是周几 去循环 用lastMonthDay上个月的总天数
for (let i = 0; i < (7 - this.startingweek + this.firstDayisWhat) % 7; i++) {
beginTmp.push(`upday-${this.lastMonthDay - ((7 - this.startingweek + this.firstDayisWhat) % 7 - i - 1)}`)
}
// 初始化完整的月的每天格式为年月日
const lastFullTmp = []
// 拿到lastDay本月的天数循环拼接完整年月日 panelYear panelMonth i addPreZero方法补零
for (let i = 1; i <= this.lastDay; i++) {
lastFullTmp.push(`${this.panelYear}-${this.addPreZero(this.panelMonth + 1)}-${this.addPreZero(i)}`)
}
// 尾部填充
let lastinTmp = []
// 获取最后一天是周几
for (let i = 0; i < (6 - this.lastDayisWhat + this.startingweek) % 7; i++) {
lastinTmp.push(`downday-0${i + 1}`)
}
// 拼接数组拿到当前月完整的数据
this.dayFullList = [...beginTmp, ...lastFullTmp, ...lastinTmp]
},
// 小于9的需要添加0前缀
addPreZero (num) {
return num > 9 ? num : '0' + num
},
// 设置选中的日期
getActiveDay (day) {
if (this.activeCurrentDay && this.formatDate().includes(day)) {
return true
}
return this.activeDay.includes(day)
},
getStartingDay (value) {
let week
let key
key = Math.abs(value - 1 + this.startingweek) % 7
switch (key) {
case 0:
week = '日'
break;
case 1:
week = '一'
break;
case 2:
week = '二'
break;
case 3:
week = '三'
break;
case 4:
week = '四'
break;
case 5:
week = '五'
break;
case 6:
week = '六'
break;
}
if (typeof this.filtWeekTab === 'function') {
return this.filtWeekTab(week, this.panelYear, this.panelMonth)
}
return week
},
filterDay (value) {
return parseInt(value.slice(-2))
},
getClickDay (day) {
this.$emit('getClickDay', day)
},
updateDays (year = this.panelYear, month = this.panelMonth) {
this.dateInit(year, month)
this.panelYear = year
this.panelMonth = month
},
getCurrentYearMonth () {
return [this.panelYear, this.panelMonth]
},
formatDate (date = new Date()) {
let y = date.getFullYear()
let m = date.getMonth() + 1
m = m < 10 ? '0' + m : m
let d = date.getDate()
d = d < 10 ? ('0' + d) : d
return y + '-' + m + '-' + d
}
},
mounted () {
this.dateInit()
}
}
</script>
<style lang="scss" scoped>
.date-tools {
min-width: 400px;
.date-years {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
font-size: 13px;
}
.date-weeks {
display: flex;
border-bottom: 1px solid #d7dce5;
padding-bottom: 10px;
span {
width: calc(100% / 7);
text-align: center;
color: #929aac;
font-size: 13px;
}
}
.date-days {
display: flex;
flex-wrap: wrap;
.dayButton {
width: calc(100% / 7);
min-height: 50px;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #f2f8fe;
span:not(.othermonths) {
color: #1989fa;
}
}
span {
width: 20px;
height: 20px;
line-height: 20px;
border-radius: 50%;
font-size: 12px;
text-align: center;
cursor: pointer;
}
.othermonths {
color: #e3e3e3;
}
.currentmonth {
color: #000;
}
}
.active {
background-color: #f2f8fe;
cursor: pointer;
span.currentmonth {
color: #1989fa;
}
}
}
}
</style>
<template>
<div>
<demo ref="demos" :activeDay="activeDay" :activeCurrentDay="true" :startingweek="0" :filtWeekTab="filtWeekTab" @getClickDay="getClickDay">
<template slot="buttons">
<div>
<el-button @click="down">上个月</el-button>
<el-button @click="up">下个月</el-button>
</div>
</template>
<template v-slot="{ days }">
<span v-if="days.includes('day-')" class="othermonths">{{ getDays(days) }}</span>
<span v-if="!days.includes('day-')" class="currentmonth">{{ getDays(days) }} {{ activeDay.includes(days) ? '✔️' : '' }}</span>
</template>
</demo>
</div>
</template>
<script>
import demo from './demo'
export default {
components: {
demo
},
data () {
return {
activeDay: ['2020-11-20']
}
},
mounted () {
setTimeout(() => {
this.activeDay = ['2020-11-20', '2020-11-10']
}, 3000);
},
methods: {
filtWeekTab (value, year, month) {
console.log(value, year, month);
return value
},
getDays (days) {
// console.log(days);
return parseInt(days.slice(-2))
},
getClickDay (day) {
console.log(day)
},
up () {
let t = this.$refs.demos.getCurrentYearMonth()
let year = t[0]
let month
if (Number(t[1]) >= 11) {
month = 0
year = new Date(`${t[0]}-01-01`).getFullYear() + 1
} else {
month = Number(t[1]) + 1
}
this.$refs.demos.updateDays(year, month)
},
down () {
let t = this.$refs.demos.getCurrentYearMonth()
let year = t[0]
let month
if (Number(t[1]) <= 0) {
month = 11
year = new Date(`${t[0]}-01-01`).getFullYear() - 1
} else {
month = Number(t[1]) - 1
}
this.$refs.demos.updateDays(year, month)
}
},
}
</script>