Vue 日历组件
(1) 简介
该日历组件是一个应用于移动端的日历组件采用了rem(文末附有rem.js)
(2) template
<template>
<div class="calendar">
<div class="calendar-year">
<span @click="chooseYear">{{showDate.year}}</span>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14px" height="7px">
<path fill-rule="evenodd" fill="#1e80d7" d="M7.000,7.001 L-0.001,-0.000 L14.001,-0.000 L7.000,7.001 Z"/>
</svg>
</div>
<div class="calendar-month">
<div class="month-left" @click="prevMonth">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12px" height="18px">
<path fill-rule="evenodd" fill="rgb(30, 128, 215)" d="M1.084,10.100 C3.906,12.628 6.728,15.155 9.551,17.680 C10.713,18.721 12.445,17.031 11.279,15.987 C8.767,13.739 6.255,11.491 3.744,9.242 C6.268,6.915 8.792,4.587 11.316,2.259 C12.466,1.200 10.734,-0.491 9.588,0.567 C6.753,3.180 3.918,5.793 1.084,8.407 C0.614,8.841 0.603,9.671 1.084,10.100 Z"/>
</svg>
</div>
<div class="month-center">
<div class="month-content">
<transition :name="fadeDateType">
<span :key="showDate.month">{{showDate.month}}月</span>
</transition>
</div>
</div>
<div class="month-right" @click="nextMonth">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12px" height="18px">
<path fill-rule="evenodd" fill="rgb(30, 128, 215)" d="M10.916,10.100 C8.093,12.628 5.272,15.155 2.449,17.680 C1.287,18.721 -0.445,17.031 0.721,15.987 C3.233,13.739 5.745,11.491 8.256,9.242 C5.732,6.915 3.208,4.587 0.683,2.259 C-0.467,1.200 1.266,-0.491 2.412,0.567 C5.247,3.180 8.081,5.793 10.916,8.407 C11.385,8.841 11.397,9.671 10.916,10.100 Z"/>
</svg>
</div>
</div>
<div class="calendar-content">
<!--星期-->
<div class="calendar-day">
<ul>
<li>日</li>
<li>一</li>
<li>二</li>
<li>三</li>
<li>四</li>
<li>五</li>
<li>六</li>
</ul>
</div>
<!--时间-->
<div class="calendar-dayDate">
<transition :name="fadeDateType">
<ul class="calendar-date" v-swipeleft="nextMonth" v-swiperight="prevMonth">
<li v-for="item in showDate.allDate" :class="{'calendar-now-month': item.nowMonth,'calendar-can-click': item.canClick,'isNowDay': item.isNowDay}">
<span v-if="item.canClick" @click="dealClick(item.intactDate)">{{item.dayDate}}</span>
<span v-if="!item.canClick">{{item.dayDate}}</span>
</li>
</ul>
</transition>
</div>
</div>
<!--弹出框-->
<div class="calendar-mask" v-if="showMask" @click="closeMask" @touchmove.prevent></div>
<div class="calendar-box" :style="{height:b_height}">
<div class="year-title">
<button type="button" class="year-list-cancel"><span @click="closeMask">取消</span></button>
<button type="button" class="year-list-ok"><span @click="okYear">确定</span></button>
</div>
<div class="year-list">
<transition :name="fadeUlType">
<div class="show-ul" v-swipeup="nextYear" v-swipedown="prevYear">
<ul :style="{'top':b_top + 'rem'}">
<li v-for="item in showYearMonth" :class="{'active': item.activeYear}" :key="item.year" @click="dealClickYear(item)">{{item.year}}</li>
</ul>
</div>
</transition>
<div class="center-line"></div>
</div>
</div>
</div>
</template>
(3) script
<script>
// 初始化星期
const weekJson = {
0: "日",
1: "一",
2: "二",
3: "三",
4: "四",
5: "五",
6: "六"
}
// 初始化月份 -- 兼容ie
const monthJson = {
"01": "一",
"02": "二",
"03": "三",
"04": "四",
"05": "五",
"06": "六",
"07": "七",
"08": "八",
"09": "九",
"10": "十",
"11": "十一",
"12": "十二"
}
// 闰年月份判断
const leapYeareMonth = [31,29,31,30,31,30,31,31,30,31,30,31]
// 平年月份判断
const commonYearMonth = [31,28,31,30,31,30,31,31,30,31,30,31]
// 用于touch事件
function vueTouch(el,binding,type,vnode){
let _this = this;
this.obj = el;
this.binding = binding;
this.touchType = type;
this.vueTouches = {x: 0,y: 0};
this.vueMoves = true;
this.vueLeave = true;
this.longTouch = true;
this.vueCallBack = typeof(binding.value) == "object" ? binding.value.fn : binding.value;
this.obj.addEventListener("touchstart",function(e){
_this.start(e);
});
this.obj.addEventListener("touchend",function(e){
_this.end(e);
});
this.obj.addEventListener("touchmove",function(e){
e.preventDefault();
_this.move(e);
});
vnode.key = this.randomString();
}
vueTouch.prototype={
start(e){
this.vueMoves = true;
this.vueLeave = true;
this.longTouch = true;
this.vueTouches = {x: e.changedTouches[0].pageX,y: e.changedTouches[0].pageY};
this.time = setTimeout(function(){
if(this.vueLeave && this.vueMoves){
this.touchType == "longtap" && this.vueCallBack(this.binding.value,e);
this.longTouch = false;
};
}.bind(this),1000);
},
end(e){
var disX = e.changedTouches[0].pageX-this.vueTouches.x;
var disY = e.changedTouches[0].pageY-this.vueTouches.y;
clearTimeout(this.time);
if(Math.abs(disX) > 10 || Math.abs(disY)>10){
this.touchType == "swipe" && this.vueCallBack(this.binding.value,e);
if(Math.abs(disX) > Math.abs(disY)){
if(disX > 10){
this.touchType == "swiperight" && this.vueCallBack(this.binding.value,e);
};
if(disX < -10){
this.touchType == "swipeleft" && this.vueCallBack(this.binding.value,e);
};
}else{
if(disY > 10){
this.touchType == "swipedown" && this.vueCallBack(this.binding.value,e);
};
if(disY < -10){
this.touchType == "swipeup" && this.vueCallBack(this.binding.value,e);
};
};
}else{
if(this.longTouch && this.vueMoves){
this.touchType == "tap" && this.vueCallBack(this.binding.value,e);
this.vueLeave = false;
};
};
},
move(e){
this.vueMoves = false;
},
randomString(){
var len = 10;
var $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
var maxPos = $chars.length;
var pwd = '';
for (var i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
}
export default{
data:() => ({
fadeDateType: "", // 动画的name
showYearMonth: [], // 存放显示年份区间及月份
showDefaultDate: "",
startDate: "",
endDate: "",
showDate: {
date: "", // 完整的日期显示 -- 默认日期
year: "", // 年
month: "", // 月 -- 中文
allDate: [] // 需要显示的日期
/*
{
intactDate: "", // 完整的时间 -- 2018-06-11
dayDate: "", // 日期,默认显示 "12"
nowMonth: false, // 是否为显示月
canClick: false, // 是否有比赛
isNowDay: false, // 是否是今天
activeDay: false // 是否被点击
}
*/
},
// 弹出框
showMask: false,
showBox: false,
b_height:"0", // 初始化弹出框的高
b_top: 0, // 弹出框中ul的top值
fadeUlType: "" // 执行动画的名
}),
props:{
// 日期最小值 -- 必填
minDate: {
type: String,
required: true
},
// 日期最大值 -- 必填
maxDate: {
type: String,
required: true
},
// 设置默认时间 -- 选填
defaultDate: {
type: String
},
// 显示区域比赛日 -- 数据格式 "2018-07-11"
gameDate: {
type: Array // 可设置所有天数可点 -- >设置数组首位为All ["all"]
}
},
created(){
// 初始化时间
this.showDefaultDate = this.defaultDate ? this.defaultDate : "";
this.initDate();
},
watch:{
showMask: function(){
this.forbidScroll(this.showMask);
}
},
methods:{
// 初始化日期
initDate(){
this.startDate = this.minDate.split("-"); //开始时间
this.endDate = this.maxDate.split("-"); // 结束时间
this.dealMonthDate();
this.dealYearMonth();
},
// 总年份处理,总月份处理
dealYearMonth(){
let allMonth = ["01","02","03","04","05","06","07","08","09","10","11","12"];
let num = this.endDate[0] - this.startDate[0];
if(num == 0){
// 只有一年
let item = {
year: this.startDate[0],
month: allMonth.slice(allMonth.indexOf(this.startDate[1]),allMonth.indexOf(this.endDate[1]) + 1),
activeYear: true
}
this.showYearMonth.push(item);
}else if(num == 1){
// 只有开始和结束两年
let itemStart = {
year: this.startDate[0],
month: allMonth.slice(allMonth.indexOf(this.startDate[1])),
activeYear: false
}
let itemEnd = {
year: this.endDate[0],
month: allMonth.slice(0,allMonth.indexOf(this.endDate[1]) + 1),
activeYear: false
}
this.showYearMonth.push(itemStart);
this.showYearMonth.push(itemEnd);
}else{
// 有多年
let itemStart = {
year: this.startDate[0],
month: allMonth.slice(allMonth.indexOf(this.startDate[1])),
activeYear: false
}
let itemEnd = {
year: this.endDate[0],
month: allMonth.slice(0,allMonth.indexOf(this.endDate[1]) + 1),
activeYear: false
}
this.showYearMonth.push(itemStart);
for(let i = 0; i < this.endDate[0] - this.startDate[0] - 1; i++){
let item = {
year: Number(this.startDate[0]) + 1 + i,
month: allMonth,
activeYear: false
}
this.showYearMonth.push(item);
}
this.showYearMonth.push(itemEnd);
}
this.amendActiveYear();
},
// 处理所有年份中的焦点年份 -- 修改activeYear
amendActiveYear(){
for(let i = 0; i < this.showYearMonth.length; i++){
let item = this.showYearMonth[i];
item.activeYear = item.year == this.showDate.date[0] ? true : false;
}
},
// 处理显示天数
dealMonthDate(){
this.showDate.allDate = [];
// 获取当前时间
let date1 = new Date();
let nowDate = [date1.getFullYear(),((date1.getMonth() + 1) > 9 ? (date1.getMonth() + 1) : "0" + (date1.getMonth() + 1)),(date1.getDate() > 9 ? date1.getDate() : "0" + date1.getDate())];
// 如果没有设置默认时间,默认为当天 -- > 当月 -- > 当年
this.showDate.date = this.showDefaultDate ? this.showDefaultDate.split("-") : nowDate;
this.showDate.year = this.showDate.date[0];
this.showDate.month = monthJson[this.showDate.date[1]];
// 设置总显示天数
let showDayNum = 0;
let numDay = this.judgeYear(this.showDate.date[0],Number(this.showDate.date[1])); // 显示月天数
showDayNum += numDay;
// 获取显示年月
let showDateFirstDay = this.showDate.date[0] + "-" + this.showDate.date[1] + "-" + "01"; // 显示月开始
let showDateEndDay = this.showDate.date[0] + "-" + this.showDate.date[1] + "-" + numDay; // 显示月结束
// 判断星期 , 获取显示区域第一天
let date = new Date(showDateFirstDay);
let _firstDay = date.getDay();
showDayNum = showDayNum + date.getDay() + (6 -new Date(showDateEndDay).getDay());
date.setDate(date.getDate() - date.getDay());
for(let i = 0; i < showDayNum; i++){
let _date = date.getFullYear() + "-" + ((date.getMonth() + 1) > 9 ? (date.getMonth() + 1) : "0" + (date.getMonth() + 1)) + "-" + (date.getDate() > 9 ? date.getDate() : "0" + date.getDate());
let _dateMsg = {
intactDate: _date,
dayDate: date.getDate(),
nowMonth: false,
canClick: this.dateCanClick(_date),
isNowDay: this.judgeNowDay(_date)
}
if(i >= _firstDay && i < numDay + _firstDay){
_dateMsg.nowMonth = true;
}
this.showDate.allDate.push(_dateMsg);
// 设置下一天
date.setDate(date.getDate() + 1);
}
},
// 检测是否可点 -- 用于判断当天是否有比赛
dateCanClick(date){
if(this.gameDate == undefined){
return false;
}else{
if(this.gameDate.indexOf(date) > -1){
return true;
}else{
return false;
}
}
},
// 判断是否是今天
judgeNowDay(date){
let date1 = new Date();
let _date = date1.getFullYear() + "-" + ((date1.getMonth() + 1) > 9 ? (date1.getMonth() + 1) : "0" + (date1.getMonth() + 1)) + "-" + (date1.getDate() > 9 ? date1.getDate() : "0" + date1.getDate());
if(_date == date){
return true;
}else{
return false;
}
},
// 判断闰平年
judgeYear(year,month){
if(year % 4 == 0 && year % 100 != 0){
// 闰年
return leapYeareMonth[month - 1];
}else if(year % 400 == 0){
// 闰年
return leapYeareMonth[month - 1];
}else{
// 平年
return commonYearMonth[month - 1];
}
},
// 上一月
prevMonth(){
if(this.judgeBorder("prev")){
return;
}
this.fadeDateType = "fadeDatePrev";
let showDate = this.showDate.date[0] + "-" + this.showDate.date[1] + "-" + "01";
let date = new Date(showDate);
date.setMonth(date.getMonth() - 1, 1);
this.showDefaultDate = date.getFullYear() + "-" + ((date.getMonth() + 1) > 9 ? (date.getMonth() + 1) : "0" + (date.getMonth() + 1)) + "-" + (date.getDate() > 9 ? date.getDate() : "0" + date.getDate());
this.dealMonthDate();
},
// 下一月
nextMonth(){
if(this.judgeBorder("next")){
return;
}
this.fadeDateType = "fadeDateNext";
let showDate = this.showDate.date[0] + "-" + this.showDate.date[1] + "-" + "01";
let date = new Date(showDate);
date.setMonth(date.getMonth() + 1, 1);
this.showDefaultDate = date.getFullYear() + "-" + ((date.getMonth() + 1) > 9 ? (date.getMonth() + 1) : "0" + (date.getMonth() + 1)) + "-" + (date.getDate() > 9 ? date.getDate() : "0" + date.getDate());
this.dealMonthDate();
},
// 边界判断
judgeBorder(ele){
let borderYear = [this.minDate.split("-")[0],this.maxDate.split("-")[0]];
let borderMonth = [this.minDate.split("-")[1],this.maxDate.split("-")[1]];
let nowYear = this.showDate.date[0];
let nowMonth = this.showDate.date[1];
let monthFlag = 0;
let returnFlag = true;
if(ele == "prev"){
// 上一月
monthFlag = -1;
}else{
// 下一月
monthFlag = 1;
}
// 设定时间
let date = new Date((nowYear + "-" + nowMonth + "-" + "01"));
date.setMonth(date.getMonth() + monthFlag,"01");
for(let i = 0; i < this.showYearMonth.length; i++){
let item = this.showYearMonth[i];
if(item.year == date.getFullYear()){
let _month = date.getMonth() + 1 > 9 ? (date.getMonth() + 1) + "" : "0" + (date.getMonth() + 1);
if(item.month.indexOf(_month) > -1){
returnFlag = false;
break;
}
}
}
return returnFlag;
},
// 点击选择年份、月份
chooseYear(){
this.fadeDateType = "";
this.showMask = true;
this.b_height = "5rem";
this.calculateUlTop();
},
// 计算当前显示的ul top
calculateUlTop(){
for(let i = 0; i < this.showYearMonth.length; i++){
if(this.showYearMonth[i].activeYear){
this.b_top = 2 * 0.6 - i * 0.6;
return;
}
}
},
// 年份上滑
nextYear(){
if(this.showYearMonth[this.showYearMonth.length - 1].year == Number(this.showDate.date[0])) return;
this.fadeUlType = "fadeUlPrev";
this.showDate.date[0] = Number(this.showDate.date[0]) + 1;
this.amendActiveYear();
this.calculateUlTop();
},
// 年份下滑
prevYear(){
if(this.showYearMonth[0].year == Number(this.showDate.date[0])) return;
this.fadeUlType = "fadeUlNext";
this.showDate.date[0] = Number(this.showDate.date[0]) - 1;
this.amendActiveYear();
this.calculateUlTop();
},
// 点击某一年
dealClickYear(item){
if(!item.activeYear) return;
this.showDefaultDate = item.year + "-" + this.showDate.date[1] + "-" + this.showDate.date[2];
this.dealMonthDate();
// 关闭弹出框
this.closeMask();
},
// 点击确认按钮
okYear(){
// 获取当前的activeYear
console.log(this.showYearMonth)
for(let i = 0; i < this.showYearMonth.length; i++){
let item = this.showYearMonth[i];
if(item.activeYear){
this.dealClickYear(item);
break;
}
}
},
// 处理点击某一天
dealClick(date){
// 传递当前日期给父组件
this.$emit("clickDate",date);
},
// 关闭弹出框
closeMask(){
this.showMask = false;
this.b_height = "0";
},
// 禁止滚动事件
forbidScroll(isPin){
if(isPin){
document.body.style.height = '100vh';
document.body.style['overflow-y'] = 'hidden';
}else{
document.body.style.height = 'unset';
document.body.style['overflow-y'] = 'auto';
}
}
},
directives:{
// 轻点
"tap":{
bind(el,binding,vnode){
new vueTouch(el,binding,"tap",vnode);
}
},
// 左滑
"swipeleft":{
bind(el,binding,vnode){
new vueTouch(el,binding,"swipeleft",vnode);
}
},
// 右滑
"swiperight":{
bind(el,binding,vnode){
new vueTouch(el,binding,"swiperight",vnode);
}
},
// 上滑
"swipeup":{
bind(el,binding,vnode){
new vueTouch(el,binding,"swipeup",vnode);
}
},
// 下滑
"swipedown":{
bind(el,binding,vnode){
new vueTouch(el,binding,"swipedown",vnode);
}
}
}
}
</script>
(4) style
<style>
.calendar{
width:5.68rem;
height:5.74rem;
margin:0 auto;
box-sizing:border-box;
padding:0.18rem;
overflow:hidden;
}
.calendar .calendar-year{
width:5.32rem;
height:0.68rem;
line-height:0.68rem;
text-align:center;
font-size:0.32rem;
color:#1e80d7;
}
.calendar .calendar-month{
width:5.32rem;
height:0.5rem;
text-align:center;
line-height:0.5rem;
font-size:0.26rem;
color:#1e80d7;
overflow:hidden;
}
.calendar .calendar-month .month-left{
width:0.76rem;
height:0.5rem;
float:left;
}
.calendar .calendar-month .month-center{
width:3.8rem;
height:0.5rem;
float:left;
}
.calendar .month-center .month-content{
position:relative;
width:1rem;
height:100%;
margin:0 auto;
text-align:center;
}
.calendar .month-content span{
display:block;
position:absolute;
top:0;
left:0;
width:100%;
height:100%;
}
.calendar .calendar-month .month-right{
width:0.76rem;
height:0.5rem;
float:left;
}
.calendar .calendar-content{
width:5.32rem;
box-sizing:border-box;
font-size:0.22rem;
}
.calendar .calendar-content .calendar-dayDate{
position:relative;
width:100%;
height:3.6rem;
}
.calendar .calendar-content ul{
width:100%;
overflow:hidden;
}
.calendar .calendar-content .calendar-dayDate ul{
position:absolute;
width:100%;
height:100%;
}
.calendar .calendar-content li{
width:0.76rem;
height:0.6rem;
text-align:center;
line-height:0.6rem;
box-sizing:border-box;
float:left;
}
.calendar .calendar-content li span{
display:block;
width:0.4rem;
height:0.4rem;
margin:0.1rem auto;
line-height: 0.4rem;
border-radius:50%;
box-sizing:border-box;
}
.calendar .calendar-content .calendar-day{
color:#1e80d7;
}
.calendar-date{
color:#a8a8a8;
}
.calendar-now-month{
color:#62676B;
}
.calendar-can-click span{
border:1px solid #EBEBEB;
}
.calendar-can-click span.click-active{
border:1px solid #1e80d7;
}
.isNowDay span{
background:#1E80D7;
color:#fff;
}
/*====日历动画====*/
/*prevMonth*/
.fadeDatePrev-enter-active,.fadeDatePrev-leave-active{
transition: all .3s ease-in-out;
}
.fadeDatePrev-enter{
transform: translateX(-3rem);
opacity: 0;
}
.fadeDatePrev-leave-active{
transform: translateX(3rem);
opacity: 0;
}
/*nextMonth*/
.fadeDateNext-enter-active,.fadeDateNext-leave-active{
transition: all .3s ease-in-out;
}
.fadeDateNext-enter{
transform: translateX(3rem);
opacity: 0;
}
.fadeDateNext-leave-active{
transform: translateX(-3rem);
opacity: 0;
}
/*==== 弹出框 ====*/
.calendar-mask{
position:fixed;
left:0;
top:0;
width:100%;
height:100%;
z-index:100;
background:rgba(0,0,0,.25);
}
.calendar-box{
position:fixed;
left:0;
bottom:0;
width:100%;
height:0;
overflow:hidden;
background:#fff;
z-index:101;
transition:height .5s ease;
}
.calendar-box .year-title{
width:100%;
height:0.78rem;
background:#F9F9F9;
overflow:hidden;
box-shadow: 1px 1px 10px rgba(0,0,0,.2);
}
.calendar-box .year-title button{
display:block;
height:100%;
width:1.1rem;
text-align:center;
line-height:0.8rem;
font-size:0.24rem;
background:rgba(0,0,0,0);
outline:none;
}
.calendar-box .year-title .year-list-cancel{
float:left;
}
.calendar-box .year-title .year-list-ok{
float:right;
}
.year-list{
position: relative;
margin:0.6rem 0;
height: 3rem;
overflow:hidden;
}
.year-list .center-line{
position: absolute;
top: 50%;
left: 0;
width: 100%;
margin-top: -0.3rem;
height: .6rem;
border-top: 1px solid #E6E6E6;
border-bottom: 1px solid #E6E6E6;
z-index:1;
}
.year-list .show-ul{
width:100%;
height:100%;
position:relative;
z-index:2;
}
.year-list ul{
position: absolute;
top: 0;
left: 0;
width: 100%;
text-align: center;
transition: all .5s ease;
}
.year-list li{
width:100%;
height: .6rem;
line-height: .6rem;
font-size:0.25rem;
color:#555;
}
.year-list li.active{
font-size:0.28rem;
color:#000;
}
.fadeUlPrev-enter-active,.fadeUlPrev-leave-active{
transition: all .1s ease-in-out;
}
.fadeUlPrev-enter{
opacity:0;
transform: translateY(0.6rem);
}
.fadeUlPrev-leave-active{
opacity:0;
transform: translateY(-0.6rem);
}
.fadeUlNext-enter-active,.fadeUlNext-leave-active{
transition: all .1s ease-in-out;
}
.fadeUlNext-enter{
opacity:0;
transform: translateY(-0.6rem);
}
.fadeUlNext-leave-active{
opacity:0;
transform: translateY(0.6rem);
}
</style>
(5) rem.js
;(function(doc, win) {
var docEl = doc.documentElement,
resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
recalc = function() {
var clientWidth = docEl.clientWidth;
if(!clientWidth) return;
docEl.style.fontSize = 100 * (clientWidth / 640) + 'px';
console.log(docEl.style.fontSize)
};
if(!doc.addEventListener) return;
win.addEventListener(resizeEvt, recalc, false);
doc.addEventListener('DOMContentLoaded', recalc, false);
})(document, window);
自用,持续更新中...