feat(视频播放器): 添加VideoPlayer组件并集成到HomePage
新增VideoPlayer组件以支持视频播放功能,并在HomePage中集成。同时添加了相关依赖video.js和@videojs-player/vue,确保视频播放功能的实现和兼容性。
Showing
5 changed files
with
128 additions
and
20 deletions
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 | } | ... | ... |
src/components/ui/VideoPlayer.vue
0 → 100644
| 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,30 +443,41 @@ | ... | @@ -443,30 +443,41 @@ |
| 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 | - <img | 453 | + <template v-if="activeVideoIndex !== index"> |
| 454 | - :src="item.image" | 454 | + <img |
| 455 | - :alt="item.title" | 455 | + :src="item.image" |
| 456 | - class="w-full h-full object-cover" | 456 | + :alt="item.title" |
| 457 | - @error="handleImageError" | 457 | + class="w-full h-full object-cover" |
| 458 | - /> | 458 | + @error="handleImageError" |
| 459 | - <div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/70 flex flex-col justify-end p-4"> | 459 | + /> |
| 460 | - <h4 class="text-white font-medium mb-1">{{ item.title }}</h4> | 460 | + <div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/70 flex flex-col justify-end p-4"> |
| 461 | - <div class="flex justify-between items-center"> | 461 | + <h4 class="text-white font-medium mb-1">{{ item.title }}</h4> |
| 462 | - <p class="text-white/80 text-xs">{{ item.views }}次播放 · {{ item.duration }}</p> | 462 | + <div class="flex justify-between items-center"> |
| 463 | - <button class="bg-white/20 backdrop-blur-sm p-2 rounded-full"> | 463 | + <p class="text-white/80 text-xs">{{ item.views }}次播放 · {{ item.duration }}</p> |
| 464 | - <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor"> | 464 | + <button |
| 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" /> | 465 | + class="bg-white/20 backdrop-blur-sm p-2 rounded-full" |
| 466 | - </svg> | 466 | + @click="playVideo(index, item.video_url)" |
| 467 | - </button> | 467 | + > |
| 468 | + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor"> | ||
| 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" /> | ||
| 470 | + </svg> | ||
| 471 | + </button> | ||
| 472 | + </div> | ||
| 468 | </div> | 473 | </div> |
| 469 | - </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) | ... | ... |
-
Please register or login to post a comment