概述:目前市面上有很多开源的日历控件,但都需要通过npm安装,可根据自身项目需求选择适合自己的方案,本文主要讲诉element官网自身的el-calenda组件,实现自定义日历功能,废话不多说,直接上代码!
需求描述:
- 日历禁用掉非当前日期,不可点击,且颜色置灰
- 本月和非本月的日期进行颜色上的区分
- 如某天日期添加了信息,则展示当前信息,无添加显示待维护字段,默认显示待维护字段
- 日历上的日期+信息展示,可根据筛选条件进行联调,如日期控件切换、条件查询等
- 日历可点击,根据是否维护过,可弹出新增或编辑弹框,进行数据修改
- 日历只显示当前月数据,格式为月-日
- 禁用当前日期之前的数据中,如果无添加,则不显示待维护字段
实际效果图展示:
默认展示
条件查询后展示
核心点:
接口只返回当前月维护过的月份, 如当前月为5月,只维护了23 24 25,则接口只会返回这三天的数据,此时要做的就是先获取当前月份所有的日期,然后将接口数据组装进自己获取日期的数组中,后续在根据业务需求对数据进行处理
源码解析
- 组件提供了slot插槽,这个是实现日历自定义的核心。 返回两个参数date:单元格代表的日期,data:日期数据对象
<el-calendar v-model="calendarValue">
<template slot="dateCell" slot-scope="{date, data}">
</template>
</el-calendar>
- 如果要自定义日历上的显示格式,可通过以下方式进行组装,data.day.split('-').slice(1)[0]:获取当前月, data.day.split('-').slice(2).join():获取当前日,组装后,就展示为了年-月,如05-17
<div class="calendar-day" >{{ data.day.split('-').slice(1)[0] + '-' + data.day.split('-').slice(2).join() }}</div>
- 禁用当前之前之前的日期核心是:先通过class动态获取到当月所有的日期,然后和当前日期进行比对,如果小于当前日期,则加is-pervDay类名标识,在页面初始化或切换日历的时候,以is-pervDay类名为基准,进行动态添加DOM节点类名,最后根据此类名,进行样式上的控制
<div :class="findPrevDate(data.day) ? 'is-pervDay' : 'mainContainer'"></div>
created() {
this.setDisabledDayClass()
},
findPrevDate(date) {
const currentDate = new Date();
const year = currentDate.getFullYear();
// 月份是从 0 开始计数的,因此要加1
const month = currentDate.getMonth() + 1 < 10 ? "0" + (currentDate.getMonth() + 1) : currentDate.getMonth() + 1
const day = currentDate.getDate();
const currentDay = year + '-' + month + '-' + day
return date < currentDay
},
setDisabledDayClass() {
this.$nextTick(() => {
if (this.calendarStatus) {
let disabledDayNode = this.$refs.refCalendar.$el.querySelectorAll('td')
disabledDayNode.forEach(element => {
if (element.children[0].children[0].className === 'is-pervDay') {
element.className.includes('is-pervDay') ? element.className += '' : element.className += ' disabledDays'
}
})
}
})
},
::v-deep td.disabledDays {
// 日历不可点击
pointer-events: none;
background-color: rgba(0, 0, 0, .3);
color: #fff;
}
此时做到这里有个BUG:切换到其他日期后,之前被禁用的日期样式,还是会被继承,没有被清除,根本原因是,切换日期后,日历组件的DOM结构没有进行重排,还是显示之前的DOM结构,解决方案很简单,在el-calendar组件上加上v-if="calendarStatus",再每次切换或者初始化页面时,对DOM元素进行销毁!
<el-calendar v-model="calendarValue" ref="refCalendar" v-if="calendarStatus"></el-calendar>
gitChangeMonth(date) {
// 清空DOM缓存,用来每次切换日期,更新DOM节点
this.calendarStatus = false
setTimeout(() => {
this.calendarStatus = true
this.setDisabledDayClass()
}, 100)
},
- 想对日历上的内容进行自定义,核心则是:通过自己组装当前月的所有日期数据,和日期上的日期进行比对,相等则展示自定义DOM,否则就影藏,maintenanceMonth:为日期筛选条件,ticketDate:为接口比对参数,如果不通过v-if比对方式,数组有多少个对象,则会在日历上渲染多少个DIV,所有需要去除不匹配日期的数据。
获取当前月所有日期方法:
created() {
this.getCurrnetDays()
},
getCurrnetDays() {
// 获取标准时间
const date = new Date();
// 获取当前月份(实际月份需要加1)
// 如果已经选择日期,取当前选择日期中的月份,否则取当前月份
const currentMonth = this.maintenanceMonth.toString().indexOf('-') > -1 ? this.maintenanceMonth.split('-')[1] : date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1
// 获取当前年份
const currentYear = date.getFullYear();
// 获取当前月有多少天
const currentMonthDays = new Date(currentYear, currentMonth, 0).getDate();
// 当前月份所有日期集合
const currentMonthArr = [];
this.currneAlltDay = []
for (let day = 1; day <= currentMonthDays; day++) {
// 截至当前日期为止
let dateItem = currentMonth + "-" + (day < 10 ? '0' + day : day)
this.currneAlltDay.push({
ticketDate: dateItem
})
}
this.getCalendarList()
return currentMonthArr;
},
此时组装之后的数据结构为:当前月1号-31号所有的日期数据
将日历上的月-日,和接口的日-月进行比对,如果想等则展示该DOM元素下自定义的内容,否则影藏
<div v-for="item in currneAlltDay: " style="width: 100%;" :class="item.calendarFlagName ? '' : 'default'" v-if="data.day.split('-').slice(1)[0] + '-' + data.day.split('-').slice(2).join() === item.ticketDate">
<!-- 接口数据的月日 和 日历的月日 进行匹配 -->
<div class="tickContainer" @click="getCurrentDay(item, data.day)">
<div class="tickName">{{item.ticketName}}</div>
<div class="tickPrice" v-if="item.calendarFlagName">{{item.ticketPrice}}</div>
</div>
</div>
- 最后在对数据进行组装,实现业务上的需求
完整源码:
<template>
<div class="app-container" v-loading="loading">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="73px">
<el-form-item label="维护月份" prop="maintenanceMonth">
<el-date-picker
format="yyyy-MM"
value-format="yyyy-MM"
@change="gitChangeMonth"
:clearable="false"
@clear="getClear"
v-model="maintenanceMonth"
type="month"
placeholder="选择维护月份"
:picker-options="pickerOptions">
</el-date-picker>
</el-form-item>
<el-form-item label="景区项目" prop="region">
<el-select filterable v-model="queryParams.itemRegion" placeholder="请选择景区项目" clearable @change="getTicketArr" @clear="getClear('item')">
<el-option
v-for="dict in regionList"
:key="dict.id"
:label="dict.itemName"
:value="dict.id"
/>
</el-select>
</el-form-item>
<el-form-item label="景区门票" prop="ticketRegion">
<el-select filterable value-key="id" v-model="queryParams.ticketRegion" placeholder="请选择景区门票" clearable @change="selectTick" @clear="getClear">
<el-option
v-for="dict in ticketList"
:key="dict.id"
:label="dict.ticketName"
:value="dict"
/>
</el-select>
</el-form-item>
<el-form-item>
<div class="tips">
<div class="symbol">*</div>
<div class="desc">请先选择景区项目和景区门票,再进行设置</div>
</div>
</el-form-item>
</el-form>
<div class="money-init">单位:元</div>
<el-calendar v-model="calendarValue" ref="refCalendar" v-if="calendarStatus">
<template slot="dateCell" slot-scope="{date, data}">
<!--自定义内容-->
<div :class="findPrevDate(data.day) ? 'is-pervDay' : 'mainContainer'">
<!-- 在日历中显示当前日 -->
<div class="calendar-day" >{{ data.day.split('-').slice(1)[0] + '-' + data.day.split('-').slice(2).join() }}</div>
<div class="customerBox" :class="calendarArrList.length > 0 ? '' : 'disableContainer'">
<div v-for="item in currneAlltDay" style="width: 100%;" :class="item.calendarFlagName ? '' : 'default'" v-if="data.day.split('-').slice(1)[0] + '-' + data.day.split('-').slice(2).join() === item.ticketDate">
<!-- 接口数据的月日 和 日历的月日 进行匹配 -->
<div class="tickContainer" @click="getCurrentDay(item, data.day)">
<div class="tickName">{{item.ticketName}}</div>
<div class="tickPrice" v-if="item.calendarFlagName">{{item.ticketPrice}}</div>
</div>
</div>
</div>
</div>
</template>
</el-calendar>
<priceDialog ref="priceDialog" @getInit="getInitcalendar"></priceDialog>
</div>
</template>
<script>
import priceDialog from "./priceDialog";
import { getTicketList, itemList, calendarList } from "@/api/scenice/attractions"
export default {
components: { priceDialog },
data() {
return {
ticketDate: {},
selectMonth: '',
regionList:[],
loading: false,
size: 10,
page: 0,
calendarStatus: true,
queryParams: {},
currneAlltDay: [],
maintenanceMonth: new Date(),
ticketList: [],
pickerOptions: {
disabledDate(time) {
return time.getTime() < Date.now() - 8.64e7;
}
},
currentDate: '',
calendarArrList: [],
calendarValue: new Date(),
defaultYearMonth: new Date(),
currentNewDate: (new Date().getMonth() + 1 < 10 ? "0" + (new Date().getMonth() + 1) : currentDate.getMonth() + 1) + '-' + new Date().getDate()
}
},
created() {
this.setDisabledDayClass()
this.getRegionList()
this.getCurrnetDays()
},
methods: {
getCurrnetDays() {
// 获取标准时间
const date = new Date();
// 获取当前月份(实际月份需要加1)
// 如果已经选择日期,取当前选择日期中的月份,否则取当前月份
const currentMonth = this.maintenanceMonth.toString().indexOf('-') > -1 ? this.maintenanceMonth.split('-')[1] : date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1
// 获取当前年份
const currentYear = date.getFullYear();
// 获取当前月有多少天
const currentMonthDays = new Date(currentYear, currentMonth, 0).getDate();
// 当前月份所有日期集合
const currentMonthArr = [];
this.currneAlltDay = []
for (let day = 1; day <= currentMonthDays; day++) {
// 截至当前日期为止
let dateItem = currentMonth + "-" + (day < 10 ? '0' + day : day)
this.currneAlltDay.push({
ticketDate: dateItem
})
}
this.getCalendarList()
return currentMonthArr;
},
findPrevDate(date) {
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1 < 10 ? "0" + (currentDate.getMonth() + 1) : currentDate.getMonth() + 1 // 月份是从 0 开始计数的,因此要加1
const day = currentDate.getDate();
const currentDay = year + '-' + month + '-' + day
return date < currentDay
},
setDisabledDayClass() {
this.$nextTick(() => {
if (this.calendarStatus) {
let disabledDayNode = this.$refs.refCalendar.$el.querySelectorAll('td')
disabledDayNode.forEach(element => {
if (element.children[0].children[0].className === 'is-pervDay') {
element.className.includes('is-pervDay') ? element.className += '' : element.className += ' disabledDays'
}
})
}
})
},
// 查询景区项目
getRegionList() {
itemList(this.queryParams).then(response => {
this.regionList = response.data.filter(item => item.priceMode === 1 && item.status === 1)
})
},
// 查询项目下的门票
getTicketArr(id) {
if (id) {
getTicketList(id).then(response => {
this.ticketList = response.data
})
} else {
this.ticketList = []
this.calendarArrList = []
this.queryParams.ticketRegion = ''
this.ticketDate = {}
}
this.getCurrnetDays()
},
// 清空月份/项目/门票
getClear(type) {
if (type == 'item') {
this.ticketList = []
}
this.ticketDate = {}
this.queryParams.ticketRegion = ''
},
selectTick(data) {
this.ticketDate = data
this.getCurrnetDays()
this.$forceUpdate()
},
getInitcalendar() {
this.getCurrnetDays()
},
gitChangeMonth(date) {
// 清空DOM缓存,用来每次切换日期,更新DOM节点
this.calendarStatus = false
setTimeout(() => {
this.calendarStatus = true
this.setDisabledDayClass()
}, 100)
this.selectMonth = date.split('-').join().slice(5)
this.getCurrnetDays()
this.calendarValue = date
},
getCalendarList(data) {
this.calendarArrList = []
if (this.ticketDate.id) {
this.loading = true;
calendarList(this.ticketDate.id, this.selectMonth ? this.selectMonth : new Date().getMonth() + 1 ).then(response => {
this.calendarArrList = response.data
if (this.calendarArrList.length > 0) {
this.currneAlltDay.forEach((val) => {
const ticketDate = val.ticketDate
const matchingItem = this.calendarArrList.find(item => item.ticketDate.split('-').join('-').slice(5) === ticketDate);
// 维护过
if (matchingItem) {
val.id = matchingItem.id
val.ticketName = matchingItem.ticketName
val.ticketPrice = matchingItem.ticketPrice
val.createBy = matchingItem.createBy;
val.createTime = matchingItem.createTime;
val.delFlag = matchingItem.delFlag;
val.stock = matchingItem.stock;
val.ticketId = matchingItem.ticketId;
val.updateBy = matchingItem.updateBy;
val.updateTime = matchingItem.updateTime;
val.calendarFlagName = true;
} else {
if (val.ticketDate >= this.currentNewDate) {
val.ticketName = '待维护'
val.calendarFlagName = false;
}
}
})
} else {
this.currneAlltDay.forEach((item) => {
item.ticketName = '待维护'
})
}
this.loading = false;
});
} else {
this.currneAlltDay.forEach((item) => {
if (item.ticketDate >= this.currentNewDate) {
item.ticketName = '待维护'
}
})
}
},
getCurrentDay(item, date) {
// 修改
if (item.id) {
this.$refs.priceDialog.show(item, date)
} else {
// 新增
if (this.queryParams.ticketRegion) {
this.$refs.priceDialog.show(this.queryParams, date)
} else {
this.$modal.msgWarning('请先选择景区项目,再选择景区门票')
}
}
},
handleDateChange(date) {
if (date < new Date()) {
this.calendarValue = new Date(); // 重置为当前日期
} else {
this.calendarValue = date;
}
}
}
}
</script>
<style lang="scss" scoped>
::v-deep tbody {
font-size: 15px;
color: #757575;
border: 3px solid #dfe6ec;
}
.symbol {
font-size: 14px;
color: #ff4949;
margin: 4px 4px 0 0;
}
::v-deep .el-calendar-day {
height: 108px;
border: 2px solid #dfe6ec;
}
::v-deep .current + .el-calendar-day {
border: 1px;
}
::v-deep .el-calendar__header {
display: none;
}
::v-deep .el-calendar__body {
padding: 0;
}
::v-deep.calendar-day {
color: #7a7aa9;
}
.app-container {
.tips {
display: flex;
margin-bottom: 20px;
.symbol {
color: #ff4949;
}
.desc {
color: #ff4949;
}
}
.nameScenic {
color: #97a8be;
}
.el-calendar {
border-top: 1px solid #dfe6ec;
.is-pervDay {
height: 100%;
}
.mainContainer {
height: 100%;
}
.customerBox {
height: 75%;
display: flex;
align-items: center;
.tickContainer {
width: 100%;
display: flex;
align-items: center;
padding: 30px 10px;
border-radius: 5px;
justify-content: space-between;
.tickName {
width: 155px;
color: #606266;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tickPrice {
width: 80px;
text-align: right;
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
}
}
}
}
}
.default {
.tickContainer {
.tickName {
display: flex;
justify-content: center;
width: 100%!important;
}
}
}
::v-deep {
.el-calendar-table:not(.is-range) td.next,.el-calendar-table:not(.is-range) td.prev{
pointer-events: none;
background-color: #B0C9DD;
// border-bottom: 5px solid #dfe6ec;
// border-right: 5px solid #dfe6ec;
}
}
::v-deep thead th {
font-size: 13px;
font-weight: 800;
}
::v-deep td.disabledDays {
pointer-events: none;
background-color: rgba(0, 0, 0, .3);
color: #fff;
}
.money-init {
width: 100%;
text-align: right;
margin-bottom: 10px;
color: #606266;
font-weight: 600;
}
</style>