目录
效果图:
月视图:

周视图:
功能:
1.月视图下,可以显示日期和当日的事件,上下滑动可以切换月份,点击日期可以切换到周视图查看明细。
2.周视图下,可以查看日期当日事件数量,左右滑动切换上周或者下周日期,点击日期可以查看事件明细。
3.右上角图标点击后返回月视图并锁定到当天日期。
完整代码:
<template>
<view class="box">
<view class="navbar"> 我的日程 </view>
<u-tabs
:list="list"
lineColor="#15B3B4"
activeColor="#15B3B4"
@change="changeTabs"
:current="currentIndex"
:activeStyle="{fontWeight: '600'}"
:itemStyle="{fontSize: '32rpx',lineHeight: '48rpx'}"
></u-tabs>
<view class="dateCenter">
<view class="date">
{{ currentMonth }}月
</view>
<image src="@/static/images/interview/calendar.png" mode="aspectFit" class="calendar" @click="changeContract"></image>
</view>
<view class="weekdays">
<text v-for="(day, index) in weekdays" :key="index" class="weekday">{{ day }}</text>
</view>
<view class="month" v-if="!contract">
<text>{{ currentMonth }}月</text>
</view>
<scroll-view
scroll-x
class="week-date-scroll"
v-if="contract"
@touchstart="handleWeekTouchStart"
@touchend="handleWeekTouchEnd"
>
<view v-for="(date, index) in weekDates" :key="index" class="week-date">
<view class='week-date-item'>
<view
:class="['itemDate', date.getDate() === selectedDate ? 'weekSelected' : '']"
@click="selectWeekDate(date)"
>
{{ date.getDate() }}
</view>
<view v-if="getTripForWeekDate(date)" class="trip2Box">
<!-- 遍历所有的 type -->
<view v-for="(trip, idx) in getTripForWeekDate(date)" :key="idx" :class="['trip2-label',trip.type === 1 ? 'label1' : 'label2']">
<text class="labelText">{{ trip.number }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
<scroll-view
class="dates"
style="width: 100%; height: 1000rpx; overflow-y: auto; overflow-x: hidden;"
scroll-y
@scroll="handleScroll"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
v-if="!contract"
>
<view class="dates-container">
<!-- 添加空盒子占位 -->
<view v-if="startOffset < 7" v-for="n in startOffset" :key="'empty-' + n" class="empty-slot"></view>
<!-- 日期内容 -->
<view
v-for="(date, index) in dates"
:key="index"
class="date-item"
@click="selectDate(date)"
:style="{ color: (index + startOffset) % 7 === 6 || (index + startOffset) % 7 === 0 ? '#999' : '' }"
>
<view class="itemStyle" :class="{ selected: date === selectedDate }">
{{ date }}
</view>
<!-- 显示标签 -->
<view
v-if="getTripForDate(date)"
class="trip-label"
>
<text v-for="(trip, i) in getTripForDate(date).list" :key="i" :class="['trip-item', getTripForDate(date).type === 1 ? 'item1' : 'item2']">
{{ trip }}
</text>
</view>
</view>
<!-- 添加空盒子占位 -->
<!-- <view v-for="n in (7 - (dates.length + startOffset) % 7) % 7" :key="'empty-end-' + n" class="empty-slot"></view> -->
</view>
</scroll-view>
<scroll-view
scroll-x
>
</scroll-view>
</view>
</template>
<script>
export default {
data() {
const today = new Date();
return {
list: [
{ name: "全部" },
{ name: "面试" },
{ name: "培训" }
],
currentIndex: 0,
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
currentMonth: today.getMonth() + 1,
dates: [],
selectedDate: today.getDate(),
touchStartY: 0,
touchEndY: 0,
startOffset: 0,
atTop: false,
atBottom: false,
contract:false,
weekDates:[],
trip:{
20250415:{
type:1,
list:[
'面试面试1',
'面试面试2',
'面试面试3'
]
},
20250414:{
type:2,
list:[
'培训培训培训',
'培训培训培训',
'培训培训培训'
]
},
20250515:{
type:1,
list:[
'面试面试1',
'面试面试2',
'面试面试3'
]
},
20250514:{
type:2,
list:[
'培训培训培训',
'培训培训培训',
'培训培训培训'
]
}
},
trip2: {
"20250416": [
{ type: 1, number: 2 },
{ type: 2, number: 3 }
],
"20250414": [
{ type: 1, number: 1 }
],
"20250515": [
{ type: 2, number: 4 }
]
},
weekTouchStartX: 0,
weekTouchEndX: 0,
currentTrip:{},
}
},
created() {
this.updateDates();
},
methods: {
changeTabs(index) {
this.currentIndex = index;
},
selectDate(date) {
this.selectedDate = date;
this.contract = true;
this.updateWeekDates(date);
},
updateWeekDates(selectedDateOrDateObject) {
// 支持传入日期数字或 Date 对象
const baseDate =
selectedDateOrDateObject instanceof Date
? selectedDateOrDateObject
: new Date(new Date().getFullYear(), this.currentMonth - 1, selectedDateOrDateObject);
const weekDay = baseDate.getDay(); // 获取当天是周几
this.weekDates = Array.from({ length: 7 }, (_, i) => {
return new Date(baseDate.getTime() + (i - weekDay) * 24 * 60 * 60 * 1000);
});
// 统计 weekDates 中的月份分布
const monthCount = this.weekDates.reduce((acc, date) => {
const month = date.getMonth() + 1; // 获取月份 (0 ~ 11) 转为 1 ~ 12
acc[month] = (acc[month] || 0) + 1;
return acc;
}, {});
// 更新 currentMonth 为出现最多的月份
const predominantMonth = Object.keys(monthCount).reduce((a, b) =>
monthCount[a] > monthCount[b] ? a : b
);
this.currentMonth = parseInt(predominantMonth, 10);
// 调试输出
// console.log("Updated weekDates:", this.weekDates.map(d => d.toISOString().split("T")[0]));
// console.log("Updated currentMonth:", this.currentMonth);
},
changeMonth(direction) {
if (direction === 'up') {
this.currentMonth = (this.currentMonth - 1) || 12;
} else if (direction === 'down') {
this.currentMonth = (this.currentMonth % 12) + 1;
}
this.updateDates();
},
handleTouchStart(e) {
this.touchStartY = e.touches[0].clientY; // 记录初始触摸位置
},
handleTouchEnd(e) {
this.touchEndY = e.changedTouches[0].clientY; // 记录结束触摸位置
const distance = this.touchEndY - this.touchStartY; // 计算滑动距离
if (this.atTop && distance > 150) {
// 触顶且从上往下滑动超过阈值
this.changeMonth("up");
} else if (this.atBottom && distance < -150) {
// 触底且从下往上滑动超过阈值
this.changeMonth("down");
}
},
handleScroll(e) {
const target = e.currentTarget || e.target;
if (target) {
this.scrollTop = target.scrollTop || 0; // 当前滚动高度
this.scrollHeight = target.scrollHeight || 0; // 内容总高度
this.offsetHeight = target.offsetHeight || 0; // 容器可见高度
// 实时更新触顶和触底状态
this.atTop = this.scrollTop <= 0;
this.atBottom =
this.scrollTop + this.offsetHeight >= this.scrollHeight - 1;
}
},
updateDates() {
const year = new Date().getFullYear();
const month = this.currentMonth - 1;
const daysInMonth = new Date(year, month + 1, 0).getDate();
this.dates = Array.from({ length: daysInMonth }, (_, i) => i + 1);
const firstDayOfMonth = new Date(year, month, 1).getDay();
this.startOffset = (firstDayOfMonth + 6) % 7 + 1;
},
getTripForDate(date) {
const year = new Date().getFullYear();
const month = this.currentMonth.toString().padStart(2, "0");
const day = date.toString().padStart(2, "0");
const key = `${year}${month}${day}`;
return this.trip[key] || null;
},
changeContract(){
this.contract = false;
const today = new Date();
this.selectedDate = today.getDate();
this.currentMonth = today.getMonth() + 1;
this.updateWeekDates(today);
},
getDateForWeek(n) {
const today = new Date();
const weekDay = today.getDay();
const date = new Date(today.getTime() + (n - weekDay - 1) * 24 * 60 * 60 * 1000);
return date.getDate();
},
selectWeekDate(date) {
this.selectedDate = date.getDate();
this.updateWeekDates(date);
},
handleWeekTouchStart(e) {
this.weekTouchStartX = e.touches[0].clientX;
},
handleWeekTouchEnd(e) {
this.weekTouchEndX = e.changedTouches[0].clientX;
const distance = this.weekTouchEndX - this.weekTouchStartX;
if (distance > 50) {
// 从左往右滑动:上一周
const baseDate = this.weekDates[0]; // 当前周的第一天
const newDate = new Date(baseDate.getTime() - 7 * 24 * 60 * 60 * 1000);
this.updateWeekDates(newDate);
} else if (distance < -50) {
// 从右往左滑动:下一周
const baseDate = this.weekDates[this.weekDates.length - 1]; // 当前周的最后一天
const newDate = new Date(baseDate.getTime() + 7 * 24 * 60 * 60 * 1000);
this.updateWeekDates(newDate);
}
},
getTripForWeekDate(date) {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0"); // 月份补零
const day = date.getDate().toString().padStart(2, "0"); // 日期补零
const key = `${year}${month}${day}`; // 格式化为例如 20250415 的键
return this.trip2[key] || null; // 返回该日期所有的 trip2 数据
}
},
watch: {
currentMonth() {
this.updateDates();
}
}
};
</script>
<style lang="scss" scoped>
.box{
width: 100%;
height: calc(100vh - 100rpx);
overflow: hidden;
}
.navbar{
width: 100%;
height: 160rpx;
padding: 16rpx;
padding-top:110rpx;
font-size: 32rpx;
line-height: 48rpx;
font-weight: 600;
box-sizing: border-box;
}
.dateCenter{
width: 100%;
padding: 24rpx 16rpx 0;
display: flex;
align-items: center;
justify-content: space-between;
.date{
font-size: 48rpx;
line-height: 48rpx;
font-weight: 600;
}
.calendar{
width: 48rpx;
height: 48rpx;
}
}
.month{
margin: 20rpx 20rpx;
text-align: right;
font-size: 40rpx;
line-height: 56rpx;
font-weight: 600;
}
.weekdays {
display: flex;
justify-content: space-around;
margin: 20rpx 0;
font-size: 28rpx;
}
.week-date-scroll {
white-space: nowrap;
// padding: 0 16rpx 16rpx;
padding-bottom: 16rpx;
}
.week-date {
display: inline-block;
width: 14.28%;
height: 106rpx;
// text-align: center;
}
.week-date-item {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
// justify-content: center;
align-items: center;
.itemDate{
width: 70rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
font-weight: 400;
line-height: 48rpx;
border-radius: 50%;
box-sizing: border-box;
}
.trip2Box{
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
}
.trip2-label{
margin: 4rpx 4rpx;
padding: 4rpx 8rpx;
width: 28rpx;
height: 28rpx;
display: flex;
align-items: center;
// justify-content: center;
box-sizing: border-box;
border-radius: 50%;
text-align: center;
.labelText{
color: #fff;
font-size: 20rpx;
font-weight: 500;
line-height: 28rpx;
text-align: center;
white-space: nowrap;
}
}
.label1{
background-color: #15b3b4;
}
.label2{
background-color: #fb602d;
}
}
.weekSelected {
background-color: #15b3b4;
color: #fff !important;
// border-radius: 50%;
}
.dates {
// display: flex;
// flex-wrap: wrap;
position: relative;
padding: 0 16rpx;
}
.dates-container {
display: flex;
flex-wrap: wrap;
}
.date-item {
flex: 0 0 14.28%; /* 每行 7 个 */
height: 208rpx;
display: flex;
flex-direction: column;
align-items: center;
border-top: 2rpx solid #e5e5e5;
padding-top: 8rpx;
.itemStyle{
width: 70rpx;
height: 70rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 36rpx;
font-weight: 500;
line-height: 48rpx;
border-radius: 50%;
box-sizing: border-box;
}
}
.selected {
background-color: #15b3b4;
color: #fff !important;
border-radius: 50%;
}
.empty-slot {
flex: 0 0 14.28%;
height: 208rpx;
}
.trip-label {
margin-top: 16rpx;
width: 86rpx;
height: 104rpx;
display: flex;
flex-direction: column;
align-items: center;
.trip-item{
width: 86rpx;
height: 28rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 4rpx 0;
padding: 0 8rpx;
border-radius: 4rpx;
font-size: 20rpx;
line-height: 28rpx;
}
.item1{
color: #15b3b4;
background-color: #e8f8f8;
}
.item2{
color:#fb602d;
background-color: #fff0ea;
}
}
</style>