hookehuyr

feat(打卡模块): 新增打卡功能相关页面和路由配置

添加打卡模块的四个页面(阅读、运动、学习、写作)及其路由配置,并在用户资料页面增加打卡项目点击跳转功能。同时更新了mock数据和组件类型声明以支持新功能。
......@@ -21,7 +21,10 @@ declare module 'vue' {
RouterView: typeof import('vue-router')['RouterView']
SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
VanDatePicker: typeof import('vant/es')['DatePicker']
VanList: typeof import('vant/es')['List']
VanPickerGroup: typeof import('vant/es')['PickerGroup']
VanPopup: typeof import('vant/es')['Popup']
VanRate: typeof import('vant/es')['Rate']
VanTab: typeof import('vant/es')['Tab']
VanTabs: typeof import('vant/es')['Tabs']
......
/*
* @Date: 2025-03-21 13:28:30
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-21 13:33:06
* @FilePath: /mlaj/src/router/checkin.js
* @Description: 文件描述
*/
export default [
{
path: '/checkin/reading',
name: 'ReadingCheckIn',
component: () => import('@/views/checkin/ReadingCheckInPage.vue'),
meta: {
title: '阅读打卡',
requiresAuth: true
}
},
{
path: '/checkin/exercise',
name: 'ExerciseCheckIn',
component: () => import('@/views/checkin/ExerciseCheckInPage.vue'),
meta: {
title: '运动打卡',
requiresAuth: true
}
},
{
path: '/checkin/study',
name: 'StudyCheckIn',
component: () => import('@/views/checkin/StudyCheckInPage.vue'),
meta: {
title: '学习打卡',
requiresAuth: true
}
},
{
path: '/checkin/writing',
name: 'WritingCheckIn',
component: () => import('@/views/checkin/WritingCheckInPage.vue'),
meta: {
title: '反思打卡',
requiresAuth: true
}
}
]
......@@ -6,6 +6,7 @@
* @Description: 文件描述
*/
import { createRouter, createWebHistory } from 'vue-router'
import checkinRoutes from './checkin'
const routes = [
{
......@@ -114,6 +115,7 @@ const routes = [
component: () => import('../views/test.vue'),
meta: { title: 'test' }
},
...checkinRoutes
]
const router = createRouter({
......
......@@ -185,10 +185,10 @@ export const userProfile = {
// Daily check-in data
export const checkInTypes = [
{ id: 'reading', name: '阅读打卡', icon: 'book' },
{ id: 'exercise', name: '运动打卡', icon: 'running' },
{ id: 'study', name: '学习打卡', icon: 'graduation-cap' },
{ id: 'reflection', name: '反思打卡', icon: 'pencil-alt' }
{ id: 'reading', name: '阅读打卡', icon: 'book', path: '/checkin/reading' },
{ id: 'exercise', name: '运动打卡', icon: 'running', path: '/checkin/exercise' },
{ id: 'study', name: '学习打卡', icon: 'graduation-cap', path: '/checkin/study' },
{ id: 'reflection', name: '反思打卡', icon: 'pencil-alt', path: '/checkin/writing' }
];
// Community posts data
......
<!--
* @Date: 2025-03-21 13:27:50
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-21 13:53:13
* @FilePath: /mlaj/src/views/checkin/ExerciseCheckInPage.vue
* @Description: 文件描述
-->
<template>
<AppLayout title="运动打卡" :show-back="true">
<div
class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen p-4"
>
<!-- 时间筛选 -->
<FrostedGlass class="p-4 rounded-xl mb-4">
<div class="flex items-center justify-between mb-4">
<div class="text-sm text-gray-600">选择时间范围</div>
<div @click="showDatePicker = true" class="text-green-600 text-sm">
{{ formatDateRange }}
</div>
</div>
</FrostedGlass>
<!-- 打卡列表 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<FrostedGlass v-for="item in list" :key="item.id" class="p-4 rounded-xl mb-4">
<div class="flex justify-between items-start mb-2">
<div class="text-gray-900 font-medium">{{ item.exerciseType }}</div>
<div class="text-sm text-gray-500">{{ item.submitTime }}</div>
</div>
<div class="text-gray-600 text-sm">
<div class="mb-1">
时长:{{ item.duration }}分钟 | 强度:{{ item.intensity }}
</div>
<div class="whitespace-pre-wrap">{{ item.thoughts }}</div>
</div>
</FrostedGlass>
</van-list>
<!-- 时间选择器 -->
<van-popup v-model:show="showDatePicker" position="bottom">
<van-picker-group
title="预约日期"
:tabs="['开始日期', '结束日期']"
@confirm="onConfirmDate"
@cancel="onCancelDate"
>
<van-date-picker v-model="startDate" :min-date="minDate" :max-date="maxDate" />
<van-date-picker v-model="endDate" :min-date="minDate" :max-date="maxDate" />
</van-picker-group>
</van-popup>
</div>
</AppLayout>
</template>
<script setup>
import { ref, computed } from "vue";
import { DatePicker, List, Popup } from "vant";
import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
// 列表数据
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const pageSize = 10;
const currentPage = ref(1);
// 日期选择
const showDatePicker = ref(false);
const startDate = ref(["2022", "06", "01"]);
const endDate = ref(["2023", "06", "01"]);
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2025, 5, 1);
// 格式化日期范围显示
const formatDateRange = computed(() => {
return `${startDate.value.join("-")} ~ ${endDate.value.join("-")}`;
});
// 确认日期选择
const onConfirmDate = (values) => {
const [start, end] = values;
startDate.value = start.selectedValues;
endDate.value = end.selectedValues;
showDatePicker.value = false;
// 重置列表并重新加载
list.value = [];
finished.value = false;
currentPage.value = 1;
onLoad();
};
// 取消日期选择
const onCancelDate = () => {
showDatePicker.value = false;
};
// 加载数据
const onLoad = () => {
loading.value = true;
// 模拟数据加载
setTimeout(() => {
const newItems = Array.from({ length: pageSize }, (_, index) => ({
id: list.value.length + index + 1,
exerciseType: ["跑步", "步行", "骑行", "游泳"][Math.floor(Math.random() * 4)],
duration: Math.floor(Math.random() * 120) + 30,
intensity: ["低强度", "中等强度", "高强度"][Math.floor(Math.random() * 3)],
thoughts: "今天的运动很充实,感觉状态不错!",
submitTime: "2024-03-21 14:30:00",
}));
list.value.push(...newItems);
loading.value = false;
currentPage.value += 1;
if (list.value.length >= 30) {
finished.value = true;
}
}, 1000);
};
</script>
<!--
* @Date: 2025-03-21 13:27:25
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-21 13:27:26
* @FilePath: /mlaj/src/views/checkin/ReadingCheckInPage.vue
* @Description: 文件描述
-->
<template>
<AppLayout title="阅读打卡" :show-back="true">
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen p-4">
<!-- 时间筛选 -->
<FrostedGlass class="p-4 rounded-xl mb-4">
<div class="flex items-center justify-between mb-4">
<div class="text-sm text-gray-600">选择时间范围</div>
<div @click="showDatePicker = true" class="text-green-600 text-sm">
{{ formatDateRange }}
</div>
</div>
</FrostedGlass>
<!-- 打卡列表 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<FrostedGlass v-for="item in list" :key="item.id" class="p-4 rounded-xl mb-4">
<div class="flex justify-between items-start mb-2">
<div class="text-gray-900 font-medium">{{ item.bookTitle }}</div>
<div class="text-sm text-gray-500">{{ item.submitTime }}</div>
</div>
<div class="text-gray-600 text-sm">
<div class="mb-1">
阅读时间:{{ item.readingTime }}
</div>
<div class="whitespace-pre-wrap">{{ item.thoughts }}</div>
</div>
</FrostedGlass>
</van-list>
<!-- 时间选择器 -->
<van-popup v-model:show="showDatePicker" position="bottom">
<van-picker-group
title="预约日期"
:tabs="['开始日期', '结束日期']"
@confirm="onConfirmDate"
@cancel="onCancelDate"
>
<van-date-picker v-model="startDate" :min-date="minDate" :max-date="maxDate" />
<van-date-picker v-model="endDate" :min-date="minDate" :max-date="maxDate" />
</van-picker-group>
</van-popup>
</div>
</AppLayout>
</template>
<script setup>
import { ref, computed } from "vue";
import { DatePicker, List, Popup } from "vant";
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
// 列表数据
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const pageSize = 10;
const currentPage = ref(1);
// 日期选择
const showDatePicker = ref(false);
const startDate = ref(["2022", "06", "01"]);
const endDate = ref(["2023", "06", "01"]);
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2025, 5, 1);
// 格式化日期范围显示
const formatDateRange = computed(() => {
return `${startDate.value.join("-")} ~ ${endDate.value.join("-")}`;
});
// 确认日期选择
const onConfirmDate = (values) => {
const [start, end] = values;
startDate.value = start.selectedValues;
endDate.value = end.selectedValues;
showDatePicker.value = false;
// 重置列表并重新加载
list.value = [];
finished.value = false;
currentPage.value = 1;
onLoad();
};
// 取消日期选择
const onCancelDate = () => {
showDatePicker.value = false;
};
// 加载数据
const onLoad = () => {
loading.value = true;
// 模拟数据加载
setTimeout(() => {
const newItems = Array.from({ length: pageSize }, (_, index) => ({
id: list.value.length + index + 1,
bookTitle: ["深入理解计算机系统", "JavaScript高级程序设计", "算法导论", "设计模式"][Math.floor(Math.random() * 4)],
readingTime: "1小时30分钟",
thoughts: "今天的阅读收获很多,对这个主题有了更深的理解!",
submitTime: "2024-03-21 14:30:00",
}));
list.value.push(...newItems);
loading.value = false;
currentPage.value += 1;
if (list.value.length >= 30) {
finished.value = true;
}
}, 1000);
};
</script>
<!--
* @Date: 2025-03-21 13:28:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-21 13:28:08
* @FilePath: /mlaj/src/views/checkin/StudyCheckInPage.vue
* @Description: 文件描述
-->
<template>
<AppLayout title="学习打卡" :show-back="true">
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen p-4">
<!-- 时间筛选 -->
<FrostedGlass class="p-4 rounded-xl mb-4">
<div class="flex items-center justify-between mb-4">
<div class="text-sm text-gray-600">选择时间范围</div>
<div @click="showDatePicker = true" class="text-green-600 text-sm">
{{ formatDateRange }}
</div>
</div>
</FrostedGlass>
<!-- 打卡列表 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<FrostedGlass v-for="item in list" :key="item.id" class="p-4 rounded-xl mb-4">
<div class="flex justify-between items-start mb-2">
<div class="text-gray-900 font-medium">{{ item.subject }}</div>
<div class="text-sm text-gray-500">{{ item.submitTime }}</div>
</div>
<div class="text-gray-600 text-sm">
<div class="mb-1">
学习时长:{{ item.duration }}分钟
</div>
<div class="mb-1">学习内容:{{ item.content }}</div>
<div class="whitespace-pre-wrap">学习收获:{{ item.thoughts }}</div>
</div>
</FrostedGlass>
</van-list>
<!-- 时间选择器 -->
<van-popup v-model:show="showDatePicker" position="bottom">
<van-picker-group
title="预约日期"
:tabs="['开始日期', '结束日期']"
@confirm="onConfirmDate"
@cancel="onCancelDate"
>
<van-date-picker v-model="startDate" :min-date="minDate" :max-date="maxDate" />
<van-date-picker v-model="endDate" :min-date="minDate" :max-date="maxDate" />
</van-picker-group>
</van-popup>
</div>
</AppLayout>
</template>
<script setup>
import { ref, computed } from "vue";
import { DatePicker, List, Popup } from "vant";
import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
// 列表数据
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const pageSize = 10;
const currentPage = ref(1);
// 日期选择
const showDatePicker = ref(false);
const startDate = ref(["2022", "06", "01"]);
const endDate = ref(["2023", "06", "01"]);
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2025, 5, 1);
// 格式化日期范围显示
const formatDateRange = computed(() => {
return `${startDate.value.join("-")} ~ ${endDate.value.join("-")}`;
});
// 确认日期选择
const onConfirmDate = (values) => {
const [start, end] = values;
startDate.value = start.selectedValues;
endDate.value = end.selectedValues;
showDatePicker.value = false;
// 重置列表并重新加载
list.value = [];
finished.value = false;
currentPage.value = 1;
onLoad();
};
// 取消日期选择
const onCancelDate = () => {
showDatePicker.value = false;
};
// 加载数据
const onLoad = () => {
loading.value = true;
// 模拟数据加载
setTimeout(() => {
const newItems = Array.from({ length: pageSize }, (_, index) => ({
id: list.value.length + index + 1,
subject: ["数学", "英语", "物理", "化学"][Math.floor(Math.random() * 4)],
duration: Math.floor(Math.random() * 120) + 30,
content: "今天学习了很多知识点,收获颇丰。",
thoughts: "通过今天的学习,加深了对知识的理解。",
submitTime: "2024-03-21 14:30:00",
}));
list.value.push(...newItems);
loading.value = false;
currentPage.value += 1;
if (list.value.length >= 30) {
finished.value = true;
}
}, 1000);
};
</script>
<!--
* @Date: 2025-03-21 13:28:22
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-21 13:28:24
* @FilePath: /mlaj/src/views/checkin/WritingCheckInPage.vue
* @Description: 文件描述
-->
<template>
<AppLayout title="写作打卡" :show-back="true">
<div class="bg-gradient-to-br from-green-50 via-green-100/30 to-blue-50/30 min-h-screen p-4">
<!-- 时间筛选 -->
<FrostedGlass class="p-4 rounded-xl mb-4">
<div class="flex items-center justify-between mb-4">
<div class="text-sm text-gray-600">选择时间范围</div>
<div @click="showDatePicker = true" class="text-green-600 text-sm">
{{ formatDateRange }}
</div>
</div>
</FrostedGlass>
<!-- 打卡列表 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<FrostedGlass v-for="item in list" :key="item.id" class="p-4 rounded-xl mb-4">
<div class="flex justify-between items-start mb-2">
<div class="text-gray-900 font-medium">{{ item.topic }}</div>
<div class="text-sm text-gray-500">{{ item.submitTime }}</div>
</div>
<div class="text-gray-600 text-sm">
<div class="mb-1">
写作时长:{{ item.duration }}分钟
</div>
<div class="mb-1">写作内容:{{ item.content }}</div>
<div class="whitespace-pre-wrap">感悟:{{ item.thoughts }}</div>
</div>
</FrostedGlass>
</van-list>
<!-- 时间选择器 -->
<van-popup v-model:show="showDatePicker" position="bottom">
<van-picker-group
title="预约日期"
:tabs="['开始日期', '结束日期']"
@confirm="onConfirmDate"
@cancel="onCancelDate"
>
<van-date-picker v-model="startDate" :min-date="minDate" :max-date="maxDate" />
<van-date-picker v-model="endDate" :min-date="minDate" :max-date="maxDate" />
</van-picker-group>
</van-popup>
</div>
</AppLayout>
</template>
<script setup>
import { ref, computed } from "vue";
import { DatePicker, List, Popup } from "vant";
import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
// 列表数据
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
const pageSize = 10;
const currentPage = ref(1);
// 日期选择
const showDatePicker = ref(false);
const startDate = ref(["2022", "06", "01"]);
const endDate = ref(["2023", "06", "01"]);
const minDate = new Date(2020, 0, 1);
const maxDate = new Date(2025, 5, 1);
// 格式化日期范围显示
const formatDateRange = computed(() => {
return `${startDate.value.join("-")} ~ ${endDate.value.join("-")}`;
});
// 确认日期选择
const onConfirmDate = (values) => {
const [start, end] = values;
startDate.value = start.selectedValues;
endDate.value = end.selectedValues;
showDatePicker.value = false;
// 重置列表并重新加载
list.value = [];
finished.value = false;
currentPage.value = 1;
onLoad();
};
// 取消日期选择
const onCancelDate = () => {
showDatePicker.value = false;
};
// 加载数据
const onLoad = () => {
loading.value = true;
// 模拟数据加载
setTimeout(() => {
const newItems = Array.from({ length: pageSize }, (_, index) => ({
id: list.value.length + index + 1,
topic: ["每日随笔", "读书感悟", "生活记录", "技术博客"][Math.floor(Math.random() * 4)],
duration: Math.floor(Math.random() * 120) + 30,
content: "这是一段写作内容的示例...",
thoughts: "今天的写作让我收获颇丰,继续加油!",
submitTime: "2024-03-21 14:30:00",
}));
list.value.push(...newItems);
loading.value = false;
currentPage.value += 1;
if (list.value.length >= 30) {
finished.value = true;
}
}, 1000);
};
</script>
......@@ -57,7 +57,7 @@
<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">
<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" />
......@@ -159,6 +159,11 @@ const handleImageError = (e) => {
e.target.src = '/assets/images/user-avatar-1.jpg'
}
// Handle check-in type click
const handleCheckInClick = (path) => {
router.push(path)
}
// Right content component
const rightContent = h('div', { class: 'flex items-center' }, [
h('button', { class: 'p-2' }, [
......