feat(打卡): 添加课程打卡弹窗功能并显示关联打卡数量
- 新增打卡弹窗组件,包含打卡类型选择和提交功能 - 动态显示关联打卡数量而非固定文本 - 处理打卡提交逻辑和状态管理 - 优化打卡入口逻辑,无任务时提示用户
Showing
1 changed file
with
124 additions
and
28 deletions
| ... | @@ -103,7 +103,7 @@ | ... | @@ -103,7 +103,7 @@ |
| 103 | <van-icon size="3rem" name="calendar-o" class="text-xl text-gray-600" /> | 103 | <van-icon size="3rem" name="calendar-o" class="text-xl text-gray-600" /> |
| 104 | <div> | 104 | <div> |
| 105 | <div class="text-base font-medium">打卡</div> | 105 | <div class="text-base font-medium">打卡</div> |
| 106 | - <div class="text-sm text-gray-500">关联7个打卡</div> | 106 | + <div class="text-sm text-gray-500">关联{{ task_list.length }}个打卡</div> |
| 107 | </div> | 107 | </div> |
| 108 | </div> | 108 | </div> |
| 109 | <van-icon name="arrow" class="text-gray-400" /> | 109 | <van-icon name="arrow" class="text-gray-400" /> |
| ... | @@ -114,6 +114,67 @@ | ... | @@ -114,6 +114,67 @@ |
| 114 | <div style="height: 30vh;"></div> | 114 | <div style="height: 30vh;"></div> |
| 115 | </div> | 115 | </div> |
| 116 | </div> | 116 | </div> |
| 117 | + | ||
| 118 | + <!-- 打卡弹窗 --> | ||
| 119 | + <van-popup | ||
| 120 | + v-model:show="showCheckInDialog" | ||
| 121 | + round | ||
| 122 | + position="bottom" | ||
| 123 | + :style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }" | ||
| 124 | + > | ||
| 125 | + <div class="p-4"> | ||
| 126 | + <div class="flex justify-between items-center mb-3"> | ||
| 127 | + <h3 class="font-medium">今日打卡</h3> | ||
| 128 | + <van-icon name="cross" @click="showCheckInDialog = false" /> | ||
| 129 | + </div> | ||
| 130 | + | ||
| 131 | + <div v-if="checkInSuccess" class="bg-green-50 border border-green-200 rounded-lg p-4 text-center"> | ||
| 132 | + <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 133 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> | ||
| 134 | + </svg> | ||
| 135 | + <h4 class="text-green-700 font-medium mb-1">打卡成功!</h4> | ||
| 136 | + </div> | ||
| 137 | + <template v-else> | ||
| 138 | + <div class="grid grid-cols-2 gap-4 py-2"> | ||
| 139 | + <button | ||
| 140 | + v-for="checkInType in task_list" | ||
| 141 | + :key="checkInType.id" | ||
| 142 | + class="flex flex-col items-center p-2 rounded-lg border transition-colors | ||
| 143 | + bg-white/70 border-gray-100 hover:bg-white" | ||
| 144 | + :class="{ | ||
| 145 | + 'bg-green-100 border-green-200': selectedCheckIn?.id === checkInType.id | ||
| 146 | + }" | ||
| 147 | + @click="handleCheckInSelect(checkInType)" | ||
| 148 | + > | ||
| 149 | + <div class="w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors | ||
| 150 | + bg-gray-100 text-gray-500" | ||
| 151 | + :class="{ | ||
| 152 | + 'bg-green-500 text-white': selectedCheckIn?.id === checkInType.id | ||
| 153 | + }" | ||
| 154 | + > | ||
| 155 | + <van-icon v-if="checkInType.task_type === 'checkin'" name="edit" size="1.5rem" /> | ||
| 156 | + <van-icon v-if="checkInType.task_type === 'upload'" name="tosend" size="1.5rem" /> | ||
| 157 | + </div> | ||
| 158 | + <span class="text-xs">{{ checkInType.title }}</span> | ||
| 159 | + </button> | ||
| 160 | + </div> | ||
| 161 | + | ||
| 162 | + <div v-if="selectedCheckIn" class="mt-3"> | ||
| 163 | + <button | ||
| 164 | + class="mt-2 w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-2 rounded-lg flex items-center justify-center" | ||
| 165 | + @click="handleCheckInSubmit" | ||
| 166 | + :disabled="isCheckingIn" | ||
| 167 | + > | ||
| 168 | + <template v-if="isCheckingIn"> | ||
| 169 | + <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> | ||
| 170 | + 提交中... | ||
| 171 | + </template> | ||
| 172 | + <template v-else>提交打卡</template> | ||
| 173 | + </button> | ||
| 174 | + </div> | ||
| 175 | + </template> | ||
| 176 | + </div> | ||
| 177 | + </van-popup> | ||
| 117 | </div> | 178 | </div> |
| 118 | </template> | 179 | </template> |
| 119 | 180 | ||
| ... | @@ -122,9 +183,11 @@ import { ref, onMounted, nextTick, onUnmounted } from 'vue'; | ... | @@ -122,9 +183,11 @@ import { ref, onMounted, nextTick, onUnmounted } from 'vue'; |
| 122 | import { useTitle } from '@vueuse/core'; | 183 | import { useTitle } from '@vueuse/core'; |
| 123 | import { useRouter } from "vue-router"; | 184 | import { useRouter } from "vue-router"; |
| 124 | import dayjs from 'dayjs'; | 185 | import dayjs from 'dayjs'; |
| 186 | +import { showToast } from 'vant'; | ||
| 125 | 187 | ||
| 126 | // 导入接口 | 188 | // 导入接口 |
| 127 | import { getCourseDetailAPI } from '@/api/course' | 189 | import { getCourseDetailAPI } from '@/api/course' |
| 190 | +import { checkinTaskAPI } from '@/api/checkin'; | ||
| 128 | 191 | ||
| 129 | const router = useRouter(); | 192 | const router = useRouter(); |
| 130 | 193 | ||
| ... | @@ -166,6 +229,8 @@ const course_type_maps = ref({ | ... | @@ -166,6 +229,8 @@ const course_type_maps = ref({ |
| 166 | file: '文件', | 229 | file: '文件', |
| 167 | }) | 230 | }) |
| 168 | 231 | ||
| 232 | +const task_list = ref([]); | ||
| 233 | + | ||
| 169 | onMounted(async () => { | 234 | onMounted(async () => { |
| 170 | /** | 235 | /** |
| 171 | * 组件挂载时获取课程详情 | 236 | * 组件挂载时获取课程详情 |
| ... | @@ -176,6 +241,7 @@ onMounted(async () => { | ... | @@ -176,6 +241,7 @@ onMounted(async () => { |
| 176 | const { code, data } = await getCourseDetailAPI({ i: courseId }); | 241 | const { code, data } = await getCourseDetailAPI({ i: courseId }); |
| 177 | if (code) { | 242 | if (code) { |
| 178 | course.value = data; | 243 | course.value = data; |
| 244 | + task_list.value = data.task_list || []; | ||
| 179 | course_lessons.value = data.schedule || []; | 245 | course_lessons.value = data.schedule || []; |
| 180 | } | 246 | } |
| 181 | /** | 247 | /** |
| ... | @@ -292,38 +358,68 @@ const handleTabChange = (name) => { | ... | @@ -292,38 +358,68 @@ const handleTabChange = (name) => { |
| 292 | }); | 358 | }); |
| 293 | }; | 359 | }; |
| 294 | 360 | ||
| 295 | -// 课程数据 | ||
| 296 | -// const course = ref({ | ||
| 297 | -// title: '开学礼·止的智慧·心法老师·20241001', | ||
| 298 | -// coverImage: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg', | ||
| 299 | -// updateTime: '2024.01.17', | ||
| 300 | -// viewCount: 1897, | ||
| 301 | -// description: '这是一门关于心法的课程,帮助学员掌握止的智慧...', | ||
| 302 | -// lessons: [ | ||
| 303 | -// { | ||
| 304 | -// title: '第一课:止的基础', | ||
| 305 | -// duration: '45分钟', | ||
| 306 | -// progress: 100 | ||
| 307 | -// }, | ||
| 308 | -// { | ||
| 309 | -// title: '第二课:止的技巧', | ||
| 310 | -// duration: '50分钟', | ||
| 311 | -// progress: 60 | ||
| 312 | -// }, | ||
| 313 | -// { | ||
| 314 | -// title: '第三课:止的应用', | ||
| 315 | -// duration: '40分钟', | ||
| 316 | -// progress: 0 | ||
| 317 | -// } | ||
| 318 | -// ] | ||
| 319 | -// }); | ||
| 320 | - | ||
| 321 | const goToStudyDetail = (lessonId) => { | 361 | const goToStudyDetail = (lessonId) => { |
| 322 | router.push(`/studyDetail/${lessonId}`); | 362 | router.push(`/studyDetail/${lessonId}`); |
| 323 | }; | 363 | }; |
| 324 | 364 | ||
| 365 | +// 打卡相关状态 | ||
| 366 | +const showCheckInDialog = ref(false); | ||
| 367 | +const selectedCheckIn = ref(null); | ||
| 368 | +const isCheckingIn = ref(false); | ||
| 369 | +const checkInSuccess = ref(false); | ||
| 370 | + | ||
| 371 | +// 处理打卡选择 | ||
| 372 | +const handleCheckInSelect = (type) => { | ||
| 373 | + if (type.is_gray && type.task_type === 'checkin') { | ||
| 374 | + showToast('您已经完成了今天的打卡'); | ||
| 375 | + return; | ||
| 376 | + } | ||
| 377 | + if (type.task_type === 'upload') { | ||
| 378 | + router.push({ | ||
| 379 | + path: '/checkin/index', | ||
| 380 | + query: { | ||
| 381 | + id: type.id | ||
| 382 | + } | ||
| 383 | + }); | ||
| 384 | + showCheckInDialog.value = false; | ||
| 385 | + return; | ||
| 386 | + } | ||
| 387 | + selectedCheckIn.value = type; | ||
| 388 | +}; | ||
| 389 | + | ||
| 390 | +// 处理打卡提交 | ||
| 391 | +const handleCheckInSubmit = async () => { | ||
| 392 | + if (!selectedCheckIn.value) { | ||
| 393 | + showToast('请选择打卡项目'); | ||
| 394 | + return; | ||
| 395 | + } | ||
| 396 | + | ||
| 397 | + isCheckingIn.value = true; | ||
| 398 | + try { | ||
| 399 | + const { code } = await checkinTaskAPI({ task_id: selectedCheckIn.value.id }); | ||
| 400 | + if (code) { | ||
| 401 | + checkInSuccess.value = true; | ||
| 402 | + // 重置表单 | ||
| 403 | + setTimeout(() => { | ||
| 404 | + checkInSuccess.value = false; | ||
| 405 | + selectedCheckIn.value = null; | ||
| 406 | + showCheckInDialog.value = false; | ||
| 407 | + }, 1500); | ||
| 408 | + } | ||
| 409 | + } catch (error) { | ||
| 410 | + console.error('打卡失败:', error); | ||
| 411 | + showToast('打卡失败,请重试'); | ||
| 412 | + } finally { | ||
| 413 | + isCheckingIn.value = false; | ||
| 414 | + } | ||
| 415 | +}; | ||
| 416 | + | ||
| 325 | const goToCheckin = () => { | 417 | const goToCheckin = () => { |
| 326 | - router.push(`/checkin/index?course_id=${course.value.id}`); | 418 | + if(!task_list.length) { |
| 419 | + showToast('暂无打卡任务'); | ||
| 420 | + return; | ||
| 421 | + } | ||
| 422 | + showCheckInDialog.value = true; | ||
| 327 | }; | 423 | }; |
| 328 | </script> | 424 | </script> |
| 329 | 425 | ... | ... |
-
Please register or login to post a comment