local_alert/src/html/Home.vue

410 lines
10 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="camera-container">
<!-- 左侧摄像头列表 -->
<div class="camera-list">
<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' }">
{{ camera.status === 'online' ? '在线' : '离线' }}
</span>
</div>
<div class="camera-content">
<img :src="camera.snapshot" alt="camera-preview" class="camera-thumbnail" />
<p class="camera-name">{{ camera.name }}</p>
</div>
</el-card>
</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">
<!-- 未播放时显示快照或占位符 -->
<div class="camera-placeholder" v-if="!camera.playing && !camera.snapshot">
<el-icon size="48">
<VideoCameraFilled />
</el-icon>
</div>
<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, nextTick,computed } from 'vue';
import { BoxApi } from '@/utils/boxApi.ts';
import { VideoPlay, VideoPause, VideoCameraFilled } from '@element-plus/icons-vue';
// 存储所有摄像头列表
const cameras = ref([]);
// 存储已选择并展示的摄像头
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);
cameras.value = cameraData;
} catch (error) {
console.error('获取摄像头列表失败:', error);
}
};
// 选择摄像头并展示详情
const selectCamera = (camera) => {
if (!selectedCameras.value.some(c => c.id === camera.id)) {
selectedCameras.value.push({ ...camera, playing: false, streamPort: null });
}
};
// 设置 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, camera.id);
camera.streamPort = response.port;
camera.playing = true;
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) {
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 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();
}
});
});
</script>
<style scoped>
.camera-container {
display: flex;
height: 100vh;
background-color: #1E2E4A;
}
/* 左侧摄像头列表 */
.camera-list {
width: 20%;
min-width: 215px;
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: 8px;
cursor: pointer;
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;
font-weight: bold;
}
.online {
color: green;
}
.offline {
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;
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;
}
/* 播放按钮容器 */
.play-button-container {
position: relative;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
/* 播放按钮 */
.play-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
opacity: 0.4;
transition: opacity 0.2s ease-in-out;
}
.play-button:hover {
opacity: 1;
}
</style>