hookehuyr

feat(打卡功能): 在课程详情页和课程学习页添加打卡弹窗功能

添加今日打卡和历史打卡切换功能,支持任务选择和提交打卡
......@@ -120,11 +120,15 @@
v-model:show="showCheckInDialog"
round
position="bottom"
@close="closeCheckInDialog"
: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>
<h3 class="font-medium">
<span :class="{ 'text-green-500' : showTaskList }" @click="toggleTask('today')">今日打卡</span>&nbsp;&nbsp;&nbsp;&nbsp;
<span :class="{ 'text-green-500' : showTimeoutTaskList }" @click="toggleTask('timeout')">历史打卡</span>
</h3>
<van-icon name="cross" @click="showCheckInDialog = false" />
</div>
......@@ -137,7 +141,7 @@
<template v-else>
<div class="grid grid-cols-2 gap-4 py-2">
<button
v-for="checkInType in task_list"
v-for="checkInType in default_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"
......@@ -186,7 +190,7 @@ import dayjs from 'dayjs';
import { showToast } from 'vant';
// 导入接口
import { getCourseDetailAPI } from '@/api/course'
import { getCourseDetailAPI } from '@/api/course';
import { checkinTaskAPI } from '@/api/checkin';
const router = useRouter();
......@@ -230,6 +234,10 @@ const course_type_maps = ref({
})
const task_list = ref([]);
const timeout_task_list = ref([]);
const default_list = ref([]);
const showTaskList = ref(true);
const showTimeoutTaskList = ref(false);
onMounted(async () => {
/**
......@@ -242,7 +250,10 @@ onMounted(async () => {
if (code) {
course.value = data;
task_list.value = data.task_list || [];
timeout_task_list.value = data.timeout_task_list || [];
course_lessons.value = data.schedule || [];
default_list.value = task_list.value;
showTaskList.value = true;
}
/**
* 初始化时计算topWrapperHeight
......@@ -415,12 +426,32 @@ const handleCheckInSubmit = async () => {
};
const goToCheckin = () => {
if(!task_list.value.length) {
if(!default_list.value.length) {
showToast('暂无打卡任务');
return;
}
showCheckInDialog.value = true;
};
const toggleTask = (type) => {
if(type === 'today') {
showTaskList.value = true;
showTimeoutTaskList.value = false;
default_list.value = task_list.value;
} else {
showTaskList.value = false;
showTimeoutTaskList.value = true;
default_list.value = timeout_task_list.value;
}
}
const closeCheckInDialog = () => {
showCheckInDialog.value = false;
// 切换到今日任务
showTaskList.value = true;
showTimeoutTaskList.value = false;
default_list.value = task_list.value;
}
</script>
<style scoped>
......
......@@ -73,7 +73,7 @@
</div>
</div>
<!-- 标签页区域 -->
<div class="px-4 py-3 bg-white">
<div class="px-4 py-3 bg-white" style="position: relative;">
<van-tabs v-model:active="activeTab" sticky animated swipeable shrink @change="handleTabChange">
<van-tab title="介绍" name="intro">
</van-tab>
......@@ -81,6 +81,7 @@
<template #title>评论({{ commentCount }})</template>
</van-tab>
</van-tabs>
<div @click="goToCheckin" style="position: absolute; right: 1rem; top: 1.5rem; font-size: 0.875rem; color: #666;">课程互动</div>
</div>
</div>
......@@ -239,6 +240,71 @@
<!-- PDF预览 -->
<PdfPreview v-model:show="pdfShow" :url="pdfUrl" :title="pdfTitle" @onLoad="onPdfLoad" />
<!-- 打卡弹窗 -->
<van-popup
v-model:show="showCheckInDialog"
round
position="bottom"
@close="closeCheckInDialog"
:style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }"
>
<div class="p-4">
<div class="flex justify-between items-center mb-3">
<h3 class="font-medium">
<span :class="{ 'text-green-500' : showTaskList }" @click="toggleTask('today')">今日打卡</span>&nbsp;&nbsp;&nbsp;&nbsp;
<span :class="{ 'text-green-500' : showTimeoutTaskList }" @click="toggleTask('timeout')">历史打卡</span>
</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 default_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>
......@@ -254,10 +320,12 @@ import axios from 'axios';
import { v4 as uuidv4 } from "uuid";
import { useIntersectionObserver } from '@vueuse/core';
import PdfPreview from '@/components/ui/PdfPreview.vue';
import { showToast } from 'vant';
// 导入接口
import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI, getCourseDetailAPI } from '@/api/course';
import { addStudyRecordAPI } from "@/api/record";
import { checkinTaskAPI } from '@/api/checkin';
const route = useRoute();
const router = useRouter();
......@@ -479,6 +547,10 @@ onMounted(async () => {
const detail = await getCourseDetailAPI({ i: course.value.group_id });
if (detail.code) {
course_lessons.value = detail.data.schedule || [];
task_list.value = detail.data.task_list || [];
timeout_task_list.value = detail.timeout_task_list || [];
default_list.value = task_list.value;
showTaskList.value = true;
}
}
// 图片附件或者附件不存在
......@@ -883,6 +955,91 @@ watch(showCatalog, (newVal) => {
});
}
});
// 打卡相关状态
const showCheckInDialog = ref(false);
const selectedCheckIn = ref(null);
const isCheckingIn = ref(false);
const checkInSuccess = ref(false);
const task_list = ref([]);
const timeout_task_list = ref([]);
const default_list = ref([]);
const showTaskList = ref(true);
const showTimeoutTaskList = 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 = () => {
if(!default_list.value.length) {
showToast('暂无打卡任务');
return;
}
showCheckInDialog.value = true;
};
const toggleTask = (type) => {
if(type === 'today') {
showTaskList.value = true;
showTimeoutTaskList.value = false;
default_list.value = task_list.value;
} else {
showTaskList.value = false;
showTimeoutTaskList.value = true;
default_list.value = timeout_task_list.value;
}
}
const closeCheckInDialog = () => {
showCheckInDialog.value = false;
// 切换到今日任务
showTaskList.value = true;
showTimeoutTaskList.value = false;
default_list.value = task_list.value;
}
</script>
<style lang="less" scoped>
......