hookehuyr

feat(打卡): 完善打卡功能逻辑和接口

- 在ProfilePage.vue中重构打卡按钮点击逻辑,添加无任务时的提示
- 在checkin.js中添加删除、点赞和取消点赞的API接口
- 在IndexCheckInPage.vue中实现点赞和删除打卡动态的功能
- 在HomePage.vue中优化打卡任务显示逻辑,添加无任务时的提示
/*
* @Date: 2025-06-06 09:26:16
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-06 17:46:06
* @LastEditTime: 2025-06-06 18:12:47
* @FilePath: /mlaj/src/api/checkin.js
* @Description: 签到模块相关接口
*/
......@@ -15,6 +15,9 @@ const Api = {
TASK_UPLOAD_LIST: '/srv/?a=checkin&t=upload_list',
TASK_UPLOAD_INFO: '/srv/?a=checkin&t=upload_info',
TASK_UPLOAD_EDIT: '/srv/?a=checkin&t=upload_edit',
TASK_UPLOAD_DEL: '/srv/?a=checkin&t=upload_del',
TASK_UPLOAD_LIKE: '/srv/?a=checkin&t=like',
TASK_UPLOAD_DISLIKE: '/srv/?a=checkin&t=dislike',
}
/**
......@@ -78,3 +81,24 @@ export const getUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_IN
* @returns
*/
export const editUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_EDIT, params))
/**
* @description: 删除打卡动态详情
* @param i 打卡动态ID
* @returns
*/
export const delUploadTaskInfoAPI = (params) => fn(fetch.get(Api.TASK_UPLOAD_DEL, params))
/**
* @description: 给打卡点赞
* @param checkin_id 打卡动态ID
* @returns
*/
export const likeUploadTaskInfoAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_LIKE, params))
/**
* @description: 取消点赞
* @param checkin_id 打卡动态ID
* @returns
*/
export const dislikeUploadTaskInfoAPI = (params) => fn(fetch.post(Api.TASK_UPLOAD_DISLIKE, params))
......
<!--
* @Date: 2025-03-20 19:55:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-06 17:26:39
* @LastEditTime: 2025-06-06 18:22:14
* @FilePath: /mlaj/src/views/HomePage.vue
* @Description: 美乐爱觉教育首页组件
*
......@@ -74,62 +74,69 @@
</FrostedGlass>
<!-- Daily Check-in -->
<FrostedGlass v-if="checkInTypes.length" class="p-4 rounded-xl">
<FrostedGlass class="p-4 rounded-xl">
<div class="flex justify-between items-center mb-3">
<h3 class="font-medium">今日打卡</h3>
<router-link to="/profile" class="text-green-600 text-sm">打卡记录</router-link>
</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>
<!-- <p class="text-green-600 text-sm">+5 积分已添加到您的账户</p> -->
</div>
<template v-else>
<div class="flex space-x-2 py-2">
<button
v-for="checkInType in checkInTypes"
:key="checkInType.id"
:class="[
'flex-1 flex flex-col items-center p-2 rounded-lg transition-colors',
selectedCheckIn?.id === checkInType.id
? 'bg-green-100 border border-green-200'
: 'bg-white/70 border border-gray-100 hover:bg-white'
]"
@click="handleCheckInSelect(checkInType)"
>
<div :class="[
'w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors',
selectedCheckIn?.id === checkInType.id
? 'bg-green-500 text-white'
: 'bg-gray-100 text-gray-500'
]">
<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.name }}</span>
</button>
<template v-if="checkInTypes.length">
<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>
<!-- <p class="text-green-600 text-sm">+5 积分已添加到您的账户</p> -->
</div>
<template v-else>
<div class="flex space-x-2 py-2">
<button
v-for="checkInType in checkInTypes"
:key="checkInType.id"
:class="[
'flex-1 flex flex-col items-center p-2 rounded-lg transition-colors',
selectedCheckIn?.id === checkInType.id
? 'bg-green-100 border border-green-200'
: 'bg-white/70 border border-gray-100 hover:bg-white'
]"
@click="handleCheckInSelect(checkInType)"
>
<div :class="[
'w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors',
selectedCheckIn?.id === checkInType.id
? 'bg-green-500 text-white'
: 'bg-gray-100 text-gray-500'
]">
<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.name }}</span>
</button>
</div>
<div v-if="selectedCheckIn" class="mt-3">
<!-- <textarea
:placeholder="`请输入${selectedCheckIn.name}内容...`"
v-model="checkInContent"
class="w-full p-3 border border-gray-200 rounded-lg text-sm resize-none h-24"
/> -->
<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 v-if="selectedCheckIn" class="mt-3">
<!-- <textarea
:placeholder="`请输入${selectedCheckIn.name}内容...`"
v-model="checkInContent"
class="w-full p-3 border border-gray-200 rounded-lg text-sm resize-none h-24"
/> -->
<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>
</template>
<template v-else>
<div class="text-center">
<p class="text-gray-500">暂无打卡任务</p>
</div>
</template>
</FrostedGlass>
......
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-06 17:40:39
* @LastEditTime: 2025-06-06 18:27:24
* @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
* @Description: 文件描述
-->
......@@ -47,8 +47,7 @@
</div>
</div>
<!-- <div v-if="!taskDetail.is_gray" class="text-wrapper"> -->
<div v-if="taskDetail.is_gray" class="text-wrapper">
<div v-if="!taskDetail.is_finish" class="text-wrapper">
<div class="text-header">打卡类型</div>
<div class="upload-wrapper">
<div @click="goToCheckinImagePage" class="upload-boxer">
......@@ -164,7 +163,7 @@ import AudioPlayer from "@/components/ui/AudioPlayer.vue";
import { useTitle } from '@vueuse/core';
import dayjs from 'dayjs';
import { getTaskDetailAPI, getUploadTaskListAPI } from "@/api/checkin";
import { getTaskDetailAPI, getUploadTaskListAPI, delUploadTaskInfoAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from "@/api/checkin";
const route = useRoute()
const router = useRouter()
......@@ -449,7 +448,8 @@ const mockPosts = ref([
]);
const themeVars = {
calendarSelectedDayBackground: '#4caf50'
calendarSelectedDayBackground: '#4caf50',
calendarHeaderShadow: 'rgba(0, 0, 0, 0.1)',
}
const progress1 = ref(0);
......@@ -531,9 +531,22 @@ const goToCheckinAudioPage = (type) => {
})
}
const handLike = (post) => {
post.is_liked = !post.is_liked;
// TODO: 调用接口
const handLike = async (post) => {
if (!post.is_liked) {
const { code, data } = await likeUploadTaskInfoAPI({ checkin_id: post.id, })
if (code) {
showSuccessToast('点赞成功')
post.likes++;
post.is_liked = true;
}
} else {
const { code, data } = await dislikeUploadTaskInfoAPI({ checkin_id: post.id, })
if (code) {
showSuccessToast('取消点赞成功')
post.likes--;
post.is_liked = false;
}
}
}
const editCheckin = (post) => {
......@@ -574,12 +587,16 @@ const delCheckin = (post) => {
message: '您是否确定要删除该动态?',
confirmButtonColor: '#4caf50',
})
.then(() => {
// on confirm
// TODO: 调用接口
// 删除成功后,刷新页面
showSuccessToast('成功文案');
// showFailToast('失败文案');
.then(async () => {
// 调用接口
const { code, data } = await delUploadTaskInfoAPI({ i: post.id });
if (code) {
// 删除成功后,刷新页面
showSuccessToast('删除成功');
router.go(0);
} else {
showErrorToast('删除失败');
}
})
.catch(() => {
// on cancel
......
......@@ -55,8 +55,8 @@
</div>
<div class="text-xs text-gray-500 mt-1">最长连续</div>
</div>
<div v-if="checkinData.length">
<div @click="showCheckInDialog = true" class="cursor-pointer">
<div>
<div @click="handleCheckin" class="cursor-pointer">
<button
class="bg-gradient-to-r from-green-500 to-green-600 text-white py-2 px-6 rounded-full text-sm shadow-sm"
>
......@@ -274,4 +274,12 @@ const menuItems3 = [
path: "/profile/settings",
},
];
const handleCheckin = () => {
if (checkinData.value.length) {
showCheckInDialog.value = true;
} else {
showToast('暂无打卡任务')
}
}
</script>
......