hookehuyr

feat(checkin): 展示打卡点评列表

同步 Apifox 打卡动态字段说明,并在打卡页面展示 feedback_list 点评内容与评分。

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
...@@ -23,10 +23,10 @@ VITE_PIN = ...@@ -23,10 +23,10 @@ VITE_PIN =
23 # 反向代理服务器地址 23 # 反向代理服务器地址
24 # VITE_PROXY_TARGET = https://oa.anxinchashi.com/ 24 # VITE_PROXY_TARGET = https://oa.anxinchashi.com/
25 # VITE_PROXY_TARGET = http://behalo.onwall.cn/ 25 # VITE_PROXY_TARGET = http://behalo.onwall.cn/
26 -# VITE_PROXY_TARGET = http://oa-dev.onwall.cn/ 26 +VITE_PROXY_TARGET = http://oa-dev.onwall.cn/
27 # VITE_PROXY_TARGET = https://oa.behalo.cc/ 27 # VITE_PROXY_TARGET = https://oa.behalo.cc/
28 # VITE_PROXY_TARGET = https://www.wxgzjs.cn/ 28 # VITE_PROXY_TARGET = https://www.wxgzjs.cn/
29 -VITE_PROXY_TARGET = https://wxm.behalo.cc/ 29 +# VITE_PROXY_TARGET = https://wxm.behalo.cc/
30 30
31 # PC端地址(对应 Vite 默认开发端口 5173) 31 # PC端地址(对应 Vite 默认开发端口 5173)
32 VITE_MOBILE_URL = http://localhost:5173/ 32 VITE_MOBILE_URL = http://localhost:5173/
......
...@@ -31,7 +31,7 @@ const Api = { ...@@ -31,7 +31,7 @@ const Api = {
31 * @return: data: [{ id 大作业id, cover 作业封面, title 大作业名称, begin_date 开始时间, end_date 结束时间, task_type 任务类型 [checkin=签到 | upload=上传附件 | count=计数], is_gray 作业是否应该置灰, is_finish 作业在当前周期是否已经达标, checkin_subtask_id 签到小作业的ID }] 31 * @return: data: [{ id 大作业id, cover 作业封面, title 大作业名称, begin_date 开始时间, end_date 结束时间, task_type 任务类型 [checkin=签到 | upload=上传附件 | count=计数], is_gray 作业是否应该置灰, is_finish 作业在当前周期是否已经达标, checkin_subtask_id 签到小作业的ID }]
32 */ 32 */
33 33
34 -export const getTaskListAPI = (params) => fn(fetch.get(Api.GET_TASK_LIST, params)) 34 +export const getTaskListAPI = params => fn(fetch.get(Api.GET_TASK_LIST, params))
35 35
36 /** 36 /**
37 * @description: 大作业详情 37 * @description: 大作业详情
...@@ -47,7 +47,7 @@ export const getTaskListAPI = (params) => fn(fetch.get(Api.GET_TASK_LIST, param ...@@ -47,7 +47,7 @@ export const getTaskListAPI = (params) => fn(fetch.get(Api.GET_TASK_LIST, param
47 * subtask_list 小作业列表 [{id,title,cycle,frequency,attachment_type,begin_date,end_date,is_finish}] , 47 * subtask_list 小作业列表 [{id,title,cycle,frequency,attachment_type,begin_date,end_date,is_finish}] ,
48 * } 48 * }
49 */ 49 */
50 -export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, params)) 50 +export const getTaskDetailAPI = params => fn(fetch.get(Api.GET_TASK_DETAIL, params))
51 51
52 /** 52 /**
53 * @description: 小作业列表 53 * @description: 小作业列表
...@@ -67,14 +67,14 @@ export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, p ...@@ -67,14 +67,14 @@ export const getTaskDetailAPI = (params) => fn(fetch.get(Api.GET_TASK_DETAIL, p
67 * field_list 动态表单字段列表 [{field_name,label,type}] 67 * field_list 动态表单字段列表 [{field_name,label,type}]
68 * }] 68 * }]
69 */ 69 */
70 -export const getSubtaskListAPI = (params) => fn(fetch.get(Api.GET_SUBTASK_LIST, params)) 70 +export const getSubtaskListAPI = params => fn(fetch.get(Api.GET_SUBTASK_LIST, params))
71 71
72 /** 72 /**
73 * @description: 签到打卡 73 * @description: 签到打卡
74 * @param subtask_id 小作业ID 74 * @param subtask_id 小作业ID
75 * @returns 75 * @returns
76 */ 76 */
77 -export const checkinTaskAPI = (params) => fn(fetch.post(Api.TASK_CHECKIN, params)) 77 +export const checkinTaskAPI = params => fn(fetch.post(Api.TASK_CHECKIN, params))
78 78
79 /** 79 /**
80 * @description: 新增上传打卡 80 * @description: 新增上传打卡
...@@ -86,27 +86,39 @@ export const checkinTaskAPI = (params) => fn(fetch.post(Api.TASK_CHECKIN, param ...@@ -86,27 +86,39 @@ export const checkinTaskAPI = (params) => fn(fetch.post(Api.TASK_CHECKIN, param
86 * @param gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}] 86 * @param gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}]
87 * @returns 87 * @returns
88 */ 88 */
89 -export const addUploadTaskAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_ADD, params)) 89 +export const addUploadTaskAPI = params => fn(fetch.post(Api.TASK_UPLOAD_ADD, params))
90 90
91 /** 91 /**
92 * @description: 获取打卡动态列表 92 * @description: 获取打卡动态列表
93 - * @param task_id 上传作业ID 93 + * @param task_id 作业ID
94 * @param subtask_id 小作业ID 94 * @param subtask_id 小作业ID
95 * @param date 日期 95 * @param date 日期
96 * @param keyword 搜索 96 * @param keyword 搜索
97 * @param order_by_time asc=正序,desc=倒序。默认为倒序 97 * @param order_by_time asc=正序,desc=倒序。默认为倒序
98 * @param limit 条数 98 * @param limit 条数
99 - * @param offset 页码 99 + * @param page 页码
100 - * @returns data: [{id 打卡动态ID, status 审批状态 3=待审批,5=审批通过,7=审批不通过, created_by 打卡人ID, username 打卡人昵称 100 + * @returns data.checkin_list: [{
101 - * avatar 打卡人头像, created_time 打卡时间, created_time_desc 打卡时间描述, note 打卡内容, 101 + * id 打卡动态ID,
102 + * status 审批状态 3=待审批,5=审批通过,7=审批不通过,
103 + * created_by 打卡人ID,
104 + * username 打卡人昵称,
105 + * avatar 打卡人头像,
106 + * created_time 打卡时间,
107 + * created_time_desc 打卡时间的描述(如:2小时前,1天前),
108 + * note 打卡内容,
102 * files[{meta_id,name,value,extension,file_type[image=上传图片,video=视频,audio=音频]}] 附件列表, 109 * files[{meta_id,name,value,extension,file_type[image=上传图片,video=视频,audio=音频]}] 附件列表,
103 - * like_count 点赞数, is_my 是不是我的打卡, is_like 我是否已经点赞, is_makeup 是否补卡 110 + * like_count 点赞数,
104 - * subtask_title 小作业标题 111 + * is_my 是不是我的打卡,
105 - * gratitude_count 感恩次数 112 + * is_like 我是否已经点赞,
106 - * gratitude_form_list 感恩对象列表 [{id,name,city,unit}] 113 + * is_makeup 是否补卡,
114 + * subtask_id 小作业ID,
115 + * subtask_title 小作业标题,
116 + * gratitude_count 感恩次数,
117 + * gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}],
118 + * feedback_list 点评列表 [{id,note 点评内容,score 点评分数,created_time 点评时间,created_by 助教ID,created_avatar 助教头像,created_name 助教昵称}]
107 * }] 119 * }]
108 */ 120 */
109 -export const getUploadTaskListAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_LIST, params)) 121 +export const getUploadTaskListAPI = params => fn(fetch.post(Api.TASK_UPLOAD_LIST, params))
110 122
111 /** 123 /**
112 * @description: 上传打卡详情 124 * @description: 上传打卡详情
...@@ -119,7 +131,7 @@ export const getUploadTaskListAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_L ...@@ -119,7 +131,7 @@ export const getUploadTaskListAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_L
119 * gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}] 131 * gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}]
120 * } 132 * }
121 */ 133 */
122 -export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_INFO, params)) 134 +export const getUploadTaskInfoAPI = params => fn(fetch.get(Api.TASK_UPLOAD_INFO, params))
123 135
124 /** 136 /**
125 * @description: 编辑打卡动态 137 * @description: 编辑打卡动态
...@@ -130,28 +142,28 @@ export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_IN ...@@ -130,28 +142,28 @@ export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_IN
130 * @param gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}] 142 * @param gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}]
131 * @returns 143 * @returns
132 */ 144 */
133 -export const editUploadTaskInfoAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_EDIT, params)) 145 +export const editUploadTaskInfoAPI = params => fn(fetch.post(Api.TASK_UPLOAD_EDIT, params))
134 146
135 /** 147 /**
136 * @description: 删除打卡动态详情 148 * @description: 删除打卡动态详情
137 * @param i 打卡动态ID 149 * @param i 打卡动态ID
138 * @returns 150 * @returns
139 */ 151 */
140 -export const delUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_DEL, params)) 152 +export const delUploadTaskInfoAPI = params => fn(fetch.get(Api.TASK_UPLOAD_DEL, params))
141 153
142 /** 154 /**
143 * @description: 给打卡点赞 155 * @description: 给打卡点赞
144 * @param checkin_id 打卡动态ID 156 * @param checkin_id 打卡动态ID
145 * @returns 157 * @returns
146 */ 158 */
147 -export const likeUploadTaskInfoAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_LIKE, params)) 159 +export const likeUploadTaskInfoAPI = params => fn(fetch.post(Api.TASK_UPLOAD_LIKE, params))
148 160
149 /** 161 /**
150 * @description: 取消点赞 162 * @description: 取消点赞
151 * @param checkin_id 打卡动态ID 163 * @param checkin_id 打卡动态ID
152 * @returns 164 * @returns
153 */ 165 */
154 -export const dislikeUploadTaskInfoAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_DISLIKE, params)) 166 +export const dislikeUploadTaskInfoAPI = params => fn(fetch.post(Api.TASK_UPLOAD_DISLIKE, params))
155 167
156 /** 168 /**
157 * @description: 老师查看打卡动态列表 169 * @description: 老师查看打卡动态列表
...@@ -182,7 +194,7 @@ export const dislikeUploadTaskInfoAPI = (params) => fn(fetch.post(Api.TASK_UPLO ...@@ -182,7 +194,7 @@ export const dislikeUploadTaskInfoAPI = (params) => fn(fetch.post(Api.TASK_UPLO
182 * }] 194 * }]
183 * } 195 * }
184 */ 196 */
185 -export const getCheckinTeacherListAPI = (params) => fn(fetch.get(Api.CHECKIN_TEACHER_LIST, params)) 197 +export const getCheckinTeacherListAPI = params => fn(fetch.get(Api.CHECKIN_TEACHER_LIST, params))
186 198
187 /** 199 /**
188 * @description: 老师审批打卡 200 * @description: 老师审批打卡
...@@ -190,7 +202,7 @@ export const getCheckinTeacherListAPI = (params) => fn(fetch.get(Api.CHECKIN_TE ...@@ -190,7 +202,7 @@ export const getCheckinTeacherListAPI = (params) => fn(fetch.get(Api.CHECKIN_TE
190 * @param status 审批状态 5=通过,7=拒绝 202 * @param status 审批状态 5=通过,7=拒绝
191 * @returns 203 * @returns
192 */ 204 */
193 -export const checkinTaskReviewAPI = (params) => fn(fetch.post(Api.CHECKIN_TEACHER_REVIEW, params)) 205 +export const checkinTaskReviewAPI = params => fn(fetch.post(Api.CHECKIN_TEACHER_REVIEW, params))
194 206
195 /** 207 /**
196 * @description: 老师查看已打卡日期 208 * @description: 老师查看已打卡日期
...@@ -203,11 +215,13 @@ export const checkinTaskReviewAPI = (params) => fn(fetch.post(Api.CHECKIN_TEACH ...@@ -203,11 +215,13 @@ export const checkinTaskReviewAPI = (params) => fn(fetch.post(Api.CHECKIN_TEACH
203 * @param keyword 搜索 215 * @param keyword 搜索
204 * @returns data: { my_checkin_dates 已打卡日期列表 } 216 * @returns data: { my_checkin_dates 已打卡日期列表 }
205 */ 217 */
206 -export const getCheckinTeacherCheckedDatesAPI = (params) => fn(fetch.get(Api.CHECKIN_TEACHER_CHECKED_DATES, params)) 218 +export const getCheckinTeacherCheckedDatesAPI = params =>
219 + fn(fetch.get(Api.CHECKIN_TEACHER_CHECKED_DATES, params))
207 220
208 /** 221 /**
209 * @description: 复用感恩表单数据 222 * @description: 复用感恩表单数据
210 * @param subtask_id 小作业ID 223 * @param subtask_id 小作业ID
211 * @returns data: { gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}], last_used_list 最近使用的表单数据 [{id,name,city,unit,其他信息字段}] } 224 * @returns data: { gratitude_form_list 感恩表单数据 [{id,name,city,unit,其他信息字段}], last_used_list 最近使用的表单数据 [{id,name,city,unit,其他信息字段}] }
212 */ 225 */
213 -export const reuseGratitudeFormAPI = (params) => fn(fetch.post(Api.CHECKIN_TEACHER_REUSE_GRATITUDE_FORM, params)) 226 +export const reuseGratitudeFormAPI = params =>
227 + fn(fetch.post(Api.CHECKIN_TEACHER_REUSE_GRATITUDE_FORM, params))
......
...@@ -4,17 +4,26 @@ ...@@ -4,17 +4,26 @@
4 <div class="post-header"> 4 <div class="post-header">
5 <van-row> 5 <van-row>
6 <van-col span="4"> 6 <van-col span="4">
7 - <van-image round width="2.5rem" height="2.5rem" 7 + <van-image
8 - :src="getOptimizedUrl(post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')" 8 + round
9 - fit="cover" /> 9 + width="2.5rem"
10 + height="2.5rem"
11 + :src="
12 + getOptimizedUrl(post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg')
13 + "
14 + fit="cover"
15 + />
10 </van-col> 16 </van-col>
11 <van-col span="17"> 17 <van-col span="17">
12 <div class="user-info"> 18 <div class="user-info">
13 <div class="username"> 19 <div class="username">
14 {{ post.user.name }} 20 {{ post.user.name }}
15 <!-- 补卡标记 --> 21 <!-- 补卡标记 -->
16 - <span v-if="post.user.is_makeup" 22 + <span
17 - class="MakeupTag inline-flex items-center ml-2 px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-600 border border-green-300">补打卡</span> 23 + v-if="post.user.is_makeup"
24 + class="MakeupTag ml-2 inline-flex items-center rounded-full border border-green-300 bg-green-50 px-2 py-0.5 text-xs font-medium text-green-600"
25 + >补打卡</span
26 + >
18 <slot name="header-tags"></slot> 27 <slot name="header-tags"></slot>
19 </div> 28 </div>
20 <div class="post-time">{{ post.user.time }}</div> 29 <div class="post-time">{{ post.user.time }}</div>
...@@ -41,12 +50,44 @@ ...@@ -41,12 +50,44 @@
41 <div ref="textRef" class="post-text" :class="{ 'line-clamp-5': !isExpanded }"> 50 <div ref="textRef" class="post-text" :class="{ 'line-clamp-5': !isExpanded }">
42 {{ post.content }} 51 {{ post.content }}
43 </div> 52 </div>
44 - <div v-if="showExpandBtn" class="expand-btn text-blue-500 text-sm mt-1 cursor-pointer" 53 + <div
45 - @click.stop="toggleExpand"> 54 + v-if="showExpandBtn"
55 + class="expand-btn mt-1 cursor-pointer text-sm text-blue-500"
56 + @click.stop="toggleExpand"
57 + >
46 {{ isExpanded ? '收起' : '全文' }} 58 {{ isExpanded ? '收起' : '全文' }}
47 </div> 59 </div>
48 </div> 60 </div>
49 61
62 + <div v-if="hasFeedbackList" class="feedback-list mt-4 space-y-3">
63 + <div v-for="feedback in post.feedback_list" :key="feedback.id" class="feedback-item">
64 + <div
65 + v-if="feedback.time"
66 + class="feedback-time mb-2 flex items-center text-xs text-gray-500"
67 + >
68 + <van-icon name="calendar-o" size="14" color="#10b981" class="mr-1" />
69 + <span>{{ feedback.time }}</span>
70 + </div>
71 +
72 + <div v-if="feedback.note" class="feedback-note mb-2 flex items-start">
73 + <van-icon name="chat-o" size="14" color="#3b82f6" class="mr-2 mt-0.5" />
74 + <div class="text-sm leading-6 text-gray-700">{{ feedback.note }}</div>
75 + </div>
76 +
77 + <div v-if="feedback.score > 0" class="feedback-score flex items-center">
78 + <van-icon name="star-o" size="14" color="#f59e0b" class="mr-2" />
79 + <span class="mr-2 text-xs text-gray-500">评分</span>
80 + <van-rate
81 + :model-value="feedback.score"
82 + :size="14"
83 + color="#ffd21e"
84 + void-color="#eee"
85 + readonly
86 + />
87 + </div>
88 + </div>
89 + </div>
90 +
50 <!-- 媒体内容 --> 91 <!-- 媒体内容 -->
51 <div class="post-media mt-2"> 92 <div class="post-media mt-2">
52 <!-- 多附件Tab模式 --> 93 <!-- 多附件Tab模式 -->
...@@ -59,9 +100,14 @@ ...@@ -59,9 +100,14 @@
59 <template v-if="tab.name === 'image'"> 100 <template v-if="tab.name === 'image'">
60 <div class="post-images"> 101 <div class="post-images">
61 <div class="post-image-item" v-for="(image, index) in post.images" :key="index"> 102 <div class="post-image-item" v-for="(image, index) in post.images" :key="index">
62 - <van-image width="100%" height="100%" fit="cover" 103 + <van-image
63 - :src="getOptimizedUrl(image)" radius="5" 104 + width="100%"
64 - @click="openImagePreview(index)" /> 105 + height="100%"
106 + fit="cover"
107 + :src="getOptimizedUrl(image)"
108 + radius="5"
109 + @click="openImagePreview(index)"
110 + />
65 </div> 111 </div>
66 </div> 112 </div>
67 </template> 113 </template>
...@@ -70,34 +116,55 @@ ...@@ -70,34 +116,55 @@
70 <template v-if="tab.name === 'video'"> 116 <template v-if="tab.name === 'video'">
71 <div v-for="(v, idx) in post.videoList" :key="idx"> 117 <div v-for="(v, idx) in post.videoList" :key="idx">
72 <!-- 封面图 --> 118 <!-- 封面图 -->
73 - <div v-if="v.video && !v.isPlaying"
74 - class="relative w-full rounded-lg overflow-hidden"
75 - style="aspect-ratio: 16/9; margin-bottom: 1rem;">
76 - <img :src="getOptimizedUrl(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')"
77 - :alt="post.content" class="w-full h-full object-contain" />
78 - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
79 - @click="startPlay(v)">
80 <div 119 <div
81 - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> 120 + v-if="v.video && !v.isPlaying"
121 + class="relative w-full overflow-hidden rounded-lg"
122 + style="aspect-ratio: 16/9; margin-bottom: 1rem"
123 + >
124 + <img
125 + :src="
126 + getOptimizedUrl(
127 + v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'
128 + )
129 + "
130 + :alt="post.content"
131 + class="h-full w-full object-contain"
132 + />
133 + <div
134 + class="absolute inset-0 flex cursor-pointer items-center justify-center bg-black/20"
135 + @click="startPlay(v)"
136 + >
137 + <div
138 + class="flex h-16 w-16 items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70"
139 + >
82 <van-icon name="play-circle-o" class="text-white" size="40" /> 140 <van-icon name="play-circle-o" class="text-white" size="40" />
83 </div> 141 </div>
84 </div> 142 </div>
85 </div> 143 </div>
86 <!-- 视频播放器 --> 144 <!-- 视频播放器 -->
87 - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" 145 + <VideoPlayer
146 + v-if="v.video && v.isPlaying"
147 + :video-url="v.video"
88 :video-id="v.id || `video-${post.id}-${idx}`" 148 :video-id="v.id || `video-${post.id}-${idx}`"
89 - :use-native-on-ios="false" class="post-video rounded-lg overflow-hidden" 149 + :use-native-on-ios="false"
90 - :ref="(el) => setVideoRef(el, v.id)" 150 + class="post-video overflow-hidden rounded-lg"
91 - @onPlay="(player) => handleVideoPlay(player, v)" 151 + :ref="el => setVideoRef(el, v.id)"
92 - @onPause="handleVideoPause" /> 152 + @onPlay="player => handleVideoPlay(player, v)"
153 + @onPause="handleVideoPause"
154 + />
93 </div> 155 </div>
94 </template> 156 </template>
95 157
96 <!-- 音频内容 --> 158 <!-- 音频内容 -->
97 <template v-if="tab.name === 'audio'"> 159 <template v-if="tab.name === 'audio'">
98 - <AudioPlayer v-if="post.audio && post.audio.length" :songs="post.audio" 160 + <AudioPlayer
99 - class="post-audio" :id="post.id" :ref="(el) => setAudioRef(el, post.id)" 161 + v-if="post.audio && post.audio.length"
100 - @play="handleAudioPlay" /> 162 + :songs="post.audio"
163 + class="post-audio"
164 + :id="post.id"
165 + :ref="el => setAudioRef(el, post.id)"
166 + @play="handleAudioPlay"
167 + />
101 </template> 168 </template>
102 </div> 169 </div>
103 </van-tab> 170 </van-tab>
...@@ -109,8 +176,14 @@ ...@@ -109,8 +176,14 @@
109 <!-- 图片列表 --> 176 <!-- 图片列表 -->
110 <div v-if="post.images && post.images.length" class="post-images"> 177 <div v-if="post.images && post.images.length" class="post-images">
111 <div class="post-image-item" v-for="(image, index) in post.images" :key="index"> 178 <div class="post-image-item" v-for="(image, index) in post.images" :key="index">
112 - <van-image width="100%" height="100%" fit="cover" :src="getOptimizedUrl(image)" radius="5" 179 + <van-image
113 - @click="openImagePreview(index)" /> 180 + width="100%"
181 + height="100%"
182 + fit="cover"
183 + :src="getOptimizedUrl(image)"
184 + radius="5"
185 + @click="openImagePreview(index)"
186 + />
114 </div> 187 </div>
115 </div> 188 </div>
116 189
...@@ -118,33 +191,63 @@ ...@@ -118,33 +191,63 @@
118 <div v-if="post.videoList && post.videoList.length"> 191 <div v-if="post.videoList && post.videoList.length">
119 <div v-for="(v, idx) in post.videoList" :key="idx"> 192 <div v-for="(v, idx) in post.videoList" :key="idx">
120 <!-- 封面图 --> 193 <!-- 封面图 -->
121 - <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden"
122 - style="aspect-ratio: 16/9; margin-bottom: 1rem;">
123 - <img :src="getOptimizedUrl(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')"
124 - :alt="post.content" class="w-full h-full object-contain" />
125 - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20"
126 - @click="startPlay(v)">
127 <div 194 <div
128 - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> 195 + v-if="v.video && !v.isPlaying"
196 + class="relative w-full overflow-hidden rounded-lg"
197 + style="aspect-ratio: 16/9; margin-bottom: 1rem"
198 + >
199 + <img
200 + :src="
201 + getOptimizedUrl(
202 + v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'
203 + )
204 + "
205 + :alt="post.content"
206 + class="h-full w-full object-contain"
207 + />
208 + <div
209 + class="absolute inset-0 flex cursor-pointer items-center justify-center bg-black/20"
210 + @click="startPlay(v)"
211 + >
212 + <div
213 + class="flex h-16 w-16 items-center justify-center rounded-full bg-black/50 transition-colors hover:bg-black/70"
214 + >
129 <van-icon name="play-circle-o" class="text-white" size="40" /> 215 <van-icon name="play-circle-o" class="text-white" size="40" />
130 </div> 216 </div>
131 </div> 217 </div>
132 </div> 218 </div>
133 <!-- 视频播放器 --> 219 <!-- 视频播放器 -->
134 - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" 220 + <VideoPlayer
135 - :video-id="v.id || `video-${post.id}-${idx}`" :use-native-on-ios="false" 221 + v-if="v.video && v.isPlaying"
136 - class="post-video rounded-lg overflow-hidden" :ref="(el) => setVideoRef(el, v.id)" 222 + :video-url="v.video"
137 - @onPlay="(player) => handleVideoPlay(player, v)" @onPause="handleVideoPause" /> 223 + :video-id="v.id || `video-${post.id}-${idx}`"
224 + :use-native-on-ios="false"
225 + class="post-video overflow-hidden rounded-lg"
226 + :ref="el => setVideoRef(el, v.id)"
227 + @onPlay="player => handleVideoPlay(player, v)"
228 + @onPause="handleVideoPause"
229 + />
138 </div> 230 </div>
139 </div> 231 </div>
140 232
141 <!-- 音频播放器 --> 233 <!-- 音频播放器 -->
142 - <AudioPlayer v-if="post.audio && post.audio.length" :songs="post.audio" class="post-audio" 234 + <AudioPlayer
143 - :id="post.id" :ref="(el) => setAudioRef(el, post.id)" @play="handleAudioPlay" /> 235 + v-if="post.audio && post.audio.length"
236 + :songs="post.audio"
237 + class="post-audio"
238 + :id="post.id"
239 + :ref="el => setAudioRef(el, post.id)"
240 + @play="handleAudioPlay"
241 + />
144 </div> 242 </div>
145 243
146 - <van-image-preview v-if="post.images && post.images.length" v-model:show="showLocalImagePreview" 244 + <van-image-preview
147 - :images="post.images" :start-position="localStartPosition" :show-index="true" /> 245 + v-if="post.images && post.images.length"
246 + v-model:show="showLocalImagePreview"
247 + :images="post.images"
248 + :start-position="localStartPosition"
249 + :show-index="true"
250 + />
148 </div> 251 </div>
149 </div> 252 </div>
150 253
...@@ -152,8 +255,12 @@ ...@@ -152,8 +255,12 @@
152 <div class="post-footer flex items-center justify-between"> 255 <div class="post-footer flex items-center justify-between">
153 <!-- 左侧:点赞 --> 256 <!-- 左侧:点赞 -->
154 <div class="flex items-center"> 257 <div class="flex items-center">
155 - <van-icon @click="emit('like', post)" name="good-job" class="like-icon" 258 + <van-icon
156 - :color="post.is_liked ? 'red' : ''" /> 259 + @click="emit('like', post)"
260 + name="good-job"
261 + class="like-icon"
262 + :color="post.is_liked ? 'red' : ''"
263 + />
157 <span class="like-count ml-1">{{ post.likes }}</span> 264 <span class="like-count ml-1">{{ post.likes }}</span>
158 </div> 265 </div>
159 <!-- 右侧:自定义操作 --> 266 <!-- 右侧:自定义操作 -->
...@@ -195,9 +302,9 @@ ...@@ -195,9 +302,9 @@
195 * const handleLike = (post) => console.log('like', post) 302 * const handleLike = (post) => console.log('like', post)
196 */ 303 */
197 import { ref, computed, watchEffect, onMounted, nextTick, watch } from 'vue' 304 import { ref, computed, watchEffect, onMounted, nextTick, watch } from 'vue'
198 -import PostCountModel from "@/components/count/postCountModel.vue"; 305 +import PostCountModel from '@/components/count/postCountModel.vue'
199 -import VideoPlayer from "@/components/media/VideoPlayer.vue"; 306 +import VideoPlayer from '@/components/media/VideoPlayer.vue'
200 -import AudioPlayer from "@/components/media/AudioPlayer.vue"; 307 +import AudioPlayer from '@/components/media/AudioPlayer.vue'
201 308
202 const props = defineProps({ 309 const props = defineProps({
203 /** 310 /**
...@@ -214,11 +321,15 @@ const props = defineProps({ ...@@ -214,11 +321,15 @@ const props = defineProps({
214 */ 321 */
215 post: { type: Object, required: true }, 322 post: { type: Object, required: true },
216 /** 是否使用 CDN 优化链接 */ 323 /** 是否使用 CDN 优化链接 */
217 - useCdnOptimization: { type: Boolean, default: false } 324 + useCdnOptimization: { type: Boolean, default: false },
218 }) 325 })
219 326
220 const emit = defineEmits(['like', 'edit', 'delete', 'video-play', 'audio-play']) 327 const emit = defineEmits(['like', 'edit', 'delete', 'video-play', 'audio-play'])
221 328
329 +const hasFeedbackList = computed(
330 + () => Array.isArray(props.post.feedback_list) && props.post.feedback_list.length > 0
331 +)
332 +
222 // 文本折叠逻辑 333 // 文本折叠逻辑
223 const isExpanded = ref(false) 334 const isExpanded = ref(false)
224 const showExpandBtn = ref(false) 335 const showExpandBtn = ref(false)
...@@ -247,19 +358,23 @@ onMounted(() => { ...@@ -247,19 +358,23 @@ onMounted(() => {
247 }) 358 })
248 }) 359 })
249 360
250 -watch(() => props.post.content, () => { 361 +watch(
362 + () => props.post.content,
363 + () => {
251 isExpanded.value = false // Reset expansion on content change 364 isExpanded.value = false // Reset expansion on content change
252 nextTick(() => { 365 nextTick(() => {
253 checkTextOverflow() 366 checkTextOverflow()
254 }) 367 })
255 -}) 368 + }
369 +)
256 370
257 // 多媒体Tab逻辑 371 // 多媒体Tab逻辑
258 const activeTab = ref('') 372 const activeTab = ref('')
259 const mediaTabs = computed(() => { 373 const mediaTabs = computed(() => {
260 const tabs = [] 374 const tabs = []
261 if (props.post.images && props.post.images.length) tabs.push({ label: '图片', name: 'image' }) 375 if (props.post.images && props.post.images.length) tabs.push({ label: '图片', name: 'image' })
262 - if (props.post.videoList && props.post.videoList.length) tabs.push({ label: '视频', name: 'video' }) 376 + if (props.post.videoList && props.post.videoList.length)
377 + tabs.push({ label: '视频', name: 'video' })
263 if (props.post.audio && props.post.audio.length) tabs.push({ label: '音频', name: 'audio' }) 378 if (props.post.audio && props.post.audio.length) tabs.push({ label: '音频', name: 'audio' })
264 return tabs 379 return tabs
265 }) 380 })
...@@ -278,7 +393,7 @@ const localStartPosition = ref(0) ...@@ -278,7 +393,7 @@ const localStartPosition = ref(0)
278 * @description 打开图片预览 393 * @description 打开图片预览
279 * @param {number} index 图片索引 394 * @param {number} index 图片索引
280 */ 395 */
281 -const openImagePreview = (index) => { 396 +const openImagePreview = index => {
282 localStartPosition.value = index 397 localStartPosition.value = index
283 showLocalImagePreview.value = true 398 showLocalImagePreview.value = true
284 } 399 }
...@@ -319,7 +434,7 @@ const setAudioRef = (el, id) => { ...@@ -319,7 +434,7 @@ const setAudioRef = (el, id) => {
319 * @param {string} url 原始链接 434 * @param {string} url 原始链接
320 * @returns {string} 优化后的链接 435 * @returns {string} 优化后的链接
321 */ 436 */
322 -const getOptimizedUrl = (url) => { 437 +const getOptimizedUrl = url => {
323 if (!props.useCdnOptimization) return url 438 if (!props.useCdnOptimization) return url
324 if (url.includes('?')) return url 439 if (url.includes('?')) return url
325 return `${url}?imageMogr2/thumbnail/200x/strip/quality/70` 440 return `${url}?imageMogr2/thumbnail/200x/strip/quality/70`
...@@ -330,7 +445,7 @@ const getOptimizedUrl = (url) => { ...@@ -330,7 +445,7 @@ const getOptimizedUrl = (url) => {
330 * @description 开始播放视频 445 * @description 开始播放视频
331 * @param {Object} v 视频对象 446 * @param {Object} v 视频对象
332 */ 447 */
333 -const startPlay = (v) => { 448 +const startPlay = v => {
334 // 暂停当前卡片中的其他视频 449 // 暂停当前卡片中的其他视频
335 props.post.videoList.forEach(item => { 450 props.post.videoList.forEach(item => {
336 if (item.id !== v.id) item.isPlaying = false 451 if (item.id !== v.id) item.isPlaying = false
...@@ -362,7 +477,7 @@ const handleVideoPause = () => { ...@@ -362,7 +477,7 @@ const handleVideoPause = () => {
362 * @description 处理音频播放事件 477 * @description 处理音频播放事件
363 * @param {Object} player 播放器实例 478 * @param {Object} player 播放器实例
364 */ 479 */
365 -const handleAudioPlay = (player) => { 480 +const handleAudioPlay = player => {
366 // 停止本地视频 481 // 停止本地视频
367 stopLocalVideos() 482 stopLocalVideos()
368 // 通知父组件 483 // 通知父组件
...@@ -378,7 +493,7 @@ const stopLocalVideos = () => { ...@@ -378,7 +493,7 @@ const stopLocalVideos = () => {
378 player.pause() 493 player.pause()
379 } 494 }
380 }) 495 })
381 - props.post.videoList.forEach(v => v.isPlaying = false) 496 + props.post.videoList.forEach(v => (v.isPlaying = false))
382 } 497 }
383 498
384 /** 499 /**
...@@ -400,7 +515,7 @@ defineExpose({ ...@@ -400,7 +515,7 @@ defineExpose({
400 stopLocalAudio() 515 stopLocalAudio()
401 }, 516 },
402 // 暴露 post id 以便父组件查找 517 // 暴露 post id 以便父组件查找
403 - id: props.post.id 518 + id: props.post.id,
404 }) 519 })
405 </script> 520 </script>
406 521
...@@ -458,6 +573,15 @@ defineExpose({ ...@@ -458,6 +573,15 @@ defineExpose({
458 word-wrap: break-word; 573 word-wrap: break-word;
459 } 574 }
460 575
576 + .feedback-list {
577 + .feedback-item {
578 + padding: 0.75rem;
579 + border-radius: 0.75rem;
580 + background: #f8fafc;
581 + border: 1px solid #e5e7eb;
582 + }
583 + }
584 +
461 .post-media { 585 .post-media {
462 .post-images { 586 .post-images {
463 display: flex; 587 display: flex;
......
...@@ -10,10 +10,19 @@ ...@@ -10,10 +10,19 @@
10 <van-config-provider :theme-vars="themeVars"> 10 <van-config-provider :theme-vars="themeVars">
11 <!-- 固定的日历组件 --> 11 <!-- 固定的日历组件 -->
12 <div class="fixed-calendar" ref="fixedCalendarWrapper"> 12 <div class="fixed-calendar" ref="fixedCalendarWrapper">
13 - <CollapsibleCalendar ref="calendarRef" :title="taskDetail.title" :formatter="formatter" 13 + <CollapsibleCalendar
14 - :subtask-list="taskDetail.subtask_list" :has-selected-date="hasUserSelectedDate" v-model="selectedDate" 14 + ref="calendarRef"
15 - @select="onSelectDay" @click-subtitle="onClickSubtitle" @panel-change="onPanelChange" 15 + :title="taskDetail.title"
16 - @select-course="onSelectCourse" @clear-date="onClearSelectedDate" /> 16 + :formatter="formatter"
17 + :subtask-list="taskDetail.subtask_list"
18 + :has-selected-date="hasUserSelectedDate"
19 + v-model="selectedDate"
20 + @select="onSelectDay"
21 + @click-subtitle="onClickSubtitle"
22 + @panel-change="onPanelChange"
23 + @select-course="onSelectCourse"
24 + @clear-date="onClearSelectedDate"
25 + />
17 </div> 26 </div>
18 27
19 <!-- 可滚动的内容区域 --> 28 <!-- 可滚动的内容区域 -->
...@@ -27,26 +36,31 @@ ...@@ -27,26 +36,31 @@
27 </div> 36 </div>
28 </div> 37 </div>
29 38
30 - <div v-if="taskDetail.task_type === 'count'" class="text-wrapper" style="padding-bottom: 0;"> 39 + <div v-if="taskDetail.task_type === 'count'" class="text-wrapper" style="padding-bottom: 0">
31 <div class="text-header">我的{{ dynamicFieldText }}</div> 40 <div class="text-header">我的{{ dynamicFieldText }}</div>
32 - <div style="margin: 0.5rem 0;"> 41 + <div style="margin: 0.5rem 0">
33 <div class="flex items-center"> 42 <div class="flex items-center">
34 <!-- 今日次数 --> 43 <!-- 今日次数 -->
35 - <div class="flex-1 flex flex-col items-center border-r border-gray-100"> 44 + <div class="flex flex-1 flex-col items-center border-r border-gray-100">
36 <div class="flex items-baseline"> 45 <div class="flex items-baseline">
37 - <span class="text-3xl font-bold text-[#4caf50] leading-none">{{ myTodayGratitudeCount }}</span> 46 + <span class="text-3xl font-bold leading-none text-[#4caf50]">{{
38 - <span class="text-xs text-gray-400 ml-1 transform translate-y-0.5">次</span> 47 + myTodayGratitudeCount
48 + }}</span>
49 + <span class="ml-1 translate-y-0.5 transform text-xs text-gray-400">次</span>
39 </div> 50 </div>
40 - <div class="text-xs text-gray-500 mt-2">今日次数</div> 51 + <div class="mt-2 text-xs text-gray-500">今日次数</div>
41 </div> 52 </div>
42 <!-- 累计次数 --> 53 <!-- 累计次数 -->
43 - <div class="flex-1 flex flex-col items-center"> 54 + <div class="flex flex-1 flex-col items-center">
44 <div class="flex items-baseline"> 55 <div class="flex items-baseline">
45 - <span class="text-3xl font-bold text-[#ff9800] leading-none" style="color: #ff9800;">{{ 56 + <span
46 - myTotalGratitudeCount }}</span> 57 + class="text-3xl font-bold leading-none text-[#ff9800]"
47 - <span class="text-xs text-gray-400 ml-1 transform translate-y-0.5">次</span> 58 + style="color: #ff9800"
59 + >{{ myTotalGratitudeCount }}</span
60 + >
61 + <span class="ml-1 translate-y-0.5 transform text-xs text-gray-400">次</span>
48 </div> 62 </div>
49 - <div class="text-xs text-gray-500 mt-2">累计次数</div> 63 + <div class="mt-2 text-xs text-gray-500">累计次数</div>
50 </div> 64 </div>
51 </div> 65 </div>
52 </div> 66 </div>
...@@ -54,17 +68,17 @@ ...@@ -54,17 +68,17 @@
54 68
55 <div v-if="showProgress" class="text-wrapper"> 69 <div v-if="showProgress" class="text-wrapper">
56 <div class="text-header">目标进度</div> 70 <div class="text-header">目标进度</div>
57 - <div style="background-color: #FFF; margin-top: 1rem;"> 71 + <div style="background-color: #fff; margin-top: 1rem">
58 <div class="grade-percentage-main"> 72 <div class="grade-percentage-main">
59 - <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem;"> 73 + <van-row justify="space-between" style="margin: 0.5rem 0; font-size: 0.9rem">
60 <van-col span="12"> 74 <van-col span="12">
61 <span>作业目标</span> 75 <span>作业目标</span>
62 </van-col> 76 </van-col>
63 - <van-col span="12" style="text-align: right;"> 77 + <van-col span="12" style="text-align: right">
64 - <span style="font-weight: bold;">{{ progress1 }}%</span> 78 + <span style="font-weight: bold">{{ progress1 }}%</span>
65 </van-col> 79 </van-col>
66 </van-row> 80 </van-row>
67 - <div style="overflow: hidden;"> 81 + <div style="overflow: hidden">
68 <van-progress :percentage="progress1" color="#4caf50" :show-pivot="false" /> 82 <van-progress :percentage="progress1" color="#4caf50" :show-pivot="false" />
69 </div> 83 </div>
70 </div> 84 </div>
...@@ -79,32 +93,58 @@ ...@@ -79,32 +93,58 @@
79 </van-row> 93 </van-row>
80 <van-progress :percentage="progress2" color="#4caf50" :show-pivot="false" /> 94 <van-progress :percentage="progress2" color="#4caf50" :show-pivot="false" />
81 </div> --> 95 </div> -->
82 - <div style="padding: 0.75rem 1rem;"> 96 + <div style="padding: 0.75rem 1rem">
83 - <van-image round width="2.8rem" height="2.8rem" 97 + <van-image
84 - :src="item ? item : 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'" fit="cover" 98 + round
85 - v-for="(item, index) in teamAvatars" :key="index" 99 + width="2.8rem"
86 - :style="{ marginLeft: index > 0 ? '-0.5rem' : '', border: '2px solid #eff6ff', background: '#fff' }" /> 100 + height="2.8rem"
101 + :src="item ? item : 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg'"
102 + fit="cover"
103 + v-for="(item, index) in teamAvatars"
104 + :key="index"
105 + :style="{
106 + marginLeft: index > 0 ? '-0.5rem' : '',
107 + border: '2px solid #eff6ff',
108 + background: '#fff',
109 + }"
110 + />
87 </div> 111 </div>
88 </div> 112 </div>
89 </div> 113 </div>
90 114
91 <div class="text-wrapper"> 115 <div class="text-wrapper">
92 <div class="text-header">打卡动态</div> 116 <div class="text-header">打卡动态</div>
93 - <van-list v-if="checkinDataList.length" v-model:loading="loading" :finished="finished" finished-text="没有更多了" 117 + <van-list
94 - @load="onLoad" class="py-3 space-y-4"> 118 + v-if="checkinDataList.length"
95 - <CheckinCard v-for="post in checkinDataList" :key="post.id" :post="post" :use-cdn-optimization="true" 119 + v-model:loading="loading"
96 - :ref="(el) => setCheckinCardRef(el, post.id)" @like="handLike" @edit="editCheckin" @delete="delCheckin" 120 + :finished="finished"
97 - @video-play="handleVideoPlay" @audio-play="handleAudioPlay"> 121 + finished-text="没有更多了"
122 + @load="onLoad"
123 + class="space-y-4 py-3"
124 + >
125 + <CheckinCard
126 + v-for="post in checkinDataList"
127 + :key="post.id"
128 + :post="post"
129 + :use-cdn-optimization="true"
130 + :ref="el => setCheckinCardRef(el, post.id)"
131 + @like="handLike"
132 + @edit="editCheckin"
133 + @delete="delCheckin"
134 + @video-play="handleVideoPlay"
135 + @audio-play="handleAudioPlay"
136 + >
98 <template #content-top> 137 <template #content-top>
99 - <div class="text-gray-500 font-bold text-sm mb-4">{{ post.subtask_title }}</div> 138 + <div class="mb-4 text-sm font-bold text-gray-500">{{ post.subtask_title }}</div>
100 </template> 139 </template>
101 </CheckinCard> 140 </CheckinCard>
102 </van-list> 141 </van-list>
103 <van-empty v-else description="暂无数据" /> 142 <van-empty v-else description="暂无数据" />
104 </div> 143 </div>
105 144
106 - <div style="height: 10rem;"></div> 145 + <div style="height: 10rem"></div>
107 - </div> <!-- 闭合 scrollable-content --> 146 + </div>
147 + <!-- 闭合 scrollable-content -->
108 </van-config-provider> 148 </van-config-provider>
109 149
110 <van-dialog v-model:show="dialog_show" title="标题" show-cancel-button></van-dialog> 150 <van-dialog v-model:show="dialog_show" title="标题" show-cancel-button></van-dialog>
...@@ -112,8 +152,11 @@ ...@@ -112,8 +152,11 @@
112 <van-back-top right="5vw" bottom="25vh" offset="600" /> 152 <van-back-top right="5vw" bottom="25vh" offset="600" />
113 153
114 <!-- 底部悬浮打卡按钮 --> 154 <!-- 底部悬浮打卡按钮 -->
115 - <div v-if="is_task_detail_ready && !is_task_finished" class="floating-checkin-button" 155 + <div
116 - :class="{ 'is-compact': isCompactButton }"> 156 + v-if="is_task_detail_ready && !is_task_finished"
157 + class="floating-checkin-button"
158 + :class="{ 'is-compact': isCompactButton }"
159 + >
117 <van-button type="primary" round @click="goToCheckinDetailPage" class="checkin-action-button"> 160 <van-button type="primary" round @click="goToCheckinDetailPage" class="checkin-action-button">
118 <van-icon name="edit" size="1.2rem" /> 161 <van-icon name="edit" size="1.2rem" />
119 <span class="button-text">我要打卡</span> 162 <span class="button-text">我要打卡</span>
...@@ -124,29 +167,45 @@ ...@@ -124,29 +167,45 @@
124 167
125 <script> 168 <script>
126 export default { 169 export default {
127 - name: 'IndexCheckInPage' 170 + name: 'IndexCheckInPage',
128 } 171 }
129 </script> 172 </script>
130 173
131 <script setup> 174 <script setup>
132 -import { ref, onBeforeUnmount, onMounted, computed, nextTick, getCurrentInstance, onActivated, onDeactivated } from 'vue' 175 +import {
176 + ref,
177 + onBeforeUnmount,
178 + onMounted,
179 + computed,
180 + nextTick,
181 + getCurrentInstance,
182 + onActivated,
183 + onDeactivated,
184 +} from 'vue'
133 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router' 185 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
134 -import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant'; 186 +import { showConfirmDialog, showSuccessToast, showFailToast, showLoadingToast } from 'vant'
135 -import AppLayout from "@/components/layout/AppLayout.vue"; 187 +import AppLayout from '@/components/layout/AppLayout.vue'
136 -import FrostedGlass from "@/components/effects/FrostedGlass.vue"; 188 +import FrostedGlass from '@/components/effects/FrostedGlass.vue'
137 -import CollapsibleCalendar from "@/components/calendar/CollapsibleCalendar.vue"; 189 +import CollapsibleCalendar from '@/components/calendar/CollapsibleCalendar.vue'
138 -import PostCountModel from "@/components/count/postCountModel.vue"; 190 +import PostCountModel from '@/components/count/postCountModel.vue'
139 -import CheckinCard from "@/components/checkin/CheckinCard.vue"; 191 +import CheckinCard from '@/components/checkin/CheckinCard.vue'
140 -import { useScrollRestoration } from "@/composables/useScrollRestoration"; 192 +import { useScrollRestoration } from '@/composables/useScrollRestoration'
141 -import { useTitle, useResizeObserver, useScroll } from '@vueuse/core'; 193 +import { useTitle, useResizeObserver, useScroll } from '@vueuse/core'
142 -import dayjs from 'dayjs'; 194 +import dayjs from 'dayjs'
143 - 195 +
144 -import { getTaskDetailAPI, getUploadTaskListAPI, delUploadTaskInfoAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI, getUploadTaskInfoAPI } from "@/api/checkin"; 196 +import {
145 -import { getTeacherFindSettingsAPI } from "@/api/teacher"; 197 + getTaskDetailAPI,
198 + getUploadTaskListAPI,
199 + delUploadTaskInfoAPI,
200 + likeUploadTaskInfoAPI,
201 + dislikeUploadTaskInfoAPI,
202 + getUploadTaskInfoAPI,
203 +} from '@/api/checkin'
204 +import { getTeacherFindSettingsAPI } from '@/api/teacher'
146 205
147 const route = useRoute() 206 const route = useRoute()
148 const router = useRouter() 207 const router = useRouter()
149 -useTitle(route.meta.title); 208 +useTitle(route.meta.title)
150 209
151 // 滚动监听 210 // 滚动监听
152 const { y } = useScroll(window) 211 const { y } = useScroll(window)
...@@ -161,73 +220,73 @@ const is_task_finished = computed(() => { ...@@ -161,73 +220,73 @@ const is_task_finished = computed(() => {
161 // 动态字段文字 220 // 动态字段文字
162 const dynamicFieldText = ref('感恩') 221 const dynamicFieldText = ref('感恩')
163 222
164 -const myRefCalendar = ref(null); 223 +const myRefCalendar = ref(null)
165 224
166 // 窗口尺寸相关的响应式数据 225 // 窗口尺寸相关的响应式数据
167 -const windowHeight = ref(window.innerHeight); 226 +const windowHeight = ref(window.innerHeight)
168 -const windowWidth = ref(window.innerWidth); 227 +const windowWidth = ref(window.innerWidth)
169 228
170 // 日历高度相关的响应式数据 229 // 日历高度相关的响应式数据
171 -const calendarRef = ref(null); 230 +const calendarRef = ref(null)
172 -const fixedCalendarWrapper = ref(null); 231 +const fixedCalendarWrapper = ref(null)
173 -const calendarHeight = ref(200); // 默认高度 232 +const calendarHeight = ref(200) // 默认高度
174 233
175 // 使用 ResizeObserver 监听日历容器高度变化 234 // 使用 ResizeObserver 监听日历容器高度变化
176 -useResizeObserver(fixedCalendarWrapper, (entries) => { 235 +useResizeObserver(fixedCalendarWrapper, entries => {
177 - const entry = entries[0]; 236 + const entry = entries[0]
178 if (entry && entry.target) { 237 if (entry && entry.target) {
179 // 使用 getBoundingClientRect 获取包含 padding 和 border 的完整高度 238 // 使用 getBoundingClientRect 获取包含 padding 和 border 的完整高度
180 - calendarHeight.value = entry.target.getBoundingClientRect().height; 239 + calendarHeight.value = entry.target.getBoundingClientRect().height
181 } 240 }
182 -}); 241 +})
183 242
184 /** 243 /**
185 * 监听窗口尺寸变化 244 * 监听窗口尺寸变化
186 */ 245 */
187 const handleResize = () => { 246 const handleResize = () => {
188 - windowHeight.value = window.innerHeight; 247 + windowHeight.value = window.innerHeight
189 - windowWidth.value = window.innerWidth; 248 + windowWidth.value = window.innerWidth
190 -}; 249 +}
191 250
192 // 组件挂载时添加事件监听 251 // 组件挂载时添加事件监听
193 onMounted(() => { 252 onMounted(() => {
194 - window.addEventListener('resize', handleResize); 253 + window.addEventListener('resize', handleResize)
195 // 监听屏幕方向变化(移动端) 254 // 监听屏幕方向变化(移动端)
196 window.addEventListener('orientationchange', () => { 255 window.addEventListener('orientationchange', () => {
197 // 延迟更新,等待方向变化完成 256 // 延迟更新,等待方向变化完成
198 - setTimeout(handleResize, 100); 257 + setTimeout(handleResize, 100)
199 - }); 258 + })
200 -}); 259 +})
201 260
202 // CheckinCard refs 261 // CheckinCard refs
203 -const checkinCardRefs = ref(new Map()); 262 +const checkinCardRefs = ref(new Map())
204 const setCheckinCardRef = (el, id) => { 263 const setCheckinCardRef = (el, id) => {
205 - if (el) checkinCardRefs.value.set(id, el); 264 + if (el) checkinCardRefs.value.set(id, el)
206 -}; 265 +}
207 266
208 // 组件卸载前清理播放器引用和事件监听器 267 // 组件卸载前清理播放器引用和事件监听器
209 onBeforeUnmount(() => { 268 onBeforeUnmount(() => {
210 // 清理事件监听器 269 // 清理事件监听器
211 - window.removeEventListener('resize', handleResize); 270 + window.removeEventListener('resize', handleResize)
212 - window.removeEventListener('orientationchange', handleResize); 271 + window.removeEventListener('orientationchange', handleResize)
213 -}); 272 +})
214 273
215 // 视频播放事件处理 274 // 视频播放事件处理
216 const handleVideoPlay = ({ post, player, videoId }) => { 275 const handleVideoPlay = ({ post, player, videoId }) => {
217 checkinCardRefs.value.forEach((card, id) => { 276 checkinCardRefs.value.forEach((card, id) => {
218 if (id !== post.id) { 277 if (id !== post.id) {
219 - card.stopAllMedia(); 278 + card.stopAllMedia()
220 } 279 }
221 - }); 280 + })
222 } 281 }
223 282
224 // 音频播放事件处理 283 // 音频播放事件处理
225 const handleAudioPlay = ({ post, player }) => { 284 const handleAudioPlay = ({ post, player }) => {
226 checkinCardRefs.value.forEach((card, id) => { 285 checkinCardRefs.value.forEach((card, id) => {
227 if (id !== post.id) { 286 if (id !== post.id) {
228 - card.stopAllMedia(); 287 + card.stopAllMedia()
229 } 288 }
230 - }); 289 + })
231 } 290 }
232 291
233 const themeVars = { 292 const themeVars = {
...@@ -236,30 +295,29 @@ const themeVars = { ...@@ -236,30 +295,29 @@ const themeVars = {
236 calendarInfoLineHeight: '0.3rem', 295 calendarInfoLineHeight: '0.3rem',
237 } 296 }
238 297
239 -const progress1 = ref(0); 298 +const progress1 = ref(0)
240 // const progress2 = ref(76); 299 // const progress2 = ref(76);
241 300
242 const teamAvatars = ref([]) 301 const teamAvatars = ref([])
243 302
244 -
245 /** 303 /**
246 * 日历日期格式化函数 304 * 日历日期格式化函数
247 * @param {Object} day - 日期对象 305 * @param {Object} day - 日期对象
248 * @returns {Object} 格式化后的日期对象 306 * @returns {Object} 格式化后的日期对象
249 */ 307 */
250 -const formatter = (day) => { 308 +const formatter = day => {
251 - const year = day.date.getFullYear(); 309 + const year = day.date.getFullYear()
252 - const month = day.date.getMonth() + 1; 310 + const month = day.date.getMonth() + 1
253 - const date = day.date.getDate(); 311 + const date = day.date.getDate()
254 312
255 - let checkin_days = myCheckinDates.value; 313 + const checkin_days = myCheckinDates.value
256 - let fill_checkin_days = myFillCheckinDates.value; 314 + const fill_checkin_days = myFillCheckinDates.value
257 315
258 // 禁用未来日期 316 // 禁用未来日期
259 if (dayjs(day.date).isAfter(new Date(), 'day')) { 317 if (dayjs(day.date).isAfter(new Date(), 'day')) {
260 - day.type = 'disabled'; 318 + day.type = 'disabled'
261 - day.className = 'calendar-disabled'; 319 + day.className = 'calendar-disabled'
262 - return day; 320 + return day
263 } 321 }
264 322
265 // 如果选中的是全部作业,不执行打卡状态检查 323 // 如果选中的是全部作业,不执行打卡状态检查
...@@ -267,17 +325,17 @@ const formatter = (day) => { ...@@ -267,17 +325,17 @@ const formatter = (day) => {
267 // 检查当前日期是否在签到日期列表中 325 // 检查当前日期是否在签到日期列表中
268 if (checkin_days && checkin_days.length > 0) { 326 if (checkin_days && checkin_days.length > 0) {
269 // 格式化当前日期为YYYY-MM-DD格式,与checkin_days中的格式匹配 327 // 格式化当前日期为YYYY-MM-DD格式,与checkin_days中的格式匹配
270 - const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`; 328 + const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`
271 329
272 // 检查是否已打卡 330 // 检查是否已打卡
273 if (checkin_days.includes(formattedDate)) { 331 if (checkin_days.includes(formattedDate)) {
274 - day.type = 'selected'; 332 + day.type = 'selected'
275 - day.bottomInfo = '已打卡'; 333 + day.bottomInfo = '已打卡'
276 // 如果是当前选中的已打卡日期,使用特殊样式 334 // 如果是当前选中的已打卡日期,使用特殊样式
277 if (selectedDate.value === formattedDate) { 335 if (selectedDate.value === formattedDate) {
278 - day.className = 'calendar-selected'; 336 + day.className = 'calendar-selected'
279 } else { 337 } else {
280 - day.className = 'calendar-checkin'; 338 + day.className = 'calendar-checkin'
281 } 339 }
282 } 340 }
283 } 341 }
...@@ -285,18 +343,18 @@ const formatter = (day) => { ...@@ -285,18 +343,18 @@ const formatter = (day) => {
285 // 检查当前日期是否在补卡日期列表中 343 // 检查当前日期是否在补卡日期列表中
286 if (fill_checkin_days && fill_checkin_days.length > 0) { 344 if (fill_checkin_days && fill_checkin_days.length > 0) {
287 // 格式化当前日期为YYYY-MM-DD格式,与fill_checkin_days中的格式匹配 345 // 格式化当前日期为YYYY-MM-DD格式,与fill_checkin_days中的格式匹配
288 - const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`; 346 + const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`
289 // 检查是否已补卡 347 // 检查是否已补卡
290 if (fill_checkin_days.includes(formattedDate)) { 348 if (fill_checkin_days.includes(formattedDate)) {
291 // 如果是当前选中的已补卡日期,使用特殊样式 349 // 如果是当前选中的已补卡日期,使用特殊样式
292 - day.type = 'selected'; 350 + day.type = 'selected'
293 - day.bottomInfo = '待补卡'; 351 + day.bottomInfo = '待补卡'
294 if (selectedDate.value === formattedDate) { 352 if (selectedDate.value === formattedDate) {
295 - day.className = 'calendar-selected'; 353 + day.className = 'calendar-selected'
296 // 选中的是补卡日期 354 // 选中的是补卡日期
297 - isPatchCheckin.value = true; 355 + isPatchCheckin.value = true
298 } else { 356 } else {
299 - day.className = 'calendar-fill-checkin'; 357 + day.className = 'calendar-fill-checkin'
300 } 358 }
301 } 359 }
302 } 360 }
...@@ -304,16 +362,16 @@ const formatter = (day) => { ...@@ -304,16 +362,16 @@ const formatter = (day) => {
304 362
305 // 选中今天的日期 363 // 选中今天的日期
306 if (dayjs(day.date).isSame(new Date(), 'day')) { 364 if (dayjs(day.date).isSame(new Date(), 'day')) {
307 - day.className = 'calendar-today'; 365 + day.className = 'calendar-today'
308 - day.type = 'selected'; 366 + day.type = 'selected'
309 - day.bottomInfo = '今日'; 367 + day.bottomInfo = '今日'
310 } 368 }
311 369
312 - return day; 370 + return day
313 } 371 }
314 372
315 // 添加一个响应式变量来存储当前选中的日期 373 // 添加一个响应式变量来存储当前选中的日期
316 -const selectedDate = ref(new Date()); 374 +const selectedDate = ref(new Date())
317 375
318 const hasUserSelectedDate = ref(!!route.query.date) 376 const hasUserSelectedDate = ref(!!route.query.date)
319 const isInitializing = ref(true) 377 const isInitializing = ref(true)
...@@ -323,7 +381,7 @@ const onClearSelectedDate = () => { ...@@ -323,7 +381,7 @@ const onClearSelectedDate = () => {
323 delete next_query.date 381 delete next_query.date
324 router.replace({ 382 router.replace({
325 path: route.path, 383 path: route.path,
326 - query: next_query 384 + query: next_query,
327 }) 385 })
328 386
329 hasUserSelectedDate.value = false 387 hasUserSelectedDate.value = false
...@@ -336,19 +394,19 @@ const onClearSelectedDate = () => { ...@@ -336,19 +394,19 @@ const onClearSelectedDate = () => {
336 onLoad(null, false) 394 onLoad(null, false)
337 } 395 }
338 396
339 -const onSelectDay = (day) => { 397 +const onSelectDay = day => {
340 - const currentSelectedDate = dayjs(day).format('YYYY-MM-DD'); 398 + const currentSelectedDate = dayjs(day).format('YYYY-MM-DD')
341 399
342 if (isInitializing.value && !hasUserSelectedDate.value) { 400 if (isInitializing.value && !hasUserSelectedDate.value) {
343 - selectedDate.value = currentSelectedDate; 401 + selectedDate.value = currentSelectedDate
344 - return; 402 + return
345 } 403 }
346 404
347 - getTaskDetail(dayjs(day).format('YYYY-MM')); 405 + getTaskDetail(dayjs(day).format('YYYY-MM'))
348 406
349 // 更新当前选中的日期 407 // 更新当前选中的日期
350 - selectedDate.value = currentSelectedDate; 408 + selectedDate.value = currentSelectedDate
351 - hasUserSelectedDate.value = true; 409 + hasUserSelectedDate.value = true
352 410
353 // 修改浏览器地址把当前的date加入地址栏, 页面不刷新 411 // 修改浏览器地址把当前的date加入地址栏, 页面不刷新
354 // 使用replace替代push,避免在浏览器历史记录中添加多个条目 412 // 使用replace替代push,避免在浏览器历史记录中添加多个条目
...@@ -356,8 +414,8 @@ const onSelectDay = (day) => { ...@@ -356,8 +414,8 @@ const onSelectDay = (day) => {
356 path: route.path, 414 path: route.path,
357 query: { 415 query: {
358 ...route.query, 416 ...route.query,
359 - date: currentSelectedDate 417 + date: currentSelectedDate,
360 - } 418 + },
361 }) 419 })
362 // 重置分页参数 420 // 重置分页参数
363 page.value = 0 421 page.value = 0
...@@ -367,7 +425,7 @@ const onSelectDay = (day) => { ...@@ -367,7 +425,7 @@ const onSelectDay = (day) => {
367 onLoad(currentSelectedDate, true) 425 onLoad(currentSelectedDate, true)
368 } 426 }
369 427
370 -const onClickSubtitle = (evt) => { 428 +const onClickSubtitle = evt => {
371 // console.warn('点击了日期标题', evt); 429 // console.warn('点击了日期标题', evt);
372 } 430 }
373 431
...@@ -377,7 +435,7 @@ const onClickSubtitle = (evt) => { ...@@ -377,7 +435,7 @@ const onClickSubtitle = (evt) => {
377 */ 435 */
378 const onPanelChange = ({ date }) => { 436 const onPanelChange = ({ date }) => {
379 // console.warn('面板切换', date); 437 // console.warn('面板切换', date);
380 - getTaskDetail(dayjs(date).format('YYYY-MM')); 438 + getTaskDetail(dayjs(date).format('YYYY-MM'))
381 } 439 }
382 440
383 /** 441 /**
...@@ -386,16 +444,19 @@ const onPanelChange = ({ date }) => { ...@@ -386,16 +444,19 @@ const onPanelChange = ({ date }) => {
386 */ 444 */
387 const selectedSubtaskId = ref('') 445 const selectedSubtaskId = ref('')
388 446
389 -const onSelectCourse = (course) => { 447 +const onSelectCourse = course => {
390 - selectedSubtaskId.value = course; 448 + selectedSubtaskId.value = course
391 // 切换作业后, 刷新当前日期的打卡详情 449 // 切换作业后, 刷新当前日期的打卡详情
392 - getTaskDetail(dayjs(selectedDate.value).format('YYYY-MM')); 450 + getTaskDetail(dayjs(selectedDate.value).format('YYYY-MM'))
393 // 重置分页参数 451 // 重置分页参数
394 page.value = 0 452 page.value = 0
395 checkinDataList.value = [] 453 checkinDataList.value = []
396 finished.value = false 454 finished.value = false
397 // 重新加载数据 455 // 重新加载数据
398 - onLoad(hasUserSelectedDate.value ? (route.query.date || selectedDate.value) : null, hasUserSelectedDate.value) 456 + onLoad(
457 + hasUserSelectedDate.value ? route.query.date || selectedDate.value : null,
458 + hasUserSelectedDate.value
459 + )
399 } 460 }
400 461
401 /** 462 /**
...@@ -403,15 +464,15 @@ const onSelectCourse = (course) => { ...@@ -403,15 +464,15 @@ const onSelectCourse = (course) => {
403 * @param {string} type - 打卡类型 464 * @param {string} type - 打卡类型
404 * @returns {string} 图标名称 465 * @returns {string} 图标名称
405 */ 466 */
406 -const getIconName = (type) => { 467 +const getIconName = type => {
407 const iconMap = { 468 const iconMap = {
408 - 'text': 'edit', 469 + text: 'edit',
409 - 'image': 'photo', 470 + image: 'photo',
410 - 'video': 'video', 471 + video: 'video',
411 - 'audio': 'music' 472 + audio: 'music',
412 - }; 473 + }
413 - return iconMap[type] || 'edit'; 474 + return iconMap[type] || 'edit'
414 -}; 475 +}
415 476
416 /** 477 /**
417 * 处理打卡类型点击事件 478 * 处理打卡类型点击事件
...@@ -445,8 +506,11 @@ const getIconName = (type) => { ...@@ -445,8 +506,11 @@ const getIconName = (type) => {
445 * 业务逻辑调整, 需要把打卡类型带到下一页判断 506 * 业务逻辑调整, 需要把打卡类型带到下一页判断
446 */ 507 */
447 const goToCheckinDetailPage = () => { 508 const goToCheckinDetailPage = () => {
448 - const current_date = route.query.date || dayjs().format('YYYY-MM-DD'); 509 + const current_date = route.query.date || dayjs().format('YYYY-MM-DD')
449 - save_checkin_scroll_state({ return_full_path: route.fullPath, calendar_height: get_calendar_offset() }) 510 + save_checkin_scroll_state({
511 + return_full_path: route.fullPath,
512 + calendar_height: get_calendar_offset(),
513 + })
450 router.push({ 514 router.push({
451 path: '/checkin/detail', 515 path: '/checkin/detail',
452 query: { 516 query: {
...@@ -456,31 +520,35 @@ const goToCheckinDetailPage = () => { ...@@ -456,31 +520,35 @@ const goToCheckinDetailPage = () => {
456 date: current_date, 520 date: current_date,
457 is_patch: isPatchCheckin.value ? '1' : '0', 521 is_patch: isPatchCheckin.value ? '1' : '0',
458 task_type: taskDetail.value.task_type, 522 task_type: taskDetail.value.task_type,
459 - } 523 + },
460 }) 524 })
461 } 525 }
462 526
463 -const handLike = async (post) => { 527 +const handLike = async post => {
464 if (!post.is_liked) { 528 if (!post.is_liked) {
465 - const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, }) 529 + const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id })
466 if (code === 1) { 530 if (code === 1) {
467 showSuccessToast('点赞成功') 531 showSuccessToast('点赞成功')
468 - post.likes++; 532 + post.likes++
469 - post.is_liked = true; 533 + post.is_liked = true
470 } 534 }
471 } else { 535 } else {
472 - const { code, data } = await dislikeUploadTaskInfoAPI({ checkin_id: post.id, }) 536 + const { code, data } = await dislikeUploadTaskInfoAPI({ checkin_id: post.id })
473 if (code === 1) { 537 if (code === 1) {
474 showSuccessToast('取消点赞成功') 538 showSuccessToast('取消点赞成功')
475 - post.likes--; 539 + post.likes--
476 - post.is_liked = false; 540 + post.is_liked = false
477 } 541 }
478 } 542 }
479 } 543 }
480 544
481 -const editCheckin = (post) => { 545 +const editCheckin = post => {
482 // 统一跳转到CheckinDetailPage页面处理所有类型的编辑 546 // 统一跳转到CheckinDetailPage页面处理所有类型的编辑
483 - save_checkin_scroll_state({ anchor_id: post.id, return_full_path: route.fullPath, calendar_height: get_calendar_offset() }) 547 + save_checkin_scroll_state({
548 + anchor_id: post.id,
549 + return_full_path: route.fullPath,
550 + calendar_height: get_calendar_offset(),
551 + })
484 router.push({ 552 router.push({
485 path: '/checkin/detail', 553 path: '/checkin/detail',
486 query: { 554 query: {
...@@ -490,11 +558,11 @@ const editCheckin = (post) => { ...@@ -490,11 +558,11 @@ const editCheckin = (post) => {
490 type: post.file_type, 558 type: post.file_type,
491 task_type: taskDetail.value.task_type, 559 task_type: taskDetail.value.task_type,
492 status: 'edit', 560 status: 'edit',
493 - } 561 + },
494 }) 562 })
495 } 563 }
496 564
497 -const delCheckin = (post) => { 565 +const delCheckin = post => {
498 showConfirmDialog({ 566 showConfirmDialog({
499 title: '温馨提示', 567 title: '温馨提示',
500 message: '您是否确定要删除该动态?', 568 message: '您是否确定要删除该动态?',
...@@ -502,68 +570,75 @@ const delCheckin = (post) => { ...@@ -502,68 +570,75 @@ const delCheckin = (post) => {
502 }) 570 })
503 .then(async () => { 571 .then(async () => {
504 // 调用接口 572 // 调用接口
505 - const { code, data } = await delUploadTaskInfoAPI({ i: post.id }); 573 + const { code, data } = await delUploadTaskInfoAPI({ i: post.id })
506 if (code === 1) { 574 if (code === 1) {
507 // 删除成功后,刷新页面 575 // 删除成功后,刷新页面
508 - showSuccessToast('删除成功'); 576 + showSuccessToast('删除成功')
509 577
510 // 优化:使用 splice 替代 filter,避免大数组重新分配内存,性能更好 578 // 优化:使用 splice 替代 filter,避免大数组重新分配内存,性能更好
511 // checkinDataList.value = checkinDataList.value.filter(item => item.id !== post.id); 579 // checkinDataList.value = checkinDataList.value.filter(item => item.id !== post.id);
512 - const index = checkinDataList.value.findIndex(item => item.id === post.id); 580 + const index = checkinDataList.value.findIndex(item => item.id === post.id)
513 if (index > -1) { 581 if (index > -1) {
514 - checkinDataList.value.splice(index, 1); 582 + checkinDataList.value.splice(index, 1)
515 } 583 }
516 584
517 // 检查是否还可以打卡 585 // 检查是否还可以打卡
518 - const current_date = route.query.date; 586 + const current_date = route.query.date
519 if (current_date) { 587 if (current_date) {
520 - getTaskDetail(dayjs(current_date).format('YYYY-MM')); 588 + getTaskDetail(dayjs(current_date).format('YYYY-MM'))
521 } else { 589 } else {
522 - getTaskDetail(dayjs().format('YYYY-MM')); 590 + getTaskDetail(dayjs().format('YYYY-MM'))
523 } 591 }
524 } else { 592 } else {
525 - showErrorToast('删除失败'); 593 + showErrorToast('删除失败')
526 } 594 }
527 }) 595 })
528 .catch(() => { 596 .catch(() => {
529 // on cancel 597 // on cancel
530 - }); 598 + })
531 } 599 }
532 600
533 -const taskDetail = ref({}); 601 +const taskDetail = ref({})
534 -const myCheckinDates = ref([]); 602 +const myCheckinDates = ref([])
535 -const myFillCheckinDates = ref([]); 603 +const myFillCheckinDates = ref([])
536 -const checkinDataList = ref([]); 604 +const checkinDataList = ref([])
537 -const showProgress = ref(true); 605 +const showProgress = ref(true)
538 // 是否为补卡模式 606 // 是否为补卡模式
539 -const isPatchCheckin = ref(false); 607 +const isPatchCheckin = ref(false)
540 608
541 // 作品类型选项 609 // 作品类型选项
542 -const attachmentTypeOptions = ref([]); 610 +const attachmentTypeOptions = ref([])
543 611
544 // 今日计数次数 612 // 今日计数次数
545 -const myTodayGratitudeCount = ref(0); 613 +const myTodayGratitudeCount = ref(0)
546 // 累计计数次数 614 // 累计计数次数
547 -const myTotalGratitudeCount = ref(0); 615 +const myTotalGratitudeCount = ref(0)
548 616
549 -const getTaskDetail = async (month) => { 617 +const getTaskDetail = async month => {
550 - const { code, data } = await getTaskDetailAPI({ i: route.query.id, month, subtask_id: selectedSubtaskId.value }); 618 + const { code, data } = await getTaskDetailAPI({
619 + i: route.query.id,
620 + month,
621 + subtask_id: selectedSubtaskId.value,
622 + })
551 if (code === 1) { 623 if (code === 1) {
552 - taskDetail.value = data; 624 + taskDetail.value = data
553 is_task_detail_ready.value = true 625 is_task_detail_ready.value = true
554 - progress1.value = ((data.checkin_number / data.target_number) * 100).toFixed(1); // 计算进度条百分比 626 + progress1.value = ((data.checkin_number / data.target_number) * 100).toFixed(1) // 计算进度条百分比
555 - showProgress.value = !isNaN(progress1.value); // 如果是NaN,就不显示进度条 627 + showProgress.value = !isNaN(progress1.value) // 如果是NaN,就不显示进度条
556 - teamAvatars.value = taskDetail.value.checkin_avatars?.length > 8 ? taskDetail.value.checkin_avatars.splice(0, 8) : taskDetail.value.checkin_avatars; 628 + teamAvatars.value =
629 + taskDetail.value.checkin_avatars?.length > 8
630 + ? taskDetail.value.checkin_avatars.splice(0, 8)
631 + : taskDetail.value.checkin_avatars
557 // 获取当前用户的打卡日期 632 // 获取当前用户的打卡日期
558 - myCheckinDates.value = data.my_checkin_dates; 633 + myCheckinDates.value = data.my_checkin_dates
559 // 获取当前用户的补卡日期 634 // 获取当前用户的补卡日期
560 - myFillCheckinDates.value = data.makeup_checkin_dates; 635 + myFillCheckinDates.value = data.makeup_checkin_dates
561 // 把['2025-06-06'] 转化为 [6] 只取日期去掉0 636 // 把['2025-06-06'] 转化为 [6] 只取日期去掉0
562 // myCheckinDates.value = myCheckinDates.value.map(date => { 637 // myCheckinDates.value = myCheckinDates.value.map(date => {
563 // return dayjs(date).date(); 638 // return dayjs(date).date();
564 // }) 639 // })
565 - myTodayGratitudeCount.value = data.my_today_gratitude_count || 0; 640 + myTodayGratitudeCount.value = data.my_today_gratitude_count || 0
566 - myTotalGratitudeCount.value = data.my_total_gratitude_count || 0; 641 + myTotalGratitudeCount.value = data.my_total_gratitude_count || 0
567 } 642 }
568 } 643 }
569 644
...@@ -573,87 +648,90 @@ const limit = ref(3) ...@@ -573,87 +648,90 @@ const limit = ref(3)
573 const page = ref(0) 648 const page = ref(0)
574 649
575 const onLoad = async (date, isUserInitiated) => { 650 const onLoad = async (date, isUserInitiated) => {
576 - const nextPage = page.value; 651 + const nextPage = page.value
577 - const current_date = date || route.query.date; 652 + const current_date = date || route.query.date
578 - const shouldPassDate = typeof isUserInitiated === 'boolean' ? isUserInitiated : hasUserSelectedDate.value 653 + const shouldPassDate =
654 + typeof isUserInitiated === 'boolean' ? isUserInitiated : hasUserSelectedDate.value
579 const params = { 655 const params = {
580 limit: limit.value, 656 limit: limit.value,
581 page: nextPage, 657 page: nextPage,
582 task_id: route.query.id, 658 task_id: route.query.id,
583 subtask_id: selectedSubtaskId.value, 659 subtask_id: selectedSubtaskId.value,
584 - }; 660 + }
585 if (current_date && shouldPassDate) { 661 if (current_date && shouldPassDate) {
586 - params.date = current_date; 662 + params.date = current_date
587 } 663 }
588 664
589 - const res = await getUploadTaskListAPI(params); 665 + const res = await getUploadTaskListAPI(params)
590 if (res.code === 1) { 666 if (res.code === 1) {
591 // 整理数据结构 667 // 整理数据结构
592 - const newItems = formatData(res.data); 668 + const newItems = formatData(res.data)
593 // 去重合并 669 // 去重合并
594 - const existingIds = new Set(checkinDataList.value.map(item => item.id)); 670 + const existingIds = new Set(checkinDataList.value.map(item => item.id))
595 - const uniqueNewItems = newItems.filter(item => !existingIds.has(item.id)); 671 + const uniqueNewItems = newItems.filter(item => !existingIds.has(item.id))
596 672
597 if (uniqueNewItems.length > 0) { 673 if (uniqueNewItems.length > 0) {
598 - checkinDataList.value.push(...uniqueNewItems); 674 + checkinDataList.value.push(...uniqueNewItems)
599 } 675 }
600 676
601 - finished.value = res.data.checkin_list.length < limit.value; 677 + finished.value = res.data.checkin_list.length < limit.value
602 - page.value = nextPage + 1; 678 + page.value = nextPage + 1
603 } 679 }
604 - loading.value = false; 680 + loading.value = false
605 -}; 681 +}
606 682
607 -const initPage = async (date) => { 683 +const initPage = async date => {
608 // 重置数据 684 // 重置数据
609 - checkinDataList.value = []; 685 + checkinDataList.value = []
610 - page.value = 0; 686 + page.value = 0
611 - finished.value = false; 687 + finished.value = false
612 - loading.value = false; 688 + loading.value = false
613 - taskDetail.value = {}; 689 + taskDetail.value = {}
614 is_task_detail_ready.value = false 690 is_task_detail_ready.value = false
615 - selectedSubtaskId.value = ''; 691 + selectedSubtaskId.value = ''
616 692
617 - const current_date = date || route.query.date; 693 + const current_date = date || route.query.date
618 - hasUserSelectedDate.value = !!current_date; 694 + hasUserSelectedDate.value = !!current_date
619 if (current_date) { 695 if (current_date) {
620 - selectedDate.value = new Date(current_date); 696 + selectedDate.value = new Date(current_date)
621 - await getTaskDetail(dayjs(current_date).format('YYYY-MM')); 697 + await getTaskDetail(dayjs(current_date).format('YYYY-MM'))
622 // 确保日历组件已挂载再调用 reset 698 // 确保日历组件已挂载再调用 reset
623 nextTick(() => { 699 nextTick(() => {
624 - calendarRef.value?.reset(new Date(current_date)); 700 + calendarRef.value?.reset(new Date(current_date))
625 }) 701 })
626 - onLoad(current_date, true); 702 + onLoad(current_date, true)
627 } else { 703 } else {
628 - selectedDate.value = new Date(); 704 + selectedDate.value = new Date()
629 - await getTaskDetail(dayjs().format('YYYY-MM')); 705 + await getTaskDetail(dayjs().format('YYYY-MM'))
630 // 确保日历组件已挂载再调用 reset 706 // 确保日历组件已挂载再调用 reset
631 nextTick(() => { 707 nextTick(() => {
632 - calendarRef.value?.reset(new Date()); 708 + calendarRef.value?.reset(new Date())
633 }) 709 })
634 - onLoad(null, false); 710 + onLoad(null, false)
635 } 711 }
636 } 712 }
637 713
638 onMounted(async () => { 714 onMounted(async () => {
639 // 记录当前的taskId 715 // 记录当前的taskId
640 - lastTaskId.value = route.query.id; 716 + lastTaskId.value = route.query.id
641 - await initPage(route.query.date); 717 + await initPage(route.query.date)
642 nextTick(() => { 718 nextTick(() => {
643 - isInitializing.value = false; 719 + isInitializing.value = false
644 }) 720 })
645 721
646 // 获取作品类型数据 722 // 获取作品类型数据
647 try { 723 try {
648 - const { code, data } = await getTeacherFindSettingsAPI(); 724 + const { code, data } = await getTeacherFindSettingsAPI()
649 if (code === 1 && data.task_attachment_type) { 725 if (code === 1 && data.task_attachment_type) {
650 - attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({ 726 + attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(
727 + ([key, value]) => ({
651 key, 728 key,
652 - value 729 + value,
653 - })); 730 + })
731 + )
654 } 732 }
655 } catch (error) { 733 } catch (error) {
656 - console.error('获取作品类型数据失败:', error); 734 + console.error('获取作品类型数据失败:', error)
657 } 735 }
658 }) 736 })
659 737
...@@ -671,39 +749,52 @@ onBeforeRouteLeave((to, from) => { ...@@ -671,39 +749,52 @@ onBeforeRouteLeave((to, from) => {
671 } 749 }
672 }) 750 })
673 751
674 -const formatData = (data) => { 752 +const formatData = data => {
675 - let formattedData = []; 753 + let formattedData = []
676 formattedData = data?.checkin_list.map((item, index) => { 754 formattedData = data?.checkin_list.map((item, index) => {
677 - let images = []; 755 + const images = []
678 - let audio = []; 756 + const audio = []
679 - let videoList = []; 757 + const videoList = []
680 758
681 // 支持多类型混合显示:遍历 files 数组根据 file_type 分类 759 // 支持多类型混合显示:遍历 files 数组根据 file_type 分类
682 if (item.files && Array.isArray(item.files)) { 760 if (item.files && Array.isArray(item.files)) {
683 item.files.forEach(file => { 761 item.files.forEach(file => {
684 // 优先使用文件自身的 file_type,如果没有则回退到 item.file_type 762 // 优先使用文件自身的 file_type,如果没有则回退到 item.file_type
685 - const type = file.file_type || item.file_type; 763 + const type = file.file_type || item.file_type
686 764
687 if (type === 'image') { 765 if (type === 'image') {
688 - images.push(file.value); 766 + images.push(file.value)
689 } else if (type === 'video') { 767 } else if (type === 'video') {
690 videoList.push({ 768 videoList.push({
691 id: file.meta_id, 769 id: file.meta_id,
692 video: file.value, 770 video: file.value,
693 videoCover: file.cover, 771 videoCover: file.cover,
694 isPlaying: false, 772 isPlaying: false,
695 - }); 773 + })
696 } else if (type === 'audio') { 774 } else if (type === 'audio') {
697 audio.push({ 775 audio.push({
698 title: file.name ? file.name : '打卡音频', 776 title: file.name ? file.name : '打卡音频',
699 artist: file.artist ? file.artist : '', 777 artist: file.artist ? file.artist : '',
700 url: file.value, 778 url: file.value,
701 cover: file.cover ? file.cover : '', 779 cover: file.cover ? file.cover : '',
702 - }); 780 + })
703 } 781 }
704 - }); 782 + })
705 } 783 }
706 784
785 + const feedback_list = Array.isArray(item.feedback_list)
786 + ? item.feedback_list.map((feedback, feedbackIndex) => {
787 + const score = Number(feedback.score ?? 0)
788 +
789 + return {
790 + id: feedback.id ?? `feedback-${item.id}-${feedbackIndex}`,
791 + time: feedback.created_time || '',
792 + note: feedback.note || '',
793 + score: Number.isNaN(score) ? 0 : score,
794 + }
795 + })
796 + : []
797 +
707 return { 798 return {
708 id: item.id, 799 id: item.id,
709 task_id: item.task_id, 800 task_id: item.task_id,
...@@ -726,9 +817,10 @@ const formatData = (data) => { ...@@ -726,9 +817,10 @@ const formatData = (data) => {
726 subtask_id: item.subtask_id, 817 subtask_id: item.subtask_id,
727 gratitude_count: item.gratitude_count, 818 gratitude_count: item.gratitude_count,
728 gratitude_form_list: item.gratitude_form_list, 819 gratitude_form_list: item.gratitude_form_list,
820 + feedback_list,
729 } 821 }
730 }) 822 })
731 - return formattedData; 823 + return formattedData
732 } 824 }
733 825
734 // 记录上次的taskId,用于判断是否切换了任务 826 // 记录上次的taskId,用于判断是否切换了任务
...@@ -758,9 +850,9 @@ const get_calendar_offset = () => { ...@@ -758,9 +850,9 @@ const get_calendar_offset = () => {
758 * @description 是否开启滚动调试日志 850 * @description 是否开启滚动调试日志
759 * @returns {boolean} 是否输出日志 851 * @returns {boolean} 是否输出日志
760 */ 852 */
761 -const is_scroll_log_enabled = () => { 853 +const is_scroll_log_enabled = () =>
762 - return String(route.query.scroll_log || '') === '1' || sessionStorage.getItem('checkin_scroll_log') === '1' 854 + String(route.query.scroll_log || '') === '1' ||
763 -} 855 + sessionStorage.getItem('checkin_scroll_log') === '1'
764 856
765 const { 857 const {
766 save_state: save_checkin_scroll_state, 858 save_state: save_checkin_scroll_state,
...@@ -805,20 +897,20 @@ onActivated(async () => { ...@@ -805,20 +897,20 @@ onActivated(async () => {
805 const lastId = String(lastTaskId.value || '') 897 const lastId = String(lastTaskId.value || '')
806 898
807 if (currentId && currentId !== lastId) { 899 if (currentId && currentId !== lastId) {
808 - lastTaskId.value = currentId; 900 + lastTaskId.value = currentId
809 // 如果任务ID变化,强制刷新整个页面数据 901 // 如果任务ID变化,强制刷新整个页面数据
810 - await initPage(route.query.date); 902 + await initPage(route.query.date)
811 // 滚动到顶部 903 // 滚动到顶部
812 clear_checkin_scroll_state() 904 clear_checkin_scroll_state()
813 const scroll_el = resolve_checkin_scroll_el() 905 const scroll_el = resolve_checkin_scroll_el()
814 if (scroll_el === window) { 906 if (scroll_el === window) {
815 - window.scrollTo(0, 0); 907 + window.scrollTo(0, 0)
816 } else if (scroll_el && typeof scroll_el.scrollTo === 'function') { 908 } else if (scroll_el && typeof scroll_el.scrollTo === 'function') {
817 - scroll_el.scrollTo({ top: 0, left: 0, behavior: 'auto' }); 909 + scroll_el.scrollTo({ top: 0, left: 0, behavior: 'auto' })
818 } else if (scroll_el) { 910 } else if (scroll_el) {
819 - scroll_el.scrollTop = 0; 911 + scroll_el.scrollTop = 0
820 } 912 }
821 - return; 913 + return
822 } 914 }
823 915
824 // 检查是否有数据刷新标记 916 // 检查是否有数据刷新标记
...@@ -845,20 +937,20 @@ onActivated(async () => { ...@@ -845,20 +937,20 @@ onActivated(async () => {
845 937
846 if (refreshType === 'edit') { 938 if (refreshType === 'edit') {
847 // 编辑模式:更新列表中的对应项 939 // 编辑模式:更新列表中的对应项
848 - const index = checkinDataList.value.findIndex(item => item.id == refreshId) 940 + const index = checkinDataList.value.findIndex(item => item.id === refreshId)
849 if (index > -1) { 941 if (index > -1) {
850 checkinDataList.value.splice(index, 1, formattedItem) 942 checkinDataList.value.splice(index, 1, formattedItem)
851 } 943 }
852 has_refreshed = true 944 has_refreshed = true
853 } else if (refreshType === 'add') { 945 } else if (refreshType === 'add') {
854 // 新增模式:添加到列表顶部,且去重 946 // 新增模式:添加到列表顶部,且去重
855 - const exists = checkinDataList.value.some(item => item.id == formattedItem.id) 947 + const exists = checkinDataList.value.some(item => item.id === formattedItem.id)
856 if (!exists) { 948 if (!exists) {
857 checkinDataList.value.unshift(formattedItem) 949 checkinDataList.value.unshift(formattedItem)
858 } 950 }
859 // 更新统计数据 951 // 更新统计数据
860 - const current_date = route.query.date || dayjs().format('YYYY-MM-DD'); 952 + const current_date = route.query.date || dayjs().format('YYYY-MM-DD')
861 - getTaskDetail(dayjs(current_date).format('YYYY-MM')); 953 + getTaskDetail(dayjs(current_date).format('YYYY-MM'))
862 has_refreshed = true 954 has_refreshed = true
863 } 955 }
864 } 956 }
...@@ -886,14 +978,14 @@ onActivated(async () => { ...@@ -886,14 +978,14 @@ onActivated(async () => {
886 * @param {string|number} anchor_id - 打卡动态ID 978 * @param {string|number} anchor_id - 打卡动态ID
887 * @returns {Element|null} DOM 元素 979 * @returns {Element|null} DOM 元素
888 */ 980 */
889 - const resolve_anchor_el = (anchor_id) => { 981 + const resolve_anchor_el = anchor_id => {
890 const card = checkinCardRefs.value.get(anchor_id) 982 const card = checkinCardRefs.value.get(anchor_id)
891 return card?.$el || card?.$?.vnode?.el || null 983 return card?.$el || card?.$?.vnode?.el || null
892 } 984 }
893 985
894 // 注释:优先尝试“从详情页返回”的滚动恢复;普通刷新不会走 should_restore 986 // 注释:优先尝试“从详情页返回”的滚动恢复;普通刷新不会走 should_restore
895 const restored_state = await restore_checkin_scroll_state({ 987 const restored_state = await restore_checkin_scroll_state({
896 - wait_for: (state) => { 988 + wait_for: state => {
897 const wrapper_height = fixedCalendarWrapper.value?.getBoundingClientRect?.().height || 0 989 const wrapper_height = fixedCalendarWrapper.value?.getBoundingClientRect?.().height || 0
898 const height = Number(calendarHeight.value || 0) 990 const height = Number(calendarHeight.value || 0)
899 if (wrapper_height <= 0 || height <= 0) return false 991 if (wrapper_height <= 0 || height <= 0) return false
...@@ -915,9 +1007,11 @@ onActivated(async () => { ...@@ -915,9 +1007,11 @@ onActivated(async () => {
915 const anchor_el = resolve_anchor_el(state.anchor_id) 1007 const anchor_el = resolve_anchor_el(state.anchor_id)
916 if (!anchor_el || typeof anchor_el.getBoundingClientRect !== 'function') return null 1008 if (!anchor_el || typeof anchor_el.getBoundingClientRect !== 'function') return null
917 1009
918 - const scroll_rect_top = scroll_el === window ? 0 : (scroll_el?.getBoundingClientRect?.().top || 0) 1010 + const scroll_rect_top =
1011 + scroll_el === window ? 0 : scroll_el?.getBoundingClientRect?.().top || 0
919 const anchor_rect = anchor_el.getBoundingClientRect() 1012 const anchor_rect = anchor_el.getBoundingClientRect()
920 - const current_scroll_top = scroll_el === window ? (window.scrollY || 0) : (scroll_el?.scrollTop || 0) 1013 + const current_scroll_top =
1014 + scroll_el === window ? window.scrollY || 0 : scroll_el?.scrollTop || 0
921 const base_top = anchor_rect.top - scroll_rect_top + current_scroll_top 1015 const base_top = anchor_rect.top - scroll_rect_top + current_scroll_top
922 return Math.max(0, base_top - current_calendar_height - 10) 1016 return Math.max(0, base_top - current_calendar_height - 10)
923 } 1017 }
...@@ -941,16 +1035,21 @@ onActivated(async () => { ...@@ -941,16 +1035,21 @@ onActivated(async () => {
941 if (!restored_state && is_checkin_page_reload()) { 1035 if (!restored_state && is_checkin_page_reload()) {
942 log_scroll('触发刷新兜底逻辑:准备强制滚动') 1036 log_scroll('触发刷新兜底逻辑:准备强制滚动')
943 await nextTick() 1037 await nextTick()
944 - await wait_scroll_ready(() => { 1038 + await wait_scroll_ready(
1039 + () => {
945 const wrapper_height = fixedCalendarWrapper.value?.getBoundingClientRect?.().height || 0 1040 const wrapper_height = fixedCalendarWrapper.value?.getBoundingClientRect?.().height || 0
946 const height = Number(calendarHeight.value || 0) 1041 const height = Number(calendarHeight.value || 0)
947 if (wrapper_height <= 0 || height <= 0) return false 1042 if (wrapper_height <= 0 || height <= 0) return false
948 return Math.abs(wrapper_height - height) <= 2 1043 return Math.abs(wrapper_height - height) <= 2
949 - }, { timeout_ms: 1500, interval_ms: 16 }) 1044 + },
1045 + { timeout_ms: 1500, interval_ms: 16 }
1046 + )
950 const forced_result = await force_scroll_to(0, { times: 3, settle_frames: 1, behavior: 'auto' }) 1047 const forced_result = await force_scroll_to(0, { times: 3, settle_frames: 1, behavior: 'auto' })
951 log_scroll('刷新兜底逻辑结束', { forced_top: true, ...forced_result }) 1048 log_scroll('刷新兜底逻辑结束', { forced_top: true, ...forced_result })
952 } else if (!restored_state) { 1049 } else if (!restored_state) {
953 - log_scroll('未恢复且不满足刷新兜底条件(不会强制滚动)', { is_reload: is_checkin_page_reload() }) 1050 + log_scroll('未恢复且不满足刷新兜底条件(不会强制滚动)', {
1051 + is_reload: is_checkin_page_reload(),
1052 + })
954 } 1053 }
955 }) 1054 })
956 </script> 1055 </script>
...@@ -963,7 +1062,12 @@ onActivated(async () => { ...@@ -963,7 +1062,12 @@ onActivated(async () => {
963 left: 0; 1062 left: 0;
964 right: 0; 1063 right: 0;
965 z-index: 1000; 1064 z-index: 1000;
966 - background: linear-gradient(to bottom right, #f0fdf4, #f0fdfa, #eff6ff); // 与AppLayout保持一致的渐变背景 1065 + background: linear-gradient(
1066 + to bottom right,
1067 + #f0fdf4,
1068 + #f0fdfa,
1069 + #eff6ff
1070 + ); // 与AppLayout保持一致的渐变背景
967 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 1071 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
968 } 1072 }
969 1073
...@@ -993,7 +1097,7 @@ onActivated(async () => { ...@@ -993,7 +1097,7 @@ onActivated(async () => {
993 1097
994 .calendar-fill-checkin { 1098 .calendar-fill-checkin {
995 .van-calendar__selected-day { 1099 .van-calendar__selected-day {
996 - background: #FFF !important; 1100 + background: #fff !important;
997 border: 1px solid darkgoldenrod !important; 1101 border: 1px solid darkgoldenrod !important;
998 color: darkgoldenrod !important; 1102 color: darkgoldenrod !important;
999 } 1103 }
...@@ -1001,7 +1105,7 @@ onActivated(async () => { ...@@ -1001,7 +1105,7 @@ onActivated(async () => {
1001 1105
1002 .calendar-today { 1106 .calendar-today {
1003 .van-calendar__selected-day { 1107 .van-calendar__selected-day {
1004 - background: #FAAB0C !important; 1108 + background: #faab0c !important;
1005 } 1109 }
1006 } 1110 }
1007 1111
......