<template>
|
<div class="camera-page">
|
<div class="scroll-wrapper">
|
<el-button class="arrow left" @click="scrollLeftManual" :disabled="canScrollLeft" circle>
|
<el-icon><ArrowLeft /></el-icon>
|
</el-button>
|
|
<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 + '_' + 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>
|
</div>
|
|
<el-button class="arrow right" @click="scrollRightManual" :disabled="canScrollRight" circle>
|
<el-icon><ArrowRight /></el-icon>
|
</el-button>
|
</div>
|
</div>
|
</template>
|
|
<script lang="ts" setup>
|
defineOptions({
|
name: 'camera-monitor',
|
});
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
import { useCool } from '/@/cool';
|
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
|
import EzuikitPlayer from '../../ezuikit/components/ezuikit-player.vue';
|
|
const { service } = useCool();
|
|
const deviceList = ref<any[]>([]);
|
const ysToken = ref('');
|
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 = 300; // 与 player-card 宽度一致
|
const gap = 12;
|
return cardWidth + gap;
|
});
|
|
// 可滚动的最大距离
|
const maxScroll = computed(() => {
|
if (!viewportRef.value) return 0;
|
const viewportWidth = viewportRef.value.clientWidth;
|
const trackWidth = deviceList.value.length * scrollStep.value;
|
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() {
|
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() {
|
if (ysToken.value) return;
|
try {
|
const res = await service.base.sys.param.stringbykey({ key: 'ys7.accessToken' });
|
ysToken.value = res as string;
|
} catch (e: any) {
|
console.error('萤石 Token 获取失败', e?.response?.data?.message || e.message);
|
}
|
}
|
|
async function fetchDevices() {
|
loading.value = true;
|
try {
|
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 = [];
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
async function refreshDevices() {
|
stopAuto();
|
await fetchDevices();
|
}
|
|
function handleKeydown(e: KeyboardEvent) {
|
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(); // 内部会在数据就绪后 startAuto
|
});
|
|
onUnmounted(() => {
|
stopAuto();
|
window.removeEventListener('keydown', handleKeydown);
|
window.removeEventListener('report-screen-fullscreen-change', onScreenFullscreenChange);
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.camera-page {
|
height: 100%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
padding: 16px;
|
}
|
|
.scroll-wrapper {
|
position: relative;
|
display: flex;
|
align-items: center;
|
width: 100%;
|
/* 移除 max-width 限制,让容器占满可用宽度 */
|
}
|
|
.scroll-viewport {
|
overflow: hidden;
|
flex: 1;
|
margin: 0 42px;
|
}
|
|
.scroll-track {
|
display: flex;
|
gap: 12px;
|
transition: transform 0.35s ease;
|
}
|
|
.player-card {
|
flex-shrink: 0;
|
width: 300px;
|
height: 360px;
|
border-radius: 8px;
|
overflow: hidden;
|
background: #000;
|
}
|
|
.arrow {
|
flex-shrink: 0;
|
z-index: 10;
|
background: #00ff00 !important;
|
border-color: #00ff00 !important;
|
color: #000 !important;
|
font-size: 18px;
|
width: 32px;
|
height: 96px; /* 加高箭头,更易点击 */
|
border-radius: 4px !important;
|
}
|
|
.arrow:hover {
|
background: #33ff33 !important;
|
border-color: #33ff33 !important;
|
}
|
|
.arrow:active {
|
background: #00cc00 !important;
|
border-color: #00cc00 !important;
|
}
|
|
.el-button.is-disabled {
|
opacity: 0.25;
|
background: #555 !important;
|
border-color: #555 !important;
|
color: #888 !important;
|
}
|
</style>
|