feat(checkin): 展示打卡点评列表
同步 Apifox 打卡动态字段说明,并在打卡页面展示 feedback_list 点评内容与评分。 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Showing
4 changed files
with
838 additions
and
596 deletions
| ... | @@ -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)) | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | - <div class="post-card shadow-md"> | 2 | + <div class="post-card shadow-md"> |
| 3 | - <!-- 头部信息 --> | 3 | + <!-- 头部信息 --> |
| 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 | - </van-col> | 10 | + height="2.5rem" |
| 11 | - <van-col span="17"> | 11 | + :src=" |
| 12 | - <div class="user-info"> | 12 | + getOptimizedUrl(post.user.avatar || 'https://cdn.ipadbiz.cn/mlaj/images/icon_1.jpeg') |
| 13 | - <div class="username"> | 13 | + " |
| 14 | - {{ post.user.name }} | 14 | + fit="cover" |
| 15 | - <!-- 补卡标记 --> | 15 | + /> |
| 16 | - <span v-if="post.user.is_makeup" | 16 | + </van-col> |
| 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> | 17 | + <van-col span="17"> |
| 18 | - <slot name="header-tags"></slot> | 18 | + <div class="user-info"> |
| 19 | - </div> | 19 | + <div class="username"> |
| 20 | - <div class="post-time">{{ post.user.time }}</div> | 20 | + {{ post.user.name }} |
| 21 | - </div> | 21 | + <!-- 补卡标记 --> |
| 22 | - </van-col> | 22 | + <span |
| 23 | - <van-col span="3"> | 23 | + v-if="post.user.is_makeup" |
| 24 | - <div v-if="post.is_my" class="post-menu"> | 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 | - <slot name="menu"> | 25 | + >补打卡</span |
| 26 | - <!-- 默认菜单项,或留空供父组件填充 --> | 26 | + > |
| 27 | - <van-icon name="edit" @click="emit('edit', post)" class="mr-2" color="#4caf50" /> | 27 | + <slot name="header-tags"></slot> |
| 28 | - <van-icon name="delete-o" @click="emit('delete', post)" color="#f44336" /> | ||
| 29 | - </slot> | ||
| 30 | - </div> | ||
| 31 | - <slot name="header-right" v-else></slot> | ||
| 32 | - </van-col> | ||
| 33 | - </van-row> | ||
| 34 | - </div> | ||
| 35 | - | ||
| 36 | - <!-- 内容区域 --> | ||
| 37 | - <div class="post-content"> | ||
| 38 | - <slot name="content-top"></slot> | ||
| 39 | - <PostCountModel :post-data="post" /> | ||
| 40 | - <div class="post-text-wrapper relative"> | ||
| 41 | - <div ref="textRef" class="post-text" :class="{ 'line-clamp-5': !isExpanded }"> | ||
| 42 | - {{ post.content }} | ||
| 43 | - </div> | ||
| 44 | - <div v-if="showExpandBtn" class="expand-btn text-blue-500 text-sm mt-1 cursor-pointer" | ||
| 45 | - @click.stop="toggleExpand"> | ||
| 46 | - {{ isExpanded ? '收起' : '全文' }} | ||
| 47 | - </div> | ||
| 48 | </div> | 28 | </div> |
| 29 | + <div class="post-time">{{ post.user.time }}</div> | ||
| 30 | + </div> | ||
| 31 | + </van-col> | ||
| 32 | + <van-col span="3"> | ||
| 33 | + <div v-if="post.is_my" class="post-menu"> | ||
| 34 | + <slot name="menu"> | ||
| 35 | + <!-- 默认菜单项,或留空供父组件填充 --> | ||
| 36 | + <van-icon name="edit" @click="emit('edit', post)" class="mr-2" color="#4caf50" /> | ||
| 37 | + <van-icon name="delete-o" @click="emit('delete', post)" color="#f44336" /> | ||
| 38 | + </slot> | ||
| 39 | + </div> | ||
| 40 | + <slot name="header-right" v-else></slot> | ||
| 41 | + </van-col> | ||
| 42 | + </van-row> | ||
| 43 | + </div> | ||
| 49 | 44 | ||
| 50 | - <!-- 媒体内容 --> | 45 | + <!-- 内容区域 --> |
| 51 | - <div class="post-media mt-2"> | 46 | + <div class="post-content"> |
| 52 | - <!-- 多附件Tab模式 --> | 47 | + <slot name="content-top"></slot> |
| 53 | - <div v-if="mediaTabs.length > 1" class="media-tabs"> | 48 | + <PostCountModel :post-data="post" /> |
| 54 | - <!-- 移除 animated 和 swipeable 属性,避免多 tab 高度不一致时容器被最高的内容撑开,导致短内容下方出现空白 --> | 49 | + <div class="post-text-wrapper relative"> |
| 55 | - <van-tabs v-model:active="activeTab" shrink color="#4caf50"> | 50 | + <div ref="textRef" class="post-text" :class="{ 'line-clamp-5': !isExpanded }"> |
| 56 | - <van-tab v-for="tab in mediaTabs" :key="tab.name" :title="tab.label" :name="tab.name"> | 51 | + {{ post.content }} |
| 57 | - <div class="pt-2"> | 52 | + </div> |
| 58 | - <!-- 图片内容 --> | 53 | + <div |
| 59 | - <template v-if="tab.name === 'image'"> | 54 | + v-if="showExpandBtn" |
| 60 | - <div class="post-images"> | 55 | + class="expand-btn mt-1 cursor-pointer text-sm text-blue-500" |
| 61 | - <div class="post-image-item" v-for="(image, index) in post.images" :key="index"> | 56 | + @click.stop="toggleExpand" |
| 62 | - <van-image width="100%" height="100%" fit="cover" | 57 | + > |
| 63 | - :src="getOptimizedUrl(image)" radius="5" | 58 | + {{ isExpanded ? '收起' : '全文' }} |
| 64 | - @click="openImagePreview(index)" /> | 59 | + </div> |
| 65 | - </div> | 60 | + </div> |
| 66 | - </div> | 61 | + |
| 67 | - </template> | 62 | + <div v-if="hasFeedbackList" class="feedback-list mt-4 space-y-3"> |
| 68 | - | 63 | + <div v-for="feedback in post.feedback_list" :key="feedback.id" class="feedback-item"> |
| 69 | - <!-- 视频内容 --> | 64 | + <div |
| 70 | - <template v-if="tab.name === 'video'"> | 65 | + v-if="feedback.time" |
| 71 | - <div v-for="(v, idx) in post.videoList" :key="idx"> | 66 | + class="feedback-time mb-2 flex items-center text-xs text-gray-500" |
| 72 | - <!-- 封面图 --> | 67 | + > |
| 73 | - <div v-if="v.video && !v.isPlaying" | 68 | + <van-icon name="calendar-o" size="14" color="#10b981" class="mr-1" /> |
| 74 | - class="relative w-full rounded-lg overflow-hidden" | 69 | + <span>{{ feedback.time }}</span> |
| 75 | - style="aspect-ratio: 16/9; margin-bottom: 1rem;"> | 70 | + </div> |
| 76 | - <img :src="getOptimizedUrl(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')" | 71 | + |
| 77 | - :alt="post.content" class="w-full h-full object-contain" /> | 72 | + <div v-if="feedback.note" class="feedback-note mb-2 flex items-start"> |
| 78 | - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" | 73 | + <van-icon name="chat-o" size="14" color="#3b82f6" class="mr-2 mt-0.5" /> |
| 79 | - @click="startPlay(v)"> | 74 | + <div class="text-sm leading-6 text-gray-700">{{ feedback.note }}</div> |
| 80 | - <div | 75 | + </div> |
| 81 | - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | 76 | + |
| 82 | - <van-icon name="play-circle-o" class="text-white" size="40" /> | 77 | + <div v-if="feedback.score > 0" class="feedback-score flex items-center"> |
| 83 | - </div> | 78 | + <van-icon name="star-o" size="14" color="#f59e0b" class="mr-2" /> |
| 84 | - </div> | 79 | + <span class="mr-2 text-xs text-gray-500">评分</span> |
| 85 | - </div> | 80 | + <van-rate |
| 86 | - <!-- 视频播放器 --> | 81 | + :model-value="feedback.score" |
| 87 | - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" | 82 | + :size="14" |
| 88 | - :video-id="v.id || `video-${post.id}-${idx}`" | 83 | + color="#ffd21e" |
| 89 | - :use-native-on-ios="false" class="post-video rounded-lg overflow-hidden" | 84 | + void-color="#eee" |
| 90 | - :ref="(el) => setVideoRef(el, v.id)" | 85 | + readonly |
| 91 | - @onPlay="(player) => handleVideoPlay(player, v)" | 86 | + /> |
| 92 | - @onPause="handleVideoPause" /> | 87 | + </div> |
| 93 | - </div> | 88 | + </div> |
| 94 | - </template> | 89 | + </div> |
| 95 | - | 90 | + |
| 96 | - <!-- 音频内容 --> | 91 | + <!-- 媒体内容 --> |
| 97 | - <template v-if="tab.name === 'audio'"> | 92 | + <div class="post-media mt-2"> |
| 98 | - <AudioPlayer v-if="post.audio && post.audio.length" :songs="post.audio" | 93 | + <!-- 多附件Tab模式 --> |
| 99 | - class="post-audio" :id="post.id" :ref="(el) => setAudioRef(el, post.id)" | 94 | + <div v-if="mediaTabs.length > 1" class="media-tabs"> |
| 100 | - @play="handleAudioPlay" /> | 95 | + <!-- 移除 animated 和 swipeable 属性,避免多 tab 高度不一致时容器被最高的内容撑开,导致短内容下方出现空白 --> |
| 101 | - </template> | 96 | + <van-tabs v-model:active="activeTab" shrink color="#4caf50"> |
| 102 | - </div> | 97 | + <van-tab v-for="tab in mediaTabs" :key="tab.name" :title="tab.label" :name="tab.name"> |
| 103 | - </van-tab> | 98 | + <div class="pt-2"> |
| 104 | - </van-tabs> | 99 | + <!-- 图片内容 --> |
| 105 | - </div> | 100 | + <template v-if="tab.name === 'image'"> |
| 106 | - | 101 | + <div class="post-images"> |
| 107 | - <!-- 单一附件模式 (保持原有布局) --> | 102 | + <div class="post-image-item" v-for="(image, index) in post.images" :key="index"> |
| 108 | - <div v-else> | 103 | + <van-image |
| 109 | - <!-- 图片列表 --> | 104 | + width="100%" |
| 110 | - <div v-if="post.images && post.images.length" class="post-images"> | 105 | + height="100%" |
| 111 | - <div class="post-image-item" v-for="(image, index) in post.images" :key="index"> | 106 | + fit="cover" |
| 112 | - <van-image width="100%" height="100%" fit="cover" :src="getOptimizedUrl(image)" radius="5" | 107 | + :src="getOptimizedUrl(image)" |
| 113 | - @click="openImagePreview(index)" /> | 108 | + radius="5" |
| 114 | - </div> | 109 | + @click="openImagePreview(index)" |
| 110 | + /> | ||
| 115 | </div> | 111 | </div> |
| 116 | - | 112 | + </div> |
| 117 | - <!-- 视频列表 --> | 113 | + </template> |
| 118 | - <div v-if="post.videoList && post.videoList.length"> | 114 | + |
| 119 | - <div v-for="(v, idx) in post.videoList" :key="idx"> | 115 | + <!-- 视频内容 --> |
| 120 | - <!-- 封面图 --> | 116 | + <template v-if="tab.name === 'video'"> |
| 121 | - <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" | 117 | + <div v-for="(v, idx) in post.videoList" :key="idx"> |
| 122 | - style="aspect-ratio: 16/9; margin-bottom: 1rem;"> | 118 | + <!-- 封面图 --> |
| 123 | - <img :src="getOptimizedUrl(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')" | 119 | + <div |
| 124 | - :alt="post.content" class="w-full h-full object-contain" /> | 120 | + v-if="v.video && !v.isPlaying" |
| 125 | - <div class="absolute inset-0 flex items-center justify-center cursor-pointer bg-black/20" | 121 | + class="relative w-full overflow-hidden rounded-lg" |
| 126 | - @click="startPlay(v)"> | 122 | + style="aspect-ratio: 16/9; margin-bottom: 1rem" |
| 127 | - <div | 123 | + > |
| 128 | - class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | 124 | + <img |
| 129 | - <van-icon name="play-circle-o" class="text-white" size="40" /> | 125 | + :src=" |
| 130 | - </div> | 126 | + getOptimizedUrl( |
| 131 | - </div> | 127 | + v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png' |
| 132 | - </div> | 128 | + ) |
| 133 | - <!-- 视频播放器 --> | 129 | + " |
| 134 | - <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" | 130 | + :alt="post.content" |
| 135 | - :video-id="v.id || `video-${post.id}-${idx}`" :use-native-on-ios="false" | 131 | + class="h-full w-full object-contain" |
| 136 | - class="post-video rounded-lg overflow-hidden" :ref="(el) => setVideoRef(el, v.id)" | 132 | + /> |
| 137 | - @onPlay="(player) => handleVideoPlay(player, v)" @onPause="handleVideoPause" /> | 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 | + > | ||
| 140 | + <van-icon name="play-circle-o" class="text-white" size="40" /> | ||
| 138 | </div> | 141 | </div> |
| 142 | + </div> | ||
| 139 | </div> | 143 | </div> |
| 140 | - | 144 | + <!-- 视频播放器 --> |
| 141 | - <!-- 音频播放器 --> | 145 | + <VideoPlayer |
| 142 | - <AudioPlayer v-if="post.audio && post.audio.length" :songs="post.audio" class="post-audio" | 146 | + v-if="v.video && v.isPlaying" |
| 143 | - :id="post.id" :ref="(el) => setAudioRef(el, post.id)" @play="handleAudioPlay" /> | 147 | + :video-url="v.video" |
| 144 | - </div> | 148 | + :video-id="v.id || `video-${post.id}-${idx}`" |
| 145 | - | 149 | + :use-native-on-ios="false" |
| 146 | - <van-image-preview v-if="post.images && post.images.length" v-model:show="showLocalImagePreview" | 150 | + class="post-video overflow-hidden rounded-lg" |
| 147 | - :images="post.images" :start-position="localStartPosition" :show-index="true" /> | 151 | + :ref="el => setVideoRef(el, v.id)" |
| 148 | - </div> | 152 | + @onPlay="player => handleVideoPlay(player, v)" |
| 153 | + @onPause="handleVideoPause" | ||
| 154 | + /> | ||
| 155 | + </div> | ||
| 156 | + </template> | ||
| 157 | + | ||
| 158 | + <!-- 音频内容 --> | ||
| 159 | + <template v-if="tab.name === 'audio'"> | ||
| 160 | + <AudioPlayer | ||
| 161 | + v-if="post.audio && post.audio.length" | ||
| 162 | + :songs="post.audio" | ||
| 163 | + class="post-audio" | ||
| 164 | + :id="post.id" | ||
| 165 | + :ref="el => setAudioRef(el, post.id)" | ||
| 166 | + @play="handleAudioPlay" | ||
| 167 | + /> | ||
| 168 | + </template> | ||
| 169 | + </div> | ||
| 170 | + </van-tab> | ||
| 171 | + </van-tabs> | ||
| 149 | </div> | 172 | </div> |
| 150 | 173 | ||
| 151 | - <!-- 底部操作栏 --> | 174 | + <!-- 单一附件模式 (保持原有布局) --> |
| 152 | - <div class="post-footer flex items-center justify-between"> | 175 | + <div v-else> |
| 153 | - <!-- 左侧:点赞 --> | 176 | + <!-- 图片列表 --> |
| 154 | - <div class="flex items-center"> | 177 | + <div v-if="post.images && post.images.length" class="post-images"> |
| 155 | - <van-icon @click="emit('like', post)" name="good-job" class="like-icon" | 178 | + <div class="post-image-item" v-for="(image, index) in post.images" :key="index"> |
| 156 | - :color="post.is_liked ? 'red' : ''" /> | 179 | + <van-image |
| 157 | - <span class="like-count ml-1">{{ post.likes }}</span> | 180 | + width="100%" |
| 181 | + height="100%" | ||
| 182 | + fit="cover" | ||
| 183 | + :src="getOptimizedUrl(image)" | ||
| 184 | + radius="5" | ||
| 185 | + @click="openImagePreview(index)" | ||
| 186 | + /> | ||
| 158 | </div> | 187 | </div> |
| 159 | - <!-- 右侧:自定义操作 --> | 188 | + </div> |
| 160 | - <div class="flex items-center"> | 189 | + |
| 161 | - <slot name="footer-right"></slot> | 190 | + <!-- 视频列表 --> |
| 191 | + <div v-if="post.videoList && post.videoList.length"> | ||
| 192 | + <div v-for="(v, idx) in post.videoList" :key="idx"> | ||
| 193 | + <!-- 封面图 --> | ||
| 194 | + <div | ||
| 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 | + > | ||
| 215 | + <van-icon name="play-circle-o" class="text-white" size="40" /> | ||
| 216 | + </div> | ||
| 217 | + </div> | ||
| 218 | + </div> | ||
| 219 | + <!-- 视频播放器 --> | ||
| 220 | + <VideoPlayer | ||
| 221 | + v-if="v.video && v.isPlaying" | ||
| 222 | + :video-url="v.video" | ||
| 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 | + /> | ||
| 162 | </div> | 230 | </div> |
| 231 | + </div> | ||
| 232 | + | ||
| 233 | + <!-- 音频播放器 --> | ||
| 234 | + <AudioPlayer | ||
| 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 | + /> | ||
| 163 | </div> | 242 | </div> |
| 243 | + | ||
| 244 | + <van-image-preview | ||
| 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 | + /> | ||
| 251 | + </div> | ||
| 252 | + </div> | ||
| 253 | + | ||
| 254 | + <!-- 底部操作栏 --> | ||
| 255 | + <div class="post-footer flex items-center justify-between"> | ||
| 256 | + <!-- 左侧:点赞 --> | ||
| 257 | + <div class="flex items-center"> | ||
| 258 | + <van-icon | ||
| 259 | + @click="emit('like', post)" | ||
| 260 | + name="good-job" | ||
| 261 | + class="like-icon" | ||
| 262 | + :color="post.is_liked ? 'red' : ''" | ||
| 263 | + /> | ||
| 264 | + <span class="like-count ml-1">{{ post.likes }}</span> | ||
| 265 | + </div> | ||
| 266 | + <!-- 右侧:自定义操作 --> | ||
| 267 | + <div class="flex items-center"> | ||
| 268 | + <slot name="footer-right"></slot> | ||
| 269 | + </div> | ||
| 164 | </div> | 270 | </div> |
| 271 | + </div> | ||
| 165 | </template> | 272 | </template> |
| 166 | 273 | ||
| 167 | <script setup> | 274 | <script setup> |
| ... | @@ -195,30 +302,34 @@ | ... | @@ -195,30 +302,34 @@ |
| 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 | + /** |
| 204 | - * 帖子数据对象 | 311 | + * 帖子数据对象 |
| 205 | - * @property {number|string} id - 帖子ID | 312 | + * @property {number|string} id - 帖子ID |
| 206 | - * @property {Object} user - 用户信息 | 313 | + * @property {Object} user - 用户信息 |
| 207 | - * @property {string} content - 帖子内容 | 314 | + * @property {string} content - 帖子内容 |
| 208 | - * @property {Array} images - 图片列表 | 315 | + * @property {Array} images - 图片列表 |
| 209 | - * @property {Array} videoList - 视频列表 | 316 | + * @property {Array} videoList - 视频列表 |
| 210 | - * @property {Array} audio - 音频列表 | 317 | + * @property {Array} audio - 音频列表 |
| 211 | - * @property {number} likes - 点赞数 | 318 | + * @property {number} likes - 点赞数 |
| 212 | - * @property {boolean} is_liked - 是否已点赞 | 319 | + * @property {boolean} is_liked - 是否已点赞 |
| 213 | - * @property {boolean} is_my - 是否是自己的帖子 | 320 | + * @property {boolean} is_my - 是否是自己的帖子 |
| 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) |
| ... | @@ -228,46 +339,50 @@ const textRef = ref(null) | ... | @@ -228,46 +339,50 @@ const textRef = ref(null) |
| 228 | * @description 切换文本展开/收起状态 | 339 | * @description 切换文本展开/收起状态 |
| 229 | */ | 340 | */ |
| 230 | const toggleExpand = () => { | 341 | const toggleExpand = () => { |
| 231 | - isExpanded.value = !isExpanded.value | 342 | + isExpanded.value = !isExpanded.value |
| 232 | } | 343 | } |
| 233 | 344 | ||
| 234 | /** | 345 | /** |
| 235 | * @description 检查文本是否溢出 | 346 | * @description 检查文本是否溢出 |
| 236 | */ | 347 | */ |
| 237 | const checkTextOverflow = () => { | 348 | const checkTextOverflow = () => { |
| 238 | - if (textRef.value) { | 349 | + if (textRef.value) { |
| 239 | - // 如果滚动高度大于客户区高度,说明有溢出(因为设置了 line-clamp) | 350 | + // 如果滚动高度大于客户区高度,说明有溢出(因为设置了 line-clamp) |
| 240 | - showExpandBtn.value = textRef.value.scrollHeight > textRef.value.clientHeight | 351 | + showExpandBtn.value = textRef.value.scrollHeight > textRef.value.clientHeight |
| 241 | - } | 352 | + } |
| 242 | } | 353 | } |
| 243 | 354 | ||
| 244 | onMounted(() => { | 355 | onMounted(() => { |
| 245 | - nextTick(() => { | 356 | + nextTick(() => { |
| 246 | - checkTextOverflow() | 357 | + checkTextOverflow() |
| 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) |
| 263 | - if (props.post.audio && props.post.audio.length) tabs.push({ label: '音频', name: 'audio' }) | 377 | + tabs.push({ label: '视频', name: 'video' }) |
| 264 | - return tabs | 378 | + if (props.post.audio && props.post.audio.length) tabs.push({ label: '音频', name: 'audio' }) |
| 379 | + return tabs | ||
| 265 | }) | 380 | }) |
| 266 | 381 | ||
| 267 | watchEffect(() => { | 382 | watchEffect(() => { |
| 268 | - if (mediaTabs.value.length > 0 && !activeTab.value) { | 383 | + if (mediaTabs.value.length > 0 && !activeTab.value) { |
| 269 | - activeTab.value = mediaTabs.value[0].name | 384 | + activeTab.value = mediaTabs.value[0].name |
| 270 | - } | 385 | + } |
| 271 | }) | 386 | }) |
| 272 | 387 | ||
| 273 | // 图片预览状态 | 388 | // 图片预览状态 |
| ... | @@ -278,9 +393,9 @@ const localStartPosition = ref(0) | ... | @@ -278,9 +393,9 @@ 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 | } |
| 285 | 400 | ||
| 286 | // 媒体引用 | 401 | // 媒体引用 |
| ... | @@ -293,11 +408,11 @@ const audioRefs = ref(new Map()) | ... | @@ -293,11 +408,11 @@ const audioRefs = ref(new Map()) |
| 293 | * @param {string|number} id 视频ID | 408 | * @param {string|number} id 视频ID |
| 294 | */ | 409 | */ |
| 295 | const setVideoRef = (el, id) => { | 410 | const setVideoRef = (el, id) => { |
| 296 | - if (el) { | 411 | + if (el) { |
| 297 | - videoRefs.value.set(id, el) | 412 | + videoRefs.value.set(id, el) |
| 298 | - } else { | 413 | + } else { |
| 299 | - videoRefs.value.delete(id) | 414 | + videoRefs.value.delete(id) |
| 300 | - } | 415 | + } |
| 301 | } | 416 | } |
| 302 | 417 | ||
| 303 | /** | 418 | /** |
| ... | @@ -306,11 +421,11 @@ const setVideoRef = (el, id) => { | ... | @@ -306,11 +421,11 @@ const setVideoRef = (el, id) => { |
| 306 | * @param {string|number} id 音频ID | 421 | * @param {string|number} id 音频ID |
| 307 | */ | 422 | */ |
| 308 | const setAudioRef = (el, id) => { | 423 | const setAudioRef = (el, id) => { |
| 309 | - if (el) { | 424 | + if (el) { |
| 310 | - audioRefs.value.set(id, el) | 425 | + audioRefs.value.set(id, el) |
| 311 | - } else { | 426 | + } else { |
| 312 | - audioRefs.value.delete(id) | 427 | + audioRefs.value.delete(id) |
| 313 | - } | 428 | + } |
| 314 | } | 429 | } |
| 315 | 430 | ||
| 316 | // CDN 链接优化 | 431 | // CDN 链接优化 |
| ... | @@ -319,10 +434,10 @@ const setAudioRef = (el, id) => { | ... | @@ -319,10 +434,10 @@ 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` |
| 326 | } | 441 | } |
| 327 | 442 | ||
| 328 | // 视频播放逻辑 | 443 | // 视频播放逻辑 |
| ... | @@ -330,12 +445,12 @@ const getOptimizedUrl = (url) => { | ... | @@ -330,12 +445,12 @@ 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 |
| 337 | - }) | 452 | + }) |
| 338 | - v.isPlaying = true | 453 | + v.isPlaying = true |
| 339 | } | 454 | } |
| 340 | 455 | ||
| 341 | /** | 456 | /** |
| ... | @@ -344,17 +459,17 @@ const startPlay = (v) => { | ... | @@ -344,17 +459,17 @@ const startPlay = (v) => { |
| 344 | * @param {Object} v 视频对象 | 459 | * @param {Object} v 视频对象 |
| 345 | */ | 460 | */ |
| 346 | const handleVideoPlay = (player, v) => { | 461 | const handleVideoPlay = (player, v) => { |
| 347 | - // 停止本地音频 | 462 | + // 停止本地音频 |
| 348 | - stopLocalAudio() | 463 | + stopLocalAudio() |
| 349 | - // 通知父组件停止其他卡片的播放 | 464 | + // 通知父组件停止其他卡片的播放 |
| 350 | - emit('video-play', { post: props.post, player, videoId: v.id }) | 465 | + emit('video-play', { post: props.post, player, videoId: v.id }) |
| 351 | } | 466 | } |
| 352 | 467 | ||
| 353 | /** | 468 | /** |
| 354 | * @description 处理视频暂停事件 | 469 | * @description 处理视频暂停事件 |
| 355 | */ | 470 | */ |
| 356 | const handleVideoPause = () => { | 471 | const handleVideoPause = () => { |
| 357 | - // 暂无操作 | 472 | + // 暂无操作 |
| 358 | } | 473 | } |
| 359 | 474 | ||
| 360 | // 音频播放逻辑 | 475 | // 音频播放逻辑 |
| ... | @@ -362,141 +477,150 @@ const handleVideoPause = () => { | ... | @@ -362,141 +477,150 @@ 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 | + // 通知父组件 |
| 369 | - emit('audio-play', { post: props.post, player }) | 484 | + emit('audio-play', { post: props.post, player }) |
| 370 | } | 485 | } |
| 371 | 486 | ||
| 372 | /** | 487 | /** |
| 373 | * @description 停止当前卡片内的所有视频 | 488 | * @description 停止当前卡片内的所有视频 |
| 374 | */ | 489 | */ |
| 375 | const stopLocalVideos = () => { | 490 | const stopLocalVideos = () => { |
| 376 | - videoRefs.value.forEach(player => { | 491 | + videoRefs.value.forEach(player => { |
| 377 | - if (player && typeof player.pause === 'function') { | 492 | + if (player && typeof player.pause === 'function') { |
| 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 | /** |
| 385 | * @description 停止当前卡片内的所有音频 | 500 | * @description 停止当前卡片内的所有音频 |
| 386 | */ | 501 | */ |
| 387 | const stopLocalAudio = () => { | 502 | const stopLocalAudio = () => { |
| 388 | - audioRefs.value.forEach(player => { | 503 | + audioRefs.value.forEach(player => { |
| 389 | - if (player && typeof player.pause === 'function') { | 504 | + if (player && typeof player.pause === 'function') { |
| 390 | - player.pause() | 505 | + player.pause() |
| 391 | - } | 506 | + } |
| 392 | - }) | 507 | + }) |
| 393 | - // 同时也需要暂停 AudioPlayer 组件实例 | 508 | + // 同时也需要暂停 AudioPlayer 组件实例 |
| 394 | } | 509 | } |
| 395 | 510 | ||
| 396 | // 暴露方法给父组件 | 511 | // 暴露方法给父组件 |
| 397 | defineExpose({ | 512 | defineExpose({ |
| 398 | - stopAllMedia: () => { | 513 | + stopAllMedia: () => { |
| 399 | - stopLocalVideos() | 514 | + stopLocalVideos() |
| 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 | ||
| 407 | <style lang="less" scoped> | 522 | <style lang="less" scoped> |
| 408 | .line-clamp-5 { | 523 | .line-clamp-5 { |
| 409 | - display: -webkit-box; | 524 | + display: -webkit-box; |
| 410 | - --webkit-box-orient: vertical; | 525 | + --webkit-box-orient: vertical; |
| 411 | - --webkit-line-clamp: 5; | 526 | + --webkit-line-clamp: 5; |
| 412 | - overflow: hidden; | 527 | + overflow: hidden; |
| 413 | - text-overflow: ellipsis; | 528 | + text-overflow: ellipsis; |
| 414 | } | 529 | } |
| 415 | 530 | ||
| 416 | .post-card { | 531 | .post-card { |
| 417 | - background: #fff; | 532 | + background: #fff; |
| 418 | - border-radius: 10px; | 533 | + border-radius: 10px; |
| 419 | - padding: 1rem; | 534 | + padding: 1rem; |
| 420 | - margin-bottom: 1rem; | 535 | + margin-bottom: 1rem; |
| 421 | - | 536 | + |
| 422 | - .post-header { | 537 | + .post-header { |
| 423 | - margin-bottom: 0.5rem; | 538 | + margin-bottom: 0.5rem; |
| 424 | - | 539 | + |
| 425 | - .user-info { | 540 | + .user-info { |
| 426 | - display: flex; | 541 | + display: flex; |
| 427 | - flex-direction: column; | 542 | + flex-direction: column; |
| 428 | - justify-content: center; | 543 | + justify-content: center; |
| 429 | - height: 2.5rem; | 544 | + height: 2.5rem; |
| 430 | - margin-left: 0.5rem; | 545 | + margin-left: 0.5rem; |
| 431 | - | 546 | + |
| 432 | - .username { | 547 | + .username { |
| 433 | - font-weight: bold; | 548 | + font-weight: bold; |
| 434 | - font-size: 0.95rem; | 549 | + font-size: 0.95rem; |
| 435 | - display: flex; | 550 | + display: flex; |
| 436 | - align-items: center; | 551 | + align-items: center; |
| 437 | - } | 552 | + } |
| 438 | - | 553 | + |
| 439 | - .post-time { | 554 | + .post-time { |
| 440 | - font-size: 0.75rem; | 555 | + font-size: 0.75rem; |
| 441 | - color: #999; | 556 | + color: #999; |
| 442 | - } | 557 | + } |
| 443 | - } | 558 | + } |
| 444 | 559 | ||
| 445 | - .post-menu { | 560 | + .post-menu { |
| 446 | - display: flex; | 561 | + display: flex; |
| 447 | - justify-content: flex-end; | 562 | + justify-content: flex-end; |
| 448 | - font-size: 1.2rem; | 563 | + font-size: 1.2rem; |
| 449 | - color: #999; | 564 | + color: #999; |
| 450 | - } | 565 | + } |
| 566 | + } | ||
| 567 | + | ||
| 568 | + .post-content { | ||
| 569 | + .post-text { | ||
| 570 | + color: #666; | ||
| 571 | + margin-bottom: 1rem; | ||
| 572 | + white-space: pre-wrap; | ||
| 573 | + word-wrap: break-word; | ||
| 451 | } | 574 | } |
| 452 | 575 | ||
| 453 | - .post-content { | 576 | + .feedback-list { |
| 454 | - .post-text { | 577 | + .feedback-item { |
| 455 | - color: #666; | 578 | + padding: 0.75rem; |
| 456 | - margin-bottom: 1rem; | 579 | + border-radius: 0.75rem; |
| 457 | - white-space: pre-wrap; | 580 | + background: #f8fafc; |
| 458 | - word-wrap: break-word; | 581 | + border: 1px solid #e5e7eb; |
| 459 | - } | 582 | + } |
| 583 | + } | ||
| 460 | 584 | ||
| 461 | - .post-media { | 585 | + .post-media { |
| 462 | - .post-images { | 586 | + .post-images { |
| 463 | - display: flex; | 587 | + display: flex; |
| 464 | - flex-wrap: wrap; | 588 | + flex-wrap: wrap; |
| 465 | - gap: 0.5rem; | 589 | + gap: 0.5rem; |
| 466 | - | 590 | + |
| 467 | - .post-image-item { | 591 | + .post-image-item { |
| 468 | - width: calc((100% - 1rem) / 3); | 592 | + width: calc((100% - 1rem) / 3); |
| 469 | - aspect-ratio: 1 / 1; | 593 | + aspect-ratio: 1 / 1; |
| 470 | - border-radius: 6px; | 594 | + border-radius: 6px; |
| 471 | - overflow: hidden; | 595 | + overflow: hidden; |
| 472 | - } | ||
| 473 | - } | ||
| 474 | - | ||
| 475 | - .post-video { | ||
| 476 | - margin: 1rem 0; | ||
| 477 | - width: 100%; | ||
| 478 | - border-radius: 8px; | ||
| 479 | - overflow: hidden; | ||
| 480 | - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 481 | - } | ||
| 482 | - | ||
| 483 | - .post-audio { | ||
| 484 | - margin: 1rem 0; | ||
| 485 | - } | ||
| 486 | } | 596 | } |
| 597 | + } | ||
| 598 | + | ||
| 599 | + .post-video { | ||
| 600 | + margin: 1rem 0; | ||
| 601 | + width: 100%; | ||
| 602 | + border-radius: 8px; | ||
| 603 | + overflow: hidden; | ||
| 604 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | ||
| 605 | + } | ||
| 606 | + | ||
| 607 | + .post-audio { | ||
| 608 | + margin: 1rem 0; | ||
| 609 | + } | ||
| 487 | } | 610 | } |
| 611 | + } | ||
| 488 | 612 | ||
| 489 | - .post-footer { | 613 | + .post-footer { |
| 490 | - margin-top: 1rem; | 614 | + margin-top: 1rem; |
| 491 | - color: #666; | 615 | + color: #666; |
| 492 | 616 | ||
| 493 | - .like-icon { | 617 | + .like-icon { |
| 494 | - margin-right: 0.25rem; | 618 | + margin-right: 0.25rem; |
| 495 | - } | 619 | + } |
| 496 | 620 | ||
| 497 | - .like-count { | 621 | + .like-count { |
| 498 | - font-size: 0.9rem; | 622 | + font-size: 0.9rem; |
| 499 | - } | ||
| 500 | } | 623 | } |
| 624 | + } | ||
| 501 | } | 625 | } |
| 502 | </style> | 626 | </style> | ... | ... |
| ... | @@ -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( |
| 651 | - key, | 727 | + ([key, value]) => ({ |
| 652 | - value | 728 | + key, |
| 653 | - })); | 729 | + value, |
| 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,用于判断是否切换了任务 |
| ... | @@ -748,19 +840,19 @@ const get_checkin_scroll_key = () => { | ... | @@ -748,19 +840,19 @@ const get_checkin_scroll_key = () => { |
| 748 | * @returns {number} 顶部日历高度(像素) | 840 | * @returns {number} 顶部日历高度(像素) |
| 749 | */ | 841 | */ |
| 750 | const get_calendar_offset = () => { | 842 | const get_calendar_offset = () => { |
| 751 | - const wrapper_height = fixedCalendarWrapper.value?.getBoundingClientRect?.().height | 843 | + const wrapper_height = fixedCalendarWrapper.value?.getBoundingClientRect?.().height |
| 752 | - const fallback_height = Number(calendarHeight.value || 0) | 844 | + const fallback_height = Number(calendarHeight.value || 0) |
| 753 | - const height = Number(wrapper_height || fallback_height || 0) | 845 | + const height = Number(wrapper_height || fallback_height || 0) |
| 754 | - return height > 0 ? height : 0 | 846 | + return height > 0 ? height : 0 |
| 755 | } | 847 | } |
| 756 | 848 | ||
| 757 | /** | 849 | /** |
| 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( |
| 945 | - const wrapper_height = fixedCalendarWrapper.value?.getBoundingClientRect?.().height || 0 | 1039 | + () => { |
| 946 | - const height = Number(calendarHeight.value || 0) | 1040 | + const wrapper_height = fixedCalendarWrapper.value?.getBoundingClientRect?.().height || 0 |
| 947 | - if (wrapper_height <= 0 || height <= 0) return false | 1041 | + const height = Number(calendarHeight.value || 0) |
| 948 | - return Math.abs(wrapper_height - height) <= 2 | 1042 | + if (wrapper_height <= 0 || height <= 0) return false |
| 949 | - }, { timeout_ms: 1500, interval_ms: 16 }) | 1043 | + return Math.abs(wrapper_height - height) <= 2 |
| 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 | ... | ... |
-
Please register or login to post a comment