hookehuyr

feat(checkin): 实现打卡动态列表分页加载和日期选择优化

重构打卡动态列表为分页加载方式,提升性能
优化日期选择逻辑,自动重置分页参数并重新加载数据
修复日历日期匹配逻辑,支持完整日期格式匹配
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-06-09 15:50:10 4 + * @LastEditTime: 2025-06-10 10:20:08
5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue 5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -67,83 +67,92 @@ ...@@ -67,83 +67,92 @@
67 67
68 <div class="text-wrapper"> 68 <div class="text-wrapper">
69 <div class="text-header">打卡动态</div> 69 <div class="text-header">打卡动态</div>
70 - <div class="post-card" v-for="post in checkinDataList" :key="post.id"> 70 + <van-list
71 - <div class="post-header"> 71 + v-if="checkinDataList.length"
72 - <van-row> 72 + v-model:loading="loading"
73 - <van-col span="4"> 73 + :finished="finished"
74 - <van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar" fit="cover" /> 74 + finished-text="没有更多了"
75 - </van-col> 75 + @load="onLoad"
76 - <van-col span="17"> 76 + class="px-4 py-3 space-y-4"
77 - <div class="user-info"> 77 + >
78 - <div class="username">{{ post.user.name }}</div> 78 + <div class="post-card" v-for="post in checkinDataList" :key="post.id">
79 - <div class="post-time">{{ post.user.time }}</div> 79 + <div class="post-header">
80 - </div> 80 + <van-row>
81 - </van-col> 81 + <van-col span="4">
82 - <van-col span="3"> 82 + <van-image round width="2.5rem" height="2.5rem" :src="post.user.avatar" fit="cover" />
83 - <div v-if="post.is_my" class="post-menu"> 83 + </van-col>
84 - <van-icon name="edit" @click="editCheckin(post)" /> 84 + <van-col span="17">
85 - <van-icon name="delete-o" @click="delCheckin(post)" /> 85 + <div class="user-info">
86 + <div class="username">{{ post.user.name }}</div>
87 + <div class="post-time">{{ post.user.time }}</div>
88 + </div>
89 + </van-col>
90 + <van-col span="3">
91 + <div v-if="post.is_my" class="post-menu">
92 + <van-icon name="edit" @click="editCheckin(post)" />
93 + <van-icon name="delete-o" @click="delCheckin(post)" />
94 + </div>
95 + </van-col>
96 + </van-row>
97 + </div>
98 + <div class="post-content">
99 + <div class="post-text">{{ post.content }}</div>
100 + <div class="post-media">
101 + <div v-if="post.images.length" class="post-images">
102 + <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5"
103 + @click="openImagePreview(index, post)" />
86 </div> 104 </div>
87 - </van-col> 105 + <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" />
88 - </van-row> 106 + <div v-for="(v, idx) in post.videoList" :key="idx">
89 - </div> 107 + <!-- 视频封面和播放按钮 -->
90 - <div class="post-content"> 108 + <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;">
91 - <div class="post-text">{{ post.content }}</div> 109 + <img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'"
92 - <div class="post-media"> 110 + :alt="v.content" class="w-full h-full object-cover" />
93 - <div v-if="post.images.length" class="post-images"> 111 + <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
94 - <van-image width="30%" fit="cover" v-for="(image, index) in post.images" :key="index" :src="image" radius="5" 112 + @click="startPlay(v)">
95 - @click="openImagePreview(index, post)" /> 113 + <div
96 - </div> 114 + class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
97 - <van-image-preview v-if="currentPost" v-model:show="showImagePreview" :images="currentPost.images" :start-position="startPosition" :show-index="true" @change="onChange" /> 115 + <van-icon name="play-circle-o" class="text-white" size="40" />
98 - <div v-for="(v, idx) in post.videoList" :key="idx"> 116 + </div>
99 - <!-- 视频封面和播放按钮 -->
100 - <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" style="aspect-ratio: 16/9; margin-bottom: 1rem;">
101 - <img :src="v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'"
102 - :alt="v.content" class="w-full h-full object-cover" />
103 - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
104 - @click="startPlay(v)">
105 - <div
106 - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
107 - <van-icon name="play-circle-o" class="text-white" size="40" />
108 </div> 117 </div>
109 </div> 118 </div>
119 + <!-- 视频播放器 -->
120 + <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden"
121 + :ref="el => {
122 + if(el) {
123 + // 确保不重复添加
124 + if (!videoPlayers?.includes(el)) {
125 + videoPlayers?.push(el);
126 + }
127 + }
128 + }"
129 + @onPlay="handleVideoPlay(player, post)"
130 + @onPause="handleVideoPause(post)" />
110 </div> 131 </div>
111 - <!-- 视频播放器 --> 132 + <AudioPlayer
112 - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" class="post-video rounded-lg overflow-hidden" 133 + v-if="post.audio.length"
134 + :songs="post.audio"
135 + class="post-audio"
136 + :id="post.id"
113 :ref="el => { 137 :ref="el => {
114 if(el) { 138 if(el) {
115 // 确保不重复添加 139 // 确保不重复添加
116 - if (!videoPlayers?.includes(el)) { 140 + if (!audioPlayers?.includes(el)) {
117 - videoPlayers?.push(el); 141 + audioPlayers?.push(el);
118 } 142 }
119 } 143 }
120 }" 144 }"
121 - @onPlay="handleVideoPlay(player, post)" 145 + @play="(player) => handleAudioPlay(player, post)"
122 - @onPause="handleVideoPause(post)" /> 146 + />
123 </div> 147 </div>
124 - <AudioPlayer 148 + </div>
125 - v-if="post.audio.length" 149 + <div class="post-footer">
126 - :songs="post.audio" 150 + <van-icon @click="handLike(post)"name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" />
127 - class="post-audio" 151 + <span class="like-count">{{ post.likes }}</span>
128 - :id="post.id"
129 - :ref="el => {
130 - if(el) {
131 - // 确保不重复添加
132 - if (!audioPlayers?.includes(el)) {
133 - audioPlayers?.push(el);
134 - }
135 - }
136 - }"
137 - @play="(player) => handleAudioPlay(player, post)"
138 - />
139 </div> 152 </div>
140 </div> 153 </div>
141 - <div class="post-footer"> 154 + </van-list>
142 - <van-icon @click="handLike(post)"name="good-job" class="like-icon" :color="post.is_liked ? 'red' : ''" /> 155 + <van-empty v-else image="error" description="暂无数据" />
143 - <span class="like-count">{{ post.likes }}</span>
144 - </div>
145 - </div>
146 - <van-empty v-if="!checkinDataList.length" image="error" description="暂无数据" />
147 </div> 156 </div>
148 157
149 <div style="height: 5rem;"></div> 158 <div style="height: 5rem;"></div>
...@@ -353,33 +362,37 @@ const onChange = (index) => { ...@@ -353,33 +362,37 @@ const onChange = (index) => {
353 startPosition.value = index; 362 startPosition.value = index;
354 } 363 }
355 const formatter = (day) => { 364 const formatter = (day) => {
365 + const year = day.date.getFullYear();
356 const month = day.date.getMonth() + 1; 366 const month = day.date.getMonth() + 1;
357 const date = day.date.getDate(); 367 const date = day.date.getDate();
358 368
359 let checkin_days = myCheckinDates.value; 369 let checkin_days = myCheckinDates.value;
360 370
361 - if (month === 6) { 371 + // 检查当前日期是否在签到日期列表中
362 - if (checkin_days.includes(date)) { 372 + if (checkin_days && checkin_days.length > 0) {
373 + // 格式化当前日期为YYYY-MM-DD格式,与checkin_days中的格式匹配
374 + const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`;
375 +
376 + // 检查是否已签到
377 + if (checkin_days.includes(formattedDate)) {
363 day.className = 'calendar-checkin'; 378 day.className = 'calendar-checkin';
364 day.type = 'selected'; 379 day.type = 'selected';
365 - day.bottomInfo = '已签到' 380 + day.bottomInfo = '已签到';
366 } 381 }
367 } 382 }
383 +
368 // 选中今天的日期 384 // 选中今天的日期
369 if (dayjs(day.date).isSame(new Date(), 'day')) { 385 if (dayjs(day.date).isSame(new Date(), 'day')) {
370 day.className = 'calendar-today'; 386 day.className = 'calendar-today';
371 - day.type ='selected'; 387 + day.type = 'selected';
372 - day.bottomInfo = '今日' 388 + day.bottomInfo = '今日';
373 } 389 }
374 390
375 return day; 391 return day;
376 } 392 }
377 393
378 const onSelectDay = (day) => { 394 const onSelectDay = (day) => {
379 - console.warn('选择了日期', dayjs(day).format('YYYY-MM-DD'));
380 getTaskDetail(dayjs(day).format('YYYY-MM')); 395 getTaskDetail(dayjs(day).format('YYYY-MM'));
381 - // 获取打卡动态列表
382 - initUploadTaskList(dayjs(day).format('YYYY-MM-DD'));
383 // 修改浏览器地址把当前的date加入地址栏, 页面不刷新 396 // 修改浏览器地址把当前的date加入地址栏, 页面不刷新
384 router.push({ 397 router.push({
385 path: route.path, 398 path: route.path,
...@@ -388,6 +401,12 @@ const onSelectDay = (day) => { ...@@ -388,6 +401,12 @@ const onSelectDay = (day) => {
388 date: dayjs(day).format('YYYY-MM-DD') 401 date: dayjs(day).format('YYYY-MM-DD')
389 } 402 }
390 }) 403 })
404 + // 重置分页参数
405 + page.value = 0
406 + checkinDataList.value = []
407 + finished.value = false
408 + // 重新加载数据
409 + onLoad(dayjs(day).format('YYYY-MM-DD'))
391 } 410 }
392 411
393 const onClickSubtitle = (evt) => { 412 const onClickSubtitle = (evt) => {
...@@ -442,7 +461,6 @@ const handLike = async (post) => { ...@@ -442,7 +461,6 @@ const handLike = async (post) => {
442 } 461 }
443 462
444 const editCheckin = (post) => { 463 const editCheckin = (post) => {
445 - console.warn(post);
446 if (post.file_type === 'image') { 464 if (post.file_type === 'image') {
447 router.push({ 465 router.push({
448 path: '/checkin/image', 466 path: '/checkin/image',
...@@ -505,7 +523,6 @@ const showProgress = ref(true); ...@@ -505,7 +523,6 @@ const showProgress = ref(true);
505 const getTaskDetail = async (month) => { 523 const getTaskDetail = async (month) => {
506 const { code, data } = await getTaskDetailAPI({ i: route.query.id, month }); 524 const { code, data } = await getTaskDetailAPI({ i: route.query.id, month });
507 if (code) { 525 if (code) {
508 - console.warn(data);
509 taskDetail.value = data; 526 taskDetail.value = data;
510 progress1.value = (data.checkin_number/data.target_number)*100; // 计算进度条百分比 527 progress1.value = (data.checkin_number/data.target_number)*100; // 计算进度条百分比
511 showProgress.value = !isNaN(progress1.value); // 如果是NaN,就不显示进度条 528 showProgress.value = !isNaN(progress1.value); // 如果是NaN,就不显示进度条
...@@ -513,37 +530,45 @@ const getTaskDetail = async (month) => { ...@@ -513,37 +530,45 @@ const getTaskDetail = async (month) => {
513 // 获取当前用户的打卡日期 530 // 获取当前用户的打卡日期
514 myCheckinDates.value = data.my_checkin_dates; 531 myCheckinDates.value = data.my_checkin_dates;
515 // 把['2025-06-06'] 转化为 [6] 只取日期去掉0 532 // 把['2025-06-06'] 转化为 [6] 只取日期去掉0
516 - myCheckinDates.value = myCheckinDates.value.map(date => { 533 + // myCheckinDates.value = myCheckinDates.value.map(date => {
517 - return dayjs(date).date(); 534 + // return dayjs(date).date();
518 - }) 535 + // })
519 } 536 }
520 } 537 }
521 538
522 -const initUploadTaskList = async (date) => { 539 +const loading = ref(false)
523 - const toast = showLoadingToast({ 540 +const finished = ref(false)
524 - message: '加载中...', 541 +const limit = ref(3)
525 - forbidClick: true, 542 +const page = ref(0)
543 +
544 +const onLoad = async (date) => {
545 + const nextPage = page.value;
546 + const current_date = date || route.query.date || dayjs().format('YYYY-MM-DD');
547 + //
548 + const res = await getUploadTaskListAPI({
549 + limit: limit.value,
550 + page: nextPage,
551 + task_id: route.query.id,
552 + date: current_date
526 }); 553 });
527 - const { code, data } = await getUploadTaskListAPI({ task_id: route.query.id, date, limit: 999 }); 554 + if (res.code) {
528 - if (code) {
529 - console.warn(data?.checkin_list);
530 // 整理数据结构 555 // 整理数据结构
531 - checkinDataList.value = formatData(data) 556 + checkinDataList.value = [...checkinDataList.value, ...formatData(res.data)];
532 - toast.close(); 557 + finished.value = res.data.checkin_list.length < limit.value;
558 + page.value = nextPage + 1;
533 } 559 }
534 -} 560 + loading.value = false;
561 +};
535 562
536 onMounted(async () => { 563 onMounted(async () => {
537 const current_date = route.query.date; 564 const current_date = route.query.date;
538 if (current_date) { 565 if (current_date) {
539 getTaskDetail(dayjs(current_date).format('YYYY-MM')); 566 getTaskDetail(dayjs(current_date).format('YYYY-MM'));
540 - // 获取打卡动态列表
541 - initUploadTaskList(current_date);
542 myRefCalendar.value?.reset(new Date(current_date)); 567 myRefCalendar.value?.reset(new Date(current_date));
568 + onLoad(current_date);
543 } else { 569 } else {
544 getTaskDetail(dayjs().format('YYYY-MM')); 570 getTaskDetail(dayjs().format('YYYY-MM'));
545 - // 获取打卡动态列表 571 + onLoad(dayjs().format('YYYY-MM-DD'));
546 - initUploadTaskList(dayjs().format('YYYY-MM-DD'));
547 } 572 }
548 }) 573 })
549 574
...@@ -606,6 +631,12 @@ const formatData = (data) => { ...@@ -606,6 +631,12 @@ const formatData = (data) => {
606 } 631 }
607 } 632 }
608 633
634 +.calendar-today {
635 + .van-calendar__selected-day {
636 + background: #FAAB0C !important;
637 + }
638 +}
639 +
609 .text-wrapper { 640 .text-wrapper {
610 padding: 1rem; 641 padding: 1rem;
611 color: #4caf50; 642 color: #4caf50;
......