hookehuyr

feat(打卡功能): 添加打卡对话框组件并集成到个人主页

- 新增 `CheckInDialog.vue` 组件,支持用户选择打卡类型并提交打卡内容
- 在 `ProfilePage.vue` 中集成打卡对话框,处理打卡成功后的逻辑
- 更新 `vite.config.js` 和 `components.d.ts` 以支持 Vant 组件的自动导入
......@@ -11,6 +11,7 @@ declare module 'vue' {
ActivityCard: typeof import('./components/ui/ActivityCard.vue')['default']
AppLayout: typeof import('./components/layout/AppLayout.vue')['default']
BottomNav: typeof import('./components/layout/BottomNav.vue')['default']
CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default']
ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default']
CourseCard: typeof import('./components/ui/CourseCard.vue')['default']
FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default']
......@@ -22,6 +23,7 @@ declare module 'vue' {
SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
VanDatePicker: typeof import('vant/es')['DatePicker']
VanIcon: typeof import('vant/es')['Icon']
VanList: typeof import('vant/es')['List']
VanPickerGroup: typeof import('vant/es')['PickerGroup']
VanPopup: typeof import('vant/es')['Popup']
......
<template>
<van-popup
:show="show"
@update:show="$emit('update:show', $event)"
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="handleClose" />
</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'
]">
<svg v-if="checkInType.id === 'reading'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<svg v-if="checkInType.id === 'exercise'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<svg v-if="checkInType.id === 'study'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path d="M12 14l9-5-9-5-9 5 9 5z" />
<path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998a12.078 12.078 0 01.665-6.479L12 14z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998a12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222" />
</svg>
<svg v-if="checkInType.id === 'reflection'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</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>
</template>
</div>
</van-popup>
</template>
<script setup>
import { ref } from 'vue'
import { showToast } from 'vant'
import 'vant/lib/toast/style'
import { checkInTypes } from '@/utils/mockData'
const props = defineProps({
show: {
type: Boolean,
required: true,
default: false
}
})
const emit = defineEmits(['update:show', 'check-in-success'])
const selectedCheckIn = ref(null)
const checkInContent = ref('')
const isCheckingIn = ref(false)
const checkInSuccess = ref(false)
const handleCheckInSelect = (type) => {
selectedCheckIn.value = type
}
const handleCheckInSubmit = async () => {
if (!selectedCheckIn.value) {
showToast('请选择打卡项目')
return
}
if (!checkInContent.value.trim()) {
showToast('请输入打卡内容')
return
}
isCheckingIn.value = true
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
checkInSuccess.value = true
emit('check-in-success')
// 重置表单
setTimeout(() => {
checkInSuccess.value = false
selectedCheckIn.value = null
checkInContent.value = ''
emit('update:show', false)
}, 1500)
} catch (error) {
showToast('打卡失败,请重试')
} finally {
isCheckingIn.value = false
}
}
const handleClose = () => {
selectedCheckIn.value = null
checkInContent.value = ''
checkInSuccess.value = false
emit('update:show', false)
}
</script>
<template>
<AppLayout title="我的" :right-content="rightContent">
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen">
<div
class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen"
>
<!-- User Profile Header with Enhanced Design -->
<div class="pt-6 pb-8 relative">
<div class="absolute inset-0 bg-gradient-to-r from-green-500 to-blue-500 opacity-15"></div>
<div
class="absolute inset-0 bg-gradient-to-r from-green-500 to-blue-500 opacity-15"
></div>
<div class="relative z-10 flex flex-col items-center">
<div class="w-24 h-24 rounded-full overflow-hidden border-4 border-white shadow-lg mb-4">
<div
class="w-24 h-24 rounded-full overflow-hidden border-4 border-white shadow-lg mb-4"
>
<img
:src="profile.avatar || '/assets/images/user-avatar-1.jpg'"
:alt="profile.name"
......@@ -32,23 +38,33 @@
<div class="flex justify-between">
<div class="flex flex-col items-center">
<div class="text-2xl font-bold text-gray-800">{{ profile.checkIns?.totalDays || 0 }}</div>
<div class="text-2xl font-bold text-gray-800">
{{ profile.checkIns?.totalDays || 0 }}
</div>
<div class="text-xs text-gray-500 mt-1">累计打卡</div>
</div>
<div class="flex flex-col items-center">
<div class="text-2xl font-bold text-green-600">{{ profile.checkIns?.currentStreak || 0 }}</div>
<div class="text-2xl font-bold text-green-600">
{{ profile.checkIns?.currentStreak || 0 }}
</div>
<div class="text-xs text-gray-500 mt-1">连续打卡</div>
</div>
<div class="flex flex-col items-center">
<div class="text-2xl font-bold text-blue-600">{{ profile.checkIns?.longestStreak || 0 }}</div>
<div class="text-2xl font-bold text-blue-600">
{{ profile.checkIns?.longestStreak || 0 }}
</div>
<div class="text-xs text-gray-500 mt-1">最长连续</div>
</div>
<div>
<button class="bg-gradient-to-r from-green-500 to-green-600 text-white py-2 px-6 rounded-full text-sm shadow-sm">
<div @click="showCheckInDialog = true" 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"
>
立即打卡
</button>
</div>
</div>
</div>
</FrostedGlass>
</div>
......@@ -57,21 +73,78 @@
<FrostedGlass class="p-4 rounded-xl">
<h3 class="font-semibold text-base mb-4">打卡项目</h3>
<div class="grid grid-cols-4 gap-2">
<div v-for="type in checkInTypes" :key="type.id" class="flex flex-col items-center cursor-pointer" @click="handleCheckInClick(type.path)">
<div class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center mb-1">
<svg v-if="type.icon === 'book'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
<div
v-for="type in checkInTypes"
:key="type.id"
class="flex flex-col items-center cursor-pointer"
@click="handleCheckInClick(type.path)"
>
<div
class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center mb-1"
>
<svg
v-if="type.icon === 'book'"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
<svg v-if="type.icon === 'running'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
<svg
v-if="type.icon === 'running'"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
<svg v-if="type.icon === 'graduation-cap'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg
v-if="type.icon === 'graduation-cap'"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M12 14l9-5-9-5-9 5 9 5z" />
<path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222" />
<path
d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
/>
</svg>
<svg v-if="type.icon === 'pencil-alt'" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
<svg
v-if="type.icon === 'pencil-alt'"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"
/>
</svg>
</div>
<span class="text-xs">{{ type.name }}</span>
......@@ -110,9 +183,7 @@
</FrostedGlass>
<!-- Version Info -->
<div class="text-center text-xs text-gray-400 mb-4">
亲子教育 App v1.2.0
</div>
<div class="text-center text-xs text-gray-400 mb-4">亲子教育 App v1.2.0</div>
<!-- Logout Button -->
<button
......@@ -124,125 +195,147 @@
</div>
</div>
</AppLayout>
<CheckInDialog
v-model:show="showCheckInDialog"
@check-in-success="handleCheckInSuccess"
/>
</template>
<script setup>
import { ref, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import MenuItem from '@/components/ui/MenuItem.vue'
import { useAuth } from '@/contexts/auth'
import { userProfile, checkInTypes } from '@/utils/mockData'
import { useTitle } from '@vueuse/core';
const router = useRouter()
import { ref, h } from "vue";
import { useRoute, useRouter } from "vue-router";
import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
import MenuItem from "@/components/ui/MenuItem.vue";
import { useAuth } from "@/contexts/auth";
import { userProfile, checkInTypes } from "@/utils/mockData";
import CheckInDialog from "@/components/ui/CheckInDialog.vue";
import { useTitle } from "@vueuse/core";
const router = useRouter();
const $route = useRoute();
const $router = useRouter();
useTitle($route.meta.title);
const { currentUser, logout } = useAuth()
const profile = ref(userProfile)
const { currentUser, logout } = useAuth();
const profile = ref(userProfile);
const showCheckInDialog = ref(false);
// 处理打卡成功
const handleCheckInSuccess = () => {
profile.value.checkIns.totalDays++;
profile.value.checkIns.currentStreak++;
profile.value.checkIns.longestStreak = Math.max(
profile.value.checkIns.longestStreak,
profile.value.checkIns.currentStreak
);
};
// Handle logout
const handleLogout = () => {
logout()
router.push('/login')
}
logout();
router.push("/login");
};
// Handle menu item click
const handleMenuClick = (path) => {
router.push(path)
}
router.push(path);
};
// Handle image error
const handleImageError = (e) => {
e.target.onerror = null
e.target.src = '/assets/images/user-avatar-1.jpg'
}
e.target.onerror = null;
e.target.src = "/assets/images/user-avatar-1.jpg";
};
// Handle check-in type click
const handleCheckInClick = (path) => {
router.push(path)
}
router.push(path);
};
// Right content component
const rightContent = h('div', { class: 'flex items-center' }, [
h('button', { class: 'p-2' }, [
h('svg', {
xmlns: 'http://www.w3.org/2000/svg',
class: 'h-6 w-6 text-gray-700',
fill: 'none',
viewBox: '0 0 24 24',
stroke: 'currentColor'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
d: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9'
})
])
])
])
const rightContent = h("div", { class: "flex items-center" }, [
h("button", { class: "p-2" }, [
h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
class: "h-6 w-6 text-gray-700",
fill: "none",
viewBox: "0 0 24 24",
stroke: "currentColor",
},
[
h("path", {
"stroke-linecap": "round",
"stroke-linejoin": "round",
"stroke-width": "2",
d:
"M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9",
}),
]
),
]),
]);
// Menu items
const menuItems1 = [
{
icon: 'clock',
title: '学习记录',
path: '/profile/learning-records',
badge: 'NEW'
icon: "clock",
title: "学习记录",
path: "/profile/learning-records",
badge: "NEW",
},
{
icon: 'user',
title: '我的活动',
path: '/profile/activities'
icon: "user",
title: "我的活动",
path: "/profile/activities",
},
{
icon: 'book',
title: '我的课程',
path: '/profile/courses'
icon: "book",
title: "我的课程",
path: "/profile/courses",
},
{
icon: 'heart',
title: '我的收藏',
path: '/profile/favorites'
}
]
icon: "heart",
title: "我的收藏",
path: "/profile/favorites",
},
];
const menuItems2 = [
{
icon: 'wallet',
title: '我的钱包',
path: '/profile/wallet'
icon: "wallet",
title: "我的钱包",
path: "/profile/wallet",
},
{
icon: 'document',
title: '我的订单',
path: '/profile/orders'
icon: "document",
title: "我的订单",
path: "/profile/orders",
},
{
icon: 'chat',
title: '我的圈子',
path: '/profile/community'
}
]
icon: "chat",
title: "我的圈子",
path: "/profile/community",
},
];
const menuItems3 = [
{
icon: 'email',
title: '消息中心',
path: '/profile/messages',
badge: '3'
icon: "email",
title: "消息中心",
path: "/profile/messages",
badge: "3",
},
{
icon: 'question',
title: '帮助中心',
path: '/profile/help'
icon: "question",
title: "帮助中心",
path: "/profile/help",
},
{
icon: 'settings',
title: '设置',
path: '/profile/settings'
}
]
icon: "settings",
title: "设置",
path: "/profile/settings",
},
];
</script>
......
/*
* @Date: 2025-03-20 19:53:12
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-20 23:44:51
* @LastEditTime: 2025-03-21 14:21:42
* @FilePath: /mlaj/vite.config.js
* @Description: 文件描述
*/
......@@ -31,6 +31,7 @@ export default ({ command, mode }) => {
vue(),
vueJsx(),
AutoImport({
resolvers: [VantResolver()],
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts',
// 解决eslint报错问题
......