feat(学习详情): 添加学习详情页面及相关功能
- 新增学习详情页面,包括视频播放、评论功能 - 在课程列表中添加点击跳转至学习详情的功能 - 更新路由配置以支持学习详情页面 - 在VideoPlayer组件中暴露play方法以支持外部调用
Showing
5 changed files
with
182 additions
and
5 deletions
| ... | @@ -31,8 +31,8 @@ declare module 'vue' { | ... | @@ -31,8 +31,8 @@ declare module 'vue' { |
| 31 | VanCell: typeof import('vant/es')['Cell'] | 31 | VanCell: typeof import('vant/es')['Cell'] |
| 32 | VanCellGroup: typeof import('vant/es')['CellGroup'] | 32 | VanCellGroup: typeof import('vant/es')['CellGroup'] |
| 33 | VanCheckbox: typeof import('vant/es')['Checkbox'] | 33 | VanCheckbox: typeof import('vant/es')['Checkbox'] |
| 34 | - VanCol: typeof import('vant/es')['Col'] | ||
| 35 | VanDatePicker: typeof import('vant/es')['DatePicker'] | 34 | VanDatePicker: typeof import('vant/es')['DatePicker'] |
| 35 | + VanEmpty: typeof import('vant/es')['Empty'] | ||
| 36 | VanField: typeof import('vant/es')['Field'] | 36 | VanField: typeof import('vant/es')['Field'] |
| 37 | VanForm: typeof import('vant/es')['Form'] | 37 | VanForm: typeof import('vant/es')['Form'] |
| 38 | VanIcon: typeof import('vant/es')['Icon'] | 38 | VanIcon: typeof import('vant/es')['Icon'] |
| ... | @@ -43,7 +43,6 @@ declare module 'vue' { | ... | @@ -43,7 +43,6 @@ declare module 'vue' { |
| 43 | VanPopup: typeof import('vant/es')['Popup'] | 43 | VanPopup: typeof import('vant/es')['Popup'] |
| 44 | VanProgress: typeof import('vant/es')['Progress'] | 44 | VanProgress: typeof import('vant/es')['Progress'] |
| 45 | VanRate: typeof import('vant/es')['Rate'] | 45 | VanRate: typeof import('vant/es')['Rate'] |
| 46 | - VanRow: typeof import('vant/es')['Row'] | ||
| 47 | VanTab: typeof import('vant/es')['Tab'] | 46 | VanTab: typeof import('vant/es')['Tab'] |
| 48 | VanTabs: typeof import('vant/es')['Tabs'] | 47 | VanTabs: typeof import('vant/es')['Tabs'] |
| 49 | VanToast: typeof import('vant/es')['Toast'] | 48 | VanToast: typeof import('vant/es')['Toast'] | ... | ... |
| ... | @@ -6,7 +6,7 @@ | ... | @@ -6,7 +6,7 @@ |
| 6 | <div class="course-list"> | 6 | <div class="course-list"> |
| 7 | <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> | 7 | <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> |
| 8 | <div v-for="course in courses" :key="course.id" | 8 | <div v-for="course in courses" :key="course.id" |
| 9 | - class="course-item bg-white p-3 rounded-lg overflow-hidden flex"> | 9 | + class="course-item bg-white p-3 rounded-lg overflow-hidden flex cursor-pointer" @click="router.push(`/studyDetail/${course.id}`)"> |
| 10 | <div class="relative w-[120px] h-full flex-shrink-0"> | 10 | <div class="relative w-[120px] h-full flex-shrink-0"> |
| 11 | <van-image :src="course.thumbnail" width="120" height="100%" fit="cover" class="item-cover"> | 11 | <van-image :src="course.thumbnail" width="120" height="100%" fit="cover" class="item-cover"> |
| 12 | <div class="absolute bg-white/80 px-2 py-0.5 text-xs rounded" | 12 | <div class="absolute bg-white/80 px-2 py-0.5 text-xs rounded" |
| ... | @@ -37,6 +37,9 @@ | ... | @@ -37,6 +37,9 @@ |
| 37 | 37 | ||
| 38 | <script setup> | 38 | <script setup> |
| 39 | import { ref } from "vue"; | 39 | import { ref } from "vue"; |
| 40 | +import { useRouter } from "vue-router"; | ||
| 41 | + | ||
| 42 | +const router = useRouter(); | ||
| 40 | 43 | ||
| 41 | // 接收课程列表数据 | 44 | // 接收课程列表数据 |
| 42 | const props = defineProps({ | 45 | const props = defineProps({ | ... | ... |
| ... | @@ -82,7 +82,7 @@ const handleMounted = (payload) => { | ... | @@ -82,7 +82,7 @@ const handleMounted = (payload) => { |
| 82 | // 监听视频播放状态 | 82 | // 监听视频播放状态 |
| 83 | player.value.on('play', () => { | 83 | player.value.on('play', () => { |
| 84 | // 播放时隐藏controls | 84 | // 播放时隐藏controls |
| 85 | - player.value.controlBar.hide(); | 85 | + // player.value.controlBar.hide(); |
| 86 | }); | 86 | }); |
| 87 | player.value.on('pause', () => { | 87 | player.value.on('pause', () => { |
| 88 | 88 | ||
| ... | @@ -122,6 +122,11 @@ defineExpose({ | ... | @@ -122,6 +122,11 @@ defineExpose({ |
| 122 | player.value.pause(); | 122 | player.value.pause(); |
| 123 | } | 123 | } |
| 124 | }, | 124 | }, |
| 125 | + play() { | ||
| 126 | + if (player.value) { | ||
| 127 | + player.value.play(); | ||
| 128 | + } | ||
| 129 | + }, | ||
| 125 | }); | 130 | }); |
| 126 | </script> | 131 | </script> |
| 127 | 132 | ... | ... |
| 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-04-08 12:45:20 | 4 | + * @LastEditTime: 2025-04-08 13:39:00 |
| 5 | * @FilePath: /mlaj/src/router/routes.js | 5 | * @FilePath: /mlaj/src/router/routes.js |
| 6 | * @Description: 路由地址映射配置 | 6 | * @Description: 路由地址映射配置 |
| 7 | */ | 7 | */ |
| ... | @@ -205,5 +205,12 @@ export const routes = [ | ... | @@ -205,5 +205,12 @@ export const routes = [ |
| 205 | title: '学习页面', | 205 | title: '学习页面', |
| 206 | } | 206 | } |
| 207 | }, | 207 | }, |
| 208 | + { | ||
| 209 | + path: '/studyDetail/:id', | ||
| 210 | + component: () => import('@/views/study/studyDetailPage.vue'), | ||
| 211 | + meta: { | ||
| 212 | + title: '学习详情页面', | ||
| 213 | + } | ||
| 214 | + }, | ||
| 208 | ...checkinRoutes, | 215 | ...checkinRoutes, |
| 209 | ] | 216 | ] | ... | ... |
src/views/study/StudyDetailPage.vue
0 → 100644
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-04-07 | ||
| 3 | + * @Description: 学习详情页面 | ||
| 4 | +--> | ||
| 5 | +<template> | ||
| 6 | + <div class="study-detail-page bg-gradient-to-b from-green-50/70 to-white/90 min-h-screen pb-20"> | ||
| 7 | + <div v-if="course" class="flex flex-col"> | ||
| 8 | + <!-- 视频播放区域 --> | ||
| 9 | + <div class="w-full bg-black relative"> | ||
| 10 | + <!-- 视频封面和播放按钮 --> | ||
| 11 | + <div v-if="!isPlaying" class="relative w-full" style="aspect-ratio: 16/9;"> | ||
| 12 | + <img :src="course.cover" :alt="course.title" class="w-full h-full object-cover" /> | ||
| 13 | + <div class="absolute inset-0 flex items-center justify-center cursor-pointer" @click="startPlay"> | ||
| 14 | + <div | ||
| 15 | + class="w-24 h-24 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors"> | ||
| 16 | + <font-awesome-icon icon="circle-play" class="text-5xl text-white" style="font-size: 3rem;" /> | ||
| 17 | + </div> | ||
| 18 | + </div> | ||
| 19 | + </div> | ||
| 20 | + <!-- 视频播放器 --> | ||
| 21 | + <VideoPlayer v-show="isPlaying" ref="videoPlayerRef" :video-url="course.videoUrl" :autoplay="false" | ||
| 22 | + @onPlay="handleVideoPlay" @onPause="handleVideoPause" /> | ||
| 23 | + </div> | ||
| 24 | + | ||
| 25 | + <!-- 标签页区域 --> | ||
| 26 | + <div class="px-4 py-3"> | ||
| 27 | + <van-tabs v-model:active="activeTab" sticky animated swipeable> | ||
| 28 | + <!-- 介绍标签页 --> | ||
| 29 | + <van-tab title="介绍" name="intro"> | ||
| 30 | + <div class="py-4"> | ||
| 31 | + <h1 class="text-xl font-bold mb-2">{{ course.title }}</h1> | ||
| 32 | + <div class="text-gray-500 text-sm"> | ||
| 33 | + {{ course.studyCount || 0 }}次学习 | ||
| 34 | + </div> | ||
| 35 | + </div> | ||
| 36 | + </van-tab> | ||
| 37 | + | ||
| 38 | + <!-- 评论标签页 --> | ||
| 39 | + <van-tab title="评论" :title-style="{ 'min-width': '50%' }" name="comments"> | ||
| 40 | + <div class="py-4 space-y-4"> | ||
| 41 | + <div v-for="comment in comments" :key="comment.id" | ||
| 42 | + class="bg-white rounded-lg p-4 shadow-sm"> | ||
| 43 | + <div class="flex items-center mb-2"> | ||
| 44 | + <img :src="comment.avatar" class="w-8 h-8 rounded-full mr-2" /> | ||
| 45 | + <div> | ||
| 46 | + <div class="font-medium">{{ comment.username }}</div> | ||
| 47 | + <div class="text-gray-500 text-xs">{{ comment.time }}</div> | ||
| 48 | + </div> | ||
| 49 | + </div> | ||
| 50 | + <div class="text-gray-700">{{ comment.content }}</div> | ||
| 51 | + </div> | ||
| 52 | + </div> | ||
| 53 | + </van-tab> | ||
| 54 | + </van-tabs> | ||
| 55 | + </div> | ||
| 56 | + | ||
| 57 | + <!-- 底部操作栏 --> | ||
| 58 | + <div class="fixed bottom-0 left-0 right-0 bg-white border-t px-4 py-2 flex items-center space-x-4"> | ||
| 59 | + <van-button icon="bars" class="flex-none" @click="showCatalog = true">课程目录</van-button> | ||
| 60 | + <div class="flex-grow"> | ||
| 61 | + <van-field v-model="newComment" placeholder="说点什么吧~" class="bg-gray-100 rounded-full" /> | ||
| 62 | + </div> | ||
| 63 | + <van-button type="primary" size="small" @click="submitComment">发送</van-button> | ||
| 64 | + </div> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 67 | +</template> | ||
| 68 | + | ||
| 69 | +<script setup> | ||
| 70 | +import { ref, onMounted, nextTick } from 'vue'; | ||
| 71 | +import { useRoute } from 'vue-router'; | ||
| 72 | +import { useTitle } from '@vueuse/core'; | ||
| 73 | +import VideoPlayer from '@/components/ui/VideoPlayer.vue'; | ||
| 74 | + | ||
| 75 | +const route = useRoute(); | ||
| 76 | +const course = ref(null); | ||
| 77 | +const activeTab = ref('intro'); | ||
| 78 | +const newComment = ref(''); | ||
| 79 | +const showCatalog = ref(false); | ||
| 80 | +const isPlaying = ref(false); | ||
| 81 | +const videoPlayerRef = ref(null); | ||
| 82 | + | ||
| 83 | +// 开始播放视频 | ||
| 84 | +const startPlay = async () => { | ||
| 85 | + isPlaying.value = true; | ||
| 86 | + await nextTick(); | ||
| 87 | + if (videoPlayerRef.value) { | ||
| 88 | + videoPlayerRef.value.play(); | ||
| 89 | + } | ||
| 90 | +}; | ||
| 91 | + | ||
| 92 | +// 处理视频播放状态 | ||
| 93 | +const handleVideoPlay = () => { | ||
| 94 | + isPlaying.value = true; | ||
| 95 | +}; | ||
| 96 | + | ||
| 97 | +const handleVideoPause = () => { | ||
| 98 | + // 保持视频播放器可见,只在初始状态显示封面 | ||
| 99 | +}; | ||
| 100 | + | ||
| 101 | +// 评论列表 | ||
| 102 | +const comments = ref([ | ||
| 103 | + { | ||
| 104 | + id: 1, | ||
| 105 | + username: '欢乐马', | ||
| 106 | + avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg', | ||
| 107 | + content: '教育的顶级传承,是你用什么样的心,传承智慧', | ||
| 108 | + time: '2024-12-04 18:51' | ||
| 109 | + }, | ||
| 110 | + { | ||
| 111 | + id: 2, | ||
| 112 | + username: '欢乐马', | ||
| 113 | + avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg', | ||
| 114 | + content: '不要用战术上的勤奋,掩盖战略上的懒惰', | ||
| 115 | + time: '2024-12-04 08:01' | ||
| 116 | + }, | ||
| 117 | + { | ||
| 118 | + id: 3, | ||
| 119 | + username: '欢乐马', | ||
| 120 | + avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg', | ||
| 121 | + content: '和老师积极互动,整个课堂像为你而讲', | ||
| 122 | + time: '2024-12-04 07:54' | ||
| 123 | + } | ||
| 124 | +]); | ||
| 125 | + | ||
| 126 | +// 设置页面标题 | ||
| 127 | +useTitle('学习详情'); | ||
| 128 | + | ||
| 129 | +// 获取课程详情 | ||
| 130 | + | ||
| 131 | +onMounted(() => { | ||
| 132 | + const courseId = route.params.id; | ||
| 133 | + if (courseId) { | ||
| 134 | + // TODO: 这里需要替换为实际的API调用 | ||
| 135 | + // 临时使用模拟数据 | ||
| 136 | + course.value = { | ||
| 137 | + id: courseId, | ||
| 138 | + title: '开学礼·止的智慧·心法老师·20241001(上)', | ||
| 139 | + videoUrl: 'https://sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4', | ||
| 140 | + progress: 35, | ||
| 141 | + studyTime: 3600, | ||
| 142 | + type: '视频课程', | ||
| 143 | + studyCount: 1896, | ||
| 144 | + cover: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg' | ||
| 145 | + }; | ||
| 146 | + } | ||
| 147 | +}) | ||
| 148 | + | ||
| 149 | +// 提交评论 | ||
| 150 | +const submitComment = () => { | ||
| 151 | + if (!newComment.value.trim()) return; | ||
| 152 | + | ||
| 153 | + comments.value.unshift({ | ||
| 154 | + id: Date.now(), | ||
| 155 | + username: '当前用户', | ||
| 156 | + avatar: 'https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg', | ||
| 157 | + content: newComment.value, | ||
| 158 | + time: new Date().toLocaleString() | ||
| 159 | + }); | ||
| 160 | + | ||
| 161 | + newComment.value = ''; | ||
| 162 | +}; | ||
| 163 | +</script> |
-
Please register or login to post a comment