hookehuyr

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

新增VideoPlayer组件以支持视频播放功能,并在HomePage中集成。同时添加了相关依赖video.js和@videojs-player/vue,确保视频播放功能的实现和兼容性。
This diff is collapsed. Click to expand it.
......@@ -17,10 +17,12 @@
"dependencies": {
"@heroicons/vue": "^2.2.0",
"@vant/use": "^1.6.0",
"@videojs-player/vue": "^1.0.0",
"dayjs": "^1.11.13",
"swiper": "^11.2.6",
"vant": "^4.9.18",
"vconsole": "^3.15.1",
"video.js": "^7.21.7",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"weixin-js-sdk": "^1.6.5"
......
......@@ -39,5 +39,6 @@ declare module 'vue' {
VanTab: typeof import('vant/es')['Tab']
VanTabs: typeof import('vant/es')['Tabs']
VanUploader: typeof import('vant/es')['Uploader']
VideoPlayer: typeof import('./components/ui/VideoPlayer.vue')['default']
}
}
......
<!--
* @Date: 2025-03-24 15:13:35
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-24 15:13:37
* @FilePath: /mlaj/src/components/ui/VideoPlayer.vue
* @Description: 文件描述
-->
<template>
<div class="video-player-container">
<video
ref="videoRef"
class="video-js vjs-default-skin"
controls
preload="auto"
width="100%"
height="100%"
>
<source :src="videoUrl" type="video/mp4" />
</video>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, defineProps, defineEmits } from 'vue'
import videojs from 'video.js'
import 'video.js/dist/video-js.css'
const props = defineProps({
videoUrl: {
type: String,
required: true
}
})
const emit = defineEmits(['onPlay', 'onPause'])
const videoRef = ref(null)
let player = null
onMounted(() => {
player = videojs(videoRef.value, {
fluid: true,
controls: true,
preload: 'auto',
responsive: true
})
player.on('play', () => {
emit('onPlay')
})
player.on('pause', () => {
emit('onPause')
})
})
onBeforeUnmount(() => {
if (player) {
player.dispose()
}
})
defineExpose({
pause() {
if (player) {
player.pause()
}
}
})
</script>
<style scoped>
.video-player-container {
width: 100%;
height: 100%;
position: relative;
}
</style>
<!--
* @Date: 2025-03-20 19:55:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-21 09:35:57
* @LastEditTime: 2025-03-24 15:14:00
* @FilePath: /mlaj/src/views/HomePage.vue
* @Description: 文件描述
-->
......@@ -443,30 +443,41 @@
<div class="space-y-4">
<div
v-for="(item, index) in [
{ title: '亲子沟通的艺术', views: '1.2万', duration: '08:25', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-1.jpg' },
{ title: '如何做好家庭教育', views: '8千', duration: '12:40', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-2.jpg' },
{ title: '孩子营养餐制作指南', views: '5千', duration: '15:18', image: 'https://cdn.ipadbiz.cn/mlaj/images/video-3.jpg' }
{ 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' },
{ 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' },
{ 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' }
]"
:key="index"
class="relative rounded-xl overflow-hidden shadow-md h-48"
>
<img
:src="item.image"
:alt="item.title"
class="w-full h-full object-cover"
@error="handleImageError"
/>
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/70 flex flex-col justify-end p-4">
<h4 class="text-white font-medium mb-1">{{ item.title }}</h4>
<div class="flex justify-between items-center">
<p class="text-white/80 text-xs">{{ item.views }}次播放 · {{ item.duration }}</p>
<button class="bg-white/20 backdrop-blur-sm p-2 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
<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" />
</svg>
</button>
<template v-if="activeVideoIndex !== index">
<img
:src="item.image"
:alt="item.title"
class="w-full h-full object-cover"
@error="handleImageError"
/>
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/70 flex flex-col justify-end p-4">
<h4 class="text-white font-medium mb-1">{{ item.title }}</h4>
<div class="flex justify-between items-center">
<p class="text-white/80 text-xs">{{ item.views }}次播放 · {{ item.duration }}</p>
<button
class="bg-white/20 backdrop-blur-sm p-2 rounded-full"
@click="playVideo(index, item.video_url)"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor">
<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" />
</svg>
</button>
</div>
</div>
</div>
</template>
<VideoPlayer
v-else
:video-url="item.video_url"
ref="videoPlayerRefs"
@onPlay="handleVideoPlay(index)"
/>
</div>
</div>
</div>
......@@ -486,12 +497,29 @@ import CourseCard from '@/components/ui/CourseCard.vue'
import LiveStreamCard from '@/components/ui/LiveStreamCard.vue'
import ActivityCard from '@/components/ui/ActivityCard.vue'
import SummerCampCard from '@/components/ui/SummerCampCard.vue'
import VideoPlayer from '@/components/ui/VideoPlayer.vue'
import { courses, liveStreams, activities, checkInTypes, userRecommendations } from '@/utils/mockData'
import { useTitle } from '@vueuse/core'
import { useAuth } from '@/contexts/auth'
import { showToast } from 'vant'
import 'vant/lib/toast/style'
const activeVideoIndex = ref(null);
const videoPlayerRefs = ref([]);
const playVideo = (index, videoUrl) => {
if (activeVideoIndex.value !== null && activeVideoIndex.value !== index) {
videoPlayerRefs.value[activeVideoIndex.value]?.pause();
}
activeVideoIndex.value = index;
};
const handleVideoPlay = (index) => {
if (activeVideoIndex.value !== null && activeVideoIndex.value !== index) {
videoPlayerRefs.value[activeVideoIndex.value]?.pause();
}
};
const $route = useRoute()
const $router = useRouter()
useTitle($route.meta.title)
......