robot_bigScreen/src/components/common/CarouselWebRTCPlayer.vue
2025-06-13 14:12:32 +08:00

372 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>