shiyanOperationMaintenance/RuoYi-Vue3-master/src/views/Security/videoSurveillance.vue

492 lines
11 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="monitor-page">
<div class="layout-container">
<!-- 左侧设备树面板 -->
<div class="device-tree-panel">
<div class="tree-header">
<div class="search-add">
<ElInput
v-model="searchDevice"
placeholder="请输入设备"
:prefix-icon="Search"
size="small"
></ElInput>
</div>
</div>
<!-- 自定义设备列表 -->
<div class="custom-tree">
<div
v-for="group in filteredTreeData"
:key="group.label"
class="tree-group"
>
<div class="group-label">{{ group.label }}</div>
<div
v-for="item in group.children"
:key="item.label"
class="tree-item"
:class="{
'selected': isItemSelected(item.label),
'disabled': isItemDisabled && !isItemSelected(item.label)
}"
@click="handleItemClick(item)"
>
<div class="item-content">
<Camera class="item-icon"></Camera>
<span class="item-label">{{ item.label }}</span>
</div>
<div v-if="isItemSelected(item.label)" class="selected-indicator">
<el-icon><Check /></el-icon>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧视频墙面板 -->
<div class="video-wall-panel">
<div class="video-header">
<Camera class="icon"></Camera>
<span>监控视频</span>
</div>
<div
class="video-container"
:style="{
gridTemplateColumns: gridColumns,
gridTemplateRows: gridRows
}"
>
<div
v-for="(video, index) in videoList"
:key="`video-${index}`"
class="video-item"
:style="{
backgroundColor: video.bgColor,
border: video.isSelected ? '2px solid #409eff' : 'none'
}"
@click="handleVideoClick(index)"
>
<div class="video-placeholder">
<Camera class="icon"></Camera>
<span>{{ video.name }}</span>
<Close
v-if="video.name !== '待选择区域'"
class="close-icon"
@click.stop="removeVideo(index)"
></Close>
</div>
</div>
</div>
<!-- 布局切换按钮 -->
<div class="layout-switch">
<ElButton
v-for="item in buttonType"
:key="`layout-${item.type}`"
:type="currentLayout === item.type ? 'primary' : ''"
size="small"
@click="changeLayout(item.type)"
:disabled="currentLayout === item.type"
>
{{ item.label }}
</ElButton>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { ElInput, ElButton, ElMessage } from 'element-plus';
import { Search, Camera, Close, Check } from '@element-plus/icons-vue';
const searchDevice = ref('');
const deviceTreeData = ref([
{
label: "公共区域",
children: [
{ label: "电梯前厅" },
{ label: "大会议室前走道" },
{ label: "东侧走道北向" },
{ label: "西侧走道南向" },
{ label: "西侧走道北向" },
{ label: "东侧走道南向" },
{ label: "小会议室前走道" }
]
}
]);
const currentLayout = ref(1);
// 重构使用对象来记录每个位置的设备key为位置索引value为设备信息
const videoPositions = ref({});
const selectedNodes = ref([]);
const buttonType = [
{ label: "1x1", type: 1 },
{ label: "2x2", type: 2 },
{ label: "3x3", type: 3 }
];
// 计算属性
const gridColumns = computed(() => {
switch (currentLayout.value) {
case 1: return "1fr";
case 2: return "repeat(2, 1fr)";
case 3: return "repeat(3, 1fr)";
default: return "1fr";
}
});
const gridRows = computed(() => {
switch (currentLayout.value) {
case 1: return "1fr";
case 2: return "repeat(2, 1fr)";
case 3: return "repeat(3, 1fr)";
default: return "1fr";
}
});
const maxCapacity = computed(() => {
return currentLayout.value === 1 ? 1 : (currentLayout.value === 2 ? 4 : 9);
});
const isItemDisabled = computed(() => {
return selectedNodes.value.length >= maxCapacity.value;
});
// 视频列表,根据位置信息生成
const videoList = computed(() => {
const capacity = maxCapacity.value;
const list = [];
for (let i = 0; i < capacity; i++) {
if (videoPositions.value[i]) {
// 该位置有设备
const device = videoPositions.value[i];
list.push({
name: device.label,
bgColor: `rgba(64, 158, 255, ${0.1 + (i % 5) * 0.15})`,
isSelected: false
});
} else {
// 该位置为空
list.push({
name: "待选择区域",
bgColor: "#e6f7ff",
isSelected: false
});
}
}
return list;
});
// 过滤树数据
const filteredTreeData = computed(() => {
if (!searchDevice.value) return deviceTreeData.value;
return deviceTreeData.value.map(group => ({
...group,
children: group.children.filter(item =>
item.label.toLowerCase().includes(searchDevice.value.toLowerCase())
)
})).filter(group => group.children.length > 0);
});
// 判断项目是否被选中
const isItemSelected = (label) => {
return selectedNodes.value.includes(label);
};
// 查找第一个可用的位置
const findAvailablePosition = () => {
const capacity = maxCapacity.value;
for (let i = 0; i < capacity; i++) {
if (!videoPositions.value[i]) {
return i;
}
}
return -1;
};
// 查找设备所在的位置
const findDevicePosition = (label) => {
for (const [position, device] of Object.entries(videoPositions.value)) {
if (device.label === label) {
return parseInt(position);
}
}
return -1;
};
// 处理项目点击
const handleItemClick = (item) => {
const itemLabel = item.label;
const position = findDevicePosition(itemLabel);
if (position !== -1) {
// 设备已存在,取消选择
delete videoPositions.value[position];
selectedNodes.value = selectedNodes.value.filter(label => label !== itemLabel);
ElMessage.info(`已移除${itemLabel}`);
} else {
// 设备不存在,尝试添加
if (selectedNodes.value.length >= maxCapacity.value) {
ElMessage.warning(`当前${buttonType.find(b => b.type === currentLayout.value)?.label}模式最多选择${maxCapacity.value}个视频`);
return;
}
const availablePosition = findAvailablePosition();
if (availablePosition !== -1) {
videoPositions.value[availablePosition] = item;
selectedNodes.value.push(itemLabel);
}
}
};
// 切换布局
const changeLayout = (layoutType) => {
currentLayout.value = layoutType;
videoPositions.value = {};
selectedNodes.value = [];
};
// 处理视频点击
const handleVideoClick = (index) => {
// 重置所有视频的选中状态
const updatedList = [...videoList.value];
updatedList.forEach((video, i) => {
video.isSelected = i === index && video.name !== "待选择区域";
});
// 注意由于videoList是计算属性这里直接修改不会持久化
// 我们可以通过其他方式处理选中状态,或者如果需要持久化选中状态,可以添加一个响应式变量
};
// 移除视频
const removeVideo = (index) => {
if (videoPositions.value[index]) {
const removedDevice = videoPositions.value[index];
delete videoPositions.value[index];
selectedNodes.value = selectedNodes.value.filter(label => label !== removedDevice.label);
ElMessage.info(`已移除${removedDevice.label}`);
}
};
// 监听搜索
watch(searchDevice, () => {
// 搜索功能已在 computed 中处理
});
onMounted(() => {
// 初始化样式
const style = document.createElement("style");
style.textContent = `
.video-item .close-icon {
position: absolute;
top: 5px;
right: 5px;
font-size: 16px;
color: #606266;
cursor: pointer;
opacity: 0;
width: 30px;
height: 30px;
transition: opacity 0.2s ease;
}
.video-item:hover .close-icon {
opacity: 1;
}
.video-header .icon,
.video-placeholder .icon {
color: #409eff;
}
.video-header .icon {
margin-right: 8px;
}
.video-placeholder .icon {
font-size: 36px;
margin-bottom: 8px;
}
`;
document.head.appendChild(style);
});
</script>
<style scoped>
.monitor-page {
height: calc(100vh - 84px);
background-color: #f5f7fa;
padding: 20px;
box-sizing: border-box;
}
.layout-container {
display: flex;
height: 100%;
gap: 20px;
}
.device-tree-panel {
flex: 0 0 280px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.tree-header {
padding: 12px 16px;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
justify-content: space-between;
}
.search-add {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.custom-tree {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.tree-group {
margin-bottom: 16px;
}
.group-label {
font-weight: 600;
color: #303133;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 8px;
}
.tree-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.tree-item:hover {
background-color: #f5f7fa;
}
.tree-item.selected {
background-color: #e6f7ff;
color: #409eff;
}
.tree-item.disabled:not(.selected) {
opacity: 0.5;
cursor: not-allowed;
}
.item-content {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.item-icon {
color: #909399;
font-size: 16px;
}
.tree-item.selected .item-icon {
color: #409eff;
}
.item-label {
font-size: 14px;
}
.selected-indicator {
color: #409eff;
font-size: 14px;
}
.video-wall-panel {
flex: 1;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
.video-header {
padding: 12px 16px;
border-bottom: 1px solid #e4e7ed;
display: flex;
align-items: center;
}
.video-header span {
font-weight: 500;
color: #303133;
}
.video-container {
flex: 1;
display: grid;
gap: 10px;
padding: 16px;
overflow: hidden;
}
.video-item {
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
transition: all 0.2s ease;
min-height: 120px;
}
.item-icon {
width: 18px;
}
.icon {
width: 30px;
}
.video-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.video-placeholder {
display: flex;
flex-direction: column;
align-items: center;
}
.video-placeholder span {
color: #606266;
font-size: 14px;
}
.layout-switch {
padding: 12px 16px;
border-top: 1px solid #e4e7ed;
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>