hookehuyr

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

新增学习记录页面,展示用户的学习进度和时长。同时添加了格式化时长功能,将秒数转换为可读格式,便于用户理解
...@@ -34,6 +34,7 @@ declare module 'vue' { ...@@ -34,6 +34,7 @@ declare module 'vue' {
34 VanList: typeof import('vant/es')['List'] 34 VanList: typeof import('vant/es')['List']
35 VanPickerGroup: typeof import('vant/es')['PickerGroup'] 35 VanPickerGroup: typeof import('vant/es')['PickerGroup']
36 VanPopup: typeof import('vant/es')['Popup'] 36 VanPopup: typeof import('vant/es')['Popup']
37 + VanProgress: typeof import('vant/es')['Progress']
37 VanRate: typeof import('vant/es')['Rate'] 38 VanRate: typeof import('vant/es')['Rate']
38 VanTab: typeof import('vant/es')['Tab'] 39 VanTab: typeof import('vant/es')['Tab']
39 VanTabs: typeof import('vant/es')['Tabs'] 40 VanTabs: typeof import('vant/es')['Tabs']
......
...@@ -164,6 +164,12 @@ const routes = [ ...@@ -164,6 +164,12 @@ const routes = [
164 meta: { title: '修改密码' }, 164 meta: { title: '修改密码' },
165 }, 165 },
166 { 166 {
167 + path: '/profile/learning-records',
168 + name: 'LearningRecords',
169 + component: () => import('../views/profile/LearningRecordsPage.vue'),
170 + meta: { title: '学习记录' },
171 + },
172 + {
167 path: '/test', 173 path: '/test',
168 name: 'test', 174 name: 'test',
169 component: () => import('../views/test.vue'), 175 component: () => import('../views/test.vue'),
......
...@@ -125,6 +125,25 @@ const stringifyQuery = (params) => { ...@@ -125,6 +125,25 @@ const stringifyQuery = (params) => {
125 return '?' + queryString.join('&'); 125 return '?' + queryString.join('&');
126 }; 126 };
127 127
128 +// 格式化时长(秒转换为可读格式)
129 +const formatDuration = (seconds) => {
130 + const hours = Math.floor(seconds / 3600);
131 + const minutes = Math.floor((seconds % 3600) / 60);
132 + const remainingSeconds = seconds % 60;
133 +
134 + let result = '';
135 + if (hours > 0) {
136 + result += `${hours}小时`;
137 + }
138 + if (minutes > 0) {
139 + result += `${minutes}分钟`;
140 + }
141 + if (remainingSeconds > 0 || result === '') {
142 + result += `${remainingSeconds}秒`;
143 + }
144 + return result;
145 +};
146 +
128 export { 147 export {
129 formatDate, 148 formatDate,
130 wxInfo, 149 wxInfo,
...@@ -134,4 +153,5 @@ export { ...@@ -134,4 +153,5 @@ export {
134 changeURLArg, 153 changeURLArg,
135 getUrlParams, 154 getUrlParams,
136 stringifyQuery, 155 stringifyQuery,
156 + formatDuration,
137 }; 157 };
......
1 +<template>
2 + <div class="bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen pb-20">
3 + <!-- Course List -->
4 + <van-list
5 + v-model:loading="loading"
6 + :finished="finished"
7 + finished-text="没有更多了"
8 + @load="onLoad"
9 + class="px-4 py-3 space-y-4"
10 + >
11 + <FrostedGlass
12 + v-for="record in records"
13 + :key="record.id"
14 + class="p-4 rounded-xl"
15 + >
16 + <div class="flex items-start">
17 + <div
18 + class="w-20 h-20 rounded-lg overflow-hidden flex-shrink-0 mr-3"
19 + >
20 + <van-image
21 + :src="record.course.coverImage"
22 + :alt="record.course.title"
23 + class="w-full h-full"
24 + fit="cover"
25 + error-icon="photo-fail"
26 + loading-icon="photo"
27 + :error-icon-size="32"
28 + :loading-icon-size="32"
29 + />
30 + </div>
31 + <div class="flex-1">
32 + <h3 class="text-base font-medium mb-2 line-clamp-1">
33 + {{ record.course.title }}
34 + </h3>
35 + <div class="flex items-center text-sm text-gray-500 mb-2">
36 + <svg
37 + xmlns="http://www.w3.org/2000/svg"
38 + class="h-4 w-4 mr-1"
39 + fill="none"
40 + viewBox="0 0 24 24"
41 + stroke="currentColor"
42 + >
43 + <path
44 + stroke-linecap="round"
45 + stroke-linejoin="round"
46 + stroke-width="2"
47 + d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
48 + />
49 + </svg>
50 + <span>学习时长:{{ formatDuration(record.duration) }}</span>
51 + </div>
52 + <div class="flex items-center text-sm text-gray-500 mb-3">
53 + <svg
54 + xmlns="http://www.w3.org/2000/svg"
55 + class="h-4 w-4 mr-1"
56 + fill="none"
57 + viewBox="0 0 24 24"
58 + stroke="currentColor"
59 + >
60 + <path
61 + stroke-linecap="round"
62 + stroke-linejoin="round"
63 + stroke-width="2"
64 + 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"
65 + />
66 + </svg>
67 + <span>最近学习:{{ formatDate(record.lastStudyTime) }}</span>
68 + </div>
69 + <div class="flex items-center">
70 + <div class="flex-1">
71 + <van-progress
72 + :percentage="record.progress"
73 + :stroke-width="4"
74 + color="#10B981"
75 + />
76 + </div>
77 + <span class="ml-3 text-sm text-green-600">{{ record.progress }}%</span>
78 + </div>
79 + </div>
80 + </div>
81 + </FrostedGlass>
82 + </van-list>
83 +
84 + <!-- 无数据提示 -->
85 + <div
86 + v-if="!loading && records.length === 0"
87 + class="flex flex-col items-center justify-center py-12"
88 + >
89 + <svg
90 + xmlns="http://www.w3.org/2000/svg"
91 + class="h-16 w-16 text-gray-300"
92 + fill="none"
93 + viewBox="0 0 24 24"
94 + stroke="currentColor"
95 + >
96 + <path
97 + stroke-linecap="round"
98 + stroke-linejoin="round"
99 + stroke-width="2"
100 + 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"
101 + />
102 + </svg>
103 + <p class="mt-4 text-gray-500">暂无学习记录</p>
104 + </div>
105 + </div>
106 +</template>
107 +
108 +<script setup>
109 +import { ref } from 'vue';
110 +import { useRoute } from 'vue-router';
111 +import { useTitle } from '@vueuse/core';
112 +import FrostedGlass from '@/components/ui/FrostedGlass.vue';
113 +import { formatDate, formatDuration } from '@/utils/tools';
114 +
115 +const $route = useRoute();
116 +useTitle($route.meta.title);
117 +
118 +const records = ref([]);
119 +const loading = ref(false);
120 +const finished = ref(false);
121 +const page = ref(1);
122 +const pageSize = 10;
123 +
124 +// 模拟学习记录数据
125 +const mockRecords = [
126 + {
127 + id: 1,
128 + course: {
129 + title: '亲子教育必修课:如何培养孩子的学习兴趣',
130 + coverImage: '/assets/images/course-1.jpg'
131 + },
132 + duration: 3600, // 秒
133 + lastStudyTime: '2024-01-15T10:30:00',
134 + progress: 75
135 + },
136 + {
137 + id: 2,
138 + course: {
139 + title: '儿童心理发展指南:0-6岁关键期教育方法',
140 + coverImage: '/assets/images/course-2.jpg'
141 + },
142 + duration: 7200,
143 + lastStudyTime: '2024-01-14T15:20:00',
144 + progress: 45
145 + }
146 +];
147 +
148 +// 加载数据
149 +const onLoad = () => {
150 + loading.value = true;
151 + // 模拟异步加载
152 + setTimeout(() => {
153 + const start = (page.value - 1) * pageSize;
154 + const end = start + pageSize;
155 + const newRecords = mockRecords.slice(start, end);
156 +
157 + records.value.push(...newRecords);
158 + loading.value = false;
159 +
160 + if (newRecords.length < pageSize) {
161 + finished.value = true;
162 + } else {
163 + page.value += 1;
164 + }
165 + }, 1000);
166 +};
167 +
168 +// 处理图片加载错误
169 +const handleImageError = (e) => {
170 + e.target.onerror = null;
171 + e.target.src = '/assets/images/course-placeholder.jpg';
172 +};
173 +</script>
...@@ -211,11 +211,12 @@ import MenuItem from "@/components/ui/MenuItem.vue"; ...@@ -211,11 +211,12 @@ import MenuItem from "@/components/ui/MenuItem.vue";
211 import { useAuth } from "@/contexts/auth"; 211 import { useAuth } from "@/contexts/auth";
212 import { userProfile, checkInTypes } from "@/utils/mockData"; 212 import { userProfile, checkInTypes } from "@/utils/mockData";
213 import CheckInDialog from "@/components/ui/CheckInDialog.vue"; 213 import CheckInDialog from "@/components/ui/CheckInDialog.vue";
214 +
214 import { useTitle } from "@vueuse/core"; 215 import { useTitle } from "@vueuse/core";
215 const router = useRouter(); 216 const router = useRouter();
216 const $route = useRoute(); 217 const $route = useRoute();
217 -const $router = useRouter();
218 useTitle($route.meta.title); 218 useTitle($route.meta.title);
219 +
219 const { currentUser, logout } = useAuth(); 220 const { currentUser, logout } = useAuth();
220 const profile = ref(userProfile); 221 const profile = ref(userProfile);
221 const showCheckInDialog = ref(false); 222 const showCheckInDialog = ref(false);
......