Showing
2 changed files
with
193 additions
and
5 deletions
| ... | @@ -120,11 +120,15 @@ | ... | @@ -120,11 +120,15 @@ |
| 120 | v-model:show="showCheckInDialog" | 120 | v-model:show="showCheckInDialog" |
| 121 | round | 121 | round |
| 122 | position="bottom" | 122 | position="bottom" |
| 123 | + @close="closeCheckInDialog" | ||
| 123 | :style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }" | 124 | :style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }" |
| 124 | > | 125 | > |
| 125 | <div class="p-4"> | 126 | <div class="p-4"> |
| 126 | <div class="flex justify-between items-center mb-3"> | 127 | <div class="flex justify-between items-center mb-3"> |
| 127 | - <h3 class="font-medium">今日打卡</h3> | 128 | + <h3 class="font-medium"> |
| 129 | + <span :class="{ 'text-green-500' : showTaskList }" @click="toggleTask('today')">今日打卡</span> | ||
| 130 | + <span :class="{ 'text-green-500' : showTimeoutTaskList }" @click="toggleTask('timeout')">历史打卡</span> | ||
| 131 | + </h3> | ||
| 128 | <van-icon name="cross" @click="showCheckInDialog = false" /> | 132 | <van-icon name="cross" @click="showCheckInDialog = false" /> |
| 129 | </div> | 133 | </div> |
| 130 | 134 | ||
| ... | @@ -137,7 +141,7 @@ | ... | @@ -137,7 +141,7 @@ |
| 137 | <template v-else> | 141 | <template v-else> |
| 138 | <div class="grid grid-cols-2 gap-4 py-2"> | 142 | <div class="grid grid-cols-2 gap-4 py-2"> |
| 139 | <button | 143 | <button |
| 140 | - v-for="checkInType in task_list" | 144 | + v-for="checkInType in default_list" |
| 141 | :key="checkInType.id" | 145 | :key="checkInType.id" |
| 142 | class="flex flex-col items-center p-2 rounded-lg border transition-colors | 146 | class="flex flex-col items-center p-2 rounded-lg border transition-colors |
| 143 | bg-white/70 border-gray-100 hover:bg-white" | 147 | bg-white/70 border-gray-100 hover:bg-white" |
| ... | @@ -186,7 +190,7 @@ import dayjs from 'dayjs'; | ... | @@ -186,7 +190,7 @@ import dayjs from 'dayjs'; |
| 186 | import { showToast } from 'vant'; | 190 | import { showToast } from 'vant'; |
| 187 | 191 | ||
| 188 | // 导入接口 | 192 | // 导入接口 |
| 189 | -import { getCourseDetailAPI } from '@/api/course' | 193 | +import { getCourseDetailAPI } from '@/api/course'; |
| 190 | import { checkinTaskAPI } from '@/api/checkin'; | 194 | import { checkinTaskAPI } from '@/api/checkin'; |
| 191 | 195 | ||
| 192 | const router = useRouter(); | 196 | const router = useRouter(); |
| ... | @@ -230,6 +234,10 @@ const course_type_maps = ref({ | ... | @@ -230,6 +234,10 @@ const course_type_maps = ref({ |
| 230 | }) | 234 | }) |
| 231 | 235 | ||
| 232 | const task_list = ref([]); | 236 | const task_list = ref([]); |
| 237 | +const timeout_task_list = ref([]); | ||
| 238 | +const default_list = ref([]); | ||
| 239 | +const showTaskList = ref(true); | ||
| 240 | +const showTimeoutTaskList = ref(false); | ||
| 233 | 241 | ||
| 234 | onMounted(async () => { | 242 | onMounted(async () => { |
| 235 | /** | 243 | /** |
| ... | @@ -242,7 +250,10 @@ onMounted(async () => { | ... | @@ -242,7 +250,10 @@ onMounted(async () => { |
| 242 | if (code) { | 250 | if (code) { |
| 243 | course.value = data; | 251 | course.value = data; |
| 244 | task_list.value = data.task_list || []; | 252 | task_list.value = data.task_list || []; |
| 253 | + timeout_task_list.value = data.timeout_task_list || []; | ||
| 245 | course_lessons.value = data.schedule || []; | 254 | course_lessons.value = data.schedule || []; |
| 255 | + default_list.value = task_list.value; | ||
| 256 | + showTaskList.value = true; | ||
| 246 | } | 257 | } |
| 247 | /** | 258 | /** |
| 248 | * 初始化时计算topWrapperHeight | 259 | * 初始化时计算topWrapperHeight |
| ... | @@ -415,12 +426,32 @@ const handleCheckInSubmit = async () => { | ... | @@ -415,12 +426,32 @@ const handleCheckInSubmit = async () => { |
| 415 | }; | 426 | }; |
| 416 | 427 | ||
| 417 | const goToCheckin = () => { | 428 | const goToCheckin = () => { |
| 418 | - if(!task_list.value.length) { | 429 | + if(!default_list.value.length) { |
| 419 | showToast('暂无打卡任务'); | 430 | showToast('暂无打卡任务'); |
| 420 | return; | 431 | return; |
| 421 | } | 432 | } |
| 422 | showCheckInDialog.value = true; | 433 | showCheckInDialog.value = true; |
| 423 | }; | 434 | }; |
| 435 | + | ||
| 436 | +const toggleTask = (type) => { | ||
| 437 | + if(type === 'today') { | ||
| 438 | + showTaskList.value = true; | ||
| 439 | + showTimeoutTaskList.value = false; | ||
| 440 | + default_list.value = task_list.value; | ||
| 441 | + } else { | ||
| 442 | + showTaskList.value = false; | ||
| 443 | + showTimeoutTaskList.value = true; | ||
| 444 | + default_list.value = timeout_task_list.value; | ||
| 445 | + } | ||
| 446 | +} | ||
| 447 | + | ||
| 448 | +const closeCheckInDialog = () => { | ||
| 449 | + showCheckInDialog.value = false; | ||
| 450 | + // 切换到今日任务 | ||
| 451 | + showTaskList.value = true; | ||
| 452 | + showTimeoutTaskList.value = false; | ||
| 453 | + default_list.value = task_list.value; | ||
| 454 | +} | ||
| 424 | </script> | 455 | </script> |
| 425 | 456 | ||
| 426 | <style scoped> | 457 | <style scoped> | ... | ... |
| ... | @@ -73,7 +73,7 @@ | ... | @@ -73,7 +73,7 @@ |
| 73 | </div> | 73 | </div> |
| 74 | </div> | 74 | </div> |
| 75 | <!-- 标签页区域 --> | 75 | <!-- 标签页区域 --> |
| 76 | - <div class="px-4 py-3 bg-white"> | 76 | + <div class="px-4 py-3 bg-white" style="position: relative;"> |
| 77 | <van-tabs v-model:active="activeTab" sticky animated swipeable shrink @change="handleTabChange"> | 77 | <van-tabs v-model:active="activeTab" sticky animated swipeable shrink @change="handleTabChange"> |
| 78 | <van-tab title="介绍" name="intro"> | 78 | <van-tab title="介绍" name="intro"> |
| 79 | </van-tab> | 79 | </van-tab> |
| ... | @@ -81,6 +81,7 @@ | ... | @@ -81,6 +81,7 @@ |
| 81 | <template #title>评论({{ commentCount }})</template> | 81 | <template #title>评论({{ commentCount }})</template> |
| 82 | </van-tab> | 82 | </van-tab> |
| 83 | </van-tabs> | 83 | </van-tabs> |
| 84 | + <div @click="goToCheckin" style="position: absolute; right: 1rem; top: 1.5rem; font-size: 0.875rem; color: #666;">课程互动</div> | ||
| 84 | </div> | 85 | </div> |
| 85 | </div> | 86 | </div> |
| 86 | 87 | ||
| ... | @@ -239,6 +240,71 @@ | ... | @@ -239,6 +240,71 @@ |
| 239 | 240 | ||
| 240 | <!-- PDF预览 --> | 241 | <!-- PDF预览 --> |
| 241 | <PdfPreview v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" /> | 242 | <PdfPreview v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" /> |
| 243 | + | ||
| 244 | + <!-- 打卡弹窗 --> | ||
| 245 | + <van-popup | ||
| 246 | + v-model:show="showCheckInDialog" | ||
| 247 | + round | ||
| 248 | + position="bottom" | ||
| 249 | + @close="closeCheckInDialog" | ||
| 250 | + :style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }" | ||
| 251 | + > | ||
| 252 | + <div class="p-4"> | ||
| 253 | + <div class="flex justify-between items-center mb-3"> | ||
| 254 | + <h3 class="font-medium"> | ||
| 255 | + <span :class="{ 'text-green-500' : showTaskList }" @click="toggleTask('today')">今日打卡</span> | ||
| 256 | + <span :class="{ 'text-green-500' : showTimeoutTaskList }" @click="toggleTask('timeout')">历史打卡</span> | ||
| 257 | + </h3> | ||
| 258 | + <van-icon name="cross" @click="showCheckInDialog = false" /> | ||
| 259 | + </div> | ||
| 260 | + | ||
| 261 | + <div v-if="checkInSuccess" class="bg-green-50 border border-green-200 rounded-lg p-4 text-center"> | ||
| 262 | + <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"> | ||
| 263 | + <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" /> | ||
| 264 | + </svg> | ||
| 265 | + <h4 class="text-green-700 font-medium mb-1">打卡成功!</h4> | ||
| 266 | + </div> | ||
| 267 | + <template v-else> | ||
| 268 | + <div class="grid grid-cols-2 gap-4 py-2"> | ||
| 269 | + <button | ||
| 270 | + v-for="checkInType in default_list" | ||
| 271 | + :key="checkInType.id" | ||
| 272 | + class="flex flex-col items-center p-2 rounded-lg border transition-colors | ||
| 273 | + bg-white/70 border-gray-100 hover:bg-white" | ||
| 274 | + :class="{ | ||
| 275 | + 'bg-green-100 border-green-200': selectedCheckIn?.id === checkInType.id | ||
| 276 | + }" | ||
| 277 | + @click="handleCheckInSelect(checkInType)" | ||
| 278 | + > | ||
| 279 | + <div class="w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors | ||
| 280 | + bg-gray-100 text-gray-500" | ||
| 281 | + :class="{ | ||
| 282 | + 'bg-green-500 text-white': selectedCheckIn?.id === checkInType.id | ||
| 283 | + }" | ||
| 284 | + > | ||
| 285 | + <van-icon v-if="checkInType.task_type === 'checkin'" name="edit" size="1.5rem" /> | ||
| 286 | + <van-icon v-if="checkInType.task_type === 'upload'" name="tosend" size="1.5rem" /> | ||
| 287 | + </div> | ||
| 288 | + <span class="text-xs">{{ checkInType.title }}</span> | ||
| 289 | + </button> | ||
| 290 | + </div> | ||
| 291 | + | ||
| 292 | + <div v-if="selectedCheckIn" class="mt-3"> | ||
| 293 | + <button | ||
| 294 | + 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" | ||
| 295 | + @click="handleCheckInSubmit" | ||
| 296 | + :disabled="isCheckingIn" | ||
| 297 | + > | ||
| 298 | + <template v-if="isCheckingIn"> | ||
| 299 | + <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> | ||
| 300 | + 提交中... | ||
| 301 | + </template> | ||
| 302 | + <template v-else>提交打卡</template> | ||
| 303 | + </button> | ||
| 304 | + </div> | ||
| 305 | + </template> | ||
| 306 | + </div> | ||
| 307 | + </van-popup> | ||
| 242 | </div> | 308 | </div> |
| 243 | </template> | 309 | </template> |
| 244 | 310 | ||
| ... | @@ -254,10 +320,12 @@ import axios from 'axios'; | ... | @@ -254,10 +320,12 @@ import axios from 'axios'; |
| 254 | import { v4 as uuidv4 } from "uuid"; | 320 | import { v4 as uuidv4 } from "uuid"; |
| 255 | import { useIntersectionObserver } from '@vueuse/core'; | 321 | import { useIntersectionObserver } from '@vueuse/core'; |
| 256 | import PdfPreview from '@/components/ui/PdfPreview.vue'; | 322 | import PdfPreview from '@/components/ui/PdfPreview.vue'; |
| 323 | +import { showToast } from 'vant'; | ||
| 257 | 324 | ||
| 258 | // 导入接口 | 325 | // 导入接口 |
| 259 | import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI, getCourseDetailAPI } from '@/api/course'; | 326 | import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI, getCourseDetailAPI } from '@/api/course'; |
| 260 | import { addStudyRecordAPI } from "@/api/record"; | 327 | import { addStudyRecordAPI } from "@/api/record"; |
| 328 | +import { checkinTaskAPI } from '@/api/checkin'; | ||
| 261 | 329 | ||
| 262 | const route = useRoute(); | 330 | const route = useRoute(); |
| 263 | const router = useRouter(); | 331 | const router = useRouter(); |
| ... | @@ -479,6 +547,10 @@ onMounted(async () => { | ... | @@ -479,6 +547,10 @@ onMounted(async () => { |
| 479 | const detail = await getCourseDetailAPI({ i: course.value.group_id }); | 547 | const detail = await getCourseDetailAPI({ i: course.value.group_id }); |
| 480 | if (detail.code) { | 548 | if (detail.code) { |
| 481 | course_lessons.value = detail.data.schedule || []; | 549 | course_lessons.value = detail.data.schedule || []; |
| 550 | + task_list.value = detail.data.task_list || []; | ||
| 551 | + timeout_task_list.value = detail.timeout_task_list || []; | ||
| 552 | + default_list.value = task_list.value; | ||
| 553 | + showTaskList.value = true; | ||
| 482 | } | 554 | } |
| 483 | } | 555 | } |
| 484 | // 图片附件或者附件不存在 | 556 | // 图片附件或者附件不存在 |
| ... | @@ -883,6 +955,91 @@ watch(showCatalog, (newVal) => { | ... | @@ -883,6 +955,91 @@ watch(showCatalog, (newVal) => { |
| 883 | }); | 955 | }); |
| 884 | } | 956 | } |
| 885 | }); | 957 | }); |
| 958 | + | ||
| 959 | +// 打卡相关状态 | ||
| 960 | +const showCheckInDialog = ref(false); | ||
| 961 | +const selectedCheckIn = ref(null); | ||
| 962 | +const isCheckingIn = ref(false); | ||
| 963 | +const checkInSuccess = ref(false); | ||
| 964 | +const task_list = ref([]); | ||
| 965 | +const timeout_task_list = ref([]); | ||
| 966 | +const default_list = ref([]); | ||
| 967 | +const showTaskList = ref(true); | ||
| 968 | +const showTimeoutTaskList = ref(false); | ||
| 969 | + | ||
| 970 | +// 处理打卡选择 | ||
| 971 | +const handleCheckInSelect = (type) => { | ||
| 972 | + if (type.is_gray && type.task_type === 'checkin') { | ||
| 973 | + showToast('您已经完成了今天的打卡'); | ||
| 974 | + return; | ||
| 975 | + } | ||
| 976 | + if (type.task_type === 'upload') { | ||
| 977 | + router.push({ | ||
| 978 | + path: '/checkin/index', | ||
| 979 | + query: { | ||
| 980 | + id: type.id | ||
| 981 | + } | ||
| 982 | + }); | ||
| 983 | + showCheckInDialog.value = false; | ||
| 984 | + return; | ||
| 985 | + } | ||
| 986 | + selectedCheckIn.value = type; | ||
| 987 | +}; | ||
| 988 | + | ||
| 989 | +// 处理打卡提交 | ||
| 990 | +const handleCheckInSubmit = async () => { | ||
| 991 | + if (!selectedCheckIn.value) { | ||
| 992 | + showToast('请选择打卡项目'); | ||
| 993 | + return; | ||
| 994 | + } | ||
| 995 | + | ||
| 996 | + isCheckingIn.value = true; | ||
| 997 | + try { | ||
| 998 | + const { code } = await checkinTaskAPI({ task_id: selectedCheckIn.value.id }); | ||
| 999 | + if (code) { | ||
| 1000 | + checkInSuccess.value = true; | ||
| 1001 | + // 重置表单 | ||
| 1002 | + setTimeout(() => { | ||
| 1003 | + checkInSuccess.value = false; | ||
| 1004 | + selectedCheckIn.value = null; | ||
| 1005 | + showCheckInDialog.value = false; | ||
| 1006 | + }, 1500); | ||
| 1007 | + } | ||
| 1008 | + } catch (error) { | ||
| 1009 | + console.error('打卡失败:', error); | ||
| 1010 | + showToast('打卡失败,请重试'); | ||
| 1011 | + } finally { | ||
| 1012 | + isCheckingIn.value = false; | ||
| 1013 | + } | ||
| 1014 | +}; | ||
| 1015 | + | ||
| 1016 | +const goToCheckin = () => { | ||
| 1017 | + if(!default_list.value.length) { | ||
| 1018 | + showToast('暂无打卡任务'); | ||
| 1019 | + return; | ||
| 1020 | + } | ||
| 1021 | + showCheckInDialog.value = true; | ||
| 1022 | +}; | ||
| 1023 | + | ||
| 1024 | +const toggleTask = (type) => { | ||
| 1025 | + if(type === 'today') { | ||
| 1026 | + showTaskList.value = true; | ||
| 1027 | + showTimeoutTaskList.value = false; | ||
| 1028 | + default_list.value = task_list.value; | ||
| 1029 | + } else { | ||
| 1030 | + showTaskList.value = false; | ||
| 1031 | + showTimeoutTaskList.value = true; | ||
| 1032 | + default_list.value = timeout_task_list.value; | ||
| 1033 | + } | ||
| 1034 | +} | ||
| 1035 | + | ||
| 1036 | +const closeCheckInDialog = () => { | ||
| 1037 | + showCheckInDialog.value = false; | ||
| 1038 | + // 切换到今日任务 | ||
| 1039 | + showTaskList.value = true; | ||
| 1040 | + showTimeoutTaskList.value = false; | ||
| 1041 | + default_list.value = task_list.value; | ||
| 1042 | +} | ||
| 886 | </script> | 1043 | </script> |
| 887 | 1044 | ||
| 888 | <style lang="less" scoped> | 1045 | <style lang="less" scoped> | ... | ... |
-
Please register or login to post a comment