遇到一个需求,不用任何第三方库,只基于vue2实现一个日历组件,末尾附上我的代码,单文件,不要找文件长度的茬。下面是需求
- 样式是类似于window10日历
- 支持控制周一还是周日在第一列
- 支持鼠标滑动切换
- 支持单选,拖动鼠标多选,范围选择
- 支持年月日选择切换
- 支持传入选中数据
- 支持隐藏非本月日期
一共七个需求,不多。我的代码附上,欢迎批评指出不足
<template>
<!-- 外部 -->
<div class="Q-calendar" @mouseup.stop="onMouseUp" @mouseleave.stop="onMouseleave">
<!-- 头部 -->
<div class="Q-calendar-title" v-if="hideTitle">
<div class="Q-calendar-title-box" style="padding-left: 8px">
<div @click="onClickYears" class="Q-calendar-title-box-text">
{{ this.currentYear }}年
</div>
<div @click="onClickMonth" class="Q-calendar-title-box-text">
{{ this.currentMonth }}月
</div>
<!-- <div>{{this.currentDay}}号</div> -->
</div>
<slot name="Q-calendar-header-slot" v-if="isSlot===1||isSlot===2"></slot>
<div class="Q-calendar-title-box" v-if="isSlot===0 ||isSlot===2">
<div class="Q-calendar-title-box-text" @click="onClickUp">上</div>
<div class="Q-calendar-title-box-text" @click="onClickDown">下</div>
</div>
</div>
<div v-else class="Q-calendar-title">
<div class="Q-calendar-title-box" style="margin: 0 auto; font-weight: 700">
<div class="Q-calendar-title-box-text">{{ this.currentYear }}年</div>
<div class="Q-calendar-title-box-text">{{ this.currentMonth }}月</div>
</div>
</div>
<!-- 日历 -->
<transition name="change">
<div class="Q-calendar-day" v-if="hide === 1" @mousewheel="onMousewheel">
<!-- 周一到周日 -->
<div class="Q-calendar-week">
<p v-for="item in isMonday ? Monday : sun" :key="item">{{ item }}</p>
</div>
<div class="Q-calendar-box">
<!-- 上个月剩余天数 -->
<div class="Q-calendar-surplus" v-for="item in lastMonthDays" :key="'dayA' + item">
<span v-show="thisDate"> {{ item }}</span>
</div>
<!-- 当前月份天数 -->
<div v-for="item in currentMonthDays" :key="'dayB' + item.day" class="Q-calendar-current-month"
:style="cssProps" @click="onChangeDay(item)" :class="{
checked: item.checked,
nowCss:
new Date().getFullYear()+'' === item.year &&
(new Date().getMonth() + 1)+'' === item.month &&
new Date().getDate()+'' === item.day,
}" @mouseover="dragDay(item)" @mousedown="onMouseDown(item)">
<span> {{ item.day }}</span>
</div>
<!-- 下月余出 -->
<div class="Q-calendar-surplus" v-for="item in this.nextMonth()" :key="'dayC' + item">
<span v-show="thisDate">{{ item }}</span>
</div>
</div>
</div>
</transition>
<!-- 月 -->
<transition name="change">
<div class="Q-calendar-years" v-if="hide === 2" @mousewheel="onMousewheel">
<div class="Q-calendar-years-box">
<div v-for="item in clickMonth" :key="'monthA' + item.value" @click="onChangeMonth(item)" :class="{
nowCss: isNowYear && new Date().getMonth() + 1 === item.value,
}">
<span>{{ item.key }}</span>
</div>
<div class="Q-calendar-surplus" v-for="item in lastMonth" :key="'monthB' + item">
<span>{{ item }}</span>
</div>
</div>
</div>
</transition>
<!-- 年 -->
<transition name="change">
<div class="Q-calendar-years Q-calendar-year" v-if="hide === 3" @mousewheel="onMousewheel">
<div class="Q-calendar-years-box">
<div class="Q-calendar-surplus" v-for="item in lastYear" :key="'yearA' + item">
<span>{{ item }}</span>
</div>
<div v-for="item in thisYear" :key="'yearB' + item" @click="onChangeYear"
:class="{ nowCss: new Date().getFullYear() === item }">
<span>{{ item }}</span>
</div>
</div>
</div>
</transition>
</div>
</template>
<script>
/**
* 日历组件
* @description
* @property {Boolean} thisDate false 是否展示非本月份的日期
* @property {Boolean} hideTitle true 是否展示title
* @property {Boolean} multiSelect false 是否开启摁下鼠标进行多选
* @property {String} checkBGColor "#4152b3" 选中的背景色
* @property {String} checkColor "#ffffff" 选中的文字色
* @property {Boolean} isMonday true 是否从周一在开头
* @property {Number} selectionMethod 1 单选1,多选2,范围选3
* @property {Array} selectList [] 选中数据的数组
* @property {Number} isSlot 0 默认非插槽0,插槽需要传入1,插槽和默认上下选择同事存在2
*
*/
export default {
name: "QCalendar",
props: {
thisDate: {
type: Boolean,
default: false,
},
hideTitle: {
type: Boolean,
default: true,
},
multiSelect: {
type: Boolean,
default: false,
},
checkBGColor: {
type: String,
default: "#4152b3",
},
checkColor: {
type: String,
default: "#ffffff",
},
isMonday: {
type: Boolean,
default: true,
},
selectionMethod: {
type: Number,
default: 1,
},
selectList: {
type: Array,
default: () => []
},
isSlot: {
type: Number,
default: 0,
}
},
data() {
return {
isMouseDown: false,
arr: this.selectList,
isNowYear: true,
isNowMOnth: true,
hide: 1,
sun: ["日", "一", "二", "三", "四", "五", "六"],
Monday: ["一", "二", "三", "四", "五", "六", "日"],
clickMonth: [
{ key: "一月", value: 1 },
{ key: "二月", value: 2 },
{ key: "三月", value: 3 },
{ key: "四月", value: 4 },
{ key: "五月", value: 5 },
{ key: "六月", value: 6 },
{ key: "七月", value: 7 },
{ key: "八月", value: 8 },
{ key: "九月", value: 9 },
{ key: "十月", value: 10 },
{ key: "十一月", value: 11 },
{ key: "十二月", value: 12 },
],
lastMonth: ["一月", "二月", "三月", "四月"],
lastYear: [],
thisYear: [],
// 当前日
currentDay: new Date().getDate(),
// 当前月
currentMonth: new Date().getMonth() + 1,
// 当前年
currentYear: new Date().getFullYear(),
};
},
created() {
this.initParameter();
},
computed: {
cssProps() {
return {
"--background-color": this.checkBGColor,
"--color": this.checkColor,
};
},
// 当前月的天数
currentMonthDays() {
let dayLength = new Date(
this.currentYear,
this.currentMonth,
0
).getDate();
let arr = [];
for (let h = 0; h < dayLength; h++) {
let dataObj = {
year: this.currentYear + "",
month: this.currentMonth + "",
day: (h + 1) + "",
checked: false,
};
arr[h] = dataObj;
}
for (let p = 0; p < this.arr.length; p++) {
for (let k = 0; k < arr.length; k++) {
if ((this.arr[p].year === arr[k].year + "") && (this.arr[p].month === arr[k].month + "") && (this.arr[p].day === arr[k].day + "") && this.arr[p].checked) {
arr[k].checked = true
}
}
}
return arr;
},
// 获取上个月的剩余多少天
lastMonthDays() {
const lastLength = new Date(
this.currentYear,
this.currentMonth - 1,
0
).getDate();
let cutLength;
if (this.isMonday) {
cutLength = new Date(
this.currentYear,
this.currentMonth - 1,
0
).getDay();
} else {
cutLength = new Date(
this.currentYear,
this.currentMonth - 1,
1
).getDay();
}
let arr = [];
for (let h = lastLength - cutLength + 1; h <= lastLength; h++) {
arr.push(h);
}
return arr;
},
},
methods: {
onMousewheel(e) {
let evt = e || window.event; //考虑兼容性
evt.preventDefault();
if (evt.deltaY > 0) {
// console.log("向下滚动");
this.onClickDown();
} else {
// console.log("向上滚动");
this.onClickUp();
}
//检查事件
// console.log(evt.type, evt.deltaX, evt.deltaY, evt.deltaZ);
},
dragDay(dayObj) {
if (!this.multiSelect) {
return;
} else {
if (!this.isMouseDown) {
return;
} else {
this.onChangeDay(dayObj);
}
}
},
onMouseDown(dayObj) {
if (!this.multiSelect) {
return;
} else {
if (this.isMouseDown) this.onChangeDay(dayObj);
this.isMouseDown = true;
}
},
onMouseUp() {
// console.log('松开');
this.isMouseDown = false;
},
onMouseleave() {
// console.log('超出');
if (this.isMouseDown) {
this.isMouseDown = false;
}
},
// 点击多选标签
onChangeDay(val) {
// console.log(val,1111111111);
// 判断单选,多选,还是范围选,对应值1.2.3.
if (this.selectionMethod === 1) {
if (this.arr.length === 0) {
val.checked = true;
this.arr.push(val);
} else if (this.arr.length === 1) {
// console.log(this.arr[0],val,3333);
if ((this.arr[0].year === val.year) && (this.arr[0].month === val.month) && (this.arr[0].day === val.day)) {
this.arr = []
} else {
this.arr = []
val.checked = true;
this.arr.push(val);
}
} else {
return
}
} else if (this.selectionMethod === 2) {
if (val.checked) {
// 剔除
val.checked = false;
this.arr = this.arr.filter((ele) => {
return !(
ele.year === val.year &&
ele.month === val.month &&
ele.day === val.day
);
});
} else {
// 添加
val.checked = true;
this.arr.push(val);
}
} else if (this.selectionMethod === 3) {
// 范围选择,
if (this.arr.length === 0) {
val.checked = true;
this.arr.push(val);
} else if (this.arr.length === 1) {
val.checked = true;
this.arr.push(val);
const newArr1 = JSON.stringify(this.arr[0])
const newArr2 = JSON.stringify(this.arr[1])
// console.log(this.arr,'this.arr');
const arrS = this.getRangeDay(newArr1, newArr2)
this.arr = []
// this.arr.push(...arrS)
this.arr = arrS
} else {
this.arr = []
val.checked = true;
this.arr.push(val);
}
}
this.$emit("getSelectedData", this.arr);
},
// 获取日期范围内天数
getRangeDay(startDate, endDate) {
startDate = JSON.parse(startDate)
endDate = JSON.parse(endDate)
const result = [];
const db = new Date();
db.setUTCFullYear(startDate.year, startDate.month - 1, startDate.day);
const de = new Date();
de.setUTCFullYear(endDate.year, endDate.month - 1, endDate.day);
let smallDate
let bigDate
if (db.getTime() > de.getTime()) {
smallDate = de.getTime()
bigDate = db.getTime()
} else {
smallDate = db.getTime()
bigDate = de.getTime()
}
// console.log(smallDate,bigDate,6666666);
// const unixDb = db.getTime();
// const unixDe = de.getTime();
for (let k = smallDate; k <= bigDate;) {
result.push({
year: this.parseTime(k, "{y}"),
month: this.parseTime(k, "{m}"),
day: this.parseTime(k, "{d}"),
checked: true
});
k = k + 24 * 60 * 60 * 1000;
}
return result;
},
// 日期格式化
parseTime(time, pattern) {
if (arguments.length === 0 || !time) return null;
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
let date;
if (typeof time === 'object') {
date = time;
} else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time);
} else if (typeof time === 'string') {
time = time.replace(new RegExp(/-/gm), '/');
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000;
}
date = new Date(time);
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay(),
};
return format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
// @ts-ignore
let value = formatObj[key];
// Note: getDay() returns 0 on Sunday
if (key === 'a') {
return ['日', '一', '二', '三', '四', '五', '六'][value];
}
// if (result.length > 0 && value < 10) {
// value = '0' + value;
// }
return value || 0;
});
},
initParameter() {
let currentYear = this.currentYear - 1;
for (let p = 3; p >= 0; p--) {
this.lastYear[p] = currentYear--;
}
currentYear = this.currentYear;
for (let l = 0; l < 12; l++) {
this.thisYear[l] = currentYear++;
}
},
onChangeYear(val) {
this.hide = 2;
let currentYear = new Date().getFullYear();
this.currentYear = val.srcElement.innerText;
if (val.srcElement.innerText + "" === currentYear + "") {
this.isNowYear = true;
} else {
this.isNowYear = false;
}
},
onChangeMonth(val, vap) {
this.hide = 1;
let currentMonth = new Date().getMonth() + 1;
this.currentMonth = val.value;
if (val.value + "" === currentMonth + "") {
this.isNowMOnth = true;
} else {
this.isNowMOnth = false;
}
},
// 点击年
onClickYears(val) {
let currentYear = this.currentYear - 1;
for (let p = 3; p >= 0; p--) {
this.lastYear[p] = currentYear--;
}
this.hide = 3;
},
// 点击月
onClickMonth(val) {
this.hide = 2;
},
// 获取上个月的剩余多少天
nextMonth() {
const ac = 42 - this.currentMonthDays.length - this.lastMonthDays.length;
return ac;
},
// 上
onClickUp() {
let currentYear = new Date().getFullYear();
// let currentMonth= new Date().getMonth() + 1
if (this.hide === 1) {
if (this.currentMonth === 1) {
this.currentYear--, (this.currentMonth = 13);
}
this.currentMonth--;
} else if (this.hide === 2) {
this.currentYear--;
if (this.currentYear !== currentYear) {
this.isNowYear = false;
} else {
this.isNowYear = true;
}
} else {
this.switchingYear(1);
}
},
// 下
onClickDown() {
let currentYear = new Date().getFullYear();
if (this.hide === 1) {
// 日
if (this.currentMonth === 12) {
this.currentYear++, (this.currentMonth = 0);
}
this.currentMonth++;
} else if (this.hide === 2) {
// 月默认切换年
this.currentYear++;
if (this.currentYear !== currentYear) {
this.isNowYear = false;
} else {
this.isNowYear = true;
}
} else {
// 切换年的选择
this.switchingYear(2);
}
},
switchingYear(type) {
// 1上,2下
if (type === 1) {
// last最后一个为this的最后一个
let thisAnchor = this.lastYear[3] - 11;
let lastAnchor = this.lastYear[3] - 15;
this.thisYear = [];
for (let p = 0; p < 12; p++) {
this.thisYear[p] = thisAnchor++;
}
this.lastYear = [];
for (let l = 0; l < 4; l++) {
this.lastYear[l] = lastAnchor++;
}
} else if (type === 2) {
let anchor = this.thisYear[11] + 1;
this.lastYear = [];
for (let p = 3; p >= 0; p--) {
this.lastYear[p] = this.thisYear[11]--;
}
this.thisYear = [];
for (let l = 0; l < 12; l++) {
this.thisYear[l] = anchor++;
}
}
},
},
};
</script>
<style scoped lang="scss">
.change-enter-active,
.change-leave-active {
transition: opacity 0.5s;
}
.change-enter,
.change-leave-to
/* .fade-leave-active, 2.1.8 版本以下 */
{
opacity: 0;
}
.Q-calendar {
width: 100%;
height: 100%;
margin: 0 auto;
overflow: hidden;
background-color: #ffffff;
color: #000;
user-select: none;
border: 1px solid #4152b3;
.Q-calendar-title {
height: 50px;
width: 100%;
box-sizing: border-box;
display: flex;
justify-content: space-between;
div {
align-self: center;
}
.Q-calendar-button,
.top {
align-self: center;
}
.Q-calendar-title-box {
width: calc((100% / 7) * 2);
display: flex;
justify-content: space-around;
cursor: default;
.Q-calendar-title-box-text {
width: 50%;
text-align: center;
align-self: center;
}
.Q-calendar-title-box-text:hover {
// color:yellowgreen;
color: #4152b3;
font-weight: 700;
// background-color: #4152b3;
}
}
}
// rili
.Q-calendar-day {
height: calc(100% - 50px);
/* 周末 */
.Q-calendar-week {
display: flex;
justify-content: inherit;
cursor: default;
p {
display: flex;
justify-content: center;
width: calc(100% / 7);
box-sizing: border-box;
}
}
/* 日历内容 */
.Q-calendar-box {
display: flex;
justify-content: inherit;
flex-wrap: wrap;
width: 100%;
height: 80%;
div:hover {
color: yellowgreen;
font-weight: 700;
}
.Q-calendar-current-month {
box-sizing: border-box;
cursor: default;
// border: 1px solid pink;
}
.Q-calendar-current-month:hover {
// color: blueviolet;
color: #4152b3;
font-weight: 700;
font-size: 20px;
}
div {
display: flex;
justify-content: center;
width: calc(100% / 7);
height: calc(100% / 7);
span {
margin: auto;
}
}
p {
display: flex;
justify-content: center;
width: calc(100% / 7);
}
}
}
.Q-calendar-years {
height: calc(100% - 50px);
.Q-calendar-years-box {
// border: 1px solid pink;
display: flex;
justify-content: inherit;
flex-wrap: wrap;
height: 98%;
div {
display: flex;
box-sizing: border-box;
justify-content: center;
width: calc(100% / 4);
height: calc(100% / 4);
span {
margin: auto;
}
}
div:hover {
font-weight: 700;
color: yellowgreen;
}
}
}
}
/* //非本月时间内,或非本年内 */
.Q-calendar-surplus {
color: #898989;
}
.nowCss {
// border:1px solid pink;
background: #f1f3f4;
color: #40b8ff;
}
.checked {
span {
color: var(--color);
background-color: var(--background-color);
border-radius: 10px;
width: 50%;
height: 50%;
text-align: center;
line-height: 24px;
}
}
</style>