robot_bigScreen/src/views/Home.vue
2025-06-09 12:05:24 +08:00

483 lines
12 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="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>