告警主页左侧搜索功能,右侧颜色设置

This commit is contained in:
龚皓 2024-10-25 16:55:59 +08:00
parent c086787dfe
commit 32feaeb0b5
1 changed files with 280 additions and 136 deletions

View File

@ -1,8 +1,15 @@
<template>
<div class="camera-container">
<!-- 左侧摄像头列表 -->
<!-- 左侧摄像头列表 -->
<div class="camera-list">
<el-card v-for="camera in cameras" :key="camera.id" class="camera-item" @click="selectCamera(camera)">
<el-input
v-model="searchQuery"
placeholder="搜索摄像头名称"
prefix-icon="el-icon-search"
clearable
class="search-input"
/>
<el-card v-for="camera in filteredCameras" :key="camera.id" class="camera-item" @click="selectCamera(camera)">
<div class="camera-header">
<span>ID: {{ camera.id }}</span>
<span class="status" :class="{ 'online': camera.status === 'online', 'offline': camera.status !== 'online' }">
@ -16,208 +23,232 @@
</el-card>
</div>
<!-- 右侧摄像头详情和播放控制 -->
<div class="camera-details" v-if="selectedCamera">
<el-card>
<div class="stream-control">
<p>{{ selectedCamera.name }}</p>
</div>
<!-- 右侧摄像头详情栅格 -->
<div class="camera-grid">
<div v-for="camera in selectedCameras" :key="camera.id" class="camera-details">
<el-card class="camera-card">
<div class="stream-control">
<p class="camera-name-title">{{ camera.name }}</p>
<el-button @click="closeStream(camera)" class="close-button" circle size="mini">X</el-button>
</div>
<!-- 在未播放时显示播放按钮 -->
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
<img v-show="!playing && selectedCamera.snapshot" :src="selectedCamera.snapshot" alt="camera snapshot"
class="camera-snapshot" />
<canvas v-show="playing" ref="canvasRef" class="camera-large"></canvas>
<!-- 视频播放和快照部分 -->
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
<!-- 未播放时显示快照或占位符 -->
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
<el-icon size="48">
<VideoCameraFilled />
</el-icon>
</div>
<!-- 播放按钮 -->
<el-button v-show="!playing || showButton" class="play-button" type="primary" circle size="large"
@click="handlePlayPause">
<el-icon>
<VideoPlay v-if="!playing" />
<VideoPause v-if="playing" />
</el-icon>
</el-button>
</div>
</el-card>
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
class="camera-snapshot" />
<!-- 播放视频流的 canvas -->
<canvas v-show="camera.playing" :ref="el => setCanvasRef(camera.id, el)" class="camera-large"></canvas>
<!-- 播放和暂停按钮 -->
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
@click="handlePlayPause(camera)">
<el-icon>
<VideoPlay v-if="!camera.playing" />
<VideoPause v-if="camera.playing" />
</el-icon>
</el-button>
</div>
</el-card>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { ref, onMounted, onBeforeUnmount, nextTick,computed } from 'vue';
import { BoxApi } from '@/utils/boxApi.ts';
import { VideoPlay, VideoPause } from '@element-plus/icons-vue';
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
//
const cameras = ref([]);
const selectedCamera = ref(null);
const playing = ref(false);
const streamPort = ref(null);
const canvasRef = ref(null);
const playerRef = ref(null);
const showButton = ref(false); //
//
const selectedCameras = ref([]);
//
const showButton = ref(false);
// API
const apiInstance = new BoxApi();
// canvas
const canvasRefs = ref({});
//
//
const searchQuery = ref('');
//
const filteredCameras = computed(() =>
cameras.value.filter(camera =>
camera.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
);
//
const fetchCameras = async () => {
try {
const token = localStorage.getItem('alertToken');
const cameraData = await apiInstance.getMinCameras(token); // getMinCameras
cameras.value = cameraData; //
const cameraData = await apiInstance.getMinCameras(token);
cameras.value = cameraData;
} catch (error) {
console.error('Error fetching cameras:', error);
console.error('获取摄像头列表失败:', error);
}
};
//
//
const selectCamera = (camera) => {
selectedCamera.value = camera; //
playing.value = false; //
streamPort.value = null; //
showButton.value = true; //
};
//
const handlePlayPause = async () => {
if (playing.value) {
handleStopStream();
} else {
handleStartStream();
if (!selectedCameras.value.some(c => c.id === camera.id)) {
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
}
};
//
const handleStartStream = async () => {
console.log('Button clicked and playing is:', playing.value);
if (!selectedCamera.value || !canvasRef.value) {
// canvas
const setCanvasRef = (cameraId, el) => {
if (el) {
canvasRefs.value[cameraId] = el;
}
};
// /
const handlePlayPause = async (camera) => {
if (camera.playing) {
handleStopStream(camera);
} else {
handleStartStream(camera);
}
};
//
const handleStartStream = async (camera) => {
await nextTick(); // DOM
const canvas = canvasRefs.value[camera.id];
if (!camera || !canvas) {
console.error('未找到对应的 canvas');
return;
}
const token = localStorage.getItem('alertToken');
try {
const response = await apiInstance.startCameraStream(token, selectedCamera.value.id);
streamPort.value = response.port;
playing.value = true;
const response = await apiInstance.startCameraStream(token, camera.id);
camera.streamPort = response.port;
camera.playing = true;
//
const url = `ws://192.168.28.33:${streamPort.value}/`;
console.log('Playing set to true:', playing.value);
if (playerRef.value) {
playerRef.value.destroy(); //
}
const rememberedAddress = localStorage.getItem('rememberedAddress') || '127.0.0.1';
const url = `ws://${rememberedAddress}:${camera.streamPort}/`;
// const url = `ws://192.168.28.33:${camera.streamPort}/`;
console.log('播放路径:', url);
if (window.JSMpeg) {
playerRef.value = new window.JSMpeg.Player(url, {
canvas: canvasRef.value,
const player = new window.JSMpeg.Player(url, {
canvas: canvas,
autoplay: true,
videoBufferSize: 15 * 1024 * 1024,
audioBufferSize: 5 * 1024 * 1024,
});
camera.player = player;
} else {
console.error('JSMpeg is not available on window object.');
console.error('JSMpeg 未加载');
}
} catch (error) {
console.error('Error starting stream:', error);
console.error('启动视频流失败:', error);
}
};
//
const handleStopStream = async () => {
if (!selectedCamera.value) {
return;
}
//
const handleStopStream = async (camera) => {
const token = localStorage.getItem('alertToken');
try {
await apiInstance.stopCameraStream(token, selectedCamera.value.id);
playing.value = false;
await apiInstance.stopCameraStream(token, camera.id);
camera.playing = false;
//
if (playerRef.value) {
playerRef.value.destroy();
playerRef.value = null;
if (camera.player) {
camera.player.destroy();
camera.player = null;
}
} catch (error) {
console.error('Error stopping stream:', error);
console.error('停止视频流失败:', error);
}
};
//
onBeforeUnmount(() => {
if (playerRef.value) {
playerRef.value.destroy();
playerRef.value = null;
}
//
const closeStream = (camera) => {
handleStopStream(camera);
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
};
//
onMounted(() => {
fetchCameras();
});
onMounted(() => {
fetchCameras(); //
//
onBeforeUnmount(() => {
selectedCameras.value.forEach(camera => {
if (camera.player) {
camera.player.destroy();
}
});
});
</script>
<style scoped>
.camera-container {
display: flex;
height: 100vh;
background-color: #1E2E4A;
}
/* 左侧摄像头列表 */
.camera-list {
width: 20%;
min-width: 215px;
max-height: 90vh;
max-height: 100vh;
/* 限制高度为一屏 */
overflow-y: auto;
/* 超出时滚动 */
box-sizing: border-box;
border-right: 1px solid #1E2E4A;
padding-right: 10px;
}
.search-input {
margin-bottom: 10px;
width: 100%;
}
/* 每个摄像头项目的样式 */
.camera-item {
margin-bottom: 5px;
margin-bottom: 8px;
cursor: pointer;
padding: 10px;
padding: 12px;
border: 1px solid #458388;
border-radius: 4px;
transition: background-color 0.3s, box-shadow 0.3s;
}
.camera-item:hover {
background-color: #f5f7fa;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
}
/* 摄像头项目头部 */
.camera-header {
display: flex;
justify-content: space-between;
font-size: 14px;
margin-bottom: 5px;
}
/* 摄像头状态标签 */
.status {
margin-left: 5px;
}
.camera-content {
display: flex;
align-items: center;
}
.camera-thumbnail {
width: 60px;
height: 40px;
margin-right: 10px;
}
.camera-name {
flex: 1;
word-break: break-word;
}
.camera-details {
width: 80%;
padding: 10px;
}
.camera-snapshot {
width: 100%;
height: 68vh;
object-fit: cover;
}
.camera-large {
width: 100%;
height: 68vh;
font-weight: bold;
}
.online {
@ -228,28 +259,141 @@ onMounted(() => {
color: red;
}
/* 摄像头内容:缩略图和名称 */
.camera-content {
display: flex;
align-items: center;
}
.camera-thumbnail {
width: 70px;
height: 50px;
margin-right: 10px;
object-fit: cover;
border-radius: 4px;
border: 2px solid #12d1df;
}
.camera-name {
flex: 1;
word-break: break-word;
font-size: 14px;
font-weight: bold;
color: #333;
}
/* 右侧摄像头详情展示 */
.camera-grid {
width: 80%;
height: 95vh; /* 占满页面右侧区域 */
display: grid;
grid-template-columns: repeat(2, 1fr); /* 两列 */
grid-template-rows: repeat(2, 1fr); /* 两行 */
gap: 5px; /* 栅格块之间的间距 */
padding: 10px;
/* background-color: #1E2E4A; */
background: linear-gradient(to top, rgba(0, 3, 3, 0.4), rgba(9, 21, 196, 0.3));
/* background: linear-gradient(to top, rgba(8, 53, 61, 0.4), rgba(9, 21, 196, 0.3)); */
/* border: 2px solid #ece9e9; */
box-sizing: border-box;
overflow-y: auto;
}
.camera-details {
height: 45vh;
/* background-color: #3b2c2c; */
/* background: linear-gradient(to top, rgba(8, 53, 61, 0.4), rgba(9, 21, 196, 0.3)); */
border-radius: 4px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
/* overflow-y: auto; */
}
.camera-card {
width: 100%;
height: 100%;
/* background-color: #1E2E4A; */
background: linear-gradient(to top, rgba(64, 226, 255, 0.7), rgba(211, 64, 248, 0.7));
/* background: linear-gradient(to top, rgba(0, 7, 8, 0.9), rgba(12, 2, 155, 0.7)); */
border: 2px solid #0b4c5f;
border-radius: 8px;
position: relative;
/* padding: 10px; */
box-sizing: border-box;
}
.stream-control {
display: flex;
margin-top: 20px;
text-align: center;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
/* .stream-control p {
color: #f5f7fa;
font-size: 18px;
margin: 0;
line-height: 1;
font-weight: bold;
} */
.camera-name-title {
font-size: 18px;
color: white;
font-weight: bold;
margin: 0;
padding: 0;
line-height: 1;
}
.close-button {
position: absolute;
top: 10px;
right: 20px;
z-index: 10;
}
.camera-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #6b6b6b;
border: 2px dashed rgba(109, 109, 109, 0.7);
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
background-color: rgba(50, 50, 50, 0.5);
}
/* 播放中的 canvas 样式 */
.camera-large {
width: 100%;
height: 100%;
border-radius: 8px;
object-fit: cover;
border: 2px solid rgba(109, 109, 109, 0.3);
}
/* 摄像头大图和快照 */
.camera-snapshot {
width: 100%;
height: 40vh;
object-fit: cover;
border-radius: 4px;
border: 2px solid #dcdcdc;
}
.stream-control p {
margin-right: 20px;
font-size: 15px;
line-height: 5px;
}
.stream-control el-button {
height: 40px;
line-height: 40px;
}
/* 播放按钮容器 */
.play-button-container {
position: relative;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
/* 播放按钮 */
.play-button {
position: absolute;
top: 50%;