hookehuyr

feat(打卡): 添加课程打卡弹窗功能并显示关联打卡数量

- 新增打卡弹窗组件,包含打卡类型选择和提交功能
- 动态显示关联打卡数量而非固定文本
- 处理打卡提交逻辑和状态管理
- 优化打卡入口逻辑,无任务时提示用户
......@@ -103,7 +103,7 @@
<van-icon size="3rem" name="calendar-o" class="text-xl text-gray-600" />
<div>
<div class="text-base font-medium">打卡</div>
<div class="text-sm text-gray-500">关联7个打卡</div>
<div class="text-sm text-gray-500">关联{{ task_list.length }}个打卡</div>
</div>
</div>
<van-icon name="arrow" class="text-gray-400" />
......@@ -114,6 +114,67 @@
<div style="height: 30vh;"></div>
</div>
</div>
<!-- 打卡弹窗 -->
<van-popup
v-model:show="showCheckInDialog"
round
position="bottom"
:style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }"
>
<div class="p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="font-medium">今日打卡</h3>
<van-icon name="cross" @click="showCheckInDialog = false" />
</div>
<div v-if="checkInSuccess" class="bg-green-50 border border-green-200 rounded-lg p-4 text-center">
<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">
<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" />
</svg>
<h4 class="text-green-700 font-medium mb-1">打卡成功!</h4>
</div>
<template v-else>
<div class="grid grid-cols-2 gap-4 py-2">
<button
v-for="checkInType in task_list"
:key="checkInType.id"
class="flex flex-col items-center p-2 rounded-lg border transition-colors
bg-white/70 border-gray-100 hover:bg-white"
:class="{
'bg-green-100 border-green-200': selectedCheckIn?.id === checkInType.id
}"
@click="handleCheckInSelect(checkInType)"
>
<div class="w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors
bg-gray-100 text-gray-500"
:class="{
'bg-green-500 text-white': selectedCheckIn?.id === checkInType.id
}"
>
<van-icon v-if="checkInType.task_type === 'checkin'" name="edit" size="1.5rem" />
<van-icon v-if="checkInType.task_type === 'upload'" name="tosend" size="1.5rem" />
</div>
<span class="text-xs">{{ checkInType.title }}</span>
</button>
</div>
<div v-if="selectedCheckIn" class="mt-3">
<button
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"
@click="handleCheckInSubmit"
:disabled="isCheckingIn"
>
<template v-if="isCheckingIn">
<div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>
提交中...
</template>
<template v-else>提交打卡</template>
</button>
</div>
</template>
</div>
</van-popup>
</div>
</template>
......@@ -122,9 +183,11 @@ import { ref, onMounted, nextTick, onUnmounted } from 'vue';
import { useTitle } from '@vueuse/core';
import { useRouter } from "vue-router";
import dayjs from 'dayjs';
import { showToast } from 'vant';
// 导入接口
import { getCourseDetailAPI } from '@/api/course'
import { checkinTaskAPI } from '@/api/checkin';
const router = useRouter();
......@@ -166,6 +229,8 @@ const course_type_maps = ref({
file: '文件',
})
const task_list = ref([]);
onMounted(async () => {
/**
* 组件挂载时获取课程详情
......@@ -176,6 +241,7 @@ onMounted(async () => {
const { code, data } = await getCourseDetailAPI({ i: courseId });
if (code) {
course.value = data;
task_list.value = data.task_list || [];
course_lessons.value = data.schedule || [];
}
/**
......@@ -292,38 +358,68 @@ const handleTabChange = (name) => {
});
};
// 课程数据
// const course = ref({
// title: '开学礼·止的智慧·心法老师·20241001',
// coverImage: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg',
// updateTime: '2024.01.17',
// viewCount: 1897,
// description: '这是一门关于心法的课程,帮助学员掌握止的智慧...',
// lessons: [
// {
// title: '第一课:止的基础',
// duration: '45分钟',
// progress: 100
// },
// {
// title: '第二课:止的技巧',
// duration: '50分钟',
// progress: 60
// },
// {
// title: '第三课:止的应用',
// duration: '40分钟',
// progress: 0
// }
// ]
// });
const goToStudyDetail = (lessonId) => {
router.push(`/studyDetail/${lessonId}`);
};
// 打卡相关状态
const showCheckInDialog = ref(false);
const selectedCheckIn = ref(null);
const isCheckingIn = ref(false);
const checkInSuccess = ref(false);
// 处理打卡选择
const handleCheckInSelect = (type) => {
if (type.is_gray && type.task_type === 'checkin') {
showToast('您已经完成了今天的打卡');
return;
}
if (type.task_type === 'upload') {
router.push({
path: '/checkin/index',
query: {
id: type.id
}
});
showCheckInDialog.value = false;
return;
}
selectedCheckIn.value = type;
};
// 处理打卡提交
const handleCheckInSubmit = async () => {
if (!selectedCheckIn.value) {
showToast('请选择打卡项目');
return;
}
isCheckingIn.value = true;
try {
const { code } = await checkinTaskAPI({ task_id: selectedCheckIn.value.id });
if (code) {
checkInSuccess.value = true;
// 重置表单
setTimeout(() => {
checkInSuccess.value = false;
selectedCheckIn.value = null;
showCheckInDialog.value = false;
}, 1500);
}
} catch (error) {
console.error('打卡失败:', error);
showToast('打卡失败,请重试');
} finally {
isCheckingIn.value = false;
}
};
const goToCheckin = () => {
router.push(`/checkin/index?course_id=${course.value.id}`);
if(!task_list.length) {
showToast('暂无打卡任务');
return;
}
showCheckInDialog.value = true;
};
</script>
......