You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

527 lines
13 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="content">
<div class="bg"></div>
<div class="logo"></div>
<div class="time">
<div class="time1">{{ time.time }}</div>
<div style="display: inline-block">
<div class="time2">{{ time.week }}</div>
<div class="time3">{{ time.date }}</div>
</div>
</div>
<div class="title">RFID中间件监控平台</div>
<div class="topTitle" style="left: 13%">设备数量:</div>
<div class="topTitle" style="left: 37.5%">在线数量:</div>
<div class="topTitle" style="left: 62%">离线数量:</div>
<div class="topTitle" style="left: 86.5%">告警数量:</div>
<div class="topNum" style="left: 17%; color: #127feb">{{ overview.deviceTotal }}</div>
<div class="topNum" style="left: 41.5%; color: #49e1f5">{{ overview.onlineCount }}</div>
<div class="topNum" style="left: 66%; color: #d8ee30">{{ overview.offlineCount }}</div>
<div class="topNum" style="left: 90.5%; color: #901f43">{{ overview.alarmCount }}</div>
<div class="successRate">
<Chart ref="successRateRef"></Chart>
</div>
<div class="menu">
<el-menu active-text-color="#ffd04b" background-color="#545c64" class="el-menu-vertical-demo" default-active="2" text-color="#fff">
<template v-for="(i, index) in treeData" :key="i.id">
<MenuItem :data="i" />
</template>
</el-menu>
</div>
<div class="scrollTable">
<div class="th" :style="{ backgroundImage: `url(${thbg})` }">
<div class="td" style="width: 25%">时间</div>
<div class="td" style="width: 15%">设备名称</div>
<div class="td" style="width: 25%">位置</div>
<!-- <div class="td" style="width: 10%">等级</div>-->
<div class="td" style="width: 35%">告警行为</div>
</div>
<vue3ScrollSeamless
ref="scrollSeamlessRef"
:classOptions="classOptions"
:dataList="tableData"
style="overflow: hidden; height: calc(100% - 3.5vw); margin-top: 0.2vw"
>
<div
v-for="(item, index) in tableData"
:key="index"
class="tr"
style="margin-top: 8px; line-height: 2.4vw"
:style="{ backgroundImage: `url(${trbg})` }"
>
<div class="td" style="width: 25%; line-height: 2.4vw">{{ item.alarmTime }}</div>
<div class="td" style="width: 15%; line-height: 2vw">{{ item.deviceName }}</div>
<div class="td" style="width: 25%; line-height: 2vw; font-size: 0.6vw">{{ item.location || '' }}</div>
<!-- <div class="td" style="width: 10%; line-height: 2vw">{{ item.alarmLevel }}</div>-->
<div class="td" style="width: 35%; line-height: 2vw">{{ item.alarmAction }}</div>
</div>
</vue3ScrollSeamless>
</div>
<div class="center" ref="scrollNodeRef" @mouseenter="hover = true" @mouseleave="mouseleave">
<div v-masonry style="width: 100%; height: 100%" ref="masonryRef">
<TreeItem v-masonry-tile :data="i" v-for="i in centerData" :key="i.id" :wsData="wsData" />
</div>
<div v-masonry style="width: 100%; height: 100%">
<TreeItem v-masonry-tile :data="i" v-for="i in centerData" :key="i.id" :wsData="wsData" />
</div>
</div>
</div>
</template>
<script setup lang="jsx">
import Chart from '@/components/Chart/chart.vue';
import thbg from '@/assets/chart/thbg.png';
import trbg from '@/assets/chart/trbg.png';
import TreeItem from './treeItem.vue';
import { vue3ScrollSeamless } from 'vue3-scroll-seamless';
import { getRealtimeStats, getLocationTree, getSuccessRateTrends } from '@/api/rfid/dashboard';
function findParentsWithLocationType3(arr) {
const result = [];
function traverse(items) {
for (const item of items) {
let hasChildWith3 = false;
if (item.children && Array.isArray(item.children)) {
// 遍历 children看是否有 locationType 为 "3"
for (const child of item.children) {
if (child.locationType === '3') {
hasChildWith3 = true;
}
}
// 递归继续遍历 children
traverse(item.children);
}
if (hasChildWith3) {
result.push(item);
}
}
}
traverse(arr);
return result;
}
const timeFun = () => {
const now = new Date();
const pad = (n) => String(n).padStart(2, '0');
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
const weekMap = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const week = weekMap[now.getDay()];
const date = `${now.getFullYear()}.${pad(now.getMonth() + 1)}.${pad(now.getDate())}`;
return {
time,
week,
date
};
};
const mouseleave = () => {
setTimeout(() => {
hover.value = false;
scrollNodeRef.value.scrollTop -= 1;
}, 500);
};
const overview = ref({});
const classOptions = ref({
limitMoveNum: 1,
step: 1,
hover: true
});
setInterval(() => {
time.value = timeFun();
}, 1000);
const time = ref(timeFun());
const masonryRef = ref(null);
const scrollSeamlessRef = ref(null);
const scrollNodeRef = ref(null);
const successRateRef = ref(null);
const hover = ref(false);
const tableData = ref();
const treeData = ref();
const centerData = ref([]);
const wsData = ref({});
const MenuItem = defineComponent({
name: 'MenuItem',
props: {
data: {
type: Object,
required: true
}
},
setup(props) {
const { data } = props;
return () =>
data.children?.length > 0 ? (
<ElSubMenu
index={`${data.id}`}
v-slots={{
title: () => <span>{data.locationAlias}</span>
}}
>
{data.children.map((item, index) => {
return <MenuItem data={item} />;
})}
</ElSubMenu>
) : (
<el-menu-item index={`${data.id}`}>{data.locationAlias}</el-menu-item>
);
}
});
function scrollToBottom(scrollNode, speed = 1) {
if (!scrollNode) return;
function step() {
const maxScroll = scrollNode.scrollHeight - scrollNode.clientHeight;
if (!hover.value && scrollNode.scrollTop < maxScroll) {
let top = Math.min(scrollNode.scrollTop + speed, maxScroll);
if (top > masonryRef.value.clientHeight) {
top -= masonryRef.value.clientHeight;
}
scrollNode.scrollTop = top;
}
requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
const getData = () => {
getSuccessRateTrends().then((res) => {
successRateRef.value.setData({
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,0,0,.6)',
borderWidth: 0,
textStyle: {
color: '#fff'
},
formatter: function (params) {
let tooltipContent = '';
// 获取横轴坐标
let axisValue = params[0].axisValueLabel; // 获取x轴的标签
// 在每个数据后加上百分号
tooltipContent += `${axisValue}<br/>`;
params.forEach(function (param) {
// 在数值后添加 `%`,并保持圆点样式
tooltipContent += `${param.marker} ${param.seriesName}: ${param.value ? param.value + '%' : '无数据'}<br/>`;
});
return tooltipContent;
}
},
legend: {
top: '',
icon: 'path://M512.798285 0.399142C230.604561 0.399142 1.895927 229.107776 1.895927 511.301501s228.708634 510.902358 510.902358 510.902358 510.902358-228.708634 510.902358-510.902358S794.992009 0.399142 512.798285 0.399142z m0 878.712142C309.634769 879.111284 144.988501 714.465017 144.988501 511.301501S309.634769 143.491717 512.798285 143.491717s367.809784 164.646268 367.809784 367.809784-164.646268 367.809784-367.809784 367.809783z',
itemWidth: 10,
itemHeight: 10,
itemGap: 10,
textStyle: {
color: '#fff',
fontSize: 8
}
},
xAxis: {
type: 'category',
data: res.data.map((item) => item.timePoint)
},
grid: {
top: 30,
left: 30,
right: 30,
bottom: 30
},
yAxis: {
splitLine: {
show: false
},
type: 'value'
},
series: [
{
name: '今日',
data: res.data.map((item) => item.successRate),
type: 'line',
symbol: 'circle',
symbolSize: 5,
itemStyle: {
color: '#368CDC'
},
lineStyle: {
color: '#368CDC'
}
},
{
name: '昨日',
data: res.data.map((item) => item.yesterdaySuccessRate),
type: 'line',
symbol: 'circle',
symbolSize: 5,
itemStyle: {
color: '#39FFF4'
},
lineStyle: {
color: '#39FFF4'
}
}
]
});
});
getRealtimeStats().then((res) => {
overview.value = res.data?.overview || {};
tableData.value = res.data?.alarmStats || [];
});
getLocationTree().then((res) => {
treeData.value = res.data;
centerData.value = findParentsWithLocationType3(res.data);
nextTick(() => {
scrollToBottom(scrollNodeRef.value, 2);
});
});
};
let data = {
'objid': 1993874716645003264,
'deviceId': 1,
'readStatus': '1',
'epcStr': ' SW004',
'alarmFlag': '0',
'alarmLevel': '<27>',
'alarmType': '<27>',
'alarmAction': '',
'recordTime': '2025-11-27T10:49:11.25084+08:00',
'dataType': 2,
'deviceStatus': 1
};
let data1 = {
'objid': 1993880261934321664,
'deviceId': 1,
'readStatus': '0',
'epcStr': '',
'alarmFlag': '1',
'alarmLevel': '<27>',
'alarmType': '<27>',
'alarmAction': '未获取到标签信息',
'recordTime': '2025-11-27T11:11:13.314071+08:00',
'dataType': 2,
'deviceStatus': 1
};
let data2 = {
'objid': 0,
'deviceId': 1,
'readStatus': '<27>',
'epcStr': null,
'alarmFlag': '1',
'alarmLevel': '<27>',
'alarmType': '<27>',
'alarmAction': '链接断开',
'recordTime': '2025-11-27T10:54:18.600795+08:00',
'dataType': 1,
'deviceStatus': 2
};
const getSocket = () => {
const socket = new WebSocket('ws://192.168.0.16:7181/ws');
socket.addEventListener('open', () => {
// console.log('✅ WebSocket 连接成功');
});
socket.addEventListener('message', (event) => {
// console.log(JSON.parse(event.data));
});
socket.addEventListener('close', () => {
setTimeout(() => {
getSocket();
}, 1000);
});
socket.addEventListener('error', (error) => {
// console.error('⚠️ WebSocket 出错:', error);
});
};
onMounted(() => {
getData();
getSocket();
wsData.value[data.deviceId] = data;
});
</script>
<style scoped lang="less">
.content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
font-family: 'Source Han Sans', 'Noto Sans CJK SC', '', sans-serif;
.bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('@/assets/chart/bg.jpg');
background-size: 100% 100%;
background-repeat: no-repeat;
}
.logo {
position: absolute;
top: 5%;
left: 5%;
width: 8vw;
height: 1.91474vw;
background-image: url('@/assets/chart/highwayLogo.png');
background-size: 100% 100%;
background-repeat: no-repeat;
}
.time {
position: absolute;
top: 5%;
right: 5%;
color: #78caf6;
font-family: 'CSD-Block-Bold';
.time1 {
display: inline-block;
white-space: nowrap;
line-height: 2vw;
font-size: 1.6vw;
margin-right: 8px;
vertical-align: top;
font-family: 'CSD-Block-Bold';
font-feature-settings: 'tnum';
}
.time2 {
white-space: nowrap;
line-height: 1vw;
font-size: 0.8vw;
font-family: 'CSD-Block-Bold';
}
.time3 {
white-space: nowrap;
font-size: 0.9vw;
line-height: 1vw;
font-family: 'CSD-Block-Bold';
}
}
.title {
position: absolute;
top: 2vw;
left: 50%;
transform: translate(-50%, -50%);
font-size: 2.1vw;
font-weight: 700;
letter-spacing: 0.2vw;
color: #7ae7f8;
background: linear-gradient(to bottom, #ffffff 0%, #7ae7f8 100%);
background-clip: text;
color: transparent;
}
.topTitle {
position: absolute;
top: 16.7%;
transform: translate(-50%, -50%);
color: #eee;
font-size: 1.4vw;
letter-spacing: 0.2vw;
}
.topNum {
position: absolute;
top: 16.7%;
transform: translateY(-50%);
color: #eee;
font-size: 1.5vw;
letter-spacing: 0.2vw;
}
.successRate {
position: absolute;
top: 30.7%;
left: 75.6%;
width: 20.6%;
height: 28.5%;
}
.menu {
position: absolute;
top: 24%;
left: 2.8%;
width: 14.7%;
height: 69.5%;
overflow: auto;
&::-webkit-scrollbar {
display: none;
}
}
.scrollTable {
position: absolute;
top: 64.7%;
left: 75.6%;
width: 20.6%;
height: 28.5%;
}
.th {
height: 3.3vw;
background-size: 100% 100%;
background-repeat: no-repeat;
font-size: 0.9vw;
}
.tr {
height: 2.4vw;
background-size: 100% 100%;
background-repeat: no-repeat;
font-size: 0.8vw;
}
.td {
display: inline-block;
line-height: 3.3vw;
color: #fff;
text-align: center;
white-space: nowrap;
}
.center {
position: absolute;
top: 24.5%;
left: 19.6%;
width: 53%;
height: 69%;
display: flex;
flex-wrap: wrap;
overflow-y: auto;
transition: all 1s;
&::-webkit-scrollbar {
display: none;
}
}
}
/deep/ .el-menu {
border: none;
}
</style>