hookehuyr

refactor(日历组件): 优化日历高度计算和滚动隐藏逻辑

使用 ResizeObserver 替代手动计算日历高度
添加滚动时隐藏日历头部的动画效果
1 <!-- 1 <!--
2 * @Date: 2025-01-25 15:34:17 2 * @Date: 2025-01-25 15:34:17
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-12-18 22:13:17 4 + * @LastEditTime: 2025-12-18 22:33:09
5 * @FilePath: /mlaj/src/components/ui/CollapsibleCalendar.vue 5 * @FilePath: /mlaj/src/components/ui/CollapsibleCalendar.vue
6 * @Description: 可折叠日历组件 6 * @Description: 可折叠日历组件
7 --> 7 -->
8 <template> 8 <template>
9 <div class="collapsible-calendar "> 9 <div class="collapsible-calendar ">
10 <!-- 折叠状态显示 --> 10 <!-- 折叠状态显示 -->
11 - <div class="calendar-collapsed"> 11 + <div class="calendar-collapsed" :class="{ 'is-compact': isHeaderHidden }">
12 <div class="calendar-header"> 12 <div class="calendar-header">
13 <div class="calendar-icon"> 13 <div class="calendar-icon">
14 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> 14 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
29 <div class="calendar-date-display" @click="expandCalendar"> 29 <div class="calendar-date-display" @click="expandCalendar">
30 <div class="calendar-date-main">{{ formattedCurrentDate }}</div> 30 <div class="calendar-date-main">{{ formattedCurrentDate }}</div>
31 <div class="calendar-weekday">{{ formattedWeekday }}</div> 31 <div class="calendar-weekday">{{ formattedWeekday }}</div>
32 - <div class="text-xs text-gray-500 mt-1">点击切换日期</div> 32 + <div class="text-xs text-gray-500 mt-1 collapsible-text">点击切换日期</div>
33 </div> 33 </div>
34 <!-- <div class="calendar-action"> 34 <!-- <div class="calendar-action">
35 <div class="calendar-action-text">指定日期</div> 35 <div class="calendar-action-text">指定日期</div>
...@@ -49,7 +49,7 @@ ...@@ -49,7 +49,7 @@
49 </svg> 49 </svg>
50 </div> 50 </div>
51 </div> 51 </div>
52 - <div v-if="selectedSubtask" class="text-xs text-gray-500 mt-1 cursor-pointer hover:text-green-600 transition-colors" @click.stop="openRulesPopup">点击查看打卡规则</div> 52 + <div v-if="selectedSubtask" class="text-xs text-gray-500 mt-1 cursor-pointer hover:text-green-600 transition-colors collapsible-text" @click.stop="openRulesPopup">点击查看打卡规则</div>
53 </div> 53 </div>
54 </div> 54 </div>
55 </div> 55 </div>
...@@ -119,6 +119,7 @@ ...@@ -119,6 +119,7 @@
119 119
120 <script setup> 120 <script setup>
121 import { ref, computed, watch, onMounted } from 'vue' 121 import { ref, computed, watch, onMounted } from 'vue'
122 +import { useScroll } from '@vueuse/core'
122 import dayjs from 'dayjs' 123 import dayjs from 'dayjs'
123 124
124 // Props定义 125 // Props定义
...@@ -155,6 +156,44 @@ const selectedCourseText = ref('全部作业') ...@@ -155,6 +156,44 @@ const selectedCourseText = ref('全部作业')
155 const selectedCourseId = ref('') 156 const selectedCourseId = ref('')
156 const showRulesPopup = ref(false) 157 const showRulesPopup = ref(false)
157 158
159 +const { y } = useScroll(window)
160 +const isHeaderHidden = ref(false)
161 +const lastY = ref(0)
162 +
163 +watch(y, (newY) => {
164 + // 忽略弹性滚动造成的负值
165 + if (newY < 0) return
166 +
167 + // 获取文档高度和窗口高度,用于判断是否触底
168 + const windowHeight = window.innerHeight
169 + const documentHeight = document.documentElement.scrollHeight
170 + const bottomThreshold = 20 // 触底阈值
171 +
172 + // 如果接近底部,不执行隐藏逻辑(避免回弹导致的显示)
173 + if (newY + windowHeight >= documentHeight - bottomThreshold) {
174 + // 如果已经在底部,保持之前的状态,或者强制隐藏(根据需求,通常保持当前状态更稳妥,或者强制隐藏因为是在向下浏览)
175 + // 这里选择保持状态,但更新lastY防止下次误判
176 + lastY.value = newY
177 + return
178 + }
179 +
180 + // 滚动方向判定
181 + const direction = newY > lastY.value ? 'down' : 'up'
182 + const scrollThreshold = 50 // 滚动阈值
183 +
184 + if (newY > scrollThreshold && direction === 'down') {
185 + isHeaderHidden.value = true
186 + } else if (direction === 'up') {
187 + // 只有当向上滚动超过一定距离或者不在底部反弹区域时才显示
188 + // 这里的判断其实已经被上面的触底检测覆盖了一部分,但为了保险,可以加一个最小变动阈值
189 + if (Math.abs(newY - lastY.value) > 5) {
190 + isHeaderHidden.value = false
191 + }
192 + }
193 +
194 + lastY.value = newY
195 +})
196 +
158 const courseColumns = computed(() => { 197 const courseColumns = computed(() => {
159 return [ 198 return [
160 { text: '全部作业', value: '' }, 199 { text: '全部作业', value: '' },
...@@ -335,11 +374,39 @@ defineExpose({ ...@@ -335,11 +374,39 @@ defineExpose({
335 transition: all 0.1s ease; 374 transition: all 0.1s ease;
336 } 375 }
337 376
377 + // 紧凑模式下的样式控制
378 + &.is-compact {
379 + .calendar-header {
380 + max-height: 0;
381 + margin-bottom: 0;
382 + opacity: 0;
383 + pointer-events: none;
384 + }
385 +
386 + .collapsible-text {
387 + max-height: 0;
388 + margin-top: 0;
389 + opacity: 0;
390 + overflow: hidden;
391 + }
392 + }
393 +
338 .calendar-header { 394 .calendar-header {
339 display: flex; 395 display: flex;
340 align-items: center; 396 align-items: center;
341 margin-bottom: 16px; 397 margin-bottom: 16px;
342 gap: 12px; 398 gap: 12px;
399 + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
400 + max-height: 100px;
401 + opacity: 1;
402 + overflow: hidden;
403 +
404 + &.is-hidden {
405 + max-height: 0;
406 + margin-bottom: 0;
407 + opacity: 0;
408 + pointer-events: none;
409 + }
343 410
344 .calendar-icon { 411 .calendar-icon {
345 display: flex; 412 display: flex;
...@@ -458,6 +525,12 @@ defineExpose({ ...@@ -458,6 +525,12 @@ defineExpose({
458 } 525 }
459 } 526 }
460 527
528 +.collapsible-text {
529 + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
530 + max-height: 20px; // 假设文字高度不超过20px
531 + opacity: 1;
532 +}
533 +
461 .calendar-popup-content { 534 .calendar-popup-content {
462 height: 100%; 535 height: 100%;
463 overflow: hidden; 536 overflow: hidden;
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
9 <AppLayout :hasTitle="false"> 9 <AppLayout :hasTitle="false">
10 <van-config-provider :theme-vars="themeVars"> 10 <van-config-provider :theme-vars="themeVars">
11 <!-- 固定的日历组件 --> 11 <!-- 固定的日历组件 -->
12 - <div class="fixed-calendar"> 12 + <div class="fixed-calendar" ref="fixedCalendarWrapper">
13 <CollapsibleCalendar 13 <CollapsibleCalendar
14 ref="calendarRef" 14 ref="calendarRef"
15 :title="taskDetail.title" 15 :title="taskDetail.title"
...@@ -146,7 +146,7 @@ import FrostedGlass from "@/components/ui/FrostedGlass.vue"; ...@@ -146,7 +146,7 @@ import FrostedGlass from "@/components/ui/FrostedGlass.vue";
146 import CollapsibleCalendar from "@/components/ui/CollapsibleCalendar.vue"; 146 import CollapsibleCalendar from "@/components/ui/CollapsibleCalendar.vue";
147 import PostCountModel from "@/components/count/postCountModel.vue"; 147 import PostCountModel from "@/components/count/postCountModel.vue";
148 import CheckinCard from "@/components/checkin/CheckinCard.vue"; 148 import CheckinCard from "@/components/checkin/CheckinCard.vue";
149 -import { useTitle } from '@vueuse/core'; 149 +import { useTitle, useResizeObserver } from '@vueuse/core';
150 import dayjs from 'dayjs'; 150 import dayjs from 'dayjs';
151 151
152 import { getTaskDetailAPI, getUploadTaskListAPI, delUploadTaskInfoAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from "@/api/checkin"; 152 import { getTaskDetailAPI, getUploadTaskListAPI, delUploadTaskInfoAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from "@/api/checkin";
...@@ -167,18 +167,17 @@ const windowWidth = ref(window.innerWidth); ...@@ -167,18 +167,17 @@ const windowWidth = ref(window.innerWidth);
167 167
168 // 日历高度相关的响应式数据 168 // 日历高度相关的响应式数据
169 const calendarRef = ref(null); 169 const calendarRef = ref(null);
170 +const fixedCalendarWrapper = ref(null);
170 const calendarHeight = ref(200); // 默认高度 171 const calendarHeight = ref(200); // 默认高度
171 172
172 -/** 173 +// 使用 ResizeObserver 监听日历容器高度变化
173 - * 动态获取日历组件的实际高度 174 +useResizeObserver(fixedCalendarWrapper, (entries) => {
174 - */ 175 + const entry = entries[0];
175 -const updateCalendarHeight = async () => { 176 + if (entry && entry.target) {
176 - await nextTick(); 177 + // 使用 getBoundingClientRect 获取包含 padding 和 border 的完整高度
177 - if (calendarRef.value) { 178 + calendarHeight.value = entry.target.getBoundingClientRect().height;
178 - const rect = calendarRef.value.$el.getBoundingClientRect();
179 - calendarHeight.value = rect.height;
180 } 179 }
181 -}; 180 +});
182 181
183 /** 182 /**
184 * 监听窗口尺寸变化 183 * 监听窗口尺寸变化
...@@ -186,8 +185,6 @@ const updateCalendarHeight = async () => { ...@@ -186,8 +185,6 @@ const updateCalendarHeight = async () => {
186 const handleResize = () => { 185 const handleResize = () => {
187 windowHeight.value = window.innerHeight; 186 windowHeight.value = window.innerHeight;
188 windowWidth.value = window.innerWidth; 187 windowWidth.value = window.innerWidth;
189 - // 重新计算日历高度
190 - updateCalendarHeight();
191 }; 188 };
192 189
193 // 组件挂载时添加事件监听 190 // 组件挂载时添加事件监听
...@@ -198,9 +195,6 @@ onMounted(() => { ...@@ -198,9 +195,6 @@ onMounted(() => {
198 // 延迟更新,等待方向变化完成 195 // 延迟更新,等待方向变化完成
199 setTimeout(handleResize, 100); 196 setTimeout(handleResize, 100);
200 }); 197 });
201 -
202 - // 初始化时计算日历高度
203 - updateCalendarHeight();
204 }); 198 });
205 199
206 // CheckinCard refs 200 // CheckinCard refs
......