hookehuyr

feat(profile): 添加学习记录页面及格式化时长功能

新增学习记录页面,展示用户的学习进度和时长。同时添加了格式化时长功能,将秒数转换为可读格式,便于用户理解
......@@ -34,6 +34,7 @@ declare module 'vue' {
VanList: typeof import('vant/es')['List']
VanPickerGroup: typeof import('vant/es')['PickerGroup']
VanPopup: typeof import('vant/es')['Popup']
VanProgress: typeof import('vant/es')['Progress']
VanRate: typeof import('vant/es')['Rate']
VanTab: typeof import('vant/es')['Tab']
VanTabs: typeof import('vant/es')['Tabs']
......
......@@ -164,6 +164,12 @@ const routes = [
meta: { title: '修改密码' },
},
{
path: '/profile/learning-records',
name: 'LearningRecords',
component: () => import('../views/profile/LearningRecordsPage.vue'),
meta: { title: '学习记录' },
},
{
path: '/test',
name: 'test',
component: () => import('../views/test.vue'),
......
......@@ -125,6 +125,25 @@ const stringifyQuery = (params) => {
return '?' + queryString.join('&');
};
// 格式化时长(秒转换为可读格式)
const formatDuration = (seconds) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
let result = '';
if (hours > 0) {
result += `${hours}小时`;
}
if (minutes > 0) {
result += `${minutes}分钟`;
}
if (remainingSeconds > 0 || result === '') {
result += `${remainingSeconds}秒`;
}
return result;
};
export {
formatDate,
wxInfo,
......@@ -134,4 +153,5 @@ export {
changeURLArg,
getUrlParams,
stringifyQuery,
formatDuration,
};
......
<template>
<div class="bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen pb-20">
<!-- Course List -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
class="px-4 py-3 space-y-4"
>
<FrostedGlass
v-for="record in records"
:key="record.id"
class="p-4 rounded-xl"
>
<div class="flex items-start">
<div
class="w-20 h-20 rounded-lg overflow-hidden flex-shrink-0 mr-3"
>
<van-image
:src="record.course.coverImage"
:alt="record.course.title"
class="w-full h-full"
fit="cover"
error-icon="photo-fail"
loading-icon="photo"
:error-icon-size="32"
:loading-icon-size="32"
/>
</div>
<div class="flex-1">
<h3 class="text-base font-medium mb-2 line-clamp-1">
{{ record.course.title }}
</h3>
<div class="flex items-center text-sm text-gray-500 mb-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 mr-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>学习时长:{{ formatDuration(record.duration) }}</span>
</div>
<div class="flex items-center text-sm text-gray-500 mb-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 mr-1"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
<span>最近学习:{{ formatDate(record.lastStudyTime) }}</span>
</div>
<div class="flex items-center">
<div class="flex-1">
<van-progress
:percentage="record.progress"
:stroke-width="4"
color="#10B981"
/>
</div>
<span class="ml-3 text-sm text-green-600">{{ record.progress }}%</span>
</div>
</div>
</div>
</FrostedGlass>
</van-list>
<!-- 无数据提示 -->
<div
v-if="!loading && records.length === 0"
class="flex flex-col items-center justify-center py-12"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-16 w-16 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
<p class="mt-4 text-gray-500">暂无学习记录</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { useTitle } from '@vueuse/core';
import FrostedGlass from '@/components/ui/FrostedGlass.vue';
import { formatDate, formatDuration } from '@/utils/tools';
const $route = useRoute();
useTitle($route.meta.title);
const records = ref([]);
const loading = ref(false);
const finished = ref(false);
const page = ref(1);
const pageSize = 10;
// 模拟学习记录数据
const mockRecords = [
{
id: 1,
course: {
title: '亲子教育必修课:如何培养孩子的学习兴趣',
coverImage: '/assets/images/course-1.jpg'
},
duration: 3600, // 秒
lastStudyTime: '2024-01-15T10:30:00',
progress: 75
},
{
id: 2,
course: {
title: '儿童心理发展指南:0-6岁关键期教育方法',
coverImage: '/assets/images/course-2.jpg'
},
duration: 7200,
lastStudyTime: '2024-01-14T15:20:00',
progress: 45
}
];
// 加载数据
const onLoad = () => {
loading.value = true;
// 模拟异步加载
setTimeout(() => {
const start = (page.value - 1) * pageSize;
const end = start + pageSize;
const newRecords = mockRecords.slice(start, end);
records.value.push(...newRecords);
loading.value = false;
if (newRecords.length < pageSize) {
finished.value = true;
} else {
page.value += 1;
}
}, 1000);
};
// 处理图片加载错误
const handleImageError = (e) => {
e.target.onerror = null;
e.target.src = '/assets/images/course-placeholder.jpg';
};
</script>
......@@ -211,11 +211,12 @@ 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 showCheckInDialog = ref(false);
......