483 lines
12 KiB
Vue
483 lines
12 KiB
Vue
<template>
|
||
<div class="main">
|
||
<!-- 渐变边框 -->
|
||
<div class="border-gradient top"></div>
|
||
<div class="border-gradient bottom"></div>
|
||
<div class="border-gradient left"></div>
|
||
<div class="border-gradient right"></div>
|
||
|
||
<!-- 顶部组件 -->
|
||
<TopHeader />
|
||
|
||
<!-- 左侧内容 -->
|
||
<div class="left_content">
|
||
<TitleBlock title="机器人统计">机器人统计</TitleBlock>
|
||
<div class="statistics-content">
|
||
<div class="statistics-grid">
|
||
<StatisticCard
|
||
v-for="item in statistics"
|
||
:key="item.title"
|
||
:iconSrc="item.icon"
|
||
:value="item.value"
|
||
:title="item.title"
|
||
@click="handleStatisticClick(item)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<TitleBlock title="机器人状态列表">机器人状态列表</TitleBlock>
|
||
<div class="status-list-content">
|
||
<RobotStatusList />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧内容 -->
|
||
<div class="right_content">
|
||
<TitleBlock title="最新告警">最新告警</TitleBlock>
|
||
<div class="alert-content">
|
||
<LatestAlarms />
|
||
</div>
|
||
|
||
<TitleBlock title="告警事件统计">告警事件统计</TitleBlock>
|
||
<div class="event-list-content">
|
||
<AlarmStatistics />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部监控区域 -->
|
||
<div class="bottom_content">
|
||
<TitleBlock title="厂区及机器人实时监控">厂区及机器人实时监控</TitleBlock>
|
||
<div class="monitor-content">
|
||
<div class="camera-grid">
|
||
<div v-for="(camera, index) in cameras" :key="index" class="camera-item">
|
||
<div class="camera-header">
|
||
<div class="camera-title">
|
||
<img :src="camera.src" alt="摄像头" class="camera-icon" />
|
||
<p>{{camera.title}}</p>
|
||
</div>
|
||
<CustomSelect
|
||
v-model="selectedViews[camera.title]"
|
||
:options="getViewOptionsForCamera(camera)"
|
||
size="small"
|
||
variant="search"
|
||
font="small"
|
||
/>
|
||
</div>
|
||
<div class="camera-feed">
|
||
<CarouselVideoPlayer
|
||
v-if="!isWebRTCCamera(camera)"
|
||
:views="getViewsForCamera(camera)"
|
||
:title="camera.title"
|
||
:interval="10000"
|
||
v-model="selectedViews[camera.title]"
|
||
/>
|
||
<CarouselWebRTCPlayer
|
||
v-else
|
||
:views="getViewsForCamera(camera)"
|
||
:title="camera.title"
|
||
:interval="10000"
|
||
v-model="selectedViews[camera.title]"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 机器人列表弹窗 -->
|
||
<RobotListModal v-model:visible="showRobotListModal" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { onMounted, ref, computed, watch } from "vue";
|
||
import TopHeader from '../components/common/TopHeader.vue'
|
||
import TitleBlock from '../components/common/TitleBlock.vue'
|
||
import StatisticCard from '../components/StatisticCard.vue'
|
||
import RobotStatusList from '../components/RobotStatusList.vue'
|
||
import AlarmStatistics from '../components/AlarmStatistics.vue'
|
||
import LatestAlarms from '../components/LatestAlarms.vue'
|
||
import RobotListModal from '../components/dialog/RobotListModal.vue'
|
||
import CustomSelect from '../components/common/CustomSelect.vue'
|
||
import CarouselVideoPlayer from '../components/common/CarouselVideoPlayer.vue'
|
||
import CarouselWebRTCPlayer from '../components/common/CarouselWebRTCPlayer.vue'
|
||
import { homeApi } from '../api/index'
|
||
import Icon1 from '../assets/img/icon1.png'
|
||
import Icon2 from '../assets/img/icon2.png'
|
||
import Icon3 from '../assets/img/icon3.png'
|
||
import Icon4 from '../assets/img/icon4.png'
|
||
import jkA from '../assets/img/jkA.png'
|
||
import jkRobot from '../assets/img/jkRobot.png'
|
||
import VideoPlayer from '../components/common/VideoPlayer.vue'
|
||
import EmptyState from '../components/common/EmptyState.vue'
|
||
import CustomWebRTCPlayer from '../components/common/CustomWebRTCPlayer.vue'
|
||
import empty from '../assets/img/empty.png'
|
||
|
||
// 统计数据
|
||
const statistics = ref([
|
||
{ icon: Icon1, value: 0, title: '总数量' },
|
||
{ icon: Icon2, value: 0, title: '在线数量' },
|
||
{ icon: Icon3, value: 0, title: '离线数量' },
|
||
{ icon: Icon4, value: 0, title: '故障数量' }
|
||
]);
|
||
|
||
// 获取机器人统计数据
|
||
const fetchRobotStatistics = async () => {
|
||
try {
|
||
// 获取机器人列表
|
||
const res = await homeApi.getRobotList({
|
||
tenantInfoId: '4fff5d4bcc4b4239941ff077a0da8958', // 租户id
|
||
number: null, // 机器人名
|
||
status: null, // 是否故障
|
||
onlineStatus: null // 在线状态
|
||
});
|
||
|
||
if (res.code === 200) {
|
||
// 计算统计数据
|
||
let totalCount = 0;
|
||
let onlineCount = 0;
|
||
let offlineCount = 0;
|
||
let faultCount = 0;
|
||
|
||
// 遍历分组数据
|
||
Object.entries(res.data).forEach(([groupName, robots]) => {
|
||
robots.forEach(robot => {
|
||
totalCount++;
|
||
|
||
if (robot.onlineStatus === '0') {
|
||
offlineCount++;
|
||
} else {
|
||
onlineCount++;
|
||
|
||
// 检查故障状态
|
||
if (robot.status === '3') {
|
||
faultCount++;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// 更新统计数据
|
||
statistics.value = [
|
||
{ icon: Icon1, value: totalCount, title: '总数量' },
|
||
{ icon: Icon2, value: onlineCount, title: '在线数量' },
|
||
{ icon: Icon3, value: offlineCount, title: '离线数量' },
|
||
{ icon: Icon4, value: faultCount, title: '故障数量' }
|
||
];
|
||
|
||
console.log('获取机器人统计数据成功:', statistics.value);
|
||
} else {
|
||
console.error('获取机器人统计数据失败:', res);
|
||
}
|
||
} catch (err) {
|
||
console.error('获取机器人统计数据错误:', err);
|
||
}
|
||
};
|
||
|
||
// 摄像头数据
|
||
const cameras = ref([
|
||
{ title: 'A区厂区监控', src: jkA },
|
||
{ title: 'B区厂区监控', src: jkA },
|
||
{ title: '追随机器人监控', src: jkRobot },
|
||
{ title: '室外机器人监控', src: jkRobot }
|
||
]);
|
||
|
||
// 选中的视角
|
||
const selectedViews = ref({
|
||
'A区厂区监控': '',
|
||
'B区厂区监控': '',
|
||
'追随机器人监控': '',
|
||
'室外机器人监控': ''
|
||
});
|
||
|
||
// 获取特定摄像头的视角选项
|
||
const getViewOptionsForCamera = (camera) => {
|
||
const cameraData = monitorStreams.value[camera.title];
|
||
if (!cameraData) return [];
|
||
|
||
// 返回该摄像头所有可用的视角名称,无论是否有URL
|
||
return Object.keys(cameraData);
|
||
};
|
||
|
||
// 存储所有视频流数据
|
||
const monitorStreams = ref({});
|
||
|
||
// 获取监控视频流数据
|
||
const fetchMonitorStreams = async () => {
|
||
try {
|
||
const res = await homeApi.getFactoryRobotRealTime();
|
||
|
||
if (res.code === 200) {
|
||
monitorStreams.value = res.data || {};
|
||
console.log('获取监控视频流成功:', JSON.stringify(monitorStreams.value));
|
||
|
||
// 初始化每个摄像头的默认视角
|
||
cameras.value.forEach(camera => {
|
||
const viewOptions = getViewOptionsForCamera(camera);
|
||
if (viewOptions.length > 0) {
|
||
// 设置为第一个可用视角
|
||
selectedViews.value[camera.title] = viewOptions[0];
|
||
}
|
||
});
|
||
} else {
|
||
console.error('获取监控视频流失败:', res);
|
||
}
|
||
} catch (err) {
|
||
console.error('获取监控视频流错误:', err);
|
||
}
|
||
};
|
||
|
||
// 判断流类型是否为WebRTC
|
||
const isWebRTCStream = (url) => {
|
||
if (!url) return false;
|
||
return url.startsWith('webrtc://') || url.includes('31011500991180041301') || url.includes('34020000001320000');
|
||
};
|
||
|
||
// 为摄像头获取视角列表
|
||
const getViewsForCamera = (camera) => {
|
||
const cameraData = monitorStreams.value[camera.title];
|
||
if (!cameraData) return [];
|
||
|
||
// 获取所有视角并转换为视图对象数组
|
||
return Object.entries(cameraData).map(([viewName, streamUrl]) => {
|
||
return {
|
||
name: viewName,
|
||
streamUrl: streamUrl || '' // 保留空字符串URL,不替换为"empty"标识符
|
||
};
|
||
}); // 不过滤掉没有流URL的视角,保留所有视角
|
||
};
|
||
|
||
// 判断摄像头是否使用WebRTC
|
||
const isWebRTCCamera = (camera) => {
|
||
const views = getViewsForCamera(camera);
|
||
if (views.length === 0) return false;
|
||
|
||
// 检查任意视角是否为WebRTC流
|
||
return views.some(view => isWebRTCStream(view.streamUrl));
|
||
};
|
||
|
||
const showRobotListModal = ref(false);
|
||
|
||
const handleStatisticClick = (item) => {
|
||
if (item.title === '总数量') {
|
||
showRobotListModal.value = true;
|
||
}
|
||
};
|
||
|
||
// 组件挂载时获取数据
|
||
onMounted(() => {
|
||
fetchRobotStatistics();
|
||
fetchMonitorStreams();
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.main {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
background: url("../assets/img/bg.png") no-repeat;
|
||
background-size: 100% 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.left_content,
|
||
.right_content {
|
||
position: absolute;
|
||
top: 6.25rem;
|
||
width: 400px;
|
||
/* height: 85%; */
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
|
||
z-index: 1000;
|
||
}
|
||
|
||
.left_content {
|
||
left: 32px;
|
||
}
|
||
|
||
.right_content {
|
||
right: 32px;
|
||
}
|
||
|
||
.border-gradient {
|
||
position: absolute;
|
||
pointer-events: none;
|
||
z-index: 200;
|
||
}
|
||
|
||
.border-gradient.top {
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 200px;
|
||
background: linear-gradient(to bottom, rgba(0, 21, 31, 0.8), rgba(0, 21, 31, 0));
|
||
}
|
||
|
||
.border-gradient.bottom {
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
height: 300px;
|
||
background: linear-gradient(to top, rgba(0, 21, 31, 0.8), rgba(0, 21, 31, 0));
|
||
}
|
||
|
||
.border-gradient.left {
|
||
top: 0;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 35rem;
|
||
background: linear-gradient(to right, rgba(0, 21, 31, 0.8), rgba(0, 21, 31, 0));
|
||
}
|
||
|
||
.border-gradient.right {
|
||
top: 0;
|
||
bottom: 0;
|
||
right: 0;
|
||
width: 35rem;
|
||
background: linear-gradient(to left, rgba(0, 21, 31, 0.8), rgba(0, 21, 31, 0));
|
||
}
|
||
|
||
.statistics-content,
|
||
.status-list-content,
|
||
.alert-content {
|
||
width: 100%;
|
||
/* padding: 15px;
|
||
border: 1px solid red; */
|
||
/* flex: 1; */
|
||
}
|
||
|
||
.statistics-content {
|
||
width: 100%;
|
||
/* padding: 15px;
|
||
background: rgba(0, 21, 31, 0.5);
|
||
border-radius: 8px; */
|
||
}
|
||
|
||
.statistics-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 20px;
|
||
padding: 10px;
|
||
}
|
||
|
||
.status-list-content {
|
||
width: 100%;
|
||
height: 400px;
|
||
/* background: rgba(0, 21, 31, 0.5); */
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.alert-content {
|
||
width: 100%;
|
||
height: 400px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.event-list-content {
|
||
width: 100%;
|
||
height: 20rem;
|
||
}
|
||
|
||
.bottom_content {
|
||
position: absolute;
|
||
bottom: 32px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: calc(100% - 900px);
|
||
z-index: 2;
|
||
}
|
||
|
||
.monitor-title {
|
||
position: relative;
|
||
height: 40px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding-left: 20px;
|
||
}
|
||
|
||
.title-bg {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.monitor-title span {
|
||
position: relative;
|
||
z-index: 1;
|
||
color: #B9E8FF;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.monitor-content {
|
||
margin-top: 10px;
|
||
/* background: rgba(0, 21, 31, 0.3); */
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
}
|
||
|
||
.camera-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 10px;
|
||
}
|
||
|
||
.camera-item {
|
||
background: rgba(0, 21, 31, 0.5);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
/* border: 1px solid rgba(0, 168, 255, 0.2); */
|
||
}
|
||
|
||
.camera-header {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 6px 10px;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
color: #B9E8FF;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.camera-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.camera-title img {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
.camera-title p {
|
||
margin: 0;
|
||
font-size: 13px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.camera-feed {
|
||
height: 180px;
|
||
overflow: hidden;
|
||
/* background: rgba(0, 21, 31, 0.5); */
|
||
background: #033347;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.camera-feed img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
</style>
|