hookehuyr

refactor(页面布局): 重构打卡页面布局为固定日历和可滚动内容区域

将日历组件改为固定定位,内容区域改为可滚动布局
动态计算日历高度并设置内容区域margin-top
移除日历背景样式以保持整体设计一致
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-09-24 22:41:07 4 + * @LastEditTime: 2025-09-25 09:37:57
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 v-if="!isExpanded" class="calendar-collapsed" @click="expandCalendar"> 11 <div v-if="!isExpanded" class="calendar-collapsed" @click="expandCalendar">
12 <div class="calendar-header"> 12 <div class="calendar-header">
...@@ -165,8 +165,8 @@ defineExpose({ ...@@ -165,8 +165,8 @@ defineExpose({
165 } 165 }
166 166
167 .calendar-collapsed { 167 .calendar-collapsed {
168 - background: linear-gradient(135deg, #ffffff 0%, #f8fffe 100%); 168 + // background: linear-gradient(135deg, #ffffff 0%, #f8fffe 100%);
169 - border-radius: 16px; 169 + // border-radius: 16px;
170 padding: 20px; 170 padding: 20px;
171 box-shadow: 0 4px 20px rgba(76, 175, 80, 0.08), 0 2px 8px rgba(0, 0, 0, 0.06); 171 box-shadow: 0 4px 20px rgba(76, 175, 80, 0.08), 0 2px 8px rgba(0, 0, 0, 0.06);
172 cursor: pointer; 172 cursor: pointer;
......
1 <!-- 1 <!--
2 * @Date: 2025-05-29 15:34:17 2 * @Date: 2025-05-29 15:34:17
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-09-24 21:56:56 4 + * @LastEditTime: 2025-09-25 09:30:55
5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue 5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
8 <template> 8 <template>
9 <AppLayout :hasTitle="false"> 9 <AppLayout :hasTitle="false">
10 <van-config-provider :theme-vars="themeVars"> 10 <van-config-provider :theme-vars="themeVars">
11 - <CollapsibleCalendar 11 + <!-- 固定的日历组件 -->
12 - ref="myRefCalendar" 12 + <div class="fixed-calendar">
13 - :title="taskDetail.title" 13 + <CollapsibleCalendar
14 - :formatter="formatter" 14 + ref="calendarRef"
15 - v-model="selectedDate" 15 + :title="taskDetail.title"
16 - @select="onSelectDay" 16 + :formatter="formatter"
17 - @click-subtitle="onClickSubtitle" 17 + v-model="selectedDate"
18 - /> 18 + @select="onSelectDay"
19 - 19 + @click-subtitle="onClickSubtitle"
20 - <div v-if="showProgress" class="text-wrapper"> 20 + />
21 - <div class="text-header">目标进度</div>
22 - <div style="background-color: #FFF; margin-top: 1rem;">
23 - <div class="grade-percentage-main">
24 - <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
25 - <van-col span="12">
26 - <span>作业目标</span>
27 - </van-col>
28 - <van-col span="12" style="text-align: right;">
29 - <span style="font-weight: bold;">{{ progress1 }}%</span>
30 - </van-col>
31 - </van-row>
32 - <div style="overflow: hidden;">
33 - <van-progress :percentage="progress1" color="#4caf50" :show-pivot="false" />
34 - </div>
35 - </div>
36 - <!-- <div class="class-percentage-main">
37 - <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
38 - <van-col span="12">
39 - <span>班级目标</span>
40 - </van-col>
41 - <van-col span="12" style="text-align: right;">
42 - <span style="font-weight: bold;">{{ progress2 }}%</span>
43 - </van-col>
44 - </van-row>
45 - <van-progress :percentage="progress2" color="#4caf50" :show-pivot="false" />
46 - </div> -->
47 - <div style="padding: 0.75rem 1rem;">
48 - <van-image round width="2.8rem" height="2.8rem" :src="item ? item : 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="contain"
49 - v-for="(item, index) in teamAvatars.splice(0, 8)" :key="index"
50 - :style="{ marginLeft: index > 0 ? '-0.5rem' : '', border: '2px solid #eff6ff', background: '#fff' }" />
51 - </div>
52 - </div>
53 </div> 21 </div>
54 22
55 - <div class="text-wrapper"> 23 + <!-- 可滚动的内容区域 -->
56 - <div class="text-header">作业描述</div> 24 + <div class="scrollable-content">
57 - </div> 25 + <div class="text-wrapper">
58 - 26 + <div class="text-header">作业描述</div>
59 - <div v-if="!taskDetail.is_finish" class="text-wrapper">
60 - <div class="text-header">打卡类型</div>
61 - <div class="upload-wrapper">
62 - <div @click="goToCheckinImagePage" class="upload-boxer">
63 - <div><van-icon name="photo" size="2.5rem" /></div>
64 - <div style="font-size: 0.85rem;">图文打卡</div>
65 - </div>
66 - <div @click="goToCheckinVideoPage()" class="upload-boxer">
67 - <div><van-icon name="video" size="2.5rem" /></div>
68 - <div style="font-size: 0.85rem;">视频打卡</div>
69 - </div>
70 - <div @click="goToCheckinAudioPage()" class="upload-boxer">
71 - <div><van-icon name="music" size="2.5rem" /></div>
72 - <div style="font-size: 0.85rem;">音频打卡</div>
73 - </div>
74 </div> 27 </div>
75 - </div>
76 28
77 - <div class="text-wrapper"> 29 + <div v-if="showProgress" class="text-wrapper">
78 - <div class="text-header">打卡动态</div> 30 + <div class="text-header">目标进度</div>
79 - <van-list 31 + <div style="background-color: #FFF; margin-top: 1rem;">
80 - v-if="checkinDataList.length" 32 + <div class="grade-percentage-main">
81 - v-model:loading="loading" 33 + <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
82 - :finished="finished" 34 + <van-col span="12">
83 - finished-text="没有更多了" 35 + <span>作业目标</span>
84 - @load="onLoad"
85 - class="py-3 space-y-4"
86 - >
87 - <div class="post-card" v-for="post in checkinDataList" :key="post.id">
88 - <div class="post-header">
89 - <van-row>
90 - <van-col span="4">
91 - <van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" />
92 </van-col> 36 </van-col>
93 - <van-col span="17"> 37 + <van-col span="12" style="text-align: right;">
94 - <div class="user-info"> 38 + <span style="font-weight: bold;">{{ progress1 }}%</span>
95 - <div class="username">{{ post.user.name }}</div>
96 - <div class="post-time">{{ post.user.time }}</div>
97 - </div>
98 </van-col> 39 </van-col>
99 - <van-col span="3"> 40 + </van-row>
100 - <div v-if="post.is_my" class="post-menu"> 41 + <div style="overflow: hidden;">
101 - <van-icon name="edit" @click="editCheckin(post)" /> 42 + <van-progress :percentage="progress1" color="#4caf50" :show-pivot="false" />
102 - <van-icon name="delete-o" @click="delCheckin(post)" /> 43 + </div>
103 - </div> 44 + </div>
45 + <!-- <div class="class-percentage-main">
46 + <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;">
47 + <van-col span="12">
48 + <span>班级目标</span>
49 + </van-col>
50 + <van-col span="12" style="text-align: right;">
51 + <span style="font-weight: bold;">{{ progress2 }}%</span>
104 </van-col> 52 </van-col>
105 </van-row> 53 </van-row>
54 + <van-progress :percentage="progress2" color="#4caf50" :show-pivot="false" />
55 + </div> -->
56 + <div style="padding: 0.75rem 1rem;">
57 + <van-image round width="2.8rem" height="2.8rem" :src="item ? item : 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="contain"
58 + v-for="(item, index) in teamAvatars.splice(0, 8)" :key="index"
59 + :style="{ marginLeft: index > 0 ? '-0.5rem' : '', border: '2px solid #eff6ff', background: '#fff' }" />
106 </div> 60 </div>
107 - <div class="post-content"> 61 + </div>
108 - <div class="post-text">{{ post.content }}</div> 62 + </div>
109 - <div class="post-media"> 63 +
110 - <div v-if="post.images.length" class="post-images"> 64 + <div v-if="!taskDetail.is_finish" class="text-wrapper">
111 - <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5" 65 + <div class="text-header">打卡类型</div>
112 - @click="openImagePreview(index, post)" /> 66 + <div class="upload-wrapper">
113 - </div> 67 + <div @click="goToCheckinImagePage" class="upload-boxer">
114 - <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" /> 68 + <div><van-icon name="photo" size="2.5rem" /></div>
115 - <div v-for="(v, idx) in post.videoList" :key="idx"> 69 + <div style="font-size: 0.85rem;">图文打卡</div>
116 - <!-- 视频封面和播放按钮 --> 70 + </div>
117 - <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;"> 71 + <div @click="goToCheckinVideoPage()" class="upload-boxer">
118 - <img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'" 72 + <div><van-icon name="video" size="2.5rem" /></div>
119 - :alt="v.content" class="w-full h-full object-cover" /> 73 + <div style="font-size: 0.85rem;">视频打卡</div>
120 - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" 74 + </div>
121 - @click="startPlay(v)"> 75 + <div @click="goToCheckinAudioPage()" class="upload-boxer">
122 - <div 76 + <div><van-icon name="music" size="2.5rem" /></div>
123 - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> 77 + <div style="font-size: 0.85rem;">音频打卡</div>
124 - <van-icon name="play-circle-o" class="text-white" size="40" /> 78 + </div>
79 + </div>
80 + </div>
81 +
82 + <div class="text-wrapper">
83 + <div class="text-header">打卡动态</div>
84 + <van-list
85 + v-if="checkinDataList.length"
86 + v-model:loading="loading"
87 + :finished="finished"
88 + finished-text="没有更多了"
89 + @load="onLoad"
90 + class="py-3 space-y-4"
91 + >
92 + <div class="post-card" v-for="post in checkinDataList" :key="post.id">
93 + <div class="post-header">
94 + <van-row>
95 + <van-col span="4">
96 + <van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" />
97 + </van-col>
98 + <van-col span="17">
99 + <div class="user-info">
100 + <div class="username">{{ post.user.name }}</div>
101 + <div class="post-time">{{ post.user.time }}</div>
102 + </div>
103 + </van-col>
104 + <van-col span="3">
105 + <div v-if="post.is_my" class="post-menu">
106 + <van-icon name="edit" @click="editCheckin(post)" />
107 + <van-icon name="delete-o" @click="delCheckin(post)" />
108 + </div>
109 + </van-col>
110 + </van-row>
111 + </div>
112 + <div class="post-content">
113 + <div class="post-text">{{ post.content }}</div>
114 + <div class="post-media">
115 + <div v-if="post.images.length" class="post-images">
116 + <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5"
117 + @click="openImagePreview(index, post)" />
118 + </div>
119 + <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" />
120 + <div v-for="(v, idx) in post.videoList" :key="idx">
121 + <!-- 视频封面和播放按钮 -->
122 + <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;">
123 + <img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'"
124 + :alt="v.content" class="w-full h-full object-cover" />
125 + <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
126 + @click="startPlay(v)">
127 + <div
128 + class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
129 + <van-icon name="play-circle-o" class="text-white" size="40" />
130 + </div>
125 </div> 131 </div>
126 </div> 132 </div>
133 + <!-- 视频播放器 -->
134 + <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden"
135 + :ref="el => {
136 + if(el) {
137 + // 确保不重复添加
138 + if (!videoPlayers?.includes(el)) {
139 + videoPlayers?.push(el);
140 + }
141 + }
142 + }"
143 + @onPlay="handleVideoPlay(player, post)"
144 + @onPause="handleVideoPause(post)" />
127 </div> 145 </div>
128 - <!-- 视频播放器 --> 146 + <AudioPlayer
129 - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden" 147 + v-if="post.audio.length"
148 + :songs="post.audio"
149 + class="post-audio"
150 + :id="post.id"
130 :ref="el => { 151 :ref="el => {
131 if(el) { 152 if(el) {
132 // 确保不重复添加 153 // 确保不重复添加
133 - if (!videoPlayers?.includes(el)) { 154 + if (!audioPlayers?.includes(el)) {
134 - videoPlayers?.push(el); 155 + audioPlayers?.push(el);
135 } 156 }
136 } 157 }
137 }" 158 }"
138 - @onPlay="handleVideoPlay(player, post)" 159 + @play="(player) => handleAudioPlay(player, post)"
139 - @onPause="handleVideoPause(post)" /> 160 + />
140 </div> 161 </div>
141 - <AudioPlayer 162 + </div>
142 - v-if="post.audio.length" 163 + <div class="post-footer">
143 - :songs="post.audio" 164 + <van-icon @click="handLike(post)"name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" />
144 - class="post-audio" 165 + <span class="like-count">{{ post.likes }}</span>
145 - :id="post.id"
146 - :ref="el => {
147 - if(el) {
148 - // 确保不重复添加
149 - if (!audioPlayers?.includes(el)) {
150 - audioPlayers?.push(el);
151 - }
152 - }
153 - }"
154 - @play="(player) => handleAudioPlay(player, post)"
155 - />
156 </div> 166 </div>
157 </div> 167 </div>
158 - <div class="post-footer"> 168 + </van-list>
159 - <van-icon @click="handLike(post)"name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" /> 169 + <van-empty v-else description="暂无数据" />
160 - <span class="like-count">{{ post.likes }}</span> 170 + <van-back-top right="5vw" bottom="10vh" />
161 - </div> 171 + </div>
162 - </div>
163 - </van-list>
164 - <van-empty v-else description="暂无数据" />
165 - <van-back-top right="5vw" bottom="10vh" />
166 - </div>
167 172
168 - <div style="height: 5rem;"></div> 173 + <div style="height: 5rem;"></div>
174 + </div> <!-- 闭合 scrollable-content -->
169 </van-config-provider> 175 </van-config-provider>
170 176
171 <van-dialog v-model:show="dialog_show" title="标题" show-cancel-button></van-dialog> 177 <van-dialog v-model:show="dialog_show" title="标题" show-cancel-button></van-dialog>
...@@ -196,52 +202,29 @@ const myRefCalendar = ref(null); ...@@ -196,52 +202,29 @@ const myRefCalendar = ref(null);
196 const windowHeight = ref(window.innerHeight); 202 const windowHeight = ref(window.innerHeight);
197 const windowWidth = ref(window.innerWidth); 203 const windowWidth = ref(window.innerWidth);
198 204
205 +// 日历高度相关的响应式数据
206 +const calendarRef = ref(null);
207 +const calendarHeight = ref(200); // 默认高度
208 +
199 /** 209 /**
200 - * 动态计算日历高度 210 + * 动态获取日历组件的实际高度
201 - * 根据屏幕尺寸和设备类型自适应调整
202 */ 211 */
203 -const calendarHeight = computed(() => { 212 +const updateCalendarHeight = async () => {
204 - // 获取可视窗口高度 213 + await nextTick();
205 - const viewportHeight = windowHeight.value; 214 + if (calendarRef.value) {
206 - 215 + const rect = calendarRef.value.$el.getBoundingClientRect();
207 - // 预留给其他内容的空间(头部、底部等) 216 + calendarHeight.value = rect.height;
208 - const reservedSpace = 200; // 可根据实际需要调整 217 + }
209 - 218 +};
210 - // 计算可用高度
211 - const availableHeight = viewportHeight - reservedSpace;
212 -
213 - // 设置最小和最大高度限制
214 - const minHeight = 300; // 最小高度
215 - const maxHeight = 500; // 最大高度
216 -
217 - // 根据屏幕宽度调整高度比例
218 - let heightRatio = 0.6; // 默认占可用高度的55%
219 -
220 - if (windowWidth.value < 375) {
221 - // 小屏手机
222 - heightRatio = 0.95;
223 - } else if (windowWidth.value < 414) {
224 - // 中等屏幕手机
225 - heightRatio = 0.95;
226 - } else if (windowWidth.value >= 768) {
227 - // 平板或更大屏幕
228 - heightRatio = 0.95;
229 - }
230 -
231 - const calculatedHeight = Math.floor(availableHeight * heightRatio);
232 -
233 - // 确保高度在合理范围内
234 - const finalHeight = Math.max(minHeight, Math.min(maxHeight, calculatedHeight));
235 -
236 - return `${finalHeight}px`;
237 -});
238 219
239 /** 220 /**
240 * 监听窗口尺寸变化 221 * 监听窗口尺寸变化
241 */ 222 */
242 const handleResize = () => { 223 const handleResize = () => {
243 - windowHeight.value = window.innerHeight; 224 + windowHeight.value = window.innerHeight;
244 - windowWidth.value = window.innerWidth; 225 + windowWidth.value = window.innerWidth;
226 + // 重新计算日历高度
227 + updateCalendarHeight();
245 }; 228 };
246 229
247 // 组件挂载时添加事件监听 230 // 组件挂载时添加事件监听
...@@ -252,6 +235,9 @@ onMounted(() => { ...@@ -252,6 +235,9 @@ onMounted(() => {
252 // 延迟更新,等待方向变化完成 235 // 延迟更新,等待方向变化完成
253 setTimeout(handleResize, 100); 236 setTimeout(handleResize, 100);
254 }); 237 });
238 +
239 + // 初始化时计算日历高度
240 + updateCalendarHeight();
255 }); 241 });
256 242
257 // 存储所有视频播放器的引用 243 // 存储所有视频播放器的引用
...@@ -731,6 +717,25 @@ const formatData = (data) => { ...@@ -731,6 +717,25 @@ const formatData = (data) => {
731 </script> 717 </script>
732 718
733 <style lang="less"> 719 <style lang="less">
720 +// 固定日历样式
721 +.fixed-calendar {
722 + position: fixed;
723 + top: 0;
724 + left: 0;
725 + right: 0;
726 + z-index: 1000;
727 + background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff); // 与AppLayout保持一致的渐变背景
728 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
729 +}
730 +
731 +// 可滚动内容区域样式
732 +.scrollable-content {
733 + margin-top: v-bind('calendarHeight + "px"'); // 动态计算日历高度
734 + padding-top: 1rem;
735 + height: v-bind('"calc(100vh - " + calendarHeight + "px)"'); // 动态计算剩余高度
736 + overflow-y: auto;
737 +}
738 +
734 .van-back-top { 739 .van-back-top {
735 background-color: #4caf50; 740 background-color: #4caf50;
736 } 741 }
......