feat(study): 新增课程学习页面及相关组件
新增课程学习页面,包含用户信息展示、学习记录和学习统计功能。添加了CourseList组件用于展示课程列表,并在设置页面中添加了跳转至学习页面的入口。同时更新了组件类型声明文件以支持新组件。
Showing
5 changed files
with
260 additions
and
2 deletions
| ... | @@ -15,6 +15,7 @@ declare module 'vue' { | ... | @@ -15,6 +15,7 @@ declare module 'vue' { |
| 15 | CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default'] | 15 | CheckInDialog: typeof import('./components/ui/CheckInDialog.vue')['default'] |
| 16 | ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default'] | 16 | ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default'] |
| 17 | CourseCard: typeof import('./components/ui/CourseCard.vue')['default'] | 17 | CourseCard: typeof import('./components/ui/CourseCard.vue')['default'] |
| 18 | + CourseList: typeof import('./components/courses/CourseList.vue')['default'] | ||
| 18 | FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default'] | 19 | FrostedGlass: typeof import('./components/ui/FrostedGlass.vue')['default'] |
| 19 | GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] | 20 | GradientHeader: typeof import('./components/ui/GradientHeader.vue')['default'] |
| 20 | LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default'] | 21 | LiveStreamCard: typeof import('./components/ui/LiveStreamCard.vue')['default'] |
| ... | @@ -27,8 +28,10 @@ declare module 'vue' { | ... | @@ -27,8 +28,10 @@ declare module 'vue' { |
| 27 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] | 28 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] |
| 28 | UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] | 29 | UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] |
| 29 | VanButton: typeof import('vant/es')['Button'] | 30 | VanButton: typeof import('vant/es')['Button'] |
| 31 | + VanCell: typeof import('vant/es')['Cell'] | ||
| 30 | VanCellGroup: typeof import('vant/es')['CellGroup'] | 32 | VanCellGroup: typeof import('vant/es')['CellGroup'] |
| 31 | VanCheckbox: typeof import('vant/es')['Checkbox'] | 33 | VanCheckbox: typeof import('vant/es')['Checkbox'] |
| 34 | + VanCol: typeof import('vant/es')['Col'] | ||
| 32 | VanDatePicker: typeof import('vant/es')['DatePicker'] | 35 | VanDatePicker: typeof import('vant/es')['DatePicker'] |
| 33 | VanField: typeof import('vant/es')['Field'] | 36 | VanField: typeof import('vant/es')['Field'] |
| 34 | VanForm: typeof import('vant/es')['Form'] | 37 | VanForm: typeof import('vant/es')['Form'] |
| ... | @@ -40,6 +43,7 @@ declare module 'vue' { | ... | @@ -40,6 +43,7 @@ declare module 'vue' { |
| 40 | VanPopup: typeof import('vant/es')['Popup'] | 43 | VanPopup: typeof import('vant/es')['Popup'] |
| 41 | VanProgress: typeof import('vant/es')['Progress'] | 44 | VanProgress: typeof import('vant/es')['Progress'] |
| 42 | VanRate: typeof import('vant/es')['Rate'] | 45 | VanRate: typeof import('vant/es')['Rate'] |
| 46 | + VanRow: typeof import('vant/es')['Row'] | ||
| 43 | VanTab: typeof import('vant/es')['Tab'] | 47 | VanTab: typeof import('vant/es')['Tab'] |
| 44 | VanTabs: typeof import('vant/es')['Tabs'] | 48 | VanTabs: typeof import('vant/es')['Tabs'] |
| 45 | VanToast: typeof import('vant/es')['Toast'] | 49 | VanToast: typeof import('vant/es')['Toast'] | ... | ... |
src/components/courses/CourseList.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-04-07 | ||
| 3 | + * @Description: 课程列表组件 | ||
| 4 | +--> | ||
| 5 | +<template> | ||
| 6 | + <div class="course-list"> | ||
| 7 | + <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> | ||
| 8 | + <div v-for="course in courses" :key="course.id" | ||
| 9 | + class="course-item bg-white mb-3 rounded-lg overflow-hidden"> | ||
| 10 | + <van-cell :title="course.title" :label="course.type" class="pb-2"> | ||
| 11 | + <template #icon> | ||
| 12 | + <van-image :src="course.thumbnail" width="120" height="68" fit="cover" class="mr-3 rounded" /> | ||
| 13 | + </template> | ||
| 14 | + </van-cell> | ||
| 15 | + <div class="px-4 pb-4"> | ||
| 16 | + <div class="flex justify-between items-center text-sm text-gray-600 mb-2"> | ||
| 17 | + <span>已学习 {{ formatDuration(course.studyTime) }}</span> | ||
| 18 | + <span>{{ course.progress }}%</span> | ||
| 19 | + </div> | ||
| 20 | + <van-progress :percentage="course.progress" :stroke-width="4" color="#4080ff" /> | ||
| 21 | + </div> | ||
| 22 | + </div> | ||
| 23 | + </van-list> | ||
| 24 | + </div> | ||
| 25 | +</template> | ||
| 26 | + | ||
| 27 | +<script setup> | ||
| 28 | +import { ref } from 'vue' | ||
| 29 | + | ||
| 30 | +// 接收课程列表数据 | ||
| 31 | +const props = defineProps({ | ||
| 32 | + courses: { | ||
| 33 | + type: Array, | ||
| 34 | + default: () => [] | ||
| 35 | + } | ||
| 36 | +}) | ||
| 37 | + | ||
| 38 | +// 列表加载状态 | ||
| 39 | +const loading = ref(false) | ||
| 40 | +const finished = ref(true) // 由于使用的是传入的静态数据,所以直接设置为完成状态 | ||
| 41 | + | ||
| 42 | +// 加载更多 | ||
| 43 | +const onLoad = () => { | ||
| 44 | + loading.value = false | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +// 格式化时长显示 | ||
| 48 | +const formatDuration = (seconds) => { | ||
| 49 | + const hours = Math.floor(seconds / 3600) | ||
| 50 | + const minutes = Math.floor((seconds % 3600) / 60) | ||
| 51 | + if (hours > 0) { | ||
| 52 | + return `${hours}小时${minutes}分钟` | ||
| 53 | + } | ||
| 54 | + return `${minutes}分钟` | ||
| 55 | +} | ||
| 56 | +</script> | ||
| 57 | + | ||
| 58 | +<style scoped> | ||
| 59 | +.course-list { | ||
| 60 | + padding: 0.5rem; | ||
| 61 | +} | ||
| 62 | + | ||
| 63 | +.course-item { | ||
| 64 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); | ||
| 65 | +} | ||
| 66 | +</style> |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-03-20 20:36:36 | 2 | * @Date: 2025-03-20 20:36:36 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-03-28 09:39:27 | 4 | + * @LastEditTime: 2025-04-08 12:45:20 |
| 5 | * @FilePath: /mlaj/src/router/routes.js | 5 | * @FilePath: /mlaj/src/router/routes.js |
| 6 | * @Description: 路由地址映射配置 | 6 | * @Description: 路由地址映射配置 |
| 7 | */ | 7 | */ |
| ... | @@ -198,5 +198,12 @@ export const routes = [ | ... | @@ -198,5 +198,12 @@ export const routes = [ |
| 198 | title: '微信授权页面', | 198 | title: '微信授权页面', |
| 199 | } | 199 | } |
| 200 | }, | 200 | }, |
| 201 | + { | ||
| 202 | + path: '/study', | ||
| 203 | + component: () => import('@/views/study/studyPage.vue'), | ||
| 204 | + meta: { | ||
| 205 | + title: '学习页面', | ||
| 206 | + } | ||
| 207 | + }, | ||
| 201 | ...checkinRoutes, | 208 | ...checkinRoutes, |
| 202 | ] | 209 | ] | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-03-24 13:04:21 | 2 | * @Date: 2025-03-24 13:04:21 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-03-26 13:09:59 | 4 | + * @LastEditTime: 2025-04-08 11:21:01 |
| 5 | * @FilePath: /mlaj/src/views/profile/SettingsPage.vue | 5 | * @FilePath: /mlaj/src/views/profile/SettingsPage.vue |
| 6 | * @Description: 用户设置页面 | 6 | * @Description: 用户设置页面 |
| 7 | --> | 7 | --> |
| ... | @@ -75,6 +75,17 @@ | ... | @@ -75,6 +75,17 @@ |
| 75 | <ChevronRightIcon class="w-5 h-5 text-gray-400" /> | 75 | <ChevronRightIcon class="w-5 h-5 text-gray-400" /> |
| 76 | </div> | 76 | </div> |
| 77 | </div> | 77 | </div> |
| 78 | + | ||
| 79 | + <!-- 课程学习 --> | ||
| 80 | + <div class="p-4" @click="router.push('/study')"> | ||
| 81 | + <div class="flex items-center justify-between"> | ||
| 82 | + <div> | ||
| 83 | + <h3 class="text-base font-medium text-gray-900">课程学习</h3> | ||
| 84 | + <p class="text-sm text-gray-500">课程学习</p> | ||
| 85 | + </div> | ||
| 86 | + <ChevronRightIcon class="w-5 h-5 text-gray-400" /> | ||
| 87 | + </div> | ||
| 88 | + </div> | ||
| 78 | </div> | 89 | </div> |
| 79 | </FrostedGlass> | 90 | </FrostedGlass> |
| 80 | </div> | 91 | </div> | ... | ... |
src/views/study/StudyPage.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-04-07 | ||
| 3 | + * @Description: 课程学习页面 | ||
| 4 | +--> | ||
| 5 | +<template> | ||
| 6 | + <div class="study-page"> | ||
| 7 | + <!-- 用户信息区域 --> | ||
| 8 | + <div class="user-info bg-green-50/70 p-4 flex flex-col"> | ||
| 9 | + <div class="flex items-center mb-3"> | ||
| 10 | + <div class="flex-shrink-0"> | ||
| 11 | + <van-image round width="3rem" height="3rem" :src="userInfo.avatar" :alt="userInfo.name" /> | ||
| 12 | + </div> | ||
| 13 | + <div class="text-lg text-black font-bold" style="margin-left: 1rem"> | ||
| 14 | + {{ userInfo.name }} | ||
| 15 | + </div> | ||
| 16 | + </div> | ||
| 17 | + <div class="text-sm text-gray-500 mb-2"> | ||
| 18 | + 本周第 <span class="text-black font-bold">{{ userInfo.weeklyRank }}</span> 名 | ||
| 19 | + </div> | ||
| 20 | + <div class="text-sm text-gray-500"> | ||
| 21 | + 已加入 <span class="text-black font-bold">{{ userInfo.joinDays }}</span> 天 | ||
| 22 | + <span class="mx-2">·</span> | ||
| 23 | + 连续学习 | ||
| 24 | + <span class="text-black font-bold">{{ userInfo.continuousDays }}</span> 天 | ||
| 25 | + </div> | ||
| 26 | + </div> | ||
| 27 | + | ||
| 28 | + <!-- 学习记录和统计标签页 --> | ||
| 29 | + <van-tabs v-model:active="activeTab" sticky shrink> | ||
| 30 | + <!-- 学习记录标签 --> | ||
| 31 | + <van-tab title="学习记录" name="record"> | ||
| 32 | + <div class="p-4"> | ||
| 33 | + <div class="flex space-x-4 mb-4"> | ||
| 34 | + <button | ||
| 35 | + class="rounded-full px-6 py-2 text-sm transition-colors" | ||
| 36 | + :class="courseType === 'column' ? 'bg-green-500 text-white' : 'bg-gray-100 text-gray-600'" | ||
| 37 | + @click="courseType = 'column'" | ||
| 38 | + > | ||
| 39 | + 专栏 | ||
| 40 | + </button> | ||
| 41 | + <button | ||
| 42 | + class="rounded-full px-6 py-2 text-sm transition-colors" | ||
| 43 | + :class="courseType === 'single' ? 'bg-green-500 text-white' : 'bg-gray-100 text-gray-600'" | ||
| 44 | + @click="courseType = 'single'" | ||
| 45 | + > | ||
| 46 | + 单课 | ||
| 47 | + </button> | ||
| 48 | + </div> | ||
| 49 | + <course-list v-if="courseType === 'column'" :courses="columnCourses" /> | ||
| 50 | + <course-list v-else :courses="singleCourses" /> | ||
| 51 | + </div> | ||
| 52 | + </van-tab> | ||
| 53 | + | ||
| 54 | + <!-- 学习统计标签 --> | ||
| 55 | + <van-tab title="学习统计" name="stats"> | ||
| 56 | + <div class="p-4"> | ||
| 57 | + <van-cell-group inset> | ||
| 58 | + <van-cell title="总学习时长" :value="formatDuration(stats.totalDuration)" /> | ||
| 59 | + <van-cell title="已完成课程" :value="`${stats.completedCourses}门`" /> | ||
| 60 | + <van-cell title="学习天数" :value="`${stats.studyDays}天`" /> | ||
| 61 | + <van-cell title="平均每日学习" :value="formatDuration(stats.avgDailyDuration)" /> | ||
| 62 | + </van-cell-group> | ||
| 63 | + </div> | ||
| 64 | + </van-tab> | ||
| 65 | + </van-tabs> | ||
| 66 | + </div> | ||
| 67 | +</template> | ||
| 68 | + | ||
| 69 | +<script setup> | ||
| 70 | +import { ref } from "vue"; | ||
| 71 | +import { useTitle } from "@vueuse/core"; | ||
| 72 | +import CourseList from "@/components/courses/CourseList.vue"; | ||
| 73 | + | ||
| 74 | +// 设置页面标题 | ||
| 75 | +useTitle("课程学习"); | ||
| 76 | + | ||
| 77 | +// 用户信息 | ||
| 78 | +const userInfo = ref({ | ||
| 79 | + avatar: "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg", | ||
| 80 | + name: "王鸿军", | ||
| 81 | + weeklyRank: 11, | ||
| 82 | + joinDays: 448, | ||
| 83 | + continuousDays: 1, | ||
| 84 | +}); | ||
| 85 | + | ||
| 86 | +// 标签页状态 | ||
| 87 | +const activeTab = ref("record"); | ||
| 88 | +const courseType = ref("column"); | ||
| 89 | + | ||
| 90 | +// 课程列表数据 | ||
| 91 | +const columnCourses = ref([ | ||
| 92 | + { | ||
| 93 | + id: 1, | ||
| 94 | + title: "考前赋能冥想", | ||
| 95 | + type: "视频", | ||
| 96 | + thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg", | ||
| 97 | + progress: 100, | ||
| 98 | + duration: 1200, // 20分钟 | ||
| 99 | + studyTime: 1200, | ||
| 100 | + }, | ||
| 101 | + { | ||
| 102 | + id: 2, | ||
| 103 | + title: "开学礼·让的智慧·心法老师·20241001(上)", | ||
| 104 | + type: "视频", | ||
| 105 | + thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg", | ||
| 106 | + progress: 1, | ||
| 107 | + duration: 3600, // 1小时 | ||
| 108 | + studyTime: 10, | ||
| 109 | + }, | ||
| 110 | + { | ||
| 111 | + id: 1, | ||
| 112 | + title: "考前赋能冥想", | ||
| 113 | + type: "视频", | ||
| 114 | + thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg", | ||
| 115 | + progress: 100, | ||
| 116 | + duration: 1200, // 20分钟 | ||
| 117 | + studyTime: 1200, | ||
| 118 | + }, | ||
| 119 | + { | ||
| 120 | + id: 2, | ||
| 121 | + title: "开学礼·让的智慧·心法老师·20241001(上)", | ||
| 122 | + type: "视频", | ||
| 123 | + thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg", | ||
| 124 | + progress: 1, | ||
| 125 | + duration: 3600, // 1小时 | ||
| 126 | + studyTime: 10, | ||
| 127 | + }, | ||
| 128 | +]); | ||
| 129 | + | ||
| 130 | +const singleCourses = ref([ | ||
| 131 | + { | ||
| 132 | + id: 3, | ||
| 133 | + title: "冬季课·影响孩子命运的家族六要素·心法老师20250207", | ||
| 134 | + type: "视频", | ||
| 135 | + thumbnail: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg", | ||
| 136 | + progress: 1, | ||
| 137 | + duration: 3600, | ||
| 138 | + studyTime: 1, | ||
| 139 | + }, | ||
| 140 | +]); | ||
| 141 | + | ||
| 142 | +// 学习统计数据 | ||
| 143 | +const stats = ref({ | ||
| 144 | + totalDuration: 7200, // 2小时 | ||
| 145 | + completedCourses: 1, | ||
| 146 | + studyDays: 30, | ||
| 147 | + avgDailyDuration: 1800, // 30分钟 | ||
| 148 | +}); | ||
| 149 | + | ||
| 150 | +// 格式化时长显示 | ||
| 151 | +const formatDuration = (seconds) => { | ||
| 152 | + const hours = Math.floor(seconds / 3600); | ||
| 153 | + const minutes = Math.floor((seconds % 3600) / 60); | ||
| 154 | + if (hours > 0) { | ||
| 155 | + return `${hours}小时${minutes}分钟`; | ||
| 156 | + } | ||
| 157 | + return `${minutes}分钟`; | ||
| 158 | +}; | ||
| 159 | +</script> | ||
| 160 | + | ||
| 161 | +<style scoped> | ||
| 162 | +.study-page { | ||
| 163 | + min-height: 100vh; | ||
| 164 | + background-color: #fff; | ||
| 165 | +} | ||
| 166 | + | ||
| 167 | +.bg-primary { | ||
| 168 | + background-color: #4080ff; | ||
| 169 | +} | ||
| 170 | +</style> |
-
Please register or login to post a comment