布局大改,CameraRules规则弹窗引入

This commit is contained in:
龚皓 2024-11-22 17:48:24 +08:00
parent b8dfc49d55
commit 1c0a51ed5e
1 changed files with 568 additions and 317 deletions

View File

@ -1,36 +1,71 @@
<template> <template>
<div class="camera-container"> <div class="camera-container">
<div class="top-header"> <div class="top-header">
<el-select v-model="filterStatus" placeholder="筛选状态" @change="fetchCameras" class="status-filter" > <div class="search-row">
<el-option label="全部" value="all"></el-option> <el-input v-model="searchKeyword" placeholder="搜索摄像头名称" @input="filterCameras" class="search-input" />
<el-option label="在线" value="online"></el-option>
<el-option label="离线" value="offline"></el-option>
</el-select>
<div class="top-text">警戒点位</div>
<el-select v-model="selectedCameraId" placeholder="搜索摄像头名称" @change="selectCameraById" clearable filterable
class="camera-select" >
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
<span>{{ camera.id }}.</span> &nbsp;名称: {{ camera.name }}
</el-option>
</el-select>
</div> </div>
<el-select v-model="filterStatus" placeholder="筛选状态" @change="fetchCameras" class="status-filter">
<!-- 播放列表设置为可滚动 --> <el-option label="全部" value="all"></el-option>
<el-option label="在线" value="online"></el-option>
<el-option label="离线" value="offline"></el-option>
</el-select>
<!-- <div class="top-text">警戒点位</div>
<el-select v-model="selectedCameraId" placeholder="搜索摄像头名称" @change="selectCameraById" clearable filterable
class="camera-select">
<el-option v-for="camera in cameras" :key="camera.id" :label="camera.name" :value="camera.id">
<span>{{ camera.id }}.</span> &nbsp;名称: {{ camera.name }}
</el-option>
</el-select> -->
</div>
<div class="content-container">
<div class="left-part">
<div class="camera-list">
<el-card v-for="camera in filteredCameras" :key="camera.id" class="camera-item"
@click="selectCameraById(camera.id)">
<template #header>
<el-row class="row-id-name">
<el-col :span="14" class="col-camera-id">
{{ camera.name }}
</el-col>
<!-- <el-col :span="8" class="col-camera-name">
{{ camera.name }}
</el-col> -->
<el-col :span="10" class="col-camera-setting">
<el-button type="text" icon="el-icon-setting" class="settings-button"
@click.stop="handleSettings(camera.id)">设置</el-button>
</el-col>
</el-row>
</template>
<div class="div-content">
<el-row class="row-content">
<el-col :span="10" class="col-camera-snapshot">
通道{{ camera.id }}
</el-col>
<el-col :span="14" class="col-camera-snapshot">
<el-image :src="camera.snapshot" :zoom-rate="1.2" :max-scale="7" :min-scale="0.2"
:preview-src-list="camera.snapshot" class="camera-img" />
</el-col>
</el-row>
</div>
</el-card>
</div>
</div>
<div class="camera-grid"> <div class="camera-grid">
<div v-for="(camera, index) in selectedCameras" :key="camera.id" class="grid-item"> <div v-for="(camera, index) in selectedCameras" :key="camera.id" class="grid-item">
<div class="stream-control"> <div class="stream-control">
<p class="camera-name-title">{{ camera.name }}</p> <p class="camera-name-title">{{ camera.name }}</p>
<div class="close-button" @click="closeStream(camera)">×</div> <div class="close-button" @click="closeStream(camera)">×</div>
</div> </div>
<div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false"> <div class="play-button-container" @mouseenter="showButton = true" @mouseleave="showButton = false">
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot"> <div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
<el-icon size="48"> <el-icon size="48">
<VideoCameraFilled /> <VideoCameraFilled />
</el-icon> </el-icon>
</div> </div>
<img v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot" <el-image v-if="!camera.playing && camera.snapshot" :src="camera.snapshot" alt="camera snapshot"
class="camera-snapshot" /> class="camera-snapshot" />
<el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large" <el-button v-show="!camera.playing || showButton" class="play-button" type="primary" circle size="large"
@click="openDialog(camera)"> @click="openDialog(camera)">
@ -42,309 +77,525 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 弹窗播放视频 -->
<el-dialog v-model="dialogVisible" width="50%" @close="closeDialog">
<template #title>播放摄像头: {{ currentCamera?.name }}</template>
<canvas v-show="dialogVisible" ref="dialogCanvas" class="dialog-canvas"></canvas>
</el-dialog>
</div> </div>
</template>
<el-dialog v-model="dialogVisible" width="50%" @close="closeDialog">
<script setup> <template #title>播放摄像头: {{ currentCamera?.name }}</template>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'; <canvas v-show="dialogVisible" ref="dialogCanvas" class="dialog-canvas"></canvas>
import { BoxApi } from '@/utils/boxApi.ts'; </el-dialog>
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
const cameras = ref([]); <CameraRules :visible="rulesDialogVisible" :cameraData="currentCameraData"
const selectedCameras = ref([]); @update:visible="rulesDialogVisible = $event" @save-result="handleSaveResult" />
const showButton = ref(false);
const apiInstance = new BoxApi();
const canvasRefs = ref({});
const selectedCameraId = ref(null); </div>
const dialogVisible = ref(false); // </template>
const currentCamera = ref(null); //
<script setup>
// canvas import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
const dialogCanvas = ref(null); import { BoxApi } from '@/utils/boxApi.ts';
const filterStatus = ref("all"); import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
import CameraRules from '@/html/CameraRules.vue';
const fetchCameras = async () => { import { ElMessage } from 'element-plus';
try {
const token = localStorage.getItem('alertToken'); const cameras = ref([]);
const cameraData = await apiInstance.getMinCameras(token); const selectedCameras = ref([]);
const showButton = ref(false);
// filterStatus const apiInstance = new BoxApi();
if (filterStatus.value === "online") { const canvasRefs = ref({});
cameras.value = cameraData.filter(camera => camera.status === "online"); const selectedCameraId = ref(null);
} else if (filterStatus.value === "offline") { const dialogVisible = ref(false); //
cameras.value = cameraData.filter(camera => camera.status === "offline"); const currentCamera = ref(null); //
} else {
cameras.value = cameraData; // "all" // canvas
} const dialogCanvas = ref(null);
} catch (error) { const filterStatus = ref("all");
console.error('获取摄像头列表失败:', error);
} const filteredCameras = ref([]);
}; const searchKeyword = ref('');
const selectCameraById = (cameraId) => { const currentCameraData = ref(null);
const camera = cameras.value.find(c => c.id === cameraId); const rulesDialogVisible = ref(false);
if (camera && !selectedCameras.value.some(c => c.id === camera.id)) {
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
} const fetchCameras = async () => {
}; try {
//
const openDialog = async (camera) => {
currentCamera.value = camera;
dialogVisible.value = true;
await nextTick();
startStreamInDialog(camera);
};
//
const startStreamInDialog = async (camera) => {
const canvas = dialogCanvas.value;
if (!camera || !canvas) {
console.error('未找到对应的 canvas');
return;
}
const token = localStorage.getItem('alertToken'); const token = localStorage.getItem('alertToken');
try { const cameraData = await apiInstance.getAllCameras(token);
const response = await apiInstance.startCameraStream(token, camera.id); // console.log("cameraData>>>>>>>>>>>>>>>", cameraData);
camera.streamPort = response.port;
camera.playing = true; // filterStatus
if (filterStatus.value === "online") {
const url = `ws://192.168.28.33:${camera.streamPort}/`; cameras.value = cameraData.filter(camera => camera.status === "online");
console.log('播放路径:', url); } else if (filterStatus.value === "offline") {
cameras.value = cameraData.filter(camera => camera.status === "offline");
if (window.JSMpeg) { } else {
const player = new window.JSMpeg.Player(url, { cameras.value = cameraData;
canvas: canvas, // console.log("all cameras:", cameras.value);
autoplay: true,
videoBufferSize: 15 * 1024 * 1024,
audioBufferSize: 5 * 1024 * 1024,
});
camera.player = player;
} else {
console.error('JSMpeg 未加载');
}
} catch (error) {
console.error('启动视频流失败:', error);
} }
}; filteredCameras.value = [...cameras.value];
} catch (error) {
// console.error('获取摄像头列表失败:', error);
const closeDialog = () => { }
if (currentCamera.value) { };
handleStopStream(currentCamera.value);
} const filterCameras = () => {
dialogVisible.value = false; if (!searchKeyword.value.trim()) {
currentCamera.value = null; filteredCameras.value = [...cameras.value];
}; } else {
filteredCameras.value = cameras.value.filter(camera =>
const handleStopStream = async (camera) => { camera.name.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
}
// console.log("filteredCameras>>:", filteredCameras.value);
};
const selectCameraById = (cameraId) => {
const camera = cameras.value.find(c => c.id === cameraId);
if (camera && !selectedCameras.value.some(c => c.id === camera.id)) {
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
// console.log("", selectedCameras.value)
}
};
const handleSettings = async (cameraId) => {
try {
const token = localStorage.getItem('alertToken'); const token = localStorage.getItem('alertToken');
try { const cameraData = await apiInstance.getCameraById(token, cameraId);
await apiInstance.stopCameraStream(token, camera.id); // console.log(" cameraData:", cameraData);
camera.playing = false; currentCameraData.value = cameraData;
rulesDialogVisible.value = true;
if (camera.player) { } catch (error) {
camera.player.destroy(); console.error('获取摄像头规则失败:', error);
camera.player = null; }
} };
} catch (error) {
console.error('停止视频流失败:', error); const handleSaveResult = ({ success, message }) => {
console.log('收到子组件保存结果事件:', { success, message });
ElMessage({
message,
type: success ? 'success' : 'error',
duration: 2000,
});
if (success) {
rulesDialogVisible.value = false; //
}
};
//
const openDialog = async (camera) => {
currentCamera.value = camera;
dialogVisible.value = true;
await nextTick();
startStreamInDialog(camera);
};
//
const startStreamInDialog = async (camera) => {
const canvas = dialogCanvas.value;
if (!camera || !canvas) {
console.error('未找到对应的 canvas');
return;
}
const token = localStorage.getItem('alertToken');
const rememberedAddress = localStorage.getItem('rememberedAddress');
if (!rememberedAddress) {
alert('主机地址获取异常,请确认地址配置');
console.error('主机地址获取异常');
return;
}
try {
const response = await apiInstance.startCameraStream(token, camera.id);
camera.streamPort = response.port;
camera.playing = true;
const url = `ws://${rememberedAddress}:${camera.streamPort}/`;
// const url = `ws://192.168.28.33:${camera.streamPort}/`;
// console.log(':', url);
if (window.JSMpeg) {
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 未加载');
}
} catch (error) {
console.error('启动视频流失败:', error);
}
};
//
const closeDialog = () => {
if (currentCamera.value) {
handleStopStream(currentCamera.value);
}
dialogVisible.value = false;
currentCamera.value = null;
};
const handleStopStream = async (camera) => {
const token = localStorage.getItem('alertToken');
try {
await apiInstance.stopCameraStream(token, camera.id);
camera.playing = false;
if (camera.player) {
camera.player.destroy();
camera.player = null;
}
} catch (error) {
console.error('停止视频流失败:', error);
}
};
const closeStream = (camera) => {
handleStopStream(camera);
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
};
onMounted(() => {
fetchCameras();
});
onBeforeUnmount(() => {
selectedCameras.value.forEach(camera => {
if (camera.player) {
camera.player.destroy();
} }
};
const closeStream = (camera) => {
handleStopStream(camera);
selectedCameras.value = selectedCameras.value.filter(c => c.id !== camera.id);
};
onMounted(() => {
fetchCameras();
}); });
});
</script>
<style scoped>
.camera-container {
display: flex;
flex-direction: column;
background-color: #F1F1F1;
border-radius: 20px;
width: 80vw;
}
.top-header {
/* width: 100%; */
display: flex;
flex-direction: row;
margin: 0;
}
.search-row {
display: flex;
align-content: center;
justify-content: center;
position: relative;
top: 0vh;
left: 0vw;
width: 10vw;
margin-left: 1vh;
margin: 1vh;
}
::v-deep .search-row input{
color: #fffefe;
}
::v-deep .search-input .el-input__inner{
background-color: #001529;
/* background-color:red; */
display: flex;
align-content: center;
justify-content: center;
}
::v-deep .search-input .el-input__wrapper{
background-color: #001529;
/* background-color: red; */
box-shadow: 0 0 0 0px
}
::v-deep .search-input .el-input__inner::placeholder{
color: #fffefe;
}
.status-filter {
position: relative;
top: 0vh;
left: 0vw;
width: 10vw;
margin-left: 1vh;
margin: 1vh;
}
.content-container {
display: flex;
height: 58vh;
width: 80vw;
}
.left-part {
display: flex;
flex-direction: column;
min-width: 12vw;
overflow-y: auto;
}
.camera-list {
display: flex;
flex-direction: column;
gap: 10px;
padding: 1vh;
}
.camera-item {
display: flex;
flex-direction: column;
flex: 40%;
background-color: #001529;
border-radius: 10px;
border: none;
}
::v-deep .camera-item .el-card__header {
border-radius: 10px 10px 0 0 ;
/* background-color: rgb(41, 12, 150); */
background: linear-gradient(to left top, rgb(18, 110, 196), rgba(3, 55, 153, 0.3));
height: 4vh;
/* min-height: 3vh;
min-width: 13vw; */
min-height: 50px;
min-width: 300px;
color: #ffffff;
padding: 0;
border: none;
display: flex;
border: none;
/* align-items: center; */
}
.row-id-name {
display: flex;
width: 100%;
box-sizing: border-box;
}
.col-camera-id {
display: flex;
text-align: center;
align-items: center;
justify-content: center;
}
.col-camera-name {
text-align: center;
display: flex;
align-items: center;
}
.col-camera-setting {
text-align: center;
display: flex;
align-items: center;
justify-content: start;
onBeforeUnmount(() => { }
selectedCameras.value.forEach(camera => { .settings-button{
if (camera.player) { color: #fffefe;
camera.player.destroy(); }
}
}); ::v-deep .camera-item .el-card__body {
}); /* min-height: 10vh;
</script> min-width: 13vw; */
border-radius: 0 0 10px 10px;
<style scoped> background-color: #001529;
.camera-container { padding: 1vh;
display: flex; }
flex-direction: column;
} .div-content,
.row-content {
.top-header { display: flex;
width: 100%; }
display: flex;
flex-direction: row; .col-camera-snapshot {
margin: 0; text-align: center;
} background-color: #001529;
width: 10vw;
.status-filter { align-content: center;
position: relative; color: #ffffff;
top: 0vh; }
left: 0vw;
width: 5vw; .camera-img {
margin-left: 1vh; /* object-fit: cover; */
margin-top: 1vh; cursor: pointer;
padding: 1vh;
} max-height: 100px;
::v-deep .status-filter .el-select__wrapper{ }
background-color: #001529;
box-shadow: 0 0 0 0 !important;
border-radius: 0;
} ::v-deep .status-filter .el-select__wrapper {
background-color: #001529;
::v-deep .camera-select .el-select__wrapper{ box-shadow: 0 0 0 0 !important;
background-color: #001529; border-radius: 0;
box-shadow: 0 0 0 0 !important; }
border-radius: 0;
} ::v-deep .camera-select .el-select__wrapper {
background-color: #001529;
::v-deep .camera-select .el-select__selected-item { box-shadow: 0 0 0 0 !important;
color: #ffffff !important; border-radius: 0;
} }
::v-deep .status-filter .el-select__selected-item { ::v-deep .camera-select .el-select__selected-item {
color: #ffffff !important; color: #ffffff !important;
} }
.camera-select { ::v-deep .status-filter .el-select__selected-item {
position: relative; color: #ffffff !important;
top: 0vh; }
left: 0vw;
width: 12vw; .camera-select {
margin-left: 0vh; position: relative;
margin-top: 1vh; top: 0vh;
} left: 0vw;
width: 12vw;
margin-left: 0vh;
margin-top: 1vh;
}
.top-text {
display: block;
font-size: 15px;
width: 7vw;
margin: 1vh 0 0 0; .top-text {
padding: 0 0 0 10vw; display: block;
/* justify-content: center; */ font-size: 15px;
align-content: center; width: 7vw;
background-color: #001529; margin: 1vh 0 0 0;
line-height: 0px; padding: 0 0 0 10vw;
color: aliceblue; /* justify-content: center; */
font-weight: bold; align-content: center;
} background-color: #001529;
line-height: 0px;
.stream-control { color: aliceblue;
display: flex; font-weight: bold;
align-items: center; }
text-align: center;
background-color: black; .stream-control {
} display: flex;
align-items: center;
.camera-name-title { text-align: center;
padding: 0.2vh; background-color: black;
margin: 0; }
font-size: 13px;
display: block; .camera-name-title {
color: white; padding: 0.2vh;
} margin: 0;
font-size: 13px;
.camera-grid { display: block;
margin: 0vh 1vh; color: white;
padding: 0 0 2vh 0; }
display: grid;
grid-template-columns: 1fr 1fr; .camera-grid {
gap: 1vh 0vh; margin: 0vh 1vh;
/* height: 39vh; padding: 0 0 2vh 0;
width: 34vw; */ display: grid;
width: 68vw; grid-template-columns: 1fr 1fr;
height: 55vh; gap: 1vh 0vh;
max-height: 58vh; /* height: 39vh;
overflow-y: scroll; width: 34vw; */
scrollbar-width: none; width: 68vw;
/* background-color: #001529; */ height: 55vh;
background-color: #001529; max-height: 58vh;
} overflow-y: scroll;
scrollbar-width: none;
.camera-grid::-webkit-scrollbar { /* background-color: #ffffff; */
display: none; background-color: #F1F1F1;
} /* background-color: #c71515; */
}
.grid-item {
margin: 1vh; .camera-grid::-webkit-scrollbar {
position: relative; display: none;
height: 25vh; }
display: flex;
flex-direction: column; .grid-item {
} margin: 1vh;
position: relative;
.camera-snapshot, height: 25vh;
.camera-large { display: flex;
width: 100%; flex-direction: column;
height: 100%; }
object-fit: cover;
} .camera-snapshot,
.camera-large {
.close-button { width: 100%;
position: absolute; height: 100%;
top: 1px; object-fit: cover;
right: 1px; }
width: 15px;
height: 15px; .close-button {
background-color: #000000; position: absolute;
color: aliceblue; top: 1px;
padding: 0 2px 2px 0; right: 1px;
border-radius: 0; width: 15px;
display: flex; height: 15px;
align-items: center; background-color: #000000;
justify-content: center; color: aliceblue;
font-size: 13px; padding: 0 2px 2px 0;
cursor: pointer; border-radius: 0;
transition: background-color 0.2s ease; display: flex;
} align-items: center;
justify-content: center;
.close-button:hover { font-size: 13px;
background-color: red; cursor: pointer;
} transition: background-color 0.2s ease;
}
.play-button-container {
position: relative; .close-button:hover {
height: 100%; background-color: red;
width: 100%; }
display: flex;
align-items: center; .play-button-container {
justify-content: center; position: relative;
} height: 100%;
width: 100%;
.play-button { display: flex;
position: absolute; align-items: center;
top: 50%; justify-content: center;
left: 50%; }
transform: translate(-50%, -50%);
z-index: 10; .play-button {
opacity: 0.4; position: absolute;
transition: opacity 0.2s ease-in-out; top: 50%;
} left: 50%;
transform: translate(-50%, -50%);
.play-button:hover { z-index: 10;
opacity: 1; opacity: 0.4;
} transition: opacity 0.2s ease-in-out;
}
.dialog-canvas {
width: 100%; .play-button:hover {
height: 100%; opacity: 1;
} }
.dialog-canvas {
width: 100%;
height: 100%;
}
.rule-card {
margin-bottom: 1rem;
padding: 1rem;
background-color: #f9f9f9;
}
</style> </style>