hookehuyr

feat(学习详情): 添加学习详情页面及相关功能

- 新增学习详情页面,包括视频播放、评论功能
- 在课程列表中添加点击跳转至学习详情的功能
- 更新路由配置以支持学习详情页面
- 在VideoPlayer组件中暴露play方法以支持外部调用
...@@ -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 ]
......
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>