hookehuyr

feat(视频播放器): 添加VideoPlayer组件并集成到HomePage

新增VideoPlayer组件以支持视频播放功能,并在HomePage中集成。同时添加了相关依赖video.js和@videojs-player/vue,确保视频播放功能的实现和兼容性。
This diff is collapsed. Click to expand it.
...@@ -17,10 +17,12 @@ ...@@ -17,10 +17,12 @@
17 "dependencies": { 17 "dependencies": {
18 "@heroicons/vue": "^2.2.0", 18 "@heroicons/vue": "^2.2.0",
19 "@vant/use": "^1.6.0", 19 "@vant/use": "^1.6.0",
20 + "@videojs-player/vue": "^1.0.0",
20 "dayjs": "^1.11.13", 21 "dayjs": "^1.11.13",
21 "swiper": "^11.2.6", 22 "swiper": "^11.2.6",
22 "vant": "^4.9.18", 23 "vant": "^4.9.18",
23 "vconsole": "^3.15.1", 24 "vconsole": "^3.15.1",
25 + "video.js": "^7.21.7",
24 "vue": "^3.5.13", 26 "vue": "^3.5.13",
25 "vue-router": "^4.5.0", 27 "vue-router": "^4.5.0",
26 "weixin-js-sdk": "^1.6.5" 28 "weixin-js-sdk": "^1.6.5"
......
...@@ -39,5 +39,6 @@ declare module 'vue' { ...@@ -39,5 +39,6 @@ declare module 'vue' {
39 VanTab: typeof import('vant/es')['Tab'] 39 VanTab: typeof import('vant/es')['Tab']
40 VanTabs: typeof import('vant/es')['Tabs'] 40 VanTabs: typeof import('vant/es')['Tabs']
41 VanUploader: typeof import('vant/es')['Uploader'] 41 VanUploader: typeof import('vant/es')['Uploader']
42 + VideoPlayer: typeof import('./components/ui/VideoPlayer.vue')['default']
42 } 43 }
43 } 44 }
......
1 +<!--
2 + * @Date: 2025-03-24 15:13:35
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-03-24 15:13:37
5 + * @FilePath: /mlaj/src/components/ui/VideoPlayer.vue
6 + * @Description: 文件描述
7 +-->
8 +<template>
9 + <div class="video-player-container">
10 + <video
11 + ref="videoRef"
12 + class="video-js vjs-default-skin"
13 + controls
14 + preload="auto"
15 + width="100%"
16 + height="100%"
17 + >
18 + <source :src="videoUrl" type="video/mp4" />
19 + </video>
20 + </div>
21 +</template>
22 +
23 +<script setup>
24 +import { ref, onMounted, onBeforeUnmount, defineProps, defineEmits } from 'vue'
25 +import videojs from 'video.js'
26 +import 'video.js/dist/video-js.css'
27 +
28 +const props = defineProps({
29 + videoUrl: {
30 + type: String,
31 + required: true
32 + }
33 +})
34 +
35 +const emit = defineEmits(['onPlay', 'onPause'])
36 +const videoRef = ref(null)
37 +let player = null
38 +
39 +onMounted(() => {
40 + player = videojs(videoRef.value, {
41 + fluid: true,
42 + controls: true,
43 + preload: 'auto',
44 + responsive: true
45 + })
46 +
47 + player.on('play', () => {
48 + emit('onPlay')
49 + })
50 +
51 + player.on('pause', () => {
52 + emit('onPause')
53 + })
54 +})
55 +
56 +onBeforeUnmount(() => {
57 + if (player) {
58 + player.dispose()
59 + }
60 +})
61 +
62 +defineExpose({
63 + pause() {
64 + if (player) {
65 + player.pause()
66 + }
67 + }
68 +})
69 +</script>
70 +
71 +<style scoped>
72 +.video-player-container {
73 + width: 100%;
74 + height: 100%;
75 + position: relative;
76 +}
77 +</style>
1 <!-- 1 <!--
2 * @Date: 2025-03-20 19:55:21 2 * @Date: 2025-03-20 19:55:21
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-03-21 09:35:57 4 + * @LastEditTime: 2025-03-24 15:14:00
5 * @FilePath: /mlaj/src/views/HomePage.vue 5 * @FilePath: /mlaj/src/views/HomePage.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -443,13 +443,14 @@ ...@@ -443,13 +443,14 @@
443 <div class="space-y-4"> 443 <div class="space-y-4">
444 <div 444 <div
445 v-for="(item, index) in [ 445 v-for="(item, index) in [
446 - { title: '亲子沟通的艺术', views: '1.2万', duration: '08:25', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-1.jpg' }, 446 + { title: '亲子沟通的艺术', views: '1.2万', duration: '08:25', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-1.jpg', video_url: 'http://vjs.zencdn.net/v/oceans.mp4' },
447 - { title: '如何做好家庭教育', views: '8千', duration: '12:40', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-2.jpg' }, 447 + { title: '如何做好家庭教育', views: '8千', duration: '12:40', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-2.jpg', video_url: 'http://vjs.zencdn.net/v/oceans.mp4' },
448 - { title: '孩子营养餐制作指南', views: '5千', duration: '15:18', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-3.jpg' } 448 + { title: '孩子营养餐制作指南', views: '5千', duration: '15:18', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-3.jpg', video_url: 'http://vjs.zencdn.net/v/oceans.mp4' }
449 ]" 449 ]"
450 :key="index" 450 :key="index"
451 class="relative rounded-xl overflow-hidden shadow-md h-48" 451 class="relative rounded-xl overflow-hidden shadow-md h-48"
452 > 452 >
453 + <template v-if="activeVideoIndex !== index">
453 <img 454 <img
454 :src="item.image" 455 :src="item.image"
455 :alt="item.title" 456 :alt="item.title"
...@@ -460,13 +461,23 @@ ...@@ -460,13 +461,23 @@
460 <h4 class="text-white font-medium mb-1">{{ item.title }}</h4> 461 <h4 class="text-white font-medium mb-1">{{ item.title }}</h4>
461 <div class="flex justify-between items-center"> 462 <div class="flex justify-between items-center">
462 <p class="text-white/80 text-xs">{{ item.views }}次播放 · {{ item.duration }}</p> 463 <p class="text-white/80 text-xs">{{ item.views }}次播放 · {{ item.duration }}</p>
463 - <button class="bg-white/20 backdrop-blur-sm p-2 rounded-full"> 464 + <button
465 + class="bg-white/20 backdrop-blur-sm p-2 rounded-full"
466 + @click="playVideo(index, item.video_url)"
467 + >
464 <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor"> 468 <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
465 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" /> 469 <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
466 </svg> 470 </svg>
467 </button> 471 </button>
468 </div> 472 </div>
469 </div> 473 </div>
474 + </template>
475 + <VideoPlayer
476 + v-else
477 + :video-url="item.video_url"
478 + ref="videoPlayerRefs"
479 + @onPlay="handleVideoPlay(index)"
480 + />
470 </div> 481 </div>
471 </div> 482 </div>
472 </div> 483 </div>
...@@ -486,12 +497,29 @@ import CourseCard from '@/components/ui/CourseCard.vue' ...@@ -486,12 +497,29 @@ import CourseCard from '@/components/ui/CourseCard.vue'
486 import LiveStreamCard from '@/components/ui/LiveStreamCard.vue' 497 import LiveStreamCard from '@/components/ui/LiveStreamCard.vue'
487 import ActivityCard from '@/components/ui/ActivityCard.vue' 498 import ActivityCard from '@/components/ui/ActivityCard.vue'
488 import SummerCampCard from '@/components/ui/SummerCampCard.vue' 499 import SummerCampCard from '@/components/ui/SummerCampCard.vue'
500 +import VideoPlayer from '@/components/ui/VideoPlayer.vue'
489 import { courses, liveStreams, activities, checkInTypes, userRecommendations } from '@/utils/mockData' 501 import { courses, liveStreams, activities, checkInTypes, userRecommendations } from '@/utils/mockData'
490 import { useTitle } from '@vueuse/core' 502 import { useTitle } from '@vueuse/core'
491 import { useAuth } from '@/contexts/auth' 503 import { useAuth } from '@/contexts/auth'
492 import { showToast } from 'vant' 504 import { showToast } from 'vant'
493 import 'vant/lib/toast/style' 505 import 'vant/lib/toast/style'
494 506
507 +const activeVideoIndex = ref(null);
508 +const videoPlayerRefs = ref([]);
509 +
510 +const playVideo = (index, videoUrl) => {
511 + if (activeVideoIndex.value !== null && activeVideoIndex.value !== index) {
512 + videoPlayerRefs.value[activeVideoIndex.value]?.pause();
513 + }
514 + activeVideoIndex.value = index;
515 +};
516 +
517 +const handleVideoPlay = (index) => {
518 + if (activeVideoIndex.value !== null && activeVideoIndex.value !== index) {
519 + videoPlayerRefs.value[activeVideoIndex.value]?.pause();
520 + }
521 +};
522 +
495 const $route = useRoute() 523 const $route = useRoute()
496 const $router = useRouter() 524 const $router = useRouter()
497 useTitle($route.meta.title) 525 useTitle($route.meta.title)
......