664 lines
15 KiB
Vue
664 lines
15 KiB
Vue
|
|
<template>
|
|||
|
|
<view class="month-calendar-container">
|
|||
|
|
<!-- 日期选择器头部 -->
|
|||
|
|
<view class="calendar-header" @click="toggleCalendar">
|
|||
|
|
<view class="date-display">
|
|||
|
|
<text class="year-month">{{ displayYearMonth }}</text>
|
|||
|
|
<text class="day">{{ displayDay }}</text>
|
|||
|
|
<text class="weekday">{{ displayWeekday }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="event-count" v-if="eventCount > 0">
|
|||
|
|
<text>日程数:{{ eventCount }}</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="arrow-icon" :class="{ 'rotate': isExpanded }">
|
|||
|
|
<text>▼</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 日历下拉区域 -->
|
|||
|
|
<view class="calendar-dropdown" :class="{ 'expanded': isExpanded }">
|
|||
|
|
<!-- 月份切换栏 -->
|
|||
|
|
<view class="month-header">
|
|||
|
|
<view class="month-nav-btn" @click="prevMonth">
|
|||
|
|
<text>‹</text>
|
|||
|
|
</view>
|
|||
|
|
<view class="month-title">{{ currentYear }}年{{ currentMonth }}月</view>
|
|||
|
|
<view class="month-nav-btn" @click="nextMonth">
|
|||
|
|
<text>›</text>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 星期标题 -->
|
|||
|
|
<view class="weekdays">
|
|||
|
|
<view class="weekday-item" v-for="day in weekdays" :key="day">{{ day }}</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 日历滑动容器 -->
|
|||
|
|
<view
|
|||
|
|
class="calendar-swipe-container"
|
|||
|
|
@touchstart="handleTouchStart"
|
|||
|
|
@touchmove="handleTouchMove"
|
|||
|
|
@touchend="handleTouchEnd"
|
|||
|
|
>
|
|||
|
|
<view
|
|||
|
|
class="calendar-wrapper"
|
|||
|
|
:style="{
|
|||
|
|
transform: `translateX(${translateX}px)`,
|
|||
|
|
transition: isAnimating ? 'transform 0.3s ease-out' : 'none'
|
|||
|
|
}"
|
|||
|
|
>
|
|||
|
|
<!-- 上一个月 -->
|
|||
|
|
<view class="calendar-month">
|
|||
|
|
<view
|
|||
|
|
class="calendar-day"
|
|||
|
|
v-for="(dayObj, index) in prevMonthDays"
|
|||
|
|
:key="`prev-${index}`"
|
|||
|
|
:class="{
|
|||
|
|
'other-month': !dayObj.isCurrentMonth,
|
|||
|
|
'today': isToday(dayObj),
|
|||
|
|
'selected': isSelected(dayObj),
|
|||
|
|
'has-event': hasEvent(dayObj)
|
|||
|
|
}"
|
|||
|
|
@click="selectDate(dayObj)"
|
|||
|
|
>
|
|||
|
|
<text class="day-number">{{ dayObj.day }}</text>
|
|||
|
|
<view class="event-dot" v-if="hasEvent(dayObj)"></view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 当前月 -->
|
|||
|
|
<view class="calendar-month">
|
|||
|
|
<view
|
|||
|
|
class="calendar-day"
|
|||
|
|
v-for="(dayObj, index) in currentMonthDays"
|
|||
|
|
:key="`current-${index}`"
|
|||
|
|
:class="{
|
|||
|
|
'other-month': !dayObj.isCurrentMonth,
|
|||
|
|
'today': isToday(dayObj),
|
|||
|
|
'selected': isSelected(dayObj),
|
|||
|
|
'has-event': hasEvent(dayObj)
|
|||
|
|
}"
|
|||
|
|
@click="selectDate(dayObj)"
|
|||
|
|
>
|
|||
|
|
<text class="day-number">{{ dayObj.day }}</text>
|
|||
|
|
<view class="event-dot" v-if="hasEvent(dayObj)"></view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
|
|||
|
|
<!-- 下一个月 -->
|
|||
|
|
<view class="calendar-month">
|
|||
|
|
<view
|
|||
|
|
class="calendar-day"
|
|||
|
|
v-for="(dayObj, index) in nextMonthDays"
|
|||
|
|
:key="`next-${index}`"
|
|||
|
|
:class="{
|
|||
|
|
'other-month': !dayObj.isCurrentMonth,
|
|||
|
|
'today': isToday(dayObj),
|
|||
|
|
'selected': isSelected(dayObj),
|
|||
|
|
'has-event': hasEvent(dayObj)
|
|||
|
|
}"
|
|||
|
|
@click="selectDate(dayObj)"
|
|||
|
|
>
|
|||
|
|
<text class="day-number">{{ dayObj.day }}</text>
|
|||
|
|
<view class="event-dot" v-if="hasEvent(dayObj)"></view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</view>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed, watch } from 'vue';
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
selectedDate: {
|
|||
|
|
type: String,
|
|||
|
|
default: () => new Date().toISOString().slice(0, 10)
|
|||
|
|
},
|
|||
|
|
events: {
|
|||
|
|
type: Array,
|
|||
|
|
default: () => []
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const emit = defineEmits(['change']);
|
|||
|
|
|
|||
|
|
// 展开/收起状态
|
|||
|
|
const isExpanded = ref(false);
|
|||
|
|
|
|||
|
|
// 当前显示的年月
|
|||
|
|
const currentYear = ref(new Date().getFullYear());
|
|||
|
|
const currentMonth = ref(new Date().getMonth() + 1);
|
|||
|
|
|
|||
|
|
// 星期标题
|
|||
|
|
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
|||
|
|
|
|||
|
|
// 滑动相关
|
|||
|
|
const touchStartX = ref(0);
|
|||
|
|
const screenWidth = ref(375);
|
|||
|
|
const translateX = ref(0);
|
|||
|
|
const baseTranslateX = ref(0);
|
|||
|
|
const isAnimating = ref(false);
|
|||
|
|
const isDragging = ref(false);
|
|||
|
|
|
|||
|
|
// 初始化屏幕宽度
|
|||
|
|
const initScreenWidth = () => {
|
|||
|
|
uni.getSystemInfo({
|
|||
|
|
success: (res) => {
|
|||
|
|
screenWidth.value = res.windowWidth || res.screenWidth || 375;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 获取月份的第一天是星期几(0-6,0是星期日)
|
|||
|
|
const getFirstDayOfMonth = (year, month) => {
|
|||
|
|
return new Date(year, month - 1, 1).getDay();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 获取月份的天数
|
|||
|
|
const getDaysInMonth = (year, month) => {
|
|||
|
|
return new Date(year, month, 0).getDate();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 生成月份的日期数组(包含前后月份的填充日期)
|
|||
|
|
const generateMonthDays = (year, month) => {
|
|||
|
|
const firstDay = getFirstDayOfMonth(year, month);
|
|||
|
|
const daysInMonth = getDaysInMonth(year, month);
|
|||
|
|
const days = [];
|
|||
|
|
|
|||
|
|
// 计算上个月的最后几天
|
|||
|
|
const prevMonth = month === 1 ? 12 : month - 1;
|
|||
|
|
const prevYear = month === 1 ? year - 1 : year;
|
|||
|
|
const prevMonthDays = getDaysInMonth(prevYear, prevMonth);
|
|||
|
|
|
|||
|
|
// 填充前面月份的日期(从最后几天开始)
|
|||
|
|
if (firstDay > 0) {
|
|||
|
|
for (let i = firstDay - 1; i >= 0; i--) {
|
|||
|
|
days.push({
|
|||
|
|
day: prevMonthDays - i,
|
|||
|
|
year: prevYear,
|
|||
|
|
month: prevMonth,
|
|||
|
|
isCurrentMonth: false
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 填充当前月份的日期
|
|||
|
|
for (let i = 1; i <= daysInMonth; i++) {
|
|||
|
|
days.push({
|
|||
|
|
day: i,
|
|||
|
|
year: year,
|
|||
|
|
month: month,
|
|||
|
|
isCurrentMonth: true
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 填充后面月份的日期(确保有6行,即42个格子)
|
|||
|
|
const totalDays = days.length;
|
|||
|
|
const remainingDays = 42 - totalDays; // 6行 * 7列 = 42
|
|||
|
|
const nextMonth = month === 12 ? 1 : month + 1;
|
|||
|
|
const nextYear = month === 12 ? year + 1 : year;
|
|||
|
|
|
|||
|
|
for (let i = 1; i <= remainingDays; i++) {
|
|||
|
|
days.push({
|
|||
|
|
day: i,
|
|||
|
|
year: nextYear,
|
|||
|
|
month: nextMonth,
|
|||
|
|
isCurrentMonth: false
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return days;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 计算当前月份的日期
|
|||
|
|
const currentMonthDays = computed(() => {
|
|||
|
|
return generateMonthDays(currentYear.value, currentMonth.value);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 计算上一个月
|
|||
|
|
const prevMonthYear = computed(() => {
|
|||
|
|
if (currentMonth.value === 1) {
|
|||
|
|
return currentYear.value - 1;
|
|||
|
|
}
|
|||
|
|
return currentYear.value;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const prevMonthMonth = computed(() => {
|
|||
|
|
if (currentMonth.value === 1) {
|
|||
|
|
return 12;
|
|||
|
|
}
|
|||
|
|
return currentMonth.value - 1;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const prevMonthDays = computed(() => {
|
|||
|
|
return generateMonthDays(prevMonthYear.value, prevMonthMonth.value);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 计算下一个月
|
|||
|
|
const nextMonthYear = computed(() => {
|
|||
|
|
if (currentMonth.value === 12) {
|
|||
|
|
return currentYear.value + 1;
|
|||
|
|
}
|
|||
|
|
return currentYear.value;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const nextMonthMonth = computed(() => {
|
|||
|
|
if (currentMonth.value === 12) {
|
|||
|
|
return 1;
|
|||
|
|
}
|
|||
|
|
return currentMonth.value + 1;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const nextMonthDays = computed(() => {
|
|||
|
|
return generateMonthDays(nextMonthYear.value, nextMonthMonth.value);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 格式化日期为 YYYY-MM-DD
|
|||
|
|
const formatDate = (year, month, day) => {
|
|||
|
|
const m = String(month).padStart(2, '0');
|
|||
|
|
const d = String(day).padStart(2, '0');
|
|||
|
|
return `${year}-${m}-${d}`;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 判断是否是今天
|
|||
|
|
const isToday = (dayObj) => {
|
|||
|
|
if (!dayObj || !dayObj.day) return false;
|
|||
|
|
const today = new Date();
|
|||
|
|
return (
|
|||
|
|
dayObj.year === today.getFullYear() &&
|
|||
|
|
dayObj.month === today.getMonth() + 1 &&
|
|||
|
|
dayObj.day === today.getDate()
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 判断是否被选中
|
|||
|
|
const isSelected = (dayObj) => {
|
|||
|
|
if (!dayObj || !dayObj.day) return false;
|
|||
|
|
const selected = new Date(props.selectedDate);
|
|||
|
|
return (
|
|||
|
|
dayObj.year === selected.getFullYear() &&
|
|||
|
|
dayObj.month === selected.getMonth() + 1 &&
|
|||
|
|
dayObj.day === selected.getDate()
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 判断是否有事件
|
|||
|
|
const hasEvent = (dayObj) => {
|
|||
|
|
if (!dayObj || !dayObj.day) return false;
|
|||
|
|
const dateStr = formatDate(dayObj.year, dayObj.month, dayObj.day);
|
|||
|
|
return props.events.some(event => event.date === dateStr);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 选择日期
|
|||
|
|
const selectDate = (dayObj) => {
|
|||
|
|
if (!dayObj || !dayObj.day) return;
|
|||
|
|
const dateStr = formatDate(dayObj.year, dayObj.month, dayObj.day);
|
|||
|
|
emit('change', dateStr);
|
|||
|
|
// 选择后不收起日历
|
|||
|
|
// setTimeout(() => {
|
|||
|
|
// isExpanded.value = false;
|
|||
|
|
// }, 200);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 切换日历展开/收起
|
|||
|
|
const toggleCalendar = () => {
|
|||
|
|
isExpanded.value = !isExpanded.value;
|
|||
|
|
if (isExpanded.value) {
|
|||
|
|
initScreenWidth();
|
|||
|
|
// 同步当前显示月份到选中日期
|
|||
|
|
const selected = new Date(props.selectedDate);
|
|||
|
|
currentYear.value = selected.getFullYear();
|
|||
|
|
currentMonth.value = selected.getMonth() + 1;
|
|||
|
|
translateX.value = -screenWidth.value;
|
|||
|
|
baseTranslateX.value = -screenWidth.value;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 切换到上一个月
|
|||
|
|
const prevMonth = () => {
|
|||
|
|
if (currentMonth.value === 1) {
|
|||
|
|
currentYear.value -= 1;
|
|||
|
|
currentMonth.value = 12;
|
|||
|
|
} else {
|
|||
|
|
currentMonth.value -= 1;
|
|||
|
|
}
|
|||
|
|
translateX.value = -screenWidth.value;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 切换到下一个月
|
|||
|
|
const nextMonth = () => {
|
|||
|
|
if (currentMonth.value === 12) {
|
|||
|
|
currentYear.value += 1;
|
|||
|
|
currentMonth.value = 1;
|
|||
|
|
} else {
|
|||
|
|
currentMonth.value += 1;
|
|||
|
|
}
|
|||
|
|
translateX.value = -screenWidth.value;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 触摸开始
|
|||
|
|
const handleTouchStart = (e) => {
|
|||
|
|
if (isAnimating.value) return;
|
|||
|
|
const touch = e.touches[0];
|
|||
|
|
touchStartX.value = touch.clientX;
|
|||
|
|
isDragging.value = true;
|
|||
|
|
baseTranslateX.value = translateX.value;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 触摸移动
|
|||
|
|
const handleTouchMove = (e) => {
|
|||
|
|
if (!isDragging.value || isAnimating.value) return;
|
|||
|
|
const touch = e.touches[0];
|
|||
|
|
const deltaX = touch.clientX - touchStartX.value;
|
|||
|
|
translateX.value = baseTranslateX.value + deltaX;
|
|||
|
|
|
|||
|
|
// 限制滑动范围
|
|||
|
|
const minTranslate = -screenWidth.value * 2;
|
|||
|
|
const maxTranslate = 0;
|
|||
|
|
translateX.value = Math.max(minTranslate, Math.min(maxTranslate, translateX.value));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 触摸结束
|
|||
|
|
const handleTouchEnd = (e) => {
|
|||
|
|
if (!isDragging.value || isAnimating.value) return;
|
|||
|
|
|
|||
|
|
const touch = e.changedTouches[0];
|
|||
|
|
const deltaX = touch.clientX - touchStartX.value;
|
|||
|
|
const minSwipeDistance = screenWidth.value * 0.2;
|
|||
|
|
|
|||
|
|
isDragging.value = false;
|
|||
|
|
|
|||
|
|
if (Math.abs(deltaX) > minSwipeDistance) {
|
|||
|
|
if (deltaX > 0) {
|
|||
|
|
// 向左滑动,显示上一个月
|
|||
|
|
slideToPrevMonth();
|
|||
|
|
} else {
|
|||
|
|
// 向右滑动,显示下一个月
|
|||
|
|
slideToNextMonth();
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 回到中间位置
|
|||
|
|
resetToCenter();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 重置到中心位置
|
|||
|
|
const resetToCenter = () => {
|
|||
|
|
isAnimating.value = true;
|
|||
|
|
translateX.value = -screenWidth.value;
|
|||
|
|
setTimeout(() => {
|
|||
|
|
isAnimating.value = false;
|
|||
|
|
}, 300);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 滑动到上一个月
|
|||
|
|
const slideToPrevMonth = () => {
|
|||
|
|
isAnimating.value = true;
|
|||
|
|
translateX.value = -screenWidth.value * 2;
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
isAnimating.value = false;
|
|||
|
|
prevMonth();
|
|||
|
|
setTimeout(() => {
|
|||
|
|
translateX.value = -screenWidth.value;
|
|||
|
|
baseTranslateX.value = -screenWidth.value;
|
|||
|
|
}, 0);
|
|||
|
|
}, 300);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 滑动到下一个月
|
|||
|
|
const slideToNextMonth = () => {
|
|||
|
|
isAnimating.value = true;
|
|||
|
|
translateX.value = 0;
|
|||
|
|
|
|||
|
|
setTimeout(() => {
|
|||
|
|
isAnimating.value = false;
|
|||
|
|
nextMonth();
|
|||
|
|
setTimeout(() => {
|
|||
|
|
translateX.value = -screenWidth.value;
|
|||
|
|
baseTranslateX.value = -screenWidth.value;
|
|||
|
|
}, 0);
|
|||
|
|
}, 300);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 显示的年月日
|
|||
|
|
const displayYearMonth = computed(() => {
|
|||
|
|
const date = new Date(props.selectedDate);
|
|||
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const displayDay = computed(() => {
|
|||
|
|
const date = new Date(props.selectedDate);
|
|||
|
|
return String(date.getDate()).padStart(2, '0');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const displayWeekday = computed(() => {
|
|||
|
|
const date = new Date(props.selectedDate);
|
|||
|
|
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
|||
|
|
return `星期${weekdays[date.getDay()]}`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 日程数量
|
|||
|
|
const eventCount = computed(() => {
|
|||
|
|
return props.events.filter(e => e.date === props.selectedDate).length;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 监听选中日期变化,同步月份显示
|
|||
|
|
watch(() => props.selectedDate, (newDate) => {
|
|||
|
|
if (!isExpanded.value) {
|
|||
|
|
const selected = new Date(newDate);
|
|||
|
|
currentYear.value = selected.getFullYear();
|
|||
|
|
currentMonth.value = selected.getMonth() + 1;
|
|||
|
|
}
|
|||
|
|
}, { immediate: true });
|
|||
|
|
|
|||
|
|
// 初始化
|
|||
|
|
initScreenWidth();
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped lang="scss">
|
|||
|
|
.month-calendar-container {
|
|||
|
|
background: #fff;
|
|||
|
|
border-bottom: 1px solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.calendar-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding: 20rpx 30rpx;
|
|||
|
|
background: #fff;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.date-display {
|
|||
|
|
flex: 1;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 10rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.year-month {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.day {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #2885ff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.weekday {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.event-count {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #666;
|
|||
|
|
margin-right: 20rpx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.arrow-icon {
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #999;
|
|||
|
|
transition: transform 0.3s;
|
|||
|
|
|
|||
|
|
&.rotate {
|
|||
|
|
transform: rotate(180deg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.calendar-dropdown {
|
|||
|
|
max-height: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
transition: max-height 0.3s ease-out;
|
|||
|
|
|
|||
|
|
&.expanded {
|
|||
|
|
max-height: 800rpx;
|
|||
|
|
overflow: visible;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.month-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
padding: 20rpx 30rpx;
|
|||
|
|
border-bottom: 1px solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.month-nav-btn {
|
|||
|
|
width: 60rpx;
|
|||
|
|
height: 60rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 40rpx;
|
|||
|
|
color: #666;
|
|||
|
|
cursor: pointer;
|
|||
|
|
|
|||
|
|
&:active {
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.month-title {
|
|||
|
|
font-size: 32rpx;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.weekdays {
|
|||
|
|
display: flex;
|
|||
|
|
padding: 20rpx 0;
|
|||
|
|
border-bottom: 1px solid #f0f0f0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.weekday-item {
|
|||
|
|
flex: 1;
|
|||
|
|
text-align: center;
|
|||
|
|
font-size: 24rpx;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.calendar-swipe-container {
|
|||
|
|
position: relative;
|
|||
|
|
width: 100%;
|
|||
|
|
overflow: hidden;
|
|||
|
|
touch-action: pan-x;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.calendar-wrapper {
|
|||
|
|
display: flex;
|
|||
|
|
width: 300%;
|
|||
|
|
will-change: transform;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.calendar-month {
|
|||
|
|
flex: 0 0 33.333%;
|
|||
|
|
width: 33.333%;
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
padding: 20rpx 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.calendar-day {
|
|||
|
|
flex: 0 0 calc(100% / 7);
|
|||
|
|
width: calc(100% / 7);
|
|||
|
|
height: 80rpx;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
position: relative;
|
|||
|
|
cursor: pointer;
|
|||
|
|
|
|||
|
|
&:active {
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.other-month {
|
|||
|
|
.day-number {
|
|||
|
|
color: #ddd;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.today {
|
|||
|
|
.day-number {
|
|||
|
|
color: #2885ff;
|
|||
|
|
font-weight: 600;
|
|||
|
|
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.selected {
|
|||
|
|
.day-number {
|
|||
|
|
color: #fff;
|
|||
|
|
background: #2885ff;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
width: 60rpx;
|
|||
|
|
height: 60rpx;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
&.has-event {
|
|||
|
|
&::after {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 8rpx;
|
|||
|
|
width: 8rpx;
|
|||
|
|
height: 8rpx;
|
|||
|
|
background: #2885ff;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.day-number {
|
|||
|
|
font-size: 28rpx;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.event-dot {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 8rpx;
|
|||
|
|
width: 8rpx;
|
|||
|
|
height: 8rpx;
|
|||
|
|
background: #2885ff;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
|