372 lines
8.5 KiB
Vue
372 lines
8.5 KiB
Vue
<template>
|
||
<div class="carousel-webrtc-player">
|
||
<div class="video-container">
|
||
<!-- WebRTC视频播放器 -->
|
||
<CustomWebRTCPlayer
|
||
v-if="currentStream"
|
||
:streamUrl="currentStream"
|
||
/>
|
||
<EmptyState
|
||
v-else
|
||
subtitle="暂无信息"
|
||
:iconSrc="empty"
|
||
class="video-empty-state"
|
||
/>
|
||
|
||
<!-- 左右箭头 -->
|
||
<div class="carousel-arrows" v-if="hasValidViews && validViews.length > 1">
|
||
<div class="arrow left-arrow" @click="prevView">
|
||
<i class="arrow-icon left"></i>
|
||
</div>
|
||
<div class="arrow right-arrow" @click="nextView">
|
||
<i class="arrow-icon right"></i>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 视角指示器 -->
|
||
<div class="view-indicators" v-if="hasValidViews && validViews.length > 1">
|
||
<div
|
||
v-for="(view, index) in validViews"
|
||
:key="index"
|
||
class="indicator"
|
||
:class="{ active: currentViewIndex === validViewsIndices[index] }"
|
||
@click="setView(validViewsIndices[index])"
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted, onUnmounted, watch, computed } from 'vue';
|
||
import CustomWebRTCPlayer from './CustomWebRTCPlayer.vue';
|
||
import EmptyState from './EmptyState.vue';
|
||
import empty from '../../assets/img/empty.png';
|
||
|
||
const props = defineProps({
|
||
// 视角列表,格式为 [{ name: '视角1', streamUrl: 'url1' }, { name: '视角2', streamUrl: 'url2' }]
|
||
views: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
// 轮播间隔,单位毫秒
|
||
interval: {
|
||
type: Number,
|
||
default: 10000
|
||
},
|
||
// 是否自动轮播
|
||
autoplay: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// v-model绑定值,表示当前选中的视角名称
|
||
modelValue: {
|
||
type: String,
|
||
default: ''
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['update:modelValue']);
|
||
|
||
// 当前视角索引
|
||
const currentViewIndex = ref(0);
|
||
|
||
// 筛选出有效的视角(有streamUrl的视角)
|
||
const validViews = computed(() => {
|
||
return props.views.filter(view => view.streamUrl);
|
||
});
|
||
|
||
// 有效视角的索引映射
|
||
const validViewsIndices = computed(() => {
|
||
return props.views.map((view, index) => view.streamUrl ? index : -1).filter(index => index !== -1);
|
||
});
|
||
|
||
// 是否有有效的视角
|
||
const hasValidViews = computed(() => {
|
||
return validViews.value.length > 0;
|
||
});
|
||
|
||
// 计算当前视角的流URL
|
||
const currentStream = computed(() => {
|
||
if (props.views.length === 0) return '';
|
||
return props.views[currentViewIndex.value]?.streamUrl || '';
|
||
});
|
||
|
||
// 视角名称列表
|
||
const viewList = computed(() => {
|
||
return props.views.map(view => view.name);
|
||
});
|
||
|
||
// 根据modelValue设置当前视角索引
|
||
watch(() => props.modelValue, (newValue) => {
|
||
if (newValue && props.views.length > 0) {
|
||
const index = props.views.findIndex(view => view.name === newValue);
|
||
if (index !== -1) {
|
||
currentViewIndex.value = index;
|
||
}
|
||
}
|
||
}, { immediate: true });
|
||
|
||
// 当currentViewIndex变化时,更新modelValue
|
||
watch(currentViewIndex, (newIndex) => {
|
||
if (props.views.length > 0 && newIndex >= 0 && newIndex < props.views.length) {
|
||
const viewName = props.views[newIndex].name;
|
||
if (viewName !== props.modelValue) {
|
||
emit('update:modelValue', viewName);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 轮播定时器
|
||
let carouselTimer = null;
|
||
|
||
// 开始轮播
|
||
const startCarousel = () => {
|
||
if (!props.autoplay || validViews.value.length <= 1) return;
|
||
|
||
stopCarousel(); // 先停止之前的定时器
|
||
|
||
carouselTimer = setInterval(() => {
|
||
nextView();
|
||
}, props.interval);
|
||
};
|
||
|
||
// 停止轮播
|
||
const stopCarousel = () => {
|
||
if (carouselTimer) {
|
||
clearInterval(carouselTimer);
|
||
carouselTimer = null;
|
||
}
|
||
};
|
||
|
||
// 切换到下一个有效视角
|
||
const nextView = () => {
|
||
if (validViews.value.length <= 1) return;
|
||
|
||
// 找到下一个有效视角
|
||
let nextIndex = currentViewIndex.value;
|
||
do {
|
||
nextIndex = (nextIndex + 1) % props.views.length;
|
||
} while (!props.views[nextIndex].streamUrl && nextIndex !== currentViewIndex.value);
|
||
|
||
if (props.views[nextIndex].streamUrl) {
|
||
currentViewIndex.value = nextIndex;
|
||
restartCarousel();
|
||
}
|
||
};
|
||
|
||
// 切换到上一个有效视角
|
||
const prevView = () => {
|
||
if (validViews.value.length <= 1) return;
|
||
|
||
// 找到上一个有效视角
|
||
let prevIndex = currentViewIndex.value;
|
||
do {
|
||
prevIndex = (prevIndex - 1 + props.views.length) % props.views.length;
|
||
} while (!props.views[prevIndex].streamUrl && prevIndex !== currentViewIndex.value);
|
||
|
||
if (props.views[prevIndex].streamUrl) {
|
||
currentViewIndex.value = prevIndex;
|
||
restartCarousel();
|
||
}
|
||
};
|
||
|
||
// 设置特定视角
|
||
const setView = (index) => {
|
||
if (index >= 0 && index < props.views.length && props.views[index].streamUrl) {
|
||
currentViewIndex.value = index;
|
||
restartCarousel();
|
||
}
|
||
};
|
||
|
||
// 重新开始轮播(手动切换后)
|
||
const restartCarousel = () => {
|
||
if (props.autoplay) {
|
||
stopCarousel();
|
||
startCarousel();
|
||
}
|
||
};
|
||
|
||
// 监听视角列表变化
|
||
watch(() => props.views, () => {
|
||
// 如果视角列表变化,尝试保持当前选中的视角
|
||
if (props.modelValue && props.views.length > 0) {
|
||
const index = props.views.findIndex(view => view.name === props.modelValue);
|
||
if (index !== -1 && props.views[index].streamUrl) {
|
||
currentViewIndex.value = index;
|
||
} else {
|
||
// 如果找不到匹配的视角或视角没有流,找第一个有效视角
|
||
const firstValidIndex = props.views.findIndex(view => view.streamUrl);
|
||
if (firstValidIndex !== -1) {
|
||
currentViewIndex.value = firstValidIndex;
|
||
emit('update:modelValue', props.views[firstValidIndex].name);
|
||
} else {
|
||
// 如果没有有效视角,设置为第一个视角
|
||
currentViewIndex.value = 0;
|
||
if (props.views.length > 0) {
|
||
emit('update:modelValue', props.views[0].name);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 如果没有modelValue,找第一个有效视角
|
||
const firstValidIndex = props.views.findIndex(view => view.streamUrl);
|
||
if (firstValidIndex !== -1) {
|
||
currentViewIndex.value = firstValidIndex;
|
||
emit('update:modelValue', props.views[firstValidIndex].name);
|
||
} else {
|
||
// 如果没有有效视角,设置为第一个视角
|
||
currentViewIndex.value = 0;
|
||
if (props.views.length > 0) {
|
||
emit('update:modelValue', props.views[0].name);
|
||
}
|
||
}
|
||
}
|
||
|
||
restartCarousel();
|
||
}, { deep: true });
|
||
|
||
// 监听自动播放属性变化
|
||
watch(() => props.autoplay, (newValue) => {
|
||
if (newValue) {
|
||
startCarousel();
|
||
} else {
|
||
stopCarousel();
|
||
}
|
||
});
|
||
|
||
// 组件挂载时开始轮播
|
||
onMounted(() => {
|
||
startCarousel();
|
||
});
|
||
|
||
// 组件卸载时停止轮播
|
||
onUnmounted(() => {
|
||
stopCarousel();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.carousel-webrtc-player {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.video-container {
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
/* 左右箭头样式 */
|
||
.carousel-arrows {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
pointer-events: none; /* 避免干扰视频点击 */
|
||
}
|
||
|
||
.arrow {
|
||
width: 30px;
|
||
height: 30px;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
margin: 0 10px;
|
||
pointer-events: auto; /* 恢复箭头点击 */
|
||
transition: opacity 0.3s;
|
||
opacity: 0;
|
||
}
|
||
|
||
.video-container:hover .arrow {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.arrow:hover {
|
||
opacity: 1;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
}
|
||
|
||
.arrow-icon {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-top: 2px solid #fff;
|
||
border-right: 2px solid #fff;
|
||
display: block;
|
||
}
|
||
|
||
.arrow-icon.left {
|
||
transform: rotate(-135deg);
|
||
margin-left: 2px;
|
||
}
|
||
|
||
.arrow-icon.right {
|
||
transform: rotate(45deg);
|
||
margin-right: 2px;
|
||
}
|
||
|
||
/* 视角指示器样式 */
|
||
.view-indicators {
|
||
position: absolute;
|
||
bottom: 10px;
|
||
left: 0;
|
||
right: 0;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
z-index: 10;
|
||
opacity: 0;
|
||
transition: opacity 0.3s;
|
||
}
|
||
|
||
.video-container:hover .view-indicators {
|
||
opacity: 1;
|
||
}
|
||
|
||
.indicator {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.5);
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.indicator.active {
|
||
background: #fff;
|
||
transform: scale(1.2);
|
||
}
|
||
|
||
.video-empty-state {
|
||
background: #033347;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
:deep(.empty-icon) {
|
||
height: 46px !important;
|
||
}
|
||
|
||
:deep(.empty-title) {
|
||
display: none;
|
||
}
|
||
|
||
:deep(.empty-subtitle) {
|
||
color: #fff;
|
||
font-size: 10px;
|
||
letter-spacing: 1px;
|
||
margin-top: 5px;
|
||
}
|
||
</style> |