【uniapp3】分享一个自己写的h5日历组件
简言
分享一下自己基于uniapp写的日历组件。如果不太满足你的需求,可以自己改造。
日历
实现分析:
- 页面显示 - 分为顶部显示和日历显示,我这里做了多行和单行显示两种情况,主要是当时看着手机的日历做的,手机上的日历单行和多行显示切换特别丝滑,但是我没实现出来。(我觉得限制原因是当时水平不够,再加上滚动用的uniapp的swipper组件,不能定制化实现)。
- 分屏滚动 - 使用 uniapp的swipper组件,我这里,单行使用这个月周+上个月最后一周+下个月最后一周数据,多行使用这个月+上个月+下个月数据;这样处理的原因是在进行月份切换的时候可以先显示数据然后进行数据更新,实现无感无限切换滚动。
- 各种事件 - 点击日期发送选中日期的数据。
- 数据逻辑 - 主要是先确定你想要的数据结构,然后以这个为单位组装成行(周)数组或多行(月)数组。
没有自己实现滚动,也算是取了巧。感觉难点就是日期的数据处理 和 滚动,已经单多行切换了。
代码: uniapp写的 vue2和vue3应该都能用,样式注意使用了sass。
<template>
<view class="calendar">
<view class="wrapper">
<slot
name="top"
:nowMonthText="nowMonthText"
:pickerMonth="pickerMonth"
:prevMonth="prevMonth"
:nextMonth="nextMonth"
>
<view class="top">
<view class="month-box">
<view class="month-text">
<view class="uni-input" @tap="pickerMonth">{{
nowMonthText
}}</view>
</view>
<view class="back-today" @tap="goToday">回到今天</view>
</view>
<view class="top-left">
<view class="icon-arrow arrow-left" @tap="previousFn"></view>
<view class="icon-arrow arrow-right" @tap="nextFn"></view>
</view>
</view>
</slot>
<view class="calender-box">
<view class="head-title">
<view
class="head-title-item"
v-for="v in calenderTitleList"
:key="v"
>{{ v }}</view
>
</view>
<!-- 一行显示 -->
<swiper
v-if="showType === 1"
class="row-swiper"
circular
:disable-programmatic-animation="true"
:duration="duration"
:current="swiperCurrent"
@change="swipereChangFn"
>
<swiper-item
v-for="(v, index) in calenderRowDaysList"
:key="index"
:item-id="v.label"
>
<view class="row-days-list">
<view
class="days-list-item"
v-for="(item, i) in v.value"
:key="`${index}-${i}`"
:class="{
used: item.used,
}"
>
<view
class="label"
v-if="(isNowMonth && item.isNowMonth) || !isNowMonth"
:class="{
disabled: item.disabled,
}"
@tap="clickDay(item)"
>
<view
class="text"
:class="{
'active-item':
nowSelectDay && nowSelectDay['time'] === item.time,
'active-item--disabled':
nowSelectDay &&
nowSelectDay['time'] === item.time &&
item.disabled,
'today-text':
item.label === '今' &&
nowSelectDay &&
nowSelectDay['time'] !== item.time &&
!item.disabled,
}"
>
{{ item.label }}
</view>
<view
v-show="!item.disabled && item.state"
class="state-item text-state-item"
>
</view>
<view
class="item-adjust"
v-if="!item.disabled && item.adjust && item.adjust.value"
:class="{ 'text-state-leave': item.adjust.value == '1' }"
>
{{ item.adjust.value == "1" ? "休" : "课" }}
</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
<!-- 全行显示 -->
<swiper
v-else
class="all-swiper"
circular
:disable-programmatic-animation="true"
:duration="duration"
@change="swipereChangFn"
:current="swiperCurrent"
:class="{
'six-height':
swiperDaysList[0] && swiperDaysList[0].value.length / 7 === 6,
}"
>
<swiper-item
v-for="(v, index) in swiperDaysList"
:key="index"
:item-id="v.label"
>
<view class="days-list">
<view
class="days-list-item"
v-for="(item, i) in v.value"
:key="`${index}-${i}`"
:class="{
used: item.used,
}"
>
<view
class="label"
v-if="(isNowMonth && item.isNowMonth) || !isNowMonth"
:class="{
disabled: item.disabled,
}"
@tap="clickDay(item)"
>
<view
class="text"
:class="{
'active-item':
nowSelectDay && nowSelectDay['time'] === item.time,
'active-item--disabled':
nowSelectDay &&
nowSelectDay['time'] === item.time &&
item.disabled,
'today-text':
item.label === '今' &&
nowSelectDay &&
nowSelectDay['time'] !== item.time &&
!item.disabled,
}"
>
{{ item.label }}
</view>
<view
v-show="!item.disabled && item.state"
class="state-item"
></view>
<view
class="item-adjust"
v-if="!item.disabled && item.adjust && item.adjust.value"
:class="{ 'text-state-leave': item.adjust.value == '1' }"
>
{{ item.adjust.value == "1" ? "休" : "课" }}
</view>
</view>
</view>
</view>
</swiper-item>
</swiper>
<view v-if="!hideArrow" class="arrow-wrapper" @click="showTypeChange">
<view
class="arrow-left"
:class="{ 'arrow-left--up': showType === 0 }"
></view>
<view
class="arrow-right"
:class="{ 'arrow-right--up': showType === 0 }"
></view>
</view>
</view>
<view class="content">
<slot :value="nowSelectDay"></slot>
</view>
</view>
<!-- 选择月份 -->
<uni-popup ref="monthPopup" :type="'bottom'">
<view class="month-popup-box">
<view class="month-top">
<view class="cancel-text" @tap="cancelDateFn">取消</view>
<view class="ok-text" @tap="sucessDate">完成</view>
</view>
<picker-view
:value="selectValue"
@change="bindChange"
class="picker-view"
>
<picker-view-column>
<view class="item" v-for="(item, index) in years" :key="index"
>{{ item }}年</view
>
</picker-view-column>
<picker-view-column>
<view class="item" v-for="(item, index) in months" :key="index"
>{{ item }}月</view
>
</picker-view-column>
</picker-view>
</view>
</uni-popup>
</view>
</template>
<script>
export default {
props: {
isNowMonth: {
// 是否只显示当前月日历值
type: Boolean,
default: false,
},
limitNowMoth: {
// 限制只有当月
type: Boolean,
default: false,
},
showRowType: {
type: Boolean,
default: true,
},
dateStateList: {
// 日历中的状态 [{date:'2023/12/1',value:1}]
type: Array,
default: () => [],
},
dateAdjustList: {
// 日历中的调课状态 [{date:'2023/12/1',value:1}] 1为休,2为调课
type: Array,
default: () => [],
},
defaultValue: {
type: String,
default: "",
},
hideArrow: {
type: Boolean,
default: false,
},
},
emits: ["ok", "cancel", "changeMonth"],
data() {
const date = new Date();
const years = [];
const year = date.getFullYear();
const months = [];
const month = date.getMonth() + 1;
const day = date.getDate();
for (let i = 1990; i <= date.getFullYear() + 30; i++) {
years.push(i);
}
for (let i = 1; i <= 12; i++) {
months.push(i);
}
return {
monthValue: [years.findIndex((item) => item === year), month - 1], // 选择月份值
years,
year, //当前年
months,
month, // 当前月
day, // 当前日
selectValue: [], // 月份选择器选中的值
// 日历
calenderTitleList: ["一", "二", "三", "四", "五", "六", "日"],
swiperDaysList: [], // swiper全行显示列表 0-当月 1-下月 2-上月
swiperCurrent: 0, // swiper当前显示索引
duration: 500, // 动画时长
calenderRowDaysList: [], // rowSwiper行显示列表
nowSelectDay: null, // 当前选中值
showType: 0, // 0 全部行显示 1,1行显示
isJust: false, // 是否是校准
};
},
computed: {
nowMonthText() {
const [yearIndex, monthIndex] = this.monthValue;
let str = `${
this.years[yearIndex]
? this.years[yearIndex]
: this.years[this.years.length - 1]
}年${this.months[monthIndex]}月`;
this.$emit("changeMonth", [
this.years[yearIndex],
this.months[monthIndex].toString().padStart(2, "0"),
]);
return str;
},
},
watch: {
showRowType: {
handler(val) {
if (val) {
this.showType = 1;
} else {
this.showType = 0;
}
},
immediate: true,
},
defaultValue(val) {
if (val && val !== "") {
this.nowSelectDay = {
time: Date.parse(val),
};
}
},
},
mounted() {
this.updateCalender();
},
methods: {
showTypeChange() {
this.showType = 1 - this.showType;
// this.goToday();
this.updateCalender(false);
this.initCurrent();
},
// 点击天数
clickDay(item) {
if (item.disabled) return;
this.nowSelectDay = item;
this.$emit("ok", this.nowSelectDay);
},
pickerMonth() {
if (this.limitNowMoth) return;
// 同步当前月份值
this.selectValue = this.monthValue.map((item) => item);
this.$refs.monthPopup.open();
},
// 选择月份赋值
bindChange({ target }) {
this.selectValue = target.value;
},
// 选择月份完成
sucessDate() {
this.monthValue = this.selectValue.map((item) => item);
this.updateCalender(false);
if (this.showType === 1) {
// 一行显示
// 默认回到第一项
this.initCurrent();
}
this.$refs.monthPopup.close();
},
// 取消选择月份
cancelDateFn() {
this.$refs.monthPopup.close();
},
swipereChangFn(event) {
if (this.isJust) return;
const { currentItemId, current } = event.detail;
this.swiperCurrent = current;
switch (currentItemId) {
case "next":
this.nextMonth();
break;
case "pre":
this.prevMonth();
break;
}
// 月份改变后定位
if (this.showType === 0) {
setTimeout(() => this.initCurrent(), 50);
} else {
if (currentItemId === "next") {
setTimeout(() => this.initCurrent(), 50);
} else if (currentItemId === "pre") {
const CalenderDaysList = this.getDayList(
this.years[this.monthValue[0]],
this.months[this.monthValue[1]]
);
let preArr = this.group(CalenderDaysList, 7);
setTimeout(() => this.initCurrent(preArr.length - 1), 50);
}
}
},
// 默认回到第一项
initCurrent(index = 0) {
if (index === 0) this.isJust = true;
this.duration = 0;
this.swiperCurrent = index;
setTimeout(() => {
this.isJust = false;
this.duration = 500;
}, 0);
},
// 回到今天
goToday() {
const { years, year, month, day } = this;
const nowDayDate = `${year}/${month}/${day}`;
this.monthValue = [years.findIndex((item) => item === year), month - 1];
this.updateCalender();
// 默认点击今天
this.nowSelectDay = {
value: day, // 值
label: "今", // 描述
disabled: false, // 禁用
isNowMonth: true,
date: nowDayDate,
time: new Date(year, month - 1, day).valueOf(),
state: this.dateStateList.find((data) => data.date === nowDayDate),
adjust: this.dateAdjustList.find((data) => data.date === nowDayDate),
};
// 生成日历碰到今日会触发,这里注释掉
// this.$emit("ok", this.nowSelectDay);
},
//
previousFn() {
if (this.swiperCurrent !== 0) {
this.swiperCurrent--;
} else {
this.swipereChangFn({
detail: {
currentItemId: "pre",
current: 0,
},
});
}
},
nextFn() {
this.swiperCurrent++;
},
// 上一月
prevMonth() {
this.monthValue = this.monthValue.map((item, index) => {
if (this.monthValue[1] <= 0 && index == 0) {
return --item;
} else if (this.monthValue[1] <= 0 && index == 1) {
return 11;
} else if (index === 1) {
return --item;
}
return item;
});
this.updateCalender(false);
},
// 下一月
nextMonth() {
this.monthValue = this.monthValue.map((item, index) => {
if (this.monthValue[1] >= 11 && index == 0) {
return item + 1;
} else if (this.monthValue[1] >= 11 && index == 1) {
return 0;
} else if (index === 1) {
return ++item;
}
return item;
});
this.updateCalender(false);
},
// 更新日历
updateCalender(updateNowDay = true) {
const year = this.years[this.monthValue[0]];
const month = this.months[this.monthValue[1]];
const preYMArr = this.getMonthV(this.monthValue, 2);
const nextYMArr = this.getMonthV(this.monthValue);
const nowCalenderDaysList = this.getDayList(year, month);
const preCalenderDaysList = this.getDayList(preYMArr[0], preYMArr[1]);
const nextCalenderDaysList = this.getDayList(nextYMArr[0], nextYMArr[1]);
if (this.year === year && this.month === month) {
// 含有(今)年月
for (let i in nowCalenderDaysList) {
if (nowCalenderDaysList[i].label === "今" && updateNowDay) {
this.nowSelectDay = nowCalenderDaysList[i];
this.$emit("ok", this.nowSelectDay);
if (this.showType !== 0) {
this.swiperCurrent = Math.ceil((i * 1 + 1) / 7) - 1;
}
}
}
}
if (this.showType === 0) {
// 全行显示
this.swiperDaysList = this.limitNowMoth
? [
{
label: "now",
value: nowCalenderDaysList,
}, // 当月
]
: [
{
label: "now",
value: nowCalenderDaysList,
}, // 当月
{
label: "next",
value: nextCalenderDaysList,
}, // 下一月
{
label: "pre",
value: preCalenderDaysList,
},
// 上一月
];
} else {
//1 一行
let nowArr = this.group(nowCalenderDaysList, 7);
let preArr = this.group(preCalenderDaysList, 7);
let nextArr = this.group(nextCalenderDaysList, 7);
this.calenderRowDaysList = this.limitNowMoth
? [
...nowArr.map((arr) => {
return { label: "now", value: arr };
}), // 当月
]
: [
...nowArr.map((arr) => {
return { label: "now", value: arr };
}), // 当月
{
label: "next",
value: nextArr[0],
}, // 下月第一行
{
label: "pre",
value: preArr[preArr.length - 1],
}, // 上月最后一行
];
}
},
// 根据当前月份值获取上下年月值
getMonthV(value, type = 1) {
let arr = []; // 当前月份
if (type === 1) {
// 默认获取下一月
arr = value.map((item, index) => {
if (value[1] >= 11 && index == 0) {
return item + 1;
} else if (value[1] >= 11 && index == 1) {
return 0;
} else if (index === 1) {
return ++item;
}
return item;
});
} else {
// 获取上一月
arr = value.map((item, index) => {
if (value[1] <= 1 && index == 0) {
return --item;
} else if (value <= 1 && index == 1) {
return 11;
} else if (index === 1) {
return --item;
}
return item;
});
}
return [this.years[arr[0]], this.months[arr[1]]];
},
// 根据年月获取天数列表
getDayList(year, month) {
let list = [];
const startDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 1);
const days = (endDate - startDate) / (1000 * 60 * 60 * 24);
for (let i = 1; i <= days; i++) {
const dateStr = `${year}/${month}/${i}`;
let week = new Date(dateStr).getDay();
if (i === 1) {
let startIndex = week === 0 ? 6 : week - 1;
// 上月天数列表
let prevDays =
(startDate - new Date(year, month - 2, 1)) / (1000 * 60 * 60 * 24);
let preList = [];
let piL = 7 + (startIndex - 7);
for (let pi = 0; pi < piL; pi++) {
const preDay = prevDays - piL + pi + 1;
const date = `${year}/${month - 1}/${preDay}`;
preList.push({
value: preDay, // 值
label: preDay, // 描述
disabled: true, // 禁用
isNowMonth: false,
date,
time: new Date(date).valueOf(),
state: this.dateStateList.find((data) => data.date === date),
adjust: this.dateAdjustList.find((data) => data.date === date),
});
}
list.splice(0, startIndex, ...preList);
}
list.push({
value: i, // 值
label:
new Date(year, month - 1, i).valueOf() ===
new Date(this.year, this.month - 1, this.day).valueOf()
? "今"
: i, // 描述
disabled: false, // 禁用
isNowMonth: true,
date: dateStr,
time: new Date(year, month - 1, i).valueOf(),
state: this.dateStateList.find((data) => data.date === dateStr),
adjust: this.dateAdjustList.find((data) => data.date === dateStr),
});
}
// 补齐
if (list.length % 7 !== 0) {
let endIndex = 7 - (list.length % 7);
// 下月天数列表
let nextList = [];
for (let ni = 0; ni < endIndex; ni++) {
nextList.push({
value: ni + 1, // 值
label: ni + 1,
disabled: true, // 禁用
isNowMonth: false,
date: `${year}/${month + 1}/${ni + 1}`,
time: new Date(year, month, ni + 1).valueOf(),
});
}
list.push(...nextList);
}
return list;
},
// 单数组分割成等长二维数组
group(list, len) {
let index = 0;
const arr = [];
while (index < list.length) {
arr.push(list.slice(index, (index += len)));
}
return arr;
},
},
};
</script>
<style lang="scss" scoped>
.calendar {
position: relative;
background: #fff;
}
.wrapper {
font-family: PingFangSC-Regular, PingFang SC;
color: #222;
}
// 顶部
.top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 48rpx 16rpx;
.title {
height: 48rpx;
font-size: 34rpx;
font-weight: 600;
color: #222222;
line-height: 48rpx;
}
.parting-line {
width: 1rpx;
height: 28rpx;
background: #666;
margin: 0 16rpx;
}
.month-box {
display: flex;
align-items: center;
line-height: 37rpx;
font-size: 32rpx;
color: #333333;
font-weight: 600;
.back-today {
font-size: 24rpx;
font-weight: 400;
line-height: 24rpx;
color: $c-primary;
margin-left: 12rpx;
}
}
.top-left {
display: flex;
align-items: center;
.icon-arrow {
width: 28rpx;
height: 28rpx;
}
.arrow-left {
background: center/100%
url(#{$img-url}/static/img/free/b-icon-arrow-left.png);
}
.arrow-right {
margin-left: 24rpx;
background: center/100%
url(#{$img-url}/static/img/free/b-icon-arrow-right.png);
}
}
}
.row-swiper {
height: 110rpx;
}
.all-swiper {
height: 450rpx;
transition: height ease 0.5s;
}
.six-height {
height: 53 0rpx;
}
.calender-box {
position: relative;
padding: 24rpx 24rpx 0;
// box-shadow: 30vw 5rpx 10rpx rgba($color: #eee, $alpha: 0.4),
// -30vw 5rpx 10rpx rgba($color: #eee, $alpha: 0.4);
z-index: 10;
.head-title {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-gap: 10rpx;
margin-bottom: 16rpx;
&-item {
width: 90rpx;
height: 33rpx;
font-size: 24rpx;
font-weight: 400;
color: #888888;
line-height: 33rpx;
text-align: center;
}
}
.days-list {
display: grid;
height: 100%;
grid-template-columns: repeat(7, 1fr);
grid-gap: 10rpx;
padding-top: 14rpx;
align-content: stretch;
}
.row-days-list {
display: grid;
height: 100%;
grid-template-columns: repeat(7, 1fr);
grid-gap: 10rpx;
margin-bottom: 10rpx;
// align-content: space-around;
align-content: stretch;
padding-top: 14rpx;
}
.days-list-item {
display: flex;
align-items: flex-start;
justify-content: center;
width: 100%;
height: 74rpx;
font-size: 34rpx;
font-family: Helvetica;
color: #222222;
line-height: 41rpx;
.label {
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
.text {
flex-shrink: 0;
position: relative;
width: 62rpx;
height: 62rpx;
line-height: 62rpx;
text-align: center;
}
}
.active-item {
background-color: #006eff;
color: #fff;
border-radius: 50%;
}
.today-text {
color: #006eff;
}
.active-item--disabled {
opacity: 0.5;
}
.state-item {
flex-shrink: 0;
margin-top: 4rpx;
width: 10rpx;
height: 10rpx;
background: #006eff;
border-radius: 50%;
}
.text-state-item {
width: 8rpx;
height: 8rpx;
background-color: #ff7400;
}
.item-adjust {
position: absolute;
top: -24rpx;
right: -6rpx;
width: 20rpx;
height: 20rpx;
font-family: PingFangSC, PingFang SC;
font-weight: 400;
font-size: 20rpx;
color: #666666;
text-align: right;
font-style: normal;
}
.text-state-leave {
color: #ff7400;
}
.disabled {
color: #eaeaea;
}
}
.arrow-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 50rpx;
height: 40rpx;
margin: 10rpx auto;
.arrow-left {
width: 20rpx;
height: 4rpx;
background-color: #ddd;
border-radius: 2rpx 0 0 2rpx;
transform: rotate(30deg);
transform-origin: center right;
transition: transform ease-in 0.3s;
}
.arrow-right {
width: 20rpx;
height: 4rpx;
background-color: #ddd;
border-radius: 0rpx 2rpx 2rpx 0;
transform: rotate(-30deg);
transform-origin: center left;
transition: transform ease-in 0.3s;
}
.arrow-left--up {
transform: rotate(-30deg);
}
.arrow-right--up {
transform: rotate(30deg);
}
}
}
.month-popup-box {
font-size: 30rpx;
.month-top {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1rpx solid #ccc;
padding: 28rpx 32rpx;
.ok-text {
font-size: 30rpx;
color: #006eff;
line-height: 42rpx;
}
.cancel-text {
color: #999;
font-size: 30rpx;
line-height: 42rpx;
}
}
.picker-view {
width: 100%;
height: 400rpx;
text-align: center;
}
}
:deep(.popup .uni-popup__wrapper.uni-custom.bottom .uni-popup__wrapper-box) {
max-height: 100vh;
}
</style>
使用,记得引用先。
结语
结束了。本来想写更多呢,结果代码字数太多,太卡了,有兴趣自己看吧。