| | |
| | | <template> |
| | | <div class="camera-page"> |
| | | <div class="scroll-wrapper"> |
| | | <el-button class="arrow left" @click="scrollLeft" :disabled="canScrollLeft" circle> |
| | | <el-button class="arrow left" @click="scrollLeftManual" :disabled="canScrollLeft" circle> |
| | | <el-icon><ArrowLeft /></el-icon> |
| | | </el-button> |
| | | |
| | | <div class="scroll-viewport" ref="viewportRef"> |
| | | <div class="scroll-viewport" ref="viewportRef" @mouseenter="pauseAuto" @mouseleave="resumeAuto"> |
| | | <div class="scroll-track" :style="{ transform: `translateX(${-scrollPos}px)` }"> |
| | | <div v-for="dev in deviceList" :key="dev.id" class="player-card"> |
| | | <ezuikit-player |
| | | v-if="ysToken" |
| | | :key="'p_' + dev.id + '_' + ysToken" |
| | | :container-id="'player_' + dev.id" |
| | | :key="'p_' + dev.id + '_' + ysToken + '_' + playerRemountKey" |
| | | :container-id="'player_' + dev.id + '_' + playerRemountKey" |
| | | :access-token="ysToken" |
| | | :url="`ezopen://open.ys7.com/${dev.iotCode || dev.code}/1.hd.live`" |
| | | :pause-after-first-frame="true" |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <el-button class="arrow right" @click="scrollRight" :disabled="canScrollRight" circle> |
| | | <el-button class="arrow right" @click="scrollRightManual" :disabled="canScrollRight" circle> |
| | | <el-icon><ArrowRight /></el-icon> |
| | | </el-button> |
| | | </div> |
| | |
| | | const loading = ref(false); |
| | | const scrollPos = ref(0); |
| | | const viewportRef = ref<HTMLElement | null>(null); |
| | | /** 全屏切换后递增,强制重建萤石播放器 */ |
| | | const playerRemountKey = ref(0); |
| | | |
| | | // 每次滚动的距离(一个摄像头的宽度 + 间距) |
| | | function onScreenFullscreenChange() { |
| | | playerRemountKey.value += 1; |
| | | } |
| | | |
| | | // 自动滚动控制 |
| | | const AUTO_INTERVAL = 3000; // 每次滚动间隔(ms) |
| | | const autoPlay = ref(true); // 是否自动滚动 |
| | | let autoTimer: ReturnType<typeof setInterval> | null = null; |
| | | |
| | | // 每次滚动的距离 |
| | | const scrollStep = computed(() => { |
| | | const cardWidth = 312; // 300px 宽度 + 12px 间距的一半 |
| | | const cardWidth = 300; // 与 player-card 宽度一致 |
| | | const gap = 12; |
| | | return cardWidth + gap; |
| | | }); |
| | | |
| | | // 是否可以向左滚动 |
| | | const canScrollLeft = computed(() => scrollPos.value <= 0); |
| | | |
| | | // 是否可以向右滚动 |
| | | const canScrollRight = computed(() => { |
| | | if (!viewportRef.value) return true; |
| | | // 可滚动的最大距离 |
| | | const maxScroll = computed(() => { |
| | | if (!viewportRef.value) return 0; |
| | | const viewportWidth = viewportRef.value.clientWidth; |
| | | const trackWidth = deviceList.value.length * scrollStep.value; |
| | | // 增加 2px 容差 |
| | | return scrollPos.value >= trackWidth - viewportWidth - 2; |
| | | return Math.max(0, trackWidth - viewportWidth); |
| | | }); |
| | | |
| | | // 是否可以向左/向右滚动(手动箭头用) |
| | | const canScrollLeft = computed(() => scrollPos.value <= 0); |
| | | const canScrollRight = computed(() => scrollPos.value >= maxScroll.value - 2); |
| | | |
| | | // 自动滚动方向: 1 向右, -1 向左 |
| | | const direction = ref(1); |
| | | |
| | | function scrollLeft() { |
| | | scrollPos.value = Math.max(0, scrollPos.value - scrollStep.value); |
| | | } |
| | | |
| | | function scrollRight() { |
| | | if (!viewportRef.value) return; |
| | | const viewportWidth = viewportRef.value.clientWidth; |
| | | const trackWidth = deviceList.value.length * scrollStep.value; |
| | | const maxScroll = trackWidth - viewportWidth; |
| | | |
| | | // 如果剩余距离小于一个步长,直接滚动到底部 |
| | | if (scrollPos.value + scrollStep.value >= maxScroll) { |
| | | scrollPos.value = maxScroll; |
| | | const max = maxScroll.value; |
| | | if (scrollPos.value + scrollStep.value >= max) { |
| | | scrollPos.value = max; |
| | | } else { |
| | | scrollPos.value += scrollStep.value; |
| | | } |
| | | } |
| | | |
| | | // 执行一步自动滚动(到边界自动换向,回到起点循环) |
| | | function autoTick() { |
| | | if (deviceList.value.length === 0) return; |
| | | if (maxScroll.value <= 0) return; // 一屏装下全部,不滚动 |
| | | |
| | | scrollPos.value += scrollStep.value * direction.value; |
| | | |
| | | if (scrollPos.value >= maxScroll.value) { |
| | | scrollPos.value = maxScroll.value; |
| | | direction.value = -1; // 到最右,回头向左 |
| | | } else if (scrollPos.value <= 0) { |
| | | scrollPos.value = 0; |
| | | direction.value = 1; // 回到起点,循环向右 |
| | | pauseAuto(); // 回到起点停一下(可选,见下方 startAuto) |
| | | startAuto(); // 立即开始新一轮 |
| | | } |
| | | } |
| | | |
| | | function startAuto() { |
| | | stopAuto(); |
| | | if (!autoPlay.value) return; |
| | | autoTimer = setInterval(autoTick, AUTO_INTERVAL); |
| | | } |
| | | |
| | | function stopAuto() { |
| | | if (autoTimer) { |
| | | clearInterval(autoTimer); |
| | | autoTimer = null; |
| | | } |
| | | } |
| | | |
| | | // 鼠标悬停暂停,移开继续 |
| | | function pauseAuto() { |
| | | stopAuto(); |
| | | } |
| | | function resumeAuto() { |
| | | startAuto(); |
| | | } |
| | | |
| | | // 手动点击箭头:重置自动滚动定时器,避免与自动节奏冲突 |
| | | function scrollLeftManual() { |
| | | scrollLeft(); |
| | | startAuto(); |
| | | } |
| | | function scrollRightManual() { |
| | | scrollRight(); |
| | | startAuto(); |
| | | } |
| | | |
| | | async function fetchYsToken() { |
| | |
| | | const res = await service.basicdata.iot.getCameras(); |
| | | deviceList.value = (res || []) as any[]; |
| | | scrollPos.value = 0; |
| | | direction.value = 1; |
| | | // 数据加载完再启动自动滚动 |
| | | startAuto(); |
| | | } catch (e: any) { |
| | | console.error('获取摄像头列表失败', e?.response?.data?.message || e.message); |
| | | deviceList.value = []; |
| | |
| | | } |
| | | |
| | | async function refreshDevices() { |
| | | stopAuto(); |
| | | await fetchDevices(); |
| | | } |
| | | |
| | | function handleKeydown(e: KeyboardEvent) { |
| | | if (e.key === 'ArrowLeft') scrollLeft(); |
| | | if (e.key === 'ArrowRight') scrollRight(); |
| | | if (e.key === 'ArrowLeft') { scrollLeftManual(); } |
| | | if (e.key === 'ArrowRight') { scrollRightManual(); } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | window.addEventListener('keydown', handleKeydown); |
| | | window.addEventListener('report-screen-fullscreen-change', onScreenFullscreenChange); |
| | | await fetchYsToken(); |
| | | await fetchDevices(); |
| | | await fetchDevices(); // 内部会在数据就绪后 startAuto |
| | | }); |
| | | |
| | | onUnmounted(() => { |
| | | stopAuto(); |
| | | window.removeEventListener('keydown', handleKeydown); |
| | | window.removeEventListener('report-screen-fullscreen-change', onScreenFullscreenChange); |
| | | }); |
| | | </script> |
| | | |